MiniLayout
Since LD57 I've made a bunch of little tools and libraries for my engine (built on top of SDL2) to help shave off annoying or time consuming parts of game dev. I thought I would do a few posts talking about them. The first one is for creating immediate mode UIs, simply called MiniLayout (and LAYN internally which stands for Layouts in Nim). It's inspired by Nic Barker's Clay after watching a video on him talking through his algorithm.
Programming interfaces is a slog. I find them especially limiting in a jam since often the quickest approach was to just hard code things, making any changes later on a time consuming chore. My goal was to end up with something with minimal boilerplate. Just describe what you want to draw, automatically support things like text, sprites and handling interactions.
Here's a simple example and the code used to generate it:

``` frame: style game.sprites.fancycornersframe size 160, HUG direction VERTICAL padding 16 gap 8 shadow 0, 2 shadow_color CLIGHTGREY
for i in 0 ..< 3: frame: interactive buttontag[i] if hovered(): style game.sprites.rowhovered else: style game.sprites.row width FILL padding 8 gap 8 align CENTER sprite: content game.sprites.little_face text: content "Hello world!"
draw(game.layout) ```
To quickly explain what some of this means:
- A frame is a container. By default it hugs its content, lays them out horizontally and has no fill color or style. (A style is just a Sprite pointer that is then used for ninepatch drawing.)
- Sections like frame:, sprite:, text: are macros which begin a new Element of that type, make any further Element declared within its children and automatically call close_element() at the end of the scope block.
- Interactive elements are given a string tag, like interactive "my cool button" which can then be later used to check the state of that interactive element. (if get_hovered(game.layout) == "my cool button": ...)
- The library knows the current element scope as you declare them, so you can easily set parameters based on the interaction state with simple checks like if hovered(): ..., if pressed(): ..., etc.
- Dimensions can be set to FILL to automatically grow to fill any remaining space in the parent.
Calling draw(), which takes a pointer to the MiniLayout struct, is used to finalize the layout and do the calculations before drawing all the elements. The multiple passes used for calculations are very similar to Clay (so you can watch the video above for more in depth explanation for most of these):
- Widths are grown and shrank.
- Text is wrapped.
- Heights are adjusted to fit content.
- Heights are grown and shrank.
- Positions are calculated.
- Scroll requests are handled.
- Interactive elements are cached for the next frame.
The last one is really important because otherwise you have this problem: How do you know what's interacted with during a frame if you don't know how the layout will be calculated until all the layout is defined and drawn? So my solution was that all interactive elements have a lightweight copy of some of their important details (like position, interactive tag, size) kept for the next frame, which does introduce a one frame latency but I think that's a mild tradeoff for all the other wins introduced.
The lack of boilerplate is achieved through a few means: First, Nim has a lot of flexibility in its syntax. foo(bar), bar.foo() and foo bar are all the same and just based on your preference. While I don't normally use the last style, it's great here to reduce noise and give the layout areas a distinct different style than the rest of my code. Second, MiniLayout internally has a single private struct that all the parameter procedure calls operate on. So width 100, which is the same as width(100), operates on LAYN.target.cons.size.x where target is a pointer to the current Element in scope.
A side benefit to this system is you can define and draw multiple layouts per frame as you like, in between drawing the rest of the game. There's no conflicts because after drawing we no longer care about any of MiniLayout's state (except for cached interactive elements).
There's tons more not covered here. Like frames can be set as scrollable and automatically clip their scrolled content, interactive elements can automatically be navigated through on controller/keyboard with no additional code, absolute positioning with parameters like position RIGHT, TOP, text wrapping, etc, etc, but thought I'd keep it brief.
It's been a blast working on it so far and excited to use it more in the future!