From 3dc5ca4e11abc4b62e5c0169636fce7013e41d55 Mon Sep 17 00:00:00 2001 From: Vaxry Date: Thu, 7 Mar 2024 16:21:04 +0000 Subject: [PATCH] all: add support for animated cursors --- README.md | 2 +- hyprcursor-util/src/main.cpp | 50 +++++++++++------ libhyprcursor/hyprcursor.cpp | 79 ++++++++++++++++----------- libhyprcursor/hyprcursor_c.cpp | 6 +- libhyprcursor/internalDefines.hpp | 2 +- libhyprcursor/internalSharedTypes.hpp | 5 +- tests/test.cpp | 8 ++- 7 files changed, 94 insertions(+), 58 deletions(-) diff --git a/README.md b/README.md index 54853b0..42cb4dc 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ It provides C and C++ bindings. ## TODO Library: - - [ ] Support animated cursors + - [x] Support animated cursors - [ ] Support SVG cursors Util: diff --git a/hyprcursor-util/src/main.cpp b/hyprcursor-util/src/main.cpp index cdc2b6d..962b6cb 100644 --- a/hyprcursor-util/src/main.cpp +++ b/hyprcursor-util/src/main.cpp @@ -47,10 +47,26 @@ static Hyprlang::CParseResult parseDefineSize(const char* C, const char* V) { return result; } - const auto LHS = removeBeginEndSpacesTabs(VALUE.substr(0, VALUE.find_first_of(","))); - const auto RHS = removeBeginEndSpacesTabs(VALUE.substr(VALUE.find_first_of(",") + 1)); + auto LHS = removeBeginEndSpacesTabs(VALUE.substr(0, VALUE.find_first_of(","))); + auto RHS = removeBeginEndSpacesTabs(VALUE.substr(VALUE.find_first_of(",") + 1)); + auto DELAY = 0; SCursorImage image; + + if (RHS.contains(",")) { + const auto LL = removeBeginEndSpacesTabs(RHS.substr(0, RHS.find(","))); + const auto RR = removeBeginEndSpacesTabs(RHS.substr(RHS.find(",") + 1)); + + try { + image.delay = std::stoull(RR); + } catch (std::exception& e) { + result.setError(e.what()); + return result; + } + + RHS = LL; + } + image.filename = RHS; try { @@ -60,7 +76,7 @@ static Hyprlang::CParseResult parseDefineSize(const char* C, const char* V) { return result; } - currentTheme->shapes.back().images.push_back(image); + currentTheme->shapes.back()->images.push_back(image); return result; } @@ -69,7 +85,7 @@ static Hyprlang::CParseResult parseOverride(const char* C, const char* V) { Hyprlang::CParseResult result; const std::string VALUE = V; - currentTheme->shapes.back().overrides.push_back(V); + currentTheme->shapes.back()->overrides.push_back(V); return result; } @@ -107,7 +123,7 @@ static std::optional createCursorThemeFromPath(const std::string& p for (auto& dir : std::filesystem::directory_iterator(CURSORDIR)) { const auto METAPATH = dir.path().string() + "/meta.hl"; - auto& SHAPE = currentTheme->shapes.emplace_back(); + auto& SHAPE = currentTheme->shapes.emplace_back(std::make_unique()); // std::unique_ptr meta; @@ -124,21 +140,21 @@ static std::optional createCursorThemeFromPath(const std::string& p } catch (const char* err) { return "failed parsing meta (" + METAPATH + "): " + std::string{err}; } // check if we have at least one image. - for (auto& i : SHAPE.images) { + for (auto& i : SHAPE->images) { if (!std::filesystem::exists(dir.path().string() + "/" + i.filename)) return "meta invalid: image " + i.filename + " does not exist"; break; } - if (SHAPE.images.empty()) + if (SHAPE->images.empty()) return "meta invalid: no images for shape " + dir.path().stem().string(); - SHAPE.directory = dir.path().stem().string(); - SHAPE.hotspotX = std::any_cast(meta->getConfigValue("hotspot_x")); - SHAPE.hotspotY = std::any_cast(meta->getConfigValue("hotspot_y")); - SHAPE.resizeAlgo = stringToAlgo(std::any_cast(meta->getConfigValue("resize_algorithm"))); + SHAPE->directory = dir.path().stem().string(); + SHAPE->hotspotX = std::any_cast(meta->getConfigValue("hotspot_x")); + SHAPE->hotspotY = std::any_cast(meta->getConfigValue("hotspot_y")); + SHAPE->resizeAlgo = stringToAlgo(std::any_cast(meta->getConfigValue("resize_algorithm"))); - std::cout << "Shape " << SHAPE.directory << ": \n\toverrides: " << SHAPE.overrides.size() << "\n\tsizes: " << SHAPE.images.size() << "\n"; + std::cout << "Shape " << SHAPE->directory << ": \n\toverrides: " << SHAPE->overrides.size() << "\n\tsizes: " << SHAPE->images.size() << "\n"; } // create output fs structure @@ -158,8 +174,8 @@ static std::optional createCursorThemeFromPath(const std::string& p // create zips (.hlc) for each for (auto& shape : currentTheme->shapes) { - const auto CURRENTCURSORSDIR = path + "/" + CURSORSSUBDIR + "/" + shape.directory; - const auto OUTPUTFILE = out + "/" + CURSORSSUBDIR + "/" + shape.directory + ".hlc"; + const auto CURRENTCURSORSDIR = path + "/" + CURSORSSUBDIR + "/" + shape->directory; + const auto OUTPUTFILE = out + "/" + CURSORSSUBDIR + "/" + shape->directory + ".hlc"; int errp = 0; zip_t* zip = zip_open(OUTPUTFILE.c_str(), ZIP_CREATE | ZIP_EXCL, &errp); @@ -179,14 +195,14 @@ static std::optional createCursorThemeFromPath(const std::string& p meta = nullptr; // add each cursor png - for (auto& i : shape.images) { + for (auto& i : shape->images) { zip_source_t* image = zip_source_file(zip, (CURRENTCURSORSDIR + "/" + i.filename).c_str(), 0, 0); if (!image) return "(1) failed to add image " + (CURRENTCURSORSDIR + "/" + i.filename) + " to hlc"; if (zip_file_add(zip, (i.filename).c_str(), image, ZIP_FL_ENC_UTF_8) < 0) return "(2) failed to add image " + i.filename + " to hlc"; - std::cout << "Added image " << i.filename << " to shape " << shape.directory << "\n"; + std::cout << "Added image " << i.filename << " to shape " << shape->directory << "\n"; } // close zip and write @@ -326,7 +342,7 @@ static std::optional extractXTheme(const std::string& xpath, const for (auto& entry : entries) { const auto ENTRYSTEM = entry.image.substr(entry.image.find_last_of('/') + 1); - metaString += std::format("define_size = {}, {}\n", entry.size, ENTRYSTEM); + metaString += std::format("define_size = {}, {}, {}\n", entry.size, ENTRYSTEM, entry.delay); } metaString += "\n"; diff --git a/libhyprcursor/hyprcursor.cpp b/libhyprcursor/hyprcursor.cpp index cdc4495..45c407b 100644 --- a/libhyprcursor/hyprcursor.cpp +++ b/libhyprcursor/hyprcursor.cpp @@ -168,12 +168,12 @@ SCursorImageData** CHyprcursorManager::getShapesC(int& outSize, const char* shap std::vector resultingImages; for (auto& shape : impl->theme.shapes) { - if (REQUESTEDSHAPE != shape.directory && std::find(shape.overrides.begin(), shape.overrides.end(), REQUESTEDSHAPE) == shape.overrides.end()) + if (REQUESTEDSHAPE != shape->directory && std::find(shape->overrides.begin(), shape->overrides.end(), REQUESTEDSHAPE) == shape->overrides.end()) continue; // matched :) bool foundAny = false; - for (auto& image : impl->loadedShapes[&shape].images) { + for (auto& image : impl->loadedShapes[shape.get()].images) { if (image->side != info.size) continue; @@ -186,14 +186,14 @@ SCursorImageData** CHyprcursorManager::getShapesC(int& outSize, const char* shap break; // if we get here, means loadThemeStyle wasn't called most likely. If resize algo is specified, this is an error. - if (shape.resizeAlgo != RESIZE_NONE) { + if (shape->resizeAlgo != RESIZE_NONE) { Debug::log(ERR, "getSurfaceFor didn't match a size?"); return nullptr; } // find nearest int leader = 13371337; - for (auto& image : impl->loadedShapes[&shape].images) { + for (auto& image : impl->loadedShapes[shape.get()].images) { if (std::abs((int)(image->side - info.size)) > leader) continue; @@ -206,7 +206,7 @@ SCursorImageData** CHyprcursorManager::getShapesC(int& outSize, const char* shap } // we found nearest size - for (auto& image : impl->loadedShapes[&shape].images) { + for (auto& image : impl->loadedShapes[shape.get()].images) { if (image->side != leader) continue; @@ -225,9 +225,9 @@ SCursorImageData** CHyprcursorManager::getShapesC(int& outSize, const char* shap // alloc and return what we need SCursorImageData** data = (SCursorImageData**)malloc(sizeof(SCursorImageData*) * resultingImages.size()); for (size_t i = 0; i < resultingImages.size(); ++i) { - data[i] = (SCursorImageData*)malloc(sizeof(SCursorImageData)); - data[i]->delay = resultingImages[i]->delay; - data[i]->size = resultingImages[i]->side; + data[i] = (SCursorImageData*)malloc(sizeof(SCursorImageData)); + data[i]->delay = resultingImages[i]->delay; + data[i]->size = resultingImages[i]->side; data[i]->surface = resultingImages[i]->cairoSurface; } @@ -238,12 +238,12 @@ SCursorImageData** CHyprcursorManager::getShapesC(int& outSize, const char* shap bool CHyprcursorManager::loadThemeStyle(const SCursorStyleInfo& info) { for (auto& shape : impl->theme.shapes) { - if (shape.resizeAlgo == RESIZE_NONE) + if (shape->resizeAlgo == RESIZE_NONE) continue; // don't resample NONE style cursors bool sizeFound = false; - for (auto& image : impl->loadedShapes[&shape].images) { + for (auto& image : impl->loadedShapes[shape.get()].images) { if (image->side != info.size) continue; @@ -257,7 +257,7 @@ bool CHyprcursorManager::loadThemeStyle(const SCursorStyleInfo& info) { // size wasn't found, let's resample. SLoadedCursorImage* leader = nullptr; int leaderVal = 1000000; - for (auto& image : impl->loadedShapes[&shape].images) { + for (auto& image : impl->loadedShapes[shape.get()].images) { if (image->side < info.size) continue; @@ -269,10 +269,7 @@ bool CHyprcursorManager::loadThemeStyle(const SCursorStyleInfo& info) { } if (!leader) { - for (auto& image : impl->loadedShapes[&shape].images) { - if (image->side < info.size) - continue; - + for (auto& image : impl->loadedShapes[shape.get()].images) { if (std::abs((int)(image->side - info.size)) > leaderVal) continue; @@ -286,7 +283,7 @@ bool CHyprcursorManager::loadThemeStyle(const SCursorStyleInfo& info) { return false; } - auto& newImage = impl->loadedShapes[&shape].images.emplace_back(std::make_unique()); + auto& newImage = impl->loadedShapes[shape.get()].images.emplace_back(std::make_unique()); newImage->artificial = true; newImage->side = info.size; newImage->artificialData = new char[info.size * info.size * 4]; @@ -294,7 +291,7 @@ bool CHyprcursorManager::loadThemeStyle(const SCursorStyleInfo& info) { const auto PCAIRO = cairo_create(newImage->cairoSurface); - cairo_set_antialias(PCAIRO, shape.resizeAlgo == RESIZE_BILINEAR ? CAIRO_ANTIALIAS_GOOD : CAIRO_ANTIALIAS_NONE); + cairo_set_antialias(PCAIRO, shape->resizeAlgo == RESIZE_BILINEAR ? CAIRO_ANTIALIAS_GOOD : CAIRO_ANTIALIAS_NONE); cairo_save(PCAIRO); cairo_set_operator(PCAIRO, CAIRO_OPERATOR_CLEAR); @@ -305,7 +302,7 @@ bool CHyprcursorManager::loadThemeStyle(const SCursorStyleInfo& info) { cairo_pattern_set_extend(PTN, CAIRO_EXTEND_NONE); const float scale = info.size / (float)leader->side; cairo_scale(PCAIRO, scale, scale); - cairo_pattern_set_filter(PTN, shape.resizeAlgo == RESIZE_BILINEAR ? CAIRO_FILTER_GOOD : CAIRO_FILTER_NEAREST); + cairo_pattern_set_filter(PTN, shape->resizeAlgo == RESIZE_BILINEAR ? CAIRO_FILTER_GOOD : CAIRO_FILTER_NEAREST); cairo_set_source(PCAIRO, PTN); cairo_rectangle(PCAIRO, 0, 0, info.size, info.size); @@ -322,10 +319,10 @@ bool CHyprcursorManager::loadThemeStyle(const SCursorStyleInfo& info) { void CHyprcursorManager::cursorSurfaceStyleDone(const SCursorStyleInfo& info) { for (auto& shape : impl->theme.shapes) { - if (shape.resizeAlgo == RESIZE_NONE) + if (shape->resizeAlgo == RESIZE_NONE) continue; - std::erase_if(impl->loadedShapes[&shape].images, [info](const auto& e) { return !e->artificial && (info.size == 0 || e->side == info.size); }); + std::erase_if(impl->loadedShapes[shape.get()].images, [info](const auto& e) { return !e->artificial && (info.size == 0 || e->side == info.size); }); } } @@ -365,10 +362,26 @@ static Hyprlang::CParseResult parseDefineSize(const char* C, const char* V) { return result; } - const auto LHS = removeBeginEndSpacesTabs(VALUE.substr(0, VALUE.find_first_of(","))); - const auto RHS = removeBeginEndSpacesTabs(VALUE.substr(VALUE.find_first_of(",") + 1)); + auto LHS = removeBeginEndSpacesTabs(VALUE.substr(0, VALUE.find_first_of(","))); + auto RHS = removeBeginEndSpacesTabs(VALUE.substr(VALUE.find_first_of(",") + 1)); + auto DELAY = 0; SCursorImage image; + + if (RHS.contains(",")) { + const auto LL = removeBeginEndSpacesTabs(RHS.substr(0, RHS.find(","))); + const auto RR = removeBeginEndSpacesTabs(RHS.substr(RHS.find(",") + 1)); + + try { + image.delay = std::stoull(RR); + } catch (std::exception& e) { + result.setError(e.what()); + return result; + } + + RHS = LL; + } + image.filename = RHS; try { @@ -378,7 +391,7 @@ static Hyprlang::CParseResult parseDefineSize(const char* C, const char* V) { return result; } - currentTheme->shapes.back().images.push_back(image); + currentTheme->shapes.back()->images.push_back(image); return result; } @@ -387,7 +400,7 @@ static Hyprlang::CParseResult parseOverride(const char* C, const char* V) { Hyprlang::CParseResult result; const std::string VALUE = V; - currentTheme->shapes.back().overrides.push_back(V); + currentTheme->shapes.back()->overrides.push_back(V); return result; } @@ -451,8 +464,8 @@ std::optional CHyprcursorImplementation::loadTheme() { if (!cursor.is_regular_file()) continue; - auto& SHAPE = theme.shapes.emplace_back(); - auto& LOADEDSHAPE = loadedShapes[&SHAPE]; + auto& SHAPE = theme.shapes.emplace_back(std::make_unique()); + auto& LOADEDSHAPE = loadedShapes[SHAPE.get()]; // extract zip to raw data. int errp = 0; @@ -490,7 +503,7 @@ std::optional CHyprcursorImplementation::loadTheme() { delete[] buffer; - for (auto& i : SHAPE.images) { + for (auto& i : SHAPE->images) { // load image Debug::log(LOG, "Loading {} for shape {}", i.filename, cursor.path().stem().string()); auto* IMAGE = LOADEDSHAPE.images.emplace_back(std::make_unique()).get(); @@ -511,6 +524,8 @@ std::optional CHyprcursorImplementation::loadTheme() { IMAGE->cairoSurface = cairo_image_surface_create_from_png_stream(::readPNG, IMAGE); + IMAGE->delay = i.delay; + if (const auto STATUS = cairo_surface_status(IMAGE->cairoSurface); STATUS != CAIRO_STATUS_SUCCESS) { delete[] (char*)IMAGE->data; IMAGE->data = nullptr; @@ -518,13 +533,13 @@ std::optional CHyprcursorImplementation::loadTheme() { } } - if (SHAPE.images.empty()) + if (SHAPE->images.empty()) return "meta invalid: no images for shape " + cursor.path().stem().string(); - SHAPE.directory = cursor.path().stem().string(); - SHAPE.hotspotX = std::any_cast(meta->getConfigValue("hotspot_x")); - SHAPE.hotspotY = std::any_cast(meta->getConfigValue("hotspot_y")); - SHAPE.resizeAlgo = stringToAlgo(std::any_cast(meta->getConfigValue("resize_algorithm"))); + SHAPE->directory = cursor.path().stem().string(); + SHAPE->hotspotX = std::any_cast(meta->getConfigValue("hotspot_x")); + SHAPE->hotspotY = std::any_cast(meta->getConfigValue("hotspot_y")); + SHAPE->resizeAlgo = stringToAlgo(std::any_cast(meta->getConfigValue("resize_algorithm"))); zip_discard(zip); } diff --git a/libhyprcursor/hyprcursor_c.cpp b/libhyprcursor/hyprcursor_c.cpp index 1831362..e554206 100644 --- a/libhyprcursor/hyprcursor_c.cpp +++ b/libhyprcursor/hyprcursor_c.cpp @@ -26,10 +26,10 @@ int hyprcursor_load_theme_style(hyprcursor_manager_t* manager, hyprcursor_cursor struct SCursorImageData** hyprcursor_get_cursor_image_data(struct hyprcursor_manager_t* manager, const char* shape, struct hyprcursor_cursor_style_info info_, int* out_size) { const auto MGR = (CHyprcursorManager*)manager; SCursorStyleInfo info; - info.size = info_.size; - int size = 0; + info.size = info_.size; + int size = 0; struct SCursorImageData** data = MGR->getShapesC(size, shape, info); - *out_size = size; + *out_size = size; return data; } diff --git a/libhyprcursor/internalDefines.hpp b/libhyprcursor/internalDefines.hpp index d948dbb..0ef18a0 100644 --- a/libhyprcursor/internalDefines.hpp +++ b/libhyprcursor/internalDefines.hpp @@ -23,7 +23,7 @@ struct SLoadedCursorImage { cairo_surface_t* cairoSurface = nullptr; int side = 0; - int delay = 0; + int delay = 0; // means this was created by resampling void* artificialData = nullptr; diff --git a/libhyprcursor/internalSharedTypes.hpp b/libhyprcursor/internalSharedTypes.hpp index b464ad0..a1e8f12 100644 --- a/libhyprcursor/internalSharedTypes.hpp +++ b/libhyprcursor/internalSharedTypes.hpp @@ -1,6 +1,7 @@ #pragma once #include #include +#include enum eResizeAlgo { RESIZE_NONE = 0, @@ -18,7 +19,7 @@ inline eResizeAlgo stringToAlgo(const std::string& s) { struct SCursorImage { std::string filename; - int size = 0; + int size = 0; int delay = 0; }; @@ -31,5 +32,5 @@ struct SCursorShape { }; struct SCursorTheme { - std::vector shapes; + std::vector> shapes; }; \ No newline at end of file diff --git a/tests/test.cpp b/tests/test.cpp index e774ca3..13e60bf 100644 --- a/tests/test.cpp +++ b/tests/test.cpp @@ -11,10 +11,14 @@ int main(int argc, char** argv) { } // get cursor for left_ptr - const auto SHAPEDATA = mgr.getShape("left_ptr", Hyprcursor::SCursorStyleInfo{.size = 48}); + const auto SHAPEDATA = mgr.getShape("wait", Hyprcursor::SCursorStyleInfo{.size = 48}); - if (SHAPEDATA.images.empty()) + if (SHAPEDATA.images.empty()) { + std::cout << "no images\n"; return 1; + } + + std::cout << "hyprcursor returned " << SHAPEDATA.images.size() << " images\n"; // save to disk const auto RET = cairo_surface_write_to_png(SHAPEDATA.images[0].surface, "/tmp/arrow.png");