inkferro_core/layout/engine.rs
1//! `LayoutEngine` trait — the stable seam between inkferro and layout backends.
2//!
3//! See `mod.rs` for the measure-seam and rounding rationale.
4
5use taffy::geometry::Size;
6use taffy::style::AvailableSpace;
7
8/// A measure function for a leaf node.
9///
10/// Signature mirrors the closure shape that taffy 0.10's
11/// `compute_layout_with_measure` accepts (see `taffy_tree.rs:912-913`):
12///
13/// ```text
14/// FnMut(known_dimensions, available_space) -> Size<f32>
15/// ```
16///
17/// We drop the `NodeId` and `Style` parameters that taffy passes to the closure
18/// because the measure function is keyed by dom id in the registry; the closure
19/// captures whatever state it needs (M1-4 will capture a text-width table).
20///
21/// `Send` is not required: taffy imposes no thread-safety bound on the measure
22/// function, and the M3 napi layer calls render on the JS main thread by design.
23/// Re-add `+ Send` when a real thread crossing appears.
24pub type MeasureFn = dyn FnMut(Size<Option<f32>>, Size<AvailableSpace>) -> Size<f32> + 'static;
25
26/// The computed position and size of a node in terminal cells.
27///
28/// Coordinates are relative to the node's parent (same contract as yoga's
29/// `getComputedLeft` / `getComputedTop` used by ink's renderer —
30/// `render-node-to-output.ts:129-130`).
31///
32/// * `x`, `y` — signed to accommodate negative values from margins.
33/// * `width`, `height` — unsigned; terminal columns/rows cannot be negative.
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
35pub struct Rect {
36 /// Left edge relative to parent, in terminal columns.
37 pub x: i32,
38 /// Top edge relative to parent, in terminal rows.
39 pub y: i32,
40 /// Width in terminal columns.
41 pub width: u16,
42 /// Height in terminal rows.
43 pub height: u16,
44}
45
46/// Stable interface over a layout backend.
47///
48/// The only required implementation is [`TaffyEngine`]. ADR-1 requires this
49/// trait so a yoga-ffi backend can be swapped in if taffy diverges >10% on the
50/// conformance corpus.
51///
52/// # Error handling
53/// Methods that can encounter an unknown id return `Result<_, String>`. The
54/// string carries a human-readable diagnostic; the caller (RT layer) will
55/// convert it to a JS exception in M3. `computed` returns `Option` because
56/// rendering an unknown id is not fatal — the renderer skips absent nodes.
57pub trait LayoutEngine {
58 /// Register a new node with the engine.
59 ///
60 /// `id` must match the dom arena id so the caller can correlate results.
61 /// Calling `create` with a duplicate id is an error (matches dom `Create`
62 /// no-op logic — the caller should not do it, but the engine must not
63 /// panic).
64 fn create(&mut self, id: u32) -> Result<(), String>;
65
66 /// Update the layout style for `id`.
67 ///
68 /// Style is the dom placeholder for now; M1-3 will add fields.
69 /// The engine must mark the node dirty automatically (taffy does so
70 /// inside `set_style`).
71 fn apply_style(&mut self, id: u32, style: &crate::dom::Style) -> Result<(), String>;
72
73 /// Attach a measure callback to leaf node `id`.
74 ///
75 /// The engine owns the callback; `calculate` will call it for nodes that
76 /// have one registered.
77 ///
78 /// INVALIDATION: the callback snapshots text + wrap mode at build time
79 /// (see `text_measure::build_measure_fn`). The caller MUST call
80 /// `set_measure` again to rebuild it after `Op::SetText` / `Op::SetStyle`
81 /// on the node.
82 fn set_measure(&mut self, id: u32, f: Box<MeasureFn>);
83
84 /// Insert `child` under `parent` at `index`.
85 ///
86 /// `index` past the end appends (matches taffy `insert_child_at_index`
87 /// behavior — out-of-bounds is an error there; we clamp to append to avoid
88 /// panics when the reconciler sends a slightly stale index).
89 fn insert_child(&mut self, parent: u32, child: u32, index: usize) -> Result<(), String>;
90
91 /// Remove `child` from `parent`.
92 ///
93 /// The child node stays in the engine; it is simply detached.
94 fn remove_child(&mut self, parent: u32, child: u32) -> Result<(), String>;
95
96 /// Remove a node from the engine entirely, freeing its taffy node.
97 ///
98 /// Called when the dom arena frees the corresponding slot (`Op::Free`).
99 /// The measure function registered for `id` is also dropped.
100 ///
101 /// Taffy 0.10 `remove(node)` detaches the node from its parent and any
102 /// children it may have (children become orphan taffy leaves — their dom
103 /// `Free` ops arrive separately per the Free-no-cascade contract in
104 /// `dom/mod.rs`). After `destroy` `computed(id)` returns `None`.
105 ///
106 /// Calling `destroy` on an unknown id is a silent no-op (matches ink's
107 /// guard-style error philosophy).
108 fn destroy(&mut self, id: u32);
109
110 /// Mark node `id` dirty so it is recomputed on the next `calculate`.
111 ///
112 /// Normally not needed (style/child changes mark dirty automatically), but
113 /// exposed so the RT layer can force recalculation after a terminal resize.
114 fn mark_dirty(&mut self, id: u32) -> Result<(), String>;
115
116 /// Compute layout for the whole tree rooted at `root_id`.
117 ///
118 /// `viewport_width` maps to `AvailableSpace::Definite(viewport_width)`.
119 ///
120 /// `viewport_height`:
121 /// - `Some(h)` → `AvailableSpace::Definite(h)` (bounded terminal frame, M2).
122 /// - `None` → `AvailableSpace::MaxContent` (unconstrained height, used
123 /// by `render_to_string` — mirrors ink's `calculateLayout(undefined,
124 /// undefined, LTR)` in render-to-string.ts:62-68).
125 fn calculate(
126 &mut self,
127 root_id: u32,
128 viewport_width: f32,
129 viewport_height: Option<f32>,
130 ) -> Result<(), String>;
131
132 /// Return the computed rect for `id` after the last `calculate`.
133 ///
134 /// Returns `None` for unknown ids — rendering an absent node is not fatal
135 /// (the renderer skips it, matching ink's guard-style error philosophy).
136 fn computed(&self, id: u32) -> Option<Rect>;
137}