merman_render/presentation/
theme.rs1use 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}