Repository navigation
UI: a panel widget, and a border that stays inside the box it was given - #1740
Merged
Merged
Conversation
`UIPanel` is a rectangular surface with a background, an optional border and children: the thing every menu, dialog and HUD frame starts by writing by hand. One example in this repository had three of them, each repeating the same half dozen renderer calls and the same correction for the way `setColor` folds a colour's own alpha into the global one. That correction is why this is a widget rather than a snippet: leaving it out does not fail, it renders the panel at the wrong opacity. It is a `UIBaseElement`, so children are positioned relative to it and are asked about a click before it is, and `blocking` decides whether a click that misses every child is swallowed rather than reaching the game beneath. `bobAmplitude` / `bobPeriod` give it the slow float an idle dialogue or pause panel usually has. The offset goes into `getAbsolutePosition()`, which is the one hook both bounds paths funnel through, so the panel is clickable WHERE IT IS DRAWN and so is every widget inside it, at any depth. `pos` is left alone, so a caller still reads and writes the resting place rather than a value that moves under them. `currentTransform` was the obvious route and is a dead end: it reaches a renderable's own bounds but not its children's, so a bobbing panel's swallow rectangle would drift off the widgets sitting in it. `ProgressBar` and `UIPanel` now share the shape and colour plumbing, which is how the border came to be drawn two different ways in two widgets that look the same. Two shipped `ProgressBar` defects go with it: - Its border was centred on the bar's own edge, so half of it fell outside the width and height the bar was given and painted over its neighbour, while `getBounds()` knew nothing about those pixels. A square border is four fills tiling the frame exactly; a rounded one is stroked on a path inset by half the border width with its radius pulled in to match. This also fixes a square border at the default width of 1 losing all four corners, since the renderers only generate corner joins above a line width of one. - Destroying one twice threw `Instance is already in pool.` It handed its cached colour back without clearing the handle, and a pool refuses a double release. The bob offset is rounded, which is not cosmetic: the renderers snap a translation with `|0`, truncating toward zero, so an amplitude of 3 moved 4px instead of 6 and spent half the cycle parked at the extremes. Handing the renderer a whole number makes its own snap a no-op, and makes the drawn position and the hit area agree exactly. Closes part 4 of #1722. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NGvtaUNATVCVxD2qcbiY4t
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Closes part 4 of #1722.
UIPanelA rectangular surface with a background, an optional border and children: what every menu, dialog and HUD frame starts by writing by hand.
It is a
UIBaseElement, so children are positioned relative to it and are asked about a click before it is, andblocking(defaulttrue) decides whether a click that misses every child is swallowed rather than reaching the game beneath.One example in this repo had three hand-rolled panels, each repeating the same half dozen renderer calls and the same correction for the way
setColorfolds a colour's own alpha into the global one. That correction is why this is a widget rather than a snippet: leaving it out does not fail, it just renders at the wrong opacity.The bob
bobAmplitude/bobPeriodgive a panel the slow float an idle dialogue or pause panel usually has, so a screen waiting for input does not look frozen.The interesting part is that the panel stays clickable where it is drawn. The offset goes into
getAbsolutePosition(), the one hook both bounds paths funnel through, so the hit area of the panel and of every widget inside it follows the float at any depth.posis untouched, so a caller still reads and writes the resting place.Two routes I tried first and rejected, both measured:
currentTransformreaches a renderable's own bounds but not its children's. A bobbing panel's swallow rectangle would drift off the widgets sitting in it, which is worse than either consistent answer.pos.yreaches everything correctly but silently eats a caller's own write on the next frame. Fixing that needs re-entrancy detection inside theObservableVector3dcallback.Particleoverrides the same methods for the same reason, its placement not being in itsposchain either.The offset is rounded, and that is not cosmetic: the renderers snap a translation with
|0, truncating toward zero, so an amplitude of 3 travelled 4px instead of 6 and spent half the cycle parked at the extremes. Handing the renderer a whole number makes its own snap a no-op, and makes the drawn position and the hit area agree exactly.Two shipped
ProgressBarfixesProgressBarandUIPanelnow share the shape and colour plumbing, which is how the border came to be drawn two different ways in two widgets that look the same.getBounds()knew nothing about those pixels. A square border is now four fills tiling the frame exactly; a rounded one is stroked on a path inset by half the border width with its radius pulled in to match. This also fixes a square border at the default width of 1 losing all four corners, since the renderers only generate corner joins above a line width of one.Instance is already in pool.It handed its cached colour back without clearing the handle, and a pool refuses a double release.Testing
uipanel.spec.jsis new (the widget had none);progressbar.spec.jsandui-interaction.spec.jsgain geometry and pointer coverage, including a button nested in a bobbing panel and a grandchild two levels down.Everything here was mutation-tested. Three of my own tests turned out to be decoration and were rewritten — one asserted on a sine's output where the claim was about the clock behind it, one could not see a
pos-writing bob because the spec never drew, and one "anchors top left" test passed with the line deleted becauseContaineralready pins it. That last one found dead code, which is gone.Full suite green, declarations check clean.