php-tui/cassowary
A PHP implementation of the Cassowary constraint solver for building flexible terminal interfaces. Add and remove linear equality and inequality constraints with priorities to automatically calculate widget sizes and positions in PHP-TUI layouts.
Install it as a normal Composer dependency:
composer require php-tui/cassowary
The package is a PHP port of the Cassowary linear constraint solver — the same algorithm behind Apple's Auto Layout. You give it variables, declare relationships between them, and it solves for a consistent set of numeric values. It is a pure solving library: no layout, no rendering, no widgets. If you are already using php-tui, it is pulled in transitively by the layout system; adding it directly is only needed when you want to drive layouts yourself.
Look first at:
Solver — the entry point (addConstraint, removeConstraint, addEditVariable, solve).Variable — the unknowns (typically x, y, width, height per widget).Constraint — the factory methods equal, lessThanOrEqual, greaterThanOrEqual, and Constraint::fromExpression(...).Strength — required(), strong(), medium(), weak(); the priority system that lets preferred layouts bend when required ones cannot.First use case — a centered box inside a 80-column terminal:
use PhpTui\Cassowary\{Solver, Variable, Constraint, Strength};
$solver = new Solver();
$left = new Variable('left');
$width = new Variable('width');
$center = new Variable('center');
$solver->addConstraint(Constraint::equal($left, 0)); // required
$solver->addConstraint(Constraint::greaterThanOrEqual($width, 10));
$solver->addConstraint(Constraint::lessThanOrEqual($width, 60));
$solver->addConstraint(
Constraint::equal($center, 40)->withStrength(Strength::required())
);
$solver->addConstraint(
Constraint::equal($left + $width / 2, $center)->withStrength(Strength::strong())
);
$solver->solve();
printf("left=%d width=%d\n", $solver->value($left), $solver->value($width));
Once that compiles you have everything you need to reason about the rest of the package.
One variable set per axis. The most common real-world pattern is to model your layout as four variables (left, top, width, height) per widget, plus derived variables for right = left + width and bottom = top + height. Build these once when a widget is created and reuse them for the widget's lifetime — recreating variables on every frame is the easiest way to make the solver slow.
Separate structural from cosmetic constraints. Structural relationships (siblings touch, parent contains child) should be required. Cosmetic preferences (center it, prefer this widget larger) should be strong or medium. Never make two required constraints that can contradict each other — solver throws rather than returning a compromise.
Wrap the solver in a layout engine. Don't sprinkle Solver calls through your render loop. Build a small Layout class that:
suggestValue() on the edit variables when input arrives.solve() and reads back coordinates for rendering.This is precisely how php-tui itself wires Cassowary in: a render pass mutates edit variables, then solve() updates the layout tree.
Use edit variables for anything interactive. For a resizable pane or a draggable split, prefer addEditVariable($var, $strength) plus suggestValue($var, $target) over removeConstraint/addConstraint pairs. Adding and removing constraints on every frame is significantly slower and can leave floating-point debris across solves.
Cache and re-solve lazily. Cache the solver and the variable graph between frames. Only rebuild from scratch when the widget tree structurally changes (a panel is added/removed). For everything else, mutate edit variables and re-solve — that path is designed to be cheap.
Integrate with php-tui at the widget level. If you are writing your own widget for a php-tui app, expose the usual Layout hint API, and inside the widget's render() resolve your constraints against the rectangle the framework hands you. Do not fight the framework's own layout pass; treat Cassowary as a way to compute internal offsets within your bounding box.
Compose expressions, don't invent new variables. Cassowary lets you write $a + $b*0.5 - $c*2. Prefer expressions like Constraint::equal($right, $parentRight - 1) over introducing an intermediate $margin variable — each variable is solver state you have to keep track of.
UnsatisfiableConstraint it almost always means you declared two required constraints on the same variable with conflicting values. Bisect by removing constraints, or downgrade the suspicious one to strong — the solver will then return a best-effort answer.required means "must hold or fail". strong/medium/weak are preference weights that only interact with each other. If nothing else can bend, the solver will throw, not silently pick the strong one.strong() to everything "important". Reserve strong for one or two constraints per axis; use medium/weak for the rest. If you need a numeric strength, Strength::create(750.0) gives you finer control than the named buckets.$solver->value($var) returns a float. Terminal columns are integers, so round before rendering (round(), not floor() — flooring accumulates drift across nested layouts).new Variable('panel.left'). When the solver throws an unsatisfiable-constraint error, having readable names in the message is the difference between a ten-second fix and a thirty-minute one.suggestValue() on edit variables before solve(), not after. Values suggested after solving are used on the next pass, which produces subtle frame-lag bugs.addEditVariable duplication. Adding the same variable as an edit variable twice (once per component that thinks it owns it) is easy to do in a widget tree. Track owners explicitly or wrap edit-variable registration behind a single manager.NaN, you likely have a constraint referencing a variable you never added any bounded constraint for. Add Constraint::greaterThanOrEqual($var, 0) as a cheap safety net.Constraint to attach metadata (widget ID, debug label) and reading that metadata back from inside your layout engine is the cleanest way to trace which widget produced a bad constraint — the base class is small and the solver only calls into it for the expression and strength.composer.json and read the changelog before upgrading; the constraint API surface is stable but sparse, so any change is likely to break your code rather than go unnoticed.How can I help you explore Laravel packages today?