use std::hash::{Hash, Hasher};
use std::sync::Arc;
use crate::style::{BorderGlyphs, BorderStyle, Style};
use super::{FragmentKind, MessageStyle};
pub const MESSAGE_STYLE_COUNT: usize = MessageStyle::INDEX_COUNT;
pub const FRAGMENT_KIND_COUNT: usize = FragmentKind::INDEX_COUNT;
const ASCII_BORDER: BorderStyle = BorderStyle::Custom {
glyphs: BorderGlyphs {
top_left: "+",
top: "-",
top_right: "+",
left: "|",
right: "|",
bottom_left: "+",
bottom: "-",
bottom_right: "+",
},
};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SequenceDiagramTheme {
pub message_styles: [Style; MESSAGE_STYLE_COUNT],
pub message_glyphs: MessageGlyphs,
pub fragment_styles: [Style; FRAGMENT_KIND_COUNT],
pub fragment_glyphs: FragmentGlyphs,
pub lifeline: LifelineTheme,
pub activation: ActivationTheme,
pub autonumber: AutonumberTheme,
pub note_style: Style,
pub note_border: BorderStyle,
pub participant_style: Style,
pub participant_border: BorderStyle,
pub message_label_style: Style,
pub hover_style: Style,
}
impl SequenceDiagramTheme {
pub fn classic() -> Self {
Self {
message_styles: [Style::default(); MESSAGE_STYLE_COUNT],
message_glyphs: MessageGlyphs::classic(),
fragment_styles: [Style::default(); FRAGMENT_KIND_COUNT],
fragment_glyphs: FragmentGlyphs::classic(),
lifeline: LifelineTheme {
glyph: '┊',
style: Style::default(),
},
activation: ActivationTheme::classic(),
autonumber: AutonumberTheme::classic(),
note_style: Style::default(),
note_border: BorderStyle::Plain,
participant_style: Style::default(),
participant_border: BorderStyle::Plain,
message_label_style: Style::default(),
hover_style: Style::default(),
}
}
pub fn minimal() -> Self {
Self {
lifeline: LifelineTheme {
glyph: '│',
style: Style::default(),
},
autonumber: AutonumberTheme {
format: Arc::from("{n}"),
style: Style::default(),
},
..Self::classic()
}
}
pub fn ascii() -> Self {
Self {
message_glyphs: MessageGlyphs::ascii(),
fragment_glyphs: FragmentGlyphs::ascii(),
lifeline: LifelineTheme {
glyph: '|',
style: Style::default(),
},
activation: ActivationTheme {
fill_glyph: '|',
style: Style::default(),
fill_background: true,
},
note_border: ASCII_BORDER,
participant_border: ASCII_BORDER,
autonumber: AutonumberTheme::classic(),
..Self::classic()
}
}
pub(crate) fn message_style(&self, kind: MessageStyle) -> Style {
self.message_styles[kind.index()]
}
pub(crate) fn message_style_mut(&mut self, kind: MessageStyle) -> &mut Style {
&mut self.message_styles[kind.index()]
}
pub(crate) fn fragment_style(&self, kind: FragmentKind) -> Style {
self.fragment_styles[kind.index()]
}
pub(crate) fn fragment_style_mut(&mut self, kind: FragmentKind) -> &mut Style {
&mut self.fragment_styles[kind.index()]
}
}
impl Default for SequenceDiagramTheme {
fn default() -> Self {
Self::classic()
}
}
impl Hash for SequenceDiagramTheme {
fn hash<H: Hasher>(&self, state: &mut H) {
self.message_styles.hash(state);
self.message_glyphs.hash(state);
self.fragment_styles.hash(state);
self.fragment_glyphs.hash(state);
self.lifeline.hash(state);
self.activation.hash(state);
self.autonumber.hash(state);
self.note_style.hash(state);
self.note_border.hash(state);
self.participant_style.hash(state);
self.participant_border.hash(state);
self.message_label_style.hash(state);
self.hover_style.hash(state);
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct MessageGlyphs {
pub line: char,
pub dashed_line: char,
pub arrow_filled_right: char,
pub arrow_filled_left: char,
pub arrow_open_right: char,
pub arrow_open_left: char,
pub lost_terminator: char,
pub self_loop_top_right: char,
pub self_loop_bottom_right: char,
}
impl MessageGlyphs {
pub fn classic() -> Self {
Self {
line: '─',
dashed_line: '╌',
arrow_filled_right: '▶',
arrow_filled_left: '◀',
arrow_open_right: '〉',
arrow_open_left: '〈',
lost_terminator: '╳',
self_loop_top_right: '╮',
self_loop_bottom_right: '╯',
}
}
pub fn ascii() -> Self {
Self {
line: '-',
dashed_line: '-',
arrow_filled_right: '>',
arrow_filled_left: '<',
arrow_open_right: '>',
arrow_open_left: '<',
lost_terminator: 'x',
self_loop_top_right: '+',
self_loop_bottom_right: '+',
}
}
}
impl Default for MessageGlyphs {
fn default() -> Self {
Self::classic()
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct FragmentGlyphs {
pub border: BorderStyle,
pub branch_separator: char,
}
impl FragmentGlyphs {
pub fn classic() -> Self {
Self {
border: BorderStyle::Plain,
branch_separator: '╌',
}
}
pub fn ascii() -> Self {
Self {
border: ASCII_BORDER,
branch_separator: '-',
}
}
}
impl Default for FragmentGlyphs {
fn default() -> Self {
Self::classic()
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct LifelineTheme {
pub glyph: char,
pub style: Style,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct ActivationTheme {
pub fill_glyph: char,
pub style: Style,
pub fill_background: bool,
}
impl ActivationTheme {
pub fn classic() -> Self {
Self {
fill_glyph: '┃',
style: Style::default(),
fill_background: true,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct AutonumberTheme {
pub format: Arc<str>,
pub style: Style,
}
impl AutonumberTheme {
pub fn classic() -> Self {
Self {
format: Arc::from("[{n}]"),
style: Style::default(),
}
}
}
#[cfg(test)]
mod tests {
use super::super::{SequenceDiagram, SequenceDiagramNode};
use super::*;
use crate::style::Color;
use crate::widgets::sequence_diagram::reconcile_sequence_diagram;
#[test]
fn default_theme_matches_classic_preset() {
assert_eq!(
SequenceDiagramTheme::default(),
SequenceDiagramTheme::classic()
);
}
#[test]
fn flat_setter_lifeline_style_mutates_theme_slot() {
let style = Style::new().fg(Color::Red);
let diagram = SequenceDiagram::new().lifeline_style(style);
assert_eq!(diagram.theme.lifeline.style, style);
}
#[test]
fn message_kind_style_only_affects_target_kind() {
let style = Style::new().fg(Color::Red);
let diagram = SequenceDiagram::new().message_kind_style(MessageStyle::Sync, style);
assert_eq!(diagram.theme.message_style(MessageStyle::Sync), style);
assert_eq!(
diagram.theme.message_style(MessageStyle::Async),
Style::default()
);
}
#[test]
fn theme_changes_invalidate_widget_cache() {
let mut node = SequenceDiagramNode::default();
let diagram = SequenceDiagram::new();
assert!(reconcile_sequence_diagram(&diagram, &mut node));
let themed = diagram.theme(SequenceDiagramTheme::ascii());
assert!(reconcile_sequence_diagram(&themed, &mut node));
}
#[test]
fn ascii_preset_uses_no_unicode_box_drawing() {
let theme = SequenceDiagramTheme::ascii();
let glyphs = [
theme.message_glyphs.line,
theme.message_glyphs.dashed_line,
theme.message_glyphs.arrow_filled_right,
theme.message_glyphs.arrow_filled_left,
theme.message_glyphs.arrow_open_right,
theme.message_glyphs.arrow_open_left,
theme.message_glyphs.lost_terminator,
theme.message_glyphs.self_loop_top_right,
theme.message_glyphs.self_loop_bottom_right,
theme.fragment_glyphs.branch_separator,
theme.lifeline.glyph,
theme.activation.fill_glyph,
];
assert!(glyphs.iter().all(char::is_ascii));
assert_ascii_border(theme.fragment_glyphs.border);
assert_ascii_border(theme.note_border);
}
fn assert_ascii_border(border: BorderStyle) {
let BorderStyle::Custom { glyphs } = border else {
panic!("expected custom ASCII border");
};
let all = [
glyphs.top_left,
glyphs.top,
glyphs.top_right,
glyphs.left,
glyphs.right,
glyphs.bottom_left,
glyphs.bottom,
glyphs.bottom_right,
];
assert!(all.iter().all(|glyph| glyph.is_ascii()));
}
}