It's best used for developer tools or simple UIs that don't have complex layout constraints.
For what it's worth, I'm building all of my game's UI using a pseudo-imgui framework, but I've had to do manual layout in specific places, and I updated the layout engine to run a second pass in specific cases (dynamically sized elements, primarily when text is being auto-wrapped to fit available space). This sort of stuff is only possible when you control things end-to-end.
In practice even these IMGUI frameworks don't generally do their layout in "one pass", it just appears to be a single pass to you. oui and its derivative layout.h both do a size calculation pass and then an arranging pass, for example. I originally used layout.h's algorithm, but eventually designed a new one which operates in ~3 passes:
* 1. Calculate minimum sizes and, for containers with multiple rows/columns, construct 'runs' of sequential boxes in a given row/column.
* 2a. For containers with wrapping enabled, scan over runs and when we find one that's too big for its container's available space, split controls from one run into a new one.
* 2b. For containers with children, scan through their children and grow any children that are meant to expand into available space. (You can't do this until you've measured everything and wrapped.)
* 2c. For any containers where you wrapped controls or expanded controls, recalculate their size and throw out the result from 1, since wrapping/expanding potentially changes their size.
* 2d. For simplicity you can introduce an alternative version of pass 2 for 'grid layout', where children all have fixed sizes and predictable wrapping. I've only started doing this recently, but it's great for things like listboxes and dataviews. If you do this, you don't need to do the multiple subpasses of 2a/2b/2c, and arranging becomes easier.
* 3. Now that you've measured everything and distributed boxes into rows/columns, you can scan through each 'run' and put every control into its place, applying things like centering and RTL/LTR modes.
I do think "it should be possible to efficiently perform layout for your whole UI from scratch every frame" is a good principle, it pressures you to architect your application in a cleaner, less fragile way. But sometimes you really want to retain stuff, like shaped glyphs for big blocks of unicode text, etc. Right now my game runs over 120FPS on a terrible laptop from 2015 and around 800FPS on my 3-year-old workstation, but a major portion of the CPU time is all spent doing layout. That's not great.
Did you profile the layout code? How many UI elements do you display normally? And what is the O complexity of the layout algorithm above? My intuition is that even if it looks like a lot of code, it should be incredibly fast for at least hundreds of elements.
I profile aggressively using Superluminal. All of the passes are O(N), it's mostly an issue of the amount of time it takes to go through and lay out a few thousand boxes with constraints and configuration flags set. There aren't many 'bottlenecks' and it's more just a bunch of CPU time spread across the whole algorithm.
For what it's worth, I'm building all of my game's UI using a pseudo-imgui framework, but I've had to do manual layout in specific places, and I updated the layout engine to run a second pass in specific cases (dynamically sized elements, primarily when text is being auto-wrapped to fit available space). This sort of stuff is only possible when you control things end-to-end.
In practice even these IMGUI frameworks don't generally do their layout in "one pass", it just appears to be a single pass to you. oui and its derivative layout.h both do a size calculation pass and then an arranging pass, for example. I originally used layout.h's algorithm, but eventually designed a new one which operates in ~3 passes:
* 1. Calculate minimum sizes and, for containers with multiple rows/columns, construct 'runs' of sequential boxes in a given row/column.
* 2a. For containers with wrapping enabled, scan over runs and when we find one that's too big for its container's available space, split controls from one run into a new one.
* 2b. For containers with children, scan through their children and grow any children that are meant to expand into available space. (You can't do this until you've measured everything and wrapped.)
* 2c. For any containers where you wrapped controls or expanded controls, recalculate their size and throw out the result from 1, since wrapping/expanding potentially changes their size.
* 2d. For simplicity you can introduce an alternative version of pass 2 for 'grid layout', where children all have fixed sizes and predictable wrapping. I've only started doing this recently, but it's great for things like listboxes and dataviews. If you do this, you don't need to do the multiple subpasses of 2a/2b/2c, and arranging becomes easier.
* 3. Now that you've measured everything and distributed boxes into rows/columns, you can scan through each 'run' and put every control into its place, applying things like centering and RTL/LTR modes.
I do think "it should be possible to efficiently perform layout for your whole UI from scratch every frame" is a good principle, it pressures you to architect your application in a cleaner, less fragile way. But sometimes you really want to retain stuff, like shaped glyphs for big blocks of unicode text, etc. Right now my game runs over 120FPS on a terrible laptop from 2015 and around 800FPS on my 3-year-old workstation, but a major portion of the CPU time is all spent doing layout. That's not great.