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}