use crate::Color;
#[derive(Debug, Clone, Default)]
pub struct Style {
pub fill: Option<Color>,
pub stroke: Option<Color>,
pub stroke_width: Option<f64>,
pub stroke_dasharray: Option<Vec<f64>>,
pub opacity: Option<f64>,
pub css_classes: Vec<String>,
}
impl Style {
pub fn resolved_stroke(&self, theme: &Theme) -> Color {
self.stroke.unwrap_or(theme.edge_stroke)
}
pub fn resolved_stroke_width(&self, theme: &Theme) -> f64 {
self.stroke_width.unwrap_or(theme.default_stroke_width)
}
pub fn has_explicit_stroke(&self) -> bool {
self.stroke.is_some() || self.stroke_width.is_some()
}
pub fn resolve_stroke_opt(&self, theme: &Theme) -> Option<(Color, f64)> {
if self.has_explicit_stroke() {
Some((
self.resolved_stroke(theme),
self.resolved_stroke_width(theme),
))
} else {
None
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum FontWeight {
#[default]
Normal,
Bold,
}
pub use crate::font_fallback::SVG_FONT_FAMILY as DEFAULT_FONT_FAMILY;
#[derive(Debug, Clone)]
pub struct Theme {
pub node_fill: Color,
pub node_stroke: Color,
pub node_text: Color,
pub edge_stroke: Color,
pub edge_label_text: Color,
pub edge_label_bg: Color,
pub start_fill: Color,
pub end_inner_fill: Color,
pub composite_fill: Color,
pub composite_stroke: Color,
pub composite_label: Color,
pub note_fill: Color,
pub note_stroke: Color,
pub note_text: Color,
pub subgraph_fill: Color,
pub subgraph_stroke: Color,
pub subgraph_label: Color,
pub divider_stroke: Color,
pub region_stroke: Color,
pub lifeline_stroke: Color,
pub activation_fill: Color,
pub activation_stroke: Color,
pub grid_stroke: Color,
pub muted_text: Color,
pub face_fill: Color,
pub detail_stroke: Color,
pub font_size_node: f64,
pub font_size_edge_label: f64,
pub font_size_label: f64,
pub font_size_small: f64,
pub font_size_tiny: f64,
pub font_size_title: f64,
pub default_stroke_width: f64,
pub padding: f64,
pub background: Color,
pub custom_font: Option<Vec<u8>>,
}
impl Default for Theme {
fn default() -> Self {
Self::light()
}
}
impl Theme {
pub fn light() -> Self {
Self {
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),
note_fill: Color::rgba(255, 248, 200, 178), note_stroke: Color::rgb(170, 170, 51), note_text: Color::rgb(51, 51, 51),
subgraph_fill: Color::rgba(236, 242, 220, 153), subgraph_stroke: Color::rgb(168, 174, 142), subgraph_label: Color::rgb(51, 51, 51),
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,
font_size_edge_label: 12.0,
font_size_label: 13.0,
font_size_small: 11.0,
font_size_tiny: 9.0,
font_size_title: 16.0,
default_stroke_width: 1.5,
padding: 20.0,
background: Color::WHITE,
custom_font: None,
}
}
pub fn dark() -> Self {
Self {
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),
composite_label: Color::rgb(186, 194, 222),
note_fill: Color::rgb(62, 60, 40), note_stroke: Color::rgb(170, 170, 51),
note_text: Color::rgb(205, 214, 244),
subgraph_fill: Color::rgb(40, 43, 35), subgraph_stroke: Color::rgb(105, 112, 85), subgraph_label: Color::rgb(205, 214, 244),
divider_stroke: Color::rgb(88, 91, 112),
region_stroke: Color::rgb(88, 91, 112),
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,
font_size_edge_label: 12.0,
font_size_label: 13.0,
font_size_small: 11.0,
font_size_tiny: 9.0,
font_size_title: 16.0,
default_stroke_width: 1.5,
padding: 20.0,
background: Color::rgb(30, 30, 46), custom_font: None,
}
}
}
#[derive(Debug, Clone)]
pub struct TextStyle {
pub font_size: f64,
pub font_family: String,
pub fill: Option<Color>,
pub font_weight: FontWeight,
}
impl Default for TextStyle {
fn default() -> Self {
Self {
font_size: 14.0,
font_family: String::from(DEFAULT_FONT_FAMILY),
fill: None,
font_weight: FontWeight::Normal,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn style_default_is_empty() {
let s = Style::default();
assert!(s.fill.is_none());
assert!(s.stroke.is_none());
assert!(s.stroke_width.is_none());
assert!(s.stroke_dasharray.is_none());
assert!(s.opacity.is_none());
assert!(s.css_classes.is_empty());
}
#[test]
fn text_style_default() {
let ts = TextStyle::default();
assert!((ts.font_size - 14.0).abs() < f64::EPSILON);
assert_eq!(ts.font_family, DEFAULT_FONT_FAMILY);
assert!(ts.font_family.starts_with("'Intel One Mono'"));
assert!(ts.font_family.ends_with("monospace"));
assert!(ts.fill.is_none());
assert_eq!(ts.font_weight, FontWeight::Normal);
}
#[test]
fn style_with_dash_array() {
let s = Style {
stroke_dasharray: Some(vec![5.0, 3.0]),
..Default::default()
};
assert_eq!(s.stroke_dasharray.as_ref().unwrap(), &[5.0, 3.0]);
}
#[test]
fn style_with_css_classes() {
let s = Style {
css_classes: vec!["node".into(), "highlighted".into()],
..Default::default()
};
assert_eq!(s.css_classes.len(), 2);
assert_eq!(s.css_classes[0], "node");
}
#[test]
fn theme_default_is_light() {
let t = Theme::default();
assert_eq!(t.node_fill, Color::rgba(236, 236, 255, 178));
assert_eq!(t.node_stroke, Color::rgb(147, 112, 219));
}
#[test]
fn theme_dark_has_dark_fills() {
let t = Theme::dark();
assert!(t.node_fill.luminance() < 0.1);
assert!(t.node_text.luminance() > 0.5);
}
#[test]
fn theme_light_typography_and_stroke() {
let t = Theme::light();
assert!((t.font_size_node - 14.0).abs() < f64::EPSILON);
assert!((t.font_size_edge_label - 12.0).abs() < f64::EPSILON);
assert!((t.font_size_label - 13.0).abs() < f64::EPSILON);
assert!((t.font_size_small - 11.0).abs() < f64::EPSILON);
assert!((t.font_size_title - 16.0).abs() < f64::EPSILON);
assert!((t.default_stroke_width - 1.5).abs() < f64::EPSILON);
}
#[test]
fn theme_light_sequence_colors() {
let t = Theme::light();
assert_eq!(t.lifeline_stroke, Color::rgb(175, 165, 200));
assert_eq!(t.activation_fill, Color::rgba(200, 190, 230, 180));
assert_eq!(t.activation_stroke, Color::rgb(153, 153, 153));
}
#[test]
fn theme_dark_has_all_new_fields() {
let t = Theme::dark();
assert!((t.font_size_node - 14.0).abs() < f64::EPSILON);
assert!((t.default_stroke_width - 1.5).abs() < f64::EPSILON);
assert!(t.lifeline_stroke.luminance() < 0.3);
assert!(t.activation_fill.a < 255);
}
#[test]
fn text_style_custom() {
let ts = TextStyle {
font_size: 24.0,
font_family: String::from("monospace"),
fill: Some(Color::BLACK),
font_weight: FontWeight::Bold,
};
assert!((ts.font_size - 24.0).abs() < f64::EPSILON);
assert_eq!(ts.font_family, "monospace");
assert_eq!(ts.fill, Some(Color::BLACK));
assert_eq!(ts.font_weight, FontWeight::Bold);
}
#[test]
fn resolved_stroke_uses_explicit() {
let theme = Theme::light();
let s = Style {
stroke: Some(Color::rgb(255, 0, 0)),
..Default::default()
};
assert_eq!(s.resolved_stroke(&theme), Color::rgb(255, 0, 0));
}
#[test]
fn resolved_stroke_falls_back_to_theme() {
let theme = Theme::light();
let s = Style::default();
assert_eq!(s.resolved_stroke(&theme), theme.edge_stroke);
}
#[test]
fn resolved_stroke_width_uses_explicit() {
let theme = Theme::light();
let s = Style {
stroke_width: Some(3.0),
..Default::default()
};
assert!((s.resolved_stroke_width(&theme) - 3.0).abs() < f64::EPSILON);
}
#[test]
fn resolved_stroke_width_falls_back_to_theme() {
let theme = Theme::light();
let s = Style::default();
assert!(
(s.resolved_stroke_width(&theme) - theme.default_stroke_width).abs() < f64::EPSILON
);
}
#[test]
fn has_explicit_stroke_both_none() {
assert!(!Style::default().has_explicit_stroke());
}
#[test]
fn has_explicit_stroke_color_only() {
let s = Style {
stroke: Some(Color::BLACK),
..Default::default()
};
assert!(s.has_explicit_stroke());
}
#[test]
fn resolve_stroke_opt_none_when_no_explicit() {
let theme = Theme::light();
assert!(Style::default().resolve_stroke_opt(&theme).is_none());
}
#[test]
fn resolve_stroke_opt_some_with_color_only() {
let theme = Theme::light();
let s = Style {
stroke: Some(Color::rgb(0, 128, 0)),
..Default::default()
};
let (color, width) = s.resolve_stroke_opt(&theme).unwrap();
assert_eq!(color, Color::rgb(0, 128, 0));
assert!((width - theme.default_stroke_width).abs() < f64::EPSILON);
}
#[test]
fn resolve_stroke_opt_some_with_width_only() {
let theme = Theme::light();
let s = Style {
stroke_width: Some(5.0),
..Default::default()
};
let (color, width) = s.resolve_stroke_opt(&theme).unwrap();
assert_eq!(color, theme.edge_stroke);
assert!((width - 5.0).abs() < f64::EPSILON);
}
}