I started work on a simplified UI system, with the intent of using it as a test bed for editField. Neither are ready for public release (though editField is now mostly functional). I also dabbled a bit with LÖVE for Android, and got a simple Hello World! program running on my phone.
No videos, as it would just be me typing into a text box.
Okay, here’s a screenshot. The completely messed up stuff at the bottom is a debug-view of the history state, with states spanning left to right.
- Worked on editField. Always editField.
- Started demoUI.
- Small updates to QuickPrint.
- Utils / examples:
Since UIKit is an Apple thing, I renamed my ‘uiKit’ library to prodUI. As mentioned above, I also started a second, sibling UI toolkit, called demoUI, which is intended for basic usage and (big surprise) demonstration programs. They share some similarities, but demoUI is easier to work with as it doesn’t support any kind of widget nesting.
I didn’t touch the prodUI codebase for a few weeks. When I came back to it, I realized that some of the “neat” features that I had implemented were almost guaranteed to cause problems, so I spent some time gutting them:
- Don’t share widget defs across UI Contexts.
- Don’t auto-load widgets internally. Just give the user some basic loader functions, and leave them to figure out the most suitable loading scheme for their program.
- Don’t allow “exporting” widget instances, cutting them out of their context and putting them into a suspended state. If any kind of transfer should be supported, it should be a single transaction that cuts the widget out of one owner and pastes it into a new owner, both within the same context.
While prodUI is built around trees of nested widgets, demoUI supports only flat layouts with no nesting. To provide a bit more flexibility, demoUI contexts use a scene-based state system, where every scene functions as a separate container for widgets.
Other differences: widget defs in prodUI are per-context, while in demoUI, there is only one def repository which is shared among all contexts. If any theme system is implemented, it will be bare-bones. Most likely, the user will have to overwrite the draw callbacks for widgets to make them look the way they want.
Despite the name, and its origin as scaffolding for a text input box, I think it will have some potential for games that don’t require complex layouts. It might be worth cleaning it up and building some demo projects. Solitaire, Minesweeper, something like that.
So from May 31st to today, editField has gone from a broken pile of code originally written for single-line input, to a mostly functioning multi-line widget. I made some questionable design choices along the way, some of which I had to spend time rethinking and correcting, others which I have left in because changing them now would require rewriting low-level functions that are called throughout the library.
I stored the internal / core text as a table of strings, with one string for every line of text. I did this partly because I thought it would facilitate partial updates of the wrapped display version of the text. Every function that steps through one of these text objects now needs to deal with the implicit line feeds which are supposed to exist between every line.
Display text uses a similar structure, where each logical line becomes a “super-line” array of wrapped sub-line structures. In addition to wrapped text, the sub-lines also contain cached highlight rectangle coordinates. I planned to make it so that super-lines could be swapped out with just the number of sub-lines they would contain if they were initialized, and only keep super-lines that are close to the visible viewport in memory. Well, in practice, this is extreme overkill for a text box which might hold about 5000 code points at most.
Attempts to implement delta-based history, where only textual changes and their offsets are stored in the history ledger, didn’t really get off the ground. I wanted to amend the current history entry if a series of changes belonged to a similar group of actions, and I couldn’t wrap my head around how to do this with only a partial bit of the ledger state saved. (Specifically, I grouped together words as their own entries. The amend sequence is broken by typing space, hitting enter, deleting text, etc.) Instead, I ended up saving a copy of every logical line in the sequential string object. Modifying that is a piece of cake.
rxi’s Text Box Behavior article was a great reference to have handy, and I cannot stress enough how helpful it was to be able to check an existing Lua+LÖVE implementation of a text box in the form of ReFreezed’s InputField.
More complaining about handling text
In LuaJIT, there is a limit to what
string.sub() will accept for its offset arguments. Under normal circumstances, it will clamp offsets to the bounds of the string. For example,
string.sub("foobar", -100, 100) will clamp to bytes 1-6. I assumed that I could initialize a couple of variables to 1 and
math.huge, conditionally update them with
math.min(), and then pass them into
string.sub() where they would be clamped. Not quite. Any number greater than 2147483647, at least as the second argument, can cause the function to return an empty string.
I guess there wouldn’t be any harm in just using 2147483647 (hereby christened
math.prettybig), as there’s virtually no chance that a string that size would ever be passed into editField.
Font:getWrap() will omit glyphs if the
wraplimit is thinner than a given single glyph. This is technically correct — by dropping the glyph, it does prevent the specified limit from being surpassed — but for the purposes of calculating offsets, editField requires that the source string and wrapped text contain the same number of code points. As a workaround, I’m replacing empty lines that are supposed to have one code point with a string containing a single tilde (~) character.
I spent some time attempting to write my own line-wrapping function which guarantees at least one code point per line, regardless of the wrap limit. It turns out this is pretty difficult to get right, as there are two or three separate states to keep track of as you traverse each line. As LÖVE’s text handling may change in the next release, I decided to stick with
Font:getWrap() for now, as it’s otherwise pretty solid.
I spent some time checking out LÖVE on Android. After putting my phone in dev mode and installing
adb, I successfully uploaded and launched a test program that draws circles around touch locations. Neat.
I was able to install the 11.4 APK, but I couldn’t get it to load .love files. Ultimately, I ended up switching to the outdated 11.3 version that’s available on the Play Store. That version works, and besides a few bugs, it’s probably good enough to experiment with for now.
I guess that’s all. Next up, I need to get editField integrated into prodUI. That will require some code reorganization, and probably moving some stuff handled by the widget entity into the editField core object. After that, I’m not sure. In any case, I’ll probably post again near the end of July.
(e: 2022-07-31: Spelling.)