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}