window management library
git clone https://git.lucas.co/cce-window-manager.git
feat: independent desktop cell width and height
The desktop grid cell is no longer forced square. Every module that
consumed one cell size now carries both axes:
- snap: SnapParams gains cell_w/cell_h; the per-axis target math moved
to AxisSnapParams (SnapParams::x()/y()), which snap_low_edge /
snap_high_edge / resize_axis now take — callers name the axis they
drag. for_zoom caps the widened threshold at 45% of the SMALLER
dimension.
- cells: square_rect / block_rect / window_span / window_span_label
take cell_w + cell_h; columns and rows carry independent periods.
- background: GridFrame splits period_px / period_px_exact / cell_px
per axis; density fade and the fade-inset cap key off the smaller
dimension.
- api: GridSpec cell_w/cell_h; ActionCtx grid_period_x/grid_period_y —
keyed pans step one column period horizontally, one row period
vertically.
- arrange: NormalParams desktop_cell_w/desktop_cell_h; the Tiled arm
spans columns by width and rows by height.
tiled_span itself was already per-axis by argument and is unchanged.
Co-Authored-By: Claude Fable 5 <[email protected]>
src/actions.rs | 7 ++-
src/api.rs | 12 +++-
src/arrange.rs | 28 +++++----
src/background.rs | 177 ++++++++++++++++++++++++++++++++++--------------------
src/cells.rs | 74 ++++++++++++++---------
src/overview.rs | 4 +-
src/snap.rs | 144 +++++++++++++++++++++++++++++++-------------
7 files changed, 291 insertions(+), 155 deletions(-)
diff --git a/src/actions.rs b/src/actions.rs
index 360a73a..d504557 100644
--- a/src/actions.rs
+++ b/src/actions.rs
@@ -87,11 +87,11 @@ fn pan_step(ctx: &ActionCtx, action: Action) -> Vec<Command> {
let mut y = None;
if dx != 0.0 {
let base = ctx.pan_target_x.unwrap_or(ctx.camera.pan_x);
- x = Some(pan::aligned_step(base, ctx.grid_period, dx));
+ x = Some(pan::aligned_step(base, ctx.grid_period_x, dx));
}
if dy != 0.0 {
let base = ctx.pan_target_y.unwrap_or(ctx.camera.pan_y);
- y = Some(pan::aligned_step(base, ctx.grid_period, dy));
+ y = Some(pan::aligned_step(base, ctx.grid_period_y, dy));
}
vec![Command::PanTo { x, y }]
}
@@ -388,7 +388,8 @@ mod tests {
cursor_y: 540.0,
hovered: None,
focused: None,
- grid_period: 512.0,
+ grid_period_x: 512.0,
+ grid_period_y: 512.0,
windows: Vec::new(),
}
}
diff --git a/src/api.rs b/src/api.rs
index 21096fb..9ebd187 100644
--- a/src/api.rs
+++ b/src/api.rs
@@ -254,7 +254,10 @@ pub struct GridSpec {
/// Backdrop color behind and between the cells (premultiplied).
pub gap_color: Rgba,
pub cell_color: Rgba,
- pub cell_size: f64,
+ /// Cell width (x axis); the column period is `cell_w + gap_width`.
+ pub cell_w: f64,
+ /// Cell height (y axis); the row period is `cell_h + gap_width`.
+ pub cell_h: f64,
pub gap_width: f64,
pub cell_corner_radius: i32,
/// Cells fade inward by this many virtual px.
@@ -320,8 +323,11 @@ pub struct ActionCtx {
/// Non-status, non-background window under the cursor.
pub hovered: Option<WindowId>,
pub focused: Option<WindowId>,
- /// Desktop grid period (cell size + gap width) for cell-aligned panning.
- pub grid_period: f64,
+ /// Desktop grid periods (cell size + gap width, per axis) for
+ /// cell-aligned panning: keyed horizontal pans step `grid_period_x`,
+ /// vertical pans `grid_period_y`.
+ pub grid_period_x: f64,
+ pub grid_period_y: f64,
pub windows: Vec<ActionWindow>,
}
diff --git a/src/arrange.rs b/src/arrange.rs
index 3da0cca..8c01564 100644
--- a/src/arrange.rs
+++ b/src/arrange.rs
@@ -386,8 +386,10 @@ pub struct NormalParams {
pub gap_right: i32,
pub gap_top: i32,
pub cloud_position_default: Option<[i32; 2]>,
- pub desktop_grid_scale: f64,
- /// Desktop grid gap between cells (the grid period is scale + gap).
+ /// Desktop grid cell width/height (each axis's period is cell + gap).
+ pub desktop_cell_w: f64,
+ pub desktop_cell_h: f64,
+ /// Desktop grid gap between cells.
pub desktop_gap_width: f64,
/// Visual inset of a cell's edge (the fade inset); Tiled windows
/// snap to the visible cell edges like interactive snapping does.
@@ -473,10 +475,10 @@ pub fn place_normal_window(
let y2 = y1 + snap.box_geom.height.max(1) as f64;
let (low_x, high_x) = crate::snap::tiled_span(
- x1, x2, p.desktop_grid_scale, p.desktop_gap_width, p.desktop_cell_inset,
+ x1, x2, p.desktop_cell_w, p.desktop_gap_width, p.desktop_cell_inset,
);
let (low_y, high_y) = crate::snap::tiled_span(
- y1, y2, p.desktop_grid_scale, p.desktop_gap_width, p.desktop_cell_inset,
+ y1, y2, p.desktop_cell_h, p.desktop_gap_width, p.desktop_cell_inset,
);
// The content fills the covered cells edge to edge; the border
@@ -1657,7 +1659,7 @@ mod tests {
active_resize: None,
is_cloud: false,
};
- 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 p = NormalParams { gap_right: 10, gap_top: 6, cloud_position_default: None, desktop_cell_w: 100.0, desktop_cell_h: 100.0, desktop_gap_width: 0.0, desktop_cell_inset: 0.0 };
let placement = place_normal_window(&snap, &p, &ctx());
// Current geometry spans grid columns 1-2 and row 1 → snapped to
// (100,100) with size 200x100.
@@ -1678,7 +1680,7 @@ mod tests {
active_resize: None,
is_cloud: false,
};
- 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 p = NormalParams { gap_right: 10, gap_top: 6, cloud_position_default: None, desktop_cell_w: 100.0, desktop_cell_h: 100.0, desktop_gap_width: 0.0, desktop_cell_inset: 0.0 };
let placement = place_normal_window(&snap, &p, &ctx());
// The content fills the covered cells (100,100)+200x100 exactly; the
// border draws outside that and overhangs into the grid gap.
@@ -1703,7 +1705,8 @@ mod tests {
gap_right: 10,
gap_top: 6,
cloud_position_default: None,
- desktop_grid_scale: 100.0,
+ desktop_cell_w: 100.0,
+ desktop_cell_h: 100.0,
desktop_gap_width: 10.0,
desktop_cell_inset: 5.0,
};
@@ -1723,7 +1726,7 @@ mod tests {
active_resize: None,
is_cloud: false,
};
- 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 p = NormalParams { gap_right: 10, gap_top: 6, cloud_position_default: None, desktop_cell_w: 100.0, desktop_cell_h: 100.0, desktop_gap_width: 0.0, desktop_cell_inset: 0.0 };
let placement = place_normal_window(&snap, &p, &ctx());
// Defaults to 360x100, docked inside the usable area (below the bar).
assert_eq!(placement.pos, (1920 - 360 - 10, 30 + 6));
@@ -1745,7 +1748,7 @@ mod tests {
active_resize: None,
is_cloud: false,
};
- 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 p = NormalParams { gap_right: 10, gap_top: 6, cloud_position_default: None, desktop_cell_w: 100.0, desktop_cell_h: 100.0, desktop_gap_width: 0.0, desktop_cell_inset: 0.0 };
let placement = place_normal_window(&snap, &p, &ctx());
assert_eq!(placement.size, (0, 0));
assert_eq!(placement.hidden, None);
@@ -1773,7 +1776,7 @@ mod tests {
// 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 p = NormalParams { gap_right: 10, gap_top: 6, cloud_position_default: None, desktop_cell_w: 100.0, desktop_cell_h: 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 },
@@ -1806,7 +1809,7 @@ mod tests {
active_resize: None,
is_cloud: false,
};
- 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 p = NormalParams { gap_right: 10, gap_top: 6, cloud_position_default: None, desktop_cell_w: 100.0, desktop_cell_h: 100.0, desktop_gap_width: 0.0, desktop_cell_inset: 0.0 };
let mut c = ctx();
c.pan_x = 50.0;
c.pan_y = 100.0;
@@ -1890,7 +1893,8 @@ mod tests {
gap_right: 10,
gap_top: 6,
cloud_position_default: None,
- desktop_grid_scale: 100.0,
+ desktop_cell_w: 100.0,
+ desktop_cell_h: 100.0,
desktop_gap_width: 0.0,
desktop_cell_inset: 0.0,
},
diff --git a/src/background.rs b/src/background.rs
index 97dd710..6ede900 100644
--- a/src/background.rs
+++ b/src/background.rs
@@ -14,29 +14,34 @@ pub fn sanitized_zoom(zoom: f64) -> f64 {
if zoom.is_nan() || zoom <= 0.0 { 1.0 } else { zoom }
}
-/// One frame's grid drawing plan, in output-local px unless noted.
+/// One frame's grid drawing plan, in output-local px unless noted. The two
+/// axes carry independent periods (cell_w + gap and cell_h + gap).
#[derive(Debug, Clone, PartialEq)]
pub struct GridFrame {
/// Grid-tree translation in layout px (includes the output's own
- /// offset): the pan shift folded modulo one EXACT period, minus one
- /// period so the tree always overhangs the top-left edge. None when the
- /// zoomed period rounds to zero — the tree keeps its last position.
+ /// offset): the pan shift folded modulo one EXACT period per axis,
+ /// minus one period so the tree always overhangs the top-left edge.
+ /// None when either zoomed period rounds to zero — the tree keeps its
+ /// last position.
pub tree_pos: Option<(i32, i32)>,
- /// The zoomed grid period (cell + gap), rounded to px.
- pub period_px: i32,
- /// The exact (unrounded) zoomed period. Cell k must be placed at
- /// `round(k * period_px_exact)` tree-local — NOT `k * period_px`: at
- /// fractional zooms the rounded period drifts from the world-true cell
- /// positions by its rounding error per period, so the grid slides
- /// relative to the (world-anchored) windows as the camera pans, and
- /// anything meant to hug a window edge (a client shadow, a snap)
- /// visibly jitters against the grid.
- pub period_px_exact: f64,
- /// Backdrop (gap color) extent: viewport plus one period, so pan shifts
- /// never expose the edge.
+ /// The zoomed grid periods (cell + gap per axis), rounded to px.
+ pub period_px_x: i32,
+ pub period_px_y: i32,
+ /// The exact (unrounded) zoomed periods. Cell (col, row) must be placed
+ /// at `(round(col * period_px_exact_x), round(row * period_px_exact_y))`
+ /// tree-local — NOT multiples of the rounded periods: at fractional
+ /// zooms the rounded period drifts from the world-true cell positions
+ /// by its rounding error per period, so the grid slides relative to the
+ /// (world-anchored) windows as the camera pans, and anything meant to
+ /// hug a window edge (a client shadow, a snap) visibly jitters against
+ /// the grid.
+ pub period_px_exact_x: f64,
+ pub period_px_exact_y: f64,
+ /// Backdrop (gap color) extent: viewport plus one period per axis, so
+ /// pan shifts never expose the edge.
pub backdrop_w: i32,
pub backdrop_h: i32,
- /// None when the cells are fully density-faded, the period degenerates,
+ /// None when the cells are fully density-faded, a period degenerates,
/// or the cell count exceeds the safety caps — backdrop only.
pub cells: Option<GridCells>,
/// World square indices of the cell drawn at tree-local (0, 0). The grid
@@ -47,18 +52,21 @@ pub struct GridFrame {
pub first_row: i32,
}
-/// The repeated cell lattice: draw a cell at (col * period_px,
-/// row * period_px) for col in 0..=cols, row in 0..=rows, tree-local.
+/// The repeated cell lattice: draw a cell at (round(col * period_px_exact_x),
+/// round(row * period_px_exact_y)) for col in 0..=cols, row in 0..=rows,
+/// tree-local.
#[derive(Debug, Clone, PartialEq)]
pub struct GridCells {
- pub cell_px: i32,
+ pub cell_w_px: i32,
+ pub cell_h_px: i32,
pub cols: i32,
pub rows: i32,
/// Cell color with the density fade already applied to alpha.
pub color: Rgba,
pub corner_radius_px: i32,
- /// Zoom-scaled fade inset, capped at 45% of the cell so cells can't
- /// blur out entirely when far zoomed out; 0 disables the fade.
+ /// Zoom-scaled fade inset, capped at 45% of the smaller cell dimension
+ /// so cells can't blur out entirely when far zoomed out; 0 disables the
+ /// fade.
pub fade_inset_px: i32,
}
@@ -71,21 +79,26 @@ pub fn grid_frame(
output_y: i32,
) -> GridFrame {
let zoom = sanitized_zoom(cam.zoom);
- let cell_size = spec.cell_size.max(5.0);
+ let cell_w = spec.cell_w.max(5.0);
+ let cell_h = spec.cell_h.max(5.0);
let gap = spec.gap_width.max(0.0);
- let period = cell_size + gap;
+ let period_x = cell_w + gap;
+ let period_y = cell_h + gap;
- // Fade the cells out as they shrink (period under 30 screen px) to
- // prevent visual noise and pathological cell counts when zooming.
- let period_pixels = period * zoom;
- let density_fade = if period_pixels < 15.0 {
+ // Fade the cells out as they shrink (smaller period under 30 screen px)
+ // to prevent visual noise and pathological cell counts when zooming.
+ let period_pixels_x = period_x * zoom;
+ let period_pixels_y = period_y * zoom;
+ let min_period_pixels = period_pixels_x.min(period_pixels_y);
+ let density_fade = if min_period_pixels < 15.0 {
0.0
- } else if period_pixels < 30.0 {
- (period_pixels - 15.0) / 15.0
+ } else if min_period_pixels < 30.0 {
+ (min_period_pixels - 15.0) / 15.0
} else {
1.0
};
- let period_px = period_pixels.round() as i32;
+ let period_px_x = period_pixels_x.round() as i32;
+ let period_px_y = period_pixels_y.round() as i32;
// Modulo shift for infinite scrolling, in EXACT period units: the phase
// is folded over the true zoomed period and only rounded once at the
@@ -93,30 +106,39 @@ pub fn grid_frame(
// boundary at any pan. (Folding over the ROUNDED period accumulated its
// rounding error into the phase and made the whole grid jump relative
// to the windows whenever the fold wrapped.)
- let tree_pos = if period_px > 0 {
- let phase_x = ((-cam.pan_x) * zoom).rem_euclid(period_pixels);
- let phase_y = ((-cam.pan_y) * zoom).rem_euclid(period_pixels);
+ let tree_pos = if period_px_x > 0 && period_px_y > 0 {
+ let phase_x = ((-cam.pan_x) * zoom).rem_euclid(period_pixels_x);
+ let phase_y = ((-cam.pan_y) * zoom).rem_euclid(period_pixels_y);
Some((
- output_x + (phase_x - period_pixels).round() as i32,
- output_y + (phase_y - period_pixels).round() as i32,
+ output_x + (phase_x - period_pixels_x).round() as i32,
+ output_y + (phase_y - period_pixels_y).round() as i32,
))
} else {
None
};
- let cells = if period_px > 0 && density_fade > 0.0 {
- let cols = (viewport_w as f64 / period_px as f64).ceil() as i32 + 1;
- let rows = (viewport_h as f64 / period_px as f64).ceil() as i32 + 1;
- let cell_px = (cell_size * zoom).round() as i32;
- if cols > 0 && rows > 0 && cols <= 1000 && rows <= 1000 && cols * rows <= 20000 && cell_px > 0 {
+ let cells = if period_px_x > 0 && period_px_y > 0 && density_fade > 0.0 {
+ let cols = (viewport_w as f64 / period_px_x as f64).ceil() as i32 + 1;
+ let rows = (viewport_h as f64 / period_px_y as f64).ceil() as i32 + 1;
+ let cell_w_px = (cell_w * zoom).round() as i32;
+ let cell_h_px = (cell_h * zoom).round() as i32;
+ if cols > 0
+ && rows > 0
+ && cols <= 1000
+ && rows <= 1000
+ && cols * rows <= 20000
+ && cell_w_px > 0
+ && cell_h_px > 0
+ {
let mut color = spec.cell_color;
color.0[3] *= density_fade as f32;
- let max_inset = (cell_px as f64 * 0.45).floor() as i32;
+ let max_inset = (cell_w_px.min(cell_h_px) as f64 * 0.45).floor() as i32;
let fade_inset_px = ((spec.cell_fade_inset as f64 * zoom).round() as i32)
.min(max_inset)
.max(0);
Some(GridCells {
- cell_px,
+ cell_w_px,
+ cell_h_px,
cols,
rows,
color,
@@ -136,18 +158,20 @@ pub fn grid_frame(
// so rounding here is exact in practice.
let (first_col, first_row) = match tree_pos {
Some((tx, ty)) => (
- ((((tx - output_x) as f64) / zoom + cam.pan_x) / period).round() as i32,
- ((((ty - output_y) as f64) / zoom + cam.pan_y) / period).round() as i32,
+ ((((tx - output_x) as f64) / zoom + cam.pan_x) / period_x).round() as i32,
+ ((((ty - output_y) as f64) / zoom + cam.pan_y) / period_y).round() as i32,
),
None => (0, 0),
};
GridFrame {
tree_pos,
- period_px,
- period_px_exact: period_pixels,
- backdrop_w: viewport_w + period_px,
- backdrop_h: viewport_h + period_px,
+ period_px_x,
+ period_px_y,
+ period_px_exact_x: period_pixels_x,
+ period_px_exact_y: period_pixels_y,
+ backdrop_w: viewport_w + period_px_x,
+ backdrop_h: viewport_h + period_px_y,
cells,
first_col,
first_row,
@@ -165,7 +189,8 @@ mod tests {
#[test]
fn first_cell_indices_name_the_drawn_lattice() {
let spec = GridSpec {
- cell_size: 512.0,
+ cell_w: 512.0,
+ cell_h: 512.0,
gap_width: 16.0,
cell_fade_inset: 4,
cell_corner_radius: 0,
@@ -173,7 +198,7 @@ mod tests {
cell_color: Rgba([0.0, 0.0, 0.0, 1.0]),
gap_color: Rgba([0.7, 0.8, 0.9, 1.0]),
};
- let period = spec.cell_size + spec.gap_width;
+ let period = spec.cell_w + spec.gap_width;
// A few cameras, including the live desktop's overview zoom and a
// deeply negative pan (where the modulo fold wraps).
for &(pan_x, pan_y, zoom) in &[
@@ -187,13 +212,14 @@ mod tests {
let (tx, ty) = f.tree_pos.expect("period is drawable here");
for &(col, row) in &[(0, 0), (1, 2), (3, 1)] {
// Where the mechanism draws this cell.
- let drawn_x = tx as f64 + (col as f64 * f.period_px_exact).round();
- let drawn_y = ty as f64 + (row as f64 * f.period_px_exact).round();
+ let drawn_x = tx as f64 + (col as f64 * f.period_px_exact_x).round();
+ let drawn_y = ty as f64 + (row as f64 * f.period_px_exact_y).round();
// Where the square it is named after actually is.
let (wx, wy, _, _) = crate::cells::square_rect(
f.first_col + col,
f.first_row + row,
- spec.cell_size,
+ spec.cell_w,
+ spec.cell_h,
spec.gap_width,
0.0, // raw grid line: the lattice rect is not inset
);
@@ -208,7 +234,7 @@ mod tests {
);
}
// Sanity: the period is what we divided by.
- assert!((f.period_px_exact - period * zoom).abs() < 1e-9);
+ assert!((f.period_px_exact_x - period * zoom).abs() < 1e-9);
}
}
@@ -219,7 +245,8 @@ mod tests {
GridSpec {
gap_color: Rgba([0.0, 0.0, 0.0, 1.0]),
cell_color: Rgba([0.05, 0.05, 0.05, 0.6]),
- cell_size: 100.0,
+ cell_w: 100.0,
+ cell_h: 100.0,
gap_width: 10.0,
cell_corner_radius: 8,
cell_fade_inset: 4,
@@ -234,19 +261,38 @@ mod tests {
#[test]
fn unpanned_grid_overhangs_one_period() {
let f = grid_frame(&spec(), cam(0.0, 0.0, 1.0), VW, VH, 100, 50);
- assert_eq!(f.period_px, 110);
+ assert_eq!((f.period_px_x, f.period_px_y), (110, 110));
assert_eq!(f.tree_pos, Some((100 - 110, 50 - 110)));
assert_eq!((f.backdrop_w, f.backdrop_h), (VW + 110, VH + 110));
let cells = f.cells.unwrap();
// ceil(1920/110)+1 = 19, ceil(1080/110)+1 = 11.
assert_eq!((cells.cols, cells.rows), (19, 11));
- assert_eq!(cells.cell_px, 100);
+ assert_eq!((cells.cell_w_px, cells.cell_h_px), (100, 100));
assert_eq!(cells.fade_inset_px, 4);
assert_eq!(cells.corner_radius_px, 8);
// Full density: color untouched.
assert_eq!(cells.color, Rgba([0.05, 0.05, 0.05, 0.6]));
}
+ #[test]
+ fn rectangular_cells_get_per_axis_periods_and_counts() {
+ // 100-wide, 50-tall cells, gap 10: periods 110 x 60.
+ let mut sp = spec();
+ sp.cell_h = 50.0;
+ let f = grid_frame(&sp, cam(0.0, 0.0, 1.0), VW, VH, 0, 0);
+ assert_eq!((f.period_px_x, f.period_px_y), (110, 60));
+ assert_eq!(f.tree_pos, Some((-110, -60)));
+ assert_eq!((f.backdrop_w, f.backdrop_h), (VW + 110, VH + 60));
+ let cells = f.cells.unwrap();
+ assert_eq!((cells.cell_w_px, cells.cell_h_px), (100, 50));
+ // ceil(1920/110)+1 = 19, ceil(1080/60)+1 = 19.
+ assert_eq!((cells.cols, cells.rows), (19, 19));
+ // The fade inset caps on the SMALLER dimension (45% of 50 = 22).
+ sp.cell_fade_inset = 60;
+ let f = grid_frame(&sp, cam(0.0, 0.0, 1.0), VW, VH, 0, 0);
+ assert_eq!(f.cells.unwrap().fade_inset_px, 22);
+ }
+
#[test]
fn pan_folds_modulo_one_period() {
// Pan 30 right: origin -30, rem_euclid(110) = 80, minus a period.
@@ -266,7 +312,7 @@ mod tests {
// Zoom 0.1: period 11 px — fully faded, backdrop only.
let f = grid_frame(&spec(), cam(0.0, 0.0, 0.1), VW, VH, 0, 0);
assert!(f.cells.is_none());
- assert_eq!(f.period_px, 11);
+ assert_eq!(f.period_px_x, 11);
assert!(f.tree_pos.is_some());
}
@@ -285,12 +331,13 @@ mod tests {
// spacing alternates 422/423 so cells never drift from the
// world-anchored windows.
let mut s = spec();
- s.cell_size = 512.0;
+ s.cell_w = 512.0;
+ s.cell_h = 512.0;
s.gap_width = 16.0;
let f = grid_frame(&s, cam(0.0, 0.0, 0.8), 3840, 2400, 0, 0);
- assert_eq!(f.period_px, 422);
- assert!((f.period_px_exact - 422.4).abs() < 1e-9);
- let pos: Vec<i32> = (0..5).map(|k| (k as f64 * f.period_px_exact).round() as i32).collect();
+ assert_eq!(f.period_px_x, 422);
+ assert!((f.period_px_exact_x - 422.4).abs() < 1e-9);
+ let pos: Vec<i32> = (0..5).map(|k| (k as f64 * f.period_px_exact_x).round() as i32).collect();
assert_eq!(pos, vec![0, 422, 845, 1267, 1690]);
// Tree phase folds over the EXACT period: pan 100 → phase
// (-80).rem_euclid(422.4) = 342.4 → tree at round(342.4 - 422.4).
@@ -307,12 +354,12 @@ mod tests {
fn degenerate_zoom_and_period_are_safe() {
// NaN zoom falls back to 1.
let f = grid_frame(&spec(), cam(0.0, 0.0, f64::NAN), VW, VH, 0, 0);
- assert_eq!(f.period_px, 110);
+ assert_eq!(f.period_px_x, 110);
assert!(f.cells.is_some());
// A period that rounds to zero: no tree move, no cells, backdrop
// stays viewport-sized.
let f = grid_frame(&spec(), cam(0.0, 0.0, 0.001), VW, VH, 0, 0);
- assert_eq!(f.period_px, 0);
+ assert_eq!(f.period_px_x, 0);
assert!(f.tree_pos.is_none());
assert!(f.cells.is_none());
assert_eq!((f.backdrop_w, f.backdrop_h), (VW, VH));
diff --git a/src/cells.rs b/src/cells.rs
index 2f20490..9c2292b 100644
--- a/src/cells.rs
+++ b/src/cells.rs
@@ -2,9 +2,9 @@
//
// Coordinates in and out are the same virtual-surface CONTENT coordinates
// snap.rs works in, and the grid geometry matches it exactly: cells of
-// `cell_size` every `cell_size + gap_width`, each cell fading inward by
-// `cell_inset`, so square k's content span is
-// [k*period + inset, k*period + cell_size - inset]. A window snapped to a
+// `cell_w` x `cell_h` every `cell + gap_width` on each axis, each cell
+// fading inward by `cell_inset`, so square k's content span on an axis is
+// [k*period + inset, k*period + cell - inset]. A window snapped to a
// square therefore has its virtual position equal to that square's origin —
// the two modules must agree or a "tiled" window would not land on a named
// square.
@@ -116,11 +116,12 @@ pub fn parse_square(s: &str) -> Option<(i32, i32)> {
pub fn square_rect(
col: i32,
row: i32,
- cell_size: f64,
+ cell_w: f64,
+ cell_h: f64,
gap_width: f64,
cell_inset: f64,
) -> (f64, f64, f64, f64) {
- block_rect(col, row, col, row, cell_size, gap_width, cell_inset)
+ block_rect(col, row, col, row, cell_w, cell_h, gap_width, cell_inset)
}
/// Content rect of a whole block of squares, inclusive of both corners.
@@ -130,18 +131,21 @@ pub fn block_rect(
row0: i32,
col1: i32,
row1: i32,
- cell_size: f64,
+ cell_w: f64,
+ cell_h: f64,
gap_width: f64,
cell_inset: f64,
) -> (f64, f64, f64, f64) {
- let p = grid_period(cell_size, gap_width);
- let inset = grid_inset(cell_size, cell_inset);
+ let px = grid_period(cell_w, gap_width);
+ let py = grid_period(cell_h, gap_width);
+ let inset_x = grid_inset(cell_w, cell_inset);
+ let inset_y = grid_inset(cell_h, cell_inset);
let (cl, cr) = (col0.min(col1), col0.max(col1));
let (rt, rb) = (row0.min(row1), row0.max(row1));
- let x = cl as f64 * p + inset;
- let y = rt as f64 * p + inset;
- let w = (cr - cl) as f64 * p + cell_size - 2.0 * inset;
- let h = (rb - rt) as f64 * p + cell_size - 2.0 * inset;
+ let x = cl as f64 * px + inset_x;
+ let y = rt as f64 * py + inset_y;
+ let w = (cr - cl) as f64 * px + cell_w - 2.0 * inset_x;
+ let h = (rb - rt) as f64 * py + cell_h - 2.0 * inset_y;
(x, y, w, h)
}
@@ -154,13 +158,14 @@ pub fn window_span(
y: f64,
w: f64,
h: f64,
- cell_size: f64,
+ cell_w: f64,
+ cell_h: f64,
gap_width: f64,
) -> (i32, i32, i32, i32) {
- let col0 = cell_index(x, cell_size, gap_width);
- let row0 = cell_index(y, cell_size, gap_width);
- let col1 = cell_index(x + w.max(1.0) - 1.0, cell_size, gap_width).max(col0);
- let row1 = cell_index(y + h.max(1.0) - 1.0, cell_size, gap_width).max(row0);
+ let col0 = cell_index(x, cell_w, gap_width);
+ let row0 = cell_index(y, cell_h, gap_width);
+ let col1 = cell_index(x + w.max(1.0) - 1.0, cell_w, gap_width).max(col0);
+ let row1 = cell_index(y + h.max(1.0) - 1.0, cell_h, gap_width).max(row0);
(col0, row0, col1, row1)
}
@@ -183,10 +188,11 @@ pub fn window_span_label(
y: f64,
w: f64,
h: f64,
- cell_size: f64,
+ cell_w: f64,
+ cell_h: f64,
gap_width: f64,
) -> String {
- let (c0, r0, c1, r1) = window_span(x, y, w, h, cell_size, gap_width);
+ let (c0, r0, c1, r1) = window_span(x, y, w, h, cell_w, cell_h, gap_width);
span_label(c0, r0, c1, r1)
}
@@ -204,7 +210,7 @@ mod tests {
fn origin_square_is_a1() {
assert_eq!(square_label(0, 0), "A1");
// Its content origin is the inset corner, not the raw grid line.
- let (x, y, w, h) = square_rect(0, 0, CELL, GAP, INSET);
+ let (x, y, w, h) = square_rect(0, 0, CELL, CELL, GAP, INSET);
assert_eq!((x, y), (4.0, 4.0));
assert_eq!((w, h), (504.0, 504.0));
}
@@ -234,7 +240,7 @@ mod tests {
let row = cell_index(-4748.0, CELL, GAP);
assert_eq!((col, row), (2, -9));
assert_eq!(square_label(col, row), "C-9");
- let (x, y, _, _) = square_rect(col, row, CELL, GAP, INSET);
+ let (x, y, _, _) = square_rect(col, row, CELL, CELL, GAP, INSET);
assert_eq!((x, y), (1060.0, -4748.0));
}
@@ -269,31 +275,41 @@ mod tests {
#[test]
fn window_span_covers_only_the_squares_it_fills() {
// A window filling exactly one square claims one square.
- let (x, y, w, h) = square_rect(2, -9, CELL, GAP, INSET);
- assert_eq!(window_span(x, y, w, h, CELL, GAP), (2, -9, 2, -9));
- assert_eq!(window_span_label(x, y, w, h, CELL, GAP), "C-9");
+ let (x, y, w, h) = square_rect(2, -9, CELL, CELL, GAP, INSET);
+ assert_eq!(window_span(x, y, w, h, CELL, CELL, GAP), (2, -9, 2, -9));
+ assert_eq!(window_span_label(x, y, w, h, CELL, CELL, GAP), "C-9");
// A 2x1 block claims exactly two columns, not three.
- let (x, y, w, h) = block_rect(2, -9, 3, -9, CELL, GAP, INSET);
- assert_eq!(window_span(x, y, w, h, CELL, GAP), (2, -9, 3, -9));
- assert_eq!(window_span_label(x, y, w, h, CELL, GAP), "C-9:D-9");
+ let (x, y, w, h) = block_rect(2, -9, 3, -9, CELL, CELL, GAP, INSET);
+ assert_eq!(window_span(x, y, w, h, CELL, CELL, GAP), (2, -9, 3, -9));
+ assert_eq!(window_span_label(x, y, w, h, CELL, CELL, GAP), "C-9:D-9");
}
#[test]
fn block_rect_matches_the_tiled_snap_footprint() {
// cells.rs and snap.rs must agree: a block's rect is what tiled_span
// returns for a box spanning those cells.
- let (x, y, w, h) = block_rect(2, -9, 3, -8, CELL, GAP, INSET);
+ let (x, y, w, h) = block_rect(2, -9, 3, -8, CELL, CELL, GAP, INSET);
let (lo_x, hi_x) = crate::snap::tiled_span(x, x + w, CELL, GAP, INSET);
let (lo_y, hi_y) = crate::snap::tiled_span(y, y + h, CELL, GAP, INSET);
assert_eq!((lo_x, hi_x - lo_x), (x, w));
assert_eq!((lo_y, hi_y - lo_y), (y, h));
}
+ #[test]
+ fn rectangular_cells_use_per_axis_periods() {
+ // 512-wide, 256-tall cells: columns step 528, rows step 272.
+ let (x, y, w, h) = square_rect(1, 2, CELL, 256.0, GAP, INSET);
+ assert_eq!((x, y), (532.0, 548.0));
+ assert_eq!((w, h), (504.0, 248.0));
+ assert_eq!(window_span(x, y, w, h, CELL, 256.0, GAP), (1, 2, 1, 2));
+ assert_eq!(cell_index(547.0, 256.0, GAP), 2);
+ }
+
#[test]
fn degenerate_geometry_does_not_panic() {
assert_eq!(cell_index(f64::NAN, CELL, GAP), 0);
assert_eq!(cell_index(10.0, 0.0, 0.0), 0);
- let _ = square_rect(0, 0, 0.0, 0.0, 0.0);
+ let _ = square_rect(0, 0, 0.0, 0.0, 0.0, 0.0);
}
}
diff --git a/src/overview.rs b/src/overview.rs
index 83d992f..9ebb930 100644
--- a/src/overview.rs
+++ b/src/overview.rs
@@ -116,7 +116,7 @@ mod tests {
fn params() -> SnapParams {
// cell 512, no gap, fade inset 4: visible cell k spans
// [512k + 4, 512k + 508].
- SnapParams { cell_size: 512.0, gap_width: 0.0, cell_inset: 4.0, threshold: 24.0 }
+ SnapParams { cell_w: 512.0, cell_h: 512.0, gap_width: 0.0, cell_inset: 4.0, threshold: 24.0 }
}
fn cand(x: f64, y: f64, w: f64, h: f64) -> DisplaceCandidate {
@@ -198,7 +198,7 @@ mod tests {
// had a ~67px dead band around the midpoint: a landing spot ~256px
// from the nearest edge stayed mid-cell, and the arrange pass then
// grew the "tiled" window to every cell it touched.
- let p = SnapParams { cell_size: 512.0, gap_width: 16.0, cell_inset: 4.0, threshold: 24.0 };
+ let p = SnapParams { cell_w: 512.0, cell_h: 512.0, gap_width: 16.0, cell_inset: 4.0, threshold: 24.0 };
// Tiled candidate filling cell row 1 exactly: visible box
// y [532, 1036] → y=532, h=504.
let covered = DisplaceCandidate { x: 4.0, y: 532.0, w: 504.0, h: 504.0, tiled: true };
diff --git a/src/snap.rs b/src/snap.rs
index 8e7140d..7309f9e 100644
--- a/src/snap.rs
+++ b/src/snap.rs
@@ -38,9 +38,11 @@ pub fn tiled_span(x1: f64, x2: f64, cell_size: f64, gap_width: f64, cell_inset:
#[derive(Debug, Clone, Copy)]
pub struct SnapParams {
- /// Desktop grid cell size in virtual units.
- pub cell_size: f64,
- /// Gap between cells; the grid period is `cell_size + gap_width`.
+ /// Desktop grid cell WIDTH in virtual units (the x-axis cell size).
+ pub cell_w: f64,
+ /// Desktop grid cell HEIGHT in virtual units (the y-axis cell size).
+ pub cell_h: f64,
+ /// Gap between cells; each axis's grid period is its cell size + gap.
pub gap_width: f64,
/// Visual inset of a cell's edge (the fade inset).
pub cell_inset: f64,
@@ -48,20 +50,54 @@ pub struct SnapParams {
pub threshold: f64,
}
+/// One axis's view of the grid: the cell size along that axis plus the
+/// shared gap/inset/threshold. All the target math lives here; x/y code
+/// paths differ only in which cell size they carry.
+#[derive(Debug, Clone, Copy)]
+pub struct AxisSnapParams {
+ pub cell_size: f64,
+ pub gap_width: f64,
+ pub cell_inset: f64,
+ pub threshold: f64,
+}
+
impl SnapParams {
+ /// The horizontal axis: columns of width `cell_w`.
+ pub fn x(&self) -> AxisSnapParams {
+ AxisSnapParams {
+ cell_size: self.cell_w,
+ gap_width: self.gap_width,
+ cell_inset: self.cell_inset,
+ threshold: self.threshold,
+ }
+ }
+
+ /// The vertical axis: rows of height `cell_h`.
+ pub fn y(&self) -> AxisSnapParams {
+ AxisSnapParams {
+ cell_size: self.cell_h,
+ gap_width: self.gap_width,
+ cell_inset: self.cell_inset,
+ threshold: self.threshold,
+ }
+ }
+
/// Snapping is aimed in SCREEN space: the configured threshold is the
/// grab distance at zoom 1, and zooming out must not shrink the felt
/// target — so the virtual-space threshold grows by 1/zoom. Capped at
- /// 45% of the cell so a deep zoom-out can't snap from half a cell away
- /// (targets are one period apart; past the midpoint snapping would
- /// thrash between neighbors).
+ /// 45% of the SMALLER cell dimension so a deep zoom-out can't snap from
+ /// half a cell away (targets are one period apart; past the midpoint
+ /// snapping would thrash between neighbors).
pub fn for_zoom(mut self, zoom: f64) -> Self {
if self.threshold > 0.0 && zoom > 0.0 && zoom.is_finite() {
- self.threshold = (self.threshold / zoom).min(self.cell_size * 0.45);
+ self.threshold =
+ (self.threshold / zoom).min(self.cell_w.min(self.cell_h) * 0.45);
}
self
}
+}
+impl AxisSnapParams {
fn enabled(&self) -> bool {
self.threshold > 0.0 && self.cell_size > 0.5
}
@@ -90,7 +126,7 @@ impl SnapParams {
}
}
-fn within(delta: f64, p: &SnapParams) -> bool {
+fn within(delta: f64, p: &AxisSnapParams) -> bool {
delta.abs() <= p.threshold
}
@@ -101,23 +137,24 @@ fn within(delta: f64, p: &SnapParams) -> bool {
/// snap `threshold`: this classifies a resting geometry, it doesn't attract
/// one.
pub fn is_cell_aligned(x: f64, y: f64, w: f64, h: f64, p: &SnapParams, eps: f64) -> bool {
- if p.cell_size <= 0.5 || w <= 0.0 || h <= 0.0 {
+ if p.cell_w <= 0.5 || p.cell_h <= 0.5 || w <= 0.0 || h <= 0.0 {
return false;
}
- (p.nearest_low_target(x) - x).abs() <= eps
- && (p.nearest_high_target(x + w) - (x + w)).abs() <= eps
- && (p.nearest_low_target(y) - y).abs() <= eps
- && (p.nearest_high_target(y + h) - (y + h)).abs() <= eps
+ let (px, py) = (p.x(), p.y());
+ (px.nearest_low_target(x) - x).abs() <= eps
+ && (px.nearest_high_target(x + w) - (x + w)).abs() <= eps
+ && (py.nearest_low_target(y) - y).abs() <= eps
+ && (py.nearest_high_target(y + h) - (y + h)).abs() <= eps
}
/// Snap a window position during a move. On each axis the two content edges
/// compete for their nearest visible cell edge; the closer candidate within
/// the threshold wins. `w`/`h` are content sizes.
pub fn snap_move(x: f64, y: f64, w: f64, h: f64, p: &SnapParams) -> (f64, f64) {
- (snap_move_axis(x, w, p), snap_move_axis(y, h, p))
+ (snap_move_axis(x, w, &p.x()), snap_move_axis(y, h, &p.y()))
}
-fn snap_move_axis(pos: f64, len: f64, p: &SnapParams) -> f64 {
+fn snap_move_axis(pos: f64, len: f64, p: &AxisSnapParams) -> f64 {
if !p.enabled() {
return pos;
}
@@ -135,8 +172,9 @@ fn snap_move_axis(pos: f64, len: f64, p: &SnapParams) -> f64 {
}
/// Snap the dragged left/top CONTENT edge during a resize onto the nearest
-/// visible left/top cell edge.
-pub fn snap_low_edge(pos: f64, p: &SnapParams) -> f64 {
+/// visible left/top cell edge. Takes the axis view: `p.x()` when dragging a
+/// left edge, `p.y()` for a top edge.
+pub fn snap_low_edge(pos: f64, p: &AxisSnapParams) -> f64 {
if !p.enabled() {
return pos;
}
@@ -149,8 +187,9 @@ pub fn snap_low_edge(pos: f64, p: &SnapParams) -> f64 {
}
/// Snap the dragged right/bottom CONTENT edge during a resize onto the
-/// nearest visible right/bottom cell edge.
-pub fn snap_high_edge(pos: f64, p: &SnapParams) -> f64 {
+/// nearest visible right/bottom cell edge. Takes the axis view like
+/// [`snap_low_edge`].
+pub fn snap_high_edge(pos: f64, p: &AxisSnapParams) -> f64 {
if !p.enabled() {
return pos;
}
@@ -177,18 +216,18 @@ pub fn snap_high_edge(pos: f64, p: &SnapParams) -> f64 {
/// definition (that is what `tiled_span` renders), so disabling magnetic
/// snapping must not strand it off-grid.
pub fn snap_move_tiled(x: f64, y: f64, p: &SnapParams) -> (f64, f64) {
- if p.cell_size <= 0.5 {
- return (x, y);
- }
- (p.nearest_low_target(x), p.nearest_low_target(y))
+ let nx = if p.cell_w > 0.5 { p.x().nearest_low_target(x) } else { x };
+ let ny = if p.cell_h > 0.5 { p.y().nearest_low_target(y) } else { y };
+ (nx, ny)
}
/// One axis of an interactive resize: the dragged content edge (low =
/// left/top, high = right/bottom) follows the pointer delta and snaps to the
/// visible cell edges; the opposite edge stays anchored. Returns the new
-/// content length, at least `min_len`. The single source of this math —
-/// both the seat op and the arrange snapshot derive sizes from it, so the
-/// snapped result can't be overridden by an unsnapped recomputation.
+/// content length, at least `min_len`. Takes the axis view (`p.x()` for
+/// width, `p.y()` for height). The single source of this math — both the
+/// seat op and the arrange snapshot derive sizes from it, so the snapped
+/// result can't be overridden by an unsnapped recomputation.
pub fn resize_axis(
start_pos: f64,
start_len: f64,
@@ -196,7 +235,7 @@ pub fn resize_axis(
dragging_low: bool,
dragging_high: bool,
min_len: f64,
- p: &SnapParams,
+ p: &AxisSnapParams,
) -> f64 {
if dragging_low {
let low = snap_low_edge(start_pos + delta, p);
@@ -213,34 +252,57 @@ pub fn resize_axis(
mod tests {
use super::*;
- /// cell 512, no gap, fade inset 4: visible cell k spans
+ /// Square 512 cells, no gap, fade inset 4: visible cell k spans
/// [512k + 4, 512k + 508]. Border width is irrelevant to snapping now —
/// content edges land on the targets and the border overhangs outward.
fn params() -> SnapParams {
- SnapParams { cell_size: 512.0, gap_width: 0.0, cell_inset: 4.0, threshold: 24.0 }
+ SnapParams {
+ cell_w: 512.0,
+ cell_h: 512.0,
+ gap_width: 0.0,
+ cell_inset: 4.0,
+ threshold: 24.0,
+ }
}
#[test]
fn resize_low_edge_abuts_visible_cell_edge() {
// Content left 510 → visible edge 516 (dist 6) → content 516.
- assert_eq!(snap_low_edge(510.0, ¶ms()), 516.0);
+ assert_eq!(snap_low_edge(510.0, ¶ms().x()), 516.0);
// Far from an edge: unchanged.
- assert_eq!(snap_low_edge(300.0, ¶ms()), 300.0);
+ assert_eq!(snap_low_edge(300.0, ¶ms().x()), 300.0);
}
#[test]
fn resize_high_edge_abuts_visible_cell_edge() {
// Content right 1000 → visible edge 1020 (2*512 - 4, dist 20) → 1020.
- assert_eq!(snap_high_edge(1000.0, ¶ms()), 1020.0);
+ assert_eq!(snap_high_edge(1000.0, ¶ms().x()), 1020.0);
}
#[test]
fn gap_width_shifts_the_period() {
// cell 500 + gap 12 → period 512; cell 1's rect spans [512, 1012],
// visibly [516, 1008].
- let p = SnapParams { cell_size: 500.0, gap_width: 12.0, ..params() };
- assert_eq!(snap_low_edge(520.0, &p), 516.0);
- assert_eq!(snap_high_edge(996.0, &p), 1008.0);
+ let p = SnapParams { cell_w: 500.0, cell_h: 500.0, gap_width: 12.0, ..params() };
+ assert_eq!(snap_low_edge(520.0, &p.x()), 516.0);
+ assert_eq!(snap_high_edge(996.0, &p.x()), 1008.0);
+ }
+
+ #[test]
+ fn rectangular_cells_snap_each_axis_to_its_own_size() {
+ // 512-wide, 256-tall cells, no gap, inset 4: x targets every 512,
+ // y targets every 256 — row 1's visible top edge is 260.
+ let p = SnapParams { cell_h: 256.0, ..params() };
+ let (x, y) = snap_move(510.0, 250.0, 300.0, 100.0, &p);
+ assert_eq!((x, y), (516.0, 260.0));
+ // The hard tiled snap uses per-axis periods the same way.
+ assert_eq!(snap_move_tiled(300.0, 300.0, &p), (516.0, 260.0));
+ // A box filling one 504x248 visible cell is aligned, as is a
+ // two-row 504-tall box (2*256 - 8); a height off the row grid is
+ // not.
+ assert!(is_cell_aligned(4.0, 4.0, 504.0, 248.0, &p, 1.0));
+ assert!(is_cell_aligned(4.0, 4.0, 504.0, 504.0, &p, 1.0));
+ assert!(!is_cell_aligned(4.0, 4.0, 504.0, 400.0, &p, 1.0));
}
#[test]
@@ -267,13 +329,13 @@ mod tests {
fn resize_axis_snaps_the_dragged_edge_only() {
// Window [600, 900), dragging the left edge to 510: visible edge 516
// → content 516; anchored right edge 900 keeps the width at 384.
- assert_eq!(resize_axis(600.0, 300.0, -90.0, true, false, 50.0, ¶ms()), 384.0);
+ assert_eq!(resize_axis(600.0, 300.0, -90.0, true, false, 50.0, ¶ms().x()), 384.0);
// Dragging the right edge to 1000: visible edge 1020 → width 420.
- assert_eq!(resize_axis(600.0, 300.0, 100.0, false, true, 50.0, ¶ms()), 420.0);
+ assert_eq!(resize_axis(600.0, 300.0, 100.0, false, true, 50.0, ¶ms().x()), 420.0);
// Not dragging this axis: length unchanged.
- assert_eq!(resize_axis(600.0, 300.0, 100.0, false, false, 50.0, ¶ms()), 300.0);
+ assert_eq!(resize_axis(600.0, 300.0, 100.0, false, false, 50.0, ¶ms().x()), 300.0);
// Minimum clamps.
- assert_eq!(resize_axis(600.0, 300.0, 290.0, true, false, 50.0, ¶ms()), 50.0);
+ assert_eq!(resize_axis(600.0, 300.0, 290.0, true, false, 50.0, ¶ms().x()), 50.0);
}
#[test]
@@ -324,7 +386,7 @@ mod tests {
fn zero_threshold_disables() {
let p = SnapParams { threshold: 0.0, ..params() };
assert_eq!(snap_move(510.0, 300.0, 300.0, 100.0, &p), (510.0, 300.0));
- assert_eq!(snap_low_edge(510.0, &p), 510.0);
+ assert_eq!(snap_low_edge(510.0, &p.x()), 510.0);
}
#[test]
@@ -361,7 +423,7 @@ mod tests {
let p = SnapParams { threshold: 0.0, ..params() };
assert_eq!(snap_move_tiled(300.0, 300.0, &p), (516.0, 516.0));
// A degenerate cell size is left alone rather than dividing by ~zero.
- let p = SnapParams { cell_size: 0.0, ..params() };
+ let p = SnapParams { cell_w: 0.0, cell_h: 0.0, ..params() };
assert_eq!(snap_move_tiled(300.0, 300.0, &p), (300.0, 300.0));
}
}