Skip to main content

abstracttui_graph/
desc.rs

1//! The input half of the crate's one data contract: [`GraphDesc`].
2//!
3//! Consumers describe *what* the graph is (nodes with cell sizes, edges
4//! with optional metadata); every layout pass in this crate consumes the
5//! same description and produces the same [`crate::Layout`]. Algorithms
6//! are selectable; the contract is not.
7
8use abstracttui::base::Size;
9
10/// Flow direction of a layout, in mermaid vocabulary: TD / LR / BT / RL.
11///
12/// Directions are transposes of one canonical computation: the layered
13/// pass lays ranks along the *flow* axis and orders nodes along the
14/// *cross* axis, then maps (cross, flow) into screen (x, y). Node cards
15/// never rotate — a `w x h` card stays `w x h` in every direction.
16///
17/// This is a closed vocabulary by design (four flows exist), so the enum
18/// is deliberately exhaustive per ADR-0003 §3.
19#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
20pub enum Direction {
21    /// Rank 0 at the top, flow downward (mermaid `TD`/`TB`). The default.
22    #[default]
23    TopDown,
24    /// Rank 0 at the left, flow rightward (mermaid `LR`).
25    LeftRight,
26    /// Rank 0 at the bottom, flow upward (mermaid `BT`).
27    BottomTop,
28    /// Rank 0 at the right, flow leftward (mermaid `RL`).
29    RightLeft,
30}
31
32impl Direction {
33    /// True when the flow axis is vertical (TD / BT).
34    pub const fn is_vertical(self) -> bool {
35        matches!(self, Direction::TopDown | Direction::BottomTop)
36    }
37
38    /// True when rank 0 sits at the far end of its axis (BT / RL), i.e.
39    /// the canonical picture is mirrored along the flow axis.
40    pub const fn is_reversed(self) -> bool {
41        matches!(self, Direction::BottomTop | Direction::RightLeft)
42    }
43}
44
45/// One node: a stable string id plus its card size in terminal cells.
46///
47/// `kind` and `label` are caller metadata carried through untouched —
48/// layout never reads them (renderers and 0450's mermaid mapping do).
49///
50/// Author-written, shape-stable struct: plain fields + [`Default`], the
51/// FRU idiom (`NodeDesc { id, size, ..Default::default() }`) per
52/// ADR-0003 §2, or the fluent constructors below.
53#[derive(Clone, Debug, Default, PartialEq, Eq)]
54pub struct NodeDesc {
55    /// Unique node id. Duplicate ids are a caller mistake: the first
56    /// occurrence wins and the drop is recorded in the layout's
57    /// `fallback` label (never silent).
58    pub id: String,
59    /// Card size in cells. Non-positive extents are clamped to 1x1 at
60    /// layout time so degenerate inputs still produce a usable picture.
61    pub size: Size,
62    /// Optional caller metadata (e.g. a semantic class for theming).
63    pub kind: Option<String>,
64    /// Optional display label; layout carries it through untouched.
65    pub label: Option<String>,
66}
67
68impl NodeDesc {
69    /// A node with the two required facts: id and card size in cells.
70    pub fn new(id: impl Into<String>, w: i32, h: i32) -> Self {
71        NodeDesc {
72            id: id.into(),
73            size: Size::new(w, h),
74            ..Default::default()
75        }
76    }
77
78    /// Attach a semantic kind (builder style).
79    pub fn kind(mut self, kind: impl Into<String>) -> Self {
80        self.kind = Some(kind.into());
81        self
82    }
83
84    /// Attach a display label (builder style).
85    pub fn label(mut self, label: impl Into<String>) -> Self {
86        self.label = Some(label.into());
87        self
88    }
89}
90
91/// One directed edge between two node ids.
92///
93/// `label` and `style` are caller metadata carried through untouched.
94/// Same extensibility class as [`NodeDesc`]: plain + `Default` + FRU.
95#[derive(Clone, Debug, Default, PartialEq, Eq)]
96pub struct EdgeDesc {
97    /// Source node id.
98    pub from: String,
99    /// Target node id.
100    pub to: String,
101    /// Optional display label (v1 renderers place it at the midpoint).
102    pub label: Option<String>,
103    /// Optional style hint (opaque to layout).
104    pub style: Option<String>,
105}
106
107impl EdgeDesc {
108    /// An edge with the two required facts: endpoint ids.
109    pub fn new(from: impl Into<String>, to: impl Into<String>) -> Self {
110        EdgeDesc {
111            from: from.into(),
112            to: to.into(),
113            ..Default::default()
114        }
115    }
116
117    /// Attach a display label (builder style).
118    pub fn label(mut self, label: impl Into<String>) -> Self {
119        self.label = Some(label.into());
120        self
121    }
122
123    /// Attach a style hint (builder style).
124    pub fn style(mut self, style: impl Into<String>) -> Self {
125        self.style = Some(style.into());
126        self
127    }
128}
129
130/// A whole graph description: the single input type of every layout pass.
131///
132/// Node and edge ORDER is meaningful: it is the deterministic tiebreak
133/// for every heuristic in this crate (same `GraphDesc` in, same
134/// [`crate::Layout`] out — golden-test-pinned).
135///
136/// ```
137/// use abstracttui_graph::{layered, GraphDesc, LayeredOpts};
138///
139/// let desc = GraphDesc::new()
140///     .node("a", 8, 3)
141///     .node("b", 8, 3)
142///     .edge("a", "b");
143/// let layout = layered(&desc, &LayeredOpts::default());
144/// assert_eq!(layout.nodes.len(), 2);
145/// ```
146#[derive(Clone, Debug, Default, PartialEq, Eq)]
147pub struct GraphDesc {
148    /// Nodes, in caller order.
149    pub nodes: Vec<NodeDesc>,
150    /// Edges, in caller order. `Layout` edges echo their index here so
151    /// metadata (label/style) maps back even with duplicate endpoints.
152    pub edges: Vec<EdgeDesc>,
153}
154
155impl GraphDesc {
156    /// An empty graph.
157    pub fn new() -> Self {
158        GraphDesc::default()
159    }
160
161    /// Append a node (fluent form of pushing a [`NodeDesc`]).
162    pub fn node(mut self, id: impl Into<String>, w: i32, h: i32) -> Self {
163        self.nodes.push(NodeDesc::new(id, w, h));
164        self
165    }
166
167    /// Append a fully-specified node.
168    pub fn with_node(mut self, node: NodeDesc) -> Self {
169        self.nodes.push(node);
170        self
171    }
172
173    /// Append an edge (fluent form of pushing an [`EdgeDesc`]).
174    pub fn edge(mut self, from: impl Into<String>, to: impl Into<String>) -> Self {
175        self.edges.push(EdgeDesc::new(from, to));
176        self
177    }
178
179    /// Append a fully-specified edge.
180    pub fn with_edge(mut self, edge: EdgeDesc) -> Self {
181        self.edges.push(edge);
182        self
183    }
184}