Skip to main content

compose_rust/
design.rs

1//! Which design system the Renderer resolved the theme to, and the registrations a
2//! runtime wakes its readers through.
3//!
4//! The theme names a system or asks for the platform's, and the answer is worked out in
5//! the Renderer where the platform is known. Most screens never need it: colour, shape,
6//! spacing and type are roles, and a role is the same declaration whatever answers it.
7//!
8//! What roles cannot carry is a screen that is a different screen. Apple's calculator has
9//! no memory row and clears with `AC`; Windows' has one and clears with `C`. Both are
10//! calculators and neither is a restyling of the other.
11//!
12//! So the Renderer reports its answer the way it reports a size class: once, and again
13//! only when the answer changes. This module is that value plus the readers that asked to
14//! be told. How a reader asks is the authoring layer's: the Dioxus adapter's
15//! `use_design_system` hook holds a [`DesignSystemSubscription`].
16
17use crate::schema::DesignSystem;
18use std::cell::{Cell, RefCell};
19use std::sync::Arc;
20
21struct Subscriber {
22    id: u64,
23    notify: Arc<dyn Fn() + Send + Sync>,
24}
25
26thread_local! {
27    /// Material 3 until the Renderer says otherwise, which is the same fallback the
28    /// theme's own default carries.
29    static CURRENT: Cell<DesignSystem> = const { Cell::new(DesignSystem::Material3) };
30    static SUBSCRIBERS: RefCell<Vec<Subscriber>> = const { RefCell::new(Vec::new()) };
31    static NEXT_SUBSCRIBER_ID: Cell<u64> = const { Cell::new(1) };
32}
33
34/// The system the Renderer last reported. Available outside a component too.
35pub fn design_system() -> DesignSystem {
36    CURRENT.with(Cell::get)
37}
38
39/// Records the Renderer's answer and wakes the components that asked about it.
40///
41/// Returns whether any component was woken, which is not the same as whether the value
42/// changed. A screen that never asks which system is running must not be re-rendered
43/// because the Renderer reported one: the answer is still recorded, so a component that
44/// mounts later and does ask reads it, but nothing is redrawn for an audience of nobody.
45pub(crate) fn publish(system: DesignSystem) -> bool {
46    if CURRENT.with(Cell::get) == system {
47        return false;
48    }
49    CURRENT.with(|current| current.set(system));
50    SUBSCRIBERS.with_borrow(|subscribers| {
51        for subscriber in subscribers {
52            (subscriber.notify)();
53        }
54        !subscribers.is_empty()
55    })
56}
57
58/// Clears the answer and every subscription. Used between tests.
59#[doc(hidden)]
60pub fn reset_design_system() {
61    CURRENT.with(|current| current.set(DesignSystem::Material3));
62    SUBSCRIBERS.with_borrow_mut(Vec::clear);
63}
64
65/// A registration for changes to the resolved design system. Dropping it ends the
66/// registration, so a runtime keeps it for as long as the reader it wakes is alive: with a
67/// component's hook state, or a scope's remembered values.
68pub struct DesignSystemSubscription {
69    id: u64,
70}
71
72/// Registers `notify` to be called each time the Renderer reports a different design
73/// system. What `notify` does is the runtime's business: mark a scope dirty, most often.
74pub fn subscribe(notify: Arc<dyn Fn() + Send + Sync>) -> DesignSystemSubscription {
75    let id = NEXT_SUBSCRIBER_ID.with(|next| {
76        let id = next.get();
77        next.set(id + 1);
78        id
79    });
80    SUBSCRIBERS.with_borrow_mut(|subscribers| subscribers.push(Subscriber { id, notify }));
81    DesignSystemSubscription { id }
82}
83
84impl Drop for DesignSystemSubscription {
85    fn drop(&mut self) {
86        SUBSCRIBERS.with_borrow_mut(|subscribers| {
87            subscribers.retain(|subscriber| subscriber.id != self.id);
88        });
89    }
90}