Constraint-based layout solver for terminal grids
A ratatui-style constraint layout solver behind a one-method LayoutSolver interface: GreedySolver (deterministic 5-phase solver ported from candy-sprinkles) implements every constraint type, and the deprecated CassowarySolver shim delegates to it. Plus DockLayout, a center-plus-side-stacks pane geometry. Foundation for candy-sprinkles, sugar-bits, and candy-forms layout.
composer require sugarcraft/candy-layout
use SugarCraft\Layout\Constraint\Constraint;
use SugarCraft\Layout\{Direction, GreedySolver, Region};
$solver = GreedySolver::new();
$region = Region::fromSize(100, 24);
// The widths in the comments are what this 100-cell split returns.
$rects = $solver->solve($region, Direction::Horizontal, [
Constraint::length(20), // 20: exactly 20 cells
Constraint::min(10), // 10: a floor; the Fills below take the slack
Constraint::percentage(25), // 25: 25% of 100
Constraint::ratio(1, 10), // 10: 1/10 of 100
Constraint::fill(1), // 12: a third of the 35 cells left (+1 rounding cell, first Fill)
Constraint::fill(2), // 23: two thirds of them
]);
// Max is a greedy ceiling: it grows into free space up to its cap.
$capped = $solver->solve($region, Direction::Horizontal, [
Constraint::length(20), // 20
Constraint::max(15), // 15: clamped at its ceiling
Constraint::fill(1), // 65: everything Max did not take
]);
withoutOverflowTruncation() keeps full base sizes instead.solve() raises E_USER_DEPRECATED and delegates wholly to GreedySolver — results are identical. Kept only so existing call-sites keep working.InvalidArgumentException where it is introduced, and resolve() itself never throws, degrading side by side on small frames.LayoutSolver (solve() alone; construction lives on the concrete classes) is the only public contract.| Class | Method | Description |
|---|---|---|
| LayoutSolver | solve(Region, Direction, list<Constraint>) | Solve constraints against a region in the given direction; returns list of sub-regions |
| GreedySolver | new(): self / compat(): self | Default factory / sugar-boxer parity mode (round-split, remainder-to-last, non-truncating overflow) |
| GreedySolver | withRoundSplit() / withRemainderToLast() / withoutOverflowTruncation() / withMinShare(cells, reserveGap, reserveLead) | Immutable rounding and overflow options |
| GreedySolver / CassowarySolver | static greedy() / static cassowary() | Concrete-class factories (no longer declared on the LayoutSolver interface) |
| CassowarySolver | new(): self | Deprecated shim; solve() raises E_USER_DEPRECATED and delegates to GreedySolver |
| Direction | Horizontal, Vertical | Enum: split direction |
| Region | fromSize(int, int) | Factory: width, height |
| Constraint | length(int) | Fixed cell count |
| Constraint | min(int) | Floor — at least n cells |
| Constraint | max(int) | Ceiling — at most n cells |
| Constraint | fill(int $weight) | Proportional remainder with weight |
| Constraint | percentage(int 0-100) | Percentage of total |
| Constraint | ratio(int $num, int $denom) | Fractional proportion of the total span |