inkferro-core 0.1.0

Layout, text measurement, ANSI render, and frame-diff engine for inkferro — a Rust-backed, byte-for-byte drop-in for the ink terminal UI library.
Documentation
//! `LayoutEngine` trait — the stable seam between inkferro and layout backends.
//!
//! See `mod.rs` for the measure-seam and rounding rationale.

use taffy::geometry::Size;
use taffy::style::AvailableSpace;

/// A measure function for a leaf node.
///
/// Signature mirrors the closure shape that taffy 0.10's
/// `compute_layout_with_measure` accepts (see `taffy_tree.rs:912-913`):
///
/// ```text
/// FnMut(known_dimensions, available_space) -> Size<f32>
/// ```
///
/// We drop the `NodeId` and `Style` parameters that taffy passes to the closure
/// because the measure function is keyed by dom id in the registry; the closure
/// captures whatever state it needs (M1-4 will capture a text-width table).
///
/// `Send` is not required: taffy imposes no thread-safety bound on the measure
/// function, and the M3 napi layer calls render on the JS main thread by design.
/// Re-add `+ Send` when a real thread crossing appears.
pub type MeasureFn = dyn FnMut(Size<Option<f32>>, Size<AvailableSpace>) -> Size<f32> + 'static;

/// The computed position and size of a node in terminal cells.
///
/// Coordinates are relative to the node's parent (same contract as yoga's
/// `getComputedLeft` / `getComputedTop` used by ink's renderer —
/// `render-node-to-output.ts:129-130`).
///
/// * `x`, `y` — signed to accommodate negative values from margins.
/// * `width`, `height` — unsigned; terminal columns/rows cannot be negative.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Rect {
    /// Left edge relative to parent, in terminal columns.
    pub x: i32,
    /// Top edge relative to parent, in terminal rows.
    pub y: i32,
    /// Width in terminal columns.
    pub width: u16,
    /// Height in terminal rows.
    pub height: u16,
}

/// Stable interface over a layout backend.
///
/// The only required implementation is [`TaffyEngine`].  ADR-1 requires this
/// trait so a yoga-ffi backend can be swapped in if taffy diverges >10% on the
/// conformance corpus.
///
/// # Error handling
/// Methods that can encounter an unknown id return `Result<_, String>`.  The
/// string carries a human-readable diagnostic; the caller (RT layer) will
/// convert it to a JS exception in M3.  `computed` returns `Option` because
/// rendering an unknown id is not fatal — the renderer skips absent nodes.
pub trait LayoutEngine {
    /// Register a new node with the engine.
    ///
    /// `id` must match the dom arena id so the caller can correlate results.
    /// Calling `create` with a duplicate id is an error (matches dom `Create`
    /// no-op logic — the caller should not do it, but the engine must not
    /// panic).
    fn create(&mut self, id: u32) -> Result<(), String>;

    /// Update the layout style for `id`.
    ///
    /// Style is the dom placeholder for now; M1-3 will add fields.
    /// The engine must mark the node dirty automatically (taffy does so
    /// inside `set_style`).
    fn apply_style(&mut self, id: u32, style: &crate::dom::Style) -> Result<(), String>;

    /// Attach a measure callback to leaf node `id`.
    ///
    /// The engine owns the callback; `calculate` will call it for nodes that
    /// have one registered.
    ///
    /// INVALIDATION: the callback snapshots text + wrap mode at build time
    /// (see `text_measure::build_measure_fn`). The caller MUST call
    /// `set_measure` again to rebuild it after `Op::SetText` / `Op::SetStyle`
    /// on the node.
    fn set_measure(&mut self, id: u32, f: Box<MeasureFn>);

    /// Insert `child` under `parent` at `index`.
    ///
    /// `index` past the end appends (matches taffy `insert_child_at_index`
    /// behavior — out-of-bounds is an error there; we clamp to append to avoid
    /// panics when the reconciler sends a slightly stale index).
    fn insert_child(&mut self, parent: u32, child: u32, index: usize) -> Result<(), String>;

    /// Remove `child` from `parent`.
    ///
    /// The child node stays in the engine; it is simply detached.
    fn remove_child(&mut self, parent: u32, child: u32) -> Result<(), String>;

    /// Remove a node from the engine entirely, freeing its taffy node.
    ///
    /// Called when the dom arena frees the corresponding slot (`Op::Free`).
    /// The measure function registered for `id` is also dropped.
    ///
    /// Taffy 0.10 `remove(node)` detaches the node from its parent and any
    /// children it may have (children become orphan taffy leaves — their dom
    /// `Free` ops arrive separately per the Free-no-cascade contract in
    /// `dom/mod.rs`).  After `destroy` `computed(id)` returns `None`.
    ///
    /// Calling `destroy` on an unknown id is a silent no-op (matches ink's
    /// guard-style error philosophy).
    fn destroy(&mut self, id: u32);

    /// Mark node `id` dirty so it is recomputed on the next `calculate`.
    ///
    /// Normally not needed (style/child changes mark dirty automatically), but
    /// exposed so the RT layer can force recalculation after a terminal resize.
    fn mark_dirty(&mut self, id: u32) -> Result<(), String>;

    /// Compute layout for the whole tree rooted at `root_id`.
    ///
    /// `viewport_width` maps to `AvailableSpace::Definite(viewport_width)`.
    ///
    /// `viewport_height`:
    /// - `Some(h)` → `AvailableSpace::Definite(h)` (bounded terminal frame, M2).
    /// - `None`    → `AvailableSpace::MaxContent`   (unconstrained height, used
    ///   by `render_to_string` — mirrors ink's `calculateLayout(undefined,
    ///   undefined, LTR)` in render-to-string.ts:62-68).
    fn calculate(
        &mut self,
        root_id: u32,
        viewport_width: f32,
        viewport_height: Option<f32>,
    ) -> Result<(), String>;

    /// Return the computed rect for `id` after the last `calculate`.
    ///
    /// Returns `None` for unknown ids — rendering an absent node is not fatal
    /// (the renderer skips it, matching ink's guard-style error philosophy).
    fn computed(&self, id: u32) -> Option<Rect>;
}