Like March, this is essentially a non-update. I got some work done on a WIMP-style UI interface: windows, scroll-bars, buttons, checkboxes, radio groups, all that fun stuff. I had issues writing an input field widget, and started a separate test project to figure that out. Then I got sidetracked with a text printing module that supports virtual tab stops. That last one is nearly ready to release, but I want to experiment a bit with a companion library that works with lists of draw commands instead of requiring everything to be direct function calls.
I’m disabling blog comments due to the volume of spam versus legit messages. Thanks to the folks who posted cool info.
- Worked on UI system
- Worked on text printing library
So, progress on this since March has been pretty good. In the last devlog, uiKit was little more than a handful of rectangles that could be clicked and dragged. Now it kind of looks like a real GUI, albeit one in a prototype stage. I added a few more controls since I took the first video, such as horizontal and vertical sliders. I started working on an input field, but realized pretty quickly that it was much more complicated than buttons and scroll bars, so I created another project folder to work on it isolated from the rest of the toolkit code. More on that in a bit.
The LUIGI source has been a great reference. From it, I’ve grabbed the concept of bubbling events from a source widget “upwards” through its ancestors. Any widget that sees the event can act on it, and has the option to halt it from bubbling further. Events which aren’t halted will eventually reach the root widget, which can implement top-level controls such as tabbing across widgets.
One issue I haven’t resolved: My virtual key-repeat events are always appended to the UI queue after all “real” events, meaning they are technically out of order. I don’t think it will be a big issue, but it could lead to some unexpected behavior during poor framerate conditions.
Plug: there’s a really good standalone text input library by ReFreezed called InputField. It’s very well commented and I’ve grabbed a few snippets from it.
I have a couple of text box implementations: the one I showed in the second video above, and also a much simpler version that basically only supports adding and removing text on the right side. I believe I have them mostly figured out now, and the next step is to integrate the logic back into the main uiKit codebase.
What makes a text input field so much of a bother compared to things like buttons? The problems stem from how Lua handles strings:
- A string in Lua is an immutable sequence of bytes. You cannot change a string in-place. Instead, you need to create a new string using functions from the string library.
- This allows Lua strings to be interned. If
a == "foobar"and
b == "foobar", then internally, Lua stores one copy of
foobarand the two variables contain just a reference to it. There’s more to it than that, but it can be very economical from a memory standpoint, and also when doing equality checks between two strings.
- Lua doesn’t know which interned strings are no longer in use. It needs to clean them up as part of a garbage collection pass.
- Lua did not ship with built-in UTF-8 support until version 5.3, so its string library has no special handling of UTF-8, other than the coincidental fact that single-byte UTF-8 characters map to ASCII.
- LÖVE uses LuaJIT by default, which targets Lua 5.1 compatibility. For this reason, LÖVE includes a copy of the Lua 5.3 utf8 library as a supplemental module.
- One might reasonably assume that the utf8 library would be a UTF-8-compatible version of the string library. This is not the case. Instead, it provides five functions and one string pattern that allow you to convert code points between numbers and strings, count the number of code points (this doubles as a basic encoding checker), and get byte offsets.
- So while it does provide the ability to iterate over glyphs, it still must be used within the context of strings being immutable sequences of bytes.
From this, there are two major concerns:
1: Not all, but many Lua string operations generate garbage. If you want to calculate the position of a portion of text (for example, to render a cursor over a specific character), you’ll most likely have to split it with string.sub() and feed the resulting substring to LÖVE’s Font:getWidth(). Store copies of this information and only recalculate when necessary.
2: You need to be careful about splitting strings with multi-byte UTF-8 code points. Most, if not all functions in LÖVE that accept UTF-8 strings will raise a fatal error if they detect an encoding issue.
The Lua string stuff usually isn’t a problem, but it does get tricky here.
This started as a way to print debug text in my editField demo in a more organized manner. I vacillated between making a bigger library with more features, and one that only provides the minimum necessary. I added and removed some things multiple times while thinking it over. Then I got the flu, and that messed up my decision-making even more.
Ultimately, I added some features which ever-so-slightly elevate it above just being a debug text printer. The demo currently looks like this (the red lines show tab stop locations, and wouldn’t be visible normally):
Here’s an excerpt from the draw code, for the first handful of lines in the upper-left:
love.graphics.setColor(1, 1, 1, 1) qp:print("FPS: ", love.timer.getFPS()) qp:print("FrameDelta: ", love.timer.getAverageDelta()) qp:print("Mem (KB): ", math.floor(collectgarbage("count") * 10) / 10) qp:down() qp:print("(0) VSync: ", love.window.getVSync()) qp:print("(9) GC Mode: ", demo_garbage_modes[demo_garbage_i]) qp:down() qp:print("(Tab) Set Font: ", fonts[font_i].path) qp:print("(1-2) Font Sz: ", font_pt) qp:print("(3-4) V.Padding: ", qp.pad_v) qp:down() -- (etc.)
qp table has an internal cursor position which is updated with every write. Calls to
qp:print()will automatically move the cursor down to the next line and to the left. It uses whatever font is currently set for calculating lengths and distances. Besides drawing to the screen, it can also add text to a bound LÖVE Text object, using the same method calls (with a few exceptions: LÖVE Text objects can only have one active font, for example.)
The biggest concession was to include support for
love.graphics.printf(). This is handled using a separate method (
qp:printf()), and it doesn’t quite work the same as the other methods. LÖVE’s built-in word-wrap, alignment and
coloredtext sequences are too useful to ignore, even if they don’t quite fit with the rest of the library.
This is an auxiliary library which will let you create tables of draw commands for QuickPrint. It might be a stepping stone to a real layout system with a separate reflow/reshape step that only occurs as needed. Or it could be a waste of time. Only time will tell.
That’s all I’ve got! Unfortunate. I’ll write another update around the end of May.