use std::borrow::Cow;
use serde::{Deserialize, Serialize};
use teksilo_tokens::{
ColorTokens, InputTokens, LayoutTokens, MotionTokens, ShapeTokens, TargetDensity,
TypographyTokens,
};
use crate::styles::component_style_slots::ComponentStyleSlots;
use crate::styles::theme_appearance::ThemeAppearance;
use crate::styles::theme_extension::ThemeExtensions;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct ThemeId(Cow<'static, str>);
impl ThemeId {
pub fn new(id: impl Into<Cow<'static, str>>) -> Self {
Self(id.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl Default for ThemeId {
fn default() -> Self {
Self(Cow::Borrowed("custom"))
}
}
impl std::fmt::Display for ThemeId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Theme {
#[serde(default)]
pub id: ThemeId,
pub appearance: ThemeAppearance,
pub colors: ColorTokens,
pub layout: LayoutTokens,
pub typography: TypographyTokens,
pub shape: ShapeTokens,
pub motion: MotionTokens,
#[serde(default)]
pub input: InputTokens,
#[serde(skip, default)]
pub style_slots: ComponentStyleSlots,
#[serde(skip, default)]
pub extensions: ThemeExtensions,
}
#[derive(Clone, Copy)]
pub struct DensityProjection(pub fn(&Theme, TargetDensity) -> Theme);
impl std::fmt::Debug for DensityProjection {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("DensityProjection(<fn>)")
}
}
impl Theme {
pub fn new(
appearance: ThemeAppearance,
colors: ColorTokens,
layout: LayoutTokens,
typography: TypographyTokens,
shape: ShapeTokens,
motion: MotionTokens,
input: InputTokens,
) -> Self {
Self {
id: ThemeId::default(),
appearance,
colors,
layout,
typography,
shape,
motion,
input,
style_slots: ComponentStyleSlots::default(),
extensions: ThemeExtensions::new(),
}
}
pub fn with_density_projection(self, project: fn(&Theme, TargetDensity) -> Theme) -> Self {
self.with_extension(DensityProjection(project))
}
pub fn with_id(mut self, id: impl Into<Cow<'static, str>>) -> Self {
self.id = ThemeId::new(id);
self
}
pub fn is_dark(&self) -> bool {
self.appearance.is_dark()
}
pub fn for_inactive_window(&self) -> Theme {
Theme {
colors: self.colors.for_inactive_window(),
..self.clone()
}
}
pub fn for_high_contrast(&self) -> Theme {
Theme {
colors: self.colors.for_high_contrast(),
..self.clone()
}
}
pub fn with_density(&self, density: TargetDensity) -> Theme {
if let Some(DensityProjection(project)) = self.extension::<DensityProjection>().copied() {
return project(self, density);
}
Theme {
input: InputTokens::for_density(density),
..self.clone()
}
}
pub fn extension<T: std::any::Any + Send + Sync>(&self) -> Option<&T> {
self.extensions.get::<T>()
}
pub fn with_extension<T: std::any::Any + Send + Sync>(mut self, value: T) -> Self {
self.extensions.insert(value);
self
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::presets::intui;
use teksilo_tokens::TargetDensity;
#[test]
fn a_theme_without_an_input_key_still_deserializes() {
let mut value: serde_json::Value =
serde_json::to_value(intui::light()).expect("theme serializes");
assert!(
value
.as_object_mut()
.expect("theme is a JSON object")
.remove("input")
.is_some(),
"the fixture must actually have had an `input` key to remove"
);
let restored: Theme = serde_json::from_value(value).expect("pre-programme theme loads");
assert_eq!(restored.input, teksilo_tokens::InputTokens::default());
assert_eq!(restored.input.density, TargetDensity::Compact);
assert_eq!(restored.colors, intui::light().colors);
}
#[test]
fn with_density_projects_only_the_input_tokens() {
let base = intui::light();
let touch = base.with_density(TargetDensity::Touch);
assert_eq!(touch.input.density, TargetDensity::Touch);
assert_eq!(touch.input.target_size, 44.0);
assert_eq!(touch.id, base.id);
assert_eq!(touch.appearance, base.appearance);
assert_eq!(touch.colors, base.colors);
assert_eq!(touch.typography, base.typography);
assert_eq!(touch.shape, base.shape);
assert_eq!(touch.motion, base.motion);
}
#[test]
fn presets_start_compact() {
assert_eq!(intui::light().input, teksilo_tokens::InputTokens::default());
assert_eq!(intui::dark().input.density, TargetDensity::Compact);
}
#[test]
fn with_density_preserves_installed_style_slots() {
#[derive(Debug)]
struct TestButtonStyle;
impl crate::styles::ButtonStyle for TestButtonStyle {
fn make_body(
&self,
_cfg: &crate::styles::ButtonStyleConfig,
ctx: &mut crate::BuildContext,
) -> crate::WidgetId {
ctx.add(crate::test_widgets::FillWidget::new())
}
}
let mut base = intui::light();
base.style_slots.button = Some(std::rc::Rc::new(TestButtonStyle));
let touch = base.with_density(TargetDensity::Touch);
assert!(touch.style_slots.button.is_some());
assert_eq!(touch.input.density, TargetDensity::Touch);
}
}