Mouse-zone tracker for clickable UIs
Wrap rendered chunks with named markers, let CandyZone discover their bounding boxes, then ask zones whether a MouseMsg fell inside them. Markers are APC escape sequences โ terminals ignore them, so they don't affect layout.
composer require sugarcraft/candy-zone
use SugarCraft\Zone\Manager;
use SugarCraft\Sprinkles\Style;
$z = Manager::newGlobal();
$btnOk = $z->mark('btn:ok', Style::new()->padding(0, 2)->render('OK'));
$btnCancel = $z->mark('btn:cancel', Style::new()->padding(0, 2)->render('Cancel'));
$frame = $btnOk . ' ' . $btnCancel;
// Scan once before printing โ Manager records marker positions and strips them.
$displayable = $z->scan($frame);
echo $displayable;
// Later, when a MouseMsg arrives:
if ($z->get('btn:ok')?->inBounds($mouseMsg)) {
// ...
}
$z->mark('btn:ok', $rendered) wraps the chunk in invisible APC delimiters.$z->scan($frame) records every marker's 1-based cell bounding box and strips them in one pass.$z->get('btn:ok')?->inBounds($mouseMsg) โ clean predicate, no coordinate math.view() call rescans, every MouseMsg routes.ZoneHoverTracker emits ZoneEnterMsg / ZoneExitMsg on cursor boundary crossings.DragTracker tracks press โ move โ release drags across zones; emits ZoneDragStartMsg / ZoneDragMoveMsg / ZoneDragEndMsg.ClickCounter detects double / triple click streaks; emits DoubleClickMsg / TripleClickMsg within a configurable interval.Manager::setMotionTracking(true) returns \x1b[?1003h to enable SGR mode 1003 (all-motion events).| Class | Method | Description |
|---|---|---|
| Manager | newGlobal() | Create global manager |
| Manager | newPrefix(?prefix) | Create prefixed manager for isolation |
| Manager | mark(name, rendered) | Wrap output with zone marker |
| Manager | scan(output) | Record positions, strip markers |
| Manager | anyInBounds(mouseMsg) | Return first zone under the mouse |
| Manager | get(name) | Get zone by name |
| Zone | inBounds(mouseMsg) | Test if mouse is inside zone |
| ZoneHoverTracker | new(manager) | Track hover state over a manager |
| ZoneHoverTracker | update(mouseMsg) | Process mouse event, return enter/exit msg |
| ZoneHoverTracker | currentZone() | Get the hovered Zone or null |
| ZoneEnterMsg | zone | Zone the cursor just entered |
| ZoneExitMsg | zone | Zone the cursor just left |
| DragTracker | new(manager) | Track drag sequences over a manager |
| DragTracker | update(mouseMsg) | Process mouse event, return drag msg |
| DragTracker | originZone() | Get the origin Zone or null |
| DragTracker | currentZone() | Get the current Zone or null |
| ZoneDragStartMsg | originZone / currentZone | Zone where drag started; zone at current cursor |
| ZoneDragMoveMsg | originZone / currentZone | Fixed origin zone; zone cursor just crossed into |
| ZoneDragEndMsg | originZone / currentZone | Zone drag started from; zone at release |
| ClickCounter | new(manager, clickIntervalMs) | Track double/triple click streaks |
| ClickCounter | update(mouseMsg) | Process press event, return double/triple msg |
| ClickCounter | clickCount() | Current streak count (0 = no streak) |
| DoubleClickMsg | zone | Zone of the second press |
| TripleClickMsg | zone | Zone of the third press |
| Manager | setMotionTracking(bool) | Return CSI 1003 h/l escape sequence |
| Style | new() | Create style builder |
| Style | padding(top, bottom) | Set zone padding |
VHS-recorded GIFs of every example shipped with the library. Regenerated automatically on every push that touches the source.