Skip to main content

agg_gui/snap/
model.rs

1//! Data model for the snap engine.
2//!
3//! Defines the [`Snappable`] trait that movable types implement, the
4//! input enums that select snap behaviour ([`SnapMode`],
5//! [`ResizeEdge`]), and the result types ([`SnapResult`],
6//! [`SnapGuide`]).
7//!
8//! Kept free of widget / event dependencies so the engine can be
9//! re-used from any drag handler (window manager, node graph,
10//! diagram editor, etc.).
11
12use crate::geometry::Rect;
13
14/// Opaque identifier for a snappable rect.  Used by [`compute_snap`]
15/// to skip self-matches when the moving rect is also present in the
16/// target list.  The `u64` payload is opaque to the engine; callers
17/// pick a scheme that makes their rects distinguishable — pointer
18/// values cast to `u64`, monotonic ids, hash of a name, anything that
19/// is unique per logical entity.
20///
21/// [`compute_snap`]: crate::snap::compute_snap
22#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
23pub struct SnapId(pub u64);
24
25/// What kind of drag operation produced the candidate rect.  Moving
26/// snaps treat the whole rect; resize snaps only consider edges that
27/// the active resize handle actually controls, so a resize from the
28/// right edge can't snap the LEFT side of the moving rect to a
29/// target.
30#[derive(Clone, Copy, Debug, PartialEq, Eq)]
31pub enum SnapMode {
32    /// The whole rect is being translated — every edge is fair game
33    /// for snapping.  Equal-spacing detection runs.
34    Move,
35    /// The rect is being resized via a specific edge / corner — only
36    /// the affected edges may snap.  Equal-spacing detection is
37    /// suppressed (it doesn't make geometric sense for a resize).
38    Resize(ResizeEdge),
39}
40
41/// Which edge (or corner) is currently driving a resize.  Eight
42/// compass directions cover the standard handle layout.
43#[derive(Clone, Copy, Debug, PartialEq, Eq)]
44pub enum ResizeEdge {
45    North,
46    NorthEast,
47    East,
48    SouthEast,
49    South,
50    SouthWest,
51    West,
52    NorthWest,
53}
54
55impl ResizeEdge {
56    /// Does this resize handle move the LEFT edge of the moving rect?
57    pub fn affects_left(self) -> bool {
58        matches!(
59            self,
60            ResizeEdge::West | ResizeEdge::NorthWest | ResizeEdge::SouthWest
61        )
62    }
63    /// Does this resize handle move the RIGHT edge of the moving rect?
64    pub fn affects_right(self) -> bool {
65        matches!(
66            self,
67            ResizeEdge::East | ResizeEdge::NorthEast | ResizeEdge::SouthEast
68        )
69    }
70    /// Does this resize handle move the TOP edge of the moving rect?
71    /// "Top" is the higher Y in Y-up coords.
72    pub fn affects_top(self) -> bool {
73        matches!(
74            self,
75            ResizeEdge::North | ResizeEdge::NorthEast | ResizeEdge::NorthWest
76        )
77    }
78    /// Does this resize handle move the BOTTOM edge of the moving rect?
79    /// "Bottom" is the lower Y in Y-up coords.
80    pub fn affects_bottom(self) -> bool {
81        matches!(
82            self,
83            ResizeEdge::South | ResizeEdge::SouthEast | ResizeEdge::SouthWest
84        )
85    }
86}
87
88/// Trait implemented by anything that wants to participate in the
89/// snap layout system.  Rust's equivalent of a C# interface: callers
90/// take `&dyn Snappable` (or `&mut dyn Snappable`) and the engine
91/// reads / writes the rect through these accessors without caring
92/// about the underlying concrete type.
93///
94/// The two opt-in flags ([`is_snap_source`], [`is_snap_target`])
95/// default to `true` — implementors only override when an instance
96/// should sit out (e.g., a window is hidden / minimised and shouldn't
97/// pull other windows toward an off-screen edge).
98///
99/// [`is_snap_source`]: Snappable::is_snap_source
100/// [`is_snap_target`]: Snappable::is_snap_target
101pub trait Snappable {
102    /// Identity used to skip self-matches in the target list.
103    fn snap_id(&self) -> SnapId;
104
105    /// Current rect — both the source of the moving candidate and
106    /// what the engine reads to build the target list for stationary
107    /// neighbors.
108    fn snap_rect(&self) -> Rect;
109
110    /// Apply a snapped rect back to the implementor.  Called once per
111    /// drag tick after [`compute_snap`] returns.  Default implementors
112    /// just overwrite their internal bounds with `r`.
113    ///
114    /// [`compute_snap`]: crate::snap::compute_snap
115    fn set_snap_rect(&mut self, r: Rect);
116
117    /// `false` to opt this rect OUT of being the moving rect in a
118    /// snap.  Rare — most movable things stay sources.
119    fn is_snap_source(&self) -> bool {
120        true
121    }
122
123    /// `false` to opt this rect OUT of being a snap target for other
124    /// rects (the moving rect still won't snap to it).  Useful for
125    /// transient overlays, hidden windows, etc.
126    fn is_snap_target(&self) -> bool {
127        true
128    }
129}
130
131/// Output of [`compute_snap`].  The engine returns BOTH the corrected
132/// rect (with any snap adjustments applied) AND the visual guides the
133/// drag UI should render — alignment lines, equal-spacing dimension
134/// markers, etc.  The dragger persists the rect; the guide overlay
135/// reads `guides` and paints them on top.
136///
137/// [`compute_snap`]: crate::snap::compute_snap
138#[derive(Clone, Debug, Default)]
139pub struct SnapResult {
140    /// Adjusted rect, ready to apply via [`Snappable::set_snap_rect`].
141    /// When no snap fired the engine returns this unchanged from the
142    /// input candidate.
143    pub rect: Rect,
144    /// Visual guides for the drag overlay to render.  Empty when no
145    /// snap engaged.
146    pub guides: Vec<SnapGuide>,
147}
148
149/// One visual guide.  Vertical / horizontal lines come from
150/// edge-alignment snaps; spacing markers come from equal-gap
151/// detection.  Coordinates are in the same space as the input rects
152/// — typically the scene's root space.
153#[derive(Clone, Copy, Debug, PartialEq)]
154pub enum SnapGuide {
155    /// Vertical alignment line at `x`, drawn from `y0` to `y1` to
156    /// span the moving rect plus the target it snapped against.
157    VLine { x: f64, y0: f64, y1: f64 },
158    /// Horizontal alignment line at `y`, drawn from `x0` to `x1`.
159    HLine { y: f64, x0: f64, x1: f64 },
160    /// Horizontal spacing marker — the engine matched the moving
161    /// rect's horizontal gap to an existing gap between two
162    /// neighbors.  Drawn as a dimension line at vertical `y` between
163    /// `x0`..`x1`.
164    HSpacing { y: f64, x0: f64, x1: f64 },
165    /// Vertical spacing marker — equal-gap match on the vertical
166    /// axis.  Drawn as a dimension line at horizontal `x` between
167    /// `y0`..`y1`.
168    VSpacing { x: f64, y0: f64, y1: f64 },
169}