Skip to content

Stuck Hover/Active state on buttons when dismissing Gtk Popovers #2963

@killown

Description

@killown

Wayfire version

0.11.0-3c9a7712 (Jan 22 2026) branch master wlroots-0.19.2

Describe the bug

First I thought it was a bug in pygobject then I tested in gnome and some compositors which didnt present the same issue, so it seems related to Wayfire.

When using a button to open a menu (the Gtk4 Popover), the system correctly manages hover states under normal conditions: if you move the mouse away from the button, the highlight disappears, and if you click the button a second time to close the menu, the state resets properly.

The bug occurs specifically when you click any area outside both the button and the popover (such as clicking into a web browser or onto the desktop) to dismiss the menu. In this case, the menu closes instantly, but the button stays highlighted in its "Hover" or "Active" state even though the mouse is no longer near it.

Steps to Reproduce:

Open the popover.
Click in any view outside the popover

import sys
import gi

gi.require_version("Gtk", "4.0")
gi.require_version("Gtk4LayerShell", "1.0")
from gi.repository import Gtk, Gdk, Gtk4LayerShell, GLib

CSS_DATA = b"""
.reporting-button {
    background-color: #242424;
    color: white;
    padding: 10px;
    border: 2px solid transparent;
    transition: all 200ms;
}

/* Very visible hover state */
.reporting-button:hover {
    background-color: #005fb8;
    border-color: #00aff0;
}

/* Extreme visibility for the active/stuck state */
.reporting-button:active, 
.reporting-button:checked,
.reporting-button:hovered-stuck { 
    background-color: #ff0055 !format;
    color: white;
    border: 4px solid #ffffff;
    box-shadow: 0 0 20px #ff0055;
}

/* GTK internal state mapping */
button:hover { background: #005fb8; }
button:active { background: #ff0055; }
"""

class LayerShellReproduction(Gtk.Application):
    def __init__(self):
        super().__init__(application_id="org.waypanel.report.stuckstate")

    def do_activate(self):
        # Apply the CSS
        provider = Gtk.CssProvider()
        provider.load_from_data(CSS_DATA)
        Gtk.StyleContext.add_provider_for_display(
            Gdk.Display.get_default(),
            provider,
            Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
        )

        win = Gtk.ApplicationWindow(application=self)
        
        # Layer Shell Init
        Gtk4LayerShell.init_for_window(win)
        Gtk4LayerShell.set_layer(win, Gtk4LayerShell.Layer.TOP)
        Gtk4LayerShell.set_anchor(win, Gtk4LayerShell.Edge.TOP, True)
        Gtk4LayerShell.set_keyboard_mode(win, Gtk4LayerShell.KeyboardMode.ON_DEMAND)

        # The Button
        btn = Gtk.Button(label="REPRODUCE: CLICK THEN CLICK OUTSIDE")
        btn.add_css_class("reporting-button")
        btn.set_halign(Gtk.Align.CENTER)
        btn.set_margin_all(20)

        # The Popover
        popover = Gtk.Popover()
        popover.set_parent(btn)
        popover.set_child(Gtk.Label(label="Now click your Web Browser\nor any other window."))
        
        btn.connect("clicked", lambda _: popover.popup())

        # Console Monitor
        def monitor_flags():
            flags = btn.get_state_flags()
            hover = bool(flags & Gtk.StateFlags.HOVER)
            active = bool(flags & Gtk.StateFlags.ACTIVE)
            if hover or active:
                print(f"REPORT DATA - Hover: {hover}, Active: {active} (STUCK)")
            return True

        GLib.timeout_add(500, monitor_flags)

        win.set_child(btn)
        win.present()

if __name__ == "__main__":
    app = LayerShellReproduction()
    app.run(sys.argv)

Screenshots / Videos / Stacktrace

whatisgoingon.mp4

Additional context

[workarounds]
app_id_mode = "stock"
all_dialogs_modal = true
dynamic_repaint_delay = false
disable_primary_selection = false
discard_command_output = true
enable_input_method_v2 = false
enable_opaque_region_damage_optimizations = false
enable_so_unloading = false
focus_main_surface_instead_of_popup = false
force_frame_sync = false
force_preferred_decoration_mode = false
max_buffer_size = 16000
remove_output_limits = false
use_external_output_configuration = false

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions