← All libraries

CandyMouse

🎯 CandyMouse

Self-contained Mark/Scan/Get mouse hit-testing

port of bubblezone mouse hit-testing zone-click-track

Own your own Scanner instance — no global Manager wiring required. Wrap rendered chunks with invisible Unicode-sentinel markers, scan to discover bounding boxes, then hit-test mouse events. ZoneClickTracker deduplicates Press/Release pairs so each logical click emits once.

Install

composer require sugarcraft/candy-mouse

Quickstart

use SugarCraft\Mouse\Mark;
use SugarCraft\Mouse\Scanner;
use SugarCraft\Mouse\ZoneClickTracker;
use SugarCraft\Mouse\MouseEvent;

// 1. Wrap interactive content with invisible zone markers.
$rendered = Mark::zone('btn-ok', '  OK  ')
              . Mark::zone('btn-cancel', 'Cancel');

// 2. Scan after rendering to populate the zone registry.
$scanner = Scanner::new()->scan($rendered);

// 3. Reverse-lookup on mouse events.
$zone = $scanner->hit($mouseX, $mouseY); // ?Zone

// 4. Deduplicate clicks so each press+release pair emits one click.
$tracker = new ZoneClickTracker();
$result = $tracker->track(new MouseEvent(5, 1, 0, MouseAction::Release));
if ($result !== null) {
    echo "Clicked zone: " . $result->zone->id;
}

What's in the box

Mark sentinel wrapMark::zone($id, $content) wraps content in invisible U+E000/U+E001 codepoints safe from ANSI SGR clobbering.
Scanner hit-testingScanner::new()->scan($rendered) parses sentinels; hit(col, row) reverse-lookup by coordinates; get(id) lookup by zone id.
Zone bounding boxZone readonly value object with 1-based start/end col/row coordinates matching terminal cell space.
ZoneClickTracker dedupZoneClickTracker state machine per button — suppresses drag spurious presses and mismatched Press+Release on different zones.
No external ManagerScanner is owned by each consumer — no global Manager wiring, no shared state across components.
ANSI-safe sentinelsPrivate-use Unicode codepoints U+E000/U+E001 never collide with CSI or OSC ANSI sequences.
CJK column accountingScanning uses Width::string() from candy-core so wide East-Asian characters account for 2 cell columns each.
ZoneClickTracker improves on bubblezone issue #10Press+Release dedup per zone per button — bubblezone never shipped this fix.

Source & demos

Try the quickstart →

API

ClassMethodDescription
Mark::zone(id, content)Wrap content with invisible sentinel markers
Scanner::new()Create empty scanner
Scanner->scan(rendered)Parse sentinels, build zone registry
Scanner->get(id)Get Zone by id
Scanner->hit(col, row)Reverse-lookup Zone at coordinates
Zone$idZone identifier
Zone$startCol / $startRow / $endCol / $endRow1-based bounding box
MouseEvent::press(x, y, button)Factory for press event
MouseEvent::release(x, y, button)Factory for release event
MouseActionPress / Release / Drag / ScrollMouse action enum
ZoneClickTracker->track(MouseEvent)Feed event, receive ClickResult or null
ZoneClickTracker->setPressZone(Zone)Set zone for pending press (call after track(Press))
ClickResult$zone / $buttonCompleted click result

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.