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, Theme};
15use std::cell::{Cell, RefCell};
16
17thread_local! {
18    /// The theme the Host is running with, as the application last asked for it.
19    static CURRENT: Cell<Theme> = const { Cell::new(Theme::unified(DesignSystem::Material3)) };
20    /// A theme asked for since the last batch, not yet written into one.
21    static PENDING: Cell<Option<Theme>> = const { Cell::new(None) };
22    /// Every distinct palette handed over at run time, kept for the life of the process.
23    ///
24    /// Interned by value, so switching between the same few palettes any number of times
25    /// holds each of them once. A palette has to outlive every batch and every Renderer
26    /// frame that might still be resolving against it, and the theme that names it is
27    /// `Copy`, so there is no point at which one could be known to be unused.
28    static OWNED: RefCell<Vec<&'static Palette>> = const { RefCell::new(Vec::new()) };
29}
30
31/// The theme the application is running with: what it launched with, or what it last set.
32pub fn current_theme() -> Theme {
33    PENDING
34        .with(Cell::get)
35        .unwrap_or_else(|| CURRENT.with(Cell::get))
36}
37
38/// Replaces the whole theme. The Renderer receives it in the batch this call produces.
39///
40/// The theme is one record. Nothing in the tree is resent, because every node reads the
41/// theme from the Renderer's side.
42pub fn set_theme(theme: Theme) {
43    #[cfg(debug_assertions)]
44    if theme.palette != current_theme().palette {
45        for line in crate::boundary::palette_report(&theme) {
46            eprintln!("{line}");
47        }
48    }
49    PENDING.with(|pending| pending.set(Some(theme)));
50}
51
52/// A palette made at run time, given a home that lives as long as the program.
53///
54/// The same palette handed over twice is the same home.
55pub fn own_palette(palette: Palette) -> &'static Palette {
56    OWNED.with_borrow_mut(|owned| {
57        if let Some(existing) = owned.iter().find(|existing| ***existing == palette) {
58            return *existing;
59        }
60        let home: &'static Palette = Box::leak(Box::new(palette));
61        owned.push(home);
62        home
63    })
64}
65
66/// How many distinct run time palettes are held. Exposed for tests.
67#[doc(hidden)]
68pub fn owned_palette_count() -> usize {
69    OWNED.with_borrow(Vec::len)
70}
71
72/// Records the theme a Host starts with and forgets anything queued for the one before.
73pub(crate) fn install(theme: Theme) {
74    CURRENT.with(|current| current.set(theme));
75    PENDING.with(|pending| pending.set(None));
76}
77
78/// The theme queued since the last batch, if any. It becomes the current one.
79pub(crate) fn take_pending() -> Option<Theme> {
80    let theme = PENDING.with(Cell::take)?;
81    CURRENT.with(|current| current.set(theme));
82    Some(theme)
83}
84
85/// A handle on the running theme. `Copy`, so it can move into any number of handlers.
86#[derive(Clone, Copy, Debug, Default)]
87pub struct ThemeHandle {
88    _private: (),
89}
90
91impl ThemeHandle {
92    /// The theme in force, including one set earlier in this same call.
93    pub fn get(&self) -> Theme {
94        current_theme()
95    }
96
97    /// Replaces the whole theme.
98    pub fn set(&self, theme: Theme) {
99        set_theme(theme);
100    }
101
102    /// Lays a palette made at run time over the current theme. The palette is owned by
103    /// this module; nothing has to be leaked by the caller.
104    pub fn set_palette(&self, palette: Palette) {
105        let theme = current_theme().with_palette(own_palette(palette));
106        set_theme(theme);
107    }
108
109    /// Takes the application's palette away, leaving the design system's colours.
110    pub fn clear_palette(&self) {
111        let mut theme = current_theme();
112        theme.palette = None;
113        set_theme(theme);
114    }
115
116    /// Picks light, dark, or following the system, keeping everything else.
117    pub fn set_color_scheme(&self, scheme: ColorScheme) {
118        set_theme(current_theme().with_color_scheme(scheme));
119    }
120}