Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git
feat: grid_cell_width / grid_cell_height config keys
Wire the policy crate's per-axis cell sizes through the mechanism:
- config: Layout.desktop_cell_width/height replace desktop_grid_scale.
New desktop keys grid_cell_width / grid_cell_height; the legacy
grid_cell_size (and desktop_grid_scale alias) still parses and sets
both axes, with the specific keys winning when present. Same
precedence over the config-value IPC path.
- window_manager: ActionCtx gets both grid periods; grid-client patch
bounds align per axis; arrange params and the cells:: calls
(move-window, windows --json labels) pass both sizes.
- seat: interactive resize snaps width against the x-axis view and
height against the y-axis view.
- output: the grid lattice, backdrop, bevel rings, and overview labels
draw with per-axis periods and cell extents; label sizing and the
line-relief roll budget key off the smaller dimension.
Shadow-verified at 512x256: lattice renders rectangles on 528x272
periods, move-window B-2 lands at (532, -540), tiling spans 2x2 cells
as 1032x520, keyed pans step 528 horizontally / 272 vertically.
Co-Authored-By: Claude Fable 5 <[email protected]>
src/server/config.rs | 46 ++++++++++++++++++++++++++++++++++++++-----
src/server/output.rs | 28 +++++++++++++++-----------
src/server/seat.rs | 4 ++--
src/server/window_manager.rs | 47 +++++++++++++++++++++++++++++++-------------
4 files changed, 92 insertions(+), 33 deletions(-)
diff --git a/src/server/config.rs b/src/server/config.rs
index fa740d8..59fe588 100644
--- a/src/server/config.rs
+++ b/src/server/config.rs
@@ -52,7 +52,11 @@ pub struct Layout {
pub transparency_opacity: f32,
pub window_opacity: bool,
pub desktop_cell_color: [f32; 4],
- pub desktop_grid_scale: f64,
+ /// Desktop grid cell width/height in virtual units (each axis's period
+ /// is cell + gap). Config: `grid_cell_width` / `grid_cell_height`, with
+ /// the legacy `grid_cell_size` setting both.
+ pub desktop_cell_width: f64,
+ pub desktop_cell_height: f64,
pub desktop_gap_width: i32,
/// Grid-line lip width in logical px; None = follow the DE-wide relief
/// material (bevel_thickness clamped to the rail), 0 = no lip.
@@ -121,7 +125,8 @@ impl Layout {
BackgroundSpec::Grid(GridSpec {
gap_color: Rgba(parse_hex_color_rgba(&self.desktop_gap_color)),
cell_color: Rgba(self.desktop_cell_color),
- cell_size: self.desktop_grid_scale,
+ cell_w: self.desktop_cell_width,
+ cell_h: self.desktop_cell_height,
gap_width: self.desktop_gap_width as f64,
// Cells inherit the window backplate radius: a tiled window's
// content covers exactly the visible cell box, so its arc sits
@@ -136,7 +141,8 @@ impl Layout {
/// disabled) makes every snap function a no-op.
pub fn snap_params(&self) -> crate::policy::snap::SnapParams {
crate::policy::snap::SnapParams {
- cell_size: self.desktop_grid_scale,
+ cell_w: self.desktop_cell_width,
+ cell_h: self.desktop_cell_height,
gap_width: self.desktop_gap_width as f64,
cell_inset: self.desktop_cell_fade_inset as f64,
threshold: if self.desktop_snap { self.desktop_snap_threshold } else { 0.0 },
@@ -185,7 +191,8 @@ impl Default for Layout {
transparency_opacity: 0.9,
window_opacity: true,
desktop_cell_color: [0.05, 0.05, 0.05, 0.05],
- desktop_grid_scale: 100.0,
+ desktop_cell_width: 100.0,
+ desktop_cell_height: 100.0,
desktop_gap_width: 1,
desktop_line_relief: None,
desktop_cell_fade_inset: 0,
@@ -397,6 +404,12 @@ pub struct SurfaceConfig {
pub desktop_cell_color: String,
#[serde(default = "default_desktop_grid_scale")]
pub desktop_grid_scale: i64,
+ /// Per-axis cell sizes; None falls back to `desktop_grid_scale`
+ /// (the legacy square `grid_cell_size`).
+ #[serde(default)]
+ pub grid_cell_width: Option<i64>,
+ #[serde(default)]
+ pub grid_cell_height: Option<i64>,
#[serde(default = "default_desktop_gap_width")]
pub desktop_gap_width: i64,
/// Negative = unset (follow the DE-wide relief material).
@@ -507,6 +520,8 @@ impl Default for SurfaceConfig {
desktop_gap_color: default_desktop_gap_color(),
desktop_cell_color: default_desktop_cell_color(),
desktop_grid_scale: default_desktop_grid_scale(),
+ grid_cell_width: None,
+ grid_cell_height: None,
desktop_gap_width: default_desktop_gap_width(),
desktop_line_relief: default_desktop_line_relief(),
desktop_cell_fade_inset: default_desktop_cell_fade_inset(),
@@ -1794,6 +1809,16 @@ fn parse_kdl_config(content: &str) -> Result<Config, String> {
surface.desktop_grid_scale = val;
}
}
+ "grid_cell_width" => {
+ if let Some(val) = entry.value().as_i64() {
+ surface.grid_cell_width = Some(val);
+ }
+ }
+ "grid_cell_height" => {
+ if let Some(val) = entry.value().as_i64() {
+ surface.grid_cell_height = Some(val);
+ }
+ }
"snap" => {
if let Some(val) = entry.value().as_bool() {
surface.desktop_snap = val;
@@ -2017,6 +2042,14 @@ fn parse_kdl_config(content: &str) -> Result<Config, String> {
surface.desktop_gap_color = get_child_arg_string(node, "desktop_gap_color", &default_desktop_gap_color());
surface.desktop_cell_color = get_child_arg_string(node, "desktop_cell_color", &default_desktop_cell_color());
surface.desktop_grid_scale = get_child_arg_i64(node, "grid_cell_size", get_child_arg_i64(node, "desktop_grid_scale", default_desktop_grid_scale()));
+ surface.grid_cell_width = match get_child_arg_i64(node, "grid_cell_width", i64::MIN) {
+ i64::MIN => None,
+ v => Some(v),
+ };
+ surface.grid_cell_height = match get_child_arg_i64(node, "grid_cell_height", i64::MIN) {
+ i64::MIN => None,
+ v => Some(v),
+ };
surface.desktop_gap_width = get_child_arg_i64(node, "desktop_gap_width", default_desktop_gap_width());
surface.desktop_cell_fade_inset = get_child_arg_i64(node, "desktop_cell_fade_inset", default_desktop_cell_fade_inset());
surface.desktop_grid_fade_mode = get_child_arg_string(node, "grid_fade_mode", &default_desktop_grid_fade_mode());
@@ -2193,7 +2226,10 @@ pub fn parse_config(path: &str, state: &mut crate::window_manager::WindowManager
state.layout.background_a = 0xFFFFFFFF;
state.layout.desktop_cell_color = parse_hex_color_rgba(&config.surface.desktop_cell_color);
- state.layout.desktop_grid_scale = config.surface.desktop_grid_scale as f64;
+ state.layout.desktop_cell_width =
+ config.surface.grid_cell_width.unwrap_or(config.surface.desktop_grid_scale) as f64;
+ state.layout.desktop_cell_height =
+ config.surface.grid_cell_height.unwrap_or(config.surface.desktop_grid_scale) as f64;
state.layout.desktop_snap = config.surface.desktop_snap;
state.layout.overview_anim = if config.surface.desktop_overview_ramp.is_empty() {
None
diff --git a/src/server/output.rs b/src/server/output.rs
index 23b8cbf..b30648e 100644
--- a/src/server/output.rs
+++ b/src/server/output.rs
@@ -953,7 +953,7 @@ impl Output {
// and a tiled window's (widened) arc must land on
// the cell's arc.
let cell_radius = crate::window::widen_corner_radius(
- cells.corner_radius_px, cells.cell_px, cells.cell_px,
+ cells.corner_radius_px, cells.cell_w_px, cells.cell_h_px,
);
// The backplate-edge roll (mirroring cce-grid): the
// bevel-width knob clamped to a fraction of the
@@ -964,14 +964,17 @@ impl Output {
// width outright (0 = no lip). The ring expands the
// cell box by the roll, inner edge concentric with
// the cell arc.
- let gap_px = (frame.period_px_exact - cells.cell_px as f64).max(0.0);
+ let gap_px = (frame.period_px_exact_x - cells.cell_w_px as f64)
+ .min(frame.period_px_exact_y - cells.cell_h_px as f64)
+ .max(0.0);
let roll = layout
.desktop_line_relief
.map(|v| v * zoom)
.unwrap_or_else(|| (layout.bevel_thickness as f64 * zoom).min(gap_px * 0.25))
.max(0.0);
let hg = roll.round() as i32;
- let ring_px = cells.cell_px + 2 * hg;
+ let ring_w_px = cells.cell_w_px + 2 * hg;
+ let ring_h_px = cells.cell_h_px + 2 * hg;
let ring_radius = cell_radius + hg;
// Cell positions from the EXACT period, rounded per
// cell: a rounded-period spacing drifts from the
@@ -979,12 +982,12 @@ impl Output {
// grid visibly slides against window edges when
// panning).
for col in 0..=cells.cols {
- let rel_x = (col as f64 * frame.period_px_exact).round() as i32;
+ let rel_x = (col as f64 * frame.period_px_exact_x).round() as i32;
for row in 0..=cells.rows {
- let rel_y = (row as f64 * frame.period_px_exact).round() as i32;
- get_rect(cells.cell_px, cells.cell_px, cells.color.0.as_ptr(), rel_x, rel_y, cell_radius, inset_scaled);
+ let rel_y = (row as f64 * frame.period_px_exact_y).round() as i32;
+ get_rect(cells.cell_w_px, cells.cell_h_px, cells.color.0.as_ptr(), rel_x, rel_y, cell_radius, inset_scaled);
if bevel_on && hg > 0 {
- get_bevel(ring_px, ring_px, rel_x - hg, rel_y - hg, ring_radius, roll as f32);
+ get_bevel(ring_w_px, ring_h_px, rel_x - hg, rel_y - hg, ring_radius, roll as f32);
}
}
}
@@ -1061,8 +1064,9 @@ impl Output {
// A fixed fraction of the on-screen cell, clamped so labels stay
// readable when zoomed far out and don't swell into billboards when
// near. Below the floor there is no room for glyphs at all.
- let px = ((cells.cell_px as f32) * 0.16).clamp(9.0, 40.0);
- if px * 3.0 > cells.cell_px as f32 {
+ let min_cell_px = cells.cell_w_px.min(cells.cell_h_px);
+ let px = ((min_cell_px as f32) * 0.16).clamp(9.0, 40.0);
+ if px * 3.0 > min_cell_px as f32 {
self.disable_cell_labels();
return;
}
@@ -1078,12 +1082,12 @@ impl Output {
}
}
- let inset = (cells.cell_px as f64 * 0.06).round() as i32;
+ let inset = (min_cell_px as f64 * 0.06).round() as i32;
let mut idx = 0usize;
for col in 0..=cells.cols {
- let rel_x = (col as f64 * frame.period_px_exact).round() as i32;
+ let rel_x = (col as f64 * frame.period_px_exact_x).round() as i32;
for row in 0..=cells.rows {
- let rel_y = (row as f64 * frame.period_px_exact).round() as i32;
+ let rel_y = (row as f64 * frame.period_px_exact_y).round() as i32;
let text = crate::policy::cells::square_label(
frame.first_col + col,
frame.first_row + row,
diff --git a/src/server/seat.rs b/src/server/seat.rs
index 8bf057d..f664a59 100644
--- a/src/server/seat.rs
+++ b/src/server/seat.rs
@@ -1320,11 +1320,11 @@ impl Seat {
// through snap::resize_axis.
let new_w = crate::policy::snap::resize_axis(
op.start_win_virtual_x, op.start_win_w as f64, virtual_dx,
- edges.left, edges.right, 50.0, &sp,
+ edges.left, edges.right, 50.0, &sp.x(),
) as u32;
let new_h = crate::policy::snap::resize_axis(
op.start_win_virtual_y, op.start_win_h as f64, virtual_dy,
- edges.top, edges.bottom, 50.0, &sp,
+ edges.top, edges.bottom, 50.0, &sp.y(),
) as u32;
(*win).virtual_x = vx;
diff --git a/src/server/window_manager.rs b/src/server/window_manager.rs
index a93dd44..549710d 100644
--- a/src/server/window_manager.rs
+++ b/src/server/window_manager.rs
@@ -1274,7 +1274,9 @@ impl WindowManager {
cursor_y,
hovered,
focused,
- grid_period: self.layout.desktop_grid_scale.max(5.0)
+ grid_period_x: self.layout.desktop_cell_width.max(5.0)
+ + self.layout.desktop_gap_width.max(0) as f64,
+ grid_period_y: self.layout.desktop_cell_height.max(5.0)
+ self.layout.desktop_gap_width.max(0) as f64,
windows,
}
@@ -1976,11 +1978,11 @@ impl WindowManager {
let sp = self.layout.snap_params().for_zoom(self.desk_zoom);
let new_w = crate::policy::snap::resize_axis(
op.start_win_virtual_x, op.start_win_w as f64, virtual_dx,
- edges.left, edges.right, 50.0, &sp,
+ edges.left, edges.right, 50.0, &sp.x(),
) as u32;
let new_h = crate::policy::snap::resize_axis(
op.start_win_virtual_y, op.start_win_h as f64, virtual_dy,
- edges.top, edges.bottom, 50.0, &sp,
+ edges.top, edges.bottom, 50.0, &sp.y(),
) as u32;
return Some((new_w, new_h));
}
@@ -2122,7 +2124,9 @@ impl WindowManager {
while zoom * out_scale / q > 2.0 && q < q_max {
q = (q * 2.0).min(q_max);
}
- let period = self.layout.desktop_grid_scale
+ let period_x = self.layout.desktop_cell_width
+ + (self.layout.desktop_gap_width as f64).max(0.0);
+ let period_y = self.layout.desktop_cell_height
+ (self.layout.desktop_gap_width as f64).max(0.0);
// Target viewport (virtual units), for the union coverage below.
@@ -2269,10 +2273,10 @@ impl WindowManager {
}
let m = (((max_buf / q) - uw) / (2.0 * uw)).clamp(0.0, 0.5)
.min((((max_buf / q) - uh) / (2.0 * uh)).clamp(0.0, 0.5));
- let x0 = ((ux0 - m * uw) / period).floor() * period;
- let y0 = ((uy0 - m * uh) / period).floor() * period;
- let x1 = ((ux1 + m * uw) / period).ceil() * period;
- let y1 = ((uy1 + m * uh) / period).ceil() * period;
+ let x0 = ((ux0 - m * uw) / period_x).floor() * period_x;
+ let y0 = ((uy0 - m * uh) / period_y).floor() * period_y;
+ let x1 = ((ux1 + m * uw) / period_x).ceil() * period_x;
+ let y1 = ((uy1 + m * uh) / period_y).ceil() * period_y;
let patch = crate::policy::api::GridPatch {
x: x0,
y: y0,
@@ -2451,7 +2455,8 @@ impl WindowManager {
gap_right: self.layout.gap_right,
gap_top: self.layout.gap_top,
cloud_position_default: self.layout.cloud_position_default,
- desktop_grid_scale: self.layout.desktop_grid_scale,
+ desktop_cell_w: self.layout.desktop_cell_width,
+ desktop_cell_h: self.layout.desktop_cell_height,
desktop_gap_width: self.layout.desktop_gap_width as f64,
desktop_cell_inset: self.layout.desktop_cell_fade_inset as f64,
},
@@ -3770,7 +3775,8 @@ impl WindowManager {
let (x, y, _, _) = crate::policy::cells::square_rect(
col,
row,
- sp.cell_size,
+ sp.cell_w,
+ sp.cell_h,
sp.gap_width,
sp.cell_inset,
);
@@ -3788,7 +3794,8 @@ impl WindowManager {
y,
(*target).box_geom.width as f64,
(*target).box_geom.height as f64,
- sp.cell_size,
+ sp.cell_w,
+ sp.cell_h,
sp.gap_width,
);
format!("ok cell={} vx={:.1} vy={:.1}\n", cell, x, y)
@@ -4009,7 +4016,8 @@ impl WindowManager {
(*w).virtual_y,
(*w).box_geom.width as f64,
(*w).box_geom.height as f64,
- sp.cell_size,
+ sp.cell_w,
+ sp.cell_h,
sp.gap_width,
);
if as_json {
@@ -4086,9 +4094,20 @@ impl WindowManager {
"desktop_cell_color" => {
self.layout.desktop_cell_color = crate::config::parse_hex_color_rgba(val);
}
- "desktop_grid_scale" => {
+ "desktop_grid_scale" | "grid_cell_size" => {
+ if let Ok(v) = val.parse::<f64>() {
+ self.layout.desktop_cell_width = v;
+ self.layout.desktop_cell_height = v;
+ }
+ }
+ "grid_cell_width" => {
+ if let Ok(v) = val.parse::<f64>() {
+ self.layout.desktop_cell_width = v;
+ }
+ }
+ "grid_cell_height" => {
if let Ok(v) = val.parse::<f64>() {
- self.layout.desktop_grid_scale = v;
+ self.layout.desktop_cell_height = v;
}
}
"desktop_gap_width" => {