compose_rust/design.rs
1//! Which design system the Renderer resolved the theme to, and the hook components read
2//! it 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 components that asked
14//! to be told.
15
16use crate::schema::DesignSystem;
17use std::cell::{Cell, RefCell};
18use std::rc::Rc;
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 component's registration, dropped with the hook state when the component unmounts.
66struct DesignSystemSubscription {
67 id: u64,
68}
69
70impl DesignSystemSubscription {
71 fn new(notify: Arc<dyn Fn() + Send + Sync>) -> Self {
72 let id = NEXT_SUBSCRIBER_ID.with(|next| {
73 let id = next.get();
74 next.set(id + 1);
75 id
76 });
77 SUBSCRIBERS.with_borrow_mut(|subscribers| subscribers.push(Subscriber { id, notify }));
78 Self { id }
79 }
80}
81
82impl Drop for DesignSystemSubscription {
83 fn drop(&mut self) {
84 SUBSCRIBERS.with_borrow_mut(|subscribers| {
85 subscribers.retain(|subscriber| subscriber.id != self.id);
86 });
87 }
88}
89
90/// Reads the resolved design system inside a component, and re-renders it when the answer
91/// changes.
92///
93/// ```ignore
94/// let design = use_design_system();
95/// if design.is_apple() {
96/// // AC, and no memory row
97/// } else {
98/// // C, and a memory row
99/// }
100/// ```
101///
102/// **This is not how an application chooses a design system.** Choosing is
103/// `Theme::unified`; this reads an answer already given. And it is not a way to paint
104/// colours by hand: what a role resolves to is still the design system's, and reaching
105/// for a literal because this told you which system is running is the thing the role
106/// vocabulary exists to prevent.
107pub fn use_design_system() -> DesignSystem {
108 // Rc for the same reason the window size hook uses one: hook state has to be `Clone`,
109 // and the registration must be dropped exactly once, with the last handle.
110 dioxus_core::use_hook(|| {
111 Rc::new(DesignSystemSubscription::new(dioxus_core::schedule_update()))
112 });
113 design_system()
114}