From 127aab815908ecbd3db4d23f127d2e96b79855f9 Mon Sep 17 00:00:00 2001 From: Vaxry <43317083+vaxerski@users.noreply.github.com> Date: Tue, 2 Sep 2025 13:16:43 +0200 Subject: [PATCH] input: add per-device scroll-factor (#11241) --- hyprtester/CMakeLists.txt | 1 + hyprtester/clients/pointer-scroll.cpp | 318 ++++++++++++++++++ hyprtester/plugin/src/main.cpp | 57 +++- .../src/tests/clients/pointer-scroll.cpp | 145 ++++++++ hyprtester/src/tests/clients/pointer-warp.cpp | 7 +- hyprtester/test.conf | 9 + nix/hyprtester.nix | 1 + src/config/ConfigManager.cpp | 10 +- src/config/ConfigManager.hpp | 1 + src/debug/HyprCtl.cpp | 10 +- src/desktop/Window.cpp | 8 + src/desktop/Window.hpp | 2 + src/devices/IPointer.hpp | 13 +- src/managers/PointerManager.cpp | 4 +- src/managers/input/InputManager.cpp | 26 +- src/managers/input/InputManager.hpp | 3 +- 16 files changed, 593 insertions(+), 22 deletions(-) create mode 100644 hyprtester/clients/pointer-scroll.cpp create mode 100644 hyprtester/src/tests/clients/pointer-scroll.cpp diff --git a/hyprtester/CMakeLists.txt b/hyprtester/CMakeLists.txt index 34e62603d..0b445b0ac 100644 --- a/hyprtester/CMakeLists.txt +++ b/hyprtester/CMakeLists.txt @@ -98,3 +98,4 @@ protocolnew("staging/pointer-warp" "pointer-warp-v1" false) protocolnew("stable/xdg-shell" "xdg-shell" false) clientNew("pointer-warp" PROTOS "pointer-warp-v1" "xdg-shell") +clientNew("pointer-scroll" PROTOS "xdg-shell") diff --git a/hyprtester/clients/pointer-scroll.cpp b/hyprtester/clients/pointer-scroll.cpp new file mode 100644 index 000000000..140e4700f --- /dev/null +++ b/hyprtester/clients/pointer-scroll.cpp @@ -0,0 +1,318 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include + +using Hyprutils::Math::Vector2D; +using namespace Hyprutils::Memory; + +struct SWlState { + wl_display* display; + CSharedPointer registry; + + // protocols + CSharedPointer wlCompositor; + CSharedPointer wlSeat; + CSharedPointer wlShm; + CSharedPointer xdgShell; + + // shm/buffer stuff + CSharedPointer shmPool; + CSharedPointer shmBuf; + int shmFd; + size_t shmBufSize; + bool xrgb8888_support = false; + + // surface/toplevel stuff + CSharedPointer surf; + CSharedPointer xdgSurf; + CSharedPointer xdgToplevel; + Vector2D geom; + + // pointer + CSharedPointer pointer; + uint32_t enterSerial; + + // last delta + float lastScrollDelta = -1.F; + bool writeDelta = false; +}; + +static std::ofstream logfile; + +static bool debug, started, shouldExit; + +template +//NOLINTNEXTLINE +static void clientLog(std::format_string fmt, Args&&... args) { + std::string text = std::vformat(fmt.get(), std::make_format_args(args...)); + std::println("{}", text); + logfile << text << std::endl; + std::fflush(stdout); +} + +template +//NOLINTNEXTLINE +static void debugLog(std::format_string fmt, Args&&... args) { + std::string text = std::vformat(fmt.get(), std::make_format_args(args...)); + logfile << text << std::endl; + if (!debug) + return; + std::println("{}", text); + std::fflush(stdout); +} + +static bool bindRegistry(SWlState& state) { + state.registry = makeShared((wl_proxy*)wl_display_get_registry(state.display)); + + state.registry->setGlobal([&](CCWlRegistry* r, uint32_t id, const char* name, uint32_t version) { + const std::string NAME = name; + if (NAME == "wl_compositor") { + debugLog(" > binding to global: {} (version {}) with id {}", name, version, id); + state.wlCompositor = makeShared((wl_proxy*)wl_registry_bind((wl_registry*)state.registry->resource(), id, &wl_compositor_interface, 6)); + } else if (NAME == "wl_shm") { + debugLog(" > binding to global: {} (version {}) with id {}", name, version, id); + state.wlShm = makeShared((wl_proxy*)wl_registry_bind((wl_registry*)state.registry->resource(), id, &wl_shm_interface, 1)); + } else if (NAME == "wl_seat") { + debugLog(" > binding to global: {} (version {}) with id {}", name, version, id); + state.wlSeat = makeShared((wl_proxy*)wl_registry_bind((wl_registry*)state.registry->resource(), id, &wl_seat_interface, 9)); + } else if (NAME == "xdg_wm_base") { + debugLog(" > binding to global: {} (version {}) with id {}", name, version, id); + state.xdgShell = makeShared((wl_proxy*)wl_registry_bind((wl_registry*)state.registry->resource(), id, &xdg_wm_base_interface, 1)); + } + }); + state.registry->setGlobalRemove([](CCWlRegistry* r, uint32_t id) { debugLog("Global {} removed", id); }); + + wl_display_roundtrip(state.display); + + if (!state.wlCompositor || !state.wlShm || !state.wlSeat || !state.xdgShell) { + clientLog("Failed to get protocols from Hyprland"); + return false; + } + + return true; +} + +static bool createShm(SWlState& state, Vector2D geom) { + if (!state.xrgb8888_support) + return false; + + size_t stride = geom.x * 4; + size_t size = geom.y * stride; + if (!state.shmPool) { + const char* name = "/wl-shm-pointer-scroll"; + state.shmFd = shm_open(name, O_RDWR | O_CREAT | O_EXCL, 0600); + if (state.shmFd < 0) + return false; + + if (shm_unlink(name) < 0 || ftruncate(state.shmFd, size) < 0) { + close(state.shmFd); + return false; + } + + state.shmPool = makeShared(state.wlShm->sendCreatePool(state.shmFd, size)); + if (!state.shmPool->resource()) { + close(state.shmFd); + state.shmFd = -1; + state.shmPool.reset(); + return false; + } + state.shmBufSize = size; + } else if (size > state.shmBufSize) { + if (ftruncate(state.shmFd, size) < 0) { + close(state.shmFd); + state.shmFd = -1; + state.shmPool.reset(); + return false; + } + + state.shmPool->sendResize(size); + state.shmBufSize = size; + } + + auto buf = makeShared(state.shmPool->sendCreateBuffer(0, geom.x, geom.y, stride, WL_SHM_FORMAT_XRGB8888)); + if (!buf->resource()) + return false; + + if (state.shmBuf) { + state.shmBuf->sendDestroy(); + state.shmBuf.reset(); + } + + state.shmBuf = buf; + + return true; +} + +static bool setupToplevel(SWlState& state) { + state.wlShm->setFormat([&](CCWlShm* p, uint32_t format) { + if (format == WL_SHM_FORMAT_XRGB8888) + state.xrgb8888_support = true; + }); + + state.xdgShell->setPing([&](CCXdgWmBase* p, uint32_t serial) { state.xdgShell->sendPong(serial); }); + + state.surf = makeShared(state.wlCompositor->sendCreateSurface()); + if (!state.surf->resource()) + return false; + + state.xdgSurf = makeShared(state.xdgShell->sendGetXdgSurface(state.surf->resource())); + if (!state.xdgSurf->resource()) + return false; + + state.xdgToplevel = makeShared(state.xdgSurf->sendGetToplevel()); + if (!state.xdgToplevel->resource()) + return false; + + state.xdgToplevel->setClose([&](CCXdgToplevel* p) { exit(0); }); + + state.xdgToplevel->setConfigure([&](CCXdgToplevel* p, int32_t w, int32_t h, wl_array* arr) { + state.geom = {1280, 720}; + + if (!createShm(state, state.geom)) + exit(-1); + }); + + state.xdgSurf->setConfigure([&](CCXdgSurface* p, uint32_t serial) { + if (!state.shmBuf) + debugLog("xdgSurf configure but no buf made yet?"); + + state.xdgSurf->sendSetWindowGeometry(0, 0, state.geom.x, state.geom.y); + state.surf->sendAttach(state.shmBuf.get(), 0, 0); + state.surf->sendCommit(); + + state.xdgSurf->sendAckConfigure(serial); + + if (!started) { + started = true; + clientLog("started"); + } + }); + + state.xdgToplevel->sendSetTitle("pointer-scroll test client"); + state.xdgToplevel->sendSetAppId("pointer-scroll"); + + state.surf->sendAttach(nullptr, 0, 0); + state.surf->sendCommit(); + + return true; +} + +static bool setupSeat(SWlState& state) { + state.pointer = makeShared(state.wlSeat->sendGetPointer()); + if (!state.pointer->resource()) + return false; + + state.pointer->setEnter([&](CCWlPointer* p, uint32_t serial, wl_proxy* surf, wl_fixed_t x, wl_fixed_t y) { + debugLog("Got pointer enter event, serial {}, x {}, y {}", serial, x, y); + state.enterSerial = serial; + }); + + state.pointer->setAxis([&](CCWlPointer* p, uint32_t time, wl_pointer_axis axis, wl_fixed_t delta) { + debugLog("axis: ax {} delta {}", (int)axis, wl_fixed_to_double(delta)); + + if (state.writeDelta) { + clientLog("{:.2f}", wl_fixed_to_double(delta)); + state.writeDelta = false; + state.lastScrollDelta = -1; + return; + } + + state.lastScrollDelta = wl_fixed_to_double(delta); + state.writeDelta = true; + }); + + state.pointer->setLeave([&](CCWlPointer* p, uint32_t serial, wl_proxy* surf) { debugLog("Got pointer leave event, serial {}", serial); }); + + state.pointer->setMotion([&](CCWlPointer* p, uint32_t serial, wl_fixed_t x, wl_fixed_t y) { debugLog("Got pointer motion event, serial {}, x {}, y {}", serial, x, y); }); + + return true; +} + +// return last delta after axis +static void parseRequest(SWlState& state, std::string req) { + if (!state.writeDelta) { + state.writeDelta = true; + return; + } + + clientLog("{:.2f}", state.lastScrollDelta); + state.writeDelta = false; + state.lastScrollDelta = -1; +} + +int main(int argc, char** argv) { + logfile.open("pointer-scroll.txt", std::ios::trunc); + + if (argc != 1 && argc != 2) + clientLog("Only the \"--debug\" switch is allowed, it turns on debug logs."); + + if (argc == 2 && std::string{argv[1]} == "--debug") + debug = true; + + SWlState state; + + // WAYLAND_DISPLAY env should be set to the correct one + state.display = wl_display_connect(nullptr); + if (!state.display) { + clientLog("Failed to connect to wayland display"); + return -1; + } + + if (!bindRegistry(state) || !setupSeat(state) || !setupToplevel(state)) + return -1; + + std::array readBuf; + readBuf.fill(0); + + wl_display_flush(state.display); + + struct pollfd fds[2] = {{.fd = wl_display_get_fd(state.display), .events = POLLIN | POLLOUT}, {.fd = STDIN_FILENO, .events = POLLIN}}; + while (!shouldExit && poll(fds, 2, 0) != -1) { + if (fds[0].revents & POLLIN) { + wl_display_flush(state.display); + + if (wl_display_prepare_read(state.display) == 0) { + wl_display_read_events(state.display); + wl_display_dispatch_pending(state.display); + } else + wl_display_dispatch(state.display); + + int ret = 0; + do { + ret = wl_display_dispatch_pending(state.display); + wl_display_flush(state.display); + } while (ret > 0); + } + + if (fds[1].revents & POLLIN) { + ssize_t bytesRead = read(fds[1].fd, readBuf.data(), 1023); + if (bytesRead == -1) + continue; + readBuf[bytesRead] = 0; + + parseRequest(state, std::string{readBuf.data()}); + } + } + + wl_display* display = state.display; + state = {}; + + wl_display_disconnect(display); + logfile.flush(); + logfile.close(); + return 0; +} diff --git a/hyprtester/plugin/src/main.cpp b/hyprtester/plugin/src/main.cpp index 030359181..2b1d443ac 100644 --- a/hyprtester/plugin/src/main.cpp +++ b/hyprtester/plugin/src/main.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #undef private @@ -88,9 +89,38 @@ class CTestKeyboard : public IKeyboard { } private: - bool m_isVirtual; + bool m_isVirtual = false; }; +class CTestMouse : public IPointer { + public: + static SP create(bool isVirtual) { + auto maus = SP(new CTestMouse()); + maus->m_self = maus; + maus->m_isVirtual = isVirtual; + maus->m_deviceName = "test-mouse"; + maus->m_hlName = "test-mouse"; + return maus; + } + + virtual bool isVirtual() { + return m_isVirtual; + } + + virtual SP aq() { + return nullptr; + } + + void destroy() { + m_events.destroy.emit(); + } + + private: + bool m_isVirtual = false; +}; + +SP g_mouse; + static SDispatchResult pressAlt(std::string in) { g_pInputManager->m_lastMods = in == "1" ? HL_MODIFIER_ALT : 0; @@ -173,6 +203,23 @@ static SDispatchResult vkb(std::string in) { return {}; } +static SDispatchResult scroll(std::string in) { + int by; + try { + by = std::stoi(in); + } catch (...) { return SDispatchResult{.success = false, .error = "invalid input"}; } + + Debug::log(LOG, "tester: scrolling by {}", by); + + g_mouse->m_pointerEvents.axis.emit(IPointer::SAxisEvent{ + .delta = by, + .deltaDiscrete = 120, + .mouse = true, + }); + + return {}; +} + APICALL EXPORT PLUGIN_DESCRIPTION_INFO PLUGIN_INIT(HANDLE handle) { PHANDLE = handle; @@ -181,10 +228,16 @@ APICALL EXPORT PLUGIN_DESCRIPTION_INFO PLUGIN_INIT(HANDLE handle) { HyprlandAPI::addDispatcherV2(PHANDLE, "plugin:test:vkb", ::vkb); HyprlandAPI::addDispatcherV2(PHANDLE, "plugin:test:alt", ::pressAlt); HyprlandAPI::addDispatcherV2(PHANDLE, "plugin:test:gesture", ::simulateGesture); + HyprlandAPI::addDispatcherV2(PHANDLE, "plugin:test:scroll", ::scroll); + + // init mouse + g_mouse = CTestMouse::create(false); + g_pInputManager->newMouse(g_mouse); return {"hyprtestplugin", "hyprtestplugin", "Vaxry", "1.0"}; } APICALL EXPORT void PLUGIN_EXIT() { - ; + g_mouse->destroy(); + g_mouse.reset(); } diff --git a/hyprtester/src/tests/clients/pointer-scroll.cpp b/hyprtester/src/tests/clients/pointer-scroll.cpp new file mode 100644 index 000000000..e1ba237fa --- /dev/null +++ b/hyprtester/src/tests/clients/pointer-scroll.cpp @@ -0,0 +1,145 @@ +#include "../../shared.hpp" +#include "../../hyprctlCompat.hpp" +#include "../shared.hpp" +#include "tests.hpp" +#include "build.hpp" + +#include +#include + +#include +#include +#include + +using namespace Hyprutils::OS; +using namespace Hyprutils::Memory; + +#define SP CSharedPointer + +struct SClient { + SP proc; + std::array readBuf; + CFileDescriptor readFd, writeFd; + struct pollfd fds; +}; + +static int ret = 0; + +static bool startClient(SClient& client) { + client.proc = makeShared(binaryDir + "/pointer-scroll", std::vector{}); + + client.proc->addEnv("WAYLAND_DISPLAY", WLDISPLAY); + + int pipeFds1[2], pipeFds2[2]; + if (pipe(pipeFds1) != 0 || pipe(pipeFds2) != 0) { + NLog::log("{}Unable to open pipe to client", Colors::RED); + return false; + } + + client.writeFd = CFileDescriptor(pipeFds1[1]); + client.proc->setStdinFD(pipeFds1[0]); + + client.readFd = CFileDescriptor(pipeFds2[0]); + client.proc->setStdoutFD(pipeFds2[1]); + + client.proc->runAsync(); + + close(pipeFds1[0]); + close(pipeFds2[1]); + + client.fds = {.fd = client.readFd.get(), .events = POLLIN}; + if (poll(&client.fds, 1, 1000) != 1 || !(client.fds.revents & POLLIN)) + return false; + + client.readBuf.fill(0); + if (read(client.readFd.get(), client.readBuf.data(), client.readBuf.size() - 1) == -1) + return false; + + std::string ret = std::string{client.readBuf.data()}; + if (ret.find("started") == std::string::npos) { + NLog::log("{}Failed to start pointer-scroll client, read {}", Colors::RED, ret); + return false; + } + + // wait for window to appear + std::this_thread::sleep_for(std::chrono::milliseconds(5000)); + + if (getFromSocket(std::format("/dispatch setprop pid:{} noanim 1", client.proc->pid())) != "ok") { + NLog::log("{}Failed to disable animations for client window", Colors::RED, ret); + return false; + } + + if (getFromSocket(std::format("/dispatch focuswindow pid:{}", client.proc->pid())) != "ok") { + NLog::log("{}Failed to focus pointer-scroll client", Colors::RED, ret); + return false; + } + + NLog::log("{}Started pointer-scroll client", Colors::YELLOW); + + return true; +} + +static void stopClient(SClient& client) { + std::string cmd = "exit\n"; + write(client.writeFd.get(), cmd.c_str(), cmd.length()); + + kill(client.proc->pid(), SIGKILL); + client.proc.reset(); +} + +static int getLastDelta(SClient& client) { + std::string cmd = "hypr"; + if ((size_t)write(client.writeFd.get(), cmd.c_str(), cmd.length()) != cmd.length()) + return false; + + if (poll(&client.fds, 1, 1500) != 1 || !(client.fds.revents & POLLIN)) + return false; + ssize_t bytesRead = read(client.fds.fd, client.readBuf.data(), 1023); + if (bytesRead == -1) + return false; + + client.readBuf[bytesRead] = 0; + std::string received = std::string{client.readBuf.data()}; + received.pop_back(); + + try { + return std::stoi(received); + } catch (...) { return -1; } +} + +static bool sendScroll(int delta) { + return getFromSocket(std::format("/dispatch plugin:test:scroll {}", delta)) == "ok"; +} + +static bool test() { + SClient client; + + if (!startClient(client)) + return false; + + EXPECT(getFromSocket("/keyword input:emulate_discrete_scroll 0"), "ok"); + + EXPECT(sendScroll(10), true); + EXPECT(getLastDelta(client), 10); + + EXPECT(getFromSocket("/keyword input:scroll_factor 2"), "ok"); + EXPECT(sendScroll(10), true); + EXPECT(getLastDelta(client), 20); + + EXPECT(getFromSocket("r/keyword device[test-mouse-1]:scroll_factor 3"), "ok"); + EXPECT(sendScroll(10), true); + EXPECT(getLastDelta(client), 30); + + EXPECT(getFromSocket("r/dispatch setprop active scrollmouse 4"), "ok"); + EXPECT(sendScroll(10), true); + EXPECT(getLastDelta(client), 40); + + stopClient(client); + + NLog::log("{}Reloading the config", Colors::YELLOW); + OK(getFromSocket("/reload")); + + return !ret; +} + +REGISTER_CLIENT_TEST_FN(test); diff --git a/hyprtester/src/tests/clients/pointer-warp.cpp b/hyprtester/src/tests/clients/pointer-warp.cpp index 643da796e..f37b94c35 100644 --- a/hyprtester/src/tests/clients/pointer-warp.cpp +++ b/hyprtester/src/tests/clients/pointer-warp.cpp @@ -23,7 +23,7 @@ struct SClient { struct pollfd fds; }; -static int ret; +static int ret = 0; static bool startClient(SClient& client) { client.proc = makeShared(binaryDir + "/pointer-warp", std::vector{}); @@ -174,7 +174,10 @@ static bool test() { stopClient(client); - return true; + NLog::log("{}Reloading the config", Colors::YELLOW); + OK(getFromSocket("/reload")); + + return !ret; } REGISTER_CLIENT_TEST_FN(test); diff --git a/hyprtester/test.conf b/hyprtester/test.conf index 3e042ae6c..7b0c53b77 100644 --- a/hyprtester/test.conf +++ b/hyprtester/test.conf @@ -151,6 +151,11 @@ animations { animation = workspacesOut, 1, 1.94, almostLinear, fade } +device { + name = test-mouse-1 + enabled = true +} + # Ref https://wiki.hyprland.org/Configuring/Workspace-Rules/ # "Smart gaps" / "No gaps when only" # uncomment all if you wish to use that. @@ -213,6 +218,10 @@ device { sensitivity = -0.5 } +debug { + disable_logs = false +} + ################### ### KEYBINDINGS ### diff --git a/nix/hyprtester.nix b/nix/hyprtester.nix index cf9a9c747..9e8d28776 100644 --- a/nix/hyprtester.nix +++ b/nix/hyprtester.nix @@ -52,6 +52,7 @@ in postInstall = '' install pointer-warp -t $out/bin + install pointer-scroll -t $out/bin ''; cmakeBuildType = "Debug"; diff --git a/src/config/ConfigManager.cpp b/src/config/ConfigManager.cpp index 9277b89be..161834981 100644 --- a/src/config/ConfigManager.cpp +++ b/src/config/ConfigManager.cpp @@ -810,6 +810,7 @@ CConfigManager::CConfigManager() { m_config->addSpecialConfigValue("device", "scroll_button", Hyprlang::INT{0}); m_config->addSpecialConfigValue("device", "scroll_button_lock", Hyprlang::INT{0}); m_config->addSpecialConfigValue("device", "scroll_points", {STRVAL_EMPTY}); + m_config->addSpecialConfigValue("device", "scroll_factor", Hyprlang::FLOAT{-1}); m_config->addSpecialConfigValue("device", "transform", Hyprlang::INT{-1}); m_config->addSpecialConfigValue("device", "output", {STRVAL_EMPTY}); m_config->addSpecialConfigValue("device", "enabled", Hyprlang::INT{1}); // only for mice, touchpads, and touchdevices @@ -1335,13 +1336,18 @@ Hyprlang::CConfigValue* CConfigManager::getConfigValueSafeDevice(const std::stri const auto VAL = m_config->getSpecialConfigValuePtr("device", val.c_str(), dev.c_str()); - if ((!VAL || !VAL->m_bSetByUser) && !fallback.empty()) { + if ((!VAL || !VAL->m_bSetByUser) && !fallback.empty()) return m_config->getConfigValuePtr(fallback.c_str()); - } return VAL; } +bool CConfigManager::deviceConfigExplicitlySet(const std::string& dev, const std::string& val) { + const auto VAL = m_config->getSpecialConfigValuePtr("device", val.c_str(), dev.c_str()); + + return VAL && VAL->m_bSetByUser; +} + int CConfigManager::getDeviceInt(const std::string& dev, const std::string& v, const std::string& fallback) { return std::any_cast(getConfigValueSafeDevice(dev, v, fallback)->getValue()); } diff --git a/src/config/ConfigManager.hpp b/src/config/ConfigManager.hpp index fe1adc504..cff146f34 100644 --- a/src/config/ConfigManager.hpp +++ b/src/config/ConfigManager.hpp @@ -210,6 +210,7 @@ class CConfigManager { float getDeviceFloat(const std::string&, const std::string&, const std::string& fallback = ""); Vector2D getDeviceVec(const std::string&, const std::string&, const std::string& fallback = ""); std::string getDeviceString(const std::string&, const std::string&, const std::string& fallback = ""); + bool deviceConfigExplicitlySet(const std::string&, const std::string&); bool deviceConfigExists(const std::string&); Hyprlang::CConfigValue* getConfigValueSafeDevice(const std::string& dev, const std::string& val, const std::string& fallback); bool shouldBlurLS(const std::string&); diff --git a/src/debug/HyprCtl.cpp b/src/debug/HyprCtl.cpp index a9388789d..8598b1f6b 100644 --- a/src/debug/HyprCtl.cpp +++ b/src/debug/HyprCtl.cpp @@ -722,10 +722,11 @@ static std::string devicesRequest(eHyprCtlOutputFormat format, std::string reque R"#( {{ "address": "0x{:x}", "name": "{}", - "defaultSpeed": {:.5f} + "defaultSpeed": {:.5f}, + "scrollFactor": {:.2f} }},)#", rc(m.get()), escapeJSONStrings(m->m_hlName), - m->aq() && m->aq()->getLibinputHandle() ? libinput_device_config_accel_get_default_speed(m->aq()->getLibinputHandle()) : 0.f); + m->aq() && m->aq()->getLibinputHandle() ? libinput_device_config_accel_get_default_speed(m->aq()->getLibinputHandle()) : 0.f, m->m_scrollFactor.value_or(-1)); } trimTrailingComma(result); @@ -826,8 +827,9 @@ static std::string devicesRequest(eHyprCtlOutputFormat format, std::string reque result += "mice:\n"; for (auto const& m : g_pInputManager->m_pointers) { - result += std::format("\tMouse at {:x}:\n\t\t{}\n\t\t\tdefault speed: {:.5f}\n", rc(m.get()), m->m_hlName, - (m->aq() && m->aq()->getLibinputHandle() ? libinput_device_config_accel_get_default_speed(m->aq()->getLibinputHandle()) : 0.f)); + result += std::format("\tMouse at {:x}:\n\t\t{}\n\t\t\tdefault speed: {:.5f}\n\t\t\tscroll factor: {:.2f}\n", rc(m.get()), m->m_hlName, + (m->aq() && m->aq()->getLibinputHandle() ? libinput_device_config_accel_get_default_speed(m->aq()->getLibinputHandle()) : 0.f), + m->m_scrollFactor.value_or(-1)); } result += "\n\nKeyboards:\n"; diff --git a/src/desktop/Window.cpp b/src/desktop/Window.cpp index 3c9520054..78f12a112 100644 --- a/src/desktop/Window.cpp +++ b/src/desktop/Window.cpp @@ -1249,6 +1249,14 @@ float CWindow::getScrollTouchpad() { return m_windowData.scrollTouchpad.valueOr(*PTOUCHPADSCROLLFACTOR); } +bool CWindow::isScrollMouseOverridden() { + return m_windowData.scrollMouse.hasValue(); +} + +bool CWindow::isScrollTouchpadOverridden() { + return m_windowData.scrollTouchpad.hasValue(); +} + bool CWindow::canBeTorn() { static auto PTEARING = CConfigValue("general:allow_tearing"); return m_windowData.tearing.valueOr(m_tearingHint) && *PTEARING; diff --git a/src/desktop/Window.hpp b/src/desktop/Window.hpp index e58d4fb66..9d94baead 100644 --- a/src/desktop/Window.hpp +++ b/src/desktop/Window.hpp @@ -363,6 +363,8 @@ class CWindow { int getRealBorderSize(); float getScrollMouse(); float getScrollTouchpad(); + bool isScrollMouseOverridden(); + bool isScrollTouchpadOverridden(); void updateWindowData(); void updateWindowData(const struct SWorkspaceRule&); void onBorderAngleAnimEnd(WP pav); diff --git a/src/devices/IPointer.hpp b/src/devices/IPointer.hpp index e5d76344c..197ba123c 100644 --- a/src/devices/IPointer.hpp +++ b/src/devices/IPointer.hpp @@ -108,11 +108,12 @@ class IPointer : public IHID { CSignalT holdEnd; } m_pointerEvents; - bool m_connected = false; // means connected to the cursor - std::string m_boundOutput = ""; - bool m_flipX = false; // decide to invert horizontal movement - bool m_flipY = false; // decide to invert vertical movement - bool m_isTouchpad = false; + bool m_connected = false; // means connected to the cursor + std::string m_boundOutput = ""; + bool m_flipX = false; // decide to invert horizontal movement + bool m_flipY = false; // decide to invert vertical movement + bool m_isTouchpad = false; + std::optional m_scrollFactor = {}; - WP m_self; + WP m_self; }; diff --git a/src/managers/PointerManager.cpp b/src/managers/PointerManager.cpp index 093c751dc..aeabe4120 100644 --- a/src/managers/PointerManager.cpp +++ b/src/managers/PointerManager.cpp @@ -919,8 +919,8 @@ void CPointerManager::attachPointer(SP pointer) { PROTO::idle->onActivity(); }); - listener->axis = pointer->m_pointerEvents.axis.listen([](const IPointer::SAxisEvent& event) { - g_pInputManager->onMouseWheel(event); + listener->axis = pointer->m_pointerEvents.axis.listen([weak = WP(pointer)](const IPointer::SAxisEvent& event) { + g_pInputManager->onMouseWheel(event, weak.lock()); PROTO::idle->onActivity(); }); diff --git a/src/managers/input/InputManager.cpp b/src/managers/input/InputManager.cpp index 5224f9407..5e844bec5 100644 --- a/src/managers/input/InputManager.cpp +++ b/src/managers/input/InputManager.cpp @@ -835,7 +835,7 @@ void CInputManager::processMouseDownKill(const IPointer::SButtonEvent& e) { m_clickBehavior = CLICKMODE_DEFAULT; } -void CInputManager::onMouseWheel(IPointer::SAxisEvent e) { +void CInputManager::onMouseWheel(IPointer::SAxisEvent e, SP pointer) { static auto POFFWINDOWAXIS = CConfigValue("input:off_window_axis_events"); static auto PINPUTSCROLLFACTOR = CConfigValue("input:scroll_factor"); static auto PTOUCHPADSCROLLFACTOR = CConfigValue("input:touchpad:scroll_factor"); @@ -845,7 +845,10 @@ void CInputManager::onMouseWheel(IPointer::SAxisEvent e) { const bool ISTOUCHPADSCROLL = *PTOUCHPADSCROLLFACTOR <= 0.f || e.source == WL_POINTER_AXIS_SOURCE_FINGER; auto factor = ISTOUCHPADSCROLL ? *PTOUCHPADSCROLLFACTOR : *PINPUTSCROLLFACTOR; - const auto EMAP = std::unordered_map{{"event", e}}; + if (pointer && pointer->m_scrollFactor.has_value()) + factor = *pointer->m_scrollFactor; + + const auto EMAP = std::unordered_map{{"event", e}}; EMIT_HOOK_EVENT_CANCELLABLE("mouseAxis", EMAP); if (e.mouse) @@ -888,7 +891,11 @@ void CInputManager::onMouseWheel(IPointer::SAxisEvent e) { if (*PFOLLOWMOUSE == 1 && PCURRWINDOW && PWINDOW != PCURRWINDOW) simulateMouseMovement(); } - factor = ISTOUCHPADSCROLL ? PWINDOW->getScrollTouchpad() : PWINDOW->getScrollMouse(); + + if (!ISTOUCHPADSCROLL && PWINDOW->isScrollMouseOverridden()) + factor = PWINDOW->getScrollMouse(); + else if (ISTOUCHPADSCROLL && PWINDOW->isScrollTouchpadOverridden()) + factor = PWINDOW->getScrollTouchpad(); } } @@ -1117,6 +1124,14 @@ void CInputManager::newVirtualMouse(SP mouse) { Debug::log(LOG, "New virtual mouse created"); } +void CInputManager::newMouse(SP mouse) { + m_pointers.emplace_back(mouse); + + setupMouse(mouse); + + Debug::log(LOG, "New mouse created, pointer Hypr: {:x}", rc(mouse.get())); +} + void CInputManager::newMouse(SP mouse) { const auto PMOUSE = m_pointers.emplace_back(CMouse::create(mouse)); @@ -1172,6 +1187,11 @@ void CInputManager::setPointerConfigs() { } } + if (g_pConfigManager->deviceConfigExplicitlySet(devname, "scroll_factor")) + m->m_scrollFactor = std::clamp(g_pConfigManager->getDeviceFloat(devname, "scroll_factor", "input:scroll_factor"), 0.F, 100.F); + else + m->m_scrollFactor = std::nullopt; + if (m->aq() && m->aq()->getLibinputHandle()) { const auto LIBINPUTDEV = m->aq()->getLibinputHandle(); diff --git a/src/managers/input/InputManager.hpp b/src/managers/input/InputManager.hpp index 56751cf53..60ff49ef7 100644 --- a/src/managers/input/InputManager.hpp +++ b/src/managers/input/InputManager.hpp @@ -90,13 +90,14 @@ class CInputManager { void onMouseMoved(IPointer::SMotionEvent); void onMouseWarp(IPointer::SMotionAbsoluteEvent); void onMouseButton(IPointer::SButtonEvent); - void onMouseWheel(IPointer::SAxisEvent); + void onMouseWheel(IPointer::SAxisEvent, SP pointer = nullptr); void onKeyboardKey(const IKeyboard::SKeyEvent&, SP); void onKeyboardMod(SP); void newKeyboard(SP); void newKeyboard(SP); void newVirtualKeyboard(SP); + void newMouse(SP); void newMouse(SP); void newVirtualMouse(SP); void newTouchDevice(SP);