PHPackages                             sugarcraft/candy-zone - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [CLI &amp; Console](/categories/cli)
4. /
5. sugarcraft/candy-zone

ActiveLibrary[CLI &amp; Console](/categories/cli)

sugarcraft/candy-zone
=====================

PHP port of lrstanley/bubblezone — mouse zone tracker for TUI apps.

13.2k↑5275%1PHP

Since Jun 29Pushed 1mo agoCompare

[ Source](https://github.com/sugarcraft/candy-zone)[ Packagist](https://packagist.org/packages/sugarcraft/candy-zone)[ RSS](/packages/sugarcraft-candy-zone/feed)WikiDiscussions master Synced 3w ago

READMEChangelogDependenciesVersions (1)Used By (1)

[![candy-zone](.assets/icon.png)](.assets/icon.png)

CandyZone
=========

[](#candyzone)

[![CI](https://github.com/detain/sugarcraft/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/detain/sugarcraft/actions/workflows/ci.yml)[![codecov](https://camo.githubusercontent.com/7aa3eb82582f0f441323162ade2c462a43852825b4552753ffddfc2066e6d471/68747470733a2f2f636f6465636f762e696f2f67682f64657461696e2f737567617263726166742f6272616e63682f6d61737465722f67726170682f62616467652e7376673f666c61673d63616e64792d7a6f6e65)](https://app.codecov.io/gh/detain/sugarcraft?flags%5B0%5D=candy-zone)[![Packagist Version](https://camo.githubusercontent.com/dfdc3dd7e5f7ec665cc7d1f832104305e9c18b9ee39bb91ddf4589170dd4aca4/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f737567617263726166742f63616e64792d7a6f6e653f6c6162656c3d7061636b6167697374)](https://packagist.org/packages/sugarcraft/candy-zone)[![License](https://camo.githubusercontent.com/7013272bd27ece47364536a221edb554cd69683b68a46fc0ee96881174c4214c/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4d49542d626c75652e737667)](LICENSE)[![PHP](https://camo.githubusercontent.com/e78ffc83837c0d12647811a7fd1910c3cbeae04988de94bb4fd5b67e0874696a/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f7068702d254532253839254135382e312d3838393262662e737667)](https://www.php.net/)

PHP port of [lrstanley/bubblezone](https://github.com/lrstanley/bubblezone) — mouse-zone tracker for TUI apps. Wrap rendered chunks with named markers, let CandyZone discover their bounding boxes, then ask zones whether a {@see \\SugarCraft\\Core\\Msg\\MouseMsg} fell inside them.

```
composer require sugarcraft/candy-zone
```

```
use SugarCraft\Zone\Manager;
use SugarCraft\Sprinkles\Style;

$z = Manager::newGlobal();

// Build a frame
$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)) {
    // ...
}
```

Markers are APC escape sequences (`ESC _ ... ESC \`) — terminals ignore them, so they don't affect layout. {@see Manager::scan()} computes each zone's bounding box in 1-based terminal cells, accounting for ANSI styling and Unicode width.

Manager API
-----------

[](#manager-api)

Beyond `mark()` / `scan()` / `get()`:

- `setEnabled(bool)` / `isEnabled()` — flip marker emission off in non-interactive contexts (CI logs, file dumps). When off, `mark()`returns content verbatim and `scan()` is identity.
- `Manager::newPrefix(?string)` — namespace every id with a prefix so two CandyZone-aware components don't collide on `'item-0'`. Auto- generates a monotonic prefix when called bare.
- `prefix()` — read-only accessor for the prefix string.
- `get($id)` / `all()` / `clear(?$id)` — single-zone lookup, every zone, and targeted-or-wipe-all clear.
- `close()` — drop every zone + flip the manager into pass-through mode. Idempotent. PHP synchronous-scan has no worker to stop, so this is purely a state cleanup.

Package-level facade
--------------------

[](#package-level-facade)

`SugarCraft\Zone\Zones` mirrors bubblezone's package-level surface (`bubblezone.DefaultManager` + `Mark` / `Scan` / `Clear` / `Get` / `Close` / `SetEnabled` / `Enabled` / `NewPrefix` / `AnyInBounds*`) as static methods over a single shared `Manager`:

```
use SugarCraft\Zone\Zones;

$marked = Zones::mark('header', $header);
$cleaned = Zones::scan($marked);
if (Zones::get('header')?->inBounds($mouse)) { /* … */ }
```

`Zones::setDefaultManager(?Manager)` swaps in a custom manager — useful in tests (`Zones::setDefaultManager(null)` flushes state) or when you want every package-level call routed through a prefixed manager.

Hover tracking
--------------

[](#hover-tracking)

`ZoneHoverTracker` wraps a `Manager` and tracks which zone the cursor is in across `MouseMsg` events. It emits `ZoneEnterMsg` when the cursor crosses into a zone and `ZoneExitMsg` when it leaves — ideal for tooltips, highlights, or data-fetch-on-hover:

```
use SugarCraft\Zone\Manager;
use SugarCraft\Zone\ZoneHoverTracker;
use SugarCraft\Zone\Msg\ZoneEnterMsg;
use SugarCraft\Zone\Msg\ZoneExitMsg;

$tracker = new ZoneHoverTracker($manager);
// $manager must already have run scan() to populate zone registry.

[$tracker, $msg] = $tracker->update($mouseMsg);
if ($msg instanceof ZoneEnterMsg) {
    // cursor entered $msg->zone
} elseif ($msg instanceof ZoneExitMsg) {
    // cursor left $msg->zone
}
```

**Boundary crossing:** moving directly from zone A to zone B produces an exit for A first; call `update()` again to receive the enter for B. This two-step pattern lets the Program animate the exit before routing the enter.

**State accessors:**

- `currentZoneId()` — id of the hovered zone, or null
- `currentZone()` — `Zone` object, or null
- `withManager(Manager)` — rebind to a different manager (e.g. a prefixed manager in a sub-component)
- `withCurrentZoneId(string)` — restore from a serialized state

Drag tracking
-------------

[](#drag-tracking)

`DragTracker` wraps a `Manager` and tracks press → move → release drag sequences within and across zones. It emits a `ZoneDragStartMsg` on button-down inside a zone, `ZoneDragMoveMsg` when the cursor crosses a zone boundary while dragging, and `ZoneDragEndMsg` on button release:

```
use SugarCraft\Zone\Manager;
use SugarCraft\Zone\DragTracker;
use SugarCraft\Zone\Msg\ZoneDragStartMsg;
use SugarCraft\Zone\Msg\ZoneDragMoveMsg;
use SugarCraft\Zone\Msg\ZoneDragEndMsg;

$tracker = new DragTracker($manager);
// $manager must already have run scan() to populate zone registry.

[$tracker, $msg] = $tracker->update($mouseMsg);
if ($msg instanceof ZoneDragStartMsg) {
    // drag started in $msg->originZone
} elseif ($msg instanceof ZoneDragMoveMsg) {
    // cursor crossed from $msg->originZone into $msg->currentZone
} elseif ($msg instanceof ZoneDragEndMsg) {
    // drag ended; started at $msg->originZone, released at $msg->currentZone
}
```

**Origin vs. current zone:** the origin zone is fixed for the entire drag and never changes. The current zone updates whenever the cursor crosses a zone boundary during the drag.

**Boundary crossing:** moving directly from zone A to zone B while dragging produces a move message for A first; call `update()` again to receive the move for B. This two-step pattern lets the Program animate the transition before routing the next enter.

**State accessors:**

- `originZoneId()` / `originZone()` — zone the drag started from, or null
- `currentZoneId()` / `currentZone()` — zone the cursor is in, or null
- `withManager(Manager)` — rebind to a different manager
- `withZoneIds(?string $origin, ?string $current)` — restore from a serialized state

Click tracking
--------------

[](#click-tracking)

`ClickCounter` wraps a `Manager` and tracks double/triple click streaks inside zones. It emits `DoubleClickMsg` on the second press and `TripleClickMsg` on the third press — all within a configurable click interval (default 500 ms). The streak resets when the interval expires or when the cursor moves to a different zone:

```
use SugarCraft\Zone\Manager;
use SugarCraft\Zone\ClickCounter;
use SugarCraft\Zone\Msg\DoubleClickMsg;
use SugarCraft\Zone\Msg\TripleClickMsg;

$counter = new ClickCounter($manager);
// $manager must already have run scan() to populate zone registry.

[$counter, $msg] = $counter->update($mouseMsg);
if ($msg instanceof DoubleClickMsg) {
    // second press in same zone within interval
} elseif ($msg instanceof TripleClickMsg) {
    // third press in same zone within interval
}
```

**State accessors:**

- `clickCount()` — current streak count (0 when no streak is active)
- `withManager(Manager)` — rebind to a different manager
- `$counter->manager` / `$counter->clickIntervalMs` — public constructor params for rebinding / tuning

Motion tracking escape sequences
--------------------------------

[](#motion-tracking-escape-sequences)

`Manager::setMotionTracking(bool $on)` returns the terminal escape sequence that enables (`\x1b[?1003h`) or disables (`\x1b[?1003l`) SGR mouse mode 1003 (all motion events). Write the returned string to the TTY to activate motion reporting before processing mouse move events. This manager does not directly emit — it is a text-processing component that produces the raw CSI sequence.

Tips
----

[](#tips)

- Each id should be unique within a `Manager`. Use `Manager::newPrefix()` per UI sub-tree so two child models don't shadow each other's ids.
- Run `scan()` once on the **full root frame**, not per sub-tree — nested zone bounds depend on the outer layout.
- `lipgloss.Width()` (CandySprinkles) and CandyZone interact cleanly: `scan()` strips markers before measurement.
- `Zone::isZero()` distinguishes "never rendered" from "rendered but empty bounding box".
- Organic shapes (ASCII art) report a rectangular bounding box — the marker pair only carries 4 corners' worth of information.
- The PHP port has a synchronous `scan()` (no background worker), so `close()` is purely a state reset / disable rather than a thread join.

API summary
-----------

[](#api-summary)

ClassMethodDescription`Manager``newGlobal()`Create global manager`Manager``newPrefix(?prefix)`Create prefixed manager for isolation`Manager``mark(name, rendered)`Wrap output with zone marker`Manager``scan(output)`Record positions, strip markers`Manager``anyInBounds(mouseMsg)`Return first zone under the mouse`Manager``get(name)`Get zone by name`Manager``setMotionTracking(bool)`Return CSI 1003 h/l escape sequence`Zone``inBounds(mouseMsg)`Test if mouse is inside zone`ZoneHoverTracker``new(manager)`Track hover state over a manager`ZoneHoverTracker``update(mouseMsg)`Process mouse event, return enter/exit msg`ZoneHoverTracker``currentZone()`Get the hovered Zone or null`ZoneEnterMsg``zone`Zone the cursor just entered`ZoneExitMsg``zone`Zone the cursor just left`DragTracker``new(manager)`Track drag sequences over a manager`DragTracker``update(mouseMsg)`Process mouse event, return drag msg`DragTracker``originZone()`Get the origin Zone or null`DragTracker``currentZone()`Get the current Zone or null`ZoneDragStartMsg``originZone / currentZone`Zone where drag started; zone at current cursor`ZoneDragMoveMsg``originZone / currentZone`Fixed origin zone; zone cursor just crossed into`ZoneDragEndMsg``originZone / currentZone`Zone drag started from; zone at release`ClickCounter``new(manager, clickIntervalMs)`Track double/triple click streaks`ClickCounter``update(mouseMsg)`Process press event, return double/triple msg`ClickCounter``clickCount()`Current streak count (0 = no streak)`DoubleClickMsg``zone`Zone of the second press`TripleClickMsg``zone`Zone of the third pressTest
----

[](#test)

```
cd candy-zone && composer install && vendor/bin/phpunit
```

###  Health Score

26

—

LowBetter than 41% of packages

Maintenance59

Moderate activity, may be stable

Popularity24

Limited adoption so far

Community8

Small or concentrated contributor base

Maturity11

Early-stage or recently created project

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

### Community

Maintainers

![](https://www.gravatar.com/avatar/b1036e0717211b8030b83cbe729e8ba6ba442fdbd5285fb97a39d7dcfe339342?d=identicon)[detain](/maintainers/detain)

---

Top Contributors

[![detain](https://avatars.githubusercontent.com/u/1364504?v=4)](https://github.com/detain "detain (68 commits)")

---

Tags

bubbleteabubblezonebubblezone-portcandycorecell-mouseclickclickable-zoneshyperlinkinteractivemousemouse-eventsmouse-trackingpointersgr-mouseterminaltuiurxvt-mousezones

### Embed Badge

![Health badge](/badges/sugarcraft-candy-zone/health.svg)

```
[![Health](https://phpackages.com/badges/sugarcraft-candy-zone/health.svg)](https://phpackages.com/packages/sugarcraft-candy-zone)
```

###  Alternatives

[illuminate/console

The Illuminate Console package.

13046.0M6.8k](/packages/illuminate-console)[styleci/cli

The CLI tool for StyleCI

71470.5k9](/packages/styleci-cli)[winbox/args

Windows command-line formatter

20720.9k21](/packages/winbox-args)[mallardduck/laravel-traits

A collection of useful Laravel snippets in the form of easy to use traits.

136.2k2](/packages/mallardduck-laravel-traits)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
