From 26ca4d4caade718a00e2f72d4981abe1e97ca10d Mon Sep 17 00:00:00 2001 From: "alan (NyxTrail)" Date: Sun, 2 Mar 2025 08:05:46 +0000 Subject: [PATCH] New upstream version 0.1.11 --- .github/workflows/ci.yaml | 4 + .github/workflows/test.yml | 39 ++++++++ .gitignore | 1 + CMakeLists.txt | 110 +++++++++++++------- README.md | 10 +- VERSION | 1 + docs/MAKING_THEMES.md | 5 +- flake.lock | 38 +++++-- flake.nix | 2 +- hyprcursor-util/src/main.cpp | 1 + include/hyprcursor/shared.h | 1 + libhyprcursor/VarList.cpp | 3 +- libhyprcursor/hyprcursor.cpp | 139 ++++++++++++++++++-------- libhyprcursor/internalSharedTypes.hpp | 2 +- libhyprcursor/meta.cpp | 90 +++++++++-------- libhyprcursor/meta.hpp | 2 +- nix/default.nix | 7 +- nix/dirs.patch | 13 --- nix/overlays.nix | 9 +- tests/c_test.c | 13 ++- 20 files changed, 337 insertions(+), 153 deletions(-) create mode 100644 .github/workflows/test.yml create mode 100644 VERSION delete mode 100644 nix/dirs.patch diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 52b6153..29f04b8 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -18,6 +18,10 @@ jobs: pacman --noconfirm --noprogressbar -Syyu pacman --noconfirm --noprogressbar -Sy gcc base-devel cmake clang cairo librsvg git libzip tomlplusplus + - name: Get hyprutils-git + run: | + git clone https://github.com/hyprwm/hyprutils && cd hyprutils && cmake -DCMAKE_BUILD_TYPE:STRING=Release -DCMAKE_INSTALL_PREFIX:PATH=/usr -B build && cmake --build build --target hyprutils && cmake --install build + - name: Install hyprlang run: | git clone https://github.com/hyprwm/hyprlang --recursive diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..b7b2b85 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,39 @@ +name: Test + +on: [push, pull_request, workflow_dispatch] +jobs: + nix: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: DeterminateSystems/nix-installer-action@main + - uses: DeterminateSystems/magic-nix-cache-action@main + + # not needed (yet) + # - uses: cachix/cachix-action@v12 + # with: + # name: hyprland + # authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}' + + - name: Build + run: nix build .#hyprcursor-with-tests --print-build-logs --keep-going + + # keep a fixed rev in case anything changes + - name: Install hyprcursor theme + run: nix build github:fufexan/dotfiles/4e05e373c1c70a2ae259b2c15eec2ad6e11ce581#bibata-hyprcursor --print-build-logs --keep-going + + - name: Set up env + run: | + export HYPRCURSOR_THEME=Bibata-Modern-Classic-Hyprcursor + export HYPRCURSOR_SIZE=16 + mkdir -p $HOME/.local/share/icons + ln -s $(realpath result/share/icons/Bibata-Modern-Classic-Hyprcursor) $HOME/.local/share/icons/ + + - name: Run test1 + run: nix shell .#hyprcursor-with-tests -c hyprcursor_test1 + - name: Run test2 + run: nix shell .#hyprcursor-with-tests -c hyprcursor_test2 + - name: Run test_c + run: nix shell .#hyprcursor-with-tests -c hyprcursor_test_c + diff --git a/.gitignore b/.gitignore index e524d79..e679f58 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .vscode/ build/ +.cache/ diff --git a/CMakeLists.txt b/CMakeLists.txt index 8917490..3438cf9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,12 +1,14 @@ cmake_minimum_required(VERSION 3.19) -set(HYPRCURSOR_VERSION "0.1.9") +file(READ "${CMAKE_SOURCE_DIR}/VERSION" VER_RAW) +string(STRIP ${VER_RAW} HYPRCURSOR_VERSION) + add_compile_definitions(HYPRCURSOR_VERSION="${HYPRCURSOR_VERSION}") -project(hyprcursor - VERSION ${HYPRCURSOR_VERSION} - DESCRIPTION "A library and toolkit for the Hyprland cursor format" -) +project( + hyprcursor + VERSION ${HYPRCURSOR_VERSION} + DESCRIPTION "A library and toolkit for the Hyprland cursor format") include(CTest) include(GNUInstallDirs) @@ -20,46 +22,68 @@ configure_file(hyprcursor.pc.in hyprcursor.pc @ONLY) set(CMAKE_CXX_STANDARD 23) find_package(PkgConfig REQUIRED) -pkg_check_modules(deps REQUIRED IMPORTED_TARGET hyprlang>=0.4.2 libzip cairo librsvg-2.0 tomlplusplus) +pkg_check_modules( + deps + REQUIRED + IMPORTED_TARGET + hyprlang>=0.4.2 + libzip + cairo + librsvg-2.0 + tomlplusplus) if(CMAKE_BUILD_TYPE MATCHES Debug OR CMAKE_BUILD_TYPE MATCHES DEBUG) - message(STATUS "Configuring hyprcursor in Debug") - add_compile_definitions(HYPRLAND_DEBUG) + message(STATUS "Configuring hyprcursor in Debug") + add_compile_definitions(HYPRLAND_DEBUG) else() - add_compile_options(-O3) - message(STATUS "Configuring hyprcursor in Release") + add_compile_options(-O3) + message(STATUS "Configuring hyprcursor in Release") endif() -file(GLOB_RECURSE SRCFILES CONFIGURE_DEPENDS "libhyprcursor/*.cpp" "include/hyprcursor/hyprcursor.hpp" "include/hyprcursor/hyprcursor.h" "include/hyprcursor/shared.h") +file( + GLOB_RECURSE + SRCFILES + CONFIGURE_DEPENDS + "libhyprcursor/*.cpp" + "include/hyprcursor/hyprcursor.hpp" + "include/hyprcursor/hyprcursor.h" + "include/hyprcursor/shared.h") add_library(hyprcursor SHARED ${SRCFILES}) -target_include_directories( hyprcursor - PUBLIC "./include" - PRIVATE "./libhyprcursor" -) -set_target_properties(hyprcursor PROPERTIES - VERSION ${hyprcursor_VERSION} - SOVERSION 0 - PUBLIC_HEADER include/hyprcursor/hyprcursor.hpp include/hyprcursor/hyprcursor.h include/hyprcursor/shared.h -) +target_include_directories( + hyprcursor + PUBLIC "./include" + PRIVATE "./libhyprcursor") +set_target_properties( + hyprcursor + PROPERTIES VERSION ${hyprcursor_VERSION} + SOVERSION 0 + PUBLIC_HEADER include/hyprcursor/hyprcursor.hpp + include/hyprcursor/hyprcursor.h include/hyprcursor/shared.h) target_link_libraries(hyprcursor PkgConfig::deps) -if (CMAKE_CXX_COMPILER_ID MATCHES "Clang") - # for std::expected. - # probably evil. Arch's clang is very outdated tho... - target_compile_options(hyprcursor PUBLIC - $<$:-std=gnu++2b -D__cpp_concepts=202002L> - -Wno-builtin-macro-redefined) +if(CMAKE_CXX_COMPILER_ID MATCHES "Clang") + # for std::expected. probably evil. Arch's clang is very outdated tho... + target_compile_options( + hyprcursor PUBLIC $<$:-std=gnu++2b + -D__cpp_concepts=202002L> -Wno-builtin-macro-redefined) endif() # hyprcursor-util -file(GLOB_RECURSE UTILSRCFILES CONFIGURE_DEPENDS "hyprcursor-util/src/*.cpp" "include/hyprcursor/hyprcursor.hpp" "include/hyprcursor/hyprcursor.h" "include/hyprcursor/shared.h") +file( + GLOB_RECURSE + UTILSRCFILES + CONFIGURE_DEPENDS + "hyprcursor-util/src/*.cpp" + "include/hyprcursor/hyprcursor.hpp" + "include/hyprcursor/hyprcursor.h" + "include/hyprcursor/shared.h") add_executable(hyprcursor-util ${UTILSRCFILES}) -target_include_directories(hyprcursor-util - PUBLIC "./include" - PRIVATE "./libhyprcursor" "./hyprcursor-util/src" -) +target_include_directories( + hyprcursor-util + PUBLIC "./include" + PRIVATE "./libhyprcursor" "./hyprcursor-util/src") target_link_libraries(hyprcursor-util PkgConfig::deps hyprcursor) # tests @@ -67,21 +91,37 @@ add_custom_target(tests) add_executable(hyprcursor_test1 "tests/full_rendering.cpp") target_link_libraries(hyprcursor_test1 PRIVATE hyprcursor) -add_test(NAME "Test libhyprcursor in C++ (full rendering)" WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/tests COMMAND hyprcursor_test1) +add_test( + NAME "Test libhyprcursor in C++ (full rendering)" + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/tests + COMMAND hyprcursor_test1) add_dependencies(tests hyprcursor_test1) add_executable(hyprcursor_test2 "tests/only_metadata.cpp") target_link_libraries(hyprcursor_test2 PRIVATE hyprcursor) -add_test(NAME "Test libhyprcursor in C++ (only metadata)" WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/tests COMMAND hyprcursor_test2) +add_test( + NAME "Test libhyprcursor in C++ (only metadata)" + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/tests + COMMAND hyprcursor_test2) add_dependencies(tests hyprcursor_test2) add_executable(hyprcursor_test_c "tests/c_test.c") target_link_libraries(hyprcursor_test_c PRIVATE hyprcursor) -add_test(NAME "Test libhyprcursor in C" WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/tests COMMAND hyprcursor_test_c) +add_test( + NAME "Test libhyprcursor in C" + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/tests + COMMAND hyprcursor_test_c) add_dependencies(tests hyprcursor_test_c) # Installation install(TARGETS hyprcursor) install(TARGETS hyprcursor-util) install(DIRECTORY "include/hyprcursor" DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) -install(FILES ${CMAKE_BINARY_DIR}/hyprcursor.pc DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig) +install(FILES ${CMAKE_BINARY_DIR}/hyprcursor.pc + DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig) + +if(INSTALL_TESTS) + install(TARGETS hyprcursor_test1) + install(TARGETS hyprcursor_test2) + install(TARGETS hyprcursor_test_c) +endif() diff --git a/README.md b/README.md index 88a8d57..ca5992e 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,11 @@ doesn't suck as much. - Support for SVG cursors - Way more space-efficient. As an example, Bibata-XCursor is 44.1MB, while it's 6.6MB in hyprcursor. +## Documentation +See the [wiki here](https://wiki.hyprland.org/Hypr-Ecosystem/hyprcursor/) +check out [docs/](./docs) +and [standards](https://standards.hyprland.org/hyprcursor) + ## Tools ### hyprcursor-util @@ -32,10 +37,6 @@ It provides C and C++ bindings. For both C and C++, see `tests/`. -## Docs - -See `docs/`. - ## TODO Library: @@ -53,6 +54,7 @@ Util: - cairo - libzip - librsvg + - tomlplusplus ### Build ```sh diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..20f4951 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +0.1.11 diff --git a/docs/MAKING_THEMES.md b/docs/MAKING_THEMES.md index 56ab550..dd6a1ae 100644 --- a/docs/MAKING_THEMES.md +++ b/docs/MAKING_THEMES.md @@ -49,7 +49,7 @@ Each cursor image is a separate directory. In it, multiple size variations can b resize_algorithm = bilinear # "hotspot" is where in your cursor the actual "click point" should be. -# this is in absolute coordinates. x+ is east, y+ is north. +# this is in absolute coordinates. x+ is east, y+ is south. # the pixel coordinates of the hotspot at size are rounded to the nearest: # (round(size * hotspot_x), round(size * hotspot_y)) hotspot_x = 0.0 # this goes 0 - 1 @@ -71,6 +71,7 @@ define_size = 32, image32.png # define_size = 64, anim2.png, 500 # define_size = 64, anim3.png, 500 # define_size = 64, anim4.png, 500 +# Make sure the timeout is > 0, as otherwise the consumer might ignore your timeouts for being invalid. ``` Supported cursor image types are png and svg. @@ -101,4 +102,4 @@ define_override = 'shape1;shape2;shape3' define_size = '24,image1.png,200;24,image2.png,200;32,image3.png,200' ``` -You can put spaces around the semicolons if you prefer to. \ No newline at end of file +You can put spaces around the semicolons if you prefer to. diff --git a/flake.lock b/flake.lock index 9468df0..52511f1 100644 --- a/flake.lock +++ b/flake.lock @@ -2,6 +2,7 @@ "nodes": { "hyprlang": { "inputs": { + "hyprutils": "hyprutils", "nixpkgs": [ "nixpkgs" ], @@ -10,11 +11,11 @@ ] }, "locked": { - "lastModified": 1713121246, - "narHash": "sha256-502X0Q0fhN6tJK7iEUA8CghONKSatW/Mqj4Wappd++0=", + "lastModified": 1734364628, + "narHash": "sha256-ii8fzJfI953n/EmIxVvq64ZAwhvwuuPHWfGd61/mJG8=", "owner": "hyprwm", "repo": "hyprlang", - "rev": "78fcaa27ae9e1d782faa3ff06c8ea55ddce63706", + "rev": "16e59c1eb13d9fb6de066f54e7555eb5e8a4aba5", "type": "github" }, "original": { @@ -23,13 +24,38 @@ "type": "github" } }, + "hyprutils": { + "inputs": { + "nixpkgs": [ + "hyprlang", + "nixpkgs" + ], + "systems": [ + "hyprlang", + "systems" + ] + }, + "locked": { + "lastModified": 1733502241, + "narHash": "sha256-KAUNC4Dgq8WQjYov5auBw/usaHixhacvb7cRDd0AG/k=", + "owner": "hyprwm", + "repo": "hyprutils", + "rev": "104117aed6dd68561be38b50f218190aa47f2cd8", + "type": "github" + }, + "original": { + "owner": "hyprwm", + "repo": "hyprutils", + "type": "github" + } + }, "nixpkgs": { "locked": { - "lastModified": 1712963716, - "narHash": "sha256-WKm9CvgCldeIVvRz87iOMi8CFVB1apJlkUT4GGvA0iM=", + "lastModified": 1734119587, + "narHash": "sha256-AKU6qqskl0yf2+JdRdD0cfxX4b9x3KKV5RqA6wijmPM=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "cfd6b5fc90b15709b780a5a1619695a88505a176", + "rev": "3566ab7246670a43abd2ffa913cc62dad9cdf7d5", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 535cfee..5e6ebf8 100644 --- a/flake.nix +++ b/flake.nix @@ -30,7 +30,7 @@ packages = eachSystem (system: { default = self.packages.${system}.hyprcursor; - inherit (pkgsFor.${system}) hyprcursor; + inherit (pkgsFor.${system}) hyprcursor hyprcursor-with-tests; }); checks = eachSystem (system: self.packages.${system}); diff --git a/hyprcursor-util/src/main.cpp b/hyprcursor-util/src/main.cpp index b6235cb..97e6573 100644 --- a/hyprcursor-util/src/main.cpp +++ b/hyprcursor-util/src/main.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include #include #include diff --git a/include/hyprcursor/shared.h b/include/hyprcursor/shared.h index c3fb9c6..c36456c 100644 --- a/include/hyprcursor/shared.h +++ b/include/hyprcursor/shared.h @@ -54,6 +54,7 @@ struct SCursorRawShapeDataC { char* overridenBy; enum eHyprcursorResizeAlgo resizeAlgo; enum eHyprcursorDataType type; + float nominalSize; }; typedef struct SCursorRawShapeDataC hyprcursor_cursor_raw_shape_data; diff --git a/libhyprcursor/VarList.cpp b/libhyprcursor/VarList.cpp index 518acde..4a81de0 100644 --- a/libhyprcursor/VarList.cpp +++ b/libhyprcursor/VarList.cpp @@ -28,8 +28,7 @@ CVarList::CVarList(const std::string& in, const size_t lastArgNo, const char del std::string args{in}; size_t idx = 0; size_t pos = 0; - std::ranges::replace_if( - args, [&](const char& c) { return delim == 's' ? std::isspace(c) : c == delim; }, 0); + std::ranges::replace_if(args, [&](const char& c) { return delim == 's' ? std::isspace(c) : c == delim; }, 0); for (const auto& s : args | std::views::split(0)) { if (removeEmpty && s.empty()) diff --git a/libhyprcursor/hyprcursor.cpp b/libhyprcursor/hyprcursor.cpp index b6793ec..42aed3e 100644 --- a/libhyprcursor/hyprcursor.cpp +++ b/libhyprcursor/hyprcursor.cpp @@ -2,6 +2,8 @@ #include "internalSharedTypes.hpp" #include "internalDefines.hpp" #include +#include +#include #include #include #include @@ -15,11 +17,25 @@ using namespace Hyprcursor; -// directories for lookup -constexpr const std::array systemThemeDirs = {"/usr/share/icons"}; +static std::vector getSystemThemeDirs() { + const auto envXdgData = std::getenv("XDG_DATA_DIRS"); + std::vector result; + if (envXdgData) { + std::stringstream envXdgStream(envXdgData); + std::string tmpStr; + while (getline(envXdgStream, tmpStr, ':')) + result.push_back((tmpStr + "/icons")); + } else + result = {"/usr/share/icons"}; + + return result; +} + +const std::vector systemThemeDirs = getSystemThemeDirs(); constexpr const std::array userThemeDirs = {"/.local/share/icons", "/.icons"}; // + static std::string themeNameFromEnv(PHYPRCURSORLOGFUNC logfn) { const auto ENV = getenv("HYPRCURSOR_THEME"); if (!ENV) { @@ -291,13 +307,15 @@ SCursorImageData** CHyprcursorManager::getShapesC(int& outSize, const char* shap if (REQUESTEDSHAPE != shape->directory && std::find(shape->overrides.begin(), shape->overrides.end(), REQUESTEDSHAPE) == shape->overrides.end()) continue; + const int PIXELSIDE = std::round(info.size / shape->nominalSize); + hotX = shape->hotspotX; hotY = shape->hotspotY; // matched :) bool foundAny = false; for (auto& image : impl->loadedShapes[shape.get()].images) { - if (image->side != info.size) + if (image->side != PIXELSIDE) continue; // found size @@ -317,7 +335,7 @@ SCursorImageData** CHyprcursorManager::getShapesC(int& outSize, const char* shap // find nearest int leader = 13371337; for (auto& image : impl->loadedShapes[shape.get()].images) { - if (std::abs((int)(image->side - info.size)) > std::abs((int)(leader - info.size))) + if (std::abs((int)(image->side - PIXELSIDE)) > std::abs((int)(leader - PIXELSIDE))) continue; leader = image->side; @@ -373,14 +391,25 @@ SCursorRawShapeDataC* CHyprcursorManager::getRawShapeDataC(const char* shape_) { SCursorRawShapeDataC* data = new SCursorRawShapeDataC; std::vector resultingImages; + data->overridenBy = nullptr; + data->images = nullptr; + data->len = 0; + data->hotspotX = 0.f; + data->hotspotY = 0.F; + data->nominalSize = 1.F; + data->resizeAlgo = eHyprcursorResizeAlgo::HC_RESIZE_NONE; + data->type = eHyprcursorDataType::HC_DATA_PNG; for (auto& shape : impl->theme.shapes) { // if it's overridden just return the override - if (const auto IT = std::find(shape->overrides.begin(), shape->overrides.end(), SHAPE); IT != shape->overrides.end()) { - data->overridenBy = strdup(IT->c_str()); + if (const auto IT = std::find_if(shape->overrides.begin(), shape->overrides.end(), [&](const auto& e) { return e == SHAPE && SHAPE != shape->directory; }); + IT != shape->overrides.end()) { + data->overridenBy = strdup(shape->directory.c_str()); return data; } + } + for (auto& shape : impl->theme.shapes) { if (shape->directory != SHAPE) continue; @@ -392,9 +421,10 @@ SCursorRawShapeDataC* CHyprcursorManager::getRawShapeDataC(const char* shape_) { resultingImages.push_back(i.get()); } - data->hotspotX = shape->hotspotX; - data->hotspotY = shape->hotspotY; - data->type = shape->shapeType == SHAPE_PNG ? HC_DATA_PNG : HC_DATA_SVG; + data->hotspotX = shape->hotspotX; + data->hotspotY = shape->hotspotY; + data->nominalSize = shape->nominalSize; + data->type = shape->shapeType == SHAPE_PNG ? HC_DATA_PNG : HC_DATA_SVG; break; } @@ -424,8 +454,10 @@ bool CHyprcursorManager::loadThemeStyle(const SCursorStyleInfo& info) { bool sizeFound = false; if (shape->shapeType == SHAPE_PNG) { + const int IDEALSIDE = std::round(info.size / shape->nominalSize); + for (auto& image : impl->loadedShapes[shape.get()].images) { - if (image->side != info.size) + if (image->side != IDEALSIDE) continue; sizeFound = true; @@ -439,7 +471,7 @@ bool CHyprcursorManager::loadThemeStyle(const SCursorStyleInfo& info) { SLoadedCursorImage* leader = nullptr; int leaderVal = 1000000; for (auto& image : impl->loadedShapes[shape.get()].images) { - if (image->side < info.size) + if (image->side < IDEALSIDE) continue; if (image->side > leaderVal) @@ -451,7 +483,7 @@ bool CHyprcursorManager::loadThemeStyle(const SCursorStyleInfo& info) { if (!leader) { for (auto& image : impl->loadedShapes[shape.get()].images) { - if (std::abs((int)(image->side - info.size)) > leaderVal) + if (std::abs((int)(image->side - IDEALSIDE)) > leaderVal) continue; leaderVal = image->side; @@ -468,12 +500,16 @@ bool CHyprcursorManager::loadThemeStyle(const SCursorStyleInfo& info) { Debug::log(HC_LOG_TRACE, logFn, "loadThemeStyle: png shape {} has {} frames", shape->directory, FRAMES.size()); + const int PIXELSIDE = std::round(leader->side / shape->nominalSize); + + Debug::log(HC_LOG_TRACE, logFn, "loadThemeStyle: png shape has nominal {:.2f}, pixel size will be {}x", shape->nominalSize, PIXELSIDE); + for (auto& f : FRAMES) { 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]; - newImage->cairoSurface = cairo_image_surface_create_for_data((unsigned char*)newImage->artificialData, CAIRO_FORMAT_ARGB32, info.size, info.size, info.size * 4); + newImage->side = PIXELSIDE; + newImage->artificialData = new char[PIXELSIDE * PIXELSIDE * 4]; + newImage->cairoSurface = cairo_image_surface_create_for_data((unsigned char*)newImage->artificialData, CAIRO_FORMAT_ARGB32, PIXELSIDE, PIXELSIDE, PIXELSIDE * 4); newImage->delay = f->delay; const auto PCAIRO = cairo_create(newImage->cairoSurface); @@ -487,12 +523,12 @@ bool CHyprcursorManager::loadThemeStyle(const SCursorStyleInfo& info) { const auto PTN = cairo_pattern_create_for_surface(f->cairoSurface); cairo_pattern_set_extend(PTN, CAIRO_EXTEND_NONE); - const float scale = info.size / (float)f->side; + const float scale = PIXELSIDE / (float)f->side; cairo_scale(PCAIRO, scale, scale); cairo_pattern_set_filter(PTN, shape->resizeAlgo == HC_RESIZE_BILINEAR ? CAIRO_FILTER_GOOD : CAIRO_FILTER_NEAREST); cairo_set_source(PCAIRO, PTN); - cairo_rectangle(PCAIRO, 0, 0, info.size, info.size); + cairo_rectangle(PCAIRO, 0, 0, PIXELSIDE, PIXELSIDE); cairo_fill(PCAIRO); cairo_surface_flush(newImage->cairoSurface); @@ -505,12 +541,16 @@ bool CHyprcursorManager::loadThemeStyle(const SCursorStyleInfo& info) { Debug::log(HC_LOG_TRACE, logFn, "loadThemeStyle: svg shape {} has {} frames", shape->directory, FRAMES.size()); + const int PIXELSIDE = std::round(info.size / shape->nominalSize); + + Debug::log(HC_LOG_TRACE, logFn, "loadThemeStyle: svg shape has nominal {:.2f}, pixel size will be {}x", shape->nominalSize, PIXELSIDE); + for (auto& f : FRAMES) { 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]; - newImage->cairoSurface = cairo_image_surface_create_for_data((unsigned char*)newImage->artificialData, CAIRO_FORMAT_ARGB32, info.size, info.size, info.size * 4); + newImage->side = PIXELSIDE; + newImage->artificialData = new char[PIXELSIDE * PIXELSIDE * 4]; + newImage->cairoSurface = cairo_image_surface_create_for_data((unsigned char*)newImage->artificialData, CAIRO_FORMAT_ARGB32, PIXELSIDE, PIXELSIDE, PIXELSIDE * 4); newImage->delay = f->delay; const auto PCAIRO = cairo_create(newImage->cairoSurface); @@ -528,7 +568,7 @@ bool CHyprcursorManager::loadThemeStyle(const SCursorStyleInfo& info) { return false; } - RsvgRectangle rect = {0, 0, (double)info.size, (double)info.size}; + RsvgRectangle rect = {0, 0, (double)PIXELSIDE, (double)PIXELSIDE}; if (!rsvg_handle_render_document(handle, PCAIRO, &rect, &error)) { Debug::log(HC_LOG_ERR, logFn, "Failed rendering svg: {}", error->message); @@ -558,7 +598,7 @@ void CHyprcursorManager::cursorSurfaceStyleDone(const SCursorStyleInfo& info) { const bool isArtificial = e->artificial; // clean artificial rasters made for this - if (isArtificial && e->side == info.size) + if (isArtificial && e->side == std::round(info.size / shape->nominalSize)) return true; // clean invalid non-svg rasters @@ -634,18 +674,22 @@ std::optional CHyprcursorImplementation::loadTheme() { int errp = 0; zip_t* zip = zip_open(cursor.path().string().c_str(), ZIP_RDONLY, &errp); - zip_file_t* meta_file = zip_fopen(zip, "meta.hl", ZIP_FL_UNCHANGED); - bool metaIsHL = true; - if (!meta_file) { - meta_file = zip_fopen(zip, "meta.toml", ZIP_FL_UNCHANGED); - metaIsHL = false; - if (!meta_file) - return "cursor" + cursor.path().string() + "failed to load meta"; + zip_int64_t index = zip_name_locate(zip, "meta.hl", ZIP_FL_ENC_GUESS); + bool metaIsHL = true; + + if (index == -1) { + index = zip_name_locate(zip, "meta.toml", ZIP_FL_ENC_GUESS); + metaIsHL = false; } - char* buffer = new char[1024 * 1024]; /* 1MB should be more than enough */ + if (index == -1) + return "cursor" + cursor.path().string() + "failed to load meta"; - int readBytes = zip_fread(meta_file, buffer, 1024 * 1024 - 1); + zip_file_t* meta_file = zip_fopen_index(zip, index, ZIP_FL_UNCHANGED); + + char* buffer = new char[1024 * 1024]; /* 1MB should be more than enough */ + + int readBytes = zip_fread(meta_file, buffer, 1024 * 1024 - 1); zip_fclose(meta_file); @@ -670,6 +714,9 @@ std::optional CHyprcursorImplementation::loadTheme() { SHAPE->overrides = meta.parsedData.overrides; + zip_stat_t sb; + zip_stat_init(&sb); + for (auto& i : SHAPE->images) { if (SHAPE->shapeType == SHAPE_INVALID) { if (i.filename.ends_with(".svg")) @@ -694,14 +741,23 @@ std::optional CHyprcursorImplementation::loadTheme() { IMAGE->delay = i.delay; IMAGE->isSVG = SHAPE->shapeType == SHAPE_SVG; - // read from zip - zip_file_t* image_file = zip_fopen(zip, i.filename.c_str(), ZIP_FL_UNCHANGED); - if (!image_file) + index = zip_name_locate(zip, i.filename.c_str(), ZIP_FL_ENC_GUESS); + if (index == -1) return "cursor" + cursor.path().string() + "failed to load image_file"; - IMAGE->data = new char[1024 * 1024]; /* 1MB should be more than enough, again. This probably should be in the spec. */ + // read from zip + zip_file_t* image_file = zip_fopen_index(zip, index, ZIP_FL_UNCHANGED); + zip_stat_index(zip, index, ZIP_FL_UNCHANGED, &sb); - IMAGE->dataLen = zip_fread(image_file, IMAGE->data, 1024 * 1024 - 1); + if (sb.valid & ZIP_STAT_SIZE) { + IMAGE->data = new char[sb.size + 1]; + IMAGE->dataLen = sb.size + 1; + } else { + IMAGE->data = new char[1024 * 1024]; /* 1MB should be more than enough, again. This probably should be in the spec. */ + IMAGE->dataLen = 1024 * 1024; + } + + IMAGE->dataLen = zip_fread(image_file, IMAGE->data, IMAGE->dataLen - 1); zip_fclose(image_file); @@ -712,7 +768,7 @@ std::optional CHyprcursorImplementation::loadTheme() { IMAGE->cairoSurface = cairo_image_surface_create_from_png_stream(::readPNG, IMAGE); if (const auto STATUS = cairo_surface_status(IMAGE->cairoSurface); STATUS != CAIRO_STATUS_SUCCESS) { - delete[](char*) IMAGE->data; + delete[] (char*)IMAGE->data; IMAGE->data = nullptr; return "Failed reading cairoSurface, status " + std::to_string((int)STATUS); } @@ -724,10 +780,11 @@ std::optional CHyprcursorImplementation::loadTheme() { if (SHAPE->images.empty()) return "meta invalid: no images for shape " + cursor.path().stem().string(); - SHAPE->directory = cursor.path().stem().string(); - SHAPE->hotspotX = meta.parsedData.hotspotX; - SHAPE->hotspotY = meta.parsedData.hotspotY; - SHAPE->resizeAlgo = stringToAlgo(meta.parsedData.resizeAlgo); + SHAPE->directory = cursor.path().stem().string(); + SHAPE->hotspotX = meta.parsedData.hotspotX; + SHAPE->hotspotY = meta.parsedData.hotspotY; + SHAPE->nominalSize = meta.parsedData.nominalSize; + SHAPE->resizeAlgo = stringToAlgo(meta.parsedData.resizeAlgo); zip_discard(zip); } diff --git a/libhyprcursor/internalSharedTypes.hpp b/libhyprcursor/internalSharedTypes.hpp index 3a032aa..3775d53 100644 --- a/libhyprcursor/internalSharedTypes.hpp +++ b/libhyprcursor/internalSharedTypes.hpp @@ -37,7 +37,7 @@ struct SCursorImage { struct SCursorShape { std::string directory; - float hotspotX = 0, hotspotY = 0; + float hotspotX = 0, hotspotY = 0, nominalSize = 1.F; eHyprcursorResizeAlgo resizeAlgo = HC_RESIZE_NEAREST; std::vector images; std::vector overrides; diff --git a/libhyprcursor/meta.cpp b/libhyprcursor/meta.cpp index c7963d4..b841adb 100644 --- a/libhyprcursor/meta.cpp +++ b/libhyprcursor/meta.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include "VarList.hpp" @@ -71,49 +72,53 @@ static Hyprlang::CParseResult parseDefineSize(const char* C, const char* V) { Hyprlang::CParseResult result; const std::string VALUE = V; - if (!VALUE.contains(",")) { - result.setError("Invalid define_size"); - return result; - } + CVarList sizes(VALUE, 0, ';'); - auto LHS = removeBeginEndSpacesTabs(VALUE.substr(0, VALUE.find_first_of(","))); - auto RHS = removeBeginEndSpacesTabs(VALUE.substr(VALUE.find_first_of(",") + 1)); - auto DELAY = 0; - - CMeta::SDefinedSize size; - - if (RHS.contains(",")) { - const auto LL = removeBeginEndSpacesTabs(RHS.substr(0, RHS.find(","))); - const auto RR = removeBeginEndSpacesTabs(RHS.substr(RHS.find(",") + 1)); - - try { - size.delayMs = std::stoull(RR); - } catch (std::exception& e) { - result.setError(e.what()); + for (const auto& sizeStr : sizes) { + if (!sizeStr.contains(",")) { + result.setError("Invalid define_size"); return result; } - RHS = LL; - } + auto LHS = removeBeginEndSpacesTabs(sizeStr.substr(0, sizeStr.find_first_of(","))); + auto RHS = removeBeginEndSpacesTabs(sizeStr.substr(sizeStr.find_first_of(",") + 1)); + auto DELAY = 0; - if (!std::regex_match(RHS, std::regex("^[A-Za-z0-9_\\-\\.]+$"))) { - result.setError("Invalid cursor file name, characters must be within [A-Za-z0-9_\\-\\.] (if this seems like a mistake, check for invisible characters)"); - return result; - } + CMeta::SDefinedSize size; - size.file = RHS; + if (RHS.contains(",")) { + const auto LL = removeBeginEndSpacesTabs(RHS.substr(0, RHS.find(","))); + const auto RR = removeBeginEndSpacesTabs(RHS.substr(RHS.find(",") + 1)); - if (!size.file.ends_with(".svg")) { - try { - size.size = std::stoull(LHS); - } catch (std::exception& e) { - result.setError(e.what()); + try { + size.delayMs = std::stoull(RR); + } catch (std::exception& e) { + result.setError(e.what()); + return result; + } + + RHS = LL; + } + + if (!std::regex_match(RHS, std::regex("^[A-Za-z0-9_\\-\\.]+$"))) { + result.setError("Invalid cursor file name, characters must be within [A-Za-z0-9_\\-\\.] (if this seems like a mistake, check for invisible characters)"); return result; } - } else - size.size = 0; - currentMeta->parsedData.definedSizes.push_back(size); + size.file = RHS; + + if (!size.file.ends_with(".svg")) { + try { + size.size = std::stoull(LHS); + } catch (std::exception& e) { + result.setError(e.what()); + return result; + } + } else + size.size = 0; + + currentMeta->parsedData.definedSizes.push_back(size); + } return result; } @@ -122,7 +127,11 @@ static Hyprlang::CParseResult parseOverride(const char* C, const char* V) { Hyprlang::CParseResult result; const std::string VALUE = V; - currentMeta->parsedData.overrides.push_back(VALUE); + CVarList overrides(VALUE, 0, ';'); + + for (const auto& o : overrides) { + currentMeta->parsedData.overrides.push_back(VALUE); + } return result; } @@ -134,6 +143,7 @@ std::optional CMeta::parseHL() { meta = std::make_unique(rawdata.c_str(), Hyprlang::SConfigOptions{.pathIsStream = !dataPath}); meta->addConfigValue("hotspot_x", Hyprlang::FLOAT{0.F}); meta->addConfigValue("hotspot_y", Hyprlang::FLOAT{0.F}); + meta->addConfigValue("nominal_size", Hyprlang::FLOAT{1.F}); meta->addConfigValue("resize_algorithm", Hyprlang::STRING{"nearest"}); meta->registerHandler(::parseDefineSize, "define_size", {.allowFlags = false}); meta->registerHandler(::parseOverride, "define_override", {.allowFlags = false}); @@ -143,9 +153,10 @@ std::optional CMeta::parseHL() { return RESULT.getError(); } catch (const char* err) { return "failed parsing meta: " + std::string{err}; } - parsedData.hotspotX = std::any_cast(meta->getConfigValue("hotspot_x")); - parsedData.hotspotY = std::any_cast(meta->getConfigValue("hotspot_y")); - parsedData.resizeAlgo = std::any_cast(meta->getConfigValue("resize_algorithm")); + parsedData.hotspotX = std::any_cast(meta->getConfigValue("hotspot_x")); + parsedData.hotspotY = std::any_cast(meta->getConfigValue("hotspot_y")); + parsedData.nominalSize = std::clamp(std::any_cast(meta->getConfigValue("nominal_size")), 0.1F, 2.F); + parsedData.resizeAlgo = std::any_cast(meta->getConfigValue("resize_algorithm")); return {}; } @@ -154,8 +165,9 @@ std::optional CMeta::parseTOML() { try { auto MANIFEST = dataPath ? toml::parse_file(rawdata) : toml::parse(rawdata); - parsedData.hotspotX = MANIFEST["General"]["hotspot_x"].value_or(0.f); - parsedData.hotspotY = MANIFEST["General"]["hotspot_y"].value_or(0.f); + parsedData.hotspotX = MANIFEST["General"]["hotspot_x"].value_or(0.f); + parsedData.hotspotY = MANIFEST["General"]["hotspot_y"].value_or(0.f); + parsedData.nominalSize = std::clamp(MANIFEST["General"]["nominal_size"].value_or(1.F), 0.1F, 2.F); const std::string OVERRIDES = MANIFEST["General"]["define_override"].value_or(""); const std::string SIZES = MANIFEST["General"]["define_size"].value_or(""); diff --git a/libhyprcursor/meta.hpp b/libhyprcursor/meta.hpp index 837d5ee..9f0cb18 100644 --- a/libhyprcursor/meta.hpp +++ b/libhyprcursor/meta.hpp @@ -20,7 +20,7 @@ class CMeta { struct { std::string resizeAlgo; - float hotspotX = 0, hotspotY = 0; + float hotspotX = 0, hotspotY = 0, nominalSize = 1.F; std::vector overrides; std::vector definedSizes; } parsedData; diff --git a/nix/default.nix b/nix/default.nix index 281af4b..d6f4237 100644 --- a/nix/default.nix +++ b/nix/default.nix @@ -7,6 +7,7 @@ hyprlang, librsvg, libzip, + xcur2png, tomlplusplus, version ? "git", }: @@ -15,11 +16,6 @@ stdenv.mkDerivation { inherit version; src = ../.; - patches = [ - # adds /run/current-system/sw/share/icons to the icon lookup directories - ./dirs.patch - ]; - nativeBuildInputs = [ cmake pkg-config @@ -30,6 +26,7 @@ stdenv.mkDerivation { hyprlang librsvg libzip + xcur2png tomlplusplus ]; diff --git a/nix/dirs.patch b/nix/dirs.patch deleted file mode 100644 index adc690a..0000000 --- a/nix/dirs.patch +++ /dev/null @@ -1,13 +0,0 @@ -diff --git a/libhyprcursor/hyprcursor.cpp b/libhyprcursor/hyprcursor.cpp -index 304ab9f..1f7e95d 100644 ---- a/libhyprcursor/hyprcursor.cpp -+++ b/libhyprcursor/hyprcursor.cpp -@@ -14,7 +14,7 @@ - using namespace Hyprcursor; - - // directories for lookup --constexpr const std::array systemThemeDirs = {"/usr/share/icons"}; -+constexpr const std::array systemThemeDirs = {"/usr/share/icons", "/run/current-system/sw/share/icons"}; - constexpr const std::array userThemeDirs = {"/.local/share/icons", "/.icons"}; - - // diff --git a/nix/overlays.nix b/nix/overlays.nix index 0286ba1..75724f9 100644 --- a/nix/overlays.nix +++ b/nix/overlays.nix @@ -7,6 +7,7 @@ (builtins.substring 4 2 longDate) (builtins.substring 6 2 longDate) ]); + version = lib.removeSuffix "\n" (builtins.readFile ../VERSION); in { default = inputs.self.overlays.hyprcursor; @@ -14,10 +15,14 @@ in { inputs.hyprlang.overlays.default (final: prev: { hyprcursor = prev.callPackage ./default.nix { - stdenv = prev.gcc13Stdenv; - version = "0.pre" + "+date=" + (mkDate (inputs.self.lastModifiedDate or "19700101")) + "_" + (inputs.self.shortRev or "dirty"); + stdenv = prev.gcc14Stdenv; + version = version + "+date=" + (mkDate (inputs.self.lastModifiedDate or "19700101")) + "_" + (inputs.self.shortRev or "dirty"); inherit (final) hyprlang; }; + + hyprcursor-with-tests = final.hyprcursor.overrideAttrs (_: _: { + cmakeFlags = [(lib.cmakeBool "INSTALL_TESTS" true)]; + }); }) ]; } diff --git a/tests/c_test.c b/tests/c_test.c index 7561fd0..9b975b9 100644 --- a/tests/c_test.c +++ b/tests/c_test.c @@ -27,11 +27,22 @@ int main(int argc, char** argv) { } hyprcursor_cursor_raw_shape_data* shapeData = hyprcursor_get_raw_shape_data(mgr, "left_ptr"); - if (!shapeData || shapeData->len <= 0) { + if (!shapeData) { printf("failed querying left_ptr\n"); return 1; } + if (shapeData->overridenBy) { + hyprcursor_cursor_raw_shape_data* ov = hyprcursor_get_raw_shape_data(mgr, shapeData->overridenBy); + hyprcursor_raw_shape_data_free(shapeData); + shapeData = ov; + } + + if (!shapeData || shapeData->len <= 0) { + printf("left_ptr has no images\n"); + return 1; + } + printf("left_ptr images: %d\n", shapeData->len); for (size_t i = 0; i < shapeData->len; ++i) {