agg-gui 0.4.0

Immediate-mode Rust GUI library with AGG rendering, Y-up layout, widgets, text, SVG, and native/WASM adapters
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
//! System-wide font / text rendering settings.
//!
//! Mirrors the `theme::current_visuals` / `theme::set_visuals` pattern and
//! the scrollbar-style globals (`current_scroll_style` / `set_scroll_style`).
//! Widgets that care about rendering style (`Label`, `Button`, `TextField`,
//! ...) should consult these at **layout/paint time** so changes made by
//! the System window propagate without a widget-tree rebuild.
//!
//! # Convention
//!
//! Each setting has:
//! - an **override** stored in a thread-local cell (`None` or `false` by default),
//! - a getter (e.g. [`current_system_font`], [`lcd_enabled`]),
//! - a setter (e.g. [`set_system_font`], [`set_lcd_enabled`]).
//!
//! Widgets pick between the global override and their own per-instance value
//! — analogous to how a `ScrollView` takes the global scroll style unless
//! the caller wired an explicit one.

use std::cell::RefCell;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;

use crate::text::Font;

// ---------------------------------------------------------------------------
// Typography epoch
// ---------------------------------------------------------------------------
//
// Bumped every time any typography-style global (font, size scale,
// LCD, hinting, gamma, width, interval, faux weight/italic, primary
// weight) changes.  Backbuffered widgets (`Label`, `TextField`, …)
// compare this epoch against the one they rasterised at and
// self-invalidate on mismatch — same trick we use for theme epoch.
// Without this, dragging a slider in the System window would leave
// pre-existing `Label` caches showing the old style until something
// else invalidated them.

static TYPOGRAPHY_EPOCH: AtomicU64 = AtomicU64::new(1);

/// Current typography epoch.  Widget render paths read this each frame.
pub fn current_typography_epoch() -> u64 {
    TYPOGRAPHY_EPOCH.load(Ordering::Relaxed)
}

/// Internal helper: called by every setter in this module after it
/// writes.  Keeps the epoch in lock-step with the globals.
fn bump_typography_epoch() {
    TYPOGRAPHY_EPOCH.fetch_add(1, Ordering::Relaxed);
}

// ---------------------------------------------------------------------------
// Thread-local storage
// ---------------------------------------------------------------------------

thread_local! {
    /// System-wide font override.  `None` means "widgets keep whatever font
    /// they were constructed with".
    static SYSTEM_FONT:     RefCell<Option<Arc<Font>>> = RefCell::new(None);
    /// System-wide font size multiplier — applied to every widget's own
    /// `font_size` at paint/layout time.  `1.0` = unchanged.  Acts like
    /// egui's `pixels_per_point` for typography: shrink or enlarge ALL
    /// text while preserving the relative hierarchy (body stays smaller
    /// than headings, etc.).
    static FONT_SIZE_SCALE: RefCell<f64>  = RefCell::new(1.0);
    /// System-wide LCD-subpixel override.  When `Some(true|false)`, text-
    /// rendering widgets honour it directly.  When `None` (the default),
    /// [`lcd_enabled`] derives the effective value from
    /// [`crate::device_scale`]: LCD is enabled at standard DPI (scale ≤
    /// 1.25) and disabled at HiDPI, because LCD subpixel rendering only
    /// pays off when subpixels are roughly the size of a glyph stem; at
    /// 2× scale the AA halo is already wide enough that grayscale wins on
    /// chroma fringing while looking identical otherwise.  An explicit
    /// [`set_lcd_enabled`] overrides the auto-derivation; apps that just
    /// want the default should never need to call it.
    static LCD_ENABLED:     RefCell<Option<bool>> = const { RefCell::new(None) };
    /// System-wide hinting toggle — forwarded to the font engine when the
    /// engine supports it.  `ttf-parser` does NOT run a hinting interpreter,
    /// so what we do is **Y-axis-only baseline hinting**: snap the glyph
    /// origin's Y coordinate to the pixel grid before rasterisation,
    /// matching the `(y + 0.5).floor()` convention from the AGG C++
    /// `truetype_test_02_win` demo.  This preserves horizontal subpixel
    /// positioning (critical for LCD) while giving sharper vertical
    /// metrics — the pragmatic compromise used by the agg-rust reference.
    static HINTING_ENABLED: RefCell<bool> = RefCell::new(false);

    // ── Typography-style parameters (driven by the System window's
    // typography controls / Sample Text tab, and every text paint
    // globally).  Ranges mirror the agg-rust `truetype_test` demo so
    // numbers stay comparable against the reference implementation.

    /// Gamma correction applied post-raster.  1.0 = off (linear output).
    /// Range 0.5..=2.5.
    static GAMMA:          RefCell<f64> = RefCell::new(1.0);
    /// Horizontal glyph width scale.  1.0 = native widths.
    /// Range 0.75..=1.25.
    static WIDTH:          RefCell<f64> = RefCell::new(1.0);
    /// Extra letter-spacing as a fraction of em.  0.0 = unchanged.
    /// Range -0.2..=0.2.
    static INTERVAL:       RefCell<f64> = RefCell::new(0.0);
    /// Synthetic boldness via outline contour offset.
    /// Range -1.0..=1.0; 0.0 = unchanged, positive = heavier, negative = lighter.
    static FAUX_WEIGHT:    RefCell<f64> = RefCell::new(0.0);
    /// Synthetic italic slant expressed as a horizontal-shear factor.
    /// Range -1.0..=1.0; 0.0 = upright.
    static FAUX_ITALIC:    RefCell<f64> = RefCell::new(0.0);
    /// LCD primary-weight (the pixel coverage weight of the own-channel
    /// vs the neighbouring channels in the 3-tap distribution LUT).
    /// Range 0.0..=1.0; default 1/3 gives a neutral LUT.
    static PRIMARY_WEIGHT: RefCell<f64> = RefCell::new(1.0 / 3.0);
}

// ---------------------------------------------------------------------------
// Font
// ---------------------------------------------------------------------------

/// Current system font override, if set.  Widgets should prefer this over
/// their own `self.font` when the override is `Some(_)` so user changes in
/// the System window propagate live.
pub fn current_system_font() -> Option<Arc<Font>> {
    SYSTEM_FONT.with(|c| c.borrow().clone())
}

/// Replace the system font override.  Pass `None` to clear and fall back
/// to per-widget fonts.
pub fn set_system_font(font: Option<Arc<Font>>) {
    SYSTEM_FONT.with(|c| *c.borrow_mut() = font);
    bump_typography_epoch();
}

// ---------------------------------------------------------------------------
// Font size scale
// ---------------------------------------------------------------------------

/// Current font size multiplier.  Widgets reading a `self.font_size`
/// should consult this (via e.g. `Label::active_font_size`) so a single
/// slider in the System window can grow or shrink all text uniformly.
pub fn current_font_size_scale() -> f64 {
    FONT_SIZE_SCALE.with(|c| *c.borrow())
}

/// Set the system font-size multiplier.  Clamped to a sensible range so
/// typos or edge-case inputs can't hide every label or fry the layout.
pub fn set_font_size_scale(scale: f64) {
    let clamped = scale.clamp(0.5, 3.0);
    FONT_SIZE_SCALE.with(|c| *c.borrow_mut() = clamped);
    bump_typography_epoch();
}

// ---------------------------------------------------------------------------
// LCD subpixel toggle
// ---------------------------------------------------------------------------

/// Whether widgets should rasterise text through the LCD subpixel path.
///
/// Whether widgets should rasterise text through the LCD subpixel path.
///
/// **Hard cap first:** LCD is NEVER used above standard density.  The gate
/// keys on the *effective* scale ([`crate::ux_scale::effective_scale`] =
/// device DPR × UX zoom), because LCD subpixel rendering only pays off when
/// individual physical pixels are large enough to resolve the R/G/B
/// sub-stripes — and "small pixels" come from a high UX zoom (mobile /
/// accessibility) just as much as from a HiDPI panel.  Above `1.25×` it is
/// pure overhead with no visible benefit, so we force the cheaper grayscale
/// path *regardless of any explicit override*.  This also keeps
/// CPU-backbuffered widgets (the menu bar) off the LCD blit at high scale,
/// where they would otherwise shrink.
///
/// At standard density the explicit override set via [`set_lcd_enabled`]
/// wins if present; otherwise LCD defaults on.  So platform shells generally
/// don't need to call [`set_lcd_enabled`] at all.
pub fn lcd_enabled() -> bool {
    // Never LCD at high effective density — overrides included.
    if crate::ux_scale::effective_scale() > 1.25 {
        return false;
    }
    if let Some(explicit) = LCD_ENABLED.with(|c| *c.borrow()) {
        return explicit;
    }
    true
}

/// Pin LCD subpixel rendering to a specific value, overriding the
/// device-scale-derived default.  System-window toggles use this; apps
/// that just want sensible default behaviour should not call it.
pub fn set_lcd_enabled(on: bool) {
    LCD_ENABLED.with(|c| *c.borrow_mut() = Some(on));
    bump_typography_epoch();
}

/// Drop any explicit override and return to device-scale-derived auto.
/// Counterpart to [`set_lcd_enabled`]; used by tests and by System-
/// window "reset to default" affordances.
pub fn clear_lcd_enabled_override() {
    LCD_ENABLED.with(|c| *c.borrow_mut() = None);
    bump_typography_epoch();
}

// ---------------------------------------------------------------------------
// Hinting toggle
// ---------------------------------------------------------------------------

pub fn hinting_enabled() -> bool {
    HINTING_ENABLED.with(|c| *c.borrow())
}

pub fn set_hinting_enabled(on: bool) {
    HINTING_ENABLED.with(|c| *c.borrow_mut() = on);
    bump_typography_epoch();
}

/// Snap a text baseline Y coordinate to the pixel grid **when the global
/// hinting toggle is on**, otherwise return it unchanged.
///
/// This is the *line-level* companion to the per-glyph Y snap that
/// [`crate::text::shape_text`] applies (`(y + 0.5).floor()`).  Multi-line
/// editors (`TextArea`, `RichTextEdit`) compute each visual line's baseline
/// from fractional scroll / alignment / metric math; snapping the line
/// baseline up front keeps whole-line pitch uniform and lands stems on pixel
/// rows so the editors render exactly as crisply as `Label` under the same
/// System settings.  When hinting is off they intentionally degrade the same
/// way `Label` does (fractional baseline, softer verticals).
///
/// Coordinates are **logical**; at device scale 1 that is also the physical
/// pixel grid.  At higher scales LCD is disabled anyway and the LCD composite
/// path does its own physical rounding, so gating on the logical grid here is
/// the right granularity.
pub fn snap_baseline_y(y: f64) -> f64 {
    if hinting_enabled() {
        (y + 0.5).floor()
    } else {
        y
    }
}

// ---------------------------------------------------------------------------
// Typography-style parameters
// ---------------------------------------------------------------------------
//
// All six follow the same shape: an immutable thread-local, a getter,
// and a clamping setter.  The clamp ranges mirror the agg-rust
// `truetype_test` demo so results stay numerically comparable.  Callers
// (the System window's typography controls / Sample Text tab) bind to
// these via `Rc<Cell<f64>>` mirrors owned by `SystemCells`; the global
// is the source-of-truth for rendering, the cell is the source-of-truth
// for UI widgets and disk persistence.

pub fn current_gamma() -> f64 {
    GAMMA.with(|c| *c.borrow())
}
pub fn set_gamma(v: f64) {
    let clamped = v.clamp(0.5, 2.5);
    GAMMA.with(|c| *c.borrow_mut() = clamped);
    bump_typography_epoch();
}

pub fn current_width() -> f64 {
    WIDTH.with(|c| *c.borrow())
}
pub fn set_width(v: f64) {
    let clamped = v.clamp(0.75, 1.25);
    WIDTH.with(|c| *c.borrow_mut() = clamped);
    bump_typography_epoch();
}

pub fn current_interval() -> f64 {
    INTERVAL.with(|c| *c.borrow())
}
pub fn set_interval(v: f64) {
    let clamped = v.clamp(-0.2, 0.2);
    INTERVAL.with(|c| *c.borrow_mut() = clamped);
    bump_typography_epoch();
}

pub fn current_faux_weight() -> f64 {
    FAUX_WEIGHT.with(|c| *c.borrow())
}
pub fn set_faux_weight(v: f64) {
    let clamped = v.clamp(-1.0, 1.0);
    FAUX_WEIGHT.with(|c| *c.borrow_mut() = clamped);
    bump_typography_epoch();
}

pub fn current_faux_italic() -> f64 {
    FAUX_ITALIC.with(|c| *c.borrow())
}
pub fn set_faux_italic(v: f64) {
    let clamped = v.clamp(-1.0, 1.0);
    FAUX_ITALIC.with(|c| *c.borrow_mut() = clamped);
    bump_typography_epoch();
}

pub fn current_primary_weight() -> f64 {
    PRIMARY_WEIGHT.with(|c| *c.borrow())
}
pub fn set_primary_weight(v: f64) {
    let clamped = v.clamp(0.0, 1.0);
    PRIMARY_WEIGHT.with(|c| *c.borrow_mut() = clamped);
    bump_typography_epoch();
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_lcd_flag_explicit_override_round_trips() {
        // Reset to known state — thread-locals can leak across tests reusing
        // the same worker thread.  Standard density so the high-scale cap in
        // `lcd_enabled` doesn't mask the override under test.
        crate::device_scale::set_device_scale(1.0);
        crate::ux_scale::set_ux_scale(1.0);
        set_lcd_enabled(false);
        assert!(!lcd_enabled());
        set_lcd_enabled(true);
        assert!(lcd_enabled());
        clear_lcd_enabled_override();
    }

    #[test]
    fn test_lcd_flag_auto_derives_from_device_scale_when_no_override() {
        use crate::device_scale::set_device_scale;
        clear_lcd_enabled_override();
        set_device_scale(1.0);
        assert!(lcd_enabled(), "standard DPI should default to LCD on");
        set_device_scale(2.0);
        assert!(!lcd_enabled(), "HiDPI should default to LCD off");
        // Restore to a sane state for sibling tests.
        set_device_scale(1.0);
    }

    #[test]
    fn test_lcd_auto_disabled_at_high_effective_scale_from_ux_zoom() {
        // LCD subpixel rendering is pointless overhead once the on-screen
        // pixel density is high — and "high density" can come from the UX
        // zoom (mobile / accessibility) just as much as from the device DPR.
        // A device at 1.0 DPR with ux_scale 1.7 renders everything at 1.7×;
        // LCD must auto-disable there, exactly as it does at 1.7× DPR.
        // Regression: the auto-derivation keyed on `device_scale` alone, so a
        // ux-zoomed standard-DPI display kept LCD on, which routed the
        // CPU-backbuffered menu bar through the LCD blit and rendered it tiny.
        use crate::device_scale::set_device_scale;
        use crate::ux_scale::set_ux_scale;
        clear_lcd_enabled_override();
        set_device_scale(1.0);
        set_ux_scale(1.0);
        assert!(
            lcd_enabled(),
            "standard DPI + no zoom should default to LCD on"
        );
        set_ux_scale(1.7);
        assert!(
            !lcd_enabled(),
            "high effective scale via ux zoom should default to LCD off"
        );
        // The high-scale gate is a HARD CAP: it wins even over an explicit
        // override, so "force LCD on" can't reintroduce the overhead (and the
        // tiny-menu blit) at high density.
        set_lcd_enabled(true);
        assert!(
            !lcd_enabled(),
            "explicit LCD-on override must still be capped off at high effective scale"
        );
        // ...but at standard density the explicit override is honoured.
        set_ux_scale(1.0);
        assert!(
            lcd_enabled(),
            "override LCD-on must apply at standard density"
        );
        // Restore sane state for sibling tests.
        clear_lcd_enabled_override();
        set_ux_scale(1.0);
        set_device_scale(1.0);
    }

    #[test]
    fn test_hinting_flag_default_off() {
        set_hinting_enabled(false);
        assert!(!hinting_enabled());
        set_hinting_enabled(true);
        assert!(hinting_enabled());
        set_hinting_enabled(false);
    }

    #[test]
    fn test_snap_baseline_y_lands_on_integer_when_hinting_on() {
        // With hinting on, a fractional baseline must snap onto the pixel grid
        // (integer Y at scale 1) — the same treatment `text::shape_text` gives
        // each glyph, applied at the line level for the multi-line editors.
        set_hinting_enabled(true);
        for &raw in &[0.0, 3.2, 3.5, 3.7, 12.49, 12.5, 100.99, -0.4] {
            let snapped = snap_baseline_y(raw);
            assert_eq!(
                snapped.fract(),
                0.0,
                "snap_baseline_y({raw}) = {snapped} is not on the pixel grid"
            );
            assert_eq!(snapped, (raw + 0.5).floor());
        }

        // With hinting off, the baseline is passed through unchanged so the
        // editors degrade exactly like `Label` (softer verticals, no snap).
        set_hinting_enabled(false);
        assert_eq!(snap_baseline_y(3.7), 3.7);
        assert_eq!(snap_baseline_y(12.25), 12.25);

        set_hinting_enabled(false);
    }

    #[test]
    fn test_system_font_default_none() {
        // Reset first — other tests may have set it.
        set_system_font(None);
        assert!(current_system_font().is_none());
    }
}