1use crate::Color;
2
3#[derive(Debug, Clone, Default)]
5pub struct Style {
6 pub fill: Option<Color>,
7 pub stroke: Option<Color>,
8 pub stroke_width: Option<f64>,
9 pub stroke_dasharray: Option<Vec<f64>>,
10 pub opacity: Option<f64>,
11 pub css_classes: Vec<String>,
12}
13
14impl Style {
15 pub fn resolved_stroke(&self, theme: &Theme) -> Color {
17 self.stroke.unwrap_or(theme.edge_stroke)
18 }
19
20 pub fn resolved_stroke_width(&self, theme: &Theme) -> f64 {
22 self.stroke_width.unwrap_or(theme.default_stroke_width)
23 }
24
25 pub fn has_explicit_stroke(&self) -> bool {
27 self.stroke.is_some() || self.stroke_width.is_some()
28 }
29
30 pub fn resolve_stroke_opt(&self, theme: &Theme) -> Option<(Color, f64)> {
33 if self.has_explicit_stroke() {
34 Some((
35 self.resolved_stroke(theme),
36 self.resolved_stroke_width(theme),
37 ))
38 } else {
39 None
40 }
41 }
42}
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
46pub enum FontWeight {
47 #[default]
48 Normal,
49 Bold,
50}
51
52pub use crate::font_fallback::SVG_FONT_FAMILY as DEFAULT_FONT_FAMILY;
54
55#[derive(Debug, Clone)]
57pub struct Theme {
58 pub node_fill: Color,
60 pub node_stroke: Color,
61 pub node_text: Color,
62 pub edge_stroke: Color,
63 pub edge_label_text: Color,
64 pub edge_label_bg: Color,
65 pub start_fill: Color,
66 pub end_inner_fill: Color,
67 pub composite_fill: Color,
68 pub composite_stroke: Color,
69 pub composite_label: Color,
70 pub note_fill: Color,
71 pub note_stroke: Color,
72 pub note_text: Color,
73 pub subgraph_fill: Color,
74 pub subgraph_stroke: Color,
75 pub subgraph_label: Color,
76 pub divider_stroke: Color,
77 pub region_stroke: Color,
78 pub lifeline_stroke: Color,
79 pub activation_fill: Color,
80 pub activation_stroke: Color,
81 pub grid_stroke: Color,
83 pub muted_text: Color,
85 pub face_fill: Color,
87 pub detail_stroke: Color,
89 pub font_size_node: f64,
91 pub font_size_edge_label: f64,
92 pub font_size_label: f64,
93 pub font_size_small: f64,
94 pub font_size_tiny: f64,
95 pub font_size_title: f64,
96 pub default_stroke_width: f64,
98 pub padding: f64,
101 pub background: Color,
103 pub custom_font: Option<Vec<u8>>,
105}
106
107impl Default for Theme {
108 fn default() -> Self {
109 Self::light()
110 }
111}
112
113impl Theme {
114 pub fn light() -> Self {
116 Self {
117 node_fill: Color::rgba(236, 236, 255, 178), node_stroke: Color::rgb(147, 112, 219), node_text: Color::rgb(51, 51, 51), edge_stroke: Color::rgb(51, 51, 51), edge_label_text: Color::rgb(51, 51, 51), edge_label_bg: Color::rgba(245, 243, 255, 191), start_fill: Color::rgb(51, 51, 51), end_inner_fill: Color::rgb(147, 112, 219), composite_fill: Color::rgba(255, 255, 255, 204), composite_stroke: Color::rgb(147, 112, 219), composite_label: Color::rgb(51, 51, 51),
128 note_fill: Color::rgba(255, 248, 200, 178), note_stroke: Color::rgb(170, 170, 51), note_text: Color::rgb(51, 51, 51),
131 subgraph_fill: Color::rgba(236, 242, 220, 153), subgraph_stroke: Color::rgb(168, 174, 142), subgraph_label: Color::rgb(51, 51, 51),
134 divider_stroke: Color::rgb(128, 128, 128), region_stroke: Color::rgb(128, 128, 128), lifeline_stroke: Color::rgb(175, 165, 200), activation_fill: Color::rgba(200, 190, 230, 180), activation_stroke: Color::rgb(153, 153, 153), grid_stroke: Color::rgb(200, 200, 200), muted_text: Color::rgb(120, 120, 120), face_fill: Color::rgb(255, 248, 220), detail_stroke: Color::rgb(80, 80, 80), font_size_node: 14.0,
144 font_size_edge_label: 12.0,
145 font_size_label: 13.0,
146 font_size_small: 11.0,
147 font_size_tiny: 9.0,
148 font_size_title: 16.0,
149 default_stroke_width: 1.5,
150 padding: 20.0,
151 background: Color::WHITE,
152 custom_font: None,
153 }
154 }
155
156 pub fn dark() -> Self {
158 Self {
159 node_fill: Color::rgb(45, 45, 68), node_stroke: Color::rgb(124, 111, 189), node_text: Color::rgb(205, 214, 244), edge_stroke: Color::rgb(166, 173, 200), edge_label_text: Color::rgb(186, 194, 222), edge_label_bg: Color::rgba(30, 30, 46, 204), start_fill: Color::rgb(205, 214, 244), end_inner_fill: Color::rgb(124, 111, 189), composite_fill: Color::rgb(37, 37, 56), composite_stroke: Color::rgb(124, 111, 189),
169 composite_label: Color::rgb(186, 194, 222),
170 note_fill: Color::rgb(62, 60, 40), note_stroke: Color::rgb(170, 170, 51),
172 note_text: Color::rgb(205, 214, 244),
173 subgraph_fill: Color::rgb(40, 43, 35), subgraph_stroke: Color::rgb(105, 112, 85), subgraph_label: Color::rgb(205, 214, 244),
176 divider_stroke: Color::rgb(88, 91, 112),
177 region_stroke: Color::rgb(88, 91, 112),
178 lifeline_stroke: Color::rgb(100, 95, 130), activation_fill: Color::rgba(60, 55, 85, 180), activation_stroke: Color::rgb(88, 91, 112), grid_stroke: Color::rgb(68, 71, 90), muted_text: Color::rgb(147, 153, 178), face_fill: Color::rgb(62, 60, 40), detail_stroke: Color::rgb(166, 173, 200), font_size_node: 14.0,
186 font_size_edge_label: 12.0,
187 font_size_label: 13.0,
188 font_size_small: 11.0,
189 font_size_tiny: 9.0,
190 font_size_title: 16.0,
191 default_stroke_width: 1.5,
192 padding: 20.0,
193 background: Color::rgb(30, 30, 46), custom_font: None,
195 }
196 }
197}
198
199#[derive(Debug, Clone)]
201pub struct TextStyle {
202 pub font_size: f64,
203 pub font_family: String,
204 pub fill: Option<Color>,
205 pub font_weight: FontWeight,
206}
207
208impl Default for TextStyle {
209 fn default() -> Self {
210 Self {
211 font_size: 14.0,
212 font_family: String::from(DEFAULT_FONT_FAMILY),
213 fill: None,
214 font_weight: FontWeight::Normal,
215 }
216 }
217}
218
219#[cfg(test)]
220mod tests {
221 use super::*;
222
223 #[test]
224 fn style_default_is_empty() {
225 let s = Style::default();
226 assert!(s.fill.is_none());
227 assert!(s.stroke.is_none());
228 assert!(s.stroke_width.is_none());
229 assert!(s.stroke_dasharray.is_none());
230 assert!(s.opacity.is_none());
231 assert!(s.css_classes.is_empty());
232 }
233
234 #[test]
235 fn text_style_default() {
236 let ts = TextStyle::default();
237 assert!((ts.font_size - 14.0).abs() < f64::EPSILON);
238 assert_eq!(ts.font_family, DEFAULT_FONT_FAMILY);
239 assert!(ts.font_family.starts_with("'Intel One Mono'"));
240 assert!(ts.font_family.ends_with("monospace"));
241 assert!(ts.fill.is_none());
242 assert_eq!(ts.font_weight, FontWeight::Normal);
243 }
244
245 #[test]
246 fn style_with_dash_array() {
247 let s = Style {
248 stroke_dasharray: Some(vec![5.0, 3.0]),
249 ..Default::default()
250 };
251 assert_eq!(s.stroke_dasharray.as_ref().unwrap(), &[5.0, 3.0]);
252 }
253
254 #[test]
255 fn style_with_css_classes() {
256 let s = Style {
257 css_classes: vec!["node".into(), "highlighted".into()],
258 ..Default::default()
259 };
260 assert_eq!(s.css_classes.len(), 2);
261 assert_eq!(s.css_classes[0], "node");
262 }
263
264 #[test]
265 fn theme_default_is_light() {
266 let t = Theme::default();
267 assert_eq!(t.node_fill, Color::rgba(236, 236, 255, 178));
268 assert_eq!(t.node_stroke, Color::rgb(147, 112, 219));
269 }
270
271 #[test]
272 fn theme_dark_has_dark_fills() {
273 let t = Theme::dark();
274 assert!(t.node_fill.luminance() < 0.1);
275 assert!(t.node_text.luminance() > 0.5);
276 }
277
278 #[test]
279 fn theme_light_typography_and_stroke() {
280 let t = Theme::light();
281 assert!((t.font_size_node - 14.0).abs() < f64::EPSILON);
282 assert!((t.font_size_edge_label - 12.0).abs() < f64::EPSILON);
283 assert!((t.font_size_label - 13.0).abs() < f64::EPSILON);
284 assert!((t.font_size_small - 11.0).abs() < f64::EPSILON);
285 assert!((t.font_size_title - 16.0).abs() < f64::EPSILON);
286 assert!((t.default_stroke_width - 1.5).abs() < f64::EPSILON);
287 }
288
289 #[test]
290 fn theme_light_sequence_colors() {
291 let t = Theme::light();
292 assert_eq!(t.lifeline_stroke, Color::rgb(175, 165, 200));
293 assert_eq!(t.activation_fill, Color::rgba(200, 190, 230, 180));
294 assert_eq!(t.activation_stroke, Color::rgb(153, 153, 153));
295 }
296
297 #[test]
298 fn theme_dark_has_all_new_fields() {
299 let t = Theme::dark();
300 assert!((t.font_size_node - 14.0).abs() < f64::EPSILON);
301 assert!((t.default_stroke_width - 1.5).abs() < f64::EPSILON);
302 assert!(t.lifeline_stroke.luminance() < 0.3);
303 assert!(t.activation_fill.a < 255);
304 }
305
306 #[test]
307 fn text_style_custom() {
308 let ts = TextStyle {
309 font_size: 24.0,
310 font_family: String::from("monospace"),
311 fill: Some(Color::BLACK),
312 font_weight: FontWeight::Bold,
313 };
314 assert!((ts.font_size - 24.0).abs() < f64::EPSILON);
315 assert_eq!(ts.font_family, "monospace");
316 assert_eq!(ts.fill, Some(Color::BLACK));
317 assert_eq!(ts.font_weight, FontWeight::Bold);
318 }
319
320 #[test]
323 fn resolved_stroke_uses_explicit() {
324 let theme = Theme::light();
325 let s = Style {
326 stroke: Some(Color::rgb(255, 0, 0)),
327 ..Default::default()
328 };
329 assert_eq!(s.resolved_stroke(&theme), Color::rgb(255, 0, 0));
330 }
331
332 #[test]
333 fn resolved_stroke_falls_back_to_theme() {
334 let theme = Theme::light();
335 let s = Style::default();
336 assert_eq!(s.resolved_stroke(&theme), theme.edge_stroke);
337 }
338
339 #[test]
340 fn resolved_stroke_width_uses_explicit() {
341 let theme = Theme::light();
342 let s = Style {
343 stroke_width: Some(3.0),
344 ..Default::default()
345 };
346 assert!((s.resolved_stroke_width(&theme) - 3.0).abs() < f64::EPSILON);
347 }
348
349 #[test]
350 fn resolved_stroke_width_falls_back_to_theme() {
351 let theme = Theme::light();
352 let s = Style::default();
353 assert!(
354 (s.resolved_stroke_width(&theme) - theme.default_stroke_width).abs() < f64::EPSILON
355 );
356 }
357
358 #[test]
359 fn has_explicit_stroke_both_none() {
360 assert!(!Style::default().has_explicit_stroke());
361 }
362
363 #[test]
364 fn has_explicit_stroke_color_only() {
365 let s = Style {
366 stroke: Some(Color::BLACK),
367 ..Default::default()
368 };
369 assert!(s.has_explicit_stroke());
370 }
371
372 #[test]
373 fn resolve_stroke_opt_none_when_no_explicit() {
374 let theme = Theme::light();
375 assert!(Style::default().resolve_stroke_opt(&theme).is_none());
376 }
377
378 #[test]
379 fn resolve_stroke_opt_some_with_color_only() {
380 let theme = Theme::light();
381 let s = Style {
382 stroke: Some(Color::rgb(0, 128, 0)),
383 ..Default::default()
384 };
385 let (color, width) = s.resolve_stroke_opt(&theme).unwrap();
386 assert_eq!(color, Color::rgb(0, 128, 0));
387 assert!((width - theme.default_stroke_width).abs() < f64::EPSILON);
388 }
389
390 #[test]
391 fn resolve_stroke_opt_some_with_width_only() {
392 let theme = Theme::light();
393 let s = Style {
394 stroke_width: Some(5.0),
395 ..Default::default()
396 };
397 let (color, width) = s.resolve_stroke_opt(&theme).unwrap();
398 assert_eq!(color, theme.edge_stroke);
399 assert!((width - 5.0).abs() < f64::EPSILON);
400 }
401}