My first freewheeling application

2024-07-29

https://akkartik.name/freewheeling/

I want to make a bingo card generator for use by me to generate various bingo cards because it can be a fun game to play among my friends.

Making a card in GIMP came with a few downsides, namely how slow the whole process was and the fact that the cards weren't randomized. As such, I decided that I wanted my own solution for this, as the available choices online weren't as flexible as I wanted them to be.

My mind immediately jumped to whatever was most convenient - a web app, but then I decided I'd try my hand at something entirely new, I have read akkartik's post about freewheeling applications a few times at this point and I realized that a basic, LOVE-based lua application would be a perfect fit for my usecase, instead of relying on a whole browser to run it.

And so, here I am, writing my first 'freewheeling' application, meant to be used only by me and maybe my friends. I'm writing this before having started writing code, wish me luck!


Setup

Considering that I am on nixOS, and that my default mode for any project is to use nix-shell or nix develop, I whipped up a very quick shell.nix for LOVE projects, which is as simple as:

{ pkgs ? import <nixpkgs> {} }:
pkgs.mkShell {
  nativeBuildInputs = with pkgs.buildPackages; [
    love
  ];
}

It seems redundant here, but it allows me to use the same command for all of my projects for what could be very different setups. For reference, with the same command I can enter a shell with an arbitrary set of programs and tools within. I use a similar setup for flutter, rust, C++, C, ArmA3 mod development, so on and so forth.


Initial programming

I just realized that I'm in for a bit more than I was expecting, as LOVE, by itself, only has immediate-mode drawing functionality. I don't want to pull in external libraries for a project like this as I feel like it would hinder my learning experience, that, and I feel like a DIY UI toolkit comes with a few benefits, like an innate understanding of how it works, how it should and should not be used, and the pride that comes with implementing base funcitonality from scratch.

After some intermittent programming. I have the foundation of an UI library ready at this point, and I have successfully implemented buttons. I skimmed through akkartik's code on git.sr.ht and I re-implemented the method they used for widgets, which kinda sorta works like this:

Here is a snippet containing the logic that checks if a button is being currently hovered over:

-- acquire mouse pos
local m_x, m_y = love.mouse.getPosition()
[...]

-- for every button in global state
for index, bp in ipairs(app_state.buttons) do
	local end_x = bp.x + bp.w -- calc bounds of button as-is-defined in state
	local end_y = bp.y + bp.h
	
	-- check if mouse is within button bounds
	if (m_x >= bp.x and m_x <= end_x and m_y >= bp.y and m_y <= end_y) then
		bp.is_hovered = true -- update button state in case mouse is within button
	else
		bp.is_hovered = false
	end
end

I later ended up moving all of this code over to it's own file and lua module, labeled ui. I then proceeded to implement a dropdown menu, since I know i'll need it for my bingo project as well.

The dropdown menu mainly relies on the same exact principle of having a global table that contains the dropdown button's state. But, for the actual dropdown elements, I decided to take this approach:

This approach worked out pretty well. I later went on to add functionality to allow for the dropdown menu to open upwards instead of downward in case I'll need it, and it only took a small modification in the function that populates dd_opt_buf.

Funnily enough, I also realized that I could call the draw_button function for the drawing of dropdown menu items with zero modification to the draw_button function itself. I wrote a shim function for it regardless, in case I do need to modify it in the future.

I must also add that I find writing code in lua rather enjoyable. The language's loose types and stark syntax feel freeing rather than overwhelming.


2024-07-31

Implementing the textfield

At this point, I have two widgets successfully implemented -- a dropdown and a button, but I need a third to complete what can be considered the core of any UI library. A text input field.

First up, considering that i have to be able to handle n amount of text fields on my screen, i have to come up with a focus system to allow for the user to select which textbox they are inputting into. This is simply done with allowing focus to be selected via mouse-click.

I.e, within ui.onmousepress I have a check that loops over every defined text field within ui.state, which checks if a mouse press happened within the bounds of a text field, and sets said text field's has_focus to true. Importantly, if a mouse press is outside of an individual textfield, it's has_focus is set to false. In short, it makes it so that you can only have one textfield in focus at any time.

That's the focus system, now I had to implement basic input. I ended up using love.textinput (and by extension ui.textinput) as the base listener for text input. Within, I check if there is a focused text field, if that's the case, I add the inputted text into that textfield's value and then I call said textfield's update callback.

Additionally, I wanted a basic cursor as well. Implementing it was easier than I expected it to be, thanks to Font::getWidth(text), which takes in a string and spits out a width, in pixels, of the given text string, were it rendered with the font it's being called from. This essentially gave me a horizontal offset equal to the text I input. All it took from there was to add in any additional offset values associated with the drawing and I had a static cursor in place.

To make it blink, I simply added a time variable into ui.state (labeled ui.state.t) and I had ui.update add it's dt to said time variable. And then, within draw_textfield, I had the cursor's drawing be determined by ui.state.t.

As for basic text deletion, I relied on the example snippet provided within LOVE's documentation on love.textinput, which can be found here.

At this point, I have basic functionalty working for the textfield, as such, I'm gonna call it a day for now, but I know what else needs to be done:

And here's what I don't want to do:


2024-08-07

Wrapping up the UI library

So, it's been a short while, I have been busy with IRL activities, it being summer and whatnot. But, today I found some good time to finish up the text field, and I've managed to implement everything mentioned above, except for paste support and the ability to disable a text input.

I'm calling the UI lib done for now, and i'll reserve any additional functionality i may need for another day. That being said, possible points of improvement/upgrade are:


2024-08-11

Writing the application proper

With the UI library complete, I moved on to writing the application itself. It was weird to see a shift in how I approached the code here. I still tried to keep it somewhat clean, but I noticed a lapse in my will-to-document. The difference here stems from the intent behind the code. This is code that is and will be specific to this application. I can afford to care a little less about it's reusability and readability, since it's more likely that I won't have to re-read and re-edit it later down the line.

That being said, I took advantage of a very nice side effect of the UI library I wrote. The UI library's state is a plain lua table that gets referenced every frame. Every event update, every state update is done on a frame-by-frame basis. This means, that, in theory, if I modify the UI library's state, then the modification should be immediately represented in the application.

So far the application was a static test ground for the widgets, but I also realized that by having an entry with a string key within ui.state.buttons (or any other table that contains widget states), if I modify it, I can have it be located dynamically, from my main.lua, and testing showed that it all just.. worked, with no issues or downsides.

The rest was suprisingly uneventful, almost boring, even. I figured out how to draw a grid of squares, I figured out how to wrap text with LOVE's Font:getWrap, how to randomize the elements in a sparsely populated lua table, how to take screenshots, basic image manipulation, basic file manipulation, all.. mostly basic stuff.

That being said, as of now, I still need to write an import/export function, which may involve making use of a file picker of sorts. I intend to use lovepicker, which seems to fit my needs well.


It did not turn out as difficult as I expected. That being said, I was slightly dissapointed by the restrictions LOVE places on filesystem access. Seems to be an unfortunate side effect of how restricted mobile phone file access tends to be. That being said, I successfully wrote an import/export function and I am currently actively making a bingo board with the application. Couple of things of note for real-world use:

For now, I am considering the application complete. You can find the source here