Skip to main content

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}