ascii_dag/render/engine/node_content.rs
1//! `NodeContent` — nodes as objects (temp/07).
2//!
3//! A node's kind belongs to the node, at construction:
4//!
5//! ```
6//! use ascii_dag::{Graph, AUTO, BoxedNode, NodeContent};
7//!
8//! let mut g = Graph::new();
9//! let client = g.add_node(AUTO, "Client"); // &str → simple
10//! let server = g.add_node(AUTO, BoxedNode("Server")); // built-in kind
11//! g.add_edge(client, server, None);
12//! ```
13//!
14//! The trait is **sugar, not storage** (NC7b): implementors are
15//! resolved once at insertion into plain data — label, size, kind tag,
16//! optional painter fn, payload — so the object may be a temporary and
17//! no `&dyn` is stored anywhere. All five fields travel both IRs
18//! (including graphs built directly on `CsrGraphBuilder`) and drive
19//! rendering; custom nodes serialize their kind and payload to JSON.
20//! Rich typed state must flatten into `payload`; the painter parses it
21//! back as it draws (zero-alloc iteration — see the trait docs).
22
23use super::style::NodePaintFn;
24
25/// The resolved kind tag a [`NodeContent`] implementor declares.
26///
27/// Hidden from docs: user impls normally keep the default (`Custom`);
28/// built-ins override it so both backends can render them as pure
29/// data. Overriding it to a built-in tag in a user impl is harmless —
30/// the node simply renders through that built-in painter.
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32#[non_exhaustive]
33#[repr(u8)]
34pub enum NodeKindTag {
35 /// `[label]` on the top row — today's default look.
36 Simple = 0,
37 /// A light-stroke box spanning the reserved area.
38 Boxed = 1,
39 /// User-declared content: painter + payload (or blank, if no
40 /// painter — the area stays reserved but unpainted).
41 Custom = 2,
42}
43
44impl NodeKindTag {
45 /// Stable storage value (CSR `NODE_FLAGS` bits / heap tag array).
46 ///
47 /// Exhaustive on purpose — the CSR-tag-drift guard: a new kind
48 /// added here fails to compile until every conversion handles it.
49 #[inline]
50 // Consumers: Graph resolution (alloc) and CsrGraphBuilder (arena).
51 #[cfg_attr(not(any(feature = "alloc", feature = "arena")), allow(dead_code))]
52 pub(crate) fn to_u8(self) -> u8 {
53 match self {
54 NodeKindTag::Simple => 0,
55 NodeKindTag::Boxed => 1,
56 NodeKindTag::Custom => 2,
57 }
58 }
59
60 /// Rebuild from a storage value; unknown values fall back to
61 /// `Simple` (renders the label — nothing is silently invisible).
62 #[inline]
63 pub(crate) fn from_u8(value: u8) -> Self {
64 match value {
65 1 => NodeKindTag::Boxed,
66 2 => NodeKindTag::Custom,
67 _ => NodeKindTag::Simple,
68 }
69 }
70}
71
72/// What a node *is*: label, size, and — for custom kinds — the
73/// template/data pair that fills its reserved area at render time.
74///
75/// Implementors are resolved **at insertion** (the object may be a
76/// temporary); `label()`/`payload()` return `&'a str` so the strings
77/// outlive the object — the same borrow discipline node labels have
78/// always had. Accessors should be cheap and pure: resolution may
79/// call them more than once (the default `size()` reads `label()`),
80/// and nothing is stored until every accessor has returned.
81///
82/// - [`label`](Self::label) feeds legends, JSON, debug, and the
83/// default sizing.
84/// - [`size`](Self::size) feeds layout: the reserved `width × height`
85/// area edges route around. Default: `(label chars + 2, 1)` — the
86/// classic `[label]` footprint.
87/// - [`painter`](Self::painter) is the **template**: a plain `fn` that
88/// draws through the clipped [`NodeRegion`](super::region::NodeRegion). `None` on a custom kind
89/// means **blank**: the area stays reserved, the canvas untouched,
90/// while the label still flows to legends/JSON and hit-testing.
91/// - [`payload`](Self::payload) is the **data**: delivered to the
92/// painter via [`NodePaintCtx`](super::region::NodePaintCtx), parsed as it draws.
93///
94/// A minimal impl (just `label()`) is a *blank* node — pass `&str` or
95/// [`SimpleNode`] instead when you want the classic `[label]` look.
96///
97/// ```
98/// use ascii_dag::{BoxedNode, CustomNode, Graph, RenderOptions};
99/// use ascii_dag::render::engine::{NodePaintCtx, NodeRegion};
100///
101/// // A painter is a plain `fn`: one template, many nodes.
102/// fn card(region: &mut NodeRegion<'_, '_>, ctx: NodePaintCtx<'_>) {
103/// region.write_str(0, 0, ctx.label);
104/// for (i, line) in ctx.payload.lines().enumerate() {
105/// region.write_str(0, 1 + i, line);
106/// }
107/// }
108///
109/// let mut g = Graph::new();
110/// g.add_node(1, "Client"); // [Client]
111/// g.add_node(2, BoxedNode("Database")); // boxed label
112/// g.add_node(3, CustomNode { // your painter + its data
113/// label: "Server",
114/// width: 14,
115/// height: 3,
116/// painter: Some(card),
117/// payload: "cpu: 4\nram: 16G",
118/// });
119/// g.add_edge(1, 3, None);
120/// g.add_edge(3, 2, None);
121///
122/// let text = g.compute_layout().render_string(&RenderOptions::plain());
123/// assert!(text.contains("cpu: 4"));
124/// ```
125pub trait NodeContent<'a> {
126 /// The node's label text.
127 fn label(&self) -> &'a str;
128
129 /// Reserved `(width, height)` in cells/rows. Layout routes edges
130 /// around this area; the painter fills it.
131 fn size(&self) -> (usize, usize) {
132 (self.label().chars().count() + 2, 1)
133 }
134
135 /// The template fn that fills the reserved area, if any.
136 fn painter(&self) -> Option<NodePaintFn> {
137 None
138 }
139
140 /// Data for the painter: travels like a second label, delivered
141 /// via [`NodePaintCtx`](super::region::NodePaintCtx) at paint time
142 /// and exported to JSON as `"payload"` on custom nodes.
143 fn payload(&self) -> &'a str {
144 ""
145 }
146
147 /// The resolved kind tag — built-ins override this; user impls
148 /// keep the default.
149 #[doc(hidden)]
150 fn kind(&self) -> NodeKindTag {
151 NodeKindTag::Custom
152 }
153
154 /// Size provenance: `true` only for impls whose `size()` is the
155 /// provided default formula (`&str`, `&String`, [`SimpleNode`]) —
156 /// the graph then applies the legacy `⟨id⟩` placeholder width to
157 /// empty labels (an id-dependent value `size()` cannot compute).
158 /// Every impl that overrides `size()` keeps `false`: its declared
159 /// size is authoritative, empty label or not.
160 #[doc(hidden)]
161 fn size_is_implicit(&self) -> bool {
162 false
163 }
164}
165
166/// The classic `[label]` node as an explicit object — identical to
167/// passing the `&str` itself.
168#[derive(Debug, Clone, Copy)]
169pub struct SimpleNode<'a>(pub &'a str);
170
171impl<'a> NodeContent<'a> for SimpleNode<'a> {
172 fn label(&self) -> &'a str {
173 self.0
174 }
175
176 fn kind(&self) -> NodeKindTag {
177 NodeKindTag::Simple
178 }
179
180 fn size_is_implicit(&self) -> bool {
181 true
182 }
183}
184
185/// A light-stroke box with the label inside, sized
186/// `(label chars + 4, 3)` — a real box out of the box (D4). For a
187/// box of any other size, declare a [`CustomNode`] (or your own
188/// `NodeContent` impl) with a box-drawing painter.
189#[derive(Debug, Clone, Copy)]
190pub struct BoxedNode<'a>(pub &'a str);
191
192impl<'a> NodeContent<'a> for BoxedNode<'a> {
193 fn label(&self) -> &'a str {
194 self.0
195 }
196
197 fn size(&self) -> (usize, usize) {
198 (self.0.chars().count() + 4, 3)
199 }
200
201 fn kind(&self) -> NodeKindTag {
202 NodeKindTag::Boxed
203 }
204}
205
206/// The explicit five-field custom-node form, for callers who want no
207/// trait impl of their own.
208#[derive(Debug, Clone, Copy)]
209pub struct CustomNode<'a> {
210 /// Label (legends, JSON, debug).
211 pub label: &'a str,
212 /// Reserved width in cells.
213 pub width: usize,
214 /// Reserved height in rows.
215 pub height: usize,
216 /// Template fn; `None` = blank (area reserved, nothing painted).
217 pub painter: Option<NodePaintFn>,
218 /// Data delivered to the painter via [`NodePaintCtx`](super::region::NodePaintCtx).
219 pub payload: &'a str,
220}
221
222impl<'a> NodeContent<'a> for CustomNode<'a> {
223 fn label(&self) -> &'a str {
224 self.label
225 }
226
227 fn size(&self) -> (usize, usize) {
228 (self.width, self.height)
229 }
230
231 fn painter(&self) -> Option<NodePaintFn> {
232 self.painter
233 }
234
235 fn payload(&self) -> &'a str {
236 self.payload
237 }
238}
239
240/// `&str` is the everyday sugar: `g.add_node(1, "Client")` is a simple
241/// node, byte-identical to every release before node objects existed.
242impl<'a> NodeContent<'a> for &'a str {
243 fn label(&self) -> &'a str {
244 self
245 }
246
247 fn kind(&self) -> NodeKindTag {
248 NodeKindTag::Simple
249 }
250
251 fn size_is_implicit(&self) -> bool {
252 true
253 }
254}
255
256/// Forwarding impl so borrowed content works: Rust does not infer
257/// trait impls for references, and `g.add_node(AUTO, &my_card)` must
258/// compile when `my_card` is reused across graphs.
259impl<'a, T: NodeContent<'a>> NodeContent<'a> for &T {
260 fn label(&self) -> &'a str {
261 (**self).label()
262 }
263
264 fn size(&self) -> (usize, usize) {
265 (**self).size()
266 }
267
268 fn painter(&self) -> Option<NodePaintFn> {
269 (**self).painter()
270 }
271
272 fn payload(&self) -> &'a str {
273 (**self).payload()
274 }
275
276 fn kind(&self) -> NodeKindTag {
277 (**self).kind()
278 }
279
280 fn size_is_implicit(&self) -> bool {
281 (**self).size_is_implicit()
282 }
283}
284
285/// `&String` kept compiling into 0.10: the old `label: &'a str`
286/// parameter deref-coerced it; a generic bound does not, so the impl
287/// restores that path explicitly.
288#[cfg(feature = "alloc")]
289impl<'a> NodeContent<'a> for &'a alloc::string::String {
290 fn label(&self) -> &'a str {
291 self.as_str()
292 }
293
294 fn kind(&self) -> NodeKindTag {
295 NodeKindTag::Simple
296 }
297
298 fn size_is_implicit(&self) -> bool {
299 true
300 }
301}
302
303#[cfg(test)]
304mod tests {
305 use super::*;
306 use crate::render::engine::region::{NodePaintCtx, NodeRegion};
307
308 fn probe_painter(_region: &mut NodeRegion<'_, '_>, _ctx: NodePaintCtx<'_>) {}
309
310 struct Card<'a> {
311 title: &'a str,
312 rows: &'a str,
313 }
314
315 impl<'a> NodeContent<'a> for Card<'a> {
316 fn label(&self) -> &'a str {
317 self.title
318 }
319
320 fn size(&self) -> (usize, usize) {
321 (self.title.chars().count() + 4, 4)
322 }
323
324 fn painter(&self) -> Option<NodePaintFn> {
325 Some(probe_painter)
326 }
327
328 fn payload(&self) -> &'a str {
329 self.rows
330 }
331 }
332
333 #[test]
334 fn str_is_simple_with_classic_footprint() {
335 let s: &str = "Client";
336 assert_eq!(s.label(), "Client");
337 assert_eq!(s.size(), (8, 1)); // [Client]
338 assert_eq!(s.kind(), NodeKindTag::Simple);
339 assert!(s.painter().is_none());
340 assert_eq!(s.payload(), "");
341 }
342
343 #[test]
344 fn built_ins_declare_their_kinds_and_sizes() {
345 assert_eq!(SimpleNode("AB").size(), (4, 1));
346 assert_eq!(SimpleNode("AB").kind(), NodeKindTag::Simple);
347 assert_eq!(BoxedNode("AB").size(), (6, 3)); // D4: label+4 × 3
348 assert_eq!(BoxedNode("AB").kind(), NodeKindTag::Boxed);
349 }
350
351 #[test]
352 fn references_forward_everything() {
353 let card = Card {
354 title: "T",
355 rows: "data",
356 };
357 let r = &card;
358 assert_eq!(r.label(), "T");
359 assert_eq!(r.size(), (5, 4));
360 assert_eq!(r.kind(), NodeKindTag::Custom);
361 assert!(r.painter().is_some());
362 assert_eq!(r.payload(), "data");
363 // Double reference rides the same impl.
364 assert_eq!((&r).label(), "T");
365 }
366
367 #[test]
368 fn custom_node_is_the_plain_five_field_form() {
369 let n = CustomNode {
370 label: "L",
371 width: 7,
372 height: 2,
373 painter: None,
374 payload: "p",
375 };
376 assert_eq!(n.size(), (7, 2));
377 assert_eq!(n.kind(), NodeKindTag::Custom);
378 assert!(n.painter().is_none()); // blank node
379 assert_eq!(n.payload(), "p");
380 }
381
382 #[test]
383 fn tag_round_trips_and_unknown_falls_back_to_simple() {
384 for tag in [NodeKindTag::Simple, NodeKindTag::Boxed, NodeKindTag::Custom] {
385 assert_eq!(NodeKindTag::from_u8(tag.to_u8()), tag);
386 }
387 assert_eq!(NodeKindTag::from_u8(250), NodeKindTag::Simple);
388 }
389
390 #[cfg(feature = "alloc")]
391 #[test]
392 fn string_reference_still_compiles() {
393 let owned = alloc::string::String::from("Owned");
394 let r: &alloc::string::String = &owned;
395 assert_eq!(r.label(), "Owned");
396 assert_eq!(r.kind(), NodeKindTag::Simple);
397 }
398}