use crate::geometry::Rect;
use super::model::{ResizeEdge, SnapGuide, SnapId, SnapMode, SnapResult};
mod spacing;
pub(super) use spacing::{horizontal_equal_spacing, vertical_equal_spacing};
use spacing::{horizontal_resize_spacing, vertical_resize_spacing};
pub fn compute_snap(
moving: Rect,
moving_id: SnapId,
targets: &[(SnapId, Rect)],
threshold: f64,
mode: SnapMode,
) -> SnapResult {
if targets.is_empty() || threshold <= 0.0 {
return SnapResult {
rect: moving,
guides: Vec::new(),
};
}
let neighbours: Vec<Rect> = targets
.iter()
.filter_map(|(id, r)| (id != &moving_id).then_some(*r))
.collect();
if neighbours.is_empty() {
return SnapResult {
rect: moving,
guides: Vec::new(),
};
}
let mut rect = moving;
let mut guides: Vec<SnapGuide> = Vec::new();
let allow_left = match mode {
SnapMode::Move => true,
SnapMode::Resize(e) => e.affects_left(),
};
let allow_right = match mode {
SnapMode::Move => true,
SnapMode::Resize(e) => e.affects_right(),
};
let allow_top = match mode {
SnapMode::Move => true,
SnapMode::Resize(e) => e.affects_top(),
};
let allow_bottom = match mode {
SnapMode::Move => true,
SnapMode::Resize(e) => e.affects_bottom(),
};
let allow_cx = allow_left && allow_right;
let allow_cy = allow_top && allow_bottom;
let mut x_edge_engaged = false;
let mut y_edge_engaged = false;
if let Some(snap) = best_x_alignment(
rect,
&neighbours,
threshold,
allow_left,
allow_right,
allow_cx,
) {
if matches!(mode, SnapMode::Move) {
rect.x += snap.delta;
} else if let SnapMode::Resize(e) = mode {
apply_resize_x(&mut rect, snap.edge, snap.delta, e);
}
guides.push(SnapGuide::VLine {
x: snap.x,
y0: y_span(rect, snap.target_rect).0,
y1: y_span(rect, snap.target_rect).1,
});
x_edge_engaged = true;
}
if let Some(snap) = best_y_alignment(
rect,
&neighbours,
threshold,
allow_top,
allow_bottom,
allow_cy,
) {
if matches!(mode, SnapMode::Move) {
rect.y += snap.delta;
} else if let SnapMode::Resize(e) = mode {
apply_resize_y(&mut rect, snap.edge, snap.delta, e);
}
guides.push(SnapGuide::HLine {
y: snap.y,
x0: x_span(rect, snap.target_rect).0,
x1: x_span(rect, snap.target_rect).1,
});
y_edge_engaged = true;
}
match mode {
SnapMode::Move => {
if !x_edge_engaged {
if let Some(spacing) = horizontal_equal_spacing(rect, &neighbours, threshold) {
rect.x += spacing.delta;
guides.extend(spacing.guides);
}
}
if !y_edge_engaged {
if let Some(spacing) = vertical_equal_spacing(rect, &neighbours, threshold) {
rect.y += spacing.delta;
guides.extend(spacing.guides);
}
}
}
SnapMode::Resize(e) => {
if !x_edge_engaged {
if let Some(s) = horizontal_resize_spacing(rect, &neighbours, threshold, e) {
apply_resize_x_spacing(&mut rect, e, s.delta);
guides.extend(s.guides);
}
}
if !y_edge_engaged {
if let Some(s) = vertical_resize_spacing(rect, &neighbours, threshold, e) {
apply_resize_y_spacing(&mut rect, e, s.delta);
guides.extend(s.guides);
}
}
}
}
SnapResult { rect, guides }
}
#[derive(Clone, Copy, Debug)]
enum MovingEdge {
Left,
Right,
CenterX,
Top,
Bottom,
CenterY,
}
#[derive(Clone, Copy, Debug)]
struct AlignmentX {
delta: f64,
x: f64,
edge: MovingEdge,
target_rect: Rect,
}
#[derive(Clone, Copy, Debug)]
struct AlignmentY {
delta: f64,
y: f64,
edge: MovingEdge,
target_rect: Rect,
}
fn best_x_alignment(
moving: Rect,
targets: &[Rect],
threshold: f64,
allow_left: bool,
allow_right: bool,
allow_cx: bool,
) -> Option<AlignmentX> {
let mut best: Option<AlignmentX> = None;
let m_left = moving.x;
let m_right = moving.x + moving.width;
let m_cx = moving.x + moving.width * 0.5;
for t in targets {
let t_left = t.x;
let t_right = t.x + t.width;
let t_cx = t.x + t.width * 0.5;
let moving_edges: [(MovingEdge, f64, bool); 3] = [
(MovingEdge::Left, m_left, allow_left),
(MovingEdge::Right, m_right, allow_right),
(MovingEdge::CenterX, m_cx, allow_cx),
];
let target_points = [t_left, t_right, t_cx];
for (edge, mv, enabled) in moving_edges.iter().copied() {
if !enabled {
continue;
}
for tp in target_points {
let delta = tp - mv;
if delta.abs() <= threshold {
let cand = AlignmentX {
delta,
x: tp,
edge,
target_rect: *t,
};
best = match best {
None => Some(cand),
Some(prev) if delta.abs() < prev.delta.abs() => Some(cand),
Some(prev) => Some(prev),
};
}
}
}
}
best
}
fn best_y_alignment(
moving: Rect,
targets: &[Rect],
threshold: f64,
allow_top: bool,
allow_bottom: bool,
allow_cy: bool,
) -> Option<AlignmentY> {
let mut best: Option<AlignmentY> = None;
let m_bottom = moving.y;
let m_top = moving.y + moving.height;
let m_cy = moving.y + moving.height * 0.5;
for t in targets {
let t_bottom = t.y;
let t_top = t.y + t.height;
let t_cy = t.y + t.height * 0.5;
let moving_edges: [(MovingEdge, f64, bool); 3] = [
(MovingEdge::Bottom, m_bottom, allow_bottom),
(MovingEdge::Top, m_top, allow_top),
(MovingEdge::CenterY, m_cy, allow_cy),
];
let target_points = [t_bottom, t_top, t_cy];
for (edge, mv, enabled) in moving_edges.iter().copied() {
if !enabled {
continue;
}
for tp in target_points {
let delta = tp - mv;
if delta.abs() <= threshold {
let cand = AlignmentY {
delta,
y: tp,
edge,
target_rect: *t,
};
best = match best {
None => Some(cand),
Some(prev) if delta.abs() < prev.delta.abs() => Some(cand),
Some(prev) => Some(prev),
};
}
}
}
}
best
}
fn apply_resize_x(rect: &mut Rect, edge: MovingEdge, delta: f64, active: ResizeEdge) {
match edge {
MovingEdge::Left if active.affects_left() => {
rect.x += delta;
rect.width -= delta;
}
MovingEdge::Right if active.affects_right() => {
rect.width += delta;
}
_ => {}
}
}
fn apply_resize_y(rect: &mut Rect, edge: MovingEdge, delta: f64, active: ResizeEdge) {
match edge {
MovingEdge::Bottom if active.affects_bottom() => {
rect.y += delta;
rect.height -= delta;
}
MovingEdge::Top if active.affects_top() => {
rect.height += delta;
}
_ => {}
}
}
fn apply_resize_x_spacing(rect: &mut Rect, edge: ResizeEdge, delta: f64) {
if edge.affects_right() {
rect.width += delta;
} else if edge.affects_left() {
rect.x += delta;
rect.width -= delta;
}
}
fn apply_resize_y_spacing(rect: &mut Rect, edge: ResizeEdge, delta: f64) {
if edge.affects_top() {
rect.height += delta;
} else if edge.affects_bottom() {
rect.y += delta;
rect.height -= delta;
}
}
fn y_span(a: Rect, b: Rect) -> (f64, f64) {
let y0 = a.y.min(b.y);
let y1 = (a.y + a.height).max(b.y + b.height);
(y0, y1)
}
fn x_span(a: Rect, b: Rect) -> (f64, f64) {
let x0 = a.x.min(b.x);
let x1 = (a.x + a.width).max(b.x + b.width);
(x0, x1)
}