Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git
feat: grid-snap interactive move/resize via policy snap module
Move and resize ops now magnetically snap to the desktop grid:
op_update runs the pointer-derived position/edge through
cce-window-manager's snap module (border-inclusive, nearest grid line
within a threshold). Config: 'snap' and 'snap_threshold' on the
desktop node (defaults on, 24 virtual px); snap=false or threshold 0
disables.
Co-Authored-By: Claude Fable 5 <[email protected]>
src/server/config.rs | 34 ++++++++++++++++++++++++++++++++++
src/server/seat.rs | 43 +++++++++++++++++++++++++++++++++++++------
2 files changed, 71 insertions(+), 6 deletions(-)
diff --git a/src/server/config.rs b/src/server/config.rs
index 6442d39..b182916 100644
--- a/src/server/config.rs
+++ b/src/server/config.rs
@@ -59,6 +59,10 @@ pub struct Layout {
pub desktop_grid_fade_mode: String,
pub desktop_enable_solid_color: bool,
pub desktop_solid_color: [f32; 4],
+ /// Magnetic grid snap for interactive move/resize.
+ pub desktop_snap: bool,
+ /// Snap radius in virtual units.
+ pub desktop_snap_threshold: f64,
pub scenefx_optimized_blur: bool,
pub status_backdrop_blur_ignore_transparent: bool,
pub window_backdrop_blur_ignore_transparent: bool,
@@ -114,6 +118,8 @@ impl Default for Layout {
desktop_grid_fade_mode: "linear".to_string(),
desktop_enable_solid_color: false,
desktop_solid_color: [0.0, 0.0, 0.0, 1.0],
+ desktop_snap: true,
+ desktop_snap_threshold: 24.0,
scenefx_optimized_blur: true,
status_backdrop_blur_ignore_transparent: true,
window_backdrop_blur_ignore_transparent: true,
@@ -275,6 +281,10 @@ pub struct SurfaceConfig {
pub desktop_grid_fade_mode: String,
#[serde(default = "default_desktop_solid_color")]
pub desktop_solid_color: String,
+ #[serde(default = "default_desktop_snap")]
+ pub desktop_snap: bool,
+ #[serde(default = "default_desktop_snap_threshold")]
+ pub desktop_snap_threshold: i64,
#[serde(default = "default_backplate_color")]
pub backplate_color: String,
#[serde(default = "default_backplate_blur")]
@@ -314,6 +324,8 @@ impl Default for SurfaceConfig {
desktop_mode: default_desktop_mode(),
desktop_grid_fade_mode: default_desktop_grid_fade_mode(),
desktop_solid_color: default_desktop_solid_color(),
+ desktop_snap: default_desktop_snap(),
+ desktop_snap_threshold: default_desktop_snap_threshold(),
backplate_color: default_backplate_color(),
backplate_blur: default_backplate_blur(),
backplate_corner_radius: default_backplate_corner_radius(),
@@ -370,6 +382,14 @@ fn default_desktop_solid_color() -> String {
"#000000".to_string()
}
+fn default_desktop_snap() -> bool {
+ true
+}
+
+fn default_desktop_snap_threshold() -> i64 {
+ 24
+}
+
fn default_backplate_color() -> String {
"#151520e6".to_string()
}
@@ -1431,6 +1451,16 @@ fn parse_kdl_config(content: &str) -> Result<Config, String> {
surface.desktop_grid_scale = val;
}
}
+ "snap" => {
+ if let Some(val) = entry.value().as_bool() {
+ surface.desktop_snap = val;
+ }
+ }
+ "snap_threshold" => {
+ if let Some(val) = entry.value().as_i64() {
+ surface.desktop_snap_threshold = val;
+ }
+ }
_ => {}
}
}
@@ -1540,6 +1570,8 @@ fn parse_kdl_config(content: &str) -> Result<Config, String> {
surface.desktop_grid_fade_mode = get_child_arg_string(node, "grid_fade_mode", &default_desktop_grid_fade_mode());
surface.desktop_mode = get_child_arg_string(node, "desktop_mode", &default_desktop_mode());
surface.desktop_solid_color = get_child_arg_string(node, "desktop_solid_color", &default_desktop_solid_color());
+ surface.desktop_snap = get_child_arg_bool(node, "desktop_snap", default_desktop_snap());
+ surface.desktop_snap_threshold = get_child_arg_i64(node, "desktop_snap_threshold", default_desktop_snap_threshold());
surface.backplate_color = get_child_arg_string(node, "backplate_color", &default_backplate_color());
surface.backplate_blur = get_child_arg_f64(node, "backplate_blur", default_backplate_blur());
surface.backplate_corner_radius = get_child_arg_i64(node, "backplate_corner_radius", default_backplate_corner_radius());
@@ -1654,6 +1686,8 @@ pub fn parse_config(path: &str, state: &mut crate::window_manager::WindowManager
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_snap = config.surface.desktop_snap;
+ state.layout.desktop_snap_threshold = config.surface.desktop_snap_threshold.max(0) as f64;
state.layout.desktop_gap_width = config.surface.desktop_gap_width as i32;
state.layout.desktop_cell_corner_radius = config.surface.desktop_cell_corner_radius as i32;
state.layout.desktop_cell_fade_inset = config.surface.desktop_cell_fade_inset;
diff --git a/src/server/seat.rs b/src/server/seat.rs
index 401a95d..84e1d11 100644
--- a/src/server/seat.rs
+++ b/src/server/seat.rs
@@ -943,7 +943,19 @@ impl Seat {
found
}
+ /// Snap parameters for interactive ops, from the current layout config.
+ /// A zero threshold (snap disabled) makes every snap function a no-op.
+ unsafe fn snap_params(&self) -> crate::policy::snap::SnapParams {
+ let layout = &(*self.server).wm.layout;
+ crate::policy::snap::SnapParams {
+ grid_scale: layout.desktop_grid_scale,
+ threshold: if layout.desktop_snap { layout.desktop_snap_threshold } else { 0.0 },
+ border_width: layout.border_width as f64,
+ }
+ }
+
pub unsafe fn op_update(&mut self, x: i32, y: i32) {
+ let sp = self.snap_params();
if let Some(ref mut op) = self.op {
op.x = x;
op.y = y;
@@ -1121,6 +1133,13 @@ impl Seat {
let vx = op.start_win_virtual_x + virtual_dx;
let vy = op.start_win_virtual_y + virtual_dy;
+ let (vx, vy) = crate::policy::snap::snap_move(
+ vx,
+ vy,
+ (*win).box_geom.width as f64,
+ (*win).box_geom.height as f64,
+ &sp,
+ );
(*win).virtual_x = vx;
(*win).virtual_y = vy;
@@ -1175,18 +1194,30 @@ impl Seat {
(*win).resize_edges = Some(edges);
}
+ // Magnetic grid snap pulls the dragged edge onto grid
+ // lines; the anchored edge is untouched.
if edges.left {
- let w = std::cmp::max(50, (op.start_win_w as f64 - virtual_dx) as i32) as u32;
- new_w = w;
+ let left = crate::policy::snap::snap_low_edge(op.start_win_virtual_x + virtual_dx, &sp);
+ let anchor_right = op.start_win_virtual_x + op.start_win_w as f64;
+ new_w = std::cmp::max(50, (anchor_right - left) as i32) as u32;
} else if edges.right {
- new_w = std::cmp::max(50, (op.start_win_w as f64 + virtual_dx) as i32) as u32;
+ let right = crate::policy::snap::snap_high_edge(
+ op.start_win_virtual_x + op.start_win_w as f64 + virtual_dx,
+ &sp,
+ );
+ new_w = std::cmp::max(50, (right - op.start_win_virtual_x) as i32) as u32;
}
if edges.top {
- let h = std::cmp::max(50, (op.start_win_h as f64 - virtual_dy) as i32) as u32;
- new_h = h;
+ let top = crate::policy::snap::snap_low_edge(op.start_win_virtual_y + virtual_dy, &sp);
+ let anchor_bottom = op.start_win_virtual_y + op.start_win_h as f64;
+ new_h = std::cmp::max(50, (anchor_bottom - top) as i32) as u32;
} else if edges.bottom {
- new_h = std::cmp::max(50, (op.start_win_h as f64 + virtual_dy) as i32) as u32;
+ let bottom = crate::policy::snap::snap_high_edge(
+ op.start_win_virtual_y + op.start_win_h as f64 + virtual_dy,
+ &sp,
+ );
+ new_h = std::cmp::max(50, (bottom - op.start_win_virtual_y) as i32) as u32;
}
(*win).virtual_x = vx;