git.lucas.co / cce-status-interface
status bar
git clone https://git.lucas.co/cce-status-interface.git

commit9465cdf2b1c4e8c261826b6d9348a4d229e61681
parent4456e7f478
authorLucas Galante <[email protected]>
date2026-08-06 09:49
feat: close in-surface menu on click-away (compositor dismiss push)

Subscribe every module process to the status socket's new one-shot
'dismiss' topic. The compositor pushes a line whenever a pointer press
lands anywhere other than an expanded status segment; the payload is
the pressed segment's app_id, so a segment ignores dismisses naming
itself (its own input handler already dealt with that press). The
menu closes through the normal contraction animation.

Co-Authored-By: Claude Fable 5 <[email protected]>

 src/listeners.rs |  3 +++
 src/main.rs      | 19 +++++++++++++++++++
 2 files changed, 22 insertions(+)

diff --git a/src/listeners.rs b/src/listeners.rs
index 01a0d3b..33921fe 100644
--- a/src/listeners.rs
+++ b/src/listeners.rs
@@ -38,6 +38,9 @@ pub(crate) async fn spawn_status_listener(sub: &'static str, sender: calloop::ch
                             "viewport" => CustomEvent::ViewportUpdated(val.clone()),
                             "layout" => CustomEvent::LayoutUpdated(val.clone()),
                             "title" => CustomEvent::TitleUpdated(val.clone()),
+                            // Click-away-close: the payload is the app_id of
+                            // the segment the press landed on ("-" for none).
+                            "dismiss" => CustomEvent::MenuDismiss(val.clone()),
                             _ => unreachable!(),
                         };
                         let _ = sender.send(ev);
diff --git a/src/main.rs b/src/main.rs
index 3f5d1bd..7b92402 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -119,6 +119,10 @@ pub(crate) enum CustomEvent {
     /// A bar-built in-surface menu (window picker), fetched off-thread.
     MenuReady { title: String, pages: Vec<MenuPage>, min_w: f32 },
     SwitcherTriggered,
+    /// Compositor click-away-close: a pointer press landed somewhere other
+    /// than this expanded segment. Payload = the pressed segment's app_id
+    /// ("-" for none); a segment ignores a dismiss naming itself.
+    MenuDismiss(String),
     /// A tray icon's DBusMenu, fetched and flattened for the in-surface menu.
     TrayMenuFetched { destination: String, menu_path: String, pages: Vec<MenuPage> },
     ToggleHideModules,
@@ -1170,6 +1174,9 @@ impl cce_ui::engine::Application for StatusApp {
             tokio::spawn(spawn_status_listener("layout", sender.clone()));
             tokio::spawn(spawn_status_listener("title", sender.clone()));
         }
+        // Every module can host an in-surface menu, so every process listens
+        // for the compositor's click-away dismiss pushes.
+        tokio::spawn(spawn_status_listener("dismiss", sender.clone()));
         let is_primary_for_switcher = selected_module.as_ref().map_or(true, |(name, _)| name == "window");
         if is_primary_for_switcher {
             tokio::spawn(spawn_switcher_listener(sender.clone()));
@@ -1338,6 +1345,18 @@ impl cce_ui::engine::Application for StatusApp {
                 log::debug!("[switcher] SwitcherTriggered event received, calling trigger_switcher");
                 self.trigger_switcher(true);
             }
+            CustomEvent::MenuDismiss(pressed_app_id) => {
+                // Close-on-click-away, unless the press was on THIS segment
+                // (then handle_mouse_input already decided what to do).
+                if self.context_menu.is_some()
+                    && !self.menu_closing
+                    && pressed_app_id != self.get_app_id()
+                {
+                    self.menu_closing = true;
+                } else {
+                    changed = false;
+                }
+            }
             CustomEvent::ToggleHideModules => {
                 self.status_hide_mode = !self.status_hide_mode;
                 let cmd = if self.status_hide_mode { "true" } else { "false" };