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

commita5c07bcc3aff8c67c731f1bca84bc8d59976b4c1
parenta594cc2f14
authorLucas Galante <[email protected]>
date2026-08-17 15:36
feat: grid-layer client support — Application::grid + grid_patch events

Protocol copy synced (manager v6, toplevel v4: set_grid, grid_patch,
ack_grid_patch). An app returning true from Application::grid() binds
manager v6 and declares its surface the desktop-grid layer; the runner
handles grid_patch events by resizing the surface to the patch's buffer
size, forwarding the patch to Application::grid_patch, and acking right
before the frame that renders it — so the commit the compositor latches
is the one drawn for that anchor. A newer patch supersedes an
unconsumed older one. On an older compositor the declaration is skipped
gracefully, like utility windows.

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

 protocol/cce-window-management-v1.xml | 48 +++++++++++++++++++++++--
 src/backend/window_runner.rs          | 67 +++++++++++++++++++++++++++++------
 2 files changed, 102 insertions(+), 13 deletions(-)

diff --git a/protocol/cce-window-management-v1.xml b/protocol/cce-window-management-v1.xml
index 5e998ab..1bcfd5b 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="5">
+  <interface name="zcce_window_manager_v1" version="6">
     <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="3">
+  <interface name="zcce_toplevel_v1" version="4">
     <description summary="toplevel window management controls">
       An interface to control and listen to CCE-specific window management states
       for a client surface.
@@ -1548,11 +1548,55 @@
         Request the compositor to return the window to normal tiling mode.
       </description>
     </request>
+    <request name="set_grid" since="4">
+      <description summary="declare the desktop-grid surface">
+        Declare this surface the desktop-grid layer. The compositor anchors
+        the surface to the virtual desktop: it is positioned and scaled with
+        the camera every frame, exactly like window content, so the client is
+        never part of the pan/zoom loop. The surface becomes
+        input-transparent, is stacked above the wallpaper and below all
+        windows, is excluded from focus, session save and the overview, and
+        the compositor stops dictating sizes to it.
+
+        The compositor tells the client WHAT to render via grid_patch
+        events; the surface stays unmapped-invisible until the first
+        acknowledged patch is committed.
+      </description>
+    </request>
+    <request name="ack_grid_patch" since="4">
+      <description summary="acknowledge a grid patch">
+        Acknowledge a grid_patch event. The next buffer the client commits
+        after this request is taken to be rendered for the acknowledged
+        patch: the compositor latches that patch's anchor rectangle at that
+        commit, so an in-flight older buffer is never shown at a new
+        anchor.
+      </description>
+      <arg name="serial" type="uint" summary="the serial from the grid_patch event"/>
+    </request>
     <event name="floating_state">
       <description summary="floating status event">
         Sent by the compositor to inform the client of its current floating status.
       </description>
       <arg name="state" type="uint" summary="1 if floating, 0 otherwise"/>
     </event>
+    <event name="grid_patch" since="4">
+      <description summary="render this patch of the desktop grid">
+        Instructs the grid client what region of the virtual desktop to
+        render, and at what resolution. x/y/width/height are virtual-surface
+        coordinates; scale is buffer pixels per virtual unit, so the
+        expected buffer size is (width * scale) x (height * scale). The
+        client renders the region, sends ack_grid_patch with the serial, and
+        commits the new buffer.
+
+        A newer grid_patch supersedes an unacknowledged older one; the
+        client should render only the latest.
+      </description>
+      <arg name="serial" type="uint" summary="serial to pass to ack_grid_patch"/>
+      <arg name="x" type="fixed" summary="patch origin x, virtual units"/>
+      <arg name="y" type="fixed" summary="patch origin y, virtual units"/>
+      <arg name="width" type="fixed" summary="patch width, virtual units"/>
+      <arg name="height" type="fixed" summary="patch height, virtual units"/>
+      <arg name="scale" type="fixed" summary="buffer px per virtual unit"/>
+    </event>
   </interface>
 </protocol>
diff --git a/src/backend/window_runner.rs b/src/backend/window_runner.rs
index 509a502..329a124 100644
--- a/src/backend/window_runner.rs
+++ b/src/backend/window_runner.rs
@@ -2730,6 +2730,22 @@ pub trait Application: Sized + 'static {
     fn utility(&self) -> bool {
         false
     }
+    /// Declare the window the DESKTOP-GRID layer (zcce set_grid): the
+    /// compositor world-anchors the surface to the virtual desktop and
+    /// pans/zooms it per frame like window content; the app renders only
+    /// when handed a patch (see [`Application::grid_patch`]). The surface
+    /// becomes input-transparent and lives behind all windows. Needs
+    /// manager v6; on an older compositor the declaration is skipped.
+    /// Defaults to `false`.
+    fn grid(&self) -> bool {
+        false
+    }
+    /// A grid patch to render (grid apps only): virtual origin (`x`, `y`),
+    /// virtual size (`w`, `h`), and `scale` surface px per virtual unit.
+    /// Called right before the frame that must show it; the runner has
+    /// already resized the surface to `(w*scale, h*scale)` and acks the
+    /// patch so the coming commit is latched at the new anchor.
+    fn grid_patch(&mut self, _x: f64, _y: f64, _w: f64, _h: f64, _scale: f64) {}
     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
@@ -3038,6 +3054,9 @@ pub struct EngineState<A: Application> {
     /// 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>,
+    /// Latest unrendered grid_patch (serial, x, y, w, h, scale) — a newer
+    /// event supersedes an unconsumed older one, per protocol.
+    pub pending_grid_patch: Option<(u32, f64, f64, f64, f64, f64)>,
     pub last_pinch_scale: f32,
     pub cursor_pos: (f32, f32),
     /// Serial of the most recent pointer press, kept for
@@ -3215,6 +3234,16 @@ impl<A: Application> EngineState<A> {
     }
 
     pub fn render(&mut self) {
+        // Grid patch: resize to the patch's buffer size, tell the app what
+        // world region this frame covers, and ack — the commit this render
+        // produces is the one the compositor latches at the new anchor.
+        if let Some((serial, px, py, pw, ph, pscale)) = self.pending_grid_patch.take() {
+            self.resize((pw * pscale) as f32, (ph * pscale) as f32);
+            self.inner.as_mut().unwrap().grid_patch(px, py, pw, ph, pscale);
+            if let Some(tl) = &self.cce_toplevel {
+                tl.ack_grid_patch(serial);
+            }
+        }
         let logical_w = self.logical_width;
         let logical_h = self.logical_height;
         let scale_factor = self.scale_factor;
@@ -4351,13 +4380,20 @@ impl<A: Application> wayland_client::Dispatch<crate::protocol::cce_window_manage
 
 impl<A: Application> wayland_client::Dispatch<crate::protocol::cce_window_management_v1::zcce_toplevel_v1::ZcceToplevelV1, ()> for EngineState<A> {
     fn event(
-        _state: &mut Self,
+        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,
+        event: crate::protocol::cce_window_management_v1::zcce_toplevel_v1::Event,
         _data: &(),
         _conn: &Connection,
         _qh: &QueueHandle<Self>,
-    ) {}
+    ) {
+        use crate::protocol::cce_window_management_v1::zcce_toplevel_v1::Event;
+        if let Event::GridPatch { serial, x, y, width, height, scale } = event {
+            // A newer patch supersedes an unconsumed older one.
+            state.pending_grid_patch = Some((serial, x, y, width, height, scale));
+            state.redraw = true;
+        }
+    }
 }
 
 delegate_compositor!(@<A: Application> EngineState<A>);
@@ -4659,6 +4695,7 @@ fn run_session<'l, A: Application>(
         pointer_gestures,
         pinch_gesture: None,
         cce_toplevel: None,
+        pending_grid_patch: None,
         last_pinch_scale: 1.0,
         cursor_pos: (0.0, 0.0),
         last_press_serial: None,
@@ -4733,21 +4770,29 @@ 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() {
+        let wants_utility = engine_state.inner.as_ref().unwrap().utility();
+        let wants_grid = engine_state.inner.as_ref().unwrap().grid();
+        if wants_utility || wants_grid {
             // 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, ()) {
+            // window. Manager version 5 is where set_utility appeared, 6 is
+            // where the grid role did; on an older compositor the
+            // declaration is skipped and the app runs as a plain floating
+            // window rather than dying on an unknown opcode.
+            let version = if wants_grid { 6..=6 } else { 5..=5 };
+            match globals.bind::<crate::protocol::cce_window_management_v1::zcce_window_manager_v1::ZcceWindowManagerV1, _, _>(&qh, version, ()) {
                 Ok(cce_wm) => {
                     let toplevel = cce_wm.get_cce_toplevel(&surface, &qh, ());
-                    toplevel.set_utility();
+                    if wants_utility {
+                        toplevel.set_utility();
+                    }
+                    if wants_grid {
+                        toplevel.set_grid();
+                    }
                     engine_state.cce_toplevel = Some(toplevel);
                 }
                 Err(e) => {
-                    log::warn!("[window_runner] utility window declaration unavailable: {e}");
+                    log::warn!("[window_runner] cce window-management declaration unavailable: {e}");
                 }
             }
         }