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}