I made some good progress on UI this month, despite an initial moment of doubt. ProdUI isn’t moving the game project forward, and I could probably just make a few adjustments to Balleg’s menu system that would shore up its deficiencies. (Lack of scrolling in menus is the big issue, and with newfound perspective, easily fixed.) I archived my ProdUI work, and dug out the most recent version of Balleg that is functional. After playing through it (haven’t done so since March) and reviewing the menu system, I started writing a simple vertical menu that could be parachuted into the engine.
Starting from scratch made me appreciate what I had accomplished in ProdUI. A lot of the complexity I’ve been dealing with is related to attempting a WIMP interface, and with the exception of genres like simulation and strategy, game front-ends often have just one menu active at a time. Much of ProdUI’s high-level WIMP behavior is handled in the tree root widget, so changing that is just a matter of swapping it out with a new root.
So I took my vertical menu work, turned it into a ProdUI widget, and kept going from there.
More window-frames.
Summary
- Widgets now have innate scrolling support, instead of relying on nested containers
- Simplified implementation of scroll bars, from full widgets to shared ‘plug-in’ functions and state
- Made a menu widget, which is a container for arbitrary item components
- Finished integration of EditField into ProdUI
Built-in scrolling support
All widgets now have the ability to scroll their descendants using the fields scr_x
and scr_y
. I thought about this for a while, and kept putting it off with the concern that it would be difficult to account for every situation where the offsetting would need to be applied. I’m glad I was wrong about this, as the widget tree structure is much simpler now.
It was already possible to scroll things by nesting containers into a viewport widget (which itself was another container). To scroll the content, say, 50 pixels to the right, the viewport moved its child content container 50 pixels left. It was technically sufficient. Where it fell short was that it added more steps to interacting with the content container, to drawing it, to detecting mouse-over state, and to bubbling up events. It would be an exaggeration to say that this meaningfully impacted performance in my WIP demo, but man, it was so annoying having to dig multiple levels deep just to get at the content. It would be even worse with more nesting.
Scroll bars up to this point had been implemented as widgets owned by the viewport. My first attempt used a child widget for the scroll bar thumb, and two widgets for the less and more buttons on the edges. So one scroll bar == four additional widgets in the tree. Later on, I wrote a more integrated scroll bar for EditField with simpler behavior.
With this in mind, I considered ways of merging the viewport, the content container, and the EditField scroll bars all into one widget. I took the following steps:
- Make non-widget scroll bar components which can be installed into widgets arbitrarily. The widget uses a set of common shared functions to detect mouse-over and clicking on sub-components. (Admittedly, this adds a lot of boilerplate code in the widget definitions, but it shouldn’t impact the library user. Much.)
- Enhance
clip_hover
andclip_scissor
to support arbitrary regions. These restrict the mouse-over detection and rendering of descendants, respectively. They were originally booleans: true to limit those features to the parent’s bounding box, and false to not limit them. For example, you could visually crop descendants in a window frame (clip_scissor = true
) while still allowing the user to click on resize sensors outside of the frame (clip_hover = false
). I added a manual mode which uses a separate set of XYWH variables for arbitrary restrictions. - Add a viewport rectangle to the container definition. This allows a portion of the container to be dedicated to displaying descendants, while other parts can be carved out for embedded sensors such as scroll bars. The aforementioned
clip_hover
andclip_sensor
match the viewport. - Add
doc_w
anddoc_h
to the container, representing the scrollable document area. (In the old viewport->widget arrangement, the content container’s dimensions were used for this purpose.) - Add
scr_x
andscr_y
fields to the widget base metatable. This allows every widget to report a scroll offset, even if they never actually use scrolling.
Scroll anchoring
Some time after the scroll refactor, I found a need for occasionally exempting either child widgets or arbitrary widget content from scrolling. This can be helpful in situations where you want to embed a child control into something without going through the trouble of nesting it into another container. My dumb solution was to make a second pair of scrolling offset variables, scr2_x
and scr2_y
, and write a second set of scrolling methods that work on those variables instead of the main ones. The core UI code knows about scr_x
and scr_y
, but not the second pair. It’s stupid — really stupid — but it works.
EditField Status
I ported EditField from DemoUI (rest in peace) to ProdUI a few months ago. In an attempt to make it modular across different host libraries, I had crammed a bunch of functionality from the demo widget into the core object table. This included scrolling, which lead to a conflict with ProdUI’s own scrolling implementation. The fix required stripping non-essential variables and methods out of EditField, and affixing them to the client widget. The code is in dire need of a refactor — I hadn’t looked at EditField’s source in a while, and was legitimately disgusted at how complicated it is — but with the exception of the left margin and line numbering, everything seems as functional as it was in the old DemoUI test version.
I no longer expect EditField to be portable across libraries. As always, if you are looking for a standalone text input implementation in LÖVE, I recommend checking out InputField by ReFreezed.
Menus
My first attempt at a menu contained items that all had the same dimensions and spacing. This would be okay for basic lists of text, and I also got a horizontal mode working, but I realized it would be much more versatile if I gave each item its own position and dimension variables.
Why make menus an explicit widget type when a container of widgets could provide similar functionality?
- The main UI virtual cursor, the ‘thimble’, is difficult to manage across nested widgets. Menus can maintain an item selection that is independent of the thimble, and hold onto that selection even after the thimble has been passed to another widget.
- Menu items have less overhead than full widgets. They’re flat, with no array for nested descendants, and their callbacks have to go through the client menu widget (in other words, the UI context has no direct knowledge of them).
- By default, items are rendered one after another in a loop, but menus can also handle rendering themselves with a custom callback function. Items may need less per-instance data as a result, or menus might be able to render items more efficiently as a SpriteBatch or LÖVE Text object.
I plan to make menu widgets the basis for a number of controls, such as list-boxes, right-click menus, and menu bars.
Cascading Graphics State
I rewrote the widget draw loop so that the current graphics state is preserved when rendering a widget’s children. Previously, they were isolated using love.graphics.push("all")
. This could be a fountain of problems, but it also allows easily fading or tinting several descendants at once.
My use of scissor boxes presents issues with cascading scaling and rotation state, as scissors are not part of the transformation stack, but rather apply to screen pixels. As a workaround, I want to somehow fit in the option to render the widget and descendants to a canvas, but I probably won’t touch this for a while as it’s not an immediate need.
Closing
Closer! I’m getting closer. If things go well, I might even have some demo applications to show off in November or December. Fingers crossed. Either way, the plan is that regardless of ProdUI’s state, it needs to be integrated into Bolero 2 and Balleg by the end of the year.
Next update around Nov 30th or so.