You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
UI: clear docs with examples, UISpriteElement on UIBaseElement, presses stop at UI elements, hover and widget fixes - #1815
Follow-up to the 20.13.0 UI update (#1808), before 20.13.0 is released.
1. Docs and skill
API docs for every UI class (UIBaseElement, UITextButton, UISpriteElement, UICheckbox, UISlider, UIPanel, ProgressBar):
Each class doc opens with what the widget is for, then short sections per feature.
Every class has several focused examples.
Every setting has its unit and @default, checked against the constructor.
About ten docs that contradicted the code now match it.
The docs build with no new warnings from these classes.
melonjs-ui-and-text skill:
a "which widget for what" table;
one complete options menu example (panel, layout, checkbox, slider, button, shadow, bounce, keyboard / gamepad focus);
code for drop shadows, a scrolling level list, radio groups, skinned checkboxes and sliders, stepped and vertical sliders, and a custom focusable widget;
new troubleshooting rows.
The skill's examples were run against the engine.
2. A press stops at a UI element by default
UIBaseElement's onClick() and onRelease() now return false, as UISpriteElement's already did.
A click on a button, checkbox, slider or custom widget no longer also reaches the game behind it. A handler returns true to let it through.
Hover is unchanged. A non-blocking UIPanel still lets presses on its background through.
Two example buttons that returned true now return false.
3. UISpriteElement is a UIBaseElement holding a Sprite
It was a Sprite with its own copy of the pointer handling, which had drifted from UIBaseElement's. It now extends UIBaseElement and holds its image as sprite.
What it gains:enabled (drawn at half opacity when off), cancelPress() / onCancel(), hoverScale / pressScale, dragging, and keyboard / gamepad focus.
Kept for existing code:
(x, y) is still where the image's anchor point goes.
setRegion (which now also resizes the element), addAnimation, setCurrentAnimation and isCurrentAnimation stay on the element.
UISpriteElementSettings is exported.
Changed (in the changelog):
Other Sprite members move to .sprite.
pos is the element's top-left corner.
anchorPoint goes in the settings; setting it after construction no longer moves the image.
A second image drawn in a draw() override becomes a child.
The matter platformer's touch controls follow: the jump button passes its anchorPoint in the settings, and the joypad's knob is a child.
4. Hover fixes in pointerevent.ts
Stuck hover after a click. A pointerup used to make the region forget the pointer. Leaving straight after a click never called onOut, so a button stayed lit, and moving on over it called onOver again. The release now keeps the pointer.
Leaving the canvas. The browser's pointerleave (the pointer leaves the canvas, or a finger lifts) now releases every region holding that pointer, wherever the exit is reported. It used to reach only a region at that point.
Touch is unchanged: a tap hovers while the finger is down, not after.
5. Widget fixes found while writing the docs
Radio groups exist from creation. A radio group now holds one choice from when its checkboxes are made, not only once they are in the scene. Before, several could be checked at once until they were added, and UICheckbox.checkedIn() answered undefined. A checkbox leaves its group when destroyed; groups hold members through WeakRef, so one dropped without being destroyed can still be collected.
UICheckbox#toggle() on a checked radio button leaves it checked, as a click does. It used to leave the group with nothing checked.
UIPanel#scrollTo() (and scrollToChild, scrollX / scrollY) stops a fling that is still coasting.
UISlider lays out its texts again whenever they change:
a vertical slider widens for a longer value text (valueFormat, min, max or step set later);
a horizontal slider moves the value below the track when it would run into the caption;
the new labelText sets the caption later.
bindKey cancel: a held bindKey press called off (the window loses focus, or the button is removed or disabled) now calls onCancel(), as a pointer press does.
How it was checked
New tests/ui-spriteelement.spec.js: 17 tests of UISpriteElement's public behaviour: placement, presses, hover, leaving while held, isClickable, removal, blocking, a subclass like the demos' buttons, and the Sprite shortcuts. They pass on the new class, on the Sprite-based one from before the rewrite, and on master's.
New tests in ui-widgets.spec.js, each fix's test checked to fail without it:
Hover: leaving after a click, moving on after a click, repeated clicks, taps with new pointer ids, and leaving the canvas.
Presses stopping at UI elements: buttons, checkboxes and sprite elements, a handler returning true, and a non-blocking panel.
Widget fixes: radio groups before the scene, leaving a group on destroy, the radio toggle(), scrollTo() during a fling, the slider's labelText, widening, and value placement, and the cancelled bindKey press.
New UISpriteElement: placement, disabled drawing, setRegion, bounce, cancel, focus.
The wheel over a slider in a scrolling panel, which had no test: it moves the slider, not the panel.
Suites: UI, pointer, input, container, draggable, progress bar and sprite suites pass. Gamepad plugin: 46 pass.
Builds and lint:tsc, eslint, biome, the engine build (including the declaration check) and the docs build pass.
In a browser:
UI example: checkbox, slider, and scrolling the level list by dragging from a button.
Platformer HUD icons: they light up on hover.
Matter platformer touch controls: placement, holding the joypad moves the player and the knob, and the jump button.
Left for a separate PR: on WebGL and WebGPU, clipRect replaces the outer clip instead of intersecting with it, so a scrolling panel inside another clipping container is cropped to its own area only. Canvas gets it right. The fix touches clipping for everything those renderers draw, so it gets its own PR and tests.
- UICheckbox: a checkbox with its label, drawn or skinned, a radio button
with a `group`; `onChange` only on user input
- UISlider: horizontal or vertical, drag, track click, wheel, step, caption
and value text, drawn or skinned; `onChange` and `onChangeEnd`
- UITextButton: `onClick` setting on a completed click, atlas `skin` with
lowered pressed art, `enabled`, `pressedColor`, CSS fonts
- UIPanel: nine-slice `skin`, `title`, `draggable`, `layout`
(column/row/grid, padding, gap, align, autoSize) and `scroll` (wheel,
drag with momentum, scrollbar, scrollTo/scrollToChild)
- UIBaseElement: cancelPress/onCancel, so a drag on a button in a
scrolling panel scrolls instead of clicking
- Input: children cropped by a clipping container, or scrolled out of a
panel, no longer get pointer events there
- The UI example uses the widgets, with volume sliders wired to audio and
a scrolling level list; skill sections and changelog entries
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JnzwA4xBNBw7cEZzG1bUru
- UIBaseElement: a draggable element pressed with no hover first, as a
touch tap is, no longer throws, and drags by touch
- UIPanel: a draggable panel is moved by its title or frame, not through
a widget on it, so dragging it by a button no longer clicks the button
- UIPanel: no fling while the pointer is still down; autoSize counts the
title and frame from a zero height; a scrolling panel inside another
scrolls alone; emptying or destroying a panel does not lay it out per
child or call onScroll
- UISlider: the wheel to the right raises it; a drag also ends on a
mouse or touch release, or a release over the slider
- UICheckbox: as wide as its longer label, whichever it is
- UITextButton: a font given as fontData, an image, or with only its
image loaded is a bitmap font as before; pressedColor defaults to
hoverOnColor; bitmapText can be assigned
- 10 regression tests
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JnzwA4xBNBw7cEZzG1bUru
API, from a consistency review against the existing UI widgets:
- UITextButton: width/height, fontSize (pixels, bitmap fonts too),
labelFillStyle and borderColor, the names the other widgets use; the old
names still work. Settings are no longer written back. Colours take any
Paint. pressedColor left undefined follows hoverOnColor. onClickCallback
(was clickCallback). bitmapText keeps its BitmapText type, deprecated
- UICheckbox: boxSize, boxHoverColor; labelOn/labelOff settable and
refitting; group changes apply at once; skin hoverOn/hoverOff and
disabledOn/disabledOff
- UISlider: valueFormat(value, ratio) (was format), ratio, settable
min/max/step, thumbHoverColor, thumbBorderColor, thumbBorderWidth
- UIBaseElement: enabled shared by every UI element; keepsDrag public
- ProgressBar: onChange also gets the bar
- Paint exported; @category UI on the UI settings types
Fixes:
- align "stretch" only resizes buttons and panels, not a BitmapText
(whose resize is a scale) or a checkbox or slider
- a drag ends only on its own pointer's release, in the event family the
engine listens to, not on another finger or button
- a non-blocking scrolling panel inside another scrolls alone
- a draggable panel is not moved through a widget at any depth, and the
check no longer mixes coordinate spaces in the world
- a draggable element in the world no longer jumps by the camera position
- a stale scroll press is dropped; skin insets of 0 match the art
- slider steps in exponent notation, and a range that is not a whole
number of steps, snap correctly
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JnzwA4xBNBw7cEZzG1bUru
shadowColor, shadowOffsetX, shadowOffsetY and shadowBlur. The soft edge is
a few stacked translucent rounded rectangles, so it looks the same on
Canvas, WebGL and WebGPU; it follows the corner radius, goes under a skin
too, and is drawn outside the bounds so it never takes the pointer.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JnzwA4xBNBw7cEZzG1bUru
…game (#1805)
A touch fires pointerover and pointerenter before its pointerdown, and
pointerout and pointerleave after its pointerup. The plugin only kept the
down, move, up and cancel from the engine, so the widget under a button
got onOver as the button was pressed (the Alex 4 menu arrow jumped to the
line under B). The plugin now also captures the four hover events and
stops them for a touch that lands on, or lifted from, a control. A mouse
hovering over the controls is left alone, as its moves are.
Fixes#1805
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JnzwA4xBNBw7cEZzG1bUru
UIFocus (off until UIFocus.enable()): one widget has the focus; the arrows,
the D-pad or the left stick move it to the nearest widget that way, edge to
edge; Enter, Space or A press it as a click does; left/right step a slider;
Escape or B cancel. The pad is read directly (input.readGamepads, real or
virtual), so the game's pad bindings are untouched. The ring shows after
keyboard or pad input and hides when the mouse moves; the first key after
the mouse only shows it. A UIPanel with trapFocus keeps the focus in and
restores it after, with onBack on cancel. Focused widgets in a scrolling
panel scroll into view, with room for the ring. Any UIBaseElement can take
part: focusable, focusNeighbors, onNavigate, onFocus/onBlur,
focusPress/focusRelease. Buttons, checkboxes and sliders do.
hoverScale and pressScale on UIBaseElement, and as settings on every
widget: an eased bounce applied to the drawing only, so hit areas and
layouts stay put.
The UI example turns on both, with a hint line for the controls. Skill
sections and changelog entries; 13 new tests (87 in ui-widgets).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JnzwA4xBNBw7cEZzG1bUru
Arrows or the D-pad move to the nearest widget, Enter, Space or A press
it, sliders step, a ring shows the focus, and a focused widget in a
scrolling panel scrolls into view. Removed: trapFocus/onBack, cancel,
focusNeighbors, onFocus/onBlur, hover moving the focus, the first-key
rule, the stick with repeat, configurable keys and ring sizes. Most games
use the mouse or touch; this covers a menu driven by keys or a pad.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JnzwA4xBNBw7cEZzG1bUru
- a press held by Enter or A is called off when the focus moves, the
widget is removed or navigation is disabled, instead of staying down
- a focused widget only bounces while the focus ring shows
- the bounce eases over about 0.2 s; it snapped in a frame
- Enter goes to the focused button: another bound to Enter waits while the
ring shows elsewhere
- a pad's A held as navigation is enabled is not a press
- a widget made focusable after it was added takes part
- navigation skips widgets inside a hidden or transparent container
- an arrow from a focused widget disabled since moves on from where it is
- docs: the bounce is on UIBaseElement; Enter and bindKey; accept calls
onClick() with no event
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JnzwA4xBNBw7cEZzG1bUru
UIFocus is gone. A widget takes the keyboard and gamepad focus with
focus(); from then on the arrows or the D-pad move it, Enter, Space or A
press it, sliders step. Nothing listens until a widget has the focus, and
the listeners go when none does (blur(), or the focused widget leaving the
scene), so the keys and the pad are the game's until then. On
UIBaseElement: focus(), blur(), focused, focusable, onNavigate, the static
focusOwner and focusRingColor. The example focuses its first widget.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JnzwA4xBNBw7cEZzG1bUru
Three bugs the new tests caught:
- a slider's default value text kept the old decimals after `step` changed
- Enter on a focused button the pointer already held clicked it on release
- a virtual gamepad sharing a slot with a real one was not read
The stub-subclass tests now use the real button. New tests cover the
shadow under a faded parent, scrollbar geometry, vertical and skinned
sliders, cancelled and two-finger drags, skin regions and bitmap font
sizes, radio drawing and groups, sideways and line-mode wheel scrolling,
wheel pass-through at the end of a panel, fling limits, grid stretch,
row autoSize, clearChildren, the focus ring, and focus teardown. The
clipping test now clips through a real container.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JnzwA4xBNBw7cEZzG1bUru
- a "which widget for what" table
- one complete options menu: skinned panel, column layout, checkbox,
slider, button, drop shadow, bounce, keyboard / gamepad focus
- code for drop shadows, a scrolling level list, radio groups, skinned
checkboxes and sliders, stepped and vertical sliders, and a custom
focusable widget
- symptom rows for focus, bindKey vs focus, and shadow corners
- drops a reference to the UI example's old hand-made panel
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JnzwA4xBNBw7cEZzG1bUru
Class docs in sections (look, label, state and onChange, radio groups,
keyboard and gamepad; range, input, text, look), every setting with its
unit and default, and examples for the common uses: a settings toggle, a
label that follows the state, a radio group, skins, restoring saved
settings, a volume slider, stepped and vertical sliders, percent text, a
slider in a scrolling panel. Three docs that contradicted the code now
match it (trackThickness with a skin, the checkbox fontSize minimum,
onChangeEnd on arrow steps).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JnzwA4xBNBw7cEZzG1bUru
UIPanel's class doc is rewritten in sections (background, title,
dragging, blocking, layout, scrolling, drop shadow, idle float, things to
know), with examples for a drawn dialog, a skinned options menu, a
scrolling level grid, a draggable window, a drop shadow and a HUD frame.
Every setting has its unit and default; clearChildren is now documented.
ProgressBar's doc is tidied, with examples for a loading bar, a health
bar, a colour that follows the value, a vertical bar, a drop shadow, and
reacting when the bar empties.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JnzwA4xBNBw7cEZzG1bUru
UIBaseElement's class doc covers the pointer callbacks, enabled,
dragging, the bounce and keyboard / gamepad focus, with examples for a
widget of your own, a draggable token, a keyboard and pad menu, and an
opaque dialog. UITextButton documents every setting with its unit and
default (the old names as aliases), with examples for drawn, skinned,
bindKey, disabled and shared-style buttons. UISpriteElement gets a real
class doc and examples. Several docs that contradicted the code now match
it (onMove, onRelease, enabled on a custom element, bindKey while
disabled).
Fix: a held bindKey press that is called off (the window losing focus,
the button removed or disabled) now calls onCancel(), as a pointer press
does; it called neither onRelease nor onCancel, so work started on the
press never stopped. Test added; the skill's bindKey rules follow.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JnzwA4xBNBw7cEZzG1bUru
Its onClick() and onRelease() returned false, stopping every press and
release at the sprite, while every other UI element lets the event carry
on to what is under it unless a handler returns false. They now return
true. A subclass that relied on the old default returns false itself; the
examples already do. onActivateEvent / onDeactivateEvent now call super.
Test with real pointer events; docs, skill and changelog updated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JnzwA4xBNBw7cEZzG1bUru
UIBaseElement's onClick() and onRelease() now return false, as
UISpriteElement's always did (the previous commit's flip of
UISpriteElement is undone). A click on a button, a checkbox, a slider or
a widget of your own no longer also reaches the game behind it; a handler
returns true to let the press through. Hover is unchanged, and a
non-blocking UIPanel still lets presses on its background through.
Tests with real pointer events for buttons, checkboxes, sprite elements,
a handler letting the press through, and a non-blocking panel. Docs, both
skills and the changelog follow; two example buttons that returned true
now return false.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JnzwA4xBNBw7cEZzG1bUru
It was a Sprite carrying its own copy of the pointer handling, which had
drifted from UIBaseElement's. It now extends UIBaseElement and holds its
image as `sprite`, so it shares the one implementation and gains what the
other widgets have: enabled (drawn at half opacity when off),
cancelPress / onCancel, hoverScale / pressScale, dragging and keyboard /
gamepad focus.
Kept for existing code: (x, y) is still where the image's anchor point
goes, and setRegion (which also resizes the element), addAnimation,
setCurrentAnimation and isCurrentAnimation stay on the element. Other
Sprite members move to `sprite`; UISpriteElementSettings is exported.
The matter platformer's touch controls follow: the button passes its
anchorPoint in the settings, and the joypad's knob is a child instead of
being drawn by hand. Tests cover placement, clicks, disabled drawing,
setRegion, the bounce, cancel and focus; docs, skill and changelog
updated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JnzwA4xBNBw7cEZzG1bUru
17 tests of what a game relies on: Sprite settings and size, placement
(centred by default, or by anchorPoint in the settings), presses,
releases, hover, a press ended by leaving, isClickable, removal, the
press stopping at it or let through, a subclass overriding the callbacks
as the demos' buttons do, and the setRegion / animation / opacity
shortcuts. They only use what the Sprite-based element and the
UIBaseElement-based one both offer, and pass on both, and on master's.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JnzwA4xBNBw7cEZzG1bUru
…es the canvas
A pointerup cleared the pointer a region was tracking, so the region
forgot it was under the pointer: leaving straight after a click never
called onOut (a button stayed lit until the pointer came back over it),
and the next move over it called onOver a second time. The release now
keeps the pointer; the press still ends as before.
The browser's pointerleave, sent when the pointer leaves the canvas or a
finger lifts, now releases every region holding that pointer, wherever
the exit is reported. It used to reach only a region at that point,
which for a mouse leaving the canvas is the edge, far from most widgets.
Touch is unchanged: a tap hovers while the finger is down, not after.
Tests for leaving after a click, moving on over it, repeated clicks, a
tap with new pointer ids, and leaving the canvas; the UISpriteElement
tests check the hover strictly again, and pass on the old and new
element.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JnzwA4xBNBw7cEZzG1bUru
The reason will be displayed to describe this comment to others. Learn more.
🟡 Changes recommended
Pointer leave handling can leave hover or press state stuck, while sprite frame changes leave element size, positioning, and hit bounds out of sync with the artwork.
This follow-up to the 20.13 UI update unifies sprite buttons with the shared widget infrastructure, adjusts pointer handling, and expands UI documentation and examples.
Changes:
Refactors UISpriteElement to contain a Sprite and inherit shared UI behavior.
Makes UI presses stop propagation by default, addresses hover handling, and adds keyboard-press cancellation callbacks.
Expands widget documentation, updates examples, and adds regression tests.
File
Description
packages/melonjs/tests/ui.spec.js
Updates default press-propagation expectations.
packages/melonjs/tests/ui-widgets.spec.js
Adds hover, blocking, cancellation and sprite-widget tests.
- UICheckbox#toggle() on a checked radio button leaves it checked, as a
click does, instead of leaving its group with nothing checked
- UIPanel#scrollTo() (and scrollToChild, scrollX / scrollY) stops a
fling still coasting; the panel's own fling, drag, wheel and relayout
scroll through a private method that leaves it alone
- UISlider lays its texts out in one place, again whenever they change:
a vertical slider widens for a longer value text (valueFormat, min, max
or step set later), a horizontal one moves the value below the track
when it would run into the caption, and the new labelText sets the
caption later
Tests for each, failing before the fix, and one for the wheel over a
slider in a scrolling panel (it moves the slider, not the panel), which
had none. Docs, skill and changelog updated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JnzwA4xBNBw7cEZzG1bUru
obiot
changed the title
UI: clear docs with examples, UISpriteElement on UIBaseElement, presses stop at UI elements, hover fixes
UI: clear docs with examples, UISpriteElement on UIBaseElement, presses stop at UI elements, hover and widget fixes
Nov 1, 2026
A checkbox joined its group only once in the scene, so before a menu was
added several radio buttons could be checked at once and
UICheckbox.checkedIn() answered undefined. A checkbox now joins its group
when made (or when its group changes) and leaves it when destroyed.
Groups hold their members through WeakRef, so a checkbox dropped without
being destroyed can still be collected. A checkbox taken out of the world
but kept alive stays in its group.
Tests for a group before the scene, and for leaving on destroy while
staying in the group when kept alive; both fail before the fix. Docs,
skill and changelog updated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JnzwA4xBNBw7cEZzG1bUru
The reason will be displayed to describe this comment to others. Learn more.
🟡 Changes recommended
The animation shortcut breaks compatibility by dropping preserve_dt, while sprite changes leave stale hit bounds and incomplete pointer cleanup can leave controls hovered or pressed.
The supported TouchEvent fallback does not receive a browser touchleave after touchend. Retaining the touch identifier here therefore leaves the region occupied after the finger lifts. A subsequent mouse hover cannot synthesize an enter because that path requires handlers.pointerId === null, so a touch can prevent later mouse hovering until another press overwrites the identifier. Keep the identifier for mouse/PointerEvent releases, but clear it for legacy touchend.
Clear hover and call onOut when a pointer is cancelled
After pointercancel, the cancel branch below clears handlers.pointerId but only calls the release callback. The browser's subsequent pointerleave now fails this identity check, so UIBaseElement.leave() never clears hover or calls onOut. A cancelled touch therefore leaves the button highlighted. Perform the leave cleanup during cancellation as well, before discarding the pointer's identity.
…eElement follows its frame
- A pointerleave (the pointer leaving the canvas, or a finger lifting)
now goes to every registered region holding that pointer, not only the
candidates the spatial query finds near the exit point, which once the
quadtree splits misses a hovered region in another quadrant
- UISpriteElement follows its sprite whenever the image's size or anchor
changes: through setRegion, setCurrentAnimation, sprite.setRegion() or
an animation frame of another size. The element takes the new size and
the image's anchor point stays in place on screen, as a Sprite's pos
does, so the hit area matches the artwork
Regression tests: a crowded world with a split index and the exit in
another quadrant; a region of another size keeping the anchor; a frame
set on the sprite itself followed on the next update and hit where the
smaller image is. Each fails before the fix.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JnzwA4xBNBw7cEZzG1bUru
The kept shortcut dropped Sprite's third argument, so existing calls with
it no longer compiled in TypeScript, and in JavaScript reset the frame
timer instead of keeping it. The compatibility test checks the argument
reaches the sprite, on the old element and the new.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JnzwA4xBNBw7cEZzG1bUru
- pointercancel cleared the region's pointer and only called the cancel
callback; the browser's leave after it then no longer matched, so a
cancelled touch left a button highlighted. A cancel now also leaves
the region (hover cleared, onOut called).
- A legacy touchend kept the touch's id like a mouse release does, but
touch events send no leave after it, so the region stayed occupied
and later mouse hover could not enter it. A touchend now leaves the
region, as a lifted finger hovers nothing.
Test for the cancel; the legacy touch path is only used where pointer
events are missing, so the browser tests cannot reach it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JnzwA4xBNBw7cEZzG1bUru
Both of the "previously missed" findings from the latest Copilot review are fixed in 582b27c:
pointercancel left the hover on. The cancel callback cleared the region's pointer, so the browser's leave event after it no longer matched. That came from this PR's leave change. A cancel now also sends the leave callbacks: hover is cleared and onOut is called. The new test (a cancelled press ends both the press and the hover, and the later browser leave does nothing more) fails without the fix.
Legacy touchend kept the touch id. A touchend now sends the leave callbacks and clears the id, since a lifted finger hovers nothing and touch events send no leave of their own. Mouse and pointer-event releases still keep the id. This path only runs where pointer events are missing, which the Chromium tests can't simulate, so it has no browser test.
The reason will be displayed to describe this comment to others. Learn more.
🔵 Needs a closer look
Unresolved defects can leave controls hovered or pressed after canvas exit, desynchronize sprite hit areas from displayed frames, and cause slider labels to overlap or overflow.
No open issues 🎉
1 previously missed issue
In code that hasn't changed since last review
Relayout sliders when displayed value text outgrows its reserved space
A custom valueFormat can return short endpoint labels such as "Off" and "Max" but a much longer label between them. Measuring only the endpoints leaves that displayed value overlapping the caption or extending beyond a vertical slider's width, and changing value does not rerun layout. Include the displayed text in sizing and update layout when it outgrows the reserved space, or expose an explicit maximum-width setting.
The reason will be displayed to describe this comment to others. Learn more.
Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Follow-up to the 20.13.0 UI update (#1808), before 20.13.0 is released.
1. Docs and skill
UIBaseElement,UITextButton,UISpriteElement,UICheckbox,UISlider,UIPanel,ProgressBar):@default, checked against the constructor.melonjs-ui-and-textskill:2. A press stops at a UI element by default
UIBaseElement'sonClick()andonRelease()now returnfalse, asUISpriteElement's already did.trueto let it through.UIPanelstill lets presses on its background through.truenow returnfalse.3.
UISpriteElementis aUIBaseElementholding aSpriteIt was a
Spritewith its own copy of the pointer handling, which had drifted fromUIBaseElement's. It now extendsUIBaseElementand holds its image assprite.What it gains:
enabled(drawn at half opacity when off),cancelPress()/onCancel(),hoverScale/pressScale, dragging, and keyboard / gamepad focus.Kept for existing code:
(x, y)is still where the image's anchor point goes.setRegion(which now also resizes the element),addAnimation,setCurrentAnimationandisCurrentAnimationstay on the element.UISpriteElementSettingsis exported.Changed (in the changelog):
Spritemembers move to.sprite.posis the element's top-left corner.anchorPointgoes in the settings; setting it after construction no longer moves the image.draw()override becomes a child.The matter platformer's touch controls follow: the jump button passes its
anchorPointin the settings, and the joypad's knob is a child.4. Hover fixes in
pointerevent.tspointerupused to make the region forget the pointer. Leaving straight after a click never calledonOut, so a button stayed lit, and moving on over it calledonOveragain. The release now keeps the pointer.pointerleave(the pointer leaves the canvas, or a finger lifts) now releases every region holding that pointer, wherever the exit is reported. It used to reach only a region at that point.5. Widget fixes found while writing the docs
UICheckbox.checkedIn()answeredundefined. A checkbox leaves its group when destroyed; groups hold members throughWeakRef, so one dropped without being destroyed can still be collected.UICheckbox#toggle()on a checked radio button leaves it checked, as a click does. It used to leave the group with nothing checked.UIPanel#scrollTo()(andscrollToChild,scrollX/scrollY) stops a fling that is still coasting.UISliderlays out its texts again whenever they change:valueFormat,min,maxorstepset later);labelTextsets the caption later.bindKeycancel: a heldbindKeypress called off (the window loses focus, or the button is removed or disabled) now callsonCancel(), as a pointer press does.How it was checked
tests/ui-spriteelement.spec.js: 17 tests ofUISpriteElement's public behaviour: placement, presses, hover, leaving while held,isClickable, removal, blocking, a subclass like the demos' buttons, and the Sprite shortcuts. They pass on the new class, on the Sprite-based one from before the rewrite, and onmaster's.ui-widgets.spec.js, each fix's test checked to fail without it:true, and a non-blocking panel.toggle(),scrollTo()during a fling, the slider'slabelText, widening, and value placement, and the cancelledbindKeypress.UISpriteElement: placement, disabled drawing,setRegion, bounce, cancel, focus.tsc,eslint,biome, the engine build (including the declaration check) and the docs build pass.Left for a separate PR: on WebGL and WebGPU,
clipRectreplaces the outer clip instead of intersecting with it, so a scrolling panel inside another clipping container is cropped to its own area only. Canvas gets it right. The fix touches clipping for everything those renderers draw, so it gets its own PR and tests.Type of change
Checklist
pnpm lintpasses)pnpm build)🤖 Generated with Claude Code
https://claude.ai/code/session_01JnzwA4xBNBw7cEZzG1bUru