Skip to main content

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}