Skip to main content

inkferro_core/layout/
mod.rs

1//! Layout engine trait and Taffy backend.
2//!
3//! # Design decisions
4//!
5//! ## Trait surface (plan adaptation)
6//! The plan listed `set_measure(id, …)` as part of the trait.  Taffy 0.10's
7//! measure story is a *closure* passed to `compute_layout_with_measure` rather
8//! than a per-node callback registered up-front (see `taffy_tree.rs:905-922`
9//! and the `measure.rs` example).  Putting `set_measure` on the *trait* would
10//! force every backend to own a registry, but the *signature* of the stored
11//! function is backend-specific.  Therefore:
12//!
13//!   - The **trait** has `set_measure(id, Box<dyn MeasureFn>)` where
14//!     `MeasureFn` is a type alias for the concrete closure shape that taffy
15//!     needs.  Any yoga-ffi backend that has the same terminal-cell measure
16//!     contract can implement the same trait.  If a future backend needs a
17//!     different shape, the trait gets a second method; that is the narrowest
18//!     breaking change possible.
19//!   - `TaffyEngine` owns a `HashMap<u32, Box<dyn MeasureFn>>` keyed by dom
20//!     node id.  `calculate` passes a single closure to
21//!     `compute_layout_with_measure` that dispatches into that map.
22//!   - M1-4 wires real measurement by calling `set_measure` and does NOT need
23//!     to touch the trait definition.
24//!
25//! ## Rounding (yoga-compatible pixel-grid post-pass)
26//! `render-node-to-output.ts:129-130` uses the yoga-computed values directly
27//! as array column/row indices, with no `Math.round`/`floor`/`ceil`:
28//!
29//! ```text
30//! const x = offsetX + yogaNode.getComputedLeft();   // line 129
31//! const y = offsetY + yogaNode.getComputedTop();    // line 130
32//! ```
33//!
34//! Ink trusts yoga's own pixel rounding, which yoga applies *after* the flex
35//! solve in `roundLayoutResultsToPixelGrid` (yoga 3.2.1
36//! `yoga/algorithm/PixelGrid.cpp`).  Taffy 0.10 also rounds when
37//! `use_rounding = true`, but with a *different* rule (cumulative
38//! round-half-away in `compute::round_layout`, taffy `compute/mod.rs:219`).
39//! That difference produces ±1 divergences against the ink oracle on tie-breaks
40//! (e.g. a 3.5-cell leading space: yoga floors a text node's position to 3,
41//! taffy rounds to 4) and on text-node sizing.
42//!
43//! We therefore (M1-7):
44//!
45//!   1. Disable taffy's rounding (`TaffyTree::disable_rounding()` in
46//!      `TaffyEngine::new`); `tree.layout()` then returns unrounded floats.
47//!   2. After `compute_layout_with_measure`, run a faithful port of
48//!      `roundLayoutResultsToPixelGrid` / `roundValueToPixelGrid`
49//!      (`taffy_engine.rs` `round_node` / `round_value_to_pixel_grid`),
50//!      specialized to `pointScaleFactor == 1.0`, in f64 with yoga's
51//!      `inexactEquals` epsilon (`0.0001`).  The recursion carries *unrounded*
52//!      absolute offsets; positions round from the parent-relative value;
53//!      dimensions round as `round(absRight) - round(absLeft)`.  Nodes with a
54//!      registered measure fn are treated as yoga `NodeType::Text` (floored, not
55//!      rounded down, so glyphs are never truncated).
56//!   3. Store the rounded, parent-relative integer rects keyed by dom id;
57//!      `computed()` returns from that map (falling back to the live taffy
58//!      layout only for a node that was never laid out).  `x`/`y` are `i32`
59//!      (parent-relative, can be negative with margins); `width`/`height` are
60//!      `u16` (terminal cells fit).
61
62mod engine;
63mod taffy_engine;
64
65pub use engine::{LayoutEngine, MeasureFn, Rect};
66pub use taffy_engine::TaffyEngine;