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