Skip to main content

envision/component/diagram/
types.rs

1//! Public types for the Diagram component.
2//!
3//! This module defines the data model for nodes, edges, clusters,
4//! and configuration enums used by [`DiagramState`](super::DiagramState).
5
6use ratatui::style::Color;
7
8/// Status of a node in the diagram.
9///
10/// # Examples
11///
12/// ```
13/// use envision::component::diagram::NodeStatus;
14///
15/// let status = NodeStatus::default();
16/// assert_eq!(status, NodeStatus::Healthy);
17/// ```
18#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
19#[cfg_attr(
20    feature = "serialization",
21    derive(serde::Serialize, serde::Deserialize)
22)]
23pub enum NodeStatus {
24    /// The node is operating normally.
25    #[default]
26    Healthy,
27    /// The node is experiencing issues but still functional.
28    Degraded,
29    /// The node is not operational.
30    Down,
31    /// The node status is not known.
32    Unknown,
33}
34
35/// Shape of a diagram node's border.
36///
37/// # Examples
38///
39/// ```
40/// use envision::component::diagram::NodeShape;
41///
42/// let shape = NodeShape::default();
43/// assert_eq!(shape, NodeShape::Rectangle);
44/// ```
45#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
46#[cfg_attr(
47    feature = "serialization",
48    derive(serde::Serialize, serde::Deserialize)
49)]
50pub enum NodeShape {
51    /// Standard rectangular border using box-drawing characters.
52    #[default]
53    Rectangle,
54    /// Rounded corners using `╭╮╯╰` characters.
55    RoundedRectangle,
56    /// Diamond shape for decision/conditional nodes.
57    Diamond,
58}
59
60/// Visual style of an edge line.
61///
62/// # Examples
63///
64/// ```
65/// use envision::component::diagram::EdgeStyle;
66///
67/// let style = EdgeStyle::default();
68/// assert_eq!(style, EdgeStyle::Solid);
69/// ```
70#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
71#[cfg_attr(
72    feature = "serialization",
73    derive(serde::Serialize, serde::Deserialize)
74)]
75pub enum EdgeStyle {
76    /// Continuous line: `───────`
77    #[default]
78    Solid,
79    /// Alternating segments: `── ── ──`
80    Dashed,
81    /// Dotted line: `·······`
82    Dotted,
83}
84
85/// Which layout algorithm to use for positioning nodes.
86///
87/// # Examples
88///
89/// ```
90/// use envision::component::diagram::LayoutMode;
91///
92/// let mode = LayoutMode::default();
93/// assert_eq!(mode, LayoutMode::Hierarchical);
94/// ```
95#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
96#[cfg_attr(
97    feature = "serialization",
98    derive(serde::Serialize, serde::Deserialize)
99)]
100pub enum LayoutMode {
101    /// Sugiyama-style layered layout. Best for DAGs, dependency graphs,
102    /// and any graph with a natural flow direction.
103    #[default]
104    Hierarchical,
105    /// Fruchterman-Reingold force-directed layout. Best for network
106    /// diagrams and graphs without a clear hierarchy.
107    ForceDirected,
108}
109
110/// Rendering fidelity mode for edges.
111///
112/// # Examples
113///
114/// ```
115/// use envision::component::diagram::RenderMode;
116///
117/// let mode = RenderMode::default();
118/// assert_eq!(mode, RenderMode::BoxDrawing);
119/// ```
120#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
121#[cfg_attr(
122    feature = "serialization",
123    derive(serde::Serialize, serde::Deserialize)
124)]
125pub enum RenderMode {
126    /// Unicode box-drawing characters for edges. Standard fidelity.
127    #[default]
128    BoxDrawing,
129    /// Braille patterns for edges, giving 8 sub-pixels per cell.
130    /// Higher fidelity for dense graphs with many edge crossings.
131    Braille,
132}
133
134/// Orientation of the hierarchical layout.
135///
136/// # Examples
137///
138/// ```
139/// use envision::component::diagram::Orientation;
140///
141/// let o = Orientation::default();
142/// assert_eq!(o, Orientation::LeftToRight);
143/// ```
144#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
145#[cfg_attr(
146    feature = "serialization",
147    derive(serde::Serialize, serde::Deserialize)
148)]
149pub enum Orientation {
150    /// Root nodes on the left, flow goes right.
151    #[default]
152    LeftToRight,
153    /// Root nodes on top, flow goes down.
154    TopToBottom,
155}
156
157// ---------------------------------------------------------------------------
158// DiagramNode
159// ---------------------------------------------------------------------------
160
161/// A node in the diagram.
162///
163/// Nodes are the primary visual elements. Each has a unique string ID,
164/// a display label, and optional status, color, shape, metadata, and
165/// cluster membership.
166///
167/// # Examples
168///
169/// ```
170/// use envision::component::diagram::{DiagramNode, NodeStatus, NodeShape};
171///
172/// let node = DiagramNode::new("api", "API Gateway")
173///     .with_status(NodeStatus::Healthy)
174///     .with_shape(NodeShape::RoundedRectangle)
175///     .with_metadata("version", "2.1.0");
176///
177/// assert_eq!(node.id(), "api");
178/// assert_eq!(node.label(), "API Gateway");
179/// assert_eq!(node.status(), &NodeStatus::Healthy);
180/// assert_eq!(node.shape(), &NodeShape::RoundedRectangle);
181/// assert_eq!(node.metadata().len(), 1);
182/// ```
183#[derive(Clone, Debug, PartialEq)]
184#[cfg_attr(
185    feature = "serialization",
186    derive(serde::Serialize, serde::Deserialize)
187)]
188pub struct DiagramNode {
189    id: String,
190    label: String,
191    status: NodeStatus,
192    color: Option<Color>,
193    shape: NodeShape,
194    metadata: Vec<(String, String)>,
195    cluster_id: Option<String>,
196}
197
198impl DiagramNode {
199    /// Creates a new node with the given ID and label.
200    ///
201    /// The node starts with [`NodeStatus::Healthy`], [`NodeShape::Rectangle`],
202    /// no color override, no metadata, and no cluster.
203    ///
204    /// # Examples
205    ///
206    /// ```
207    /// use envision::component::diagram::DiagramNode;
208    ///
209    /// let node = DiagramNode::new("db", "Database");
210    /// assert_eq!(node.id(), "db");
211    /// assert_eq!(node.label(), "Database");
212    /// ```
213    pub fn new(id: impl Into<String>, label: impl Into<String>) -> Self {
214        Self {
215            id: id.into(),
216            label: label.into(),
217            status: NodeStatus::default(),
218            color: None,
219            shape: NodeShape::default(),
220            metadata: Vec::new(),
221            cluster_id: None,
222        }
223    }
224
225    /// Sets the node's status.
226    ///
227    /// # Examples
228    ///
229    /// ```
230    /// use envision::component::diagram::{DiagramNode, NodeStatus};
231    ///
232    /// let node = DiagramNode::new("db", "DB").with_status(NodeStatus::Degraded);
233    /// assert_eq!(node.status(), &NodeStatus::Degraded);
234    /// ```
235    pub fn with_status(mut self, status: NodeStatus) -> Self {
236        self.status = status;
237        self
238    }
239
240    /// Sets a color override for the node border.
241    ///
242    /// # Examples
243    ///
244    /// ```
245    /// use envision::component::diagram::DiagramNode;
246    /// use ratatui::style::Color;
247    ///
248    /// let node = DiagramNode::new("api", "API").with_color(Color::Cyan);
249    /// assert_eq!(node.color(), Some(Color::Cyan));
250    /// ```
251    pub fn with_color(mut self, color: Color) -> Self {
252        self.color = Some(color);
253        self
254    }
255
256    /// Sets the node's visual shape.
257    ///
258    /// # Examples
259    ///
260    /// ```
261    /// use envision::component::diagram::{DiagramNode, NodeShape};
262    ///
263    /// let node = DiagramNode::new("decision", "Approve?")
264    ///     .with_shape(NodeShape::Diamond);
265    /// assert_eq!(node.shape(), &NodeShape::Diamond);
266    /// ```
267    pub fn with_shape(mut self, shape: NodeShape) -> Self {
268        self.shape = shape;
269        self
270    }
271
272    /// Adds a metadata key-value pair to the node.
273    ///
274    /// Metadata is shown when the node is expanded (via Space key).
275    ///
276    /// # Examples
277    ///
278    /// ```
279    /// use envision::component::diagram::DiagramNode;
280    ///
281    /// let node = DiagramNode::new("pod", "nginx-abc123")
282    ///     .with_metadata("namespace", "default")
283    ///     .with_metadata("image", "nginx:1.25");
284    /// assert_eq!(node.metadata().len(), 2);
285    /// assert_eq!(node.metadata()[0], ("namespace".to_string(), "default".to_string()));
286    /// ```
287    pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
288        self.metadata.push((key.into(), value.into()));
289        self
290    }
291
292    /// Assigns this node to a cluster.
293    ///
294    /// # Examples
295    ///
296    /// ```
297    /// use envision::component::diagram::DiagramNode;
298    ///
299    /// let node = DiagramNode::new("api", "API").with_cluster("us-east");
300    /// assert_eq!(node.cluster_id(), Some("us-east"));
301    /// ```
302    pub fn with_cluster(mut self, cluster_id: impl Into<String>) -> Self {
303        self.cluster_id = Some(cluster_id.into());
304        self
305    }
306
307    /// Returns the node's unique identifier.
308    ///
309    /// # Examples
310    ///
311    /// ```
312    /// use envision::component::diagram::DiagramNode;
313    ///
314    /// let node = DiagramNode::new("svc", "Service");
315    /// assert_eq!(node.id(), "svc");
316    /// ```
317    pub fn id(&self) -> &str {
318        &self.id
319    }
320
321    /// Returns the node's display label.
322    ///
323    /// # Examples
324    ///
325    /// ```
326    /// use envision::component::diagram::DiagramNode;
327    ///
328    /// let node = DiagramNode::new("svc", "My Service");
329    /// assert_eq!(node.label(), "My Service");
330    /// ```
331    pub fn label(&self) -> &str {
332        &self.label
333    }
334
335    /// Returns the node's current status.
336    ///
337    /// # Examples
338    ///
339    /// ```
340    /// use envision::component::diagram::{DiagramNode, NodeStatus};
341    ///
342    /// let node = DiagramNode::new("db", "DB");
343    /// assert_eq!(node.status(), &NodeStatus::Healthy);
344    /// ```
345    pub fn status(&self) -> &NodeStatus {
346        &self.status
347    }
348
349    /// Sets the node's status.
350    ///
351    /// # Examples
352    ///
353    /// ```
354    /// use envision::component::diagram::{DiagramNode, NodeStatus};
355    ///
356    /// let mut node = DiagramNode::new("db", "DB");
357    /// node.set_status(NodeStatus::Down);
358    /// assert_eq!(node.status(), &NodeStatus::Down);
359    /// ```
360    pub fn set_status(&mut self, status: NodeStatus) {
361        self.status = status;
362    }
363
364    /// Returns the optional color override.
365    ///
366    /// # Examples
367    ///
368    /// ```
369    /// use envision::component::diagram::DiagramNode;
370    ///
371    /// let node = DiagramNode::new("api", "API");
372    /// assert_eq!(node.color(), None);
373    /// ```
374    pub fn color(&self) -> Option<Color> {
375        self.color
376    }
377
378    /// Sets a color override.
379    ///
380    /// # Examples
381    ///
382    /// ```
383    /// use envision::component::diagram::DiagramNode;
384    /// use ratatui::style::Color;
385    ///
386    /// let mut node = DiagramNode::new("api", "API");
387    /// node.set_color(Some(Color::Red));
388    /// assert_eq!(node.color(), Some(Color::Red));
389    /// ```
390    pub fn set_color(&mut self, color: Option<Color>) {
391        self.color = color;
392    }
393
394    /// Returns the node's visual shape.
395    ///
396    /// # Examples
397    ///
398    /// ```
399    /// use envision::component::diagram::{DiagramNode, NodeShape};
400    ///
401    /// let node = DiagramNode::new("x", "X");
402    /// assert_eq!(node.shape(), &NodeShape::Rectangle);
403    /// ```
404    pub fn shape(&self) -> &NodeShape {
405        &self.shape
406    }
407
408    /// Returns the node's metadata key-value pairs.
409    ///
410    /// # Examples
411    ///
412    /// ```
413    /// use envision::component::diagram::DiagramNode;
414    ///
415    /// let node = DiagramNode::new("x", "X");
416    /// assert!(node.metadata().is_empty());
417    /// ```
418    pub fn metadata(&self) -> &[(String, String)] {
419        &self.metadata
420    }
421
422    /// Returns the cluster ID this node belongs to, if any.
423    ///
424    /// # Examples
425    ///
426    /// ```
427    /// use envision::component::diagram::DiagramNode;
428    ///
429    /// let node = DiagramNode::new("api", "API").with_cluster("prod");
430    /// assert_eq!(node.cluster_id(), Some("prod"));
431    /// ```
432    pub fn cluster_id(&self) -> Option<&str> {
433        self.cluster_id.as_deref()
434    }
435
436    /// Sets the node's label.
437    ///
438    /// # Examples
439    ///
440    /// ```
441    /// use envision::component::diagram::DiagramNode;
442    ///
443    /// let mut node = DiagramNode::new("api", "API");
444    /// node.set_label("API v2");
445    /// assert_eq!(node.label(), "API v2");
446    /// ```
447    pub fn set_label(&mut self, label: impl Into<String>) {
448        self.label = label.into();
449    }
450}
451
452// ---------------------------------------------------------------------------
453// DiagramEdge
454// ---------------------------------------------------------------------------
455
456/// A directed edge connecting two nodes in the diagram.
457///
458/// Edges are identified by their (from, to) node ID pair and carry
459/// optional label, color, style, and directionality.
460///
461/// # Examples
462///
463/// ```
464/// use envision::component::diagram::{DiagramEdge, EdgeStyle};
465///
466/// let edge = DiagramEdge::new("api", "db")
467///     .with_label("SQL")
468///     .with_style(EdgeStyle::Dashed);
469///
470/// assert_eq!(edge.from(), "api");
471/// assert_eq!(edge.to(), "db");
472/// assert_eq!(edge.label(), Some("SQL"));
473/// assert_eq!(edge.style(), &EdgeStyle::Dashed);
474/// assert!(!edge.bidirectional());
475/// ```
476#[derive(Clone, Debug, PartialEq)]
477#[cfg_attr(
478    feature = "serialization",
479    derive(serde::Serialize, serde::Deserialize)
480)]
481pub struct DiagramEdge {
482    from: String,
483    to: String,
484    label: Option<String>,
485    color: Option<Color>,
486    style: EdgeStyle,
487    bidirectional: bool,
488}
489
490impl DiagramEdge {
491    /// Creates a new directed edge from one node to another.
492    ///
493    /// # Examples
494    ///
495    /// ```
496    /// use envision::component::diagram::DiagramEdge;
497    ///
498    /// let edge = DiagramEdge::new("a", "b");
499    /// assert_eq!(edge.from(), "a");
500    /// assert_eq!(edge.to(), "b");
501    /// ```
502    pub fn new(from: impl Into<String>, to: impl Into<String>) -> Self {
503        Self {
504            from: from.into(),
505            to: to.into(),
506            label: None,
507            color: None,
508            style: EdgeStyle::default(),
509            bidirectional: false,
510        }
511    }
512
513    /// Sets the edge label (displayed at the midpoint).
514    ///
515    /// # Examples
516    ///
517    /// ```
518    /// use envision::component::diagram::DiagramEdge;
519    ///
520    /// let edge = DiagramEdge::new("a", "b").with_label("HTTP");
521    /// assert_eq!(edge.label(), Some("HTTP"));
522    /// ```
523    pub fn with_label(mut self, label: impl Into<String>) -> Self {
524        self.label = Some(label.into());
525        self
526    }
527
528    /// Sets a color override for the edge.
529    ///
530    /// # Examples
531    ///
532    /// ```
533    /// use envision::component::diagram::DiagramEdge;
534    /// use ratatui::style::Color;
535    ///
536    /// let edge = DiagramEdge::new("a", "b").with_color(Color::Yellow);
537    /// assert_eq!(edge.color(), Some(Color::Yellow));
538    /// ```
539    pub fn with_color(mut self, color: Color) -> Self {
540        self.color = Some(color);
541        self
542    }
543
544    /// Sets the edge line style.
545    ///
546    /// # Examples
547    ///
548    /// ```
549    /// use envision::component::diagram::{DiagramEdge, EdgeStyle};
550    ///
551    /// let edge = DiagramEdge::new("a", "b").with_style(EdgeStyle::Dotted);
552    /// assert_eq!(edge.style(), &EdgeStyle::Dotted);
553    /// ```
554    pub fn with_style(mut self, style: EdgeStyle) -> Self {
555        self.style = style;
556        self
557    }
558
559    /// Makes the edge bidirectional (arrowheads on both ends).
560    ///
561    /// # Examples
562    ///
563    /// ```
564    /// use envision::component::diagram::DiagramEdge;
565    ///
566    /// let edge = DiagramEdge::new("a", "b").with_bidirectional(true);
567    /// assert!(edge.bidirectional());
568    /// ```
569    pub fn with_bidirectional(mut self, bidirectional: bool) -> Self {
570        self.bidirectional = bidirectional;
571        self
572    }
573
574    /// Returns the source node ID.
575    ///
576    /// # Examples
577    ///
578    /// ```
579    /// use envision::component::diagram::DiagramEdge;
580    ///
581    /// let edge = DiagramEdge::new("src", "dst");
582    /// assert_eq!(edge.from(), "src");
583    /// ```
584    pub fn from(&self) -> &str {
585        &self.from
586    }
587
588    /// Returns the target node ID.
589    ///
590    /// # Examples
591    ///
592    /// ```
593    /// use envision::component::diagram::DiagramEdge;
594    ///
595    /// let edge = DiagramEdge::new("src", "dst");
596    /// assert_eq!(edge.to(), "dst");
597    /// ```
598    pub fn to(&self) -> &str {
599        &self.to
600    }
601
602    /// Returns the edge label, if any.
603    ///
604    /// # Examples
605    ///
606    /// ```
607    /// use envision::component::diagram::DiagramEdge;
608    ///
609    /// let edge = DiagramEdge::new("a", "b");
610    /// assert_eq!(edge.label(), None);
611    /// ```
612    pub fn label(&self) -> Option<&str> {
613        self.label.as_deref()
614    }
615
616    /// Returns the optional color override.
617    ///
618    /// # Examples
619    ///
620    /// ```
621    /// use envision::component::diagram::DiagramEdge;
622    ///
623    /// let edge = DiagramEdge::new("a", "b");
624    /// assert_eq!(edge.color(), None);
625    /// ```
626    pub fn color(&self) -> Option<Color> {
627        self.color
628    }
629
630    /// Sets the edge color.
631    ///
632    /// # Examples
633    ///
634    /// ```
635    /// use envision::component::diagram::DiagramEdge;
636    /// use ratatui::style::Color;
637    ///
638    /// let mut edge = DiagramEdge::new("a", "b");
639    /// edge.set_color(Some(Color::Green));
640    /// assert_eq!(edge.color(), Some(Color::Green));
641    /// ```
642    pub fn set_color(&mut self, color: Option<Color>) {
643        self.color = color;
644    }
645
646    /// Returns the edge line style.
647    ///
648    /// # Examples
649    ///
650    /// ```
651    /// use envision::component::diagram::{DiagramEdge, EdgeStyle};
652    ///
653    /// let edge = DiagramEdge::new("a", "b");
654    /// assert_eq!(edge.style(), &EdgeStyle::Solid);
655    /// ```
656    pub fn style(&self) -> &EdgeStyle {
657        &self.style
658    }
659
660    /// Returns whether the edge is bidirectional.
661    ///
662    /// # Examples
663    ///
664    /// ```
665    /// use envision::component::diagram::DiagramEdge;
666    ///
667    /// let edge = DiagramEdge::new("a", "b");
668    /// assert!(!edge.bidirectional());
669    /// ```
670    pub fn bidirectional(&self) -> bool {
671        self.bidirectional
672    }
673}
674
675// ---------------------------------------------------------------------------
676// DiagramCluster
677// ---------------------------------------------------------------------------
678
679/// A named group of nodes displayed with a shared border.
680///
681/// Clusters visually group related nodes together. Nodes are assigned
682/// to clusters via [`DiagramNode::with_cluster`].
683///
684/// # Examples
685///
686/// ```
687/// use envision::component::diagram::DiagramCluster;
688/// use ratatui::style::Color;
689///
690/// let cluster = DiagramCluster::new("us-east", "US East")
691///     .with_color(Color::Blue);
692///
693/// assert_eq!(cluster.id(), "us-east");
694/// assert_eq!(cluster.label(), "US East");
695/// assert_eq!(cluster.color(), Some(Color::Blue));
696/// ```
697#[derive(Clone, Debug, PartialEq)]
698#[cfg_attr(
699    feature = "serialization",
700    derive(serde::Serialize, serde::Deserialize)
701)]
702pub struct DiagramCluster {
703    id: String,
704    label: String,
705    color: Option<Color>,
706}
707
708impl DiagramCluster {
709    /// Creates a new cluster with the given ID and label.
710    ///
711    /// # Examples
712    ///
713    /// ```
714    /// use envision::component::diagram::DiagramCluster;
715    ///
716    /// let cluster = DiagramCluster::new("prod", "Production");
717    /// assert_eq!(cluster.id(), "prod");
718    /// assert_eq!(cluster.label(), "Production");
719    /// ```
720    pub fn new(id: impl Into<String>, label: impl Into<String>) -> Self {
721        Self {
722            id: id.into(),
723            label: label.into(),
724            color: None,
725        }
726    }
727
728    /// Sets a color for the cluster border.
729    ///
730    /// # Examples
731    ///
732    /// ```
733    /// use envision::component::diagram::DiagramCluster;
734    /// use ratatui::style::Color;
735    ///
736    /// let cluster = DiagramCluster::new("dev", "Dev").with_color(Color::Gray);
737    /// assert_eq!(cluster.color(), Some(Color::Gray));
738    /// ```
739    pub fn with_color(mut self, color: Color) -> Self {
740        self.color = Some(color);
741        self
742    }
743
744    /// Returns the cluster's unique identifier.
745    ///
746    /// # Examples
747    ///
748    /// ```
749    /// use envision::component::diagram::DiagramCluster;
750    ///
751    /// let cluster = DiagramCluster::new("c1", "Cluster");
752    /// assert_eq!(cluster.id(), "c1");
753    /// ```
754    pub fn id(&self) -> &str {
755        &self.id
756    }
757
758    /// Returns the cluster's display label.
759    ///
760    /// # Examples
761    ///
762    /// ```
763    /// use envision::component::diagram::DiagramCluster;
764    ///
765    /// let cluster = DiagramCluster::new("c1", "My Cluster");
766    /// assert_eq!(cluster.label(), "My Cluster");
767    /// ```
768    pub fn label(&self) -> &str {
769        &self.label
770    }
771
772    /// Returns the optional color override.
773    ///
774    /// # Examples
775    ///
776    /// ```
777    /// use envision::component::diagram::DiagramCluster;
778    ///
779    /// let cluster = DiagramCluster::new("c1", "C");
780    /// assert_eq!(cluster.color(), None);
781    /// ```
782    pub fn color(&self) -> Option<Color> {
783        self.color
784    }
785
786    /// Sets the cluster color.
787    ///
788    /// # Examples
789    ///
790    /// ```
791    /// use envision::component::diagram::DiagramCluster;
792    /// use ratatui::style::Color;
793    ///
794    /// let mut cluster = DiagramCluster::new("c1", "C");
795    /// cluster.set_color(Some(Color::Magenta));
796    /// assert_eq!(cluster.color(), Some(Color::Magenta));
797    /// ```
798    pub fn set_color(&mut self, color: Option<Color>) {
799        self.color = color;
800    }
801
802    /// Sets the cluster label.
803    ///
804    /// # Examples
805    ///
806    /// ```
807    /// use envision::component::diagram::DiagramCluster;
808    ///
809    /// let mut cluster = DiagramCluster::new("c1", "Old");
810    /// cluster.set_label("New Label");
811    /// assert_eq!(cluster.label(), "New Label");
812    /// ```
813    pub fn set_label(&mut self, label: impl Into<String>) {
814        self.label = label.into();
815    }
816}