Skip to main content

compose_rust/
zoom.rs

1//! How large the Renderer is drawing, and the application's way to change it.
2//!
3//! Two things make a screen larger. The reader's operating system has a text size (`os`),
4//! and the application has a zoom level (`level`), stepped with Command or Control and
5//! plus, minus or zero, the way an editor zooms. The level is a whole number from
6//! [`MIN_ZOOM_LEVEL`] to [`MAX_ZOOM_LEVEL`], and each step is a factor of 1.2. What the
7//! screen is drawn at is their product, `k = os × 1.2^level`.
8//!
9//! The Renderer owns both: it reads the system's setting, answers the keys and remembers
10//! the level between runs. It tells the Host the result once at start and again whenever
11//! it changes, the way a size class is reported, and this module is where that report is
12//! kept for the components that read it. An application that lays out text of its own
13//! (an HTML island, a measured paragraph) uses `k`; one that only uses native widgets needs
14//! nothing here, because those are already drawn at the right size.
15//!
16//! The application can also set the level, from a menu's "Zoom In" for example. That rides
17//! out on the window record, in the batch the call produces.
18
19use std::cell::{Cell, RefCell};
20use std::sync::Arc;
21
22/// The smallest application zoom level. Eight steps of 1.2 down is about a quarter size.
23pub const MIN_ZOOM_LEVEL: i8 = -8;
24/// The largest application zoom level. Eight steps of 1.2 up is about four times the size.
25pub const MAX_ZOOM_LEVEL: i8 = 8;
26
27/// The factor one application zoom step multiplies by.
28pub const ZOOM_STEP: f32 = 1.2;
29
30/// What the Renderer is drawing at, as it last reported.
31#[derive(Clone, Copy, Debug, PartialEq)]
32pub struct Zoom {
33    /// The whole factor, `os × app`.
34    pub k: f32,
35    /// The operating system's text size, where 1 is the default.
36    pub os: f32,
37    /// The application's zoom level, from [`MIN_ZOOM_LEVEL`] to [`MAX_ZOOM_LEVEL`].
38    pub level: i8,
39}
40
41impl Zoom {
42    /// Nothing enlarged: the size before the Renderer has said anything.
43    pub const DEFAULT: Zoom = Zoom {
44        k: 1.0,
45        os: 1.0,
46        level: 0,
47    };
48
49    /// The application's share of the factor, `1.2^level`.
50    pub fn app(&self) -> f32 {
51        app_scale(self.level)
52    }
53}
54
55impl Default for Zoom {
56    fn default() -> Self {
57        Self::DEFAULT
58    }
59}
60
61/// `1.2^level`, the application's share of the factor at a level.
62pub fn app_scale(level: i8) -> f32 {
63    ZOOM_STEP.powi(i32::from(clamp_level(level)))
64}
65
66/// A level kept inside the range the Renderer accepts.
67pub fn clamp_level(level: i8) -> i8 {
68    level.clamp(MIN_ZOOM_LEVEL, MAX_ZOOM_LEVEL)
69}
70
71struct Subscriber {
72    id: u64,
73    notify: Arc<dyn Fn() + Send + Sync>,
74}
75
76thread_local! {
77    static CURRENT: Cell<Zoom> = const { Cell::new(Zoom::DEFAULT) };
78    /// A level the application asked for since the last batch, not yet written into one.
79    static PENDING_LEVEL: Cell<Option<i8>> = const { Cell::new(None) };
80    static SUBSCRIBERS: RefCell<Vec<Subscriber>> = const { RefCell::new(Vec::new()) };
81    /// What each window of a multi-window application is drawing at, by the id of its
82    /// `Window` node. Zoom is per window, as it is in VS Code and Electron.
83    static WINDOW_ZOOMS: RefCell<Vec<(u32, Zoom)>> = const { RefCell::new(Vec::new()) };
84    static WINDOW_SUBSCRIBERS: RefCell<Vec<(u32, Subscriber)>> = const { RefCell::new(Vec::new()) };
85    static NEXT_SUBSCRIBER_ID: Cell<u64> = const { Cell::new(1) };
86}
87
88/// What the Renderer last reported. Available outside a component too.
89///
90/// In a multi-window application it is the zoom of the window the caller is in.
91pub fn zoom() -> Zoom {
92    match crate::windows::current_window_id() {
93        0 => CURRENT.with(Cell::get),
94        window => zoom_of(window),
95    }
96}
97
98/// What one window is drawing at, by the id of its `Window` node, or the default for one
99/// that has not reported yet.
100pub fn zoom_of(window: u32) -> Zoom {
101    WINDOW_ZOOMS.with_borrow(|zooms| {
102        zooms
103            .iter()
104            .find(|(id, _)| *id == window)
105            .map_or(Zoom::DEFAULT, |(_, zoom)| *zoom)
106    })
107}
108
109/// Records one window's report and wakes the components in that window that read it.
110/// Returns whether any was woken.
111pub(crate) fn publish_window(window: u32, zoom: Zoom) -> bool {
112    let changed = WINDOW_ZOOMS.with_borrow_mut(|zooms| {
113        match zooms.iter_mut().find(|(id, _)| *id == window) {
114            Some(entry) => {
115                if entry.1 == zoom {
116                    return false;
117                }
118                entry.1 = zoom;
119                true
120            }
121            None => {
122                zooms.push((window, zoom));
123                true
124            }
125        }
126    });
127    if !changed {
128        return false;
129    }
130    WINDOW_SUBSCRIBERS.with_borrow(|subscribers| {
131        let mut woke = false;
132        for (id, subscriber) in subscribers {
133            if *id == window {
134                (subscriber.notify)();
135                woke = true;
136            }
137        }
138        woke
139    })
140}
141
142/// How many windows' zooms and subscriptions are held.
143pub(crate) fn window_state_len() -> usize {
144    WINDOW_ZOOMS.with_borrow(Vec::len) + WINDOW_SUBSCRIBERS.with_borrow(Vec::len)
145}
146
147/// Forgets a window that left the tree, and what it reported.
148pub(crate) fn forget_window(window: u32) {
149    WINDOW_ZOOMS.with_borrow_mut(|zooms| zooms.retain(|(id, _)| *id != window));
150}
151
152/// A registration for one window's zoom. Dropping it ends the registration.
153pub struct WindowZoomSubscription {
154    id: u64,
155}
156
157/// Registers `notify` to be called each time the window `window` reports a different zoom.
158pub fn subscribe_window(
159    window: u32,
160    notify: Arc<dyn Fn() + Send + Sync>,
161) -> WindowZoomSubscription {
162    let id = next_subscriber_id();
163    WINDOW_SUBSCRIBERS.with_borrow_mut(|subscribers| {
164        subscribers.push((window, Subscriber { id, notify }));
165    });
166    WindowZoomSubscription { id }
167}
168
169impl Drop for WindowZoomSubscription {
170    fn drop(&mut self) {
171        WINDOW_SUBSCRIBERS.with_borrow_mut(|subscribers| {
172            subscribers.retain(|(_, subscriber)| subscriber.id != self.id);
173        });
174    }
175}
176
177fn next_subscriber_id() -> u64 {
178    NEXT_SUBSCRIBER_ID.with(|next| {
179        let id = next.get();
180        next.set(id + 1);
181        id
182    })
183}
184
185/// Records the Renderer's report and wakes the components that read it.
186///
187/// Returns whether any component was woken. A report nobody reads is still recorded, so a
188/// component that mounts later reads it, but nothing is redrawn for it.
189pub(crate) fn publish(zoom: Zoom) -> bool {
190    if CURRENT.with(Cell::get) == zoom {
191        return false;
192    }
193    CURRENT.with(|current| current.set(zoom));
194    SUBSCRIBERS.with_borrow(|subscribers| {
195        for subscriber in subscribers {
196            (subscriber.notify)();
197        }
198        !subscribers.is_empty()
199    })
200}
201
202/// Asks the Renderer to draw at `level`. It goes out on the window record in the batch the
203/// current call produces, and the Renderer answers with a new report.
204///
205/// In a multi-window application it asks for the window the caller is in.
206pub fn set_zoom_level(level: i8) {
207    let level = clamp_level(level);
208    match crate::windows::current_window_id() {
209        0 => PENDING_LEVEL.with(|pending| pending.set(Some(level))),
210        window => crate::windows::queue_zoom_level(window, level),
211    }
212}
213
214/// The level asked for since the last batch, if any.
215pub(crate) fn take_pending_level() -> Option<i8> {
216    PENDING_LEVEL.with(Cell::take)
217}
218
219/// Clears the report, anything asked for, and every subscription. Used when a Host is made
220/// and between tests.
221#[doc(hidden)]
222pub fn reset_zoom() {
223    CURRENT.with(|current| current.set(Zoom::DEFAULT));
224    PENDING_LEVEL.with(|pending| pending.set(None));
225    SUBSCRIBERS.with_borrow_mut(Vec::clear);
226    WINDOW_ZOOMS.with_borrow_mut(Vec::clear);
227    WINDOW_SUBSCRIBERS.with_borrow_mut(Vec::clear);
228}
229
230/// A registration for changes to the report. Dropping it ends the registration.
231pub struct ZoomSubscription {
232    id: u64,
233}
234
235/// Registers `notify` to be called each time the Renderer reports a different zoom.
236pub fn subscribe(notify: Arc<dyn Fn() + Send + Sync>) -> ZoomSubscription {
237    let id = next_subscriber_id();
238    SUBSCRIBERS.with_borrow_mut(|subscribers| subscribers.push(Subscriber { id, notify }));
239    ZoomSubscription { id }
240}
241
242impl Drop for ZoomSubscription {
243    fn drop(&mut self) {
244        SUBSCRIBERS.with_borrow_mut(|subscribers| {
245            subscribers.retain(|subscriber| subscriber.id != self.id);
246        });
247    }
248}
249
250/// A handle on the zoom. `Copy`, so it can move into any number of handlers.
251#[derive(Clone, Copy, Debug, Default)]
252pub struct ZoomHandle {
253    _private: (),
254}
255
256impl ZoomHandle {
257    /// What the Renderer last reported.
258    pub fn get(&self) -> Zoom {
259        zoom()
260    }
261
262    /// Sets the application zoom level, kept inside its range.
263    pub fn set_level(&self, level: i8) {
264        set_zoom_level(level);
265    }
266
267    /// One step larger, as a "Zoom In" menu item does.
268    pub fn zoom_in(&self) {
269        set_zoom_level(self.current_level().saturating_add(1));
270    }
271
272    /// One step smaller.
273    pub fn zoom_out(&self) {
274        set_zoom_level(self.current_level().saturating_sub(1));
275    }
276
277    /// Back to the system's own size.
278    pub fn reset(&self) {
279        set_zoom_level(0);
280    }
281
282    /// The level asked for in this call, or else the one last reported, so two steps in
283    /// one handler are two steps.
284    fn current_level(&self) -> i8 {
285        match crate::windows::current_window_id() {
286            0 => PENDING_LEVEL
287                .with(Cell::get)
288                .unwrap_or_else(|| zoom().level),
289            window => {
290                crate::windows::pending_zoom_level(window).unwrap_or_else(|| zoom_of(window).level)
291            }
292        }
293    }
294}
295
296#[cfg(test)]
297mod tests {
298    use super::*;
299
300    #[test]
301    fn fr43_app_scale_is_one_point_two_to_the_level() {
302        assert_eq!(app_scale(0), 1.0);
303        assert!((app_scale(2) - 1.44).abs() < 1e-6);
304        assert!((app_scale(-1) - 1.0 / 1.2).abs() < 1e-6);
305        assert_eq!(app_scale(20), app_scale(MAX_ZOOM_LEVEL));
306    }
307
308    #[test]
309    fn fr43_two_steps_in_one_handler_are_two_steps() {
310        reset_zoom();
311        let handle = ZoomHandle::default();
312        handle.zoom_in();
313        handle.zoom_in();
314        assert_eq!(take_pending_level(), Some(2));
315        assert_eq!(take_pending_level(), None);
316        handle.set_level(40);
317        assert_eq!(take_pending_level(), Some(MAX_ZOOM_LEVEL));
318        reset_zoom();
319    }
320
321    #[test]
322    fn fr43_a_report_wakes_only_on_change() {
323        reset_zoom();
324        let woken = Arc::new(std::sync::atomic::AtomicUsize::new(0));
325        let counter = woken.clone();
326        let _subscription = subscribe(Arc::new(move || {
327            counter.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
328        }));
329        let report = Zoom {
330            k: 1.8,
331            os: 1.25,
332            level: 2,
333        };
334        assert!(publish(report));
335        assert!(!publish(report), "the same report twice is one change");
336        assert_eq!(woken.load(std::sync::atomic::Ordering::SeqCst), 1);
337        assert_eq!(zoom(), report);
338        reset_zoom();
339    }
340}