Repository navigation
UI: standardize the UI example's button, checkbox and panel into engine widgets, and add a slider #1722
Description
Summary
The UI example (packages/examples/src/examples/ui/ExampleUI.tsx) builds three widgets by hand that every game with a menu ends up rewriting: ButtonUI, CheckBoxUI and UIContainer. ProgressBar went through this already in 20.8: it started inside the loading screen and is now a public, self-drawing widget with typed settings. This issue does the same for the remaining three, using ProgressBar as the template, and adds the one control an options menu still lacks: a slider.
The shared shape:
- a typed settings object (
XxxSettings, exported with the class) - draws itself with renderer primitives by default, so it works with no assets on Canvas, WebGL and WebGPU
- an optional
skinfor atlas regions / nine-slice art, which is what the example uses today - a public value plus
onChange, rather than private state
Once they exist, the example uses them and its three local classes are deleted, so the demo looks the same but shows engine API.
Problems shared by the three example classes
- the label is a sibling the caller must add.
btn.label/cb.labelare created by the widget but added by the caller (panel.addChild(btn1.label)), positioned by hand, not clickable, and left behind when the widget is removed - positional constructors tied to the demo's atlas (
new CheckBoxUI(x, y, tex, "green_boxCheckmark", "grey_boxCheckmark", "Music ON", "Music OFF")), so none of them can be reused without the demo's art - hover is a hard-coded opacity change, and there is no disabled state
- state is private (
isSelected) with no way for the game to be told it changed
1. Button: ButtonUI → UITextButton gains a skin
The engine already has UITextButton (drawn with a RoundRect). ButtonUI is the same thing drawn from images, with a pressed state. Rather than a second button class:
-
UITextButtonacceptsskin: { atlas, normal, pressed?, hover?, disabled? }, regions drawn as nine-slice so a button can be any width - pressed state for both the drawn and the skinned look (the example shifts down by the height difference of the pressed art; keep that behaviour for skins)
-
onClickcallback in settings, so a button doesn't need a subclass for one action -
enabledflag - fix
bindKey:UITextButtonstores it (uitextbutton.ts:111) and never uses it. Either wire it so the key triggers the click, or remove it
2. Checkbox: CheckBoxUI → UICheckbox
- new
src/renderable/ui/uicheckbox.ts, extendingUIBaseElementso the box and label are children - settings:
checked,label(orlabelOn/labelOff, which the example uses),onChange,enabled,size, label text style,boxColor/checkColor, hover colours named as onUITextButton -
skin: { atlas, on, off }for image checkboxes -
checkedgetter/setter andtoggle(). Settingcheckedfrom code updates the look without callingonChange, so restoring saved settings doesn't trigger game logic; only user input calls it - clicking the label toggles it too
-
group: checkboxes sharing a group behave as radio buttons (one checked at a time); members leave the group when deactivated
3. Slider: new UISlider
Not in the example today, but it is the missing piece of an options menu (volume, sensitivity, brightness), and it shares everything above.
- new
src/renderable/ui/uislider.ts, extendingUIBaseElement - settings:
min,max,step,value,orientation: "horizontal" | "vertical",onChange,enabled, optionallabeland value text (with aformatcallback, e.g.v => v + "%") - drawn by default as a track, a filled part and a thumb;
skin: { atlas, track, fill?, thumb }with the track and fill as nine-slice - drag the thumb, or click / tap the track to jump there; keeps tracking while dragging even when the pointer leaves the track
-
valuegetter/setter, snapped tostepand clamped tomin/max. Setting it from code does not callonChange, same rule as the checkbox - optional
onChangeEnd, called once on release, for work that should not run on every drag step (saving settings, playing a test sound) - the mouse wheel over a hovered slider moves it by one
step - inside a scrolling
UIPanel, dragging the thumb moves the slider, not the panel
4. Panel: UIContainer → UIPanel
UIContainer would read as "the UI version of Container"; the example already names its instance "UIPanel".
- new
src/renderable/ui/uipanel.ts, extendingUIBaseElement - background: drawn by default, or
skin: { atlas, region }as aNineSliceSprite(what the example does throughcreateSpriteFromName(..., true)), resized with the panel - optional
title - optional dragging, keeping what the example does correctly today:
moveToTopon grab, and consumingonClick/onOver/pointermoveso a covered widget underneath doesn't answer too - layout:
layout: "none" | "column" | "row" | "grid",columns,padding(inside the nine-slice border, below the title),gap,align: "start" | "center" | "end" | "stretch",autoSize. Runs when children are added, removed or the panel is resized, not every frame, withrelayout()for a child that changes size."none"(the default) keeps manual placement - scrolling:
scroll: "none" | "vertical" | "horizontal" | "both". The content area clips (Container#clipping), scrolls with the wheel when hovered and with drag plus momentum on touch, shows a scrollbar only when the content does not fit, and offersscrollTo,scrollToChild,scrollX/scrollYandonScroll- children scrolled out of view must not receive pointer events
- a drag that starts on a button scrolls and cancels the press rather than clicking it
- nested scrolling panels are out of scope for now
Example and docs
-
ExampleUI.tsxusesUITextButton(skinned),UICheckboxandUIPanel;ButtonUI,CheckBoxUIandUIContainerare deleted - the options panel gains volume sliders (music, sound FX) wired to
audio - the demo adds a scrolling panel (e.g. a level list in a grid) to show layout + scroll
-
melonjs-ui-and-textskill: a section per widget, plus symptom-table rows for "onChange fires when loading settings" and "a hidden button in a scrolled panel still gets clicks" - CHANGELOG entries under 20.9.0
Tests
- checkbox: click toggles and calls
onChangeonce; settingcheckeddoes not call it; label click toggles; disabled ignores input; a radio group keeps exactly one checked; the skin swaps regions - slider: drag and track click set the value;
stepsnapping and clamping; vertical orientation; settingvaluedoes not callonChange;onChangeEndonce per drag; wheel steps; disabled ignores input - button: skinned pressed state and offset;
onClicksetting;bindKey(whichever way it is resolved); disabled - panel: column, row and grid placement with padding and gap;
autoSizegrows the nine-slice background; scroll range from content size; wheel and drag scrolling; out-of-view children are not hit; drag cancels a press; a slider drag inside a scrolling panel doesn't scroll it - all four draw on Canvas and WebGL
Out of scope
Text input, a resize grip, and nested scrolling panels.
Activity
[-]UI: standardize the UI example's button, checkbox and panel into engine widgets[/-][+]UI: standardize the UI example's button, checkbox and panel into engine widgets, and add a slider[/+]UI: a panel widget, and a border that stays inside the box it was giv…