use crate::core::{
assets,
attrs::{theme_attributes, theme_pairs},
id::IdAllocator,
platform::{Capabilities, DomBridge},
};
use dioxus::prelude::*;
use std::{
cell::Cell,
rc::Rc,
sync::atomic::{AtomicU64, Ordering},
};
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Design {
#[default]
Future,
Clarity,
Helix,
Forest,
Editorial,
Graphite,
Ocean,
Ember,
Blueprint,
Bloom,
Aurora,
Candy,
Solar,
Nocturne,
Arcade,
Parchment,
Bubble,
Garnet,
Origami,
Sundown,
Pride,
}
impl Design {
pub const ALL: &'static [Self] = &[
Self::Future,
Self::Clarity,
Self::Helix,
Self::Forest,
Self::Editorial,
Self::Graphite,
Self::Ocean,
Self::Ember,
Self::Blueprint,
Self::Bloom,
Self::Aurora,
Self::Candy,
Self::Solar,
Self::Nocturne,
Self::Arcade,
Self::Parchment,
Self::Bubble,
Self::Garnet,
Self::Origami,
Self::Sundown,
Self::Pride,
];
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Future => "future",
Self::Clarity => "clarity",
Self::Helix => "helix",
Self::Forest => "forest",
Self::Editorial => "editorial",
Self::Graphite => "graphite",
Self::Ocean => "ocean",
Self::Ember => "ember",
Self::Blueprint => "blueprint",
Self::Bloom => "bloom",
Self::Aurora => "aurora",
Self::Candy => "candy",
Self::Solar => "solar",
Self::Nocturne => "nocturne",
Self::Arcade => "arcade",
Self::Parchment => "parchment",
Self::Bubble => "bubble",
Self::Garnet => "garnet",
Self::Origami => "origami",
Self::Sundown => "sundown",
Self::Pride => "pride",
}
}
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Self::Future => "Future",
Self::Clarity => "Clarity",
Self::Helix => "Helix",
Self::Forest => "Forest",
Self::Editorial => "Editorial",
Self::Graphite => "Graphite",
Self::Ocean => "Ocean",
Self::Ember => "Ember",
Self::Blueprint => "Blueprint",
Self::Bloom => "Bloom",
Self::Aurora => "Aurora",
Self::Candy => "Candy",
Self::Solar => "Solar",
Self::Nocturne => "Nocturne",
Self::Arcade => "Arcade",
Self::Parchment => "Parchment",
Self::Bubble => "Bubble",
Self::Garnet => "Garnet",
Self::Origami => "Origami",
Self::Sundown => "Sundown",
Self::Pride => "Pride",
}
}
#[must_use]
pub const fn description(self) -> &'static str {
match self {
Self::Future => "Balanced corners, quiet surfaces and soft elevation.",
Self::Clarity => "Crisp edges, flat surfaces and outlined actions.",
Self::Helix => "Charcoal chrome, mint actions and violet feature surfaces.",
Self::Forest => "Organic curves, green accents and calm surfaces.",
Self::Editorial => "Serif reading, restrained corners and ink-like controls.",
Self::Graphite => "Monochrome surfaces with distinct, structured edges.",
Self::Ocean => "Cool teal, rounded panels and a tinted header.",
Self::Ember => "Warm accents, substantial elevation and squared panels.",
Self::Blueprint => "Square geometry, blue accents and precise outlines.",
Self::Bloom => "Playful curves, rose accents and floating colored panels.",
Self::Aurora => "Violet ribbons, luminous curves and floating surfaces.",
Self::Candy => "Rose candy stripes, pillowy panels and playful elevation.",
Self::Solar => "Amber sunshine, square panels and decisive offset shadows.",
Self::Nocturne => "Ink chrome, violet spotlights and elegant serif reading.",
Self::Arcade => "Green scanlines, square geometry and terminal-inspired type.",
Self::Parchment => "Warm ruled paper, serif type and quiet outlined controls.",
Self::Bubble => "Teal bubbles, generous curves and buoyant layered shadows.",
Self::Garnet => "Rose jewel facets, crisp serif panels and charcoal chrome.",
Self::Origami => "Blue folded ribbons, angular outlines and paper shadows.",
Self::Sundown => "Ember horizons, rounded reading panels and long soft shadows.",
Self::Pride => "Rainbow ribbons, joyful rounded panels and clear violet actions.",
}
}
#[must_use]
#[allow(clippy::match_same_arms)]
pub const fn palette(self) -> Palette {
match self {
Self::Future => Palette::Default,
Self::Clarity => Palette::Default,
Self::Helix => Palette::Helix,
Self::Forest => Palette::Forest,
Self::Editorial => Palette::Ink,
Self::Graphite => Palette::Ink,
Self::Ocean => Palette::Teal,
Self::Ember => Palette::Ember,
Self::Blueprint => Palette::Blue,
Self::Bloom => Palette::Rose,
Self::Aurora => Palette::Violet,
Self::Candy => Palette::Rose,
Self::Solar => Palette::Amber,
Self::Nocturne => Palette::Violet,
Self::Arcade => Palette::Forest,
Self::Parchment => Palette::Amber,
Self::Bubble => Palette::Teal,
Self::Garnet => Palette::Rose,
Self::Origami => Palette::Blue,
Self::Sundown => Palette::Ember,
Self::Pride => Palette::Violet,
}
}
#[must_use]
pub fn from_attribute(value: &str) -> Option<Self> {
Self::ALL
.iter()
.copied()
.find(|design| design.as_str() == value)
}
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Palette {
#[default]
Default,
Blue,
Violet,
Forest,
Ink,
Teal,
Ember,
Rose,
Amber,
Helix,
}
impl Palette {
pub const ALL: &'static [Self] = &[
Self::Default,
Self::Blue,
Self::Violet,
Self::Forest,
Self::Ink,
Self::Teal,
Self::Ember,
Self::Rose,
Self::Amber,
Self::Helix,
];
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Default => "default",
Self::Blue => "blue",
Self::Violet => "violet",
Self::Forest => "forest",
Self::Ink => "ink",
Self::Teal => "teal",
Self::Ember => "ember",
Self::Rose => "rose",
Self::Amber => "amber",
Self::Helix => "helix",
}
}
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Self::Default => "Future blue",
Self::Blue => "Blueprint blue",
Self::Violet => "Violet",
Self::Forest => "Forest",
Self::Ink => "Ink",
Self::Teal => "Ocean teal",
Self::Ember => "Ember",
Self::Rose => "Rose",
Self::Amber => "Amber",
Self::Helix => "Helix violet & mint",
}
}
#[must_use]
pub fn from_attribute(value: &str) -> Option<Self> {
Self::ALL
.iter()
.copied()
.find(|palette| palette.as_str() == value)
}
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Theme {
#[default]
System,
Light,
Dark,
HighContrastLight,
HighContrastDark,
FollowSystem,
}
impl Theme {
#[must_use]
pub const fn attribute(self) -> Option<&'static str> {
match self {
Self::System => None,
Self::Light => Some("light"),
Self::Dark => Some("dark"),
Self::HighContrastLight => Some("high-contrast-light"),
Self::HighContrastDark => Some("high-contrast-dark"),
Self::FollowSystem => Some("system"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Brand {
Forest,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Direction {
#[default]
Ltr,
Rtl,
}
impl Direction {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Ltr => "ltr",
Self::Rtl => "rtl",
}
}
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Stylesheets {
#[default]
Automatic,
External,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct DirectionContext(Direction);
#[must_use]
pub fn use_direction() -> Direction {
try_consume_context::<Signal<DirectionContext>>()
.map_or(Direction::Ltr, |direction| direction.read().0)
}
pub(crate) fn use_stylesheets() -> bool {
try_consume_context::<ReadSignal<Stylesheets>>()
.is_none_or(|policy| *policy.read() == Stylesheets::Automatic)
}
#[derive(Props, Clone, PartialEq)]
pub struct FutureCssProps {
#[props(default)]
pub design: Option<Design>,
#[props(default)]
pub palette: Option<Palette>,
#[props(default)]
pub theme: Theme,
#[props(default)]
pub brand: Option<Brand>,
#[props(default)]
pub direction: Option<Direction>,
#[props(default)]
pub stylesheets: Stylesheets,
pub children: Element,
}
static NEXT_ROOT: AtomicU64 = AtomicU64::new(0);
#[component]
pub fn FutureCss(props: FutureCssProps) -> Element {
let outermost = use_hook(|| try_consume_context::<Signal<DirectionContext>>().is_none());
use_context_provider(IdAllocator::new);
let inherited = use_direction();
let effective = DirectionContext(props.direction.unwrap_or(inherited));
let mut direction = use_context_provider(|| Signal::new(effective));
if *direction.peek() != effective {
direction.set(effective);
}
let mut policy = use_signal(|| props.stylesheets);
if *policy.peek() != props.stylesheets {
policy.set(props.stylesheets);
}
use_context_provider(|| ReadSignal::from(policy));
let capabilities = use_signal(Capabilities::default);
use_context_provider(|| capabilities);
let mut capabilities = capabilities;
let pairs = theme_pairs(
props.theme.attribute(),
props.brand.map(|brand| match brand {
Brand::Forest => "forest",
}),
props.design.map(Design::as_str),
props
.palette
.or(props.design.map(Design::palette))
.map(Palette::as_str),
);
let attrs = theme_attributes(&pairs);
let token = use_hook(|| NEXT_ROOT.fetch_add(1, Ordering::Relaxed));
let generation = use_hook(|| Rc::new(Cell::new(0_u64)));
let next = generation.clone();
use_effect(use_reactive!(|pairs| {
if !outermost {
return;
}
next.set(next.get() + 1);
let generation = next.get();
spawn(async move {
let _ = DomBridge
.mirror_document_theme(token, generation, Some(&pairs))
.await;
});
}));
use_drop(move || {
if outermost {
DomBridge.release_document_theme(token, generation.get() + 1);
}
});
rsx! {
if props.stylesheets == Stylesheets::Automatic {
document::Stylesheet { href: assets::LAYERS_CSS }
document::Stylesheet { href: assets::TOKENS_CSS }
document::Stylesheet { href: assets::RESET_CSS }
document::Stylesheet { href: assets::BASE_CSS }
document::Stylesheet { href: assets::LAYOUT_CSS }
document::Stylesheet { href: assets::TYPE_CSS }
document::Stylesheet { href: assets::UTILITIES_CSS }
document::Stylesheet { href: assets::THEMES_CSS }
}
div {
dir: props.direction.map(Direction::as_str),
onmounted: move |_| async move { capabilities.set(DomBridge.capabilities().await); },
..attrs,
{props.children}
}
}
}