Skip to main content

fc_dioxus/
theme.rs

1//! Root theme, stylesheet policy, and deterministic context.
2
3use crate::core::{
4    assets,
5    attrs::{theme_attributes, theme_pairs},
6    id::IdAllocator,
7    platform::{Capabilities, DomBridge},
8};
9use dioxus::prelude::*;
10use std::{
11    cell::Cell,
12    rc::Rc,
13    sync::atomic::{AtomicU64, Ordering},
14};
15
16// The two catalogs below mirror upstream's `src/js/theme-presets.ts` at the pin, in the
17// same order, so the demo lists them as upstream's own picker does. `tests/themes.rs`
18// compares them against the synced copy in `upstream/theme-presets.ts`.
19
20/// A stock visual preset: geometry, elevation, surface treatment, and type.
21///
22/// A design carries no colors of its own. Colors come from a [`Palette`], and each design
23/// names the palette upstream pairs it with ([`Design::palette`]), which `FutureCss` applies
24/// unless a palette is chosen explicitly.
25#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
26#[non_exhaustive]
27pub enum Design {
28    /// Future: Balanced corners, quiet surfaces and soft elevation.
29    #[default]
30    Future,
31    /// Clarity: Crisp edges, flat surfaces and outlined actions.
32    Clarity,
33    /// Helix: Charcoal chrome, mint actions and violet feature surfaces.
34    Helix,
35    /// Forest: Organic curves, green accents and calm surfaces.
36    Forest,
37    /// Editorial: Serif reading, restrained corners and ink-like controls.
38    Editorial,
39    /// Graphite: Monochrome surfaces with distinct, structured edges.
40    Graphite,
41    /// Ocean: Cool teal, rounded panels and a tinted header.
42    Ocean,
43    /// Ember: Warm accents, substantial elevation and squared panels.
44    Ember,
45    /// Blueprint: Square geometry, blue accents and precise outlines.
46    Blueprint,
47    /// Bloom: Playful curves, rose accents and floating colored panels.
48    Bloom,
49    /// Aurora: Violet ribbons, luminous curves and floating surfaces.
50    Aurora,
51    /// Candy: Rose candy stripes, pillowy panels and playful elevation.
52    Candy,
53    /// Solar: Amber sunshine, square panels and decisive offset shadows.
54    Solar,
55    /// Nocturne: Ink chrome, violet spotlights and elegant serif reading.
56    Nocturne,
57    /// Arcade: Green scanlines, square geometry and terminal-inspired type.
58    Arcade,
59    /// Parchment: Warm ruled paper, serif type and quiet outlined controls.
60    Parchment,
61    /// Bubble: Teal bubbles, generous curves and buoyant layered shadows.
62    Bubble,
63    /// Garnet: Rose jewel facets, crisp serif panels and charcoal chrome.
64    Garnet,
65    /// Origami: Blue folded ribbons, angular outlines and paper shadows.
66    Origami,
67    /// Sundown: Ember horizons, rounded reading panels and long soft shadows.
68    Sundown,
69    /// Pride: Rainbow ribbons, joyful rounded panels and clear violet actions.
70    Pride,
71}
72impl Design {
73    /// Every stock design at this pin, in upstream's catalog order.
74    pub const ALL: &'static [Self] = &[
75        Self::Future,
76        Self::Clarity,
77        Self::Helix,
78        Self::Forest,
79        Self::Editorial,
80        Self::Graphite,
81        Self::Ocean,
82        Self::Ember,
83        Self::Blueprint,
84        Self::Bloom,
85        Self::Aurora,
86        Self::Candy,
87        Self::Solar,
88        Self::Nocturne,
89        Self::Arcade,
90        Self::Parchment,
91        Self::Bubble,
92        Self::Garnet,
93        Self::Origami,
94        Self::Sundown,
95        Self::Pride,
96    ];
97    /// The `data-fc-design` value.
98    #[must_use]
99    pub const fn as_str(self) -> &'static str {
100        match self {
101            Self::Future => "future",
102            Self::Clarity => "clarity",
103            Self::Helix => "helix",
104            Self::Forest => "forest",
105            Self::Editorial => "editorial",
106            Self::Graphite => "graphite",
107            Self::Ocean => "ocean",
108            Self::Ember => "ember",
109            Self::Blueprint => "blueprint",
110            Self::Bloom => "bloom",
111            Self::Aurora => "aurora",
112            Self::Candy => "candy",
113            Self::Solar => "solar",
114            Self::Nocturne => "nocturne",
115            Self::Arcade => "arcade",
116            Self::Parchment => "parchment",
117            Self::Bubble => "bubble",
118            Self::Garnet => "garnet",
119            Self::Origami => "origami",
120            Self::Sundown => "sundown",
121            Self::Pride => "pride",
122        }
123    }
124    /// Upstream's display name.
125    #[must_use]
126    pub const fn name(self) -> &'static str {
127        match self {
128            Self::Future => "Future",
129            Self::Clarity => "Clarity",
130            Self::Helix => "Helix",
131            Self::Forest => "Forest",
132            Self::Editorial => "Editorial",
133            Self::Graphite => "Graphite",
134            Self::Ocean => "Ocean",
135            Self::Ember => "Ember",
136            Self::Blueprint => "Blueprint",
137            Self::Bloom => "Bloom",
138            Self::Aurora => "Aurora",
139            Self::Candy => "Candy",
140            Self::Solar => "Solar",
141            Self::Nocturne => "Nocturne",
142            Self::Arcade => "Arcade",
143            Self::Parchment => "Parchment",
144            Self::Bubble => "Bubble",
145            Self::Garnet => "Garnet",
146            Self::Origami => "Origami",
147            Self::Sundown => "Sundown",
148            Self::Pride => "Pride",
149        }
150    }
151    /// Upstream's one-line description.
152    #[must_use]
153    pub const fn description(self) -> &'static str {
154        match self {
155            Self::Future => "Balanced corners, quiet surfaces and soft elevation.",
156            Self::Clarity => "Crisp edges, flat surfaces and outlined actions.",
157            Self::Helix => "Charcoal chrome, mint actions and violet feature surfaces.",
158            Self::Forest => "Organic curves, green accents and calm surfaces.",
159            Self::Editorial => "Serif reading, restrained corners and ink-like controls.",
160            Self::Graphite => "Monochrome surfaces with distinct, structured edges.",
161            Self::Ocean => "Cool teal, rounded panels and a tinted header.",
162            Self::Ember => "Warm accents, substantial elevation and squared panels.",
163            Self::Blueprint => "Square geometry, blue accents and precise outlines.",
164            Self::Bloom => "Playful curves, rose accents and floating colored panels.",
165            Self::Aurora => "Violet ribbons, luminous curves and floating surfaces.",
166            Self::Candy => "Rose candy stripes, pillowy panels and playful elevation.",
167            Self::Solar => "Amber sunshine, square panels and decisive offset shadows.",
168            Self::Nocturne => "Ink chrome, violet spotlights and elegant serif reading.",
169            Self::Arcade => "Green scanlines, square geometry and terminal-inspired type.",
170            Self::Parchment => "Warm ruled paper, serif type and quiet outlined controls.",
171            Self::Bubble => "Teal bubbles, generous curves and buoyant layered shadows.",
172            Self::Garnet => "Rose jewel facets, crisp serif panels and charcoal chrome.",
173            Self::Origami => "Blue folded ribbons, angular outlines and paper shadows.",
174            Self::Sundown => "Ember horizons, rounded reading panels and long soft shadows.",
175            Self::Pride => "Rainbow ribbons, joyful rounded panels and clear violet actions.",
176        }
177    }
178    /// The palette upstream pairs with this design.
179    #[must_use]
180    // One arm per design in catalog order reads against upstream line by line; merging
181    // the arms that share a palette would hide which design a pairing belongs to.
182    #[allow(clippy::match_same_arms)]
183    pub const fn palette(self) -> Palette {
184        match self {
185            Self::Future => Palette::Default,
186            Self::Clarity => Palette::Default,
187            Self::Helix => Palette::Helix,
188            Self::Forest => Palette::Forest,
189            Self::Editorial => Palette::Ink,
190            Self::Graphite => Palette::Ink,
191            Self::Ocean => Palette::Teal,
192            Self::Ember => Palette::Ember,
193            Self::Blueprint => Palette::Blue,
194            Self::Bloom => Palette::Rose,
195            Self::Aurora => Palette::Violet,
196            Self::Candy => Palette::Rose,
197            Self::Solar => Palette::Amber,
198            Self::Nocturne => Palette::Violet,
199            Self::Arcade => Palette::Forest,
200            Self::Parchment => Palette::Amber,
201            Self::Bubble => Palette::Teal,
202            Self::Garnet => Palette::Rose,
203            Self::Origami => Palette::Blue,
204            Self::Sundown => Palette::Ember,
205            Self::Pride => Palette::Violet,
206        }
207    }
208    /// Parse a `data-fc-design` value.
209    #[must_use]
210    pub fn from_attribute(value: &str) -> Option<Self> {
211        Self::ALL
212            .iter()
213            .copied()
214            .find(|design| design.as_str() == value)
215    }
216}
217
218/// A stock color palette: every semantic color role, in light and dark.
219#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
220#[non_exhaustive]
221pub enum Palette {
222    /// Future blue.
223    #[default]
224    Default,
225    /// Blueprint blue.
226    Blue,
227    /// Violet.
228    Violet,
229    /// Forest.
230    Forest,
231    /// Ink.
232    Ink,
233    /// Ocean teal.
234    Teal,
235    /// Ember.
236    Ember,
237    /// Rose.
238    Rose,
239    /// Amber.
240    Amber,
241    /// Helix violet & mint.
242    Helix,
243}
244impl Palette {
245    /// Every stock palette at this pin, in upstream's catalog order.
246    pub const ALL: &'static [Self] = &[
247        Self::Default,
248        Self::Blue,
249        Self::Violet,
250        Self::Forest,
251        Self::Ink,
252        Self::Teal,
253        Self::Ember,
254        Self::Rose,
255        Self::Amber,
256        Self::Helix,
257    ];
258    /// The `data-fc-palette` value.
259    #[must_use]
260    pub const fn as_str(self) -> &'static str {
261        match self {
262            Self::Default => "default",
263            Self::Blue => "blue",
264            Self::Violet => "violet",
265            Self::Forest => "forest",
266            Self::Ink => "ink",
267            Self::Teal => "teal",
268            Self::Ember => "ember",
269            Self::Rose => "rose",
270            Self::Amber => "amber",
271            Self::Helix => "helix",
272        }
273    }
274    /// Upstream's display name.
275    #[must_use]
276    pub const fn name(self) -> &'static str {
277        match self {
278            Self::Default => "Future blue",
279            Self::Blue => "Blueprint blue",
280            Self::Violet => "Violet",
281            Self::Forest => "Forest",
282            Self::Ink => "Ink",
283            Self::Teal => "Ocean teal",
284            Self::Ember => "Ember",
285            Self::Rose => "Rose",
286            Self::Amber => "Amber",
287            Self::Helix => "Helix violet & mint",
288        }
289    }
290    /// Parse a `data-fc-palette` value.
291    #[must_use]
292    pub fn from_attribute(value: &str) -> Option<Self> {
293        Self::ALL
294            .iter()
295            .copied()
296            .find(|palette| palette.as_str() == value)
297    }
298}
299
300/// Upstream's supported schemes. System inherits the user's preference.
301#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
302#[non_exhaustive]
303pub enum Theme {
304    /// Follow the surrounding theme and system preference.
305    #[default]
306    System,
307    /// Light scheme.
308    Light,
309    /// Dark scheme.
310    Dark,
311    /// High-contrast light scheme.
312    HighContrastLight,
313    /// High-contrast dark scheme.
314    HighContrastDark,
315    /// Follow the user's system preference even inside a root that sets a scheme;
316    /// [`Theme::System`] inherits the surrounding one instead.
317    FollowSystem,
318}
319impl Theme {
320    /// The upstream attribute, absent in system mode.
321    #[must_use]
322    pub const fn attribute(self) -> Option<&'static str> {
323        match self {
324            Self::System => None,
325            Self::Light => Some("light"),
326            Self::Dark => Some("dark"),
327            Self::HighContrastLight => Some("high-contrast-light"),
328            Self::HighContrastDark => Some("high-contrast-dark"),
329            Self::FollowSystem => Some("system"),
330        }
331    }
332}
333/// Upstream's optional brand palette.
334#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
335#[non_exhaustive]
336pub enum Brand {
337    /// The forest palette.
338    Forest,
339}
340/// Inline text direction.
341#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
342#[non_exhaustive]
343pub enum Direction {
344    /// Left to right.
345    #[default]
346    Ltr,
347    /// Right to left.
348    Rtl,
349}
350impl Direction {
351    /// HTML's `dir` value.
352    #[must_use]
353    pub const fn as_str(self) -> &'static str {
354        match self {
355            Self::Ltr => "ltr",
356            Self::Rtl => "rtl",
357        }
358    }
359}
360/// Whether components inject their generated upstream stylesheet assets.
361#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
362pub enum Stylesheets {
363    /// Root foundation files plus the stylesheets of rendered components.
364    #[default]
365    Automatic,
366    /// The application already loads the upstream bundle.
367    External,
368}
369
370/// The reading direction in effect, from the nearest [`FutureCss`] that set one.
371#[derive(Debug, Clone, Copy, PartialEq, Eq)]
372struct DirectionContext(Direction);
373
374/// The reading direction components should use for arrow keys. Left-to-right unless a
375/// [`FutureCss`] root above sets `direction`.
376#[must_use]
377pub fn use_direction() -> Direction {
378    try_consume_context::<Signal<DirectionContext>>()
379        .map_or(Direction::Ltr, |direction| direction.read().0)
380}
381
382pub(crate) fn use_stylesheets() -> bool {
383    try_consume_context::<ReadSignal<Stylesheets>>()
384        .is_none_or(|policy| *policy.read() == Stylesheets::Automatic)
385}
386
387/// Props for the root theme and CSS provider.
388#[derive(Props, Clone, PartialEq)]
389pub struct FutureCssProps {
390    /// Optional stock visual preset; absence inherits the surrounding design.
391    #[props(default)]
392    pub design: Option<Design>,
393    /// Optional color palette. When omitted, a set `design` applies its own paired
394    /// palette, and with neither the surrounding palette is inherited.
395    #[props(default)]
396    pub palette: Option<Palette>,
397    /// Scheme, inherited when omitted.
398    #[props(default)]
399    pub theme: Theme,
400    /// Optional upstream brand.
401    #[props(default)]
402    pub brand: Option<Brand>,
403    /// Inline direction, inherited when omitted.
404    #[props(default)]
405    pub direction: Option<Direction>,
406    /// Set to external if the app already loads the CSS bundle.
407    #[props(default)]
408    pub stylesheets: Stylesheets,
409    /// The application's component tree.
410    pub children: Element,
411}
412
413// Names each outermost root to the document-side theme owner.
414static NEXT_ROOT: AtomicU64 = AtomicU64::new(0);
415
416/// Provide the pinned stylesheets and theme without changing pre-mount structure.
417#[component]
418pub fn FutureCss(props: FutureCssProps) -> Element {
419    // Only the outermost root reaches <html>; checked before this root provides its own
420    // direction context, which is what a nested root finds.
421    let outermost = use_hook(|| try_consume_context::<Signal<DirectionContext>>().is_none());
422    use_context_provider(IdAllocator::new);
423    // A root without a direction inherits the one around it.
424    let inherited = use_direction();
425    let effective = DirectionContext(props.direction.unwrap_or(inherited));
426    let mut direction = use_context_provider(|| Signal::new(effective));
427    if *direction.peek() != effective {
428        direction.set(effective);
429    }
430    let mut policy = use_signal(|| props.stylesheets);
431    if *policy.peek() != props.stylesheets {
432        policy.set(props.stylesheets);
433    }
434    use_context_provider(|| ReadSignal::from(policy));
435    let capabilities = use_signal(Capabilities::default);
436    use_context_provider(|| capabilities);
437    let mut capabilities = capabilities;
438    let pairs = theme_pairs(
439        props.theme.attribute(),
440        props.brand.map(|brand| match brand {
441            Brand::Forest => "forest",
442        }),
443        props.design.map(Design::as_str),
444        // A design without its palette restyles shapes but leaves every color at the
445        // default, which reads as the theme failing to apply.
446        props
447            .palette
448            .or(props.design.map(Design::palette))
449            .map(Palette::as_str),
450    );
451    let attrs = theme_attributes(&pairs);
452    // The scheme on this root's element cannot reach <html> and <body>, whose canvas
453    // background then stays on the OS scheme around and beyond the content. Upstream
454    // expects these attributes on <html>, as its own theme picker sets them.
455    // Tokens are unique per process, which covers every document a process hosts.
456    let token = use_hook(|| NEXT_ROOT.fetch_add(1, Ordering::Relaxed));
457    let generation = use_hook(|| Rc::new(Cell::new(0_u64)));
458    let next = generation.clone();
459    use_effect(use_reactive!(|pairs| {
460        if !outermost {
461            return;
462        }
463        next.set(next.get() + 1);
464        let generation = next.get();
465        spawn(async move {
466            let _ = DomBridge
467                .mirror_document_theme(token, generation, Some(&pairs))
468                .await;
469        });
470    }));
471    use_drop(move || {
472        if outermost {
473            DomBridge.release_document_theme(token, generation.get() + 1);
474        }
475    });
476    rsx! {
477        if props.stylesheets == Stylesheets::Automatic {
478            document::Stylesheet { href: assets::LAYERS_CSS }
479            document::Stylesheet { href: assets::TOKENS_CSS }
480            document::Stylesheet { href: assets::RESET_CSS }
481            document::Stylesheet { href: assets::BASE_CSS }
482            document::Stylesheet { href: assets::LAYOUT_CSS }
483            document::Stylesheet { href: assets::TYPE_CSS }
484            document::Stylesheet { href: assets::UTILITIES_CSS }
485            document::Stylesheet { href: assets::THEMES_CSS }
486        }
487        div {
488            dir: props.direction.map(Direction::as_str),
489            onmounted: move |_| async move { capabilities.set(DomBridge.capabilities().await); },
490            ..attrs,
491            {props.children}
492        }
493    }
494}