It's really easy to get started with immediate mode and bust out some simple UIs, but the second you start trying to involve dynamically sized elements and responsive layouts -- abandon all hope. The fact it has to calculate everything in a single pass makes these things hard/impossible. Coming from a strong CSS/React background I find the limitation maddening.
It sounds like limitations of egui / Rust instead of immediate mode GUI. I've made flexbox-like layout systems in immediate mode GUIs and found them far easier than retained mode, since everything is redrawn every frame.
That's fair. I don't have experience with other immediate mode libraries. It's good to hear that it's not an intrinsic limitation
https://github.com/emilk/egui?tab=readme-ov-file#layout Here the author discusses the issue directly. They note that there are solutions to the issue, but that they all come with (in their opinion) significant drawbacks.
For my use case, if I have to do a lot of manual work to achieve what I consider behavior that should be handled by the framework, then I don't find that compelling and am inclined to use a retained mode implementation.
I'm using egui to build an app with a mobile ui and I'm really enjoying it so far. The main reason I chose egui is because I need tight integration with wgpu and this is really seamless with egui.
In the process of building my app I have also created a couple of crates for egui that add drag and drop sorting, infinite scroll and other utilities.
In the example showcasing my crates I also try to show that you can make a pretty ui with complex layouts using egui (check the gallery and chat example):
https://lucasmerlin.github.io/hello_egui/
I've had to spend a lot of time improving egui and it's ecosystem in the process of building my app but it seems to be worth it.
If you're not building a graphical app it probably makes more sense to use something like tauri or flutter as the gui to build a cross platform app with rust, at least until it's gui ecosystem matures.
In Dear ImGui for instance, you can get the size of view containers and go as far as placing your UI elements at absolute positions (and also use a lower-level ImDrawList for custom rendering - which is also how you can extend Dear ImGui with your own custom-rendered UI elements, and all that absolute positioning and custom drawing is compatible with the automatic layout system).
The common misconception is that immediate mode UIs don't persist state between frames, but they absolutely do (it's just hidden from the API user). The 'immediate mode idea' is only about the public API, not the internal implementation (e.g. internally there could even be a traditional retained mode widget tree that's only modified, but not rebuilt each frame).
Err.. but I'm not talking about placing entities at absolute positions. egui supports that - that's how we get one widget in the center of the screen :)
I'm talking about having two labels in the center of the screen, making them full width, making the text word wrap, and having each label flow properly when resizing the window. That requires calculating word wrap for each label plus knowing where labels visually higher on the y-axis have decided to place themselves after considering word wrapping. Labels beneath other labels should get pushed downward when word wrapping occurs.
That seems really hard to achieve in immediate mode libraries without effectively recreating retained mode functionality yourself.
A layout engine is required in both cases. It's not necessarily more difficult in immediate mode than retained mode.
TFA demonstrates this exact functionality without "recreating retained mode". Check out the "Layout" section and note that the button is shrunk to the label, all of which is centered. The layout engine becomes an emergent property of the widgets rather than an algorithm evaluating a tree. Here, the code is the tree.
> That seems really hard to achieve in immediate mode libraries without effectively recreating retained mode functionality yourself.
The purpose of an IMGUI is to handle whatever data retention will simplify user's life. If you want to extend the IMGUI in some case it's perfectly adequate to do some that work yourself. Ideally the IMGUI infrastructure would make it as painless as possible.
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.
There was a post long time ago here on HN about a PhD that discovered immediate mode gui would be more efficient than the existing paradigm, but that ship has sailed
I don't think primitive layout is necessarily a limitation of the immediate mode GUI paradigm. It just requires layout to be deferred until the end of the frame. And of course to do that performantly, you'd likely need some caching between frames.
But here's the kicker: egui already does cache things between frames—for accessibility support, it already builds a full retained widget tree! From there it's not a huge jump to cache layout too. I really wish someone would experiment with this idea. Maybe Gio will be the ones to do it.
egui does not support putting two widgets in the center of the screen: https://github.com/emilk/egui/issues/3211
It's really easy to get started with immediate mode and bust out some simple UIs, but the second you start trying to involve dynamically sized elements and responsive layouts -- abandon all hope. The fact it has to calculate everything in a single pass makes these things hard/impossible. Coming from a strong CSS/React background I find the limitation maddening.
... that said, I'm still using it to build a prototype UI for https://ant.care/ (https://github.com/MeoMix/symbiants) because it's the best thing I've found so far.
I'm crossing my fingers that Bevy's UI story (or Kayak https://github.com/StarArawn/kayak_ui) becomes significantly more fleshed out sooner rather than later. Bevy 0.13 should have lots more in this area though (https://github.com/bevyengine/bevy/discussions/9538)