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;