Skip to main content

merman_render/presentation/
theme.rs

1use std::collections::BTreeMap;
2
3use merman_core::MermaidConfig;
4
5use super::{HostThemePreset, PresentationError, compiler, css_declaration_value, css_font_family};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
8#[non_exhaustive]
9pub enum HostThemeAppearance {
10    #[default]
11    Light,
12    Dark,
13}
14
15impl HostThemeAppearance {
16    pub const fn as_str(self) -> &'static str {
17        match self {
18            Self::Light => "light",
19            Self::Dark => "dark",
20        }
21    }
22
23    pub const fn is_dark(self) -> bool {
24        matches!(self, Self::Dark)
25    }
26}
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
29#[non_exhaustive]
30pub enum ThemeRole {
31    Canvas,
32    Surface,
33    SurfaceAlt,
34    SurfaceMuted,
35    Text,
36    SubtleText,
37    Border,
38    Line,
39    EdgeLabelBackground,
40    ClusterBackground,
41    ClusterBorder,
42    NoteBackground,
43    NoteBorder,
44    NoteText,
45    ActorBackground,
46    ActorBorder,
47    ActorText,
48    ActivationBackground,
49    ActivationBorder,
50    Error,
51    Warning,
52    Success,
53}
54
55impl ThemeRole {
56    pub const ALL: [Self; 22] = [
57        Self::Canvas,
58        Self::Surface,
59        Self::SurfaceAlt,
60        Self::SurfaceMuted,
61        Self::Text,
62        Self::SubtleText,
63        Self::Border,
64        Self::Line,
65        Self::EdgeLabelBackground,
66        Self::ClusterBackground,
67        Self::ClusterBorder,
68        Self::NoteBackground,
69        Self::NoteBorder,
70        Self::NoteText,
71        Self::ActorBackground,
72        Self::ActorBorder,
73        Self::ActorText,
74        Self::ActivationBackground,
75        Self::ActivationBorder,
76        Self::Error,
77        Self::Warning,
78        Self::Success,
79    ];
80
81    pub const fn id(self) -> &'static str {
82        match self {
83            Self::Canvas => "canvas",
84            Self::Surface => "surface",
85            Self::SurfaceAlt => "surface-alt",
86            Self::SurfaceMuted => "surface-muted",
87            Self::Text => "text",
88            Self::SubtleText => "subtle-text",
89            Self::Border => "border",
90            Self::Line => "line",
91            Self::EdgeLabelBackground => "edge-label-background",
92            Self::ClusterBackground => "cluster-background",
93            Self::ClusterBorder => "cluster-border",
94            Self::NoteBackground => "note-background",
95            Self::NoteBorder => "note-border",
96            Self::NoteText => "note-text",
97            Self::ActorBackground => "actor-background",
98            Self::ActorBorder => "actor-border",
99            Self::ActorText => "actor-text",
100            Self::ActivationBackground => "activation-background",
101            Self::ActivationBorder => "activation-border",
102            Self::Error => "error",
103            Self::Warning => "warning",
104            Self::Success => "success",
105        }
106    }
107
108    pub fn from_id(id: &str) -> Result<Self, PresentationError> {
109        Self::ALL
110            .into_iter()
111            .find(|role| role.id() == id)
112            .ok_or_else(|| PresentationError::UnknownThemeRole(id.to_string()))
113    }
114}
115
116#[derive(Debug, Clone, Default, PartialEq, Eq)]
117pub struct HostTheme {
118    appearance: Option<HostThemeAppearance>,
119    font_family: Option<String>,
120    font_size: Option<String>,
121    roles: BTreeMap<ThemeRole, String>,
122    series_palette: Vec<String>,
123}
124
125impl HostTheme {
126    pub fn new() -> Self {
127        Self::default()
128    }
129
130    pub fn from_preset(preset: HostThemePreset) -> Self {
131        preset.theme()
132    }
133
134    pub const fn appearance(&self) -> Option<HostThemeAppearance> {
135        self.appearance
136    }
137
138    pub fn font_family(&self) -> Option<&str> {
139        self.font_family.as_deref()
140    }
141
142    pub fn font_size(&self) -> Option<&str> {
143        self.font_size.as_deref()
144    }
145
146    pub fn role(&self, role: ThemeRole) -> Option<&str> {
147        self.roles.get(&role).map(String::as_str)
148    }
149
150    pub fn roles(&self) -> impl ExactSizeIterator<Item = (ThemeRole, &str)> {
151        self.roles
152            .iter()
153            .map(|(role, value)| (*role, value.as_str()))
154    }
155
156    pub fn series_palette(&self) -> &[String] {
157        &self.series_palette
158    }
159
160    pub fn with_appearance(mut self, appearance: HostThemeAppearance) -> Self {
161        self.appearance = Some(appearance);
162        self
163    }
164
165    pub fn try_with_font_family(
166        mut self,
167        font_family: impl Into<String>,
168    ) -> Result<Self, PresentationError> {
169        self.font_family = Some(css_font_family(font_family, "theme.font_family")?);
170        Ok(self)
171    }
172
173    pub fn try_with_font_size(
174        mut self,
175        font_size: impl Into<String>,
176    ) -> Result<Self, PresentationError> {
177        self.font_size = Some(css_declaration_value(font_size, "theme.font_size")?);
178        Ok(self)
179    }
180
181    pub fn try_with_role(
182        mut self,
183        role: ThemeRole,
184        value: impl Into<String>,
185    ) -> Result<Self, PresentationError> {
186        self.roles.insert(
187            role,
188            css_declaration_value(value, &format!("theme.roles.{}", role.id()))?,
189        );
190        Ok(self)
191    }
192
193    pub fn try_with_series_palette(
194        mut self,
195        palette: impl IntoIterator<Item = impl Into<String>>,
196    ) -> Result<Self, PresentationError> {
197        self.series_palette = palette
198            .into_iter()
199            .enumerate()
200            .map(|(index, value)| {
201                css_declaration_value(value, &format!("theme.series_palette[{index}]"))
202            })
203            .collect::<Result<Vec<_>, _>>()?;
204        Ok(self)
205    }
206
207    pub(crate) fn mermaid_config_patch(&self) -> MermaidConfig {
208        compiler::compile(self)
209    }
210
211    pub(super) fn bundled(
212        appearance: HostThemeAppearance,
213        roles: &[(ThemeRole, &str)],
214        series_palette: &[&str],
215    ) -> Self {
216        Self {
217            appearance: Some(appearance),
218            font_family: Some(
219                r#"Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif"#
220                    .to_string(),
221            ),
222            font_size: Some("14px".to_string()),
223            roles: roles
224                .iter()
225                .map(|(role, value)| (*role, (*value).to_string()))
226                .collect(),
227            series_palette: series_palette
228                .iter()
229                .map(|value| (*value).to_string())
230                .collect(),
231        }
232    }
233
234    pub(super) fn has_values(&self) -> bool {
235        self.appearance.is_some()
236            || self.font_family.is_some()
237            || self.font_size.is_some()
238            || !self.roles.is_empty()
239            || !self.series_palette.is_empty()
240    }
241}