sugarcraft / candy-mouse
Self-contained Mark/Scan/Get mouse hit-testing (bubblezone pattern) plus ZoneClickTracker for press/release deduplication. No external Manager wiring needed.
Requires
- php: ^8.3
- sugarcraft/candy-core: dev-master
Requires (Dev)
- phpunit/phpunit: ^10.5
This package is auto-updated.
Last update: 2026-07-13 00:24:40 UTC
README
Self-contained Mark/Scan/Get mouse hit-testing (bubblezone pattern) plus ZoneClickTracker for press/release deduplication. No external Manager wiring needed.
composer require sugarcraft/candy-mouse: dev-master
Role
Replaces the model where consumers wire candy-zone's Manager externally. Each consumer owns its own Scanner instance — click handling stays local, no shared global state.
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; }
Key classes
| Class | Role |
|---|---|
Mark |
Wrap content with invisible Unicode sentinel markers |
Scanner |
Parse sentinels; get(id) and hit(col, row) lookups |
Zone |
Readonly bounding box (start/end col/row) |
ZoneClickTracker |
Press+Release dedup per button |
MouseEvent |
Immutable event (x, y, button, action enum) |
MouseAction |
Press / Release / Drag / Scroll enum |
Sentinel design
Sentinels use private-use codepoints U+E000 (open) and U+E001 (close) — they never collide with ANSI SGR sequences or regular text. Scanning strips them from output.
Multi-row zones
A zone spanning multiple rows (e.g. "line1\nline2") is stored as the smallest axis-aligned bounding box that contains all marked cells. The inBounds() check uses this rectangle, so an interior cell that was never part of the original content may still report as inside the zone. Callers wrapping reflowed or indented text should be aware that the hit-test is approximate for multi-line content.
See also
candy-zone is the TEA-facing façade (Manager, hover /
drag / multi-click trackers, package-level Zones) layered over this
low-level Mark/Scan/Zone primitive. It delegates its marker wrapping and
bounding-box scanning to SugarCraft\Mouse — reach for candy-zone when you
want the Bubble Tea-style zone workflow, and for candy-mouse directly when you
just need self-contained hit-testing.
Coverage
Upstream
Inspired by lrstanley/bubblezone — the Mark/Scan/Get pattern mirrors bubblezone's API. ZoneClickTracker addresses bubblezone issue #10.