git.lucas.co / cce-compositor
Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git

commit5b5a6ed7fcba3bf36f427c346d21a65bde05fe00
parent27082cee6c
authorLucas Galante <lsgalante12@gmail.com>
date2026-10-07 22:34
refactor: depend on cce-core, not cce-ui

The compositor used cce-ui for config (paths, read, write), input.kdl
bindings, motion::enabled and the relief/droplet spec parsers — and
linked the whole toolkit for it: ash, smithay, cosmic-text, resvg, naga,
gpu-allocator. Those modules are cce-core now (cce-ui re-exports them), so
this depends on cce-core alone: Cargo.toml swaps the pin, and src renames
cce_ui::{config,input,motion,relief_spec} to cce_core::… and
scene::paint::DropletSpec to droplet::DropletSpec. Nothing else changes.

WORKSPACE.md names cce-core as the toolkit's GUI-free half and a shared
crate, and points the config/input sections at its files.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

 Cargo.toml                   |  8 +++++---
 WORKSPACE.md                 | 15 ++++++++++-----
 src/migrate_input.rs         | 10 +++++-----
 src/server/config.rs         | 12 ++++++------
 src/server/cursor.rs         |  8 ++++----
 src/server/layer_shell.rs    |  2 +-
 src/server/screenshot.rs     |  4 ++--
 src/server/window.rs         | 24 ++++++++++++------------
 src/server/window_manager.rs |  6 +++---
 9 files changed, 48 insertions(+), 41 deletions(-)

diff --git a/Cargo.toml b/Cargo.toml
index 89b77b19..583e9771 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -7,14 +7,16 @@ edition = "2021"
 doctest = false
 
 [dependencies]
-cce-ui = { git = "https://github.com/lsgalante/cce-ui.git", rev = "4dcb970d84f0a6aeeef5311ac1d4cacb0c63a803" }
+# The GUI-free half of the toolkit: config, input bindings, the animations
+# switch and the relief/droplet spec parsers. It depended on cce-ui for these
+# until 2026-10-07, linking the whole Wayland/Vulkan/text stack it never used.
+cce-core = { git = "https://github.com/lsgalante/cce-core.git", rev = "39e989d6ebc7850a1e5aa96bf228fe8714e538e0" }
 cce-window-manager = { git = "https://github.com/lsgalante/cce-window-manager.git", rev = "a55a5b02151f52e84d92a75994e00aa3c8f0a61f" }
 xkbcommon = "0.7"
 serde = { version = "1", features = ["derive"] }
 nix = { version = "0.29", features = ["signal", "process", "fs", "poll", "resource"] }
 libc = "0.2"
-# logind's sleep-delay inhibitor (`sleep_lock`). Already in the tree through
-# cce-ui (rfd -> ashpd), so this adds the blocking API, not a dependency.
+# logind's sleep-delay inhibitor (`sleep_lock`).
 zbus = "5"
 bitflags = "2"
 tokio = { version = "1.35", features = ["full"] }
diff --git a/WORKSPACE.md b/WORKSPACE.md
index 875cee95..9952fb89 100644
--- a/WORKSPACE.md
+++ b/WORKSPACE.md
@@ -31,8 +31,13 @@ desktop environment written in Rust, split into two halves:
   natively by the compositor — the former `cce-wallpaper` client was retired.)
 
 The one thing tying every crate together is **`cce-ui`**, the shared GUI toolkit. Every
-client depends on it (`cce-ui = { path = "../cce-ui" }`); the compositor depends on it
-too. There is one other shared crate: **`cce-window-manager`** — the compositor's
+client depends on it (a git pin that the root `[patch]` redirects to `../cce-ui`). Its
+GUI-free half is **`cce-core`** — config, input bindings, the animations switch, units, the
+socket IPC and the DE's spec parsers — which cce-ui re-exports at the old paths
+(`cce_ui::config`, …) and which a process that draws nothing depends on directly: the
+compositor (since 2026-10-07; it linked the whole toolkit for its config before), the
+browser's launch helper and cce-window-manager. The other shared crate is
+**`cce-window-manager`** — the compositor's
 pure-Rust window-management policy layer (arrange pass, `TilingMode`, saved state,
 slotmap; no FFI), extracted from `cce-compositor/` and consumed only by it. The compositor
 re-exports it as `crate::policy` / `crate::tiling` / `crate::slotmap`.
@@ -269,13 +274,13 @@ When adding a widget or a client, mirror an existing client (e.g.
 ## Configuration (shared across the whole DE)
 
 Config is **KDL** (`kdl` crate), loaded from `~/.config/cce/` (honoring
-`XDG_CONFIG_HOME`), via `cce-ui/src/config.rs`:
+`XDG_CONFIG_HOME`), via `cce-core/src/config.rs` (re-exported as `cce_ui::config`):
 
 - **`~/.config/cce/config.kdl`** — the shared/global config (`get_config_path()`).
 - **`~/.config/cce/<app-name>/config.kdl`** — per-app override
   (`get_app_config_path(app_name)`).
 - **`~/.config/cce/input.kdl`** — DE-wide keybindings and pointer input settings,
-  domain-scoped (`cce-ui/src/input.rs`): top-level nodes are domains
+  domain-scoped (`cce-core/src/input.rs`, re-exported as `cce_ui::input`): top-level nodes are domains
   (`cce-window-manager` for compositor actions, `cce-ui` for toolkit-wide widget
   defaults, `cce-<app>` for per-app bindings), children are `name "chord"`
   bindings. Resolution for an app is `<app>.<name>` → `cce-ui.<name>` (the
@@ -454,7 +459,7 @@ the others. The rules:
   with `ps -eo pid,ppid,cmd`, confirm it is yours via
   `/proc/<pid>/cgroup` (a unit's processes name their unit), `kill` it
   explicitly, then relaunch detached.
-- **Shared crates are exclusive.** Before editing `cce-ui`,
+- **Shared crates are exclusive.** Before editing `cce-ui`, `cce-core`,
   `cce-window-manager`, or `cce-icons`, run `git status` there. Foreign dirt
   means another session owns that crate right now — coordinate or stop; don't
   edit around it. Commit your own crate's work promptly so other sessions
diff --git a/src/migrate_input.rs b/src/migrate_input.rs
index 33496d19..d3b16b40 100644
--- a/src/migrate_input.rs
+++ b/src/migrate_input.rs
@@ -14,7 +14,7 @@
 // The brightness spawn binds synthesized from the `display` section are NOT
 // migrated — they stay derived from the display config at load time.
 
-use cce_ui::input::{BindingEntry, InputConfig, UI_DOMAIN, WINDOW_MANAGER_DOMAIN};
+use cce_core::input::{BindingEntry, InputConfig, UI_DOMAIN, WINDOW_MANAGER_DOMAIN};
 use cce_window_manager::api::Action;
 use cce_window_manager::bindings::parse_chord;
 
@@ -182,7 +182,7 @@ fn backup(path: &std::path::Path) -> Option<std::path::PathBuf> {
     if !path.exists() {
         return None;
     }
-    let backups = cce_ui::config::cce_config_dir().join("backups");
+    let backups = cce_core::config::cce_config_dir().join("backups");
     let _ = std::fs::create_dir_all(&backups);
     for n in 1..1000 {
         let candidate = backups.join(format!("input.kdl.{}.bak", n));
@@ -194,7 +194,7 @@ fn backup(path: &std::path::Path) -> Option<std::path::PathBuf> {
 }
 
 pub fn run() {
-    let config_path = cce_ui::config::get_config_path();
+    let config_path = cce_core::config::get_config_path();
     let content = match std::fs::read_to_string(&config_path) {
         Ok(c) => c,
         Err(e) => {
@@ -217,7 +217,7 @@ pub fn run() {
         return;
     }
 
-    let input_path = cce_ui::input::get_input_path();
+    let input_path = cce_core::input::get_input_path();
     let existing_content = std::fs::read_to_string(&input_path).unwrap_or_default();
     let existing = match InputConfig::parse(&existing_content) {
         Ok(c) => c,
@@ -241,7 +241,7 @@ pub fn run() {
         if added == 0 {
             return;
         }
-        match cce_ui::input::write_domain(&input_path, domain, entries) {
+        match cce_core::input::write_domain(&input_path, domain, entries) {
             Ok(()) => println!("{}: migrated {} binding(s)", domain, added),
             Err(e) => {
                 eprintln!("failed to write {}: {}", input_path.display(), e);
diff --git a/src/server/config.rs b/src/server/config.rs
index d03b4af0..1a66d951 100644
--- a/src/server/config.rs
+++ b/src/server/config.rs
@@ -1320,7 +1320,7 @@ pub fn default_config_path() -> Option<String> {
 /// `<state_home>/cce/state.json`; `None` when neither `$XDG_STATE_HOME` nor
 /// `$HOME` is set, rather than a path relative to wherever we were started.
 pub fn default_state_path() -> Option<String> {
-    let path = cce_ui::config::cce_state_dir().join("state.json");
+    let path = cce_core::config::cce_state_dir().join("state.json");
     path.is_absolute().then(|| path.to_string_lossy().into_owned())
 }
 
@@ -1782,7 +1782,7 @@ fn parse_kdl_config(content: &str) -> Result<Config, String> {
     // ~/.config/cce/cce-status-interface/config.kdl, `module { spacing height }`.
     // Re-read on every config (re)load, so `ccectl reload` picks up edits.
     {
-        let app_cfg = cce_ui::config::get_app_config_path("cce-status-interface");
+        let app_cfg = cce_core::config::get_app_config_path("cce-status-interface");
         if let Ok(content) = std::fs::read_to_string(&app_cfg) {
             if let Ok(app_doc) = content.parse::<kdl::KdlDocument>() {
                 if let Some(module) = app_doc.nodes().iter().find(|n| n.name().value() == "module") {
@@ -2236,7 +2236,7 @@ fn parse_kdl_config(content: &str) -> Result<Config, String> {
                                             // installs the full material,
                                             // but the scenefx chamfer has no
                                             // custom profile to install.
-                                            if let Some(spec) = cce_ui::relief_spec::ReliefSpec::parse(s) {
+                                            if let Some(spec) = cce_core::relief_spec::ReliefSpec::parse(s) {
                                                 surface.desktop_line_relief = spec.width.round() as i64;
                                             }
                                         }
@@ -2678,15 +2678,15 @@ pub fn parse_config(path: &str, state: &mut crate::window_manager::WindowManager
 
     let path_buf = std::path::Path::new(path);
     let input_path = path_buf.parent().unwrap_or_else(|| std::path::Path::new(".")).join("input.kdl");
-    let mut wm_domain_entries: Vec<cce_ui::input::BindingEntry> = Vec::new();
+    let mut wm_domain_entries: Vec<cce_core::input::BindingEntry> = Vec::new();
     if input_path.exists() {
         if let Ok(input_content) = fs::read_to_string(&input_path) {
             // New domain-scoped format: a `cce-window-manager { ... }` block
             // of `<action_name> "<chord>"` bindings. Other domains belong to
             // clients/widgets and are ignored here.
-            match cce_ui::input::InputConfig::parse(&input_content) {
+            match cce_core::input::InputConfig::parse(&input_content) {
                 Ok(ic) => {
-                    wm_domain_entries = ic.domain(cce_ui::input::WINDOW_MANAGER_DOMAIN).to_vec();
+                    wm_domain_entries = ic.domain(cce_core::input::WINDOW_MANAGER_DOMAIN).to_vec();
                 }
                 Err(e) => eprintln!("[WARNING] {}: {}", input_path.display(), e),
             }
diff --git a/src/server/cursor.rs b/src/server/cursor.rs
index f54fe02d..0a96a386 100644
--- a/src/server/cursor.rs
+++ b/src/server/cursor.rs
@@ -2614,8 +2614,8 @@ unsafe extern "C" fn handle_button(listener: *mut ffi::wl_listener, data: *mut s
                     _ => "top-left",
                 };
                 let key_path = format!("layout.status_bar.{}", name);
-                cce_ui::config::write_config_value(
-                    &cce_ui::config::get_config_path().to_string_lossy(),
+                cce_core::config::write_config_value(
+                    &cce_core::config::get_config_path().to_string_lossy(),
                     &key_path,
                     &format!("\"{}\"", edge_str),
                     "layout"
@@ -4049,11 +4049,11 @@ unsafe extern "C" fn handle_swipe_update(listener: *mut ffi::wl_listener, data:
         // reaches `swipe_repeat_peek` (default half of `swipe_peek`) at the
         // repeat threshold, so a swipe that has just switched focus does
         // not tug the camera toward the next window as eagerly. With
-        // animations off (`cce_ui::motion`) there is no lean at all: the
+        // animations off (`cce_core::motion`) there is no lean at all: the
         // camera stays put until the bind fires, then jumps (the step's
         // ease is instant then, `advance_camera_animation`). A lean
         // already showing when they were turned off goes back the same way.
-        let peek_px = if !cce_ui::motion::enabled() {
+        let peek_px = if !cce_core::motion::enabled() {
             0.0
         } else if cursor.gesture_triggered {
             wm.swipe_repeat_peek_px
diff --git a/src/server/layer_shell.rs b/src/server/layer_shell.rs
index 255a27c9..8c13182a 100644
--- a/src/server/layer_shell.rs
+++ b/src/server/layer_shell.rs
@@ -575,7 +575,7 @@ unsafe extern "C" fn handle_layer_surface_map(listener: *mut ffi::wl_listener, _
     // furniture — wallpaper, status bar — and map once at login, where a
     // fade reads as the desktop failing to draw.
     if (*wlr_layer_surface).current.layer == ffi::zwlr_layer_shell_v1_layer_ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY {
-        let ms = if cce_ui::motion::enabled() { (*server).wm.layout.fade_in_ms } else { 0 };
+        let ms = if cce_core::motion::enabled() { (*server).wm.layout.fade_in_ms } else { 0 };
         if ms > 0 {
             (*layer_surface).opacity = 0.0;
         }
diff --git a/src/server/screenshot.rs b/src/server/screenshot.rs
index 896ae0e0..f9d43c6a 100644
--- a/src/server/screenshot.rs
+++ b/src/server/screenshot.rs
@@ -435,11 +435,11 @@ fn spawn_encode(rgba: Vec<u8>, w: u32, h: u32, path: PathBuf) {
 /// typically an isolated one (a headless shadow session, say) whose toasts
 /// would land on someone else's screen. Stay quiet there.
 fn notifications_enabled() -> bool {
-    let Ok(content) = std::fs::read_to_string(cce_ui::config::get_config_path()) else {
+    let Ok(content) = std::fs::read_to_string(cce_core::config::get_config_path()) else {
         log::debug!("screenshot: config unreadable, staying quiet about the capture");
         return false;
     };
-    cce_ui::config::parse_kdl_to_json(&content)
+    cce_core::config::parse_kdl_to_json(&content)
         .pointer("/notifications/screenshots")
         .and_then(|v| v.as_bool())
         .unwrap_or(true)
diff --git a/src/server/window.rs b/src/server/window.rs
index 31419725..e94dcf62 100644
--- a/src/server/window.rs
+++ b/src/server/window.rs
@@ -218,9 +218,9 @@ pub const BORDER_FADE_STEP: f32 = 0.15;
 pub const BORDER_FADE_EPSILON: f32 = 0.004;
 
 /// The hover/dim step in force: [`BORDER_FADE_STEP`], or the whole distance
-/// when animations are off (`cce_ui::motion`), which lands in one tick.
+/// when animations are off (`cce_core::motion`), which lands in one tick.
 fn border_fade_step() -> f32 {
-    if cce_ui::motion::enabled() { BORDER_FADE_STEP } else { 1.0 }
+    if cce_core::motion::enabled() { BORDER_FADE_STEP } else { 1.0 }
 }
 
 /// Per-tick step of the fullscreen-toggle animation, as a fraction of the
@@ -2235,8 +2235,8 @@ impl Window {
                 let mut loaded_edge = None;
                 if name == "light_source" {
                     let mut light_pos = 2.356194490192345_f32; // Default 135 deg in rad
-                    if let Ok(content) = std::fs::read_to_string(cce_ui::config::get_config_path()) {
-                        let val = cce_ui::config::parse_kdl_to_json(&content);
+                    if let Ok(content) = std::fs::read_to_string(cce_core::config::get_config_path()) {
+                        let val = cce_core::config::parse_kdl_to_json(&content);
                         if let Some(wm_obj) = val.get("window_manager") {
                             if let Some(pos_val) = wm_obj.get("light_source_position") {
                                 if let Some(f) = pos_val.as_f64() {
@@ -2278,8 +2278,8 @@ impl Window {
                         StatusEdge::BottomRight
                     };
                     loaded_edge = Some(edge);
-                } else if let Ok(content) = std::fs::read_to_string(cce_ui::config::get_config_path()) {
-                    let val = cce_ui::config::parse_kdl_to_json(&content);
+                } else if let Ok(content) = std::fs::read_to_string(cce_core::config::get_config_path()) {
+                    let val = cce_core::config::parse_kdl_to_json(&content);
                     if let Some(layout_obj) = val.get("layout") {
                         if let Some(status_bar_obj) = layout_obj.get("status_bar") {
                             if let Some(edge_val) = status_bar_obj.get(name) {
@@ -2424,9 +2424,9 @@ impl Window {
         // that failed to map never starts one. `start_map_fade` snaps rather
         // than ramps when fading is off or this surface opts out (status
         // segments, wallpaper), so there is no second branch here.
-        // Animations off (`cce_ui::motion`) is a zero-length fade, the
+        // Animations off (`cce_core::motion`) is a zero-length fade, the
         // same as `surface { fade in_ms=0 }`.
-        let fade_ms = if cce_ui::motion::enabled() { (*self.server).wm.layout.fade_in_ms } else { 0 };
+        let fade_ms = if cce_core::motion::enabled() { (*self.server).wm.layout.fade_in_ms } else { 0 };
         if self.wants_map_fade() && fade_ms > 0 {
             self.map_fade = 0.0;
         }
@@ -4254,7 +4254,7 @@ impl Window {
             ffi::wlr_scene_node_set_enabled(node, false);
             return;
         }
-        let spec = cce_ui::scene::paint::DropletSpec::parse(
+        let spec = cce_core::droplet::DropletSpec::parse(
             layout.status_droplet.as_deref().unwrap_or(""),
         );
         if spec.refr <= 0.0 && spec.ghost <= 0.0 {
@@ -4293,7 +4293,7 @@ impl Window {
         }
         match (*self.server).wm.layout.status_droplet.as_deref() {
             Some(raw) => {
-                let spec = cce_ui::scene::paint::DropletSpec::parse(raw);
+                let spec = cce_core::droplet::DropletSpec::parse(raw);
                 spec.refr > 0.0 || spec.ghost > 0.0
             }
             None => false,
@@ -4518,7 +4518,7 @@ impl Window {
     /// pass in this same cycle.
     unsafe fn start_fs_anim(&mut self) {
         // Animations off: the window is simply drawn at its new rect.
-        if !cce_ui::motion::enabled() {
+        if !cce_core::motion::enabled() {
             self.fs_anim = None;
             return;
         }
@@ -4555,7 +4555,7 @@ impl Window {
             return false;
         };
         // Switched off mid-flight: land now, with the settling frame.
-        if !cce_ui::motion::enabled() {
+        if !cce_core::motion::enabled() {
             self.fs_anim = None;
             return true;
         }
diff --git a/src/server/window_manager.rs b/src/server/window_manager.rs
index 8db0639c..21d495ce 100644
--- a/src/server/window_manager.rs
+++ b/src/server/window_manager.rs
@@ -2406,10 +2406,10 @@ impl WindowManager {
     /// nothing is left to animate.
     unsafe fn advance_camera_animation(&mut self, dt: f64, frame_target_ns: u64) -> bool {
         let mut done = true;
-        // Animations off (`cce_ui::motion`): every ease below covers its
+        // Animations off (`cce_core::motion`): every ease below covers its
         // whole distance in this step, the ramp lands, and a flick does not
         // coast — the camera still goes where it was sent, just at once.
-        let animate = cce_ui::motion::enabled();
+        let animate = cce_core::motion::enabled();
         // Frame-rate independent exponential approach: the same fraction of
         // the remaining distance per unit time whatever the frame pacing.
         let factor = if animate { 1.0 - (-self.scroll_ease_rate() * dt).exp() } else { 1.0 };
@@ -5336,7 +5336,7 @@ impl WindowManager {
             "fade-out" => {
                 // Animations off answers 0 like a disabled fade: the client
                 // exits at once and nothing ramps.
-                let ms = if cce_ui::motion::enabled() { self.layout.fade_out_ms } else { 0 };
+                let ms = if cce_core::motion::enabled() { self.layout.fade_out_ms } else { 0 };
                 let pid = self.pending_ipc_peer_pid;
                 if pid <= 0 {
                     return "0\n".to_string();