I spent about half of May getting some LÖVE libraries cleaned up and released on GitHub, and the other half struggling to expand my text box implementation to support multiple lines. My approach backfired, and I have yet to get it fully operational. No other progress this month. That sounds bad, but I am kind of proud of two of the libraries I put together, so I don’t consider it a total loss.
No videos this time.
Devlog
Summary
- Worked on editField 🙁
- Released libraries:
- QuickPrint
- idops (LÖVE ImageData Operations)
- FontSet
- Released utils / examples:
- Inspector Kern
- love_snippets: line stipple and 9-slice examples
Libraries
I’ll discuss these first, as they were mostly handled before I turned by attention back to EditField.
idops
(LÖVE) ImageData Operations. Contains some ImageData functions that I’ve been using in multiple projects. Nothing too exciting. The goal is to amass functions that can easily manipulate LÖVE ImageFont graphics. (It can scale up pixel data, add drop shadows, remove spacing between glyphs, etc.)
FontSet
A library for selecting and scaling LÖVE ImageFonts and BMFonts relative to a virtual TrueType size. I needed something like this to help select and scale fonts within the context of scalable user interfaces.
Note that the demo is a bit misleading. It’s not for frequently rescaling text, as every rescale action takes considerable CPU and memory resources. The assumption is that you would only rescale as a result of the user making a UI change.
Inspector Kern
A utility that checks if TrueType fonts exhibit any non-zero kerning offsets, among a list of common kerning pairs, when loaded through LÖVE. As far as I understand, newer TTFs store kerning information in a way that FreeType doesn’t handle.
I patched it to also display if fonts report having glyphs (via Font:hasGlyph()
) for a handful of whitespace control codes.
QuickPrint
This is a LÖVE text-printing library that maintains an internal cursor position between writes, and which supports virtual tab stops. Originally a debug-printing tool, I added a few additional things to flesh it out, including the ability to direct print actions to a LÖVE Text object instead of the framebuffer.
Last month, I wrote about wanting to experiment with a companion library that would store lists of commands to be fed to a QuickPrint state table. Ultimately, this did not live up to expectations. It was difficult to maintain the command bindings, updating the contents of the command list was unintuitive, and frankly, it was easier to just whip up a function to display certain bits of text.
Amusingly, after finishing this library, I discovered that love.graphics.print()
basically will accept tables of strings and stitch them together. This is a result of how it handles coloredtext
sequences: while the wiki implies an alternating list of {color, string, color, string}
, the internal logic actually considers the type of each entry.
love_snippets
I wrote a stippled line function, decided it was too small to upload as a single repository, and didn’t want to deal with GitHub gists. So I made this repo as a dumping ground for small snippets. I also added a 9Slice example.
editField
I know what you’re thinking. It’s kind of ridiculous that I haven’t gotten this done and integrated into the UI toolkit from last month. Let it be known that I basically did have a single-line input box working, with undo/redo history, highlighting, cut/copy/paste, and command prompt-style history. It turned into a mess when I decided to expand it to multiple lines.
The reasoning was innocent enough. When building an application with uiKit, one may want to have a “notes” section within a window, or maybe load, edit and save a text file. For those purposes, a single-line input box isn’t sufficient. I tore down my implementation and rebuilt it with a sequence-of-strings data structure, where every string represents a line of text.
If you have multi-line text, you likely also need to support wrapping lines that would otherwise extend beyond the edge of the screen. You also need to sanitize incoming string data: if the encoding is bad, do you discard the string, trim it at the first bad byte, substitute a replacement glyph, or raise a fatal error? You also have to do something about code points which have no glyph representation in the currently loaded font. (TrueType fonts can reserve a glyph for this, often a tall outlined square, but ImageFonts AFAICT just omit the glyph.) You can replace it with question marks or something, or just not accept the input at all. And password fields need to be masked with dots or asterisks.
To handle all of these differences, there is a separate array for display text. Each logical line is broken into wrapped sub-lines which must, for offsetting purposes, contain the same number of Unicode Code Points as the source string, even though the string contents and byte counts may be different.
I made this much harder on myself by splitting the internal core state and the display state, and having a parent textBox object mediate between the two. This wasn’t working out for two reasons: the core didn’t provide enough information on what changed after each action, and the textBox lazily waited until the main loop to update the display text before draw time. As a result, the textBox had to recreate the entire display state whenever a core action was called. Eventually, I made the display state a sub-object of the core, and made it the core’s responsibility to keep the display info current.
Trying to totally separate internal behavior and external appearance also presented issues when moving the text cursor vertically. When moving up or down lines, the user expects the cursor to land, roughly, on a glyph that is close to where the cursor last moved horizontally. I was able to get a Vim-style up/down step to work, where the cursor completely steps over wrapped sub-lines, but that’s not really what people expect from a typical GUI text field.
So, I’ve abandoned the initial idea of totally separating the internal behavior and the external appearance. Compartmentalization was enticing because I thought it would ease integration into existing UI systems. I’m still trying to find the right balance on this. I might end up with something very similar to ReFreezed’s InputField, if I don’t just give up and use InputField directly.
Error Fun
In LuaJIT, if you directly return the results of table.concat()
in a function, and it has a problem, the error message will refer to the function or to a “bad self,” not table.concat()
. However, if you assign the results to a local variable and return the local, you will get the correct error during the assignment.
Lua 5.4.4: > function a() local x; return table.concat(x); end > a() stdin:1: bad argument #1 to 'concat' (table expected, got nil) stack traceback: [C]: in function 'table.concat' stdin:1: in function 'a' (...tail calls...) [C]: in ?
LuaJIT 2.1.0-beta3:
> function a() local x; return table.concat(x); end > a() stdin:1: bad argument #1 to 'a' (table expected, got nil) stack traceback: [C]: in function 'a' stdin:1: in main chunk [C]: at 0x5650201a1250 >
In LÖVE + LuaJIT, when called from from an object method:
Error: main.lua:33: calling 'getString' on bad self (table expected, got nil)
As far as I know, it’s related to tail call optimization. I incrementally stripped down my string-sequence source file to the bare minimum before I realized what was happening. Argh.
Closing
So, yeah. Not a great state to be ending the month in. Hopefully I get this sorted out in the next few days. I’ll post again around the end of June.