WIP: feat!: layout system and tree scene hierarchy #83

Closed
abart27 wants to merge 102 commits from scene-tree into main
abart27 commented 2026-05-07 07:18:13 +00:00 (Migrated from github.com)

TODO

  • Layout tests (just some basic render_rect comparisons)
  • Test compat in Redux

Breaking Changes

UID slot changes

Various controls now use more UID slots due to internal changes.

button - 2 slots
toggle_button - 2 slots
carrousel_button - 4 slots
combobox - 9 slots
spinner - 9 slots
tabcontrol - n(children) * 2 + 1

Tip: if you find yourself having to fix UID-related breakage often, space out the UIDs in your scene by 20.

Reserved UID

UID math.mininteger is now reserved by ugui and can't be used by library consumers. If you're using that UID, change it to something else.

Control references

While not explicitly documented anywhere, previous versions of ugui somewhat tolerated holding on to a Control instance after it's consumed by ugui.control (or one of the control placing functions like ugui.button).

This behavior is now out the window: reading back from control tables after they're consumed is undefined behavior.

Barely any library consumers rely on this behavior, but if you do, you can re-architect your script to store data in a safe local context instead of smuggling it through ugui.


Templated controls

ugui.control as well as all control spawning functions (e.g. ugui.button) now have an additional parameter, fn.

fn is a callback that's called immediately after the control was placed. Any controls placed within the callback will be parented to the newly placed control.

It's important to note that the controls themselves decide when fn is called. Therefore, it behaves more like a slot, similar to WPF's ContentPresenter.

Many controls now place children by default, but this isn't something public API consumers should care about.

Layout

A rudimentary layout system has been introduced: controls can specify how their children are laid out, with the only layout modes being relative and stack.

background, border, and border_width

It's now possible to specify a background, border, and border_width for any control.

These are drawn before handing over to the control-specific drawing code and are particularly useful when used together with a panel.

Control.rectangle

The Control.rectangle field has been deprecated.

Any usages of it will be internally translated to margins and sizes (see below).

Control positioning and sizing

The positioning and sizing of controls is now controlled via the Control.margin, Control.align, Control.size, and Control.padding properties. See the docs.

Note the Smart[...] units.

Units

A unit system has been introduced.

If you're familiar with CSS units, these are basically a barebones version of that. Here are the docs:

---@alias SmartUnit
---| "0"
---| "auto"
---| string
---
---A size unit specification that can be:
---
---    `"auto"` - natural size
---    `"{}px"` - absolute pixels (e.g. `"100px"`)
---    `"{}%"` - percentage of parent size (e.g. `"50%"`)
---
---Zero literals are treated as `0px` (e.g. `0` => `0px`)
---
---Basic arithmetic operations are also supported: `+`, `-`, `*`, `/`.
---
---    `100px-3%`
---    `auto*10px`
---
---Constraints can also be applied:
---
---    `min(auto,5px)`
---    `max(auto,5px)`
---    `clamp(auto,5px,10px)`


---@alias SmartUnit2
---| "0"
---| "auto"
---| "0 0"
---| "auto auto"
---| string
---
---A two-dimensional unit that is composed of two SmartUnits.
---
---If one component is omitted, it's assumed to be equal to the other component.
---
---    `100px 100px`
---    `100px` (expands to `100px 100px`)
---    `auto 50%`

---@alias SmartAlignment
---| "0"
---| "0%"
---| "0.5"
---| "50%"
---| "1"
---| "100%"
---| "left"
---| "right"
---| "top"
---| "bottom"
---| "center"
---| string
---An alignment unit that specifies how a control is aligned within its parent.
---
---    `0`, `0%` - start of parent
---    `0.5`, `50%` - center of parent
---    `1`, `100%` - end of parent

---@alias SmartAlignment2
---| "0"
---| "0 0"
---| "0%"
---| "0% 0%"
---| "0.5"
---| "0.5 0.5"
---| "50%"
---| "50% 50%"
---| "1"
---| "1 1"
---| "100%"
---| "100% 100%"
---| "left"
---| "left left"
---| "right"
---| "right right"
---| "top"
---| "top top"
---| "bottom"
---| "bottom bottom"
---| "center"
---| "center center"
---| string
---
---A two-dimensional alignment unit that is composed of two SmartAlignments.
---
---If one component is omitted, it's assumed to be equal to the other component.
---
---    `0 0` - top-left corner
---    `0` - top-left corner
---    `50%`, `center`, `center center` - center
---    `left`, `left left` - left edge
---    `right`, `right right` - right edge
---    `top`, `top top` - top edge
---    `bottom`, `bottom bottom` - bottom edge

I understand that the type design for this isn't the most efficient or idiomatic in Lua. Cleaner design would be some lightweight factory that generates these in a more type-safe manner.

But I went for this because it was the simplest and nicest for now. I'm not particularly attached to this design, so fire away lol

# TODO - [ ] Layout tests (just some basic render_rect comparisons) - [ ] Test compat in Redux ## Breaking Changes ### UID slot changes Various controls now use more UID slots due to internal changes. `button` - 2 slots `toggle_button` - 2 slots `carrousel_button` - 4 slots `combobox` - 9 slots `spinner` - 9 slots `tabcontrol` - `n(children) * 2 + 1` Tip: if you find yourself having to fix UID-related breakage often, space out the UIDs in your scene by `20`. ### Reserved UID UID `math.mininteger` is now reserved by ugui and can't be used by library consumers. If you're using that UID, change it to something else. ### Control references While not explicitly documented anywhere, previous versions of ugui somewhat tolerated holding on to a `Control` instance after it's consumed by `ugui.control` (or one of the control placing functions like `ugui.button`). This behavior is now out the window: reading back from control tables after they're consumed is undefined behavior. Barely any library consumers rely on this behavior, but if you do, you can re-architect your script to store data in a safe local context instead of smuggling it through ugui. --------------- ## Templated controls `ugui.control` as well as all control spawning functions (e.g. `ugui.button`) now have an additional parameter, `fn`. `fn` is a callback that's called immediately after the control was placed. Any controls placed within the callback will be parented to the newly placed control. It's important to note that **the controls themselves decide when `fn` is called**. Therefore, it behaves more like a slot, similar to WPF's `ContentPresenter`. Many controls now place children by default, but this isn't something public API consumers should care about. ## Layout A rudimentary layout system has been introduced: controls can specify how their children are laid out, with the only layout modes being `relative` and `stack`. ## `background`, `border`, and `border_width` It's now possible to specify a `background`, `border`, and `border_width` for any control. These are drawn before handing over to the control-specific drawing code and are particularly useful when used together with a `panel`. ## `Control.rectangle` The `Control.rectangle` field has been deprecated. Any usages of it will be internally translated to margins and sizes (see below). ## Control positioning and sizing The positioning and sizing of controls is now controlled via the `Control.margin`, `Control.align`, `Control.size`, and `Control.padding` properties. See [the docs](https://github.com/mupen64/ugui/blob/scene-tree/src/ugui/controls/control.lua). Note the `Smart[...]` units. ## Units A unit system has been introduced. If you're familiar with CSS units, these are basically a barebones version of that. Here are the docs: ```lua ---@alias SmartUnit ---| "0" ---| "auto" ---| string --- ---A size unit specification that can be: --- --- `"auto"` - natural size --- `"{}px"` - absolute pixels (e.g. `"100px"`) --- `"{}%"` - percentage of parent size (e.g. `"50%"`) --- ---Zero literals are treated as `0px` (e.g. `0` => `0px`) --- ---Basic arithmetic operations are also supported: `+`, `-`, `*`, `/`. --- --- `100px-3%` --- `auto*10px` --- ---Constraints can also be applied: --- --- `min(auto,5px)` --- `max(auto,5px)` --- `clamp(auto,5px,10px)` ---@alias SmartUnit2 ---| "0" ---| "auto" ---| "0 0" ---| "auto auto" ---| string --- ---A two-dimensional unit that is composed of two SmartUnits. --- ---If one component is omitted, it's assumed to be equal to the other component. --- --- `100px 100px` --- `100px` (expands to `100px 100px`) --- `auto 50%` ---@alias SmartAlignment ---| "0" ---| "0%" ---| "0.5" ---| "50%" ---| "1" ---| "100%" ---| "left" ---| "right" ---| "top" ---| "bottom" ---| "center" ---| string ---An alignment unit that specifies how a control is aligned within its parent. --- --- `0`, `0%` - start of parent --- `0.5`, `50%` - center of parent --- `1`, `100%` - end of parent ---@alias SmartAlignment2 ---| "0" ---| "0 0" ---| "0%" ---| "0% 0%" ---| "0.5" ---| "0.5 0.5" ---| "50%" ---| "50% 50%" ---| "1" ---| "1 1" ---| "100%" ---| "100% 100%" ---| "left" ---| "left left" ---| "right" ---| "right right" ---| "top" ---| "top top" ---| "bottom" ---| "bottom bottom" ---| "center" ---| "center center" ---| string --- ---A two-dimensional alignment unit that is composed of two SmartAlignments. --- ---If one component is omitted, it's assumed to be equal to the other component. --- --- `0 0` - top-left corner --- `0` - top-left corner --- `50%`, `center`, `center center` - center --- `left`, `left left` - left edge --- `right`, `right right` - right edge --- `top`, `top top` - top edge --- `bottom`, `bottom bottom` - bottom edge ``` I understand that the type design for this isn't the most efficient or idiomatic in Lua. Cleaner design would be some lightweight factory that generates these in a more type-safe manner. But I went for this because it was the simplest and nicest for now. I'm not particularly attached to this design, so fire away lol
copilot-pull-request-reviewer[bot] (Migrated from github.com) reviewed 2026-05-10 09:55:14 +00:00
copilot-pull-request-reviewer[bot] (Migrated from github.com) left a comment

Pull request overview

This PR restructures ugui’s immediate-mode “scene” from a flat list into a parent/child scene tree and introduces a new margin/size/align/padding-based layout + “SmartUnit” system, with several controls now composed from child controls via a placement callback.

Changes:

  • Replace the 1D scene with a SceneNode tree rooted at a reserved math.mininteger panel.
  • Add a rudimentary layout pass (natural measurement + unit resolution + alignment) and a render_rect concept for post-layout rendering bounds.
  • Update multiple controls to place internal children (labels, buttons, scrollbars, dropdowns) via registry place(...) and the new fn callback parameter.

Reviewed changes

Copilot reviewed 25 out of 25 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
src/ugui/core.lua Frame lifecycle updated to build a root panel each frame and run z-sort/layout/render passes.
src/ugui/internal.lua Scene-tree traversal, layout, and render pipeline added; various per-control layout hacks introduced.
src/ugui/helpers.lua Tree traversal utilities added; SmartUnit / SmartAlignment parsing and resolving implemented.
src/ugui/styler.lua Drawing updated to use render_rect; some controls now rely on child controls for text/icons.
src/ugui/nineslice.lua Nineslice rendering updated to use render_rect for caching/drawing.
src/ugui/controls/control.lua Control docs updated: deprecate rectangle, add margin/size/align/padding.
src/ugui/controls/panel.lua New inert container control used as the scene root and for composition.
src/ugui/controls/button.lua Button now composes a child label via registry place.
src/ugui/controls/toggle_button.lua ToggleButton now composes a child label via registry place.
src/ugui/controls/carrousel_button.lua CarrouselButton now composes arrow/text/arrow labels via registry place.
src/ugui/controls/combobox.lua ComboBox reworked to compose labels and (editable mode) textbox/button + dropdown listbox.
src/ugui/controls/listbox.lua ListBox reworked to optionally spawn scrollbars and provide a measure implementation.
src/ugui/controls/menu.lua Menu reworked for tree parenting + measuring + submenu placement.
src/ugui/controls/numberbox.lua NumberBox adds optional sign-toggle button via registry place and adds measure.
src/ugui/controls/textbox.lua TextBox logic/draw updated for render_rect; adds measure.
src/ugui/controls/scrollbar.lua Scrollbar logic/draw updated for render_rect; wrapper accepts fn.
src/ugui/controls/joystick.lua Joystick logic updated for render_rect; wrapper accepts fn.
src/ugui/controls/trackbar.lua Wrapper updated to accept fn; minor formatting.
src/ugui/controls/spinner.lua Spinner reworked to a panel-composed control using margin/size and child buttons/textbox.
src/ugui/controls/tabcontrol.lua TabControl reworked as a panel with child toggle buttons; layout hack relies on tree.
demos/tooltips.lua Demo UIDs adjusted for new reserved UID/slot behavior.
demos/overlapping_controls.lua Demo updated to use margin/size, alignment, and child placement callback.
demos/layout.lua New demo showcasing alignment, percentage sizing, menus, listbox scroll, and tabcontrol.
build.py Include new panel.lua in build ordering.
Comments suppressed due to low confidence (3)

src/ugui/internal.lua:375

  • Input processing hit-tests using control.rectangle, but the layout pass later adjusts data.render_rect for some controls (e.g. menu overflow avoidance, listbox content area shrinking). This can desync rendering vs hover/click detection (controls render at render_rect but are clickable at the old rectangle). Consider hit-testing against ugui.internal.control_data[control.uid].render_rect (or centralize this via is_point_inside_control) here.
            -- Determine the clicked control if we haven't already
            if clicked_control == nil and effective_hittestable then
                if ugui.internal.is_mouse_just_down() then
                    if is_point_inside_rectangle(ugui.internal.mouse_down_position, control.rectangle) then
                        clicked_control = control
                        keyboard_captured_control = node
                        mouse_captured_control = node
                    end
                end
            end

            -- Determine the hovered control if we haven't already
            if ugui.internal.hovered_control == nil and effective_hittestable then
                if is_point_inside_rectangle(ugui.internal.environment.mouse_position, control.rectangle) then
                    ugui.internal.hovered_control = control.uid

src/ugui/controls/toggle_button.lua:31

  • ToggleButton.is_checked is documented as optional (boolean?, with nil treated as unchecked), but validate currently asserts it must be a boolean. This turns a previously-allowed nil into a hard error. Allow nil and default to false in logic if needed.
    ---@param control ToggleButton
    validate = function(control)
        ugui.registry.button.validate(control)
        ugui.internal.assert(type(control.is_checked) == 'boolean', 'expected is_checked to be boolean')
    end,

src/ugui/controls/combobox.lua:150

  • ComboBox.selected_index is documented as optional (integer?, nil = no selection), and the drawing code already handles nil, but validate currently asserts type(control.selected_index) == 'number'. This forces callers to always provide a number and contradicts the API docs/behavior. Allow nil in validation.
    ---@param control ComboBox
    validate = function(control)
        ugui.internal.assert(type(control.items) == 'table', 'expected items to be table')
        ugui.internal.assert(type(control.selected_index) == 'number', 'expected selected_index to be number')
    end,

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

## Pull request overview This PR restructures ugui’s immediate-mode “scene” from a flat list into a parent/child scene tree and introduces a new margin/size/align/padding-based layout + “SmartUnit” system, with several controls now composed from child controls via a placement callback. **Changes:** - Replace the 1D `scene` with a `SceneNode` tree rooted at a reserved `math.mininteger` panel. - Add a rudimentary layout pass (natural measurement + unit resolution + alignment) and a `render_rect` concept for post-layout rendering bounds. - Update multiple controls to place internal children (labels, buttons, scrollbars, dropdowns) via registry `place(...)` and the new `fn` callback parameter. ### Reviewed changes Copilot reviewed 25 out of 25 changed files in this pull request and generated 10 comments. <details> <summary>Show a summary per file</summary> | File | Description | | ---- | ----------- | | src/ugui/core.lua | Frame lifecycle updated to build a root panel each frame and run z-sort/layout/render passes. | | src/ugui/internal.lua | Scene-tree traversal, layout, and render pipeline added; various per-control layout hacks introduced. | | src/ugui/helpers.lua | Tree traversal utilities added; SmartUnit / SmartAlignment parsing and resolving implemented. | | src/ugui/styler.lua | Drawing updated to use `render_rect`; some controls now rely on child controls for text/icons. | | src/ugui/nineslice.lua | Nineslice rendering updated to use `render_rect` for caching/drawing. | | src/ugui/controls/control.lua | Control docs updated: deprecate `rectangle`, add margin/size/align/padding. | | src/ugui/controls/panel.lua | New inert container control used as the scene root and for composition. | | src/ugui/controls/button.lua | Button now composes a child label via registry `place`. | | src/ugui/controls/toggle_button.lua | ToggleButton now composes a child label via registry `place`. | | src/ugui/controls/carrousel_button.lua | CarrouselButton now composes arrow/text/arrow labels via registry `place`. | | src/ugui/controls/combobox.lua | ComboBox reworked to compose labels and (editable mode) textbox/button + dropdown listbox. | | src/ugui/controls/listbox.lua | ListBox reworked to optionally spawn scrollbars and provide a `measure` implementation. | | src/ugui/controls/menu.lua | Menu reworked for tree parenting + measuring + submenu placement. | | src/ugui/controls/numberbox.lua | NumberBox adds optional sign-toggle button via registry `place` and adds `measure`. | | src/ugui/controls/textbox.lua | TextBox logic/draw updated for `render_rect`; adds `measure`. | | src/ugui/controls/scrollbar.lua | Scrollbar logic/draw updated for `render_rect`; wrapper accepts `fn`. | | src/ugui/controls/joystick.lua | Joystick logic updated for `render_rect`; wrapper accepts `fn`. | | src/ugui/controls/trackbar.lua | Wrapper updated to accept `fn`; minor formatting. | | src/ugui/controls/spinner.lua | Spinner reworked to a panel-composed control using margin/size and child buttons/textbox. | | src/ugui/controls/tabcontrol.lua | TabControl reworked as a panel with child toggle buttons; layout hack relies on tree. | | demos/tooltips.lua | Demo UIDs adjusted for new reserved UID/slot behavior. | | demos/overlapping_controls.lua | Demo updated to use margin/size, alignment, and child placement callback. | | demos/layout.lua | New demo showcasing alignment, percentage sizing, menus, listbox scroll, and tabcontrol. | | build.py | Include new `panel.lua` in build ordering. | </details> <details> <summary>Comments suppressed due to low confidence (3)</summary> **src/ugui/internal.lua:375** * Input processing hit-tests using `control.rectangle`, but the layout pass later adjusts `data.render_rect` for some controls (e.g. menu overflow avoidance, listbox content area shrinking). This can desync rendering vs hover/click detection (controls render at `render_rect` but are clickable at the old rectangle). Consider hit-testing against `ugui.internal.control_data[control.uid].render_rect` (or centralize this via `is_point_inside_control`) here. ``` -- Determine the clicked control if we haven't already if clicked_control == nil and effective_hittestable then if ugui.internal.is_mouse_just_down() then if is_point_inside_rectangle(ugui.internal.mouse_down_position, control.rectangle) then clicked_control = control keyboard_captured_control = node mouse_captured_control = node end end end -- Determine the hovered control if we haven't already if ugui.internal.hovered_control == nil and effective_hittestable then if is_point_inside_rectangle(ugui.internal.environment.mouse_position, control.rectangle) then ugui.internal.hovered_control = control.uid ``` **src/ugui/controls/toggle_button.lua:31** * `ToggleButton.is_checked` is documented as optional (`boolean?`, with `nil` treated as unchecked), but `validate` currently asserts it must be a boolean. This turns a previously-allowed `nil` into a hard error. Allow `nil` and default to `false` in logic if needed. ``` ---@param control ToggleButton validate = function(control) ugui.registry.button.validate(control) ugui.internal.assert(type(control.is_checked) == 'boolean', 'expected is_checked to be boolean') end, ``` **src/ugui/controls/combobox.lua:150** * `ComboBox.selected_index` is documented as optional (`integer?`, nil = no selection), and the drawing code already handles `nil`, but `validate` currently asserts `type(control.selected_index) == 'number'`. This forces callers to always provide a number and contradicts the API docs/behavior. Allow `nil` in validation. ``` ---@param control ComboBox validate = function(control) ugui.internal.assert(type(control.items) == 'table', 'expected items to be table') ugui.internal.assert(type(control.selected_index) == 'number', 'expected selected_index to be number') end, ``` </details> --- 💡 <a href="/mupen64/ugui/new/main?filename=.github/instructions/*.instructions.md" class="Link--inTextBlock" target="_blank" rel="noopener noreferrer">Add Copilot custom instructions</a> for smarter, more guided reviews. <a href="https://docs.github.com/en/copilot/customizing-copilot/adding-repository-custom-instructions-for-github-copilot" class="Link--inTextBlock" target="_blank" rel="noopener noreferrer">Learn how to get started</a>.
@ -14,0 +79,4 @@
},
is_enabled = control.is_enabled,
text = data.open and '[icon:arrow_up]' or '[icon:arrow_down]',
}) then
copilot-pull-request-reviewer[bot] (Migrated from github.com) commented 2026-05-10 09:55:13 +00:00

place uses data.render_rect to position the editable textbox/button and dropdown listbox, but render_rect is set during the later layout pass. When the combobox is first shown (or moved/resized this frame), these children can be positioned using stale/zero geometry. Prefer placing children using relative margin/size within the combobox callback, or derive geometry from the current control.rectangle inputs (and/or initialize render_rect before using it).

`place` uses `data.render_rect` to position the editable textbox/button and dropdown listbox, but `render_rect` is set during the later layout pass. When the combobox is first shown (or moved/resized this frame), these children can be positioned using stale/zero geometry. Prefer placing children using relative `margin`/`size` within the combobox callback, or derive geometry from the current `control.rectangle` inputs (and/or initialize `render_rect` before using it).
@ -18,0 +30,4 @@
if not x_overflow then
data.scroll_x = 0
end
copilot-pull-request-reviewer[bot] (Migrated from github.com) commented 2026-05-10 09:55:13 +00:00

place computes overflow and positions scrollbars using data.natural_size / data.render_rect, but both are produced during the end-of-frame layout pass. On first appearance (or when size changes this frame) these values can be stale/zero, leading to missing or incorrectly placed scrollbars. Consider basing scrollbar placement on the current control inputs (control.rectangle / margin+size), or initializing render_rect/natural_size before using them here, or deferring overflow decisions to the layout pass.

`place` computes overflow and positions scrollbars using `data.natural_size` / `data.render_rect`, but both are produced during the end-of-frame layout pass. On first appearance (or when size changes this frame) these values can be stale/zero, leading to missing or incorrectly placed scrollbars. Consider basing scrollbar placement on the current control inputs (`control.rectangle` / margin+size), or initializing `render_rect`/`natural_size` before using them here, or deferring overflow decisions to the layout pass.
copilot-pull-request-reviewer[bot] (Migrated from github.com) commented 2026-05-10 09:55:14 +00:00

one_item_scroll_y is computed as 1 / #control.items without guarding for an empty list. If items is {} (which is allowed and used in demos), this becomes division by zero and will break scrolling logic. Add an early return / guard path when #control.items == 0.

`one_item_scroll_y` is computed as `1 / #control.items` without guarding for an empty list. If `items` is `{}` (which is allowed and used in demos), this becomes division by zero and will break scrolling logic. Add an early return / guard path when `#control.items == 0`.
copilot-pull-request-reviewer[bot] (Migrated from github.com) commented 2026-05-10 09:55:13 +00:00

MenuResult.dismissed is documented as "dismissed by clicking outside", but the new logic never sets dismissed on outside clicks (it only sets it when an item is selected). This will leave menus stuck open unless callers implement their own dismissal. Consider restoring the previous outside-click detection (using is_mouse_just_down() + hit-test against the menu bounds, and accounting for submenus as needed).

`MenuResult.dismissed` is documented as "dismissed by clicking outside", but the new `logic` never sets `dismissed` on outside clicks (it only sets it when an item is selected). This will leave menus stuck open unless callers implement their own dismissal. Consider restoring the previous outside-click detection (using `is_mouse_just_down()` + hit-test against the menu bounds, and accounting for submenus as needed).
copilot-pull-request-reviewer[bot] (Migrated from github.com) commented 2026-05-10 09:55:12 +00:00

ugui.toggle_button accepts an fn callback, but it is currently ignored because the wrapper calls ugui.control(control, 'toggle_button') without passing fn. Pass fn through so child controls can be parented as documented.

`ugui.toggle_button` accepts an `fn` callback, but it is currently ignored because the wrapper calls `ugui.control(control, 'toggle_button')` without passing `fn`. Pass `fn` through so child controls can be parented as documented.
copilot-pull-request-reviewer[bot] (Migrated from github.com) commented 2026-05-10 09:55:11 +00:00

ugui.internal.root is a SceneNode object, not an array. Iterating with for i = 1, #ugui.internal.root will always yield 0, so previous_uids stays empty and lifecycle events (added) will fire every frame. Traverse the scene tree (e.g. via ugui.internal.foreach_node_from_root) to populate previous_uids instead.

`ugui.internal.root` is a `SceneNode` object, not an array. Iterating with `for i = 1, #ugui.internal.root` will always yield 0, so `previous_uids` stays empty and lifecycle events (`added`) will fire every frame. Traverse the scene tree (e.g. via `ugui.internal.foreach_node_from_root`) to populate `previous_uids` instead.
@ -212,0 +293,4 @@
---@param uid UID?
---@return SceneNode?
ugui.internal.find_node = function(uid)
if uid == nil then return nil end
copilot-pull-request-reviewer[bot] (Migrated from github.com) commented 2026-05-10 09:55:12 +00:00

foreach_node documents that returning false from the callback stops traversal early, but the implementation never returns false (it returns nil), so early-exit never propagates. This breaks callers like find_node (can't short-circuit) and makes control_from_point traversal semantics incorrect. Make foreach_node return a boolean and propagate false up the call stack when stopping early.

`foreach_node` documents that returning `false` from the callback stops traversal early, but the implementation never returns `false` (it returns `nil`), so early-exit never propagates. This breaks callers like `find_node` (can't short-circuit) and makes `control_from_point` traversal semantics incorrect. Make `foreach_node` return a boolean and propagate `false` up the call stack when stopping early.
@ -212,0 +577,4 @@
if percent then
return tonumber(percent) / 100
end
end
copilot-pull-request-reviewer[bot] (Migrated from github.com) commented 2026-05-10 09:55:12 +00:00

resolve_unit2 assumes unit is a string and calls unit:match(...). However the unit docs describe accepting numeric literals (e.g. 0), which would crash here. Consider normalizing inputs (e.g. unit = tostring(unit)) and similarly handling non-string inputs in resolve_alignment2 / resolve_alignment to match the documented API.

`resolve_unit2` assumes `unit` is a string and calls `unit:match(...)`. However the unit docs describe accepting numeric literals (e.g. `0`), which would crash here. Consider normalizing inputs (e.g. `unit = tostring(unit)`) and similarly handling non-string inputs in `resolve_alignment2` / `resolve_alignment` to match the documented API.
@ -393,2 +418,2 @@
ugui.internal.hovered_control = nil
end
local hovered_node = ugui.internal.hovered_control and ugui.internal.find_node(ugui.internal.hovered_control) or nil
if hovered_node and hovered_node.control.is_enabled == false then
copilot-pull-request-reviewer[bot] (Migrated from github.com) commented 2026-05-10 09:55:13 +00:00

ListBox render-rect adjustment for scrollbars appears to subtract thickness from the wrong axis: x_overflow (horizontal scrollbar) currently reduces render_rect.width, and y_overflow (vertical scrollbar) reduces render_rect.height. This is inverted vs the expected behavior (horizontal scrollbar should reduce height; vertical scrollbar should reduce width), and will mis-size the listbox content area.

ListBox render-rect adjustment for scrollbars appears to subtract thickness from the wrong axis: `x_overflow` (horizontal scrollbar) currently reduces `render_rect.width`, and `y_overflow` (vertical scrollbar) reduces `render_rect.height`. This is inverted vs the expected behavior (horizontal scrollbar should reduce height; vertical scrollbar should reduce width), and will mis-size the listbox content area.
copilot-pull-request-reviewer[bot] (Migrated from github.com) commented 2026-05-10 09:55:13 +00:00

parent_is_menu checks node.parent.control.type == 'menu', but type is stored on the SceneNode (node.parent.type), not on the Control table. As written, parent_is_menu will always be false and submenu overflow handling will never use the "snake left" path. Use node.parent.type (and guard node.parent for root).

`parent_is_menu` checks `node.parent.control.type == 'menu'`, but `type` is stored on the `SceneNode` (`node.parent.type`), not on the `Control` table. As written, `parent_is_menu` will always be false and submenu overflow handling will never use the "snake left" path. Use `node.parent.type` (and guard `node.parent` for root).
abart27 commented 2026-06-22 14:28:25 +00:00 (Migrated from github.com)

The dynamic layout part (stacks, scrollviewers, etc...) is blowing up in complexity.

For that reason, we'll delay the implementation of that functionality indefinitely.

For now, this PR will only provide basic layout functionality with no dynamic layouts.

The dynamic layout part (stacks, scrollviewers, etc...) is blowing up in complexity. For that reason, we'll delay the implementation of that functionality indefinitely. For now, this PR will only provide basic layout functionality with no dynamic layouts.

Pull request closed

Sign in to join this conversation.
No description provided.