WIP: feat!: layout system and tree scene hierarchy #83
No reviewers
Labels
No labels
bug
documentation
duplicate
enhancement
good first issue
help wanted
invalid
question
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
mupen64/ugui!83
Loading…
Reference in a new issue
No description provided.
Delete branch "scene-tree"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
TODO
Breaking Changes
UID slot changes
Various controls now use more UID slots due to internal changes.
button- 2 slotstoggle_button- 2 slotscarrousel_button- 4 slotscombobox- 9 slotsspinner- 9 slotstabcontrol-n(children) * 2 + 1Tip: if you find yourself having to fix UID-related breakage often, space out the UIDs in your scene by
20.Reserved UID
UID
math.minintegeris 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
Controlinstance after it's consumed byugui.control(or one of the control placing functions likeugui.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.controlas well as all control spawning functions (e.g.ugui.button) now have an additional parameter,fn.fnis 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
fnis called. Therefore, it behaves more like a slot, similar to WPF'sContentPresenter.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
relativeandstack.background,border, andborder_widthIt's now possible to specify a
background,border, andborder_widthfor 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.rectangleThe
Control.rectanglefield 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, andControl.paddingproperties. 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:
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
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:
scenewith aSceneNodetree rooted at a reservedmath.minintegerpanel.render_rectconcept for post-layout rendering bounds.place(...)and the newfncallback parameter.Reviewed changes
Copilot reviewed 25 out of 25 changed files in this pull request and generated 10 comments.
Show a summary per file
render_rect; some controls now rely on child controls for text/icons.render_rectfor caching/drawing.rectangle, add margin/size/align/padding.place.place.place.measureimplementation.placeand addsmeasure.render_rect; addsmeasure.render_rect; wrapper acceptsfn.render_rect; wrapper acceptsfn.fn; minor formatting.panel.luain build ordering.Comments suppressed due to low confidence (3)
src/ugui/internal.lua:375
control.rectangle, but the layout pass later adjustsdata.render_rectfor some controls (e.g. menu overflow avoidance, listbox content area shrinking). This can desync rendering vs hover/click detection (controls render atrender_rectbut are clickable at the old rectangle). Consider hit-testing againstugui.internal.control_data[control.uid].render_rect(or centralize this viais_point_inside_control) here.src/ugui/controls/toggle_button.lua:31
ToggleButton.is_checkedis documented as optional (boolean?, withniltreated as unchecked), butvalidatecurrently asserts it must be a boolean. This turns a previously-allowednilinto a hard error. Allowniland default tofalsein logic if needed.src/ugui/controls/combobox.lua:150
ComboBox.selected_indexis documented as optional (integer?, nil = no selection), and the drawing code already handlesnil, butvalidatecurrently assertstype(control.selected_index) == 'number'. This forces callers to always provide a number and contradicts the API docs/behavior. Allownilin validation.💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
@ -14,0 +79,4 @@},is_enabled = control.is_enabled,text = data.open and '[icon:arrow_up]' or '[icon:arrow_down]',}) thenplaceusesdata.render_rectto position the editable textbox/button and dropdown listbox, butrender_rectis 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 relativemargin/sizewithin the combobox callback, or derive geometry from the currentcontrol.rectangleinputs (and/or initializerender_rectbefore using it).@ -18,0 +30,4 @@if not x_overflow thendata.scroll_x = 0endplacecomputes overflow and positions scrollbars usingdata.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 initializingrender_rect/natural_sizebefore using them here, or deferring overflow decisions to the layout pass.one_item_scroll_yis computed as1 / #control.itemswithout guarding for an empty list. Ifitemsis{}(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.MenuResult.dismissedis documented as "dismissed by clicking outside", but the newlogicnever setsdismissedon 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 (usingis_mouse_just_down()+ hit-test against the menu bounds, and accounting for submenus as needed).ugui.toggle_buttonaccepts anfncallback, but it is currently ignored because the wrapper callsugui.control(control, 'toggle_button')without passingfn. Passfnthrough so child controls can be parented as documented.ugui.internal.rootis aSceneNodeobject, not an array. Iterating withfor i = 1, #ugui.internal.rootwill always yield 0, soprevious_uidsstays empty and lifecycle events (added) will fire every frame. Traverse the scene tree (e.g. viaugui.internal.foreach_node_from_root) to populateprevious_uidsinstead.@ -212,0 +293,4 @@---@param uid UID?---@return SceneNode?ugui.internal.find_node = function(uid)if uid == nil then return nil endforeach_nodedocuments that returningfalsefrom the callback stops traversal early, but the implementation never returnsfalse(it returnsnil), so early-exit never propagates. This breaks callers likefind_node(can't short-circuit) and makescontrol_from_pointtraversal semantics incorrect. Makeforeach_nodereturn a boolean and propagatefalseup the call stack when stopping early.@ -212,0 +577,4 @@if percent thenreturn tonumber(percent) / 100endendresolve_unit2assumesunitis a string and callsunit: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 inresolve_alignment2/resolve_alignmentto match the documented API.@ -393,2 +418,2 @@ugui.internal.hovered_control = nilendlocal hovered_node = ugui.internal.hovered_control and ugui.internal.find_node(ugui.internal.hovered_control) or nilif hovered_node and hovered_node.control.is_enabled == false thenListBox render-rect adjustment for scrollbars appears to subtract thickness from the wrong axis:
x_overflow(horizontal scrollbar) currently reducesrender_rect.width, andy_overflow(vertical scrollbar) reducesrender_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.parent_is_menuchecksnode.parent.control.type == 'menu', buttypeis stored on theSceneNode(node.parent.type), not on theControltable. As written,parent_is_menuwill always be false and submenu overflow handling will never use the "snake left" path. Usenode.parent.type(and guardnode.parentfor root).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