Skip to main content

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}