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 candy-mouse's invisible private-use sentinels (U+E000 / U+E001), and scan() strips them before the frame is printed, so they never 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 sentinel tags via candy-mouse Mark::zone().$z->scan($frame) records every marker's 1-based cell bounding box and strips them in one pass. It strips exactly the tags candy-mouse's Scan::parse() decodes (ids that pass Mark::isValidId()); any other stray sentinel loses only its own 3 bytes and the text after it is kept, so the printed frame matches the measured one.$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(id, content) | Wrap output with zone marker |
| Manager | scan(rendered) | Record positions, strip markers (same tag rule as candy-mouse Scan) |
| Manager | all() / clear(?id) / close() / setEnabled(bool) | Every zone, reset one or all, disable |
| 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.