โ† 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 candycore/candy-zone

Quickstart

use CandyCore\Zone\Manager;
use CandyCore\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 CandyCoreDrop into your Model โ€” every view() call rescans, every MouseMsg routes.

Source & demos

Try the quickstart โ†’