Skip to main content

inkferro_core/dom/
node.rs

1//! Node data structures mirroring ink's DOM types (dom.ts:8-93).
2
3/// The four element kinds from ink's `ElementNames` (dom.ts:18-23).
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub enum Kind {
6    /// `ink-root`
7    Root,
8    /// `ink-box`
9    Box,
10    /// `ink-text`
11    Text,
12    /// `ink-virtual-text`
13    VirtualText,
14}
15
16// ─── Style types ─────────────────────────────────────────────────────────────
17
18/// A length/percentage/auto dimension (mirrors Yoga's percent vs. point APIs).
19///
20/// Used for width, height, min/max, flex-basis, and inset properties.
21/// Percentage is stored as 0–100 (ink JS convention); the taffy mapping
22/// divides by 100 to obtain 0.0–1.0.
23#[derive(Debug, Clone, PartialEq, Default)]
24pub enum Dim {
25    /// A fixed size in terminal cells.
26    Points(f32),
27    /// A percentage (0–100) of the parent's corresponding dimension.
28    Percent(f32),
29    /// Automatic sizing (yoga/taffy default).
30    #[default]
31    Auto,
32}
33
34/// A length or percentage (no auto) used for margin/padding/border/gap.
35///
36/// Mirrors yoga's numeric-only API for these properties.
37#[derive(Debug, Clone, PartialEq)]
38pub enum Lp {
39    /// A fixed size in terminal cells.
40    Points(f32),
41    /// A percentage (0–100) of the relevant parent dimension.
42    Percent(f32),
43}
44
45/// `flexDirection` (styles.ts:504–525).
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
47pub enum FlexDir {
48    #[default]
49    Row,
50    Column,
51    RowReverse,
52    ColumnReverse,
53}
54
55/// `flexWrap` (styles.ts:527–540).
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
57pub enum FlexWrap {
58    #[default]
59    NoWrap,
60    Wrap,
61    WrapReverse,
62}
63
64/// `alignItems` / `alignSelf` (styles.ts:552–593).
65///
66/// Used for both `align_items` (`Option<AlignItems>` in taffy) and
67/// `align_self` (`Option<AlignSelf>` in taffy).  Taffy has no explicit
68/// `Auto` variant for `AlignSelf`; `None` in the `Option` encodes auto.
69#[derive(Debug, Clone, Copy, PartialEq, Eq)]
70pub enum Align {
71    Stretch,
72    FlexStart,
73    Center,
74    FlexEnd,
75    Baseline,
76}
77
78/// `alignContent` / `justifyContent` (styles.ts:595–661).
79#[derive(Debug, Clone, Copy, PartialEq, Eq)]
80pub enum ContentAlign {
81    FlexStart,
82    Center,
83    FlexEnd,
84    SpaceBetween,
85    SpaceAround,
86    SpaceEvenly,
87    Stretch,
88}
89
90/// `display` (styles.ts:721–727).
91#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
92pub enum Display {
93    #[default]
94    Flex,
95    None,
96}
97
98/// `borderStyle` — mirrors `keyof Boxes | BoxStyle` from cli-boxes (styles.ts:255 —
99/// type declaration; the `typeof borderStyle === 'string' ? cliBoxes[...] : borderStyle`
100/// ternary lives in render-border.ts:32-34).
101///
102/// Presence (`Some`) means border is active (layout width = 1 per edge); `None`
103/// means no border (width = 0).  The renderer uses the variant to select the
104/// correct box-drawing characters.
105///
106/// * `Named` — one of cli-boxes's named styles ("single", "double", "round", …).
107/// * `Custom` — caller-supplied box-drawing characters matching `BoxStyle` from
108///   cli-boxes.  Eight fields: four corners + four edges.
109#[derive(Debug, Clone, PartialEq)]
110pub enum BorderStyle {
111    /// A named style from cli-boxes (e.g. `"single"`, `"double"`, `"round"`).
112    Named(String),
113    /// A fully custom `BoxStyle` object supplied directly by the caller
114    /// (styles.ts:255 — type declaration; ternary in render-border.ts:32-34).
115    Custom {
116        top_left: String,
117        top: String,
118        top_right: String,
119        right: String,
120        bottom_right: String,
121        bottom: String,
122        bottom_left: String,
123        left: String,
124    },
125}
126
127/// `position` (styles.ts:415–442).
128///
129/// Yoga has three values: `absolute`, `relative`, and `static`.
130/// Taffy only has `Relative` and `Absolute` — `static` maps to `Relative`
131/// (divergence documented in `style_to_taffy`).
132#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
133pub enum Position {
134    #[default]
135    Relative,
136    Absolute,
137    /// Yoga-only; taffy backend maps to `Relative`. The difference is visible
138    /// only when inset (top/right/bottom/left) is set on a static node: yoga
139    /// ignores inset for static (styles.ts:21), but this mapping honors it.
140    /// Accepted: ink's API documents static as ignoring offsets (styles.ts:21)
141    /// and Box never defaults to static.
142    Static,
143}
144
145/// `textWrap` (styles.ts:10-16) — controls text wrapping within an ink-text
146/// node.  Lives on `Style` (not a node attribute) because `dom.ts:242` reads
147/// it as `node.style?.textWrap`; `wrap-text.ts:3` types it `Styles['textWrap']`.
148///
149/// Default (when `None`) is `Wrap`, matching ink's `node.style?.textWrap ?? 'wrap'`
150/// (dom.ts:242).
151#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
152pub enum TextWrap {
153    /// `'wrap'` — soft word-wrap with hard=true, trim=false (wrap-text.ts:21-25).
154    #[default]
155    Wrap,
156    /// `'hard'` — character-level hard wrap, word_wrap=false (wrap-text.ts:27-32).
157    Hard,
158    /// `'truncate'` or `'truncate-end'` — truncate at end (wrap-text.ts:33-47).
159    TruncateEnd,
160    /// `'truncate-middle'` — truncate in the middle.
161    TruncateMiddle,
162    /// `'truncate-start'` — truncate at the start.
163    TruncateStart,
164}
165
166/// `overflow` per-axis (styles.ts; Box.tsx resolves the `overflow` shorthand
167/// to `overflowX`/`overflowY` before reaching the DOM — styles.ts:731–734).
168///
169/// Only `overflowX` and `overflowY` are stored here; the `overflow` shorthand
170/// is resolved JS-side in Box.tsx (Box.tsx:90-92) and never reaches Rust.
171///
172/// styles.ts defines overflow as `'visible' | 'hidden'` only (styles.ts:384/391/398).
173#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
174pub enum Overflow {
175    #[default]
176    Visible,
177    Hidden,
178}
179
180/// The layout style for a DOM node.
181///
182/// Mirrors ink's `Styles` type (styles.ts) with every prop that ink's
183/// `apply*` functions write to yoga (styles.ts:415–777).  Layout-inert
184/// props (text color, background, border colors) are carried as raw strings
185/// for the renderer (M1-5) without influencing the taffy layout pass.
186///
187/// ### Shorthand resolution
188/// * `overflow` shorthand: resolved JS-side in Box.tsx before `setStyle` —
189///   only `overflow_x`/`overflow_y` per-axis values reach Rust.
190/// * `margin`/`marginX`/`marginY`, `padding`/`paddingX`/`paddingY`, `gap`
191///   shorthands: NOT resolved JS-side (yoga handles via `EDGE_ALL` /
192///   `GUTTER_ALL`).  Rust carries these shorthands; the taffy mapping
193///   collapses them with an `Option::or` cascade.
194#[derive(Debug, Clone, PartialEq, Default)]
195pub struct Style {
196    // ── Position (styles.ts:415–442) ──────────────────────────────────────
197    pub position: Option<Position>,
198    pub top: Option<Dim>,
199    pub right: Option<Dim>,
200    pub bottom: Option<Dim>,
201    pub left: Option<Dim>,
202
203    // ── Margin (styles.ts:444–472) ────────────────────────────────────────
204    /// Shorthand — applies to all four edges (yoga `EDGE_ALL`).
205    pub margin: Option<Lp>,
206    /// Horizontal shorthand — left + right (yoga `EDGE_HORIZONTAL`).
207    pub margin_x: Option<Lp>,
208    /// Vertical shorthand — top + bottom (yoga `EDGE_VERTICAL`).
209    pub margin_y: Option<Lp>,
210    pub margin_top: Option<Lp>,
211    pub margin_right: Option<Lp>,
212    pub margin_bottom: Option<Lp>,
213    pub margin_left: Option<Lp>,
214
215    // ── Padding (styles.ts:474–502) ───────────────────────────────────────
216    /// Shorthand — applies to all four edges.
217    pub padding: Option<Lp>,
218    /// Horizontal shorthand — left + right.
219    pub padding_x: Option<Lp>,
220    /// Vertical shorthand — top + bottom.
221    pub padding_y: Option<Lp>,
222    pub padding_top: Option<Lp>,
223    pub padding_right: Option<Lp>,
224    pub padding_bottom: Option<Lp>,
225    pub padding_left: Option<Lp>,
226
227    // ── Flex (styles.ts:504–661) ──────────────────────────────────────────
228    pub flex_direction: Option<FlexDir>,
229    pub flex_wrap: Option<FlexWrap>,
230    pub flex_grow: Option<f32>,
231    pub flex_shrink: Option<f32>,
232    /// `flexBasis` — yoga accepts number (points), percent string, or auto.
233    pub flex_basis: Option<Dim>,
234    pub align_items: Option<Align>,
235    /// `None` encodes `auto` (taffy has no explicit `AlignSelf::Auto` variant).
236    pub align_self: Option<Align>,
237    pub align_content: Option<ContentAlign>,
238    pub justify_content: Option<ContentAlign>,
239
240    // ── Dimensions (styles.ts:663–719) ────────────────────────────────────
241    pub width: Option<Dim>,
242    pub height: Option<Dim>,
243    pub min_width: Option<Dim>,
244    pub min_height: Option<Dim>,
245    pub max_width: Option<Dim>,
246    pub max_height: Option<Dim>,
247    pub aspect_ratio: Option<f32>,
248
249    // ── Display (styles.ts:721–727) ───────────────────────────────────────
250    pub display: Option<Display>,
251
252    // ── Border (styles.ts:729–763) ────────────────────────────────────────
253    /// `borderStyle` value (styles.ts:255, 745).  `Some(_)` → border active,
254    /// layout width = 1 per enabled edge.  `None` → no border (width = 0).
255    /// Renderer uses the variant to select box-drawing characters (M1-5).
256    pub border_style: Option<BorderStyle>,
257    /// `borderTop === false` disables the top border edge (styles.ts:748-749).
258    pub border_top: Option<bool>,
259    pub border_right: Option<bool>,
260    pub border_bottom: Option<bool>,
261    pub border_left: Option<bool>,
262
263    // ── Gap (styles.ts:765–777) ───────────────────────────────────────────
264    /// All-axes shorthand (yoga `GUTTER_ALL`).
265    pub gap: Option<f32>,
266    pub column_gap: Option<f32>,
267    pub row_gap: Option<f32>,
268
269    // ── Text wrap (styles.ts:10-16; dom.ts:242 reads `node.style?.textWrap`) ──
270    /// `textWrap` mode for `ink-text` nodes.  `None` → default `Wrap`
271    /// (matches ink's `?? 'wrap'` fallback at dom.ts:242).
272    pub text_wrap: Option<TextWrap>,
273
274    // ── Overflow per-axis (Box.tsx:90-92 resolves the shorthand JS-side) ──
275    pub overflow_x: Option<Overflow>,
276    pub overflow_y: Option<Overflow>,
277
278    // ── Layout-inert visual props (carried for M1-5 renderer) ─────────────
279    pub background_color: Option<String>,
280    pub border_color: Option<String>,
281    pub border_top_color: Option<String>,
282    pub border_right_color: Option<String>,
283    pub border_bottom_color: Option<String>,
284    pub border_left_color: Option<String>,
285    pub border_background_color: Option<String>,
286    pub border_top_background_color: Option<String>,
287    pub border_right_background_color: Option<String>,
288    pub border_bottom_background_color: Option<String>,
289    pub border_left_background_color: Option<String>,
290
291    // ── Per-edge border dim flags (BOOLEAN) — ink reads these from
292    // `node.style.border{Edge}DimColor`; render-border.ts:54-64 resolves
293    // border{Edge}DimColor ?? borderDimColor and threads `dim` through stylePiece.
294    pub border_dim_color: Option<bool>,
295    pub border_top_dim_color: Option<bool>,
296    pub border_right_dim_color: Option<bool>,
297    pub border_bottom_dim_color: Option<bool>,
298    pub border_left_dim_color: Option<bool>,
299}
300
301impl Style {
302    /// Per-edge border widths `[top, right, bottom, left]` in cells.
303    ///
304    /// Single source for BOTH the taffy layout reservation and the
305    /// render clip inset — these two must never disagree.
306    ///
307    /// Returns 1 for each edge that is active (border_style is `Some` and
308    /// the edge flag is not explicitly `false`), 0 otherwise.
309    pub(crate) fn border_edges(&self) -> [u16; 4] {
310        let on = self.border_style.is_some();
311        let edge = |f: Option<bool>| if on && f != Some(false) { 1u16 } else { 0u16 };
312        [
313            edge(self.border_top),
314            edge(self.border_right),
315            edge(self.border_bottom),
316            edge(self.border_left),
317        ]
318    }
319}
320
321/// Inline text styling for an `ink-text` subtree (P5.1 SET_TEXT_STYLE).  Mirrors
322/// ink's `<Text>` styling props (color, backgroundColor, bold, italic, underline,
323/// strikethrough, inverse, dimColor).
324///
325/// The render walk reads this via `resolve_transform` (render/walk.rs) to compose
326/// SGR natively instead of dispatching a per-line JS transform.  Stored alongside
327/// `has_transform`; a node carries exactly one of the two.  A styled→plain
328/// rerender clears it via `ClearTextStyle` (P6.2 CLEAR_TEXT_STYLE).
329#[derive(Debug, Default, Clone, PartialEq)]
330pub struct TextStyle {
331    pub color: Option<String>,
332    pub background_color: Option<String>,
333    pub bold: bool,
334    pub italic: bool,
335    pub underline: bool,
336    pub strikethrough: bool,
337    pub inverse: bool,
338    pub dim_color: bool,
339}
340
341// ─── Node ────────────────────────────────────────────────────────────────────
342
343/// Attribute value enum mirroring `DOMNodeAttribute` (dom.ts:93).
344///
345/// JS `DOMNodeAttribute = boolean | string | number`; the number variant
346/// uses `f64` to match JS numeric semantics.
347#[derive(Debug, Clone, PartialEq)]
348pub enum AttrValue {
349    Bool(bool),
350    Str(String),
351    Number(f64),
352}
353
354/// A single node in the arena.
355///
356/// Mirrors the union of `DOMElement` + `TextNode` from dom.ts (dom.ts:27-81).
357/// Fields common to both are always present; `text` is meaningful only on
358/// `Text` and `VirtualText` kinds (where ink stores it in a child `#text`
359/// node — folded here per task spec).
360#[derive(Debug, Clone)]
361pub struct Node {
362    pub kind: Kind,
363    pub parent: Option<u32>,
364    pub children: Vec<u32>,
365    /// Text content.  Mirrors `TextNode.nodeValue` (dom.ts:79-81).
366    /// For `Text`/`VirtualText` nodes.  No-op field on `Root`/`Box`.
367    pub text: Option<String>,
368    /// Layout and visual style for this node.
369    pub style: Style,
370    /// String/bool/number attributes — mirrors `DOMElement.attributes`
371    /// (dom.ts:29).  Does NOT include `internal_transform` or
372    /// `internal_static`; those are separate flags per the reconciler
373    /// (reconciler.ts:231-245).
374    pub attributes: Vec<(String, AttrValue)>,
375    /// `internal_static` flag (reconciler.ts:237-244).
376    pub is_static: bool,
377    /// Set by `Hide`/`Unhide` ops.  The actual yoga display effect is JS-side.
378    pub is_hidden: bool,
379    /// `internal_transform` presence flag (reconciler.ts:231-235).
380    /// The transform function itself stays JS-side per the FFI design.
381    pub has_transform: bool,
382    /// Inline text styling (P5.1 SET_TEXT_STYLE).  Read by the render walk via
383    /// `resolve_transform` to compose SGR natively.  `None` until a `SetTextStyle`
384    /// op writes it; reset to `None` by `ClearTextStyle` on a styled→plain
385    /// rerender (P6.2).
386    pub text_styling: Option<TextStyle>,
387}
388
389impl Node {
390    pub fn new(kind: Kind) -> Self {
391        Self {
392            kind,
393            parent: None,
394            children: Vec::new(),
395            text: None,
396            style: Style::default(),
397            attributes: Vec::new(),
398            is_static: false,
399            is_hidden: false,
400            has_transform: false,
401            text_styling: None,
402        }
403    }
404}