From ff76e96786cbdd0f586bc9c97e88a414635f2365 Mon Sep 17 00:00:00 2001 From: Adam Druzd Date: Sun, 17 May 2026 09:12:08 +0200 Subject: [PATCH 1/2] fix(hyprland): keep workspace hover over taskbar icons --- include/modules/hyprland/workspace.hpp | 11 +++ src/modules/hyprland/workspace.cpp | 102 +++++++++++++++++++++++++ 2 files changed, 113 insertions(+) diff --git a/include/modules/hyprland/workspace.hpp b/include/modules/hyprland/workspace.hpp index 1d80d331..5730dda9 100644 --- a/include/modules/hyprland/workspace.hpp +++ b/include/modules/hyprland/workspace.hpp @@ -45,6 +45,15 @@ class Workspace { bool isUrgent() const { return m_isUrgent; }; bool handleClicked(GdkEventButton* bt) const; + + bool handleEnter(GdkEventCrossing* event); + bool handleLeave(GdkEventCrossing* event); + + void startHoverCheck(); + void stopHoverCheck(); + bool syncHoverClass(); + bool pointerInsideButton(); + void setActive(bool value = true) { m_isActive = value; }; void setPersistentRule(bool value = true) { m_isPersistentRule = value; }; void setPersistentConfig(bool value = true) { m_isPersistentConfig = value; }; @@ -80,6 +89,8 @@ class Workspace { bool m_isUrgent = false; bool m_isVisible = false; + sigc::connection m_hoverCheckConnection; + std::vector m_windowMap; Gtk::Button m_button; diff --git a/src/modules/hyprland/workspace.cpp b/src/modules/hyprland/workspace.cpp index 753893f2..67ef4506 100644 --- a/src/modules/hyprland/workspace.cpp +++ b/src/modules/hyprland/workspace.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include @@ -30,6 +31,11 @@ Workspace::Workspace(const Json::Value& workspace_data, Workspaces& workspace_ma } m_button.add_events(Gdk::BUTTON_PRESS_MASK); + m_button.add_events(Gdk::ENTER_NOTIFY_MASK | Gdk::LEAVE_NOTIFY_MASK); + + m_button.signal_enter_notify_event().connect(sigc::mem_fun(*this, &Workspace::handleEnter)); + m_button.signal_leave_notify_event().connect(sigc::mem_fun(*this, &Workspace::handleLeave)); + m_button.signal_button_press_event().connect(sigc::mem_fun(*this, &Workspace::handleClicked), false); @@ -66,6 +72,102 @@ std::optional Workspace::closeWindow(WindowAddress const& addr) { return std::nullopt; } +bool Workspace::pointerInsideButton() { + auto display = Gdk::Display::get_default(); + if (!display) { + return false; + } + + auto seat = display->get_default_seat(); + if (!seat) { + return false; + } + + auto pointer = seat->get_pointer(); + if (!pointer) { + return false; + } + + Glib::RefPtr screen; + int pointerRootX = 0; + int pointerRootY = 0; + + pointer->get_position(screen, pointerRootX, pointerRootY); + + Gtk::Widget* toplevel = m_button.get_toplevel(); + if (toplevel == nullptr || !toplevel->get_window()) { + return false; + } + + int buttonX = 0; + int buttonY = 0; + + if (!m_button.translate_coordinates(*toplevel, 0, 0, buttonX, buttonY)) { + return false; + } + + int windowRootX = 0; + int windowRootY = 0; + toplevel->get_window()->get_root_origin(windowRootX, windowRootY); + + const auto allocation = m_button.get_allocation(); + + const int buttonRootX = windowRootX + buttonX; + const int buttonRootY = windowRootY + buttonY; + const int buttonWidth = allocation.get_width(); + const int buttonHeight = allocation.get_height(); + + return pointerRootX >= buttonRootX && pointerRootY >= buttonRootY && + pointerRootX < buttonRootX + buttonWidth && + pointerRootY < buttonRootY + buttonHeight; +} + +bool Workspace::syncHoverClass() { + auto styleContext = m_button.get_style_context(); + + if (pointerInsideButton()) { + styleContext->add_class("workspace-hover"); + return true; + } + + styleContext->remove_class("workspace-hover"); + stopHoverCheck(); + return false; +} + +void Workspace::startHoverCheck() { + if (m_hoverCheckConnection.connected()) { + return; + } + + m_hoverCheckConnection = Glib::signal_timeout().connect( + sigc::mem_fun(*this, &Workspace::syncHoverClass), + 50); +} + +void Workspace::stopHoverCheck() { + if (m_hoverCheckConnection.connected()) { + m_hoverCheckConnection.disconnect(); + } +} + +bool Workspace::handleEnter(GdkEventCrossing* /*event*/) { + m_button.get_style_context()->add_class("workspace-hover"); + startHoverCheck(); + return false; +} + +bool Workspace::handleLeave(GdkEventCrossing* /*event*/) { + /* + * Do not remove immediately. + * Workspace taskbar children can fire misleading leave events while the + * pointer is still visually inside the workspace button. + * + * The polling check will remove the class once the pointer really leaves. + */ + startHoverCheck(); + return false; +} bool Workspace::handleClicked(GdkEventButton* bt) const { if (bt->type == GDK_BUTTON_PRESS) { try { From dc017d8fd3e2550fb397627bea9981c6ec707ab7 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 3 Jul 2026 21:32:55 +0200 Subject: [PATCH 2/2] fix(hyprland/workspaces): disconnect hover-check timeout on Workspace destruction The hover-check Glib::signal_timeout re-arms every 50ms and is not owned by the Workspace. Since Workspaces are destroyed at runtime, a workspace removed while its check is armed would let the timeout fire on freed memory (use-after- free, also touching the destroyed m_button). Add a destructor that calls stopHoverCheck() to disconnect the source. --- include/modules/hyprland/workspace.hpp | 1 + src/modules/hyprland/workspace.cpp | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/include/modules/hyprland/workspace.hpp b/include/modules/hyprland/workspace.hpp index 5730dda9..a4cccee6 100644 --- a/include/modules/hyprland/workspace.hpp +++ b/include/modules/hyprland/workspace.hpp @@ -30,6 +30,7 @@ class Workspace { public: explicit Workspace(const Json::Value& workspace_data, Workspaces& workspace_manager, const Json::Value& clients_data = Json::Value::nullRef); + ~Workspace(); std::string& selectIcon(std::map& icons_map); Gtk::Button& button() { return m_button; }; diff --git a/src/modules/hyprland/workspace.cpp b/src/modules/hyprland/workspace.cpp index e31132fe..b8868236 100644 --- a/src/modules/hyprland/workspace.cpp +++ b/src/modules/hyprland/workspace.cpp @@ -51,6 +51,12 @@ Workspace::Workspace(const Json::Value& workspace_data, Workspaces& workspace_ma initializeWindowMap(clients_data); } +Workspace::~Workspace() { + // Disconnect the hover-check timeout so it can't fire on this destroyed + // instance (Workspaces are removed at runtime while a check may be armed). + stopHoverCheck(); +} + void addOrRemoveClass(const Glib::RefPtr& context, bool condition, const std::string& class_name) { if (condition) {