use core::{any::TypeId, marker::PhantomData};
use nami::{Computed, SignalExt, impl_constant, signal::IntoSignal};
use waterui_core::{Environment, env::Store, plugin::Plugin};
use crate::{
color::ResolvedColor,
text::font::{Body, Caption, Footnote, Headline, ResolvedFont, Subheadline, Title},
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum ColorScheme {
#[default]
Light,
Dark,
}
impl_constant!(ColorScheme);
#[derive(Default, Debug)]
pub struct ColorSettings {
background: Option<Computed<ResolvedColor>>,
surface: Option<Computed<ResolvedColor>>,
surface_variant: Option<Computed<ResolvedColor>>,
border: Option<Computed<ResolvedColor>>,
foreground: Option<Computed<ResolvedColor>>,
muted_foreground: Option<Computed<ResolvedColor>>,
accent: Option<Computed<ResolvedColor>>,
accent_container: Option<Computed<ResolvedColor>>,
accent_foreground: Option<Computed<ResolvedColor>>,
tertiary: Option<Computed<ResolvedColor>>,
tertiary_container: Option<Computed<ResolvedColor>>,
selection_container: Option<Computed<ResolvedColor>>,
selection_foreground: Option<Computed<ResolvedColor>>,
}
impl ColorSettings {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn background(mut self, color: impl IntoSignal<ResolvedColor>) -> Self {
self.background = Some(color.into_signal().computed());
self
}
#[must_use]
pub fn surface(mut self, color: impl IntoSignal<ResolvedColor>) -> Self {
self.surface = Some(color.into_signal().computed());
self
}
#[must_use]
pub fn surface_variant(mut self, color: impl IntoSignal<ResolvedColor>) -> Self {
self.surface_variant = Some(color.into_signal().computed());
self
}
#[must_use]
pub fn border(mut self, color: impl IntoSignal<ResolvedColor>) -> Self {
self.border = Some(color.into_signal().computed());
self
}
#[must_use]
pub fn foreground(mut self, color: impl IntoSignal<ResolvedColor>) -> Self {
self.foreground = Some(color.into_signal().computed());
self
}
#[must_use]
pub fn muted_foreground(mut self, color: impl IntoSignal<ResolvedColor>) -> Self {
self.muted_foreground = Some(color.into_signal().computed());
self
}
#[must_use]
pub fn accent(mut self, color: impl IntoSignal<ResolvedColor>) -> Self {
self.accent = Some(color.into_signal().computed());
self
}
#[must_use]
pub fn accent_container(mut self, color: impl IntoSignal<ResolvedColor>) -> Self {
self.accent_container = Some(color.into_signal().computed());
self
}
#[must_use]
pub fn accent_foreground(mut self, color: impl IntoSignal<ResolvedColor>) -> Self {
self.accent_foreground = Some(color.into_signal().computed());
self
}
#[must_use]
pub fn tertiary(mut self, color: impl IntoSignal<ResolvedColor>) -> Self {
self.tertiary = Some(color.into_signal().computed());
self
}
#[must_use]
pub fn tertiary_container(mut self, color: impl IntoSignal<ResolvedColor>) -> Self {
self.tertiary_container = Some(color.into_signal().computed());
self
}
#[must_use]
pub fn selection_container(mut self, color: impl IntoSignal<ResolvedColor>) -> Self {
self.selection_container = Some(color.into_signal().computed());
self
}
#[must_use]
pub fn selection_foreground(mut self, color: impl IntoSignal<ResolvedColor>) -> Self {
self.selection_foreground = Some(color.into_signal().computed());
self
}
fn install(self, env: &mut Environment) {
if let Some(signal) = self.background {
install_color_signal::<color::Background>(env, signal);
}
if let Some(signal) = self.surface {
install_color_signal::<color::Surface>(env, signal);
}
if let Some(signal) = self.surface_variant {
install_color_signal::<color::SurfaceVariant>(env, signal);
}
if let Some(signal) = self.border {
install_color_signal::<color::Border>(env, signal);
}
if let Some(signal) = self.foreground {
install_color_signal::<color::Foreground>(env, signal);
}
if let Some(signal) = self.muted_foreground {
install_color_signal::<color::MutedForeground>(env, signal);
}
if let Some(signal) = self.accent {
install_color_signal::<color::Accent>(env, signal);
}
if let Some(signal) = self.accent_container {
install_color_signal::<color::AccentContainer>(env, signal);
}
if let Some(signal) = self.accent_foreground {
install_color_signal::<color::AccentForeground>(env, signal);
}
if let Some(signal) = self.tertiary {
install_color_signal::<color::Tertiary>(env, signal);
}
if let Some(signal) = self.tertiary_container {
install_color_signal::<color::TertiaryContainer>(env, signal);
}
if let Some(signal) = self.selection_container {
install_color_signal::<color::SelectionContainer>(env, signal);
}
if let Some(signal) = self.selection_foreground {
install_color_signal::<color::SelectionForeground>(env, signal);
}
}
}
#[derive(Default, Debug)]
pub struct FontSettings {
body: Option<Computed<ResolvedFont>>,
title: Option<Computed<ResolvedFont>>,
headline: Option<Computed<ResolvedFont>>,
subheadline: Option<Computed<ResolvedFont>>,
caption: Option<Computed<ResolvedFont>>,
footnote: Option<Computed<ResolvedFont>>,
}
impl FontSettings {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn body(mut self, font: impl IntoSignal<ResolvedFont>) -> Self {
self.body = Some(font.into_signal().computed());
self
}
#[must_use]
pub fn title(mut self, font: impl IntoSignal<ResolvedFont>) -> Self {
self.title = Some(font.into_signal().computed());
self
}
#[must_use]
pub fn headline(mut self, font: impl IntoSignal<ResolvedFont>) -> Self {
self.headline = Some(font.into_signal().computed());
self
}
#[must_use]
pub fn subheadline(mut self, font: impl IntoSignal<ResolvedFont>) -> Self {
self.subheadline = Some(font.into_signal().computed());
self
}
#[must_use]
pub fn caption(mut self, font: impl IntoSignal<ResolvedFont>) -> Self {
self.caption = Some(font.into_signal().computed());
self
}
#[must_use]
pub fn footnote(mut self, font: impl IntoSignal<ResolvedFont>) -> Self {
self.footnote = Some(font.into_signal().computed());
self
}
fn install(self, env: &mut Environment) {
if let Some(signal) = self.body {
install_font_signal::<Body>(env, signal);
}
if let Some(signal) = self.title {
install_font_signal::<Title>(env, signal);
}
if let Some(signal) = self.headline {
install_font_signal::<Headline>(env, signal);
}
if let Some(signal) = self.subheadline {
install_font_signal::<Subheadline>(env, signal);
}
if let Some(signal) = self.caption {
install_font_signal::<Caption>(env, signal);
}
if let Some(signal) = self.footnote {
install_font_signal::<Footnote>(env, signal);
}
}
}
#[derive(Default, Debug)]
pub struct Theme {
color_scheme: Option<Computed<ColorScheme>>,
colors: Option<ColorSettings>,
fonts: Option<FontSettings>,
}
impl Theme {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn color_scheme(mut self, scheme: impl IntoSignal<ColorScheme>) -> Self {
self.color_scheme = Some(scheme.into_signal().computed());
self
}
#[must_use]
pub fn colors(mut self, colors: ColorSettings) -> Self {
self.colors = Some(colors);
self
}
#[must_use]
pub fn fonts(mut self, fonts: FontSettings) -> Self {
self.fonts = Some(fonts);
self
}
}
impl Plugin for Theme {
fn install(self, env: &mut Environment) {
if let Some(scheme) = self.color_scheme {
env.insert(ColorSchemeSignal(scheme));
}
if let Some(colors) = self.colors {
colors.install(env);
}
if let Some(fonts) = self.fonts {
fonts.install(env);
}
}
}
pub mod color {
use super::{Environment, ResolvedColor};
use nami::{Signal, impl_constant};
use waterui_core::resolve::Resolvable;
macro_rules! define_color_token {
($name:ident, $doc:literal) => {
#[doc = $doc]
#[derive(Debug, Clone, Copy, Default)]
pub struct $name;
impl Resolvable for $name {
type Resolved = ResolvedColor;
fn resolve(&self, env: &Environment) -> impl Signal<Output = Self::Resolved> {
super::resolve_color_slot::<Self>(env)
}
}
impl_constant!($name);
impl crate::View for $name {
fn body(self, _env: &Environment) -> impl crate::View {
crate::color::Color::new(self)
}
}
};
}
define_color_token!(Background, "Primary background color.");
define_color_token!(Surface, "Elevated surface color (cards, sheets).");
define_color_token!(SurfaceVariant, "Alternate surface color.");
define_color_token!(Border, "Border and divider color.");
define_color_token!(Foreground, "Primary text and icon color.");
define_color_token!(MutedForeground, "Secondary/dimmed text color.");
define_color_token!(Accent, "Accent color for interactive elements.");
define_color_token!(
AccentContainer,
"Container color associated with the accent."
);
define_color_token!(AccentForeground, "Foreground on accent backgrounds.");
define_color_token!(
Tertiary,
"Contrasting accent color for complementary emphasis."
);
define_color_token!(
TertiaryContainer,
"Container color associated with the tertiary accent."
);
define_color_token!(
SelectionContainer,
"The fill a platform paints behind a selected item."
);
define_color_token!(
SelectionForeground,
"Foreground drawn on the selection container."
);
}
#[derive(Clone)]
struct ColorSchemeSignal(Computed<ColorScheme>);
#[derive(Clone)]
struct ColorSlotValue<T> {
signal: Computed<ResolvedColor>,
_marker: PhantomData<T>,
}
impl<T> ColorSlotValue<T> {
const fn new(signal: Computed<ResolvedColor>) -> Self {
Self {
signal,
_marker: PhantomData,
}
}
}
fn resolve_color_slot<T: 'static>(env: &Environment) -> Computed<ResolvedColor> {
env.get::<ColorSlotValue<T>>()
.unwrap_or_else(|| {
panic!(
"WaterUI color token `{}` is not installed in the environment",
core::any::type_name::<T>()
)
})
.signal
.clone()
}
#[must_use]
pub fn current_color_scheme(env: &Environment) -> Computed<ColorScheme> {
installed_color_scheme(env).expect("WaterUI color scheme is not installed in the environment")
}
#[must_use]
pub fn installed_color_scheme(env: &Environment) -> Option<Computed<ColorScheme>> {
env.get::<ColorSchemeSignal>()
.map(|signal| signal.0.clone())
}
pub fn install_color_signal<T: 'static>(env: &mut Environment, signal: Computed<ResolvedColor>) {
env.insert(ColorSlotValue::<T>::new(signal.clone()));
macro_rules! mirror_graphics_color {
($theme_slot:ty, $graphics_slot:ty) => {
if TypeId::of::<T>() == TypeId::of::<$theme_slot>() {
env.insert(Store::<$graphics_slot, Computed<ResolvedColor>>::new(
signal,
));
return;
}
};
}
mirror_graphics_color!(color::Background, waterui_graphics::color::BackgroundColor);
mirror_graphics_color!(color::Surface, waterui_graphics::color::SurfaceColor);
mirror_graphics_color!(
color::SurfaceVariant,
waterui_graphics::color::SurfaceVariantColor
);
mirror_graphics_color!(color::Border, waterui_graphics::color::BorderColor);
mirror_graphics_color!(color::Foreground, waterui_graphics::color::ForegroundColor);
mirror_graphics_color!(
color::MutedForeground,
waterui_graphics::color::MutedForegroundColor
);
mirror_graphics_color!(color::Accent, waterui_graphics::color::AccentColor);
mirror_graphics_color!(
color::AccentContainer,
waterui_graphics::color::AccentContainerColor
);
mirror_graphics_color!(
color::AccentForeground,
waterui_graphics::color::AccentForegroundColor
);
mirror_graphics_color!(color::Tertiary, waterui_graphics::color::TertiaryColor);
mirror_graphics_color!(
color::TertiaryContainer,
waterui_graphics::color::TertiaryContainerColor
);
mirror_graphics_color!(
color::SelectionContainer,
waterui_graphics::color::SelectionContainerColor
);
mirror_graphics_color!(
color::SelectionForeground,
waterui_graphics::color::SelectionForegroundColor
);
}
#[must_use]
pub fn installed_color_signal<T: 'static>(env: &Environment) -> Option<Computed<ResolvedColor>> {
env.get::<ColorSlotValue<T>>()
.map(|value| value.signal.clone())
}
pub fn install_font_signal<T: 'static>(env: &mut Environment, signal: Computed<ResolvedFont>) {
env.insert(Store::<T, Computed<ResolvedFont>>::new(signal));
}
pub fn install_color_scheme(env: &mut Environment, signal: Computed<ColorScheme>) {
env.insert(ColorSchemeSignal(signal));
}
use waterui_graphics::color::Color;
#[derive(Debug)]
pub struct ForegroundOverride {
color: Color,
}
impl ForegroundOverride {
pub fn new(color: impl Into<Color>) -> Self {
Self {
color: color.into(),
}
}
}
impl Plugin for ForegroundOverride {
fn install(self, env: &mut Environment) {
let resolved = self.color.resolve(env);
install_color_signal::<color::Foreground>(env, resolved);
}
}
#[cfg(test)]
mod tests {
use alloc::rc::Rc;
use core::cell::Cell;
use nami::{Binding, Signal, SignalExt};
use waterui_graphics::color::{AccentColor, Color};
use super::*;
#[test]
fn graphics_accent_color_tracks_installed_theme_signal() {
let accent = Binding::container(ResolvedColor::default());
let mut env = Environment::new();
install_color_signal::<color::Accent>(&mut env, accent.computed());
let resolved = Color::new(AccentColor).resolve(&env);
let observed_red = Rc::new(Cell::new(0.0));
let captured_red = Rc::clone(&observed_red);
let _guard = resolved.watch(move |context| {
captured_red.set(context.into_value().red);
});
accent.set(ResolvedColor {
red: 1.0,
..ResolvedColor::default()
});
assert!((observed_red.get() - 1.0).abs() < f32::EPSILON);
assert!((resolved.get().red - 1.0).abs() < f32::EPSILON);
}
}