agg_gui/snap/mod.rs
1//! Generic snap-layout system for movable + resizable rects.
2//!
3//! Reusable across `agg-gui`'s `Window`, AdamArtist's node graph,
4//! and any third-party widget whose primary state is a rect that
5//! drags and resizes. The engine is pure — it knows nothing about
6//! widgets, events, or paint — so it can be wired into any drag
7//! handler that produces a candidate rect and wants a snapped result.
8//!
9//! ## Pattern
10//!
11//! 1. Implement [`Snappable`] on your movable type — three accessors
12//! (`snap_id`, `snap_rect`, `set_snap_rect`) and two opt-in flags.
13//! 2. When the user drags, collect `(SnapId, Rect)` tuples for every
14//! *other* visible Snappable in the scene (skip the dragger).
15//! 3. Call [`compute_snap`] with the dragger's candidate rect, the
16//! target list, a pixel threshold, and a [`SnapMode`].
17//! 4. Apply the returned [`SnapResult::rect`] back through
18//! `set_snap_rect`; render the returned [`SnapGuide`]s as overlay
19//! lines.
20//!
21//! ## Global enable flag
22//!
23//! Snapping is gated behind a thread-local flag managed via
24//! [`is_enabled`] and [`set_enabled`]. Drag handlers should check
25//! `is_enabled()` first and skip the engine entirely when off — keeps
26//! the gate at the call site so individual widgets don't pay any cost
27//! when snapping is disabled. The flag defaults to `true` so any
28//! consumer of agg-gui gets window/node snapping out of the box;
29//! call [`set_enabled(false)`] to opt out.
30
31mod engine;
32mod model;
33mod overlay;
34mod registry;
35
36#[cfg(test)]
37mod tests;
38
39pub use engine::compute_snap;
40pub use model::{ResizeEdge, SnapGuide, SnapId, SnapMode, SnapResult, Snappable};
41pub use overlay::SnapOverlay;
42pub use registry::{
43 clear_guides, guides_snapshot, register_target, set_guides, targets_snapshot, unregister_target,
44};
45
46/// Default pixel distance at which an alignment / spacing match
47/// engages. Apps can pass a different value to [`compute_snap`]
48/// directly; this is the value drag handlers should reach for when
49/// they have no specific reason to override it.
50pub const DEFAULT_THRESHOLD: f64 = 8.0;
51
52/// Mint a fresh [`SnapId`] from a process-wide atomic counter.
53/// Cheap — single relaxed increment — so widgets can call it from
54/// their constructor without caring about contention.
55pub fn next_snap_id() -> SnapId {
56 use std::sync::atomic::{AtomicU64, Ordering};
57 static COUNTER: AtomicU64 = AtomicU64::new(1);
58 SnapId(COUNTER.fetch_add(1, Ordering::Relaxed))
59}
60
61use std::cell::Cell;
62
63thread_local! {
64 // On by default — see the module-level "Global enable flag" docs.
65 static ENABLED: Cell<bool> = const { Cell::new(true) };
66}
67
68/// `true` if snapping should run during drag/resize operations.
69/// Drag handlers must check this and skip the snap path entirely
70/// when off.
71pub fn is_enabled() -> bool {
72 ENABLED.with(|c| c.get())
73}
74
75/// Toggle the global snap-enable flag. Persists for the lifetime of
76/// the thread — typically wired to a UI checkbox (see the demo's
77/// `View > Window Snapping` menu item) and to saved-state
78/// persistence so the user's preference survives a relaunch.
79pub fn set_enabled(on: bool) {
80 ENABLED.with(|c| c.set(on));
81}