From 438daf1dfba81dfd7d5a4b69fd8db5e191c088bd Mon Sep 17 00:00:00 2001 From: Virt <41426325+VirtCode@users.noreply.github.com> Date: Sat, 9 Nov 2024 16:12:27 +0100 Subject: [PATCH] feat: use hyprcursor for magnified shapes closes #20 --- Makefile | 2 +- README.md | 41 ++++++++++++++- src/config/config.cpp | 4 ++ src/config/config.hpp | 9 +++- src/cursor.cpp | 47 ++++++++++++++--- src/cursor.hpp | 10 +++- src/highres.cpp | 118 ++++++++++++++++++++++++++++++++++++++++++ src/highres.hpp | 36 +++++++++++++ src/main.cpp | 14 ++++- 9 files changed, 268 insertions(+), 13 deletions(-) create mode 100644 src/highres.cpp create mode 100644 src/highres.hpp diff --git a/Makefile b/Makefile index 19e88d0..99532e8 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ all: $(PLUGIN_NAME).so $(PLUGIN_NAME).so: $(SOURCE_FILES) mkdir -p out - g++ -shared -Wall --no-gnu-unique -fPIC $(SOURCE_FILES) -g -DWLR_USE_UNSTABLE `pkg-config --cflags pixman-1 libdrm hyprland` -std=c++23 -o out/$(PLUGIN_NAME).so + g++ -shared -Wall --no-gnu-unique -fPIC $(SOURCE_FILES) -g -I /usr/include/hyprland/src `pkg-config --cflags pixman-1 libdrm hyprland` -std=c++23 -o out/$(PLUGIN_NAME).so clean: rm -f out/$(PLUGIN_NAME).so diff --git a/README.md b/README.md index 1945d93..fa650d8 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ This plugin is still very early in its development. There are also multiple thin - [X] per-shape length and starting angle (if possible) - [X] cursor shake to find - [X] overdue refactoring (wait for aquamarine merge) -- [ ] hyprcursor magnified shape +- [X] hyprcursor magnified shape If anything here sounds interesting to you, don't hesitate to contribute. @@ -192,6 +192,31 @@ plugin:dynamic-cursors { # see the `ipc` section below ipc = false } + + # use hyprcursor to get a higher resolution texture when the cursor is magnified + # see the `hyprcursor` section below + hyprcursor { + + # use nearest-neighbour (pixelated) scaling when magnifing beyond texture size + # this will also have effect without hyprcursor support being enabled + # 0 / false - never use pixelated scaling + # 1 / true - use pixelated when no highres image + # 2 - always use pixleated scaling + nearest = true + + # enable dedicated hyprcursor support + enabled = true + + # resolution in pixels to load the magnified shapes at + # be warned that loading a very high-resolution image will take a long time and might impact memory consumption + # -1 means we use [normal cursor size] * [shake:base option] + resolution = -1 + + # shape to use when clientside cursors are being magnified + # see the shape-name property of shape rules for possible names + # specifying clientside will use the actual shape, but will be pixelated + fallback = clientside + } } ``` @@ -235,6 +260,20 @@ The following events with the described arguments are available, when IPC is ena If you only want the IPC events and not the plugin actually changing the cursor size, you can set the properties `base` to `1`, `speed`, `influence` and `timeout` to `0` in the `plugin:dynamic-cursors:shake` section such that the cursor is not magified during the shake. +### hyprcursor +This plugin supports using hyprcursor to get higher-resolution images for when the cursor is magnified, i.e. when using shake to find. Due to the nature of cursors on wayland, there are some caveats to it. All configuration for it is located in the `plugin:dynamic-cursors:hyprcursor` section. + +To use hyprcursor for magnified shapes, the following must be met: +- `plugin:dynamic-cursors:hyprcursor:enabled` must be true (is by default) +- `cursor:enable_hyprcursor` must be true (is by default) +- you must be using a hyprcursor theme +- the hyprcursor theme should be **SVG-based** + +As mentioned, there are some caveats to it. Here are the most common ones: +- **Still pixelated on GTK apps and xwayland** - These apps are using clientside cursors, so the program itself is specifying the cursor shape, hence we cannot load a higher resolution for it. You can set a specific shape to show in these cases with the `fallback` option (see config). +- **Hyprland lags when loading the plugin** - Loading a set of high resolution cursor shapes takes some time. This means your session will freeze while the theme is being loaded. You can try setting a custom / lower `resolution` option (see config). +- **Blurred at very large sizes** - The high resolution cursors are preloaded at a fixed size. If you magnify your cursor beyond this size, your cursors will look blurry. You can increase the preload size with the `resolution` option (see config), at the expense of some memory and higher loading times. + ## performance > **TL;DR:** Hardware cursor performance is about the same as if an animated cursor shape was shown whenever you move your mouse. Sofware cursor performance is not impacted. When the cursor is magnified during a shake, the compositor will temporarily switch to software cursors. diff --git a/src/config/config.cpp b/src/config/config.cpp index 1c3403b..03e8396 100644 --- a/src/config/config.cpp +++ b/src/config/config.cpp @@ -58,6 +58,10 @@ void* const* getConfig(std::string name) { return HyprlandAPI::getConfigValue(PHANDLE, NAMESPACE + name)->getDataStaticPtr(); } +void* const* getHyprlandConfig(std::string name) { + return HyprlandAPI::getConfigValue(PHANDLE, name)->getDataStaticPtr(); +} + void addRulesConfig() { HyprlandAPI::addConfigKeyword(PHANDLE, CONFIG_SHAPERULE, onShapeRuleKeyword, Hyprlang::SHandlerOptions {}); diff --git a/src/config/config.hpp b/src/config/config.hpp index e9e955e..42a4d7e 100644 --- a/src/config/config.hpp +++ b/src/config/config.hpp @@ -12,7 +12,6 @@ #define CONFIG_IGNORE_WARPS "ignore_warps" #define CONFIG_SHAKE "shake:enabled" -#define CONFIG_SHAKE_NEAREST "shake:nearest" #define CONFIG_SHAKE_EFFECTS "shake:effects" #define CONFIG_SHAKE_IPC "shake:ipc" #define CONFIG_SHAKE_THRESHOLD "shake:threshold" @@ -31,6 +30,11 @@ #define CONFIG_STRETCH_LIMIT "stretch:limit" #define CONFIG_STRETCH_FUNCTION "stretch:function" +#define CONFIG_HIGHRES_ENABLED "hyprcursor:enabled" +#define CONFIG_HIGHRES_NEAREST "hyprcursor:nearest" +#define CONFIG_HIGHRES_SIZE "hyprcursor:resolution" +#define CONFIG_HIGHRES_FALLBACK "hyprcursor:fallback" + #define CONFIG_SHAPERULE "shaperule" /* is the plugin enabled */ @@ -52,3 +56,6 @@ void addShapeConfig(std::string name, std::variant valu /* get static pointer to config value */ void* const* getConfig(std::string name); + +/* get static pointer a hyprland config value */ +void* const* getHyprlandConfig(std::string name); diff --git a/src/cursor.cpp b/src/cursor.cpp index c853c2f..80b6274 100644 --- a/src/cursor.cpp +++ b/src/cursor.cpp @@ -2,15 +2,16 @@ #include "mode/Mode.hpp" #include "src/debug/Log.hpp" #include "src/helpers/math/Math.hpp" -#include "src/managers/eventLoop/EventLoopManager.hpp" #include #include #include +#include #include #include #define private public +#include #include #include #include @@ -53,7 +54,7 @@ Reimplements rendering of the software cursor. Is also largely identical to hyprlands impl, but uses our custom rendering to rotate the cursor. */ void CDynamicCursors::renderSoftware(CPointerManager* pointers, SP pMonitor, timespec* now, CRegion& damage, std::optional overridePos) { - static auto* const* PNEAREST = (Hyprlang::INT* const*) getConfig(CONFIG_SHAKE_NEAREST); + static auto* const* PNEAREST = (Hyprlang::INT* const*) getConfig(CONFIG_HIGHRES_NEAREST); if (!pointers->hasCursor()) return; @@ -74,11 +75,35 @@ void CDynamicCursors::renderSoftware(CPointerManager* pointers, SP pMo box.y = overridePos->y; } - // poperly transform hotspot, this first has to undo the hotspot transform from getCursorBoxGlobal - box.x = box.x + pointers->currentCursorImage.hotspot.x - pointers->currentCursorImage.hotspot.x * zoom; - box.y = box.y + pointers->currentCursorImage.hotspot.y - pointers->currentCursorImage.hotspot.y * zoom; - auto texture = pointers->getCurrentCursorTexture(); + bool nearest = false; + + if (zoom > 1) { + // this first has to undo the hotspot transform from getCursorBoxGlobal + box.x += pointers->currentCursorImage.hotspot.x; + box.y += pointers->currentCursorImage.hotspot.y; + + auto high = highres.getTexture(); + + if (high) { + texture = high; + auto buf = highres.getBuffer(); + + // we calculate a more accurate hotspot location if we have bigger shapes + box.x -= (buf->hotspot.x / buf->size.x) * pointers->currentCursorImage.size.x * zoom; + box.y -= (buf->hotspot.y / buf->size.y) * pointers->currentCursorImage.size.y * zoom; + + // only use nearest-neighbour if magnifying over size + nearest = **PNEAREST == 2 && pointers->currentCursorImage.size.x * zoom > buf->size.x; + + } else { + box.x -= pointers->currentCursorImage.hotspot.x * zoom; + box.y -= pointers->currentCursorImage.hotspot.y * zoom; + + nearest = **PNEAREST; + } + } + if (!texture) return; @@ -96,7 +121,7 @@ void CDynamicCursors::renderSoftware(CPointerManager* pointers, SP pMo box.rot = resultShown.rotation; // now pass the hotspot to rotate around - renderCursorTextureInternalWithDamage(texture, &box, &damage, 1.F, nullptr, 0, pointers->currentCursorImage.hotspot * state->monitor->scale * zoom, zoom > 1 && **PNEAREST, resultShown.stretch.angle, resultShown.stretch.magnitude); + renderCursorTextureInternalWithDamage(texture, &box, &damage, 1.F, nullptr, 0, pointers->currentCursorImage.hotspot * state->monitor->scale * zoom, nearest, resultShown.stretch.angle, resultShown.stretch.magnitude); if (pointers->currentCursorImage.surface) pointers->currentCursorImage.surface->resource()->frame(now); @@ -135,7 +160,7 @@ It is largely copied from hyprland, but adjusted to allow the cursor to be rotat */ SP CDynamicCursors::renderHardware(CPointerManager* pointers, SP state, SP texture) { static auto* const* PHW_DEBUG = (Hyprlang::INT* const*) getConfig(CONFIG_HW_DEBUG); - static auto* const* PNEAREST = (Hyprlang::INT* const*) getConfig(CONFIG_SHAKE_NEAREST); + static auto* const* PNEAREST = (Hyprlang::INT* const*) getConfig(CONFIG_HIGHRES_NEAREST); auto output = state->monitor->output; @@ -371,10 +396,16 @@ void CDynamicCursors::onCursorMoved(CPointerManager* pointers) { void CDynamicCursors::setShape(const std::string& shape) { g_pShapeRuleHandler->activate(shape); + highres.loadShape(shape); } void CDynamicCursors::unsetShape() { g_pShapeRuleHandler->activate("clientside"); + highres.loadShape("clientside"); +} + +void CDynamicCursors::updateTheme() { + highres.update(); } /* diff --git a/src/cursor.hpp b/src/cursor.hpp index b7200d8..9346d9f 100644 --- a/src/cursor.hpp +++ b/src/cursor.hpp @@ -1,15 +1,18 @@ #include "globals.hpp" +#include #define private public #include #undef private #include #include +#include #include "mode/ModeRotate.hpp" #include "mode/ModeTilt.hpp" #include "mode/ModeStretch.hpp" #include "other/Shake.hpp" +#include "highres.hpp" class CDynamicCursors { public: @@ -34,6 +37,8 @@ class CDynamicCursors { void setShape(const std::string& name); /* hook on setCursorSoftware */ void unsetShape(); + /* hook on updateTheme */ + void updateTheme(); /* hook on move, indicate that next onCursorMoved is actual move */ void setMove(); @@ -41,6 +46,9 @@ class CDynamicCursors { private: SP tick; + /* hyprcursor handler for highres images */ + CHighresHandler highres; + // current state of the cursor SModeResult resultMode; double resultShake; @@ -70,4 +78,4 @@ class CDynamicCursors { void calculate(EModeUpdate type); }; -inline std::unique_ptr g_pDynamicCursors; +inline UP g_pDynamicCursors; diff --git a/src/highres.cpp b/src/highres.cpp new file mode 100644 index 0000000..c3f9b00 --- /dev/null +++ b/src/highres.cpp @@ -0,0 +1,118 @@ +#include "globals.hpp" +#include "plugins/PluginAPI.hpp" +#include +#include +#include + +#include // required so we don't "unprivate" chrono +#define private public +#include "src/managers/CursorManager.hpp" +#undef private + +#include "highres.hpp" +#include "config/config.hpp" +#include "src/debug/Log.hpp" + +CHighresHandler::CHighresHandler() { + // load stuff on creation + update(); + + // and reload on config reload + static const auto PCALLBACK = HyprlandAPI::registerCallbackDynamic(PHANDLE, "configReloaded", [&](void* self, SCallbackInfo&, std::any data) { + update(); + }); +} + +static void hcLogger(enum eHyprcursorLogLevel level, char* message) { + if (level == HC_LOG_TRACE) return; + Debug::log(NONE, "[hc (dynamic)] {}", message); +} + +void CHighresHandler::update() { + static auto* const* PENABLED = (Hyprlang::INT* const*) getConfig(CONFIG_HIGHRES_ENABLED); + static auto* const* PUSEHYPRCURSOR = (Hyprlang::INT* const*) getHyprlandConfig("cursor:enable_hyprcursor"); + static auto* const* PSIZE = (Hyprlang::INT* const*) getConfig(CONFIG_HIGHRES_SIZE); + + static auto* const* PSHAKE_BASE= (Hyprlang::FLOAT* const*) getConfig(CONFIG_SHAKE_BASE); + static auto* const* PSHAKE = (Hyprlang::INT* const*) getConfig(CONFIG_SHAKE); // currently only needed for shake + + if (!**PENABLED || !**PUSEHYPRCURSOR || !**PSHAKE) { + // free manager if no longer enabled + if (manager) { + manager = nullptr; + texture = nullptr; + buffer = nullptr; + } + + return; + } + + std::string name = g_pCursorManager->m_szTheme; + unsigned int size = **PSIZE != -1 ? **PSIZE : std::round(g_pCursorManager->m_sCurrentStyleInfo.size * **PSHAKE_BASE * 1.5f); // * 1.5f to accomodate for slight growth + + // we already have loaded the same theme and size + if (manager && loadedName == name && loadedSize == size) + return; + + auto options = Hyprcursor::SManagerOptions(); + options.logFn = hcLogger; + options.allowDefaultFallback = true; + + manager = std::make_unique(name.empty() ? nullptr : name.c_str(), options); + if (!manager->valid()) { + Debug::log(ERR, "Hyprcursor for dynamic cursors failed loading theme \"{}\", falling back to pixelated trash.", name); + + manager = nullptr; + texture = nullptr; + buffer = nullptr; + return; + } + + auto time = std::chrono::system_clock::now(); + + Debug::log(INFO, "Loading hyprcursor theme {} of size {} for dynamic cursors, this might take a while!", name, size); + style = Hyprcursor::SCursorStyleInfo { size }; + manager->loadThemeStyle(style); + loadedSize = size; + loadedName = name; + + float ms = std::chrono::duration_cast(std::chrono::system_clock::now() - time).count(); + Debug::log(INFO, "Loading finished, took {}ms", ms); +} + +void CHighresHandler::loadShape(const std::string& name) { + static auto const* PFALLBACK = (Hyprlang::STRING const*) getConfig(CONFIG_HIGHRES_FALLBACK); + + if (!manager) return; + + Hyprcursor::SCursorShapeData shape = manager->getShape(name.c_str(), style); + + // try load fallback image + if (shape.images.size() == 0) { + shape = manager->getShape(*PFALLBACK, style); + + if (shape.images.size() == 0) { + Debug::log(WARN, "Failed to load fallback shape {}, for shape {}!", *PFALLBACK, name); + + texture = nullptr; + buffer = nullptr; + return; + } + } + + buffer = makeShared( + shape.images[0].surface, + Vector2D{shape.images[0].size, shape.images[0].size}, + Vector2D{shape.images[0].hotspotX, shape.images[0].hotspotY} + ); + + texture = makeShared(buffer); +} + +SP CHighresHandler::getTexture() { + return texture; +} + +SP CHighresHandler::getBuffer() { + return buffer; +} diff --git a/src/highres.hpp b/src/highres.hpp new file mode 100644 index 0000000..dcdebd5 --- /dev/null +++ b/src/highres.hpp @@ -0,0 +1,36 @@ +#include "src/managers/CursorManager.hpp" +#include +#include +#include + +#include +#include + +class CHighresHandler { +public: + CHighresHandler(); + + /* refreshes the hyprcursor theme and stuff, should be called if config values change */ + void update(); + + /* update the currently loaded shape */ + void loadShape(const std::string& name); + + SP getTexture(); + SP getBuffer(); + +private: + bool enabled = true; + + Hyprcursor::SCursorStyleInfo style; + UP manager; + + /* keep track of loaded theme so we don't reload unnessecarily (<- i'm almost certain there's a typo in this word) */ + unsigned int loadedSize = -1; + std::string loadedName = ""; + + /* current texture and hotspot */ + std::string shape = ""; + SP texture; + SP buffer; +}; diff --git a/src/main.cpp b/src/main.cpp index 97dc886..8c35901 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -69,6 +69,13 @@ void hkMove(void* thisptr, const Vector2D& deltaLogical) { (*(origMove)g_pMoveHook->m_pOriginal)(thisptr, deltaLogical); } +typedef void (*origUpdateTheme)(void*); +inline CFunctionHook* g_pUpdateThemeHook = nullptr; +void hkUpdateTheme(void* thisptr) { + (*(origUpdateTheme) g_pUpdateThemeHook->m_pOriginal)(thisptr); + if (isEnabled()) g_pDynamicCursors->updateTheme(); +} + /* hooks a function hook */ CFunctionHook* hook(std::string name, std::string object, void* function) { auto names = HyprlandAPI::findFunctionsByName(PHANDLE, name); @@ -108,7 +115,6 @@ APICALL EXPORT PLUGIN_DESCRIPTION_INFO PLUGIN_INIT(HANDLE handle) { addConfig(CONFIG_THRESHOLD, 2); addConfig(CONFIG_SHAKE, true); - addConfig(CONFIG_SHAKE_NEAREST, true); addConfig(CONFIG_SHAKE_EFFECTS, false); addConfig(CONFIG_SHAKE_IPC, false); addConfig(CONFIG_SHAKE_THRESHOLD, 6.0f); @@ -118,6 +124,11 @@ APICALL EXPORT PLUGIN_DESCRIPTION_INFO PLUGIN_INIT(HANDLE handle) { addConfig(CONFIG_SHAKE_LIMIT, 0.0F); addConfig(CONFIG_SHAKE_TIMEOUT, 2000); + addConfig(CONFIG_HIGHRES_ENABLED, true); + addConfig(CONFIG_HIGHRES_NEAREST, true); + addConfig(CONFIG_HIGHRES_FALLBACK, "clientside"); + addConfig(CONFIG_HIGHRES_SIZE, -1); + addShapeConfig(CONFIG_TILT_FUNCTION, "negative_quadratic"); addShapeConfig(CONFIG_TILT_LIMIT, 5000); @@ -147,6 +158,7 @@ APICALL EXPORT PLUGIN_DESCRIPTION_INFO PLUGIN_INIT(HANDLE handle) { g_pSetCursorFromNameHook = hook("setCursorFromName", "CCursorManager", (void*) &hkSetCursorFromName); g_pSetCursorSurfaceHook = hook("setCursorSurface", "CCursorManager", (void*) &hkSetCursorSurface); + g_pUpdateThemeHook = hook("updateTheme", "CCursorManager", (void*) &hkUpdateTheme); } catch (...) { HyprlandAPI::addNotification(PHANDLE, "[dynamic-cursors] Failed to load, hooks could not be made!", CColor{1.0, 0.2, 0.2, 1.0}, 5000); throw std::runtime_error("hooks failed");