Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
12218f4
20.13.0: bump the version
claude Oct 31, 2026
acbb163
UI: checkbox, slider, skinned button, panel layout and scrolling (#1722)
claude Oct 31, 2026
1fbc5c9
UI: fixes from review
claude Oct 31, 2026
1bf6e4b
Examples: a taller level list in the UI example
claude Oct 31, 2026
4cacf94
Examples: the UI example's level list at the top right, beside the bars
claude Oct 31, 2026
32626f9
Examples: the UI example's level list shows the first 16 levels
claude Oct 31, 2026
2e3c8f1
UI: consistent widget API, and fixes from a second review
claude Oct 31, 2026
d4a577c
Examples: UI example lint
claude Oct 31, 2026
99c7c51
UI: drop shadows on UIPanel and ProgressBar, used in the UI example
claude Oct 31, 2026
12bedbd
Gamepad plugin: keep a touch's hover events on the controls from the …
claude Oct 31, 2026
59f6398
UI: keyboard and gamepad focus navigation, and a hover/press bounce
claude Oct 31, 2026
6f1acf6
UI: keep the focus navigation to the minimum
claude Oct 31, 2026
44ee453
UI: fixes from a review of the focus navigation and the bounce
claude Oct 31, 2026
5291863
UI: focus lives on the widgets, with no focus manager to set up
claude Oct 31, 2026
1440ba7
Remove a scratch test file committed by mistake
claude Oct 31, 2026
32a4edc
UI: fill the gaps in the widget tests, and fix what they found
claude Oct 31, 2026
596b23b
Merge remote-tracking branch 'origin/master' into claude/modest-johns…
claude Oct 31, 2026
d706c94
gamepad-plugin 1.0.2
claude Oct 31, 2026
d345737
Merge remote-tracking branch 'origin/master' into claude/modest-johns…
claude Oct 31, 2026
b39dd70
Skill: UI widgets, with a widget map and a complete menu example
claude Oct 31, 2026
ec8e1a4
Docs: UICheckbox and UISlider, with examples
claude Oct 31, 2026
57109da
Docs: UIPanel and ProgressBar, with examples
claude Oct 31, 2026
139c957
Docs: UIBaseElement, UITextButton, UISpriteElement, with examples
claude Oct 31, 2026
0a785e6
Changelog: onCancel also covers a called-off bindKey press
claude Oct 31, 2026
c365da3
UI: UISpriteElement lets events through by default, like the others
claude Oct 31, 2026
1d86e82
UI: a press stops at a UI element by default
claude Oct 31, 2026
6d971f4
UI: UISpriteElement is a UIBaseElement holding a Sprite
claude Nov 1, 2026
515a59a
Tests: UISpriteElement's public behaviour, old and new
claude Nov 1, 2026
71c91de
Input: keep the hover after a click, and end it when the pointer leav…
claude Nov 1, 2026
63bfee5
UI: fix the widget bugs found while writing the docs
claude Nov 1, 2026
8a41b06
UI: a radio group holds one choice from when its checkboxes are made
claude Nov 1, 2026
8a47072
Fixes from review: canvas leave reaches every hovered region; UISprit…
claude Nov 1, 2026
6b9b22b
UISpriteElement: setCurrentAnimation passes preserve_dt on
claude Nov 1, 2026
582b27c
Input: a cancelled pointer, or a lifted legacy touch, ends the hover
claude Nov 1, 2026
c70fe49
Changelog: a cancelled pointer ends the hover too
claude Nov 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ class BackendButton extends UIBaseElement {

override onClick() {
globalThis.location.search = `?physics=${this.target}`;
return true;
return false;
}

override draw(renderer: WebGLRenderer | CanvasRenderer) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ class BackendButton extends UIBaseElement {
// the adapter is chosen when the Application is constructed, so
// switching one means booting the example again
globalThis.location.search = `?physics=${this.target}`;
return true;
return false;
}

override draw(renderer: WebGLRenderer | CanvasRenderer) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,10 @@ class Button extends UISpriteElement {
super(x, y, {
image: gameState.texture,
region: "shadedDark36.png",
// placed by its top left corner
anchorPoint: new Vector2d(0, 0),
});
this.setOpacity(0.25);
this.anchorPoint.set(0, 0);
}

/**
Expand Down Expand Up @@ -64,8 +65,8 @@ class Joypad extends UISpriteElement {
anchorPoint: new Vector2d(0, 0),
});

// mobile part of the joypad
this.pad = new Sprite(x, y, {
// mobile part of the joypad, drawn over the back
this.pad = new Sprite(0, 0, {
image: gameState.texture,
region: "shadedDark01.png",
anchorPoint: new Vector2d(0, 0),
Expand All @@ -76,6 +77,8 @@ class Joypad extends UISpriteElement {
this.width / 2 - this.pad.width / 2,
this.height / 2 - this.pad.height / 2,
);
this.pad.pos.setV(this.relative);
this.addChild(this.pad);

// offset by which the joypad move when pressed/moved
this.joypad_offset = new Vector2d();
Expand All @@ -96,19 +99,20 @@ class Joypad extends UISpriteElement {
input.registerPointerEvent(
"pointermove",
this,
this.pointerMove.bind(this),
this.onPointerMove.bind(this),
);
}

onDestroyEvent() {
// release the registered pointer event
input.releasePointerEvent("pointermove", this);
super.onDestroyEvent();
}

/**
* pointermove function
*/
pointerMove(event) {
onPointerMove(event) {
if (this.released === false) {
const x = event.gameScreenX + event.width / 2;
const y = event.gameScreenY + event.height / 2;
Expand Down Expand Up @@ -188,13 +192,11 @@ class Joypad extends UISpriteElement {
}

/**
* extend the draw function
* move the pad with the joypad offset
*/
draw(renderer) {
// call the super constructor
super.draw(renderer);
this.pad.pos.setV(this.pos).add(this.relative).add(this.joypad_offset);
this.pad.draw(renderer);
update(dt) {
this.pad.pos.setV(this.relative).add(this.joypad_offset);
return super.update(dt);
}
}

Expand Down
10 changes: 9 additions & 1 deletion packages/melonjs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
- UI: `UISlider`, a horizontal or vertical slider with `min`, `max`, `step`, drag, track clicks and the mouse wheel, an optional caption and value text (`valueFormat`), drawn or from atlas art. `onChange` fires while it moves, `onChangeEnd` once on release, neither when `value` is set from code
- UI: `UIPanel` lays its children out (`layout`: `"column"`, `"row"` or `"grid"`, with `padding`, `gap`, `align` and `autoSize`), scrolls them (`scroll`, with the wheel, dragging, momentum and a scrollbar; `scrollTo`, `scrollToChild`), and takes a nine-slice `skin`, a `title` and `draggable`; a draggable panel comes to the front when grabbed
- UI: `UITextButton` takes an `onClick` setting, called on a completed click; a `skin` of atlas regions, with pressed art drawn lowered; `pressedColor`; and the names the other widgets use: `width` / `height` for its size, `fontSize` (pixels, also with a bitmap font), `labelFillStyle` and `borderColor`. `resize()` keeps its background and label in step. A font that is not a loaded bitmap font is drawn as a CSS font, where the button used to throw
- UI: `enabled` on every `UIBaseElement`, to turn a widget off; `cancelPress()` and `onCancel()`, to call off a press without a release, as a scrolling `UIPanel` does when a drag on a button turns into a scroll; and `keepsDrag`, for a widget of your own that is dragged inside a scrolling panel
- UI: `enabled` on every `UIBaseElement`, to turn a widget off; `cancelPress()` and `onCancel()`, to call off a press without a release, as a scrolling `UIPanel` does when a drag on a button turns into a scroll (a `UITextButton`'s held `bindKey` called off by the window losing focus, removal or `enabled = false` calls `onCancel()` too); and `keepsDrag`, for a widget of your own that is dragged inside a scrolling panel
- UI: a drop shadow under a `UIPanel` or a `ProgressBar`: `shadowColor`, `shadowOffsetX`, `shadowOffsetY` and `shadowBlur`, softened the same on Canvas, WebGL and WebGPU, rounded with the corners, and under a skin too
- UI: keyboard and gamepad navigation for a game played without a mouse or a touch screen: once a widget takes the focus with `focus()`, the arrows or the D-pad move it to the nearest widget, Enter, Space or A press it as a click does, and left and right step a slider. A ring shows the focus while the keyboard or a pad is used, and a focused widget in a scrolling panel is scrolled into view. Until a widget has the focus the keys and the pad are left to the game; `blur()` gives them back. `focusable`, `focused`, `onNavigate`, `UIBaseElement.focusOwner` and `UIBaseElement.focusRingColor`
- UI: `hoverScale` and `pressScale` on every `UIBaseElement`: a bounce under the pointer or the focus, and while held, easing by itself and scaling only the drawing, not the hit area or the layout
Expand All @@ -18,11 +18,19 @@
### Changed
- UI: `UITextButton` no longer writes the size and alignment it worked out back into the settings object, so one object can make several buttons
- UI: `UITextButton#label` is the button's text, a `BitmapText` or, with a CSS font, a `Text`
- UI: `UISpriteElement` is now a `UIBaseElement` holding a `Sprite` (`sprite`), instead of a `Sprite` with its own copy of the pointer handling, so it has everything the other widgets have: `enabled` (drawn at half opacity when off), `cancelPress()` / `onCancel()`, `hoverScale` / `pressScale`, dragging and keyboard / gamepad focus. `(x, y)` is still where the image's anchor point goes, so existing layouts do not move, and `setRegion`, `addAnimation`, `setCurrentAnimation` and `isCurrentAnimation` are kept on the element. As a sprite does, it follows its image's size, a region or animation frame of another size included, with the image's anchor point staying in place. Other `Sprite` members are now on `element.sprite`; `pos` is the element's top left corner; setting `anchorPoint` after construction no longer moves the image (pass it in the settings); and a second sprite drawn in a `draw()` override becomes a child added with `addChild`
- UI: a press stops at a UI element by default: `UIBaseElement`'s `onClick()` and `onRelease()` now return `false`, as `UISpriteElement`'s already did, so a click on a button, checkbox or widget of your own no longer also reaches the game behind it. A subclass that relied on presses passing through returns `true` from them. Hover is unchanged, and a non-blocking `UIPanel` still lets presses on its background through

### Deprecated
- UI: `UITextButton#bitmapText`, for `label`

### Fixed
- UI: a radio group 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 their members weakly, so one dropped without being destroyed can still be collected
- UI: `UICheckbox#toggle()` on a checked radio button leaves it checked, as a click does, instead of leaving its group with nothing checked
- UI: `UIPanel#scrollTo()` (and `scrollToChild`, `scrollX` / `scrollY`) stops a fling that is still coasting, instead of the fling carrying on from the new offset
- UI: a `UISlider` lays its texts out again when they change: a vertical one widens for a longer value text (a new `valueFormat`, `min`, `max` or `step`), and on a horizontal one the value moves below the track when it would run into the caption, instead of overlapping it. `labelText` sets the caption later
- Input: a region stays hovered after a click while the pointer is still on it. A release used to forget the pointer, so leaving straight after a click never called `onOut` (a button stayed lit), and moving on over it called `onOver` again
- Input: the pointer leaving the canvas ends the hover of whatever it was over, not only of a region at the point where it left; a cancelled pointer, or a finger lifted on the legacy touch-event path, ends it too
- Input: a child cropped out of view by a container with `clipping`, or scrolled out of a `UIPanel`, no longer gets pointer events where it is not drawn
- UI: a `UITextButton` moved after it was built draws its background where it is, not where it was first put
- UI: a blocking `UIPanel` stays opaque to hover for as long as the pointer is on it, not only on the frame the pointer enters
Expand Down
2 changes: 1 addition & 1 deletion packages/melonjs/skills/melonjs-input/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,7 @@ input.triggerKeyEvent(input.KEY.LEFT, false); // release
| `no action defined for keycode N` | `bindGamepad` called before `bindKey` for that keycode |
| stick has to be pushed well past its `threshold` before it registers | `threshold` is ADDED to the deadzone, not used instead of it |
| one half of a stick does nothing | each half is a separate `bindGamepad`, chosen by the sign of `threshold` |
| a widget under an overlapping panel still reacts | the panel consumes nothing — return `false` from its `onClick` and `onOver` |
| a widget under an overlapping panel still lights up | the panel consumes no hover: return `false` from its `onOver` (presses already stop at UI elements; `UIPanel` with `blocking` does both) |
| a widget stays highlighted after the pointer moves onto a panel over it | the panel consumes no `pointermove`, so nothing takes the pointer off the widget |
| a child's big `z` does not put it in front of another container | `pos.z` is container-local — use `ancestor.moveToTop()` on the container |
| every click in the game stops working | a handler on `app.viewport` returning `false`, which is asked first |
Expand Down
Loading
Loading