agg_gui/pixel_bounds.rs
1//! Pixel-alignment policy for widget bounds and draw-time translation.
2//!
3//! # Port of MatterCAD / agg-sharp
4//!
5//! `GuiWidget.DefaultEnforceIntegerBounds` (static) controls whether widgets
6//! round their bounds / padding / margin to the physical pixel grid, and is
7//! then mirrored on each widget as `EnforceIntegerBounds` so individual
8//! widgets can opt out (e.g. a smooth-scrolling marker or a zoomed canvas
9//! that genuinely wants sub-pixel positioning).
10//!
11//! We default to **true** because the vast majority of UI widgets want crisp
12//! text and strokes — fractional bounds are the exception, not the rule.
13//!
14//! # Read site
15//!
16//! `paint_subtree` reads the effective flag (widget's override, falling back
17//! to the global default) and rounds the child-translation to the nearest
18//! integer pixel before calling the child's `paint`. That single snap kills
19//! fractional CTM accumulated by cumulative-heights flex layout (e.g. Label
20//! `line_h = font_size × 1.5` is fractional for most font sizes), which is
21//! what caused the Y-axis pixel fringe on crisp rectangle fills downstream.
22//!
23//! # Opt-out
24//!
25//! ```ignore
26//! // Globally disable (rare — only for fully sub-pixel render targets):
27//! agg_gui::pixel_bounds::set_default_enforce_integer_bounds(false);
28//!
29//! // Per-widget:
30//! my_widget.widget_base_mut().enforce_integer_bounds = false;
31//! ```
32
33use std::sync::atomic::{AtomicBool, Ordering};
34
35/// Storage for the process-wide default. Reads use `Relaxed` — the flag is
36/// consulted once per paint; there are no cross-thread ordering requirements
37/// beyond "eventually sees the latest write".
38static DEFAULT_ENFORCE_INTEGER_BOUNDS: AtomicBool = AtomicBool::new(true);
39
40/// Current process-wide default used to initialise each new widget's
41/// `enforce_integer_bounds` field.
42pub fn default_enforce_integer_bounds() -> bool {
43 DEFAULT_ENFORCE_INTEGER_BOUNDS.load(Ordering::Relaxed)
44}
45
46/// Change the process-wide default. Only affects widgets constructed
47/// *after* this call; existing widgets keep whichever value they captured
48/// when they were built. Match MatterCAD semantics (`DefaultEnforceIntegerBounds`
49/// setter) exactly.
50pub fn set_default_enforce_integer_bounds(enforce: bool) {
51 DEFAULT_ENFORCE_INTEGER_BOUNDS.store(enforce, Ordering::Relaxed);
52}
53
54#[cfg(test)]
55mod tests {
56 use super::*;
57
58 /// Default must be `true` so the common case — crisp UI text + strokes
59 /// — works out of the box.
60 #[test]
61 fn test_default_is_enforced() {
62 // Not order-independent with other tests, so capture then restore.
63 let prior = default_enforce_integer_bounds();
64 set_default_enforce_integer_bounds(true);
65 assert!(default_enforce_integer_bounds());
66 set_default_enforce_integer_bounds(prior);
67 }
68
69 #[test]
70 fn test_setter_round_trip() {
71 let prior = default_enforce_integer_bounds();
72 set_default_enforce_integer_bounds(false);
73 assert!(!default_enforce_integer_bounds());
74 set_default_enforce_integer_bounds(true);
75 assert!(default_enforce_integer_bounds());
76 set_default_enforce_integer_bounds(prior);
77 }
78}