git.lucas.co / cce-ui
GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git

commit41a5c0673933cb91312d5b54fb888f33b5dfd4d6
parent65cfaab7bd
authorLucas Galante <[email protected]>
date2026-08-16 22:52
feat: Application::utility() — declare a content-shaped window

A default-false trait method (the layer() idiom — WindowSettings has
52 literal constructions across the workspace, so no new field). When
true, the runner binds zcce_window_manager_v1 at exactly version 5 and
sends set_utility before the initial commit, so the mode is set before
the compositor maps or restores the window; on an older compositor the
bind fails and the app runs as a plain floating window, with a warning
instead of a protocol error.

cce-relief turns it on — the window whose shape IS content_height():
it previously asked for ~896px and got back the 520x480 it was last
left at, collapsing the cutaway onto its 90px floor. Verified in a
shadow session mapping at its own 520x913 with mode=Utility.

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

 protocol/cce-window-management-v1.xml | 19 ++++++-
 src/backend/window_runner.rs          | 99 +++++++++++++++++++++++++++++++++++
 src/bin/cce-relief.rs                 |  7 +++
 3 files changed, 123 insertions(+), 2 deletions(-)

diff --git a/protocol/cce-window-management-v1.xml b/protocol/cce-window-management-v1.xml
index ad7d32d..5e998ab 100644
--- a/protocol/cce-window-management-v1.xml
+++ b/protocol/cce-window-management-v1.xml
@@ -28,7 +28,7 @@
     "should", "should not", "recommended", "may", and "optional" in this
     document are to be interpreted as described in IETF RFC 2119.
   </description>
-  <interface name="zcce_window_manager_v1" version="4">
+  <interface name="zcce_window_manager_v1" version="5">
     <description summary="window manager global interface">
       This global interface should only be advertised to the window manager
       process. Only one window management client may be active at a time. The
@@ -1490,7 +1490,7 @@
       </description>
     </event>
   </interface>
-  <interface name="zcce_toplevel_v1" version="2">
+  <interface name="zcce_toplevel_v1" version="3">
     <description summary="toplevel window management controls">
       An interface to control and listen to CCE-specific window management states
       for a client surface.
@@ -1533,6 +1533,21 @@
         Request the compositor to return the window to normal tiling mode.
       </description>
     </request>
+    <request name="set_utility" since="3">
+      <description summary="request utility mode">
+        Declare the window a utility window: a tool whose shape is decided by
+        its contents. The compositor stops dictating a size to it (every
+        configure carries the "you choose" 0x0), offers no resize affordance,
+        and never saves geometry for it. The window still floats and moves
+        like an ordinary window. The declaration is explicit by design — the
+        compositor never infers utility mode from size hints.
+      </description>
+    </request>
+    <request name="unset_utility" since="3">
+      <description summary="unset utility mode">
+        Request the compositor to return the window to normal tiling mode.
+      </description>
+    </request>
     <event name="floating_state">
       <description summary="floating status event">
         Sent by the compositor to inform the client of its current floating status.
diff --git a/src/backend/window_runner.rs b/src/backend/window_runner.rs
index b320dff..509a502 100644
--- a/src/backend/window_runner.rs
+++ b/src/backend/window_runner.rs
@@ -2718,6 +2718,18 @@ pub trait Application: Sized + 'static {
     fn layer(&self) -> Option<LayerSettings> {
         None
     }
+    /// Declare the window a UTILITY window: a tool whose shape is decided by
+    /// its contents. The compositor then never dictates a size to it (every
+    /// configure is the "you choose" 0x0 — [`WindowSettings::width`]/`height`
+    /// become the surface's own initial size), offers no resize affordance
+    /// (the whole border band moves the window), and never saves geometry
+    /// for it, so a stale remembered size can't be restored over what the
+    /// app asks for. Declared over the cce window-management protocol at
+    /// window creation; on a compositor too old to know the request this is
+    /// silently a plain floating window. Defaults to `false`.
+    fn utility(&self) -> bool {
+        false
+    }
     fn update(&mut self, msg: Self::Message, needs_rebuild: &mut bool, exit: &mut bool);
     fn tick(&mut self, dt: f32, needs_rebuild: &mut bool);
     /// On-top overlay quads drawn after the display list and its text (e.g. the status bar's
@@ -3023,6 +3035,9 @@ pub struct EngineState<A: Application> {
     pub just_configured: bool,
     pub pointer_gestures: Option<ZwpPointerGesturesV1>,
     pub pinch_gesture: Option<ZwpPointerGesturePinchV1>,
+    /// The cce window-management toplevel handle, held for the window's
+    /// lifetime once [`Application::utility`] declared the mode.
+    pub cce_toplevel: Option<crate::protocol::cce_window_management_v1::zcce_toplevel_v1::ZcceToplevelV1>,
     pub last_pinch_scale: f32,
     pub cursor_pos: (f32, f32),
     /// Serial of the most recent pointer press, kept for
@@ -4280,6 +4295,71 @@ impl<A: Application> wayland_client::Dispatch<crate::protocol::zcce_inspector_v1
     ) {}
 }
 
+impl<A: Application> wayland_client::Dispatch<crate::protocol::cce_window_management_v1::zcce_window_manager_v1::ZcceWindowManagerV1, ()> for EngineState<A> {
+    fn event(
+        _state: &mut Self,
+        _proxy: &crate::protocol::cce_window_management_v1::zcce_window_manager_v1::ZcceWindowManagerV1,
+        _event: crate::protocol::cce_window_management_v1::zcce_window_manager_v1::Event,
+        _data: &(),
+        _conn: &Connection,
+        _qh: &QueueHandle<Self>,
+    ) {}
+
+    wayland_client::event_created_child!(
+        EngineState<A>,
+        crate::protocol::cce_window_management_v1::zcce_window_manager_v1::ZcceWindowManagerV1,
+        [
+            6 => (crate::protocol::cce_window_management_v1::zcce_window_v1::ZcceWindowV1, ()),
+            7 => (crate::protocol::cce_window_management_v1::zcce_output_v1::ZcceOutputV1, ()),
+            8 => (crate::protocol::cce_window_management_v1::zcce_seat_v1::ZcceSeatV1, ()),
+        ]
+    );
+}
+
+impl<A: Application> wayland_client::Dispatch<crate::protocol::cce_window_management_v1::zcce_window_v1::ZcceWindowV1, ()> for EngineState<A> {
+    fn event(
+        _state: &mut Self,
+        _proxy: &crate::protocol::cce_window_management_v1::zcce_window_v1::ZcceWindowV1,
+        _event: crate::protocol::cce_window_management_v1::zcce_window_v1::Event,
+        _data: &(),
+        _conn: &Connection,
+        _qh: &QueueHandle<Self>,
+    ) {}
+}
+
+impl<A: Application> wayland_client::Dispatch<crate::protocol::cce_window_management_v1::zcce_output_v1::ZcceOutputV1, ()> for EngineState<A> {
+    fn event(
+        _state: &mut Self,
+        _proxy: &crate::protocol::cce_window_management_v1::zcce_output_v1::ZcceOutputV1,
+        _event: crate::protocol::cce_window_management_v1::zcce_output_v1::Event,
+        _data: &(),
+        _conn: &Connection,
+        _qh: &QueueHandle<Self>,
+    ) {}
+}
+
+impl<A: Application> wayland_client::Dispatch<crate::protocol::cce_window_management_v1::zcce_seat_v1::ZcceSeatV1, ()> for EngineState<A> {
+    fn event(
+        _state: &mut Self,
+        _proxy: &crate::protocol::cce_window_management_v1::zcce_seat_v1::ZcceSeatV1,
+        _event: crate::protocol::cce_window_management_v1::zcce_seat_v1::Event,
+        _data: &(),
+        _conn: &Connection,
+        _qh: &QueueHandle<Self>,
+    ) {}
+}
+
+impl<A: Application> wayland_client::Dispatch<crate::protocol::cce_window_management_v1::zcce_toplevel_v1::ZcceToplevelV1, ()> for EngineState<A> {
+    fn event(
+        _state: &mut Self,
+        _proxy: &crate::protocol::cce_window_management_v1::zcce_toplevel_v1::ZcceToplevelV1,
+        _event: crate::protocol::cce_window_management_v1::zcce_toplevel_v1::Event,
+        _data: &(),
+        _conn: &Connection,
+        _qh: &QueueHandle<Self>,
+    ) {}
+}
+
 delegate_compositor!(@<A: Application> EngineState<A>);
 delegate_xdg_shell!(@<A: Application> EngineState<A>);
 delegate_xdg_window!(@<A: Application> EngineState<A>);
@@ -4578,6 +4658,7 @@ fn run_session<'l, A: Application>(
         just_configured: false,
         pointer_gestures,
         pinch_gesture: None,
+        cce_toplevel: None,
         last_pinch_scale: 1.0,
         cursor_pos: (0.0, 0.0),
         last_press_serial: None,
@@ -4652,6 +4733,24 @@ fn run_session<'l, A: Application>(
         if let Some((min_w, min_h)) = settings.min_size {
             window.set_min_size(Some((min_w, min_h)));
         }
+        if engine_state.inner.as_ref().unwrap().utility() {
+            // Declared BEFORE the initial commit so the mode is set by the
+            // time the compositor maps (and would otherwise restore) the
+            // window. Manager version 5 is where set_utility appeared; on an
+            // older compositor the declaration is skipped and the app runs
+            // as a plain floating window rather than dying on an unknown
+            // opcode.
+            match globals.bind::<crate::protocol::cce_window_management_v1::zcce_window_manager_v1::ZcceWindowManagerV1, _, _>(&qh, 5..=5, ()) {
+                Ok(cce_wm) => {
+                    let toplevel = cce_wm.get_cce_toplevel(&surface, &qh, ());
+                    toplevel.set_utility();
+                    engine_state.cce_toplevel = Some(toplevel);
+                }
+                Err(e) => {
+                    log::warn!("[window_runner] utility window declaration unavailable: {e}");
+                }
+            }
+        }
         window.commit();
         engine_state.window = Some(window);
     }
diff --git a/src/bin/cce-relief.rs b/src/bin/cce-relief.rs
index 89e019a..3ea53d3 100644
--- a/src/bin/cce-relief.rs
+++ b/src/bin/cce-relief.rs
@@ -1211,6 +1211,13 @@ impl Application for BevelPopup {
         }
     }
 
+    /// The motivating case for the mode: this window's shape IS
+    /// `content_height`, so nothing — not a drag, not a remembered size —
+    /// should ever dictate a different one.
+    fn utility(&self) -> bool {
+        true
+    }
+
     fn update(&mut self, msg: Self::Message, _needs_rebuild: &mut bool, exit: &mut bool) {
         match msg {
             BevelMsg::Exit => *exit = true,