Skip to main content

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}