git.lucas.co / cce-grid
desktop grid client
git clone https://git.lucas.co/cce-grid.git

commitbf821b146d240297d4c09b60ef8bb7f47e559fb9
authorLucas Galante <[email protected]>
date2026-08-17 15:36
feat: initial cce-grid — the desktop grid as a cce-ui client

World-anchored grid renderer: draws compositor-issued patches (virtual
rect + px per virtual unit) as Recess wells carved into the gap-colored
rail surface, styled from the same config keys as the compositor's
fallback grid. Ships a session unit (WantedBy=cce-session.target).

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

 .gitignore       |   2 +
 CLAUDE.md        |  29 ++++++++
 Cargo.toml       |  11 +++
 cce-grid.service |  12 ++++
 src/main.rs      | 203 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
 5 files changed, 257 insertions(+)

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..96ef6c0
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+/target
+Cargo.lock
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..ff9bcc0
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,29 @@
+# CLAUDE.md
+
+`cce-grid` is the desktop-grid client of the cce desktop: a cce-ui app the
+compositor world-anchors to the virtual desktop. It renders grid *patches* —
+virtual-rect regions at a compositor-chosen resolution — and is never in the
+pan/zoom loop: the compositor transforms the committed buffer per frame like
+any window content.
+
+The contract (cce window-management protocol, manager v6 / toplevel v4):
+`Application::grid() -> true` declares the role; `grid_patch` events say what
+to render; cce-ui's runner resizes, forwards to `Application::grid_patch`,
+and acks so the next commit latches at the new anchor. The compositor keeps
+its own rect grid as the fallback whenever this client is absent or has not
+latched a patch yet, and its gap-colored backdrop always draws beneath as
+the safety net beyond patch edges.
+
+Rendering is a pure function of (patch, style config): cells are `Recess`
+wells carved into the gap-colored rail surface, reading the same
+`style.surface.desktop.*` / backplate-radius keys as the compositor
+fallback. Keep it that way — no camera state, no timers, no input (the
+surface is input-transparent compositor-side).
+
+This directory is its own git repository (gitsite-published, fetch-only
+origin; committing locally is publishing). `cce-grid.service` autostarts it
+with the session (WantedBy=cce-session.target); ccebuild installs both.
+
+Known MVP gaps: colors are passed to PaintCtx as raw sRGB (likely needs
+srgb_to_linear — the client renders brighter than the fallback), and corner
+sweeps are tighter than the compositor's span-widened superellipse.
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..ef39f39
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,11 @@
+[package]
+name = "cce-grid"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+cce-ui = { path = "../cce-ui" }
+calloop = "0.13.0"
+wayland-client = { version = "0.31", features = ["system"] }
+log = "0.4"
+env_logger = "0.11"
diff --git a/cce-grid.service b/cce-grid.service
new file mode 100644
index 0000000..2711564
--- /dev/null
+++ b/cce-grid.service
@@ -0,0 +1,12 @@
+[Unit]
+Description=cce desktop grid (cce-ui world-anchored grid client)
+PartOf=cce-session.target
+After=cce-session.target
+
+[Service]
+ExecStart=%h/.local/bin/cce-grid
+Restart=on-failure
+RestartSec=1
+
+[Install]
+WantedBy=cce-session.target
diff --git a/src/main.rs b/src/main.rs
new file mode 100644
index 0000000..6a777dc
--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,203 @@
+//! cce-grid — the desktop grid, drawn by cce-ui.
+//!
+//! The compositor world-anchors this surface to the virtual desktop and
+//! pans/zooms it per frame exactly like window content, so this app is never
+//! in the camera loop. It renders only when the compositor hands it a patch
+//! (`Application::grid_patch`): a virtual-desktop rectangle plus a
+//! px-per-virtual-unit scale. Everything here is therefore a pure function
+//! of (patch, style config) — no camera state, no timers.
+//!
+//! The look comes from the same config keys the compositor's fallback grid
+//! reads (`style.surface.desktop.*`, backplate corner radius), but drawn
+//! with cce-ui's own primitives: cells are `Recess` wells carved into the
+//! gap-colored rail surface, under the toolkit's real lighting model —
+//! not the compositor's scenefx approximation of it.
+
+use wayland_client::QueueHandle;
+
+use cce_ui::engine::{Application, EngineState, LogicalPosition, LogicalSize, WindowSettings};
+use cce_ui::scene::layout::Rect;
+use cce_ui::scene::paint::{DisplayList, PaintCtx};
+use cce_ui::widget::{ElementState, KeyEvent, MouseButton, MouseScrollDelta};
+
+#[derive(Debug, Clone)]
+enum Message {}
+
+/// The world region the current buffer must cover, as told by the
+/// compositor: virtual origin/size and surface px per virtual unit.
+#[derive(Debug, Clone, Copy)]
+struct Patch {
+    x: f64,
+    y: f64,
+    w: f64,
+    h: f64,
+    scale: f64,
+}
+
+struct GridApp {
+    patch: Option<Patch>,
+}
+
+/// Style knobs, re-read per frame from the shared config (cheap: cce-ui
+/// caches the parse on mtime), with the same defaults the compositor uses.
+struct Style {
+    cell_size: f64,
+    gap_width: f64,
+    cell_inset: f64,
+    corner_radius: f64,
+    gap_color: [f32; 4],
+    cell_color: [f32; 4],
+}
+
+fn style() -> Style {
+    use cce_ui::config::{get_color, get_i64};
+    Style {
+        cell_size: get_i64("/style/surface/desktop/grid_cell_size", 512) as f64,
+        gap_width: (get_i64("/style/surface/desktop/gap_width", 16).max(0)) as f64,
+        cell_inset: get_i64("/style/surface/desktop/cell_fade_inset", 0) as f64,
+        corner_radius: get_i64("/style/surface/backplate/corner_radius", 12) as f64,
+        gap_color: get_color("/style/surface/desktop/gap_color")
+            .unwrap_or([0.686, 0.796, 0.867, 1.0]),
+        cell_color: get_color("/style/surface/desktop/cell_color")
+            .unwrap_or([0.0, 0.0, 0.0, 1.0]),
+    }
+}
+
+/// Never emit more cells than this per frame, whatever the patch/config says
+/// (a degenerate period must not turn into an unbounded display list).
+const MAX_CELLS: usize = 8192;
+
+impl GridApp {
+    fn paint(&self, pc: &mut PaintCtx, size: LogicalSize) {
+        let Some(p) = self.patch else { return };
+        if p.scale <= 0.0 {
+            return;
+        }
+        let st = style();
+        let period = st.cell_size + st.gap_width;
+        if period < 1.0 {
+            return;
+        }
+
+        // The rail surface: the whole patch in gap color.
+        pc.quad(
+            Rect { x: 0.0, y: 0.0, width: size.width as f32, height: size.height as f32 },
+            st.gap_color,
+        );
+
+        // Visible cell box within its period slot, in virtual units.
+        let inset = st.cell_inset.clamp(0.0, st.cell_size / 2.0 - 1.0);
+        let lo = inset;
+        let len = st.cell_size - 2.0 * inset;
+        let s = p.scale;
+        let radius = (st.corner_radius * s) as f32;
+        let depth = radius.max(1.0);
+
+        let col0 = (p.x / period).floor() as i64;
+        let col1 = ((p.x + p.w) / period).ceil() as i64;
+        let row0 = (p.y / period).floor() as i64;
+        let row1 = ((p.y + p.h) / period).ceil() as i64;
+        let mut cells = 0usize;
+        for col in col0..col1 {
+            for row in row0..row1 {
+                if cells >= MAX_CELLS {
+                    return;
+                }
+                cells += 1;
+                let vx = col as f64 * period + lo;
+                let vy = row as f64 * period + lo;
+                let rect = Rect {
+                    x: ((vx - p.x) * s) as f32,
+                    y: ((vy - p.y) * s) as f32,
+                    width: (len * s) as f32,
+                    height: (len * s) as f32,
+                };
+                // The cell floor, then the well walls carved down to it.
+                pc.rounded_rect(rect, radius, (true, true, true, true), st.cell_color);
+                pc.recess(rect, (radius, radius, radius, radius), depth);
+            }
+        }
+    }
+}
+
+impl Application for GridApp {
+    type Message = Message;
+
+    fn new(
+        _qh: &QueueHandle<EngineState<Self>>,
+        _sender: calloop::channel::Sender<Self::Message>,
+    ) -> Self {
+        Self { patch: None }
+    }
+
+    fn settings(&self) -> WindowSettings {
+        WindowSettings {
+            title: "Desktop Grid".to_string(),
+            app_id: "cce-grid".to_string(),
+            // Nominal initial size; every real size comes from a patch.
+            width: 640,
+            height: 480,
+            fullscreen: false,
+            min_size: None,
+        }
+    }
+
+    fn grid(&self) -> bool {
+        true
+    }
+
+    fn grid_patch(&mut self, x: f64, y: f64, w: f64, h: f64, scale: f64) {
+        self.patch = Some(Patch { x, y, w, h, scale });
+    }
+
+    fn update(&mut self, _msg: Self::Message, _needs_rebuild: &mut bool, _exit: &mut bool) {}
+
+    fn tick(&mut self, _dt: f32, _needs_rebuild: &mut bool) {}
+
+    // The grid layer is input-transparent compositor-side; nothing ever
+    // reaches these.
+    fn handle_pointer_move(&mut self, _pos: LogicalPosition, _needs_rebuild: &mut bool) {}
+
+    fn handle_mouse_input(
+        &mut self,
+        _button: MouseButton,
+        _state: ElementState,
+        _pos: LogicalPosition,
+        _needs_rebuild: &mut bool,
+    ) -> Option<Self::Message> {
+        None
+    }
+
+    fn handle_mouse_wheel(
+        &mut self,
+        _delta: &MouseScrollDelta,
+        _pos: LogicalPosition,
+        _needs_rebuild: &mut bool,
+    ) {
+    }
+
+    fn handle_key_input(
+        &mut self,
+        _event: &KeyEvent,
+        _needs_rebuild: &mut bool,
+    ) -> Option<Self::Message> {
+        None
+    }
+
+    fn display_list(&mut self, size: LogicalSize, _scale: f64) -> Option<DisplayList> {
+        let mut pc = PaintCtx::new();
+        self.paint(&mut pc, size);
+        Some(pc.finish())
+    }
+
+    fn clear_color(&self) -> [f32; 4] {
+        // Patch edges the display list somehow misses read as rail surface,
+        // matching the compositor's always-on gap backdrop underneath.
+        style().gap_color
+    }
+}
+
+fn main() {
+    env_logger::init();
+    cce_ui::engine::run::<GridApp>();
+}