โ† All libraries

CandyZone

๐ŸŽฏ CandyZone

Mouse-zone tracker for clickable UIs

port of bubblezone mouse hit-testing regions

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.

Install

composer require sugarcraft/candy-zone

Quickstart

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)) {
    // ...
}

What's in the box

Marker injection$z->mark('btn:ok', $rendered) wraps the chunk in invisible APC delimiters.
Single-pass scan$z->scan($frame) records every marker's 1-based cell bounding box and strips them in one pass.
Bounds query$z->get('btn:ok')?->inBounds($mouseMsg) โ€” clean predicate, no coordinate math.
ANSI-awareWidth calculation honours SGR + Unicode grapheme widths; emojis, CJK, combining marks all account correctly.
Manager isolationManager::newGlobal() for app-wide; or per-component managers for nested mouse handling.
Plays with SugarCraftDrop into your Model โ€” every view() call rescans, every MouseMsg routes.
Hover trackingZoneHoverTracker emits ZoneEnterMsg / ZoneExitMsg on cursor boundary crossings.
Drag trackingDragTracker tracks press โ†’ move โ†’ release drags across zones; emits ZoneDragStartMsg / ZoneDragMoveMsg / ZoneDragEndMsg.
Click trackingClickCounter detects double / triple click streaks; emits DoubleClickMsg / TripleClickMsg within a configurable interval.
Motion tracking CSIManager::setMotionTracking(true) returns \x1b[?1003h to enable SGR mode 1003 (all-motion events).

Source & demos

Try the quickstart โ†’

API

ClassMethodDescription
ManagernewGlobal()Create global manager
ManagernewPrefix(?prefix)Create prefixed manager for isolation
Managermark(name, rendered)Wrap output with zone marker
Managerscan(output)Record positions, strip markers
ManageranyInBounds(mouseMsg)Return first zone under the mouse
Managerget(name)Get zone by name
ZoneinBounds(mouseMsg)Test if mouse is inside zone
ZoneHoverTrackernew(manager)Track hover state over a manager
ZoneHoverTrackerupdate(mouseMsg)Process mouse event, return enter/exit msg
ZoneHoverTrackercurrentZone()Get the hovered Zone or null
ZoneEnterMsgzoneZone the cursor just entered
ZoneExitMsgzoneZone the cursor just left
DragTrackernew(manager)Track drag sequences over a manager
DragTrackerupdate(mouseMsg)Process mouse event, return drag msg
DragTrackeroriginZone()Get the origin Zone or null
DragTrackercurrentZone()Get the current Zone or null
ZoneDragStartMsgoriginZone / currentZoneZone where drag started; zone at current cursor
ZoneDragMoveMsgoriginZone / currentZoneFixed origin zone; zone cursor just crossed into
ZoneDragEndMsgoriginZone / currentZoneZone drag started from; zone at release
ClickCounternew(manager, clickIntervalMs)Track double/triple click streaks
ClickCounterupdate(mouseMsg)Process press event, return double/triple msg
ClickCounterclickCount()Current streak count (0 = no streak)
DoubleClickMsgzoneZone of the second press
TripleClickMsgzoneZone of the third press
ManagersetMotionTracking(bool)Return CSI 1003 h/l escape sequence
Stylenew()Create style builder
Stylepadding(top, bottom)Set zone padding

Demos.

VHS-recorded GIFs of every example shipped with the library. Regenerated automatically on every push that touches the source.

Two clickable buttons

Two clickable buttons

Mouse-zone bounding boxes around rendered buttons.
List with zones

List with zones

Wrapping every list row in a Zone for click targeting.
Nested components

Nested components

Zones composed across nested rendered components.