window management library
git clone https://git.lucas.co/cce-window-manager.git
feat: TilingMode::Utility — the client owns the size, always
A Utility window is a tool whose shape is decided by its contents
(cce-relief is the motivating case): Status minus the docking. It
floats, moves and pans like any window, but the arrange pass restates
the xdg "you choose" 0x0 on EVERY pass — extending the fresh-floating-
spawn precedent — so the compositor can never dictate a size to it:
not from a restored state.json entry, not after an output or scale
change (the client re-commits at the new scale and the commit path
adopts that as the box). The offscreen cull still reads the real box.
Never inferred from size hints; assigned only by the explicit
set_utility request on the cce window-management protocol.
Co-Authored-By: Claude <[email protected]>
src/arrange.rs | 45 +++++++++++++++++++++++++++++++++++++++++++--
src/tiling.rs | 13 +++++++++++--
2 files changed, 54 insertions(+), 4 deletions(-)
diff --git a/src/arrange.rs b/src/arrange.rs
index e91acf1..99f6b15 100644
--- a/src/arrange.rs
+++ b/src/arrange.rs
@@ -494,7 +494,7 @@ pub fn place_normal_window(
// adopts that as the box. Guessing from the min-size hint locked
// self-sizing clients (every cce-ui app) to their minimum — they
// obey any nonzero configure, so the guess became the box forever.
- if snap.mode == TilingMode::Floating
+ if matches!(snap.mode, TilingMode::Floating | TilingMode::Utility)
&& snap.active_resize.is_none()
&& snap.box_geom.width <= 0
&& snap.box_geom.height <= 0
@@ -534,10 +534,22 @@ pub fn place_normal_window(
let (final_x, final_y) = ctx.virtual_to_screen(snap.virtual_pos.0, snap.virtual_pos.1);
+ // A Utility window's size is the client's alone: the plan restates
+ // the "you choose" 0x0 on EVERY pass, so the compositor never
+ // dictates a size to it — not from a restore, not after an output
+ // or scale change (the client re-commits at the new scale and the
+ // commit path adopts that as the box). The cull still uses the
+ // real box; only the configure size is withheld.
+ let size = if snap.mode == TilingMode::Utility {
+ (0, 0)
+ } else {
+ (fw as u32, fh as u32)
+ };
+
NormalPlacement {
pos: (final_x, final_y),
scale: ctx.zoom,
- size: (fw as u32, fh as u32),
+ size,
tiled_all_edges: false,
hidden: Some(ctx.is_offscreen(final_x, final_y, fw as f64 * ctx.zoom, fh as f64 * ctx.zoom)),
virtual_write: None,
@@ -1689,6 +1701,35 @@ mod tests {
assert_eq!(placement.size, (320, 240));
}
+ #[test]
+ fn utility_window_size_is_always_client_chosen() {
+ // A Utility window restates the "you choose" 0x0 on EVERY pass — even
+ // with an established box — so the compositor can never dictate a
+ // size to it (a restored size, an output change). The established box
+ // still drives the offscreen cull.
+ let p = NormalParams { gap_right: 10, gap_top: 6, cloud_position_default: None, desktop_grid_scale: 100.0, desktop_gap_width: 0.0, desktop_cell_inset: 0.0 };
+ let fresh = NormalSnapshot {
+ mode: TilingMode::Utility,
+ box_geom: Rect { x: 0, y: 0, width: 0, height: 0 },
+ min_size: (320, 240),
+ virtual_pos: (100.0, 200.0),
+ active_resize: None,
+ is_cloud: false,
+ };
+ let placement = place_normal_window(&fresh, &p, &ctx());
+ assert_eq!(placement.size, (0, 0));
+ assert_eq!(placement.hidden, None);
+
+ let established = NormalSnapshot {
+ box_geom: Rect { x: 0, y: 0, width: 520, height: 896 },
+ ..fresh
+ };
+ let placement = place_normal_window(&established, &p, &ctx());
+ assert_eq!(placement.size, (0, 0));
+ // The cull is computed (from the real box), unlike the unmapped case.
+ assert!(placement.hidden.is_some());
+ }
+
#[test]
fn pannable_window_follows_viewport() {
let snap = NormalSnapshot {
diff --git a/src/tiling.rs b/src/tiling.rs
index f2220df..c630888 100644
--- a/src/tiling.rs
+++ b/src/tiling.rs
@@ -3,8 +3,15 @@
// A window is either `Floating` (positioned freely on the virtual surface) or
// `Tiled` (every content edge lies on a visible desktop-grid cell edge). Tiled
// windows report the xdg maximized state to their client. The remaining
-// variants are internal roles (`Popup`, `Overlay`, `Status`) or the orthogonal
-// `Fullscreen` toggle.
+// variants are internal roles (`Popup`, `Overlay`, `Status`, `Utility`) or the
+// orthogonal `Fullscreen` toggle.
+//
+// `Utility` is `Status` minus the docking: a tool window whose shape is decided
+// by its contents (stacked sliders, fixed rows, nothing worth dragging). The
+// client owns the size, the compositor offers no resize affordance and saves no
+// geometry for it, but it floats and moves like any ordinary window. It is
+// never inferred from a sizing hint — a window is `Utility` only because the
+// client said so, via `set_utility` on the cce window-management protocol.
//
// Serde aliases keep old `state.json` files loading: the retired `Cascade` /
// `Grid` layout modes collapse to `Floating`, and `Maximized` (the old name
@@ -20,6 +27,7 @@ pub enum TilingMode {
Popup,
Overlay,
Status,
+ Utility,
}
impl TilingMode {
@@ -31,6 +39,7 @@ impl TilingMode {
TilingMode::Popup => "Popup",
TilingMode::Overlay => "Overlay",
TilingMode::Status => "Status",
+ TilingMode::Utility => "Utility",
}
}
}