Terminal overlay compositor
Composite overlay views (modals, tooltips, toasts) on top of a base TUI frame. Render your models first, then let the compositor handle positioning, backdrop dimming, z-ordering, animated transitions and click-outside hit-testing.
composer require sugarcraft/sugar-veil
use SugarCraft\Veil\{Position, Veil, VeilStack};
$veil = Veil::new()->withBackdrop(40); // dim the background to 40%
// Composite a foreground string centered over a background string
echo $veil->composite($modalView, $baseView, Position::CENTER, Position::CENTER);
// Layer several overlays by z-index
$stack = VeilStack::new()
->add(Veil::new()->withContent($modalView)->withZIndex(1))
->add(Veil::new()->withContent($tooltip)->withZIndex(2)
->withPosition(Position::TOP, Position::RIGHT));
echo $stack->compositeAll($baseView);
withAnimation() + animate(…, $progress): SLIDE enters from the anchored edge, SCALE reveals lines from the center outward, and FADE brightens the overlay in from black through a truecolor gray pen (terminals cannot alpha-blend), returning the original bytes at full opacity.maxZIndex() / minZIndex() return null for an empty stack, since 0 is a real z-index.withClickOutsideDismiss() + scan() / isClickOutside() via candy-mouse zones. The scanned zones travel with the veil across later with*() calls, so a click is judged against the frame that was on screen.composite() diffs against the previous frame and emits only changed cells. An empty background returns the overlay itself as the frame (upstream's bg == "" early return), and colours are clamped when the diff pen is built: truecolor SGR components to 0–255, and 256-colour indices to 0–255, so 38;5;300 cannot widen a cell and a negative index no longer falls back to white.withBorder() wraps veil content in a terminal border; withAutoSize() sizes the veil from its content.| Class | Method | Description |
|---|---|---|
| Veil | new() | Create a new veil |
| Veil | composite(foreground, background, Position $vertical, Position $horizontal, int $xOffset = 0, int $yOffset = 0) | Composite one string over another; offsets are cells |
| Veil | animate(foreground, background, $vertical, $horizontal, float $progress, $xOffset = 0, $yOffset = 0) | Composite with the configured animation applied at progress 0..1 |
| Veil | withBackdrop(int) / withAnimation(?AnimationKind) / withZIndex(int) / withPosition(?Position, ?Position, x, y) / withContent(string) / withBorder(?Border) / withAutoSize() / withClickOutsideDismiss() | Immutable configuration; scanned zones survive every with*() |
| Veil | scan(rendered) / hit(col, row) / isClickOutside(MouseMsg) | Zone hit-testing; isClickOutside() throws when dismissal is on but scan() has not run |
| VeilStack | new() / add(Veil) / removeWhere(\Closure) / filter(\Closure) / clear() | Immutable stack of veils |
| VeilStack | composite(background, $vertical, $horizontal, $xOffset = 0, $yOffset = 0) / compositeAll(background) | Render every veil by ascending z-index, at a shared position or each at its own |
| VeilStack | maxZIndex(): ?int / minZIndex(): ?int | Highest / lowest z-index, null for an empty stack |
| VeilStack | sorted() / all() / isEmpty() / count() | Queries (count() via Countable) |
VHS-recorded GIFs of every example shipped with the library. Regenerated automatically on every push that touches the source.