//! The operation that aligns one rectangle with a group of other rectangles. //! //! A user moves an output with the keyboard or with the mouse. The movement is //! not accurate. This module moves the output a small distance more. The result //! is an arrangement with no gap and no overlap. use crate::axis::{Axis, Side}; use crate::space::{Rect, Vector}; /// The way that the moved rectangle makes contact with a target rectangle. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Contact { /// One side of the moved rectangle aligns with one side of the target. Edges { /// The side of the moved rectangle. moved: Side, /// The side of the target rectangle. target: Side, }, /// The center of the moved rectangle aligns with the center of the target. Centers, } /// A contact between the moved rectangle and one target rectangle. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct SnapContact { /// The position of the target rectangle in the list of the targets. pub target: usize, /// The type of the contact. pub contact: Contact, /// The distance to move the rectangle to make the contact. pub delta: i32, } /// The limits for a snap operation. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct SnapConfig { /// The largest distance in logical pixels that a snap operation moves. pub threshold: i32, /// Set this field to `true` to align the centers of the rectangles. pub align_centers: bool, } impl Default for SnapConfig { fn default() -> Self { Self { threshold: 64, align_centers: true, } } } /// The result of a snap operation. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Snap { /// The movement to apply to the moved rectangle. pub offset: Vector, /// The contact on the horizontal axis. pub horizontal: Option, /// The contact on the vertical axis. pub vertical: Option, } impl Default for Snap { fn default() -> Self { Self { offset: Vector::zero(), horizontal: None, vertical: None, } } } impl Snap { /// Tell if the operation found a contact on one axis or on the two axes. #[must_use] pub const fn is_attached(&self) -> bool { self.horizontal.is_some() || self.vertical.is_some() } /// Give the position of the moved rectangle after the snap operation. #[must_use] pub fn apply(&self, moved: Rect) -> Rect { moved.translate(self.offset) } } /// Move a rectangle a small distance to align it with the target rectangles. /// /// The operation examines the two axes separately. On each axis the operation /// finds the contact with the smallest distance. The operation ignores a /// contact if the distance is more than the threshold. /// /// The list of the targets must not contain the moved rectangle. #[must_use] pub fn snap_rect(moved: Rect, targets: &[Rect], config: SnapConfig) -> Snap { let horizontal = best_contact(moved, targets, Axis::Horizontal, config); let vertical = best_contact(moved, targets, Axis::Vertical, config); let offset = Vector::new( horizontal.map_or(0, |contact| contact.delta), vertical.map_or(0, |contact| contact.delta), ); Snap { offset, horizontal, vertical, } } fn best_contact( moved: Rect, targets: &[Rect], axis: Axis, config: SnapConfig, ) -> Option { let mut best: Option = None; for (index, target) in targets.iter().enumerate() { for candidate in candidates(moved, *target, axis, index, config) { if candidate.delta.abs() > config.threshold { continue; } let is_better = best.is_none_or(|current| candidate.delta.abs() < current.delta.abs()); if is_better { best = Some(candidate); } } } best } fn candidates( moved: Rect, target: Rect, axis: Axis, index: usize, config: SnapConfig, ) -> Vec { let mut found = Vec::with_capacity(5); for moved_side in axis.sides() { for target_side in axis.sides() { found.push(SnapContact { target: index, contact: Contact::Edges { moved: moved_side, target: target_side, }, delta: target_side.coordinate_of(target) - moved_side.coordinate_of(moved), }); } } if config.align_centers { found.push(SnapContact { target: index, contact: Contact::Centers, delta: axis.center_of(target) - axis.center_of(moved), }); } found } #[cfg(test)] mod tests { use super::*; use crate::space::{Point, Size, rect_at}; fn rect(x: i32, y: i32, w: i32, h: i32) -> Rect { rect_at(Point::new(x, y), Size::new(w, h)) } fn strict() -> SnapConfig { SnapConfig { threshold: 64, align_centers: false, } } #[test] fn a_rectangle_near_a_target_moves_to_touch_it() { let target = rect(0, 0, 1920, 1080); let moved = rect(1910, 0, 1920, 1080); let snap = snap_rect(moved, &[target], strict()); assert_eq!(snap.apply(moved), rect(1920, 0, 1920, 1080)); } #[test] fn a_distant_rectangle_does_not_move() { let target = rect(0, 0, 1920, 1080); let moved = rect(3000, 2000, 1920, 1080); let snap = snap_rect(moved, &[target], strict()); assert_eq!(snap.offset, Vector::zero()); assert!(!snap.is_attached()); } #[test] fn the_operation_never_moves_more_than_the_threshold() { let target = rect(0, 0, 1920, 1080); let config = SnapConfig { threshold: 10, align_centers: true, }; for x in -400..400 { let moved = rect(x, 0, 800, 600); let snap = snap_rect(moved, &[target], config); assert!(snap.offset.x.abs() <= config.threshold); assert!(snap.offset.y.abs() <= config.threshold); } } #[test] fn the_operation_is_stable_when_it_repeats() { let target = rect(0, 0, 1920, 1080); let moved = rect(1908, 12, 1280, 1024); let first = snap_rect(moved, &[target], SnapConfig::default()).apply(moved); let second = snap_rect(first, &[target], SnapConfig::default()).apply(first); assert_eq!(first, second); } #[test] fn an_empty_target_list_gives_no_contact() { let moved = rect(10, 10, 100, 100); let snap = snap_rect(moved, &[], SnapConfig::default()); assert_eq!(snap.apply(moved), moved); assert!(!snap.is_attached()); } #[test] fn the_two_axes_snap_independently() { let target = rect(0, 0, 1920, 1080); let moved = rect(1918, 1076, 800, 600); let snap = snap_rect(moved, &[target], strict()); assert_eq!(snap.apply(moved).min, Point::new(1920, 1080)); } }