Skip to main content

agg_gui/
ux_scale.rs

1//! Global **user-experience scale** factor.
2//!
3//! Distinct from [`crate::device_scale`], which tracks the physical
4//! device-pixel ratio so glyphs stay crisp on HiDPI displays. The UX
5//! scale exists because the same logical pixel feels very different
6//! to a user on a desktop monitor at desk distance vs. a phone held
7//! at arm's length.
8//!
9//! The cleanest mental model:
10//!
11//! - **`device_scale`**: pixels per logical unit on the physical
12//!   display surface. Always set by the platform shell to whatever
13//!   `window.devicePixelRatio` / `winit::Window::scale_factor`
14//!   reports. Driven by the hardware.
15//! - **`ux_scale`**: how much bigger the *user* wants every logical
16//!   unit to be on top of that. Driven by ergonomic / accessibility
17//!   needs — small on a 27" monitor read at arm's length, bigger on
18//!   a 6" phone read at arm's length, bigger still for users with
19//!   reduced vision.
20//!
21//! The framework multiplies the two when it computes the effective
22//! viewport / paint transform inside [`crate::App`]. Widgets always
23//! see "logical" units (already divided by the effective scale), so
24//! no per-widget changes are needed — only platform shells need to
25//! call [`set_ux_scale`].
26//!
27//! ## Suggested values
28//!
29//! - `1.0` — desktop / laptop / cursor-driven UI. The default.
30//! - `1.6` – `1.8` — mobile touch (phone / tablet) where the user
31//!   reads at arm's length and needs ~44 px touch targets. Auto-set
32//!   when [`crate::input_profile::set_input_profile`] is called with
33//!   any mobile variant.
34//! - User-controlled accessibility setting on top of that — `1.0` to
35//!   `2.0+` for users who explicitly want bigger UI.
36
37use std::cell::Cell;
38
39thread_local! {
40    static UX_SCALE: Cell<f64> = Cell::new(1.0);
41}
42
43/// Current UX scale factor. Multiplied with [`crate::device_scale`] in
44/// [`crate::App::layout`] / [`crate::App::paint`] to give the effective
45/// "physical pixels per logical unit" the framework actually uses.
46#[inline]
47pub fn ux_scale() -> f64 {
48    UX_SCALE.with(|s| s.get())
49}
50
51/// Set the UX scale factor. Panics on non-positive values in debug.
52///
53/// Platform shells typically call this once at startup after they've
54/// figured out the device profile, and again from a settings UI that
55/// lets the user tune readability.
56pub fn set_ux_scale(scale: f64) {
57    debug_assert!(
58        scale > 0.0,
59        "ux_scale must be a positive value, got {scale}"
60    );
61    UX_SCALE.with(|s| s.set(scale));
62    crate::animation::request_draw();
63}
64
65/// Combined "physical pixels per logical unit" the framework uses
66/// for layout / paint scaling. `device_scale * ux_scale`.
67#[inline]
68pub fn effective_scale() -> f64 {
69    crate::device_scale() * ux_scale()
70}
71
72#[cfg(test)]
73mod tests {
74    use super::*;
75
76    #[test]
77    fn default_is_one() {
78        // Tests run sequentially in a single thread; reset to known.
79        set_ux_scale(1.0);
80        assert!((ux_scale() - 1.0).abs() < 1e-9);
81    }
82
83    #[test]
84    fn round_trip() {
85        set_ux_scale(1.7);
86        assert!((ux_scale() - 1.7).abs() < 1e-9);
87        set_ux_scale(1.0);
88    }
89}