Skip to main content

tui_lipan/widgets/state_diagram/
mod.rs

1//! Static UML state diagram widget.
2
3mod layout;
4mod node;
5mod reconcile;
6mod theme;
7
8pub use layout::measure_state_diagram;
9pub use node::StateDiagramNode;
10pub use reconcile::reconcile_state_diagram;
11pub use theme::StateDiagramTheme;
12
13use crate::core::element::{Element, ElementKind};
14use crate::style::{BorderStyle, Length, Padding, Style};
15use std::sync::Arc;
16
17/// The kind of node a [`StateSpec`] represents, controlling how it is rendered.
18#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
19pub enum StateKind {
20    /// A normal named state box.
21    #[default]
22    State,
23    /// The initial pseudo-state (filled dot).
24    Start,
25    /// The final pseudo-state (ringed dot).
26    End,
27    /// A choice (decision) pseudo-state (diamond).
28    Choice,
29    /// A fork pseudo-state (splits into parallel branches).
30    Fork,
31    /// A join pseudo-state (merges parallel branches).
32    Join,
33}
34
35/// A single state node in a [`StateDiagram`].
36#[derive(Clone, Debug, PartialEq, Eq, Hash)]
37pub struct StateSpec {
38    /// Stable identifier referenced by transitions.
39    pub id: Arc<str>,
40    /// Display label (defaults to `id`).
41    pub label: Arc<str>,
42    /// What kind of node this is.
43    pub kind: StateKind,
44    /// Optional `entry /` action text shown inside the state.
45    pub entry: Option<Arc<str>>,
46    /// Optional `exit /` action text shown inside the state.
47    pub exit: Option<Arc<str>>,
48}
49impl StateSpec {
50    /// Creates a normal state whose label defaults to its `id`.
51    pub fn new(id: impl Into<Arc<str>>) -> Self {
52        let id = id.into();
53        Self {
54            label: id.clone(),
55            id,
56            kind: StateKind::State,
57            entry: None,
58            exit: None,
59        }
60    }
61    /// Sets the display label.
62    pub fn label(mut self, label: impl Into<Arc<str>>) -> Self {
63        self.label = label.into();
64        self
65    }
66    /// Sets the node kind.
67    pub fn kind(mut self, kind: StateKind) -> Self {
68        self.kind = kind;
69        self
70    }
71    /// Sets the `entry /` action text.
72    pub fn entry(mut self, entry: impl Into<Arc<str>>) -> Self {
73        self.entry = Some(entry.into());
74        self
75    }
76    /// Sets the `exit /` action text.
77    pub fn exit(mut self, exit: impl Into<Arc<str>>) -> Self {
78        self.exit = Some(exit.into());
79        self
80    }
81}
82
83/// A directed transition between two states, with an optional label and guard.
84#[derive(Clone, Debug, PartialEq, Eq, Hash)]
85pub struct StateTransition {
86    /// Source state id.
87    pub from: Arc<str>,
88    /// Target state id.
89    pub to: Arc<str>,
90    /// Optional transition (event) label.
91    pub label: Option<Arc<str>>,
92    /// Optional `[guard]` condition shown on the edge.
93    pub guard: Option<Arc<str>>,
94}
95
96/// A static UML state diagram laid out automatically from states and transitions.
97/// Build it with the chaining setters and convert into an [`Element`].
98#[derive(Clone)]
99pub struct StateDiagram {
100    pub(crate) states: Arc<[StateSpec]>,
101    pub(crate) transitions: Arc<[StateTransition]>,
102    pub(crate) style: Style,
103    pub(crate) state_style: Style,
104    pub(crate) edge_style: Style,
105    pub(crate) label_style: Style,
106    pub(crate) border_style: BorderStyle,
107    pub(crate) padding: Padding,
108    pub(crate) node_padding: Padding,
109    pub(crate) layer_gap: u16,
110    pub(crate) node_gap: u16,
111    pub(crate) max_node_width: u16,
112    pub(crate) theme: StateDiagramTheme,
113    pub(crate) width: Length,
114    pub(crate) height: Length,
115}
116
117impl Default for StateDiagram {
118    fn default() -> Self {
119        Self {
120            states: Arc::new([]),
121            transitions: Arc::new([]),
122            style: Style::default(),
123            state_style: Style::default(),
124            edge_style: Style::default(),
125            label_style: Style::default(),
126            border_style: BorderStyle::Rounded,
127            padding: Padding::default(),
128            node_padding: (0, 1).into(),
129            layer_gap: 1,
130            node_gap: 4,
131            max_node_width: 32,
132            theme: StateDiagramTheme::default(),
133            width: Length::Auto,
134            height: Length::Auto,
135        }
136    }
137}
138
139impl StateDiagram {
140    /// Creates an empty diagram with default styling.
141    pub fn new() -> Self {
142        Self::default()
143    }
144    /// Replaces the state set with `states`.
145    pub fn states(mut self, states: impl IntoIterator<Item = StateSpec>) -> Self {
146        self.states = states.into_iter().collect::<Vec<_>>().into();
147        self
148    }
149    /// Replaces the transition set with `transitions`.
150    pub fn transitions(mut self, transitions: impl IntoIterator<Item = StateTransition>) -> Self {
151        self.transitions = transitions.into_iter().collect::<Vec<_>>().into();
152        self
153    }
154    /// Appends a normal state by id.
155    pub fn state(mut self, id: impl Into<Arc<str>>) -> Self {
156        let mut v = self.states.to_vec();
157        v.push(StateSpec::new(id));
158        self.states = v.into();
159        self
160    }
161    /// Appends a [`Choice`](StateKind::Choice) pseudo-state by id.
162    pub fn choice(mut self, id: impl Into<Arc<str>>) -> Self {
163        let mut v = self.states.to_vec();
164        v.push(StateSpec::new(id).kind(StateKind::Choice));
165        self.states = v.into();
166        self
167    }
168    /// Appends a [`Fork`](StateKind::Fork) pseudo-state by id.
169    pub fn fork(mut self, id: impl Into<Arc<str>>) -> Self {
170        let mut v = self.states.to_vec();
171        v.push(StateSpec::new(id).kind(StateKind::Fork));
172        self.states = v.into();
173        self
174    }
175    /// Appends a [`Join`](StateKind::Join) pseudo-state by id.
176    pub fn join(mut self, id: impl Into<Arc<str>>) -> Self {
177        let mut v = self.states.to_vec();
178        v.push(StateSpec::new(id).kind(StateKind::Join));
179        self.states = v.into();
180        self
181    }
182    /// Adds a transition between two states with an optional label.
183    pub fn transition(
184        mut self,
185        from: impl Into<Arc<str>>,
186        to: impl Into<Arc<str>>,
187        label: impl Into<Option<Arc<str>>>,
188    ) -> Self {
189        let mut v = self.transitions.to_vec();
190        v.push(StateTransition {
191            from: from.into(),
192            to: to.into(),
193            label: label.into(),
194            guard: None,
195        });
196        self.transitions = v.into();
197        self
198    }
199    /// Adds the initial pseudo-state (if absent) and a transition from it to `to`.
200    pub fn start_to(mut self, to: impl Into<Arc<str>>) -> Self {
201        let start = Arc::<str>::from("[*]");
202        if !self.states.iter().any(|s| s.id == start) {
203            let mut states = self.states.to_vec();
204            states.push(StateSpec::new(start.clone()).kind(StateKind::Start));
205            self.states = states.into();
206        }
207        self.transition(start, to, None::<Arc<str>>)
208    }
209    /// Adds the final pseudo-state (if absent) and a transition from `from` to it.
210    pub fn end_from(mut self, from: impl Into<Arc<str>>) -> Self {
211        let end = Arc::<str>::from("[end]");
212        if !self.states.iter().any(|s| s.id == end) {
213            let mut states = self.states.to_vec();
214            states.push(StateSpec::new(end.clone()).kind(StateKind::End));
215            self.states = states.into();
216        }
217        self.transition(from, end, None::<Arc<str>>)
218    }
219    /// Sets the base style of the diagram container.
220    pub fn style(mut self, style: Style) -> Self {
221        self.style = style;
222        self
223    }
224    /// Sets the style applied to state boxes.
225    pub fn state_style(mut self, style: Style) -> Self {
226        self.state_style = style;
227        self
228    }
229    /// Sets the style applied to transition edges.
230    pub fn edge_style(mut self, style: Style) -> Self {
231        self.edge_style = style;
232        self
233    }
234    /// Sets the style applied to edge labels.
235    pub fn label_style(mut self, style: Style) -> Self {
236        self.label_style = style;
237        self
238    }
239    /// Sets the border line style for state boxes.
240    pub fn border_style(mut self, style: BorderStyle) -> Self {
241        self.border_style = style;
242        self
243    }
244    /// Sets the outer padding of the diagram.
245    pub fn padding(mut self, padding: impl Into<Padding>) -> Self {
246        self.padding = padding.into();
247        self
248    }
249    /// Sets the inner padding of each state box.
250    pub fn node_padding(mut self, padding: impl Into<Padding>) -> Self {
251        self.node_padding = padding.into();
252        self
253    }
254    /// Caps the rendered width of a state box (minimum 1).
255    pub fn max_node_width(mut self, width: u16) -> Self {
256        self.max_node_width = width.max(1);
257        self
258    }
259    /// Sets the width of the diagram container.
260    pub fn width(mut self, width: Length) -> Self {
261        self.width = width;
262        self
263    }
264    /// Sets the height of the diagram container.
265    pub fn height(mut self, height: Length) -> Self {
266        self.height = height;
267        self
268    }
269}
270
271impl From<StateDiagram> for Element {
272    fn from(value: StateDiagram) -> Self {
273        Element::new(ElementKind::StateDiagram(Box::new(value)))
274    }
275}
276
277#[cfg(test)]
278mod tests {
279    use super::*;
280
281    #[test]
282    fn default_layer_gap_is_compact() {
283        assert_eq!(StateDiagram::default().layer_gap, 1);
284    }
285}