← All 41 libraries

CandyZone

🎯 CandyZone

Mouse-zone tracker for clickable UIs

port of lrstanley/bubblezone mouse hit-testing regions

CandyZone code coverage

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.

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 sentinel tags via candy-mouse Mark::zone().
Single-pass scan$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.
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(id, content)Wrap output with zone marker
Managerscan(rendered)Record positions, strip markers (same tag rule as candy-mouse Scan)
Managerall() / clear(?id) / close() / setEnabled(bool)Every zone, reset one or all, disable
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.