agg_gui/font_settings.rs
1//! System-wide font / text rendering settings.
2//!
3//! Mirrors the `theme::current_visuals` / `theme::set_visuals` pattern and
4//! the scrollbar-style globals (`current_scroll_style` / `set_scroll_style`).
5//! Widgets that care about rendering style (`Label`, `Button`, `TextField`,
6//! ...) should consult these at **layout/paint time** so changes made by
7//! the System window propagate without a widget-tree rebuild.
8//!
9//! # Convention
10//!
11//! Each setting has:
12//! - an **override** stored in a thread-local cell (`None` or `false` by default),
13//! - a getter (e.g. [`current_system_font`], [`lcd_enabled`]),
14//! - a setter (e.g. [`set_system_font`], [`set_lcd_enabled`]).
15//!
16//! Widgets pick between the global override and their own per-instance value
17//! — analogous to how a `ScrollView` takes the global scroll style unless
18//! the caller wired an explicit one.
19
20use std::cell::RefCell;
21use std::sync::atomic::{AtomicU64, Ordering};
22use std::sync::Arc;
23
24use crate::text::Font;
25
26// ---------------------------------------------------------------------------
27// Typography epoch
28// ---------------------------------------------------------------------------
29//
30// Bumped every time any typography-style global (font, size scale,
31// LCD, hinting, gamma, width, interval, faux weight/italic, primary
32// weight) changes. Backbuffered widgets (`Label`, `TextField`, …)
33// compare this epoch against the one they rasterised at and
34// self-invalidate on mismatch — same trick we use for theme epoch.
35// Without this, dragging a slider in the System window would leave
36// pre-existing `Label` caches showing the old style until something
37// else invalidated them.
38
39static TYPOGRAPHY_EPOCH: AtomicU64 = AtomicU64::new(1);
40
41/// Current typography epoch. Widget render paths read this each frame.
42pub fn current_typography_epoch() -> u64 {
43 TYPOGRAPHY_EPOCH.load(Ordering::Relaxed)
44}
45
46/// Internal helper: called by every setter in this module after it
47/// writes. Keeps the epoch in lock-step with the globals.
48fn bump_typography_epoch() {
49 TYPOGRAPHY_EPOCH.fetch_add(1, Ordering::Relaxed);
50}
51
52// ---------------------------------------------------------------------------
53// Thread-local storage
54// ---------------------------------------------------------------------------
55
56thread_local! {
57 /// System-wide font override. `None` means "widgets keep whatever font
58 /// they were constructed with".
59 static SYSTEM_FONT: RefCell<Option<Arc<Font>>> = RefCell::new(None);
60 /// System-wide font size multiplier — applied to every widget's own
61 /// `font_size` at paint/layout time. `1.0` = unchanged. Acts like
62 /// egui's `pixels_per_point` for typography: shrink or enlarge ALL
63 /// text while preserving the relative hierarchy (body stays smaller
64 /// than headings, etc.).
65 static FONT_SIZE_SCALE: RefCell<f64> = RefCell::new(1.0);
66 /// System-wide LCD-subpixel toggle. When `true`, text-rendering widgets
67 /// should prefer LCD output whenever they can determine their background
68 /// colour (needed for correct per-channel compositing); fall back to
69 /// grayscale AA otherwise.
70 static LCD_ENABLED: RefCell<bool> = RefCell::new(false);
71 /// System-wide hinting toggle — forwarded to the font engine when the
72 /// engine supports it. `ttf-parser` does NOT run a hinting interpreter,
73 /// so what we do is **Y-axis-only baseline hinting**: snap the glyph
74 /// origin's Y coordinate to the pixel grid before rasterisation,
75 /// matching the `(y + 0.5).floor()` convention from the AGG C++
76 /// `truetype_test_02_win` demo. This preserves horizontal subpixel
77 /// positioning (critical for LCD) while giving sharper vertical
78 /// metrics — the pragmatic compromise used by the agg-rust reference.
79 static HINTING_ENABLED: RefCell<bool> = RefCell::new(false);
80
81 // ── Typography-style parameters (drive the TrueType LCD Subpixel demo
82 // and, once the render pipeline is wired up, every text paint
83 // globally). Ranges mirror the agg-rust `truetype_test` demo so
84 // numbers stay comparable against the reference implementation.
85
86 /// Gamma correction applied post-raster. 1.0 = off (linear output).
87 /// Range 0.5..=2.5.
88 static GAMMA: RefCell<f64> = RefCell::new(1.0);
89 /// Horizontal glyph width scale. 1.0 = native widths.
90 /// Range 0.75..=1.25.
91 static WIDTH: RefCell<f64> = RefCell::new(1.0);
92 /// Extra letter-spacing as a fraction of em. 0.0 = unchanged.
93 /// Range -0.2..=0.2.
94 static INTERVAL: RefCell<f64> = RefCell::new(0.0);
95 /// Synthetic boldness via outline contour offset.
96 /// Range -1.0..=1.0; 0.0 = unchanged, positive = heavier, negative = lighter.
97 static FAUX_WEIGHT: RefCell<f64> = RefCell::new(0.0);
98 /// Synthetic italic slant expressed as a horizontal-shear factor.
99 /// Range -1.0..=1.0; 0.0 = upright.
100 static FAUX_ITALIC: RefCell<f64> = RefCell::new(0.0);
101 /// LCD primary-weight (the pixel coverage weight of the own-channel
102 /// vs the neighbouring channels in the 3-tap distribution LUT).
103 /// Range 0.0..=1.0; default 1/3 gives a neutral LUT.
104 static PRIMARY_WEIGHT: RefCell<f64> = RefCell::new(1.0 / 3.0);
105}
106
107// ---------------------------------------------------------------------------
108// Font
109// ---------------------------------------------------------------------------
110
111/// Current system font override, if set. Widgets should prefer this over
112/// their own `self.font` when the override is `Some(_)` so user changes in
113/// the System window propagate live.
114pub fn current_system_font() -> Option<Arc<Font>> {
115 SYSTEM_FONT.with(|c| c.borrow().clone())
116}
117
118/// Replace the system font override. Pass `None` to clear and fall back
119/// to per-widget fonts.
120pub fn set_system_font(font: Option<Arc<Font>>) {
121 SYSTEM_FONT.with(|c| *c.borrow_mut() = font);
122 bump_typography_epoch();
123}
124
125// ---------------------------------------------------------------------------
126// Font size scale
127// ---------------------------------------------------------------------------
128
129/// Current font size multiplier. Widgets reading a `self.font_size`
130/// should consult this (via e.g. `Label::active_font_size`) so a single
131/// slider in the System window can grow or shrink all text uniformly.
132pub fn current_font_size_scale() -> f64 {
133 FONT_SIZE_SCALE.with(|c| *c.borrow())
134}
135
136/// Set the system font-size multiplier. Clamped to a sensible range so
137/// typos or edge-case inputs can't hide every label or fry the layout.
138pub fn set_font_size_scale(scale: f64) {
139 let clamped = scale.clamp(0.5, 3.0);
140 FONT_SIZE_SCALE.with(|c| *c.borrow_mut() = clamped);
141 bump_typography_epoch();
142}
143
144// ---------------------------------------------------------------------------
145// LCD subpixel toggle
146// ---------------------------------------------------------------------------
147
148pub fn lcd_enabled() -> bool {
149 LCD_ENABLED.with(|c| *c.borrow())
150}
151
152pub fn set_lcd_enabled(on: bool) {
153 LCD_ENABLED.with(|c| *c.borrow_mut() = on);
154 bump_typography_epoch();
155}
156
157// ---------------------------------------------------------------------------
158// Hinting toggle
159// ---------------------------------------------------------------------------
160
161pub fn hinting_enabled() -> bool {
162 HINTING_ENABLED.with(|c| *c.borrow())
163}
164
165pub fn set_hinting_enabled(on: bool) {
166 HINTING_ENABLED.with(|c| *c.borrow_mut() = on);
167 bump_typography_epoch();
168}
169
170// ---------------------------------------------------------------------------
171// Typography-style parameters
172// ---------------------------------------------------------------------------
173//
174// All six follow the same shape: an immutable thread-local, a getter,
175// and a clamping setter. The clamp ranges mirror the agg-rust
176// `truetype_test` demo so results stay numerically comparable. Callers
177// (System window widgets + the TrueType LCD Subpixel demo) bind to
178// these via `Rc<Cell<f64>>` mirrors owned by `SystemCells`; the global
179// is the source-of-truth for rendering, the cell is the source-of-truth
180// for UI widgets and disk persistence.
181
182pub fn current_gamma() -> f64 {
183 GAMMA.with(|c| *c.borrow())
184}
185pub fn set_gamma(v: f64) {
186 let clamped = v.clamp(0.5, 2.5);
187 GAMMA.with(|c| *c.borrow_mut() = clamped);
188 bump_typography_epoch();
189}
190
191pub fn current_width() -> f64 {
192 WIDTH.with(|c| *c.borrow())
193}
194pub fn set_width(v: f64) {
195 let clamped = v.clamp(0.75, 1.25);
196 WIDTH.with(|c| *c.borrow_mut() = clamped);
197 bump_typography_epoch();
198}
199
200pub fn current_interval() -> f64 {
201 INTERVAL.with(|c| *c.borrow())
202}
203pub fn set_interval(v: f64) {
204 let clamped = v.clamp(-0.2, 0.2);
205 INTERVAL.with(|c| *c.borrow_mut() = clamped);
206 bump_typography_epoch();
207}
208
209pub fn current_faux_weight() -> f64 {
210 FAUX_WEIGHT.with(|c| *c.borrow())
211}
212pub fn set_faux_weight(v: f64) {
213 let clamped = v.clamp(-1.0, 1.0);
214 FAUX_WEIGHT.with(|c| *c.borrow_mut() = clamped);
215 bump_typography_epoch();
216}
217
218pub fn current_faux_italic() -> f64 {
219 FAUX_ITALIC.with(|c| *c.borrow())
220}
221pub fn set_faux_italic(v: f64) {
222 let clamped = v.clamp(-1.0, 1.0);
223 FAUX_ITALIC.with(|c| *c.borrow_mut() = clamped);
224 bump_typography_epoch();
225}
226
227pub fn current_primary_weight() -> f64 {
228 PRIMARY_WEIGHT.with(|c| *c.borrow())
229}
230pub fn set_primary_weight(v: f64) {
231 let clamped = v.clamp(0.0, 1.0);
232 PRIMARY_WEIGHT.with(|c| *c.borrow_mut() = clamped);
233 bump_typography_epoch();
234}
235
236#[cfg(test)]
237mod tests {
238 use super::*;
239
240 #[test]
241 fn test_lcd_flag_default_off() {
242 // Reset to known state — other tests in the same thread may have
243 // flipped it. Use try-reset pattern.
244 set_lcd_enabled(false);
245 assert!(!lcd_enabled());
246 set_lcd_enabled(true);
247 assert!(lcd_enabled());
248 set_lcd_enabled(false);
249 }
250
251 #[test]
252 fn test_hinting_flag_default_off() {
253 set_hinting_enabled(false);
254 assert!(!hinting_enabled());
255 set_hinting_enabled(true);
256 assert!(hinting_enabled());
257 set_hinting_enabled(false);
258 }
259
260 #[test]
261 fn test_system_font_default_none() {
262 // Reset first — other tests may have set it.
263 set_system_font(None);
264 assert!(current_system_font().is_none());
265 }
266}