Skip to main content

compose_rust/
theme.rs

1//! Changing the theme while the application runs, and the palettes that come with it.
2//!
3//! `LaunchBuilder::with_theme` settles the theme once, before anything is drawn, and a
4//! `const` palette rides on it as a `&'static` reference. An application that lets the
5//! reader pick a design system, a scheme or a brand colour at run time needs to send a new
6//! `SetTheme`, and a palette built at run time has no `'static` home of its own.
7//!
8//! [`ThemeHandle`] is that path, and the Dioxus adapter's `use_theme` hook hands one out.
9//! Setting a theme queues it and it rides out on the batch the current call produces, the
10//! way a message does: one record, applied by the Renderer to the whole tree. A palette handed over by value is owned here, so application code never
11//! leaks anything to satisfy the lifetime.
12
13use crate::palette::Palette;
14use crate::schema::{ColorScheme, DesignSystem, SystemColorScheme, Theme};
15use std::cell::{Cell, RefCell};
16use std::sync::Arc;
17
18thread_local! {
19    /// The theme the Host is running with, as the application last asked for it.
20    static CURRENT: Cell<Theme> = const { Cell::new(Theme::unified(DesignSystem::Material3)) };
21    /// A theme asked for since the last batch, not yet written into one.
22    static PENDING: Cell<Option<Theme>> = const { Cell::new(None) };
23    /// Every distinct palette handed over at run time, kept for the life of the process.
24    ///
25    /// Interned by value, so switching between the same few palettes any number of times
26    /// holds each of them once. A palette has to outlive every batch and every Renderer
27    /// frame that might still be resolving against it, and the theme that names it is
28    /// `Copy`, so there is no point at which one could be known to be unused.
29    static OWNED: RefCell<Vec<&'static Palette>> = const { RefCell::new(Vec::new()) };
30}
31
32/// The theme the application is running with: what it launched with, or what it last set.
33pub fn current_theme() -> Theme {
34    PENDING
35        .with(Cell::get)
36        .unwrap_or_else(|| CURRENT.with(Cell::get))
37}
38
39/// Replaces the whole theme. The Renderer receives it in the batch this call produces.
40///
41/// The theme is one record. Nothing in the tree is resent, because every node reads the
42/// theme from the Renderer's side.
43pub fn set_theme(theme: Theme) {
44    #[cfg(debug_assertions)]
45    if theme.palette != current_theme().palette {
46        for line in crate::boundary::palette_report(&theme) {
47            eprintln!("{line}");
48        }
49    }
50    PENDING.with(|pending| pending.set(Some(theme)));
51}
52
53/// A palette made at run time, given a home that lives as long as the program.
54///
55/// The same palette handed over twice is the same home.
56pub fn own_palette(palette: Palette) -> &'static Palette {
57    OWNED.with_borrow_mut(|owned| {
58        if let Some(existing) = owned.iter().find(|existing| ***existing == palette) {
59            return *existing;
60        }
61        let home: &'static Palette = Box::leak(Box::new(palette));
62        owned.push(home);
63        home
64    })
65}
66
67/// How many distinct run time palettes are held. Exposed for tests.
68#[doc(hidden)]
69pub fn owned_palette_count() -> usize {
70    OWNED.with_borrow(Vec::len)
71}
72
73/// Records the theme a Host starts with and forgets anything queued for the one before.
74pub(crate) fn install(theme: Theme) {
75    CURRENT.with(|current| current.set(theme));
76    PENDING.with(|pending| pending.set(None));
77}
78
79/// The theme queued since the last batch, if any. It becomes the current one.
80pub(crate) fn take_pending() -> Option<Theme> {
81    let theme = PENDING.with(Cell::take)?;
82    CURRENT.with(|current| current.set(theme));
83    Some(theme)
84}
85
86/// A handle on the running theme. `Copy`, so it can move into any number of handlers.
87#[derive(Clone, Copy, Debug, Default)]
88pub struct ThemeHandle {
89    _private: (),
90}
91
92impl ThemeHandle {
93    /// The theme in force, including one set earlier in this same call.
94    pub fn get(&self) -> Theme {
95        current_theme()
96    }
97
98    /// Replaces the whole theme.
99    pub fn set(&self, theme: Theme) {
100        set_theme(theme);
101    }
102
103    /// Lays a palette made at run time over the current theme. The palette is owned by
104    /// this module; nothing has to be leaked by the caller.
105    pub fn set_palette(&self, palette: Palette) {
106        let theme = current_theme().with_palette(own_palette(palette));
107        set_theme(theme);
108    }
109
110    /// Takes the application's palette away, leaving the design system's colours.
111    pub fn clear_palette(&self) {
112        let mut theme = current_theme();
113        theme.palette = None;
114        set_theme(theme);
115    }
116
117    /// Picks light, dark, or following the system, keeping everything else.
118    pub fn set_color_scheme(&self, scheme: ColorScheme) {
119        set_theme(current_theme().with_color_scheme(scheme));
120    }
121}
122
123struct SchemeSubscriber {
124    id: u64,
125    notify: Arc<dyn Fn() + Send + Sync>,
126}
127
128thread_local! {
129    /// The system appearance as the Renderer last reported it. Unknown until the first
130    /// report, which the Renderer makes before its first frame.
131    static SYSTEM_SCHEME: Cell<Option<SystemColorScheme>> = const { Cell::new(None) };
132    static SCHEME_SUBSCRIBERS: RefCell<Vec<SchemeSubscriber>> = const { RefCell::new(Vec::new()) };
133    static NEXT_SCHEME_SUBSCRIBER: Cell<u64> = const { Cell::new(1) };
134}
135
136/// Whether the system appearance is light or dark, as the Renderer last reported it, or
137/// `None` before its first report.
138///
139/// This is the system's setting whatever the theme asks for. An application that draws
140/// something of its own, such as an HTML page answering `prefers-color-scheme`, reads it
141/// here; native widgets already follow the theme without it.
142pub fn system_color_scheme() -> Option<SystemColorScheme> {
143    SYSTEM_SCHEME.with(Cell::get)
144}
145
146/// The scheme the theme is drawn in: the theme's own choice when it names one, and the
147/// system's when it follows the system. `None` only while it follows a system that has not
148/// been reported yet.
149pub fn resolved_color_scheme() -> Option<SystemColorScheme> {
150    match current_theme().color_scheme {
151        ColorScheme::Light => Some(SystemColorScheme::Light),
152        ColorScheme::Dark => Some(SystemColorScheme::Dark),
153        ColorScheme::FollowSystem => system_color_scheme(),
154    }
155}
156
157/// Records the Renderer's report and wakes the readers. Returns whether any was woken.
158///
159/// A report of the value already held wakes nobody, so a reader is never told the same
160/// thing twice even if the Renderer were to repeat itself.
161pub(crate) fn publish_system_color_scheme(scheme: SystemColorScheme) -> bool {
162    if SYSTEM_SCHEME.with(Cell::get) == Some(scheme) {
163        return false;
164    }
165    SYSTEM_SCHEME.with(|current| current.set(Some(scheme)));
166    // Copied out first, because a reader may subscribe or drop its subscription.
167    let readers: Vec<Arc<dyn Fn() + Send + Sync>> = SCHEME_SUBSCRIBERS
168        .with_borrow(|subscribers| subscribers.iter().map(|s| s.notify.clone()).collect());
169    for notify in &readers {
170        notify();
171    }
172    !readers.is_empty()
173}
174
175/// A registration for changes of the system appearance, ended by dropping it.
176pub struct SystemColorSchemeSubscription {
177    id: u64,
178}
179
180/// Registers `notify` to be called each time the system appearance changes. A runtime's
181/// hook for the scheme holds one for as long as the reader it wakes is alive.
182pub fn subscribe_system_color_scheme(
183    notify: Arc<dyn Fn() + Send + Sync>,
184) -> SystemColorSchemeSubscription {
185    let id = NEXT_SCHEME_SUBSCRIBER.with(|next| {
186        let id = next.get();
187        next.set(id + 1);
188        id
189    });
190    SCHEME_SUBSCRIBERS
191        .with_borrow_mut(|subscribers| subscribers.push(SchemeSubscriber { id, notify }));
192    SystemColorSchemeSubscription { id }
193}
194
195impl Drop for SystemColorSchemeSubscription {
196    fn drop(&mut self) {
197        SCHEME_SUBSCRIBERS.with_borrow_mut(|subscribers| subscribers.retain(|s| s.id != self.id));
198    }
199}
200
201/// Forgets the reported appearance and every reader. Used between tests.
202#[doc(hidden)]
203pub fn reset_system_color_scheme() {
204    SYSTEM_SCHEME.with(|current| current.set(None));
205    SCHEME_SUBSCRIBERS.with_borrow_mut(Vec::clear);
206}