Skip to content

Layout and Overflow Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [x]) syntax for tracking.

Goal: Make nested constrained layouts deterministic without changing core Layout defaults. Architecture: Ext owns bounded allocation and explicit overflow policy; reuse public Measurement, Text and Segment operations. Tech Stack: Rust, checked integer arithmetic, Rich cell/segment primitives. Spec: Section B.

Global Constraints

All index constraints apply. Requires A1. Core ratio_resolve remains untouched; width is measured in terminal cells; zero allocations must not invoke children.

Review Focus

Oversubscribed fixed/content/minimum sizes, exhausted maxima, integer overflow, zero space and grapheme splits are covered by B1/B2.

B1: Bounded allocator

Files: Create crates/rich-ext/src/layout/constraints.rs, crates/rich-ext/src/layout/mod.rs, crates/rich-ext/tests/constraints.rs; modify ext lib.rs.

Interfaces: Constraint { min: usize, max: Option<usize>, preferred: Option<usize>, flex: usize }; Allocation { sizes: Vec<usize>, padding: usize, relaxed: Vec<usize> }; allocate(total: usize, constraints: &[Constraint]) -> Result<Allocation, ConstraintError>. ConstraintError distinguishes invalid bounds, fixed requests outside bounds, invalid weights and arithmetic overflow. A fixed/content request uses preferred Some and flex=0; flex sizing uses preferred None and a positive flex.

  • [x] Write these regression cases, constructing Constraint values directly:
assert_eq!(allocate(10, &fixed_eight_twice).unwrap().sizes, vec![5, 5]);
assert_eq!(allocate(100, &content_eighty_twice).unwrap().sizes, vec![50, 50]);
assert_eq!(allocate(10, &min_eight_twice).unwrap().relaxed, vec![0, 1]);
let capped = allocate(10, &flex_capped_three_four).unwrap();
assert_eq!((capped.sizes, capped.padding), (vec![3, 4], 3));
assert_eq!(allocate(0, &fixed_eight_twice).unwrap().sizes, vec![0, 0]);

Define fixed_eight_twice as two {min:0,max:None,preferred:Some(8),flex:0}; content_eighty_twice uses preferred 80; min_eight_twice uses min 8 and preferred None/flex1; capped uses min0/maxSome(3 or4)/preferredNone/flex1. Also test invalid min/max and usize::MAX requests without panic. - [x] Run cargo test -p rs-rich-ext --test constraints; expect new API missing. - [x] Implement the design's slack/minimum reduction using u128 checked sums and products. Allocate proportional floors, then assign remaining cells in source order only to eligible children. Iteratively cap flex allocation at maxima; return unallocatable remainder as padding. Report every relaxed preferred/min request. Never call core ratio_resolve for a case it cannot bound. - [x] Assert for a deterministic table of totals 0–128 and varied constraints: sum(sizes)+padding=total, no panic and stable results. Run index commit gates; commit feat: add bounded layout constraints.

B2: Nested composition and explicit overflow

Files: Create crates/rich-ext/src/layout/node.rs, crates/rich-ext/src/layout/overflow.rs, crates/rich-ext/tests/layout.rs, crates/rich-ext/examples/layout.rs; update layout/mod.rs, README.md and docs/PLUGINS.md.

Interfaces: Axis::{Horizontal,Vertical}; Alignment::{Start,Center,End}; OverflowPolicy::{Wrap,Fold,Crop,Ellipsis,Visible}; LayoutNode::leaf(Box<dyn Renderable>) -> LayoutNode, LayoutNode::split(axis: Axis, children: Vec<LayoutNode>) -> LayoutNode, builders width(Constraint), height(Constraint), align(Alignment,Alignment), overflow(OverflowPolicy) return Self. LayoutNode implements Renderable. fit_segments(segments: &[Segment], width: usize, policy: OverflowPolicy) -> Vec<Vec<Segment>> is shared with C. Layout constructor/builder validation uses LayoutNode::validate(&self) -> Result<(), ConstraintError>; fallible callers validate first, while an invalid infallible Renderable renders no segments.

  • [x] Test two-column/sidebar/nested-panel layouts at 0/1/2/20/80 cells and heights 0/1/10, using the A1 target. Add styled raw segments and Text containing 界e\u{301} and a tab. Check shape via Segment::cell_length, never string length:
for line in lines {
    assert!(line.iter().map(Segment::cell_length).sum::<usize>() <= width);
}
assert_eq!(fit_segments(&source, 0, OverflowPolicy::Fold), Vec::<Vec<Segment>>::new());

Pin an unbroken word: Wrap uses whitespace breaks then crops an overlong word; Fold splits it across cells; Crop/Ellipsis emit one bounded line; Visible leaves it intact only before a container's final clipping boundary. - [x] Run env -u NO_COLOR cargo test -p rs-rich-ext --test layout; expect missing node/overflow APIs. - [x] Compose child blocks using B1, public Measurement and console.render_lines. Measure content height only after allocating width; cache only within that render pass. Do not dispatch zero-sized children. Flatten segment text and style spans into a Text representation where useful, retaining hyperlinks and using existing cell-aware wrapping/cropping. Never split a combining sequence from its base. Clip final cell blocks, align padding, then tile rows in source order. - [x] Run layout tests plus core golden tests; demonstrate the example at 20/80 cells and capture a styled snapshot. Run index gates and commit feat: compose bounded layouts with explicit overflow.