diff --git a/meson.build b/meson.build index 7f14591..5000842 100644 --- a/meson.build +++ b/meson.build @@ -324,6 +324,7 @@ singularity_core_sources = files( 'src/components/sidebar/pages/performance_page.vala', 'src/core/display_manager.vala', 'src/core/hot_corner_manager.vala', + 'src/core/layer_window.vala', 'src/core/monitor_osd.vala', 'src/core/system_components.vala', 'src/core/debug_manager.vala', diff --git a/src/components/dock/dock.vala b/src/components/dock/dock.vala index 39f2fec..669cde6 100644 --- a/src/components/dock/dock.vala +++ b/src/components/dock/dock.vala @@ -241,7 +241,7 @@ namespace Singularity { } if (key == "panel-fusion") { update_fusion(); - ((Gtk.Widget) this).hide(); + close_layer_window (this); update_visibility_mode(); pulse_frame_clock(); } @@ -257,7 +257,7 @@ namespace Singularity { if (key == "dock-enabled") { _enabled = _settings.get_boolean("dock-enabled"); if (!_enabled) { - ((Gtk.Widget) this).hide(); + close_layer_window (this); set_exclusive_zone(this, 0); app_system.shell_dock_height = 0; _set_reveal_barrier_active(false); @@ -927,7 +927,7 @@ namespace Singularity { queue_draw(); break; case "remap": - hide(); + close_layer_window (this); present(); queue_draw(); break; @@ -1036,7 +1036,7 @@ namespace Singularity { // is live right after the remap (and we pulse it), so the content // transform is presented reliably without moving the surface. set_body_class("dock-reveal-offset", true); - ((Gtk.Widget) this).hide(); + close_layer_window (this); present(); start_content_slide(); } @@ -1140,7 +1140,7 @@ namespace Singularity { return; } if (!_enabled) { - ((Gtk.Widget) this).hide(); + close_layer_window (this); set_exclusive_zone(this, 0); app_system.shell_dock_height = 0; _set_reveal_barrier_active(false); @@ -1157,7 +1157,7 @@ namespace Singularity { // Re-trigger size_allocate to restore the correct exclusive zone queue_resize(); } else if (visibility_mode == "overview-only") { - hide(); + close_layer_window (this); set_exclusive_zone(this, 0); } update_autohide_state(); @@ -1207,7 +1207,7 @@ namespace Singularity { // window covering it) is not composited until a frame is // committed, so closing a focused fullscreen window left the // dock buried. Remap to force a fresh buffer and present. - ((Gtk.Widget) this).hide(); + close_layer_window (this); present(); update_visibility_mode(); pulse_frame_clock(); diff --git a/src/components/overview/app_launcher_grid.vala b/src/components/overview/app_launcher_grid.vala index 5125236..d9c2820 100644 --- a/src/components/overview/app_launcher_grid.vala +++ b/src/components/overview/app_launcher_grid.vala @@ -377,9 +377,22 @@ namespace Singularity { return GLib.Source.CONTINUE; } - public void depopulate() { + /** + * Dismiss any open folder overlay without tearing down the grid. + * + * A folder overlay is its own toplevel layer-shell window, so hiding + * the launcher that spawned it leaves it on screen. Closing the + * overlays is cheap; dropping the grid contents is not, which is why + * depopulate() stays on its idle timer and this can be called + * immediately on close. + */ + public void close_folder_overlays() { _folder_overlays.foreach((id, ov) => ov.close_overlay()); _folder_overlays.remove_all(); + } + + public void depopulate() { + close_folder_overlays(); Widget? c = grid.get_first_child(); while (c != null) { Widget nc = c.get_next_sibling(); grid.remove(c); c = nc; } } diff --git a/src/components/overview/app_menu.vala b/src/components/overview/app_menu.vala index 73a1d47..94196e5 100644 --- a/src/components/overview/app_menu.vala +++ b/src/components/overview/app_menu.vala @@ -158,7 +158,7 @@ namespace Singularity { }); ((Gtk.Widget)this).add_controller(key_controller); - hide(); + close_layer_window (this); } private void apply_monitor_sizing() { @@ -269,7 +269,7 @@ namespace Singularity { menu_animation.tick.connect(() => { opacity = menu_animation.value; }); menu_animation.done.connect(() => { if (_is_open) return; - hide(); + close_layer_window (this); if (widgets_grid != null) widgets_grid.depopulate(); hidden(); }); @@ -333,7 +333,7 @@ namespace Singularity { opacity = target; if (stay_open) search_entry.grab_focus(); else { - hide(); + close_layer_window (this); if (widgets_grid != null) widgets_grid.depopulate(); hidden(); } @@ -353,7 +353,7 @@ namespace Singularity { if (menu_animation == animation) menu_animation = null; if (stay_open) search_entry.grab_focus(); else { - hide(); + close_layer_window (this); if (widgets_grid != null) widgets_grid.depopulate(); hidden(); } diff --git a/src/components/overview/overview.vala b/src/components/overview/overview.vala index ef3c3be..5d68694 100644 --- a/src/components/overview/overview.vala +++ b/src/components/overview/overview.vala @@ -333,7 +333,13 @@ namespace Singularity { _anim_out_timer = GLib.Timeout.add(180, () => { _anim_out_timer = 0; main_box.remove_css_class("animating-out"); - hide(); + close_layer_window (this); + // A folder overlay is a separate toplevel layer window, so + // it does not go away with the launcher. Dismiss it now -- + // waiting for the idle depopulate below left it on screen + // for IDLE_DEPOPULATE_MS, or indefinitely if the overview + // was reopened before that timer fired and cancelled it. + launcher_grid.close_folder_overlays(); PreviewCache.get_default().clear(); hidden(); // The overview just freed its grid widgets, icon textures @@ -433,7 +439,10 @@ namespace Singularity { } private void finish_gesture_hide() { - hide(); + close_layer_window (this); + // Same reason as the animated close path: the folder overlay is its + // own toplevel and does not go away with the launcher. + launcher_grid.close_folder_overlays(); PreviewCache.get_default().clear(); hidden(); Singularity.trim_heap(); diff --git a/src/components/overview/workspace_overview.vala b/src/components/overview/workspace_overview.vala index aea62a9..3f83515 100644 --- a/src/components/overview/workspace_overview.vala +++ b/src/components/overview/workspace_overview.vala @@ -135,7 +135,7 @@ namespace Singularity { wp_manager.wallpaper_changed.connect(update_wallpaper); update_wallpaper(); - hide(); + close_layer_window (this); } private void update_wallpaper() { @@ -420,7 +420,7 @@ namespace Singularity { _anim_out_timer = 0; opacity = 0; anim_box.remove_css_class("animating-out"); - hide(); + close_layer_window (this); // Free all window preview textures - they'll be re-captured on next open clear_overview_content(); hidden(); @@ -498,7 +498,7 @@ namespace Singularity { || Math.fabs(opacity - target) < 0.001) { opacity = target; if (!stay_open) { - hide(); + close_layer_window (this); clear_overview_content(); hidden(); } @@ -517,7 +517,7 @@ namespace Singularity { opacity = target; if (_gesture_animation == animation) _gesture_animation = null; if (!stay_open) { - hide(); + close_layer_window (this); clear_overview_content(); hidden(); } diff --git a/src/components/sidebar/sidebar.vala b/src/components/sidebar/sidebar.vala index ed0bddd..66f5c94 100644 --- a/src/components/sidebar/sidebar.vala +++ b/src/components/sidebar/sidebar.vala @@ -75,7 +75,7 @@ namespace Singularity { toggle_settings(); }); system_view.hide_sidebar.connect(() => { - hide(); + close_layer_window (this); }); system_view.open_settings_page.connect((page) => { open_page(page); @@ -90,7 +90,7 @@ namespace Singularity { main_stack.notify["visible-child-name"].connect(() => update_vertical_anchor()); update_vertical_anchor(); - hide(); + close_layer_window (this); // Close on Escape key var key_controller = new Gtk.EventControllerKey(); @@ -98,7 +98,7 @@ namespace Singularity { if (keyval == Gdk.Key.Escape) { if (!Singularity.DebugManager.get_default().sidebar_pinned) { desktop_settings.set_boolean("bar-layout-edit-mode", false); - hide(); + close_layer_window (this); } return true; } @@ -133,7 +133,7 @@ namespace Singularity { _can_close_on_focus_loss = false; opacity = 1; GtkLayerShell.set_margin(this, GtkLayerShell.Edge.RIGHT, 40); - hide(); + close_layer_window (this); return; } // Null before skip so old done handler sees slide_animation != old_anim @@ -155,7 +155,7 @@ namespace Singularity { slide_animation = null; _is_closing = false; _can_close_on_focus_loss = false; - hide(); + close_layer_window (this); } }); anim.play(); diff --git a/src/core/hot_corner_manager.vala b/src/core/hot_corner_manager.vala index e00b3be..44f182c 100644 --- a/src/core/hot_corner_manager.vala +++ b/src/core/hot_corner_manager.vala @@ -224,7 +224,7 @@ namespace Singularity { hint.add_overlay(badge); set_child(hint); - hide(); + close_layer_window (this); } public void show_hint(string action) { @@ -235,7 +235,7 @@ namespace Singularity { public void hide_hint() { hint.remove_css_class("visible"); - hide(); + close_layer_window (this); } private static string icon_for_action(string? action) { diff --git a/src/core/layer_window.vala b/src/core/layer_window.vala new file mode 100644 index 0000000..cf2ae73 --- /dev/null +++ b/src/core/layer_window.vala @@ -0,0 +1,37 @@ +using Gtk; + +namespace Singularity { + + /** + * Hide a layer-shell window and drop its GdkSurface. + * + * GTK keeps one wl_surface alive across hide/show, while gtk4-layer-shell + * destroys the zwlr_layer_surface_v1 at unmap and creates a NEW one over + * that same wl_surface on the next open. wl_surface state is persistent, so + * a frame that lands after the unmap leaves a buffer attached, and creating + * a layer surface over a surface that still holds a buffer is a client + * error the compositor answers by killing the connection: + * + * Gdk-Message: Error 71 (Protocol error) dispatching to Wayland display + * zwlr_layer_surface_v1 has never been configured + * + * Whether the stray frame lands is a race on wl_buffer.release, which is + * why a window would survive a variable number of open/close cycles before + * dying. Dropping the GdkSurface means the next open allocates a fresh + * wl_surface that cannot carry a stale buffer. + * + * This is the same sequence gtk4-layer-shell performs internally in + * gtk_layer_surface_remap(), so it is a supported, exercised path. + * unrealize() on a never-realized widget is a documented no-op, which makes + * this safe to call from a constructor-time hide. + * + * The cast matters: GtkWindow implements GtkNative, so a bare unrealize() + * binds to gtk_native_unrealize -- an internal vfunc -- rather than + * gtk_widget_unrealize. Verified by inspecting the C that valac emits for + * each spelling. + */ + public void close_layer_window (Gtk.Window window) { + ((Gtk.Widget) window).hide (); + ((Gtk.Widget) window).unrealize (); + } +} diff --git a/src/core/main.vala b/src/core/main.vala index d8f4f4a..dd78544 100644 --- a/src/core/main.vala +++ b/src/core/main.vala @@ -760,7 +760,16 @@ public class SingularityApp : Singularity.ShellApplication, Singularity.Shell.Sh private void sync_bar_layout_edit_mode() { if (settings.get_boolean("bar-layout-edit-mode")) { - sidebar?.hide(); + if (sidebar != null) { + sidebar.hide(); + // Drop the GdkSurface here too. This is the one close path that + // lives OUTSIDE Sidebar, so the in-class resets do not cover it, + // and re-opening via a panel or dock toggle calls present() on + // the old surface -- the same stale-buffer fault. + // Cast required: GtkWindow implements GtkNative, so a bare + // unrealize() binds to gtk_native_unrealize, not the widget one. + ((Gtk.Widget) sidebar).unrealize(); + } settings_window?.hide(); if (bar_layout_edit_overlay == null) { bar_layout_edit_overlay = new Singularity.BarLayoutEditOverlay(this);