I was feeling very pessimistic when I sat down to write this — I wish I was further along — but it doesn’t seem so bad when I lay everything out for the month.
Menu bar and nested sub-menus.
Some common text commands in a pop-up menu.
Summary
- Pop-up menus, menu bars, nested sub-menus
- Implemented a sibling-level priority system for widgets using Counting sort
- Tightened up context and widget locks during the update loop
- Added a secondary loop for async actions which happens after the update-lock is cleared
- Wrote a snippet to underline a portion of text based on two underscore markers
- Wrote a key-combo string packer+unpacker
- Implemented hooks for key shortcuts
- Fledgling layout system
- Wrote Inspector Key, a utility to show current KeyConstant and Scancode state
- Considered and ran tests for (but didn’t actually follow through on) an idea for a VSync-driven update loop
- Began work on a Joystick manager, and a tool to view enumerated joysticks
Pop-Up Menus and Menu Bars
These have been a long time coming, and it’s great to finally see them in action in the WIMP branch of ProdUI.
Last month, I wrote that I planned to make generic menu widgets the basis for things like right-click menus and menu bars. This was a mistake, as they both have subtle differences in handling from what is essentially a list selection box. For one thing, they require the ability to seamlessly transfer mouse-pressed state across widgets in a group. ProdUI was designed not to do this, and enabling it has required the inclusion of some pretty hacky functions at the context level.
Eventually, I split menu_bar and menu_pop_up into their own widget definitions, and it was then easier to work on them individually instead of endlessly breaking things while trying to fix other things.
Besides that, I also had a number of problems with transferring ownership of the UI thimble (selected widget with keyboard focus). I won’t go too deep into this because it’s hard to explain without the help of diagrams or a video, but when we select the menu bar or otherwise open a pop-up menu, the thimble is transferred to the pop-up menu so that it will respond to keyboard input. When that menu dies, what do we do with the thimble? Sometimes we need to restore thimble ownership to the last widget that had it. Sometimes the user has already assigned the thimble elsewhere, such as by clicking outside of the menu, and we don’t want to override that. I thought I had this under control, but just noticed while writing that there are still issues. I guess I’ll have to outline every scenario and step through them. It’s not a huge deal if a keyboard+mouse user loses focus, as they can just click on the widget again, but it’s a big problem for keyboard-only users.
Menu Bar “Key-Mode”
My first attempt at menu bars had three states: idle, opened (with a pop-up menu spawned), and an in-between state where no pop-up was present but the user could select categories with the left and right arrow keys (or tab). I ended up removing the in-between state because it made the thimble ownership issues much worse, and getting stuck in this state could also be confusing when other things are happening in a video game. (As much as the WIMP branch is intended for tools, there’s nothing stopping a library user from implementing it into a real-time sim or action game.) Menu bars don’t hold the thimble at all now, so that’s one fewer thing that can mess up.
To activate the menu bar, the user has to hit an explicit key mnemonic (ie Alt+F for File) or press F10.
Widget Sorting Priority
I originally planned to implement pop-ups and modal windows by pushing new root widgets onto a stack of instances. It would enforce an “always on top” status, and would be especially nice for modals in that it would fully prevent interacting with windows beneath the current top of the stack. However, I can’t really separate pop-ups from their client widgets like that without causing further issues. The solution was to provide a sibling-level sort method based on widget priority numbers.
I wanted an algorithm that is simple, not requiring tuning; stable, maintaining the order of widgets at the same priority level; and which works without making and throwing away temporary tables. In this specific case, ProdUI will always be sorting based on a numeric key, so we can just do a greater-than check instead of calling a comparison function. I narrowed my choices down to Pigeonhole Sort and Counting Sort. The latter eventually won out because it requires only one scratchspace table, whereas Pigeonhole would need a separate array for every priority slot. This scratchspace table is stored as an upvalue, and reused for every call to the sorting function.
(Incidentally, I have been using Pigeonhole Sort to arrange sprites in Balleg this whole time without realizing that that’s what it is. Instead of sorting the actors, however, the pigeonhole arrays are passed on to the render code.)
Here are the current priority assignments:
- Background elements
- Workspace panels
- Workspace window-frames
- Modal, always-on-top window-frames; Application menu bar
- Pop-up menus
Context and Widget Locks; Async Actions
I added some best-effort locks on certain functions that are likely to corrupt the widget update loop. There are exceptions, but bad things can happen if you add or delete entries from a table that you are currently iterating over with for
. The locks should hopefully catch some of these issues, though of course it’s impossible to catch everything.
For example, a widget can’t remove itself during its own update callback because it would mess with its parent’s children table, which is currently being iterated. However, it can post a deferred removal action to a table of asynchronous events owned by the context, to be handled immediately after the update loop.
Underlines
I wrote some Lua code to get the coordinates for an underline in a single line of text. This is needed for showing key mnemonics in menu options.
Given a string with two underscore markers: foo_b_ar
- Split into three strings: left (
foo
), mid (b
) and right (ar
). Remove the underscores. - Get the width of left and of mid
- Optional: Apply kerning based on the last code point of left and the first of mid, and the last of mid and the first of right.
- This is unlikely to make much of a difference with small fonts.
- The line’s x1 is the width of left, its width is the same as mid, and its Y position is font:getHeight()
- Return x1, the line width and Y, and a concatenated left, mid and right.
- When drawing the line, add 0.5 to the coordinates and subtract 1 from x2.
Cache the results so that you only need to do the string splitting and width calculations when the text is changed.
Key Combos
I needed a consistent way of storing and referencing key shortcuts. Just the key enumeration isn’t enough, since I also need to track if it’s a KeyConstant or a Scancode (many strings among them are identical), and if the Ctrl, Shift, Alt and/or Gui keys must be held down. I settled on two forms: a hash table with separate fields for each piece of info, and a packed string.
The table is pretty straightforward:
local demo_key_combo = { -- true: is a scancode, false: is a keyconstant is_sc = false, -- Merged modifier keys. True or false. ctrl = true, shift = false, alt = true, gui = false, -- Must be a valid KeyConstant or Scancode (depends on 'is_sc') code = "a", }
The string form poses an additional issue: both KeyConstants and Scancodes use a number of symbols that appear on the US QWERTY keyboard layout, leaving few easy-to-access symbols to use as a separator token. Thankfully, at some point, space was changed from the literal " "
to the word "space"
, so I can use that as a separator. All letters in the enums are lower case, so I can reserve upper case letters for the header bits.
The string format is then a series of single-byte chars, with a space separating them from the KeyConstant or Scancode substring. We can scan most of the header data with string.byte()
, though we do still need string.sub()
to get the final enum.
"[K|C|S|A|G][space]<Scancode/KeyConstant>"
- K: Indicates that the key substring at the end is a KeyConstant and not a Scancode
- C: Ctrl modifier key
- S: Shift modifier key
- A: Alt modifier key
- G: Gui modifier key
Ctrl+Alt+Shift+Q becomes "KCSA q"
. This string isn’t appropriate to display to the end user, so I also have a function to convert the latter to the former.
I guess another option is to map all KeyConstants and Scancodes to numeric values, and reserve some bits for the modifiers.
Hooks for Key Shortcuts
I wanted menu items to support shortcut key combos, but wasn’t sure where to put the data and code that implements them. The menu bar itself seems like a good location, since it contains the pop-up menu definitions which display the shortcuts for each command. However, I still want the commands to work even if the menu bar is destroyed, or never created in the first place. So instead, they are attached to the widget that owns the menu bar (in this case, the WIMP root).
Keypress and keyrelease hooks are stored in arrays belonging to the WIMP root. When a keyboard event bubbles up to the root, it first allows everything in the associated hook array to try and act on the event (and cancel any further work on it by returning true) before continuing its own processing.
What happens if there’s a conflict? Hooks are evaluated last-to-first, so the most recently-added hook gets first dibs. Before that, if any widget from the current thimble-holder to just below the WIMP root acts on the key event and returns true, then the hook will never be processed. I got a heaping dose of confusion when some debug keys in the text input box prevented F10 from bubbling up.
If a window-frame has a menu bar (coming soon) in addition to the WIMP root, and the window-frame has the thimble, then its menu will take precedence over the root’s. Not a problem if the window-frame is modal (coming soon), but again, for keyboard-only users, this could be a big problem.
Layouts
The basics of a layout system are in! I tried to add this very early on, but I didn’t have a firm grasp of what I needed from a layout system at the time. Now I’m a bit more informed.
Layouts always involve one parent widget and its direct descendants, and the parent layout is applied during its reshape callback to its children. Layouts are opt-in, so children that aren’t registered to the parent’s layout list are left alone.
I have a few reshape mechanisms planned. The main one (and what is currently used to set up window-frames) involves cutting chunks of a “layout space” rectangle and fitting child widgets to them, similar (as far as I can recall) to the pack geometry manager in Tk. If you want to fit widgets into a sub-section, you can cut it off of the current layout rectangle, push it onto a layout stack, fit the widgets, then pop back to the old rectangle.
Anyways, it’s far from battle-tested at this point, but I hope to continue fleshing it out.
Inspector Key
I had to hit PrintScreen with my nose to take this screenshot.
I wrote this to help answer some questions I had about Scancode and KeyConstant behavior. It gets fresh info every frame, so keyboard layout changes should be visible relatively quickly (after a pause). One important thing I learned is that while “unknown” as a Scancode or KeyConstant shows up in love.keypressed
and love.keyreleased
, it seems to never return true in love.keyboard.isDown
or love.keyboard.isScancodeDown
. At least, that’s the case on my PC.
Inspector Key needs some more work before it can be considered a well-rounded utility. It might be good to circle back to this once ProdUI is more usable.
Detour: Clock-to-VSync concept
I got curious about VSync-based update intervals in LÖVE, and spent some time experimenting and running tests. I didn’t fully get there due to practical reasons.
I wrote a few paragraphs of speculation here, but it’s probably best not to share it without a real demo to back it up. In lieu of that, here are some framerate tests I performed on my PC + monitor combo, taking the refresh rate from Fedora’s Display manager, the number from love.window.getMode()
, and a 5 minute framerate average with a few seconds of warm-up.
Resolution Refresh Rate (Hz) Fedora getMode() 5min test 1440x900 59.89 60 59.8866666666667 74.98 75 74.9833333333333 1280x1024 60.02 60 60.02 75.02 75 75.0233333333333 1280x960 60.00 60 60 1152x864 75.00 75 75 1024x768 60.00 60 60.0033333333333 70.07 70 70.07 75.03 75 75.0266666666667 800x600 56.25 56 56.25 60.32 60 60.3166666666667 75.00 75 75
In this case, Fedora’s numbers are pretty accurate to the 5 minute test, and the getMode
numbers are only reliable if the underlying refresh rate happens to be a whole number, or nearly so. Would this always be the case? Maybe on someone else’s computer, the numbers from getMode
are all dead-on. I concluded that they are good for displaying to the user, but can’t be relied on for timing.
That’s all I will write on the idea for now.
Joystick Manager, Joystick Viewer
Joystick stats for an XBox 360 controller.
The front-end branch of ProdUI needs to have pretty good joystick support. To prepare for that, I started a Joystick Manager, and also a utility to examine stats for every connected joystick.
I’m not sure what exactly the Joystick Manager is going to handle — I just started writing it on the 28th. At the very least, it needs to track button presses and releases in-order so that they can be queried during event callbacks, similar to how the ProdUI Keyboard Manager works. It’d probably be good to track joysticks that have been disconnected as well, because the LÖVE Joystick objects remain populated and will re-activate if the user reconnects them.
Closing
I wish I was further along. I told myself that regardless of what I had done on ProdUI, by about the 15th of December, I should drop my focus on it and get whatever’s finished integrated into Balleg and move on. Now I’m not so sure. It may be better to just see this through to the end. Dunno… In any case, I’ll update again near the end of the year.