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.