Skip to main content

compose_rust/
window.rs

1//! The window's measured size, and the registrations a runtime wakes its readers through.
2//!
3//! The UI is authored here but measured by the Renderer, so the only way a Rust component
4//! can know how wide the window is, is for the Renderer to tell it. The Renderer sends one
5//! event when the size class changes and nothing in between, so this module is a single
6//! current value plus the list of readers that asked to be told when it changes. The
7//! Dioxus adapter's `use_window_size` and `use_node_size` hooks are such readers.
8
9use crate::schema::{WindowHeightClass, WindowSizeClass};
10use std::cell::{Cell, RefCell};
11use std::sync::Arc;
12
13/// Insets in dp needed to avoid the window controls.
14#[derive(Clone, Copy, Debug, Default, PartialEq)]
15pub struct WindowSafeArea {
16    pub top: f32,
17    pub leading: f32,
18    pub trailing: f32,
19}
20
21/// The window's size, as the Renderer last measured it.
22///
23/// `width_dp` and `height_dp` are density-independent pixels, the same unit gesture
24/// coordinates use. They are the measurements taken at the moment the class last changed,
25/// not a value that follows every pixel of a drag: a size that changed every layout pass
26/// would run the runtime every layout pass.
27#[derive(Clone, Copy, Debug, PartialEq)]
28pub struct WindowSize {
29    pub width_dp: f32,
30    pub height_dp: f32,
31    pub class: WindowSizeClass,
32    pub height_class: WindowHeightClass,
33    pub safe_area: WindowSafeArea,
34}
35
36impl WindowSize {
37    pub const fn new(
38        width_dp: f32,
39        height_dp: f32,
40        class: WindowSizeClass,
41        height_class: WindowHeightClass,
42    ) -> Self {
43        Self {
44            width_dp,
45            height_dp,
46            class,
47            height_class,
48            safe_area: WindowSafeArea {
49                top: 0.0,
50                leading: 0.0,
51                trailing: 0.0,
52            },
53        }
54    }
55
56    pub const fn with_safe_area(mut self, safe_area: WindowSafeArea) -> Self {
57        self.safe_area = safe_area;
58        self
59    }
60
61    /// Phone-shaped: narrower than 600dp. One column.
62    pub fn is_compact(&self) -> bool {
63        matches!(self.class, WindowSizeClass::Compact)
64    }
65
66    /// Tablet-shaped: 600dp to 840dp.
67    pub fn is_medium(&self) -> bool {
68        matches!(self.class, WindowSizeClass::Medium)
69    }
70
71    /// Desktop-shaped: 840dp and wider. Room for a permanent sidebar.
72    pub fn is_expanded(&self) -> bool {
73        matches!(self.class, WindowSizeClass::Expanded)
74    }
75
76    /// Short: under 480dp tall. A phone lying on its side is the usual one, and a column
77    /// of stacked sections is what has to fold there.
78    pub fn is_short(&self) -> bool {
79        matches!(self.height_class, WindowHeightClass::Compact)
80    }
81
82    /// The ordinary height: 480dp to 900dp.
83    pub fn is_medium_height(&self) -> bool {
84        matches!(self.height_class, WindowHeightClass::Medium)
85    }
86
87    /// Tall: 900dp and more. A portrait tablet, or a window someone stretched.
88    pub fn is_tall(&self) -> bool {
89        matches!(self.height_class, WindowHeightClass::Expanded)
90    }
91}
92
93impl Default for WindowSize {
94    /// What a component sees before the Renderer has measured anything.
95    ///
96    /// Compact rather than a wider guess: a one-column layout is usable at any width, so
97    /// the single render that happens before the first measurement arrives is never
98    /// broken, only narrower than it needs to be.
99    fn default() -> Self {
100        Self::new(
101            0.0,
102            0.0,
103            WindowSizeClass::Compact,
104            WindowHeightClass::Compact,
105        )
106    }
107}
108
109struct Subscriber {
110    id: u64,
111    notify: Arc<dyn Fn() + Send + Sync>,
112}
113
114thread_local! {
115    /// What each observed node last measured, by the token its screen gave it.
116    ///
117    /// A map rather than a field, because nothing is in it until a screen asks: a tree
118    /// with no observers keeps an empty map and pays for nothing.
119    static NODES: RefCell<Vec<(u32, WindowSize)>> = const { RefCell::new(Vec::new()) };
120    static NODE_SUBSCRIBERS: RefCell<Vec<(u32, Subscriber)>> = const { RefCell::new(Vec::new()) };
121    static CURRENT: Cell<WindowSize> = const { Cell::new(WindowSize {
122        width_dp: 0.0,
123        height_dp: 0.0,
124        class: WindowSizeClass::Compact,
125        height_class: WindowHeightClass::Compact,
126        safe_area: WindowSafeArea { top: 0.0, leading: 0.0, trailing: 0.0 },
127    }) };
128    static SUBSCRIBERS: RefCell<Vec<Subscriber>> = const { RefCell::new(Vec::new()) };
129    /// What each window of a multi-window application last measured, by the id of its
130    /// `Window` node. Empty in a single-window application, which keeps its one
131    /// value in `CURRENT`.
132    static WINDOWS: RefCell<Vec<(u32, WindowSize)>> = const { RefCell::new(Vec::new()) };
133    static WINDOW_SUBSCRIBERS: RefCell<Vec<(u32, Subscriber)>> = const { RefCell::new(Vec::new()) };
134    static NEXT_SUBSCRIBER_ID: Cell<u64> = const { Cell::new(1) };
135}
136
137/// The size the Renderer last reported. Available outside a component too.
138///
139/// In a multi-window application it is the size of the window the caller is in:
140/// the window a composable is composed in, or the window whose node a handler was called
141/// for. Outside any window it is the single window's, which a multi-window application
142/// never has, so it reads as the default.
143pub fn window_size() -> WindowSize {
144    match crate::windows::current_window_id() {
145        0 => CURRENT.with(Cell::get),
146        window => window_size_of(window),
147    }
148}
149
150/// The size a window last measured, by the id of its `Window` node, or the default for one
151/// nothing has reported yet.
152pub fn window_size_of(window: u32) -> WindowSize {
153    WINDOWS.with_borrow(|windows| {
154        windows
155            .iter()
156            .find(|(id, _)| *id == window)
157            .map(|(_, size)| *size)
158            .unwrap_or_default()
159    })
160}
161
162/// Records one window's measurement and wakes the components in that window that read it.
163/// Returns whether anything was woken. A report for another window wakes nobody here.
164pub(crate) fn publish_window(window: u32, size: WindowSize) -> bool {
165    let changed =
166        WINDOWS.with_borrow_mut(
167            |windows| match windows.iter_mut().find(|(id, _)| *id == window) {
168                Some(entry) => {
169                    if entry.1 == size {
170                        return false;
171                    }
172                    entry.1 = size;
173                    true
174                }
175                None => {
176                    windows.push((window, size));
177                    true
178                }
179            },
180        );
181    if !changed {
182        return false;
183    }
184    WINDOW_SUBSCRIBERS.with_borrow(|subscribers| {
185        let mut woke = false;
186        for (id, subscriber) in subscribers {
187            if *id == window {
188                (subscriber.notify)();
189                woke = true;
190            }
191        }
192        woke
193    })
194}
195
196/// How many windows' sizes and subscriptions are held.
197pub(crate) fn window_state_len() -> usize {
198    WINDOWS.with_borrow(Vec::len) + WINDOW_SUBSCRIBERS.with_borrow(Vec::len)
199}
200
201/// Forgets a window that left the tree, and what it measured.
202pub(crate) fn forget_window(window: u32) {
203    WINDOWS.with_borrow_mut(|windows| windows.retain(|(id, _)| *id != window));
204}
205
206/// A registration for one window's size class. Dropping it ends the registration.
207pub struct WindowSubscription {
208    id: u64,
209}
210
211/// Registers `notify` to be called each time the window `window` measures a different
212/// size class.
213pub fn subscribe_window(window: u32, notify: Arc<dyn Fn() + Send + Sync>) -> WindowSubscription {
214    let id = next_subscriber_id();
215    WINDOW_SUBSCRIBERS.with_borrow_mut(|subscribers| {
216        subscribers.push((window, Subscriber { id, notify }));
217    });
218    WindowSubscription { id }
219}
220
221impl Drop for WindowSubscription {
222    fn drop(&mut self) {
223        WINDOW_SUBSCRIBERS.with_borrow_mut(|subscribers| {
224            subscribers.retain(|(_, subscriber)| subscriber.id != self.id);
225        });
226    }
227}
228
229/// Records a new measurement and wakes the components that asked about it.
230///
231/// Returns whether anything changed. Nothing is allocated here: the subscriber list is
232/// walked in place, and waking a component only marks its scope dirty.
233pub(crate) fn publish(size: WindowSize) -> bool {
234    if CURRENT.with(Cell::get) == size {
235        return false;
236    }
237    CURRENT.with(|current| current.set(size));
238    SUBSCRIBERS.with_borrow(|subscribers| {
239        for subscriber in subscribers {
240            (subscriber.notify)();
241        }
242    });
243    true
244}
245
246/// The size a node last measured, or a zero size for one nothing has reported yet.
247pub fn node_size(token: u32) -> WindowSize {
248    NODES.with_borrow(|nodes| {
249        nodes
250            .iter()
251            .find(|(name, _)| *name == token)
252            .map(|(_, size)| *size)
253            .unwrap_or_default()
254    })
255}
256
257/// Records a node's measurement and wakes whoever asked about that one.
258///
259/// Returns whether anything was woken, so a report about a node nobody is reading costs
260/// a lookup and nothing else.
261pub(crate) fn publish_node(token: u32, size: WindowSize) -> bool {
262    let changed =
263        NODES.with_borrow_mut(
264            |nodes| match nodes.iter_mut().find(|(name, _)| *name == token) {
265                Some(entry) => {
266                    if entry.1 == size {
267                        return false;
268                    }
269                    entry.1 = size;
270                    true
271                }
272                None => {
273                    nodes.push((token, size));
274                    true
275                }
276            },
277        );
278    if !changed {
279        return false;
280    }
281    NODE_SUBSCRIBERS.with_borrow(|subscribers| {
282        let mut woke = false;
283        for (name, subscriber) in subscribers {
284            if *name == token {
285                (subscriber.notify)();
286                woke = true;
287            }
288        }
289        woke
290    })
291}
292
293/// Clears the measurement and every subscription. Used between tests.
294#[doc(hidden)]
295pub fn reset_window_size() {
296    CURRENT.with(|current| current.set(WindowSize::default()));
297    SUBSCRIBERS.with_borrow_mut(Vec::clear);
298    NODES.with_borrow_mut(Vec::clear);
299    NODE_SUBSCRIBERS.with_borrow_mut(Vec::clear);
300    WINDOWS.with_borrow_mut(Vec::clear);
301    WINDOW_SUBSCRIBERS.with_borrow_mut(Vec::clear);
302}
303
304/// A registration for changes of the window's size class. Dropping it ends the
305/// registration, so a runtime keeps it for as long as the reader it wakes is alive.
306pub struct WindowSizeSubscription {
307    id: u64,
308}
309
310/// Registers `notify` to be called each time the window's measured size changes. What
311/// `notify` does is the runtime's business: mark a scope dirty, most often.
312pub fn subscribe(notify: Arc<dyn Fn() + Send + Sync>) -> WindowSizeSubscription {
313    let id = next_subscriber_id();
314    SUBSCRIBERS.with_borrow_mut(|subscribers| subscribers.push(Subscriber { id, notify }));
315    WindowSizeSubscription { id }
316}
317
318fn next_subscriber_id() -> u64 {
319    NEXT_SUBSCRIBER_ID.with(|next| {
320        let id = next.get();
321        next.set(id + 1);
322        id
323    })
324}
325
326impl Drop for WindowSizeSubscription {
327    fn drop(&mut self) {
328        SUBSCRIBERS.with_borrow_mut(|subscribers| {
329            subscribers.retain(|subscriber| subscriber.id != self.id);
330        });
331    }
332}
333
334/// A node whose measured size a reader follows.
335///
336/// The token is the name the screen gives the node, because a node id belongs to the
337/// Renderer and never crosses back as something the Host chose. Attach it with
338/// `observe_size` and read the size here. The Dioxus adapter hands one out from its
339/// `use_node_size` hook.
340///
341/// ```ignore
342/// let panel = use_node_size();
343/// rsx! {
344///     Card {
345///         observe_size: panel.token(),
346///         if panel.is_expanded() { Row { Left {} Right {} } } else { Left {} }
347///     }
348/// }
349/// ```
350///
351/// A component that never calls this costs nothing: the modifier is the only thing that
352/// makes the Renderer measure, and the modifier comes from here.
353#[derive(Clone, Copy, Debug, PartialEq)]
354pub struct NodeSize {
355    token: u32,
356    size: WindowSize,
357}
358
359impl NodeSize {
360    /// The node `token` names, with whatever it last measured.
361    pub fn of(token: u32) -> Self {
362        Self {
363            token,
364            size: node_size(token),
365        }
366    }
367
368    /// The value to hand to `observe_size`.
369    pub const fn token(&self) -> i64 {
370        self.token as i64
371    }
372
373    /// Phone-shaped: narrower than 600dp.
374    pub fn is_compact(&self) -> bool {
375        self.size.is_compact()
376    }
377
378    /// Tablet-shaped.
379    pub fn is_medium(&self) -> bool {
380        self.size.is_medium()
381    }
382
383    /// Desktop-shaped.
384    pub fn is_expanded(&self) -> bool {
385        self.size.is_expanded()
386    }
387
388    /// The measurement itself, zero until the Renderer has reported one.
389    pub const fn measured(&self) -> WindowSize {
390        self.size
391    }
392}
393
394thread_local! {
395    static NEXT_TOKEN: Cell<u32> = const { Cell::new(1) };
396}
397
398/// A fresh name for a node a screen wants measured. Never zero, which is the window's.
399pub fn next_node_token() -> u32 {
400    NEXT_TOKEN.with(|next| {
401        let token = next.get();
402        next.set(token + 1);
403        token
404    })
405}
406
407/// A registration for one node's measured size, made with [`subscribe_node`].
408///
409/// Dropping it ends the registration and forgets what that node measured: the node going
410/// out of the tree takes its measurement with it.
411pub struct NodeSizeSubscription {
412    token: u32,
413    id: u64,
414}
415
416impl NodeSizeSubscription {
417    /// The token this registration follows, the value `observe_size` takes.
418    pub fn token(&self) -> u32 {
419        self.token
420    }
421}
422
423/// Registers `notify` to be called each time the node named `token` reports a size in a
424/// different class.
425pub fn subscribe_node(token: u32, notify: Arc<dyn Fn() + Send + Sync>) -> NodeSizeSubscription {
426    let id = next_subscriber_id();
427    NODE_SUBSCRIBERS.with_borrow_mut(|subscribers| {
428        subscribers.push((token, Subscriber { id, notify }));
429    });
430    NodeSizeSubscription { token, id }
431}
432
433impl Drop for NodeSizeSubscription {
434    fn drop(&mut self) {
435        // The node going out of the tree takes the subscription with it, which is the
436        // third thing this requirement asks for.
437        NODE_SUBSCRIBERS.with_borrow_mut(|subscribers| {
438            subscribers.retain(|(_, subscriber)| subscriber.id != self.id);
439        });
440        NODES.with_borrow_mut(|nodes| nodes.retain(|(token, _)| *token != self.token));
441    }
442}
443
444#[cfg(test)]
445mod tests {
446    use super::*;
447
448    #[test]
449    fn fr19_9_safe_area_only_change_republishes() {
450        use std::sync::atomic::{AtomicUsize, Ordering};
451        reset_window_size();
452        let calls = Arc::new(AtomicUsize::new(0));
453        let counter = Arc::clone(&calls);
454        let _subscription = subscribe(Arc::new(move || {
455            counter.fetch_add(1, Ordering::SeqCst);
456        }));
457        let size = WindowSize::default().with_safe_area(WindowSafeArea {
458            top: 28.0,
459            leading: 70.0,
460            trailing: 0.0,
461        });
462        assert!(publish(size));
463        assert_eq!(window_size(), size);
464        assert_eq!(calls.load(Ordering::SeqCst), 1);
465        assert!(!publish(size));
466        assert_eq!(calls.load(Ordering::SeqCst), 1);
467    }
468
469    #[test]
470    fn fr20_window_size_starts_compact_before_the_renderer_measures_anything() {
471        reset_window_size();
472        assert_eq!(window_size(), WindowSize::default());
473        assert!(window_size().is_compact());
474    }
475
476    #[test]
477    fn fr20_publishing_the_same_size_twice_wakes_nobody() {
478        reset_window_size();
479        let size = WindowSize::new(
480            700.0,
481            800.0,
482            WindowSizeClass::Medium,
483            WindowHeightClass::Medium,
484        );
485        assert!(publish(size));
486        assert!(!publish(size));
487        reset_window_size();
488    }
489}