agg_gui/widgets/window/snap_glue.rs
1//! Snap-layout glue for [`super::Window`]: the drag/resize snap passes, the
2//! internal→engine direction mapping, and the [`Snappable`](crate::snap::Snappable)
3//! trait impl. Extracted from `window.rs` so that file stays under the 800-line
4//! guardrail; the logic is unchanged, it just lives in its own cohesive module.
5
6use super::{snap, ResizeDir, Window};
7use crate::geometry::Rect;
8
9impl Window {
10 /// Snap pass for a title-bar drag. Skipped entirely when the
11 /// global toggle is off — cheap when not in use. Replaces
12 /// `self.bounds` with the engine's snapped result and writes the
13 /// guide list for `SnapOverlay` to paint.
14 pub(crate) fn apply_move_snap(&mut self) {
15 if !crate::snap::is_enabled() {
16 crate::snap::clear_guides();
17 return;
18 }
19 let targets = crate::snap::targets_snapshot();
20 // The registry and guides live in canvas-absolute space (see
21 // `widget_impl`'s `register_target`). A nested modal dialog's `bounds`
22 // are slot-local, so translate into canvas space by the cached slot
23 // offset before snapping, then translate the snapped rect back. Zero
24 // offset for a top-level window makes this a no-op.
25 let (ox, oy) = self.world_offset.get();
26 let candidate = Rect::new(
27 self.bounds.x + ox,
28 self.bounds.y + oy,
29 self.bounds.width,
30 self.bounds.height,
31 );
32 let result = crate::snap::compute_snap(
33 candidate,
34 self.snap_id,
35 &targets,
36 crate::snap::DEFAULT_THRESHOLD,
37 crate::snap::SnapMode::Move,
38 );
39 let r = result.rect;
40 self.bounds = snap(Rect::new(r.x - ox, r.y - oy, r.width, r.height));
41 crate::snap::set_guides(result.guides);
42 }
43
44 /// Snap pass for an edge / corner resize drag. Only edges that
45 /// the active handle is allowed to move can snap — the engine
46 /// enforces that internally via `SnapMode::Resize`.
47 pub(crate) fn apply_resize_snap(&mut self, dir: ResizeDir) {
48 if !crate::snap::is_enabled() {
49 crate::snap::clear_guides();
50 return;
51 }
52 let targets = crate::snap::targets_snapshot();
53 let edge = resize_dir_to_snap_edge(dir);
54 // Same canvas-absolute translation as `apply_move_snap` — resize snaps
55 // against the same registry, so a nested dialog's slot-local bounds must
56 // be lifted into canvas space and the result folded back.
57 let (ox, oy) = self.world_offset.get();
58 let candidate = Rect::new(
59 self.bounds.x + ox,
60 self.bounds.y + oy,
61 self.bounds.width,
62 self.bounds.height,
63 );
64 let result = crate::snap::compute_snap(
65 candidate,
66 self.snap_id,
67 &targets,
68 crate::snap::DEFAULT_THRESHOLD,
69 crate::snap::SnapMode::Resize(edge),
70 );
71 let r = result.rect;
72 self.bounds = snap(Rect::new(r.x - ox, r.y - oy, r.width, r.height));
73 crate::snap::set_guides(result.guides);
74 }
75}
76
77/// Map an internal `ResizeDir` to the snap engine's compass-direction
78/// enum. Kept private — the snap engine owns its own enum so the
79/// engine isn't coupled to the Window widget.
80fn resize_dir_to_snap_edge(dir: ResizeDir) -> crate::snap::ResizeEdge {
81 use crate::snap::ResizeEdge as E;
82 match dir {
83 ResizeDir::N => E::North,
84 ResizeDir::NE => E::NorthEast,
85 ResizeDir::E => E::East,
86 ResizeDir::SE => E::SouthEast,
87 ResizeDir::S => E::South,
88 ResizeDir::SW => E::SouthWest,
89 ResizeDir::W => E::West,
90 ResizeDir::NW => E::NorthWest,
91 }
92}
93
94impl crate::snap::Snappable for Window {
95 fn snap_id(&self) -> crate::snap::SnapId {
96 self.snap_id
97 }
98 fn snap_rect(&self) -> Rect {
99 self.bounds
100 }
101 fn set_snap_rect(&mut self, r: Rect) {
102 self.bounds = snap(r);
103 }
104 fn is_snap_source(&self) -> bool {
105 self.requested_visible() && !self.maximized
106 }
107 fn is_snap_target(&self) -> bool {
108 // Maximized windows fill the canvas — pulling siblings to
109 // their edges would just glue everything to the canvas
110 // perimeter, which isn't useful as a layout aid. Hidden
111 // windows aren't valid targets either.
112 self.requested_visible() && !self.maximized
113 }
114}