Dear ImGui and LÖVE,
Published on 2020-08-14 21:47:00+02:00
Believe it or not, this one was requested. It's surprising considered that just recently I claimed that according to
stats nobody reads this website. It may or may not be a coincident that I have received this request over a phone.
Anyway, Dear ImGui is a C++ library for graphical user interface. As
for LÖVE; it's sometimes called Love2D and it's a Lua framework generally meant for
game development. Today we'll learn how to create an immediate-mode graphical user interface for your love2d game with
Dear ImGui.
First, we need to have both of them on our machine (duh!). LÖVE is insanely easy to get and install on pretty much
every platform. Just visit their website and check download section. If you use a
platform that's not supported straight-away then you probably can handle it by yourself anyway. Dear ImGui, or let's
call it ImGui from now on, is a little bit trickier. You need either a Lua binding for it, or a direct binding for LÖVE.
There're both available and let's settle for the latter one:
love-imgui.
Now then, I usually just build by myself. On Windows that's bothersome and luckily the maintainer provides binary
packages. The update frequency is rather low, so if you run into issues, you can try to use builds from more active
forks like e.g. apicici/love-imgui. At last, if you feel
adventurous, the manual build will let you down with its simplicity assuming that you have all tools already in place:
$ cd love-imgui
$ mkdir build
$ cd build
$ cmake ..
$ make
Once you obtained dynamic library file, place it in C-require-path. In LÖVE it defaults to ??
, which
means that it will look for name passed to require
with suffixed platform specific dynamic library
extension in either game's source directory, save directory or any of paths mounted with filesystem module. For you it
means, that you can just put your imgui.dll
into the directory with main.lua
and it should
work just fine.
Run it and you will see that nothing will happen. That's expected. To make it work you need to "pass" selected LÖVE
callbacks to it. Luckily for us, the README file provides us with an example that we can use as bootstrap. I copied
interesting parts here for your convenience:
require "imgui"
function love.update(dt)
imgui.NewFrame()
end
function love.quit()
imgui.ShutDown()
end
function love.textinput(text)
imgui.TextInput(text)
if not imgui.GetWantCaptureKeyboard() then
end
end
function love.keypressed(key, scancode, isrepat)
imgui.KeyPressed(key)
if not imgui.GetWantCaptureKeyboard() then
end
end
function love.keyreleased(key, scancode, isrepat)
imgui.KeyReleased(key)
if not imgui.GetWantCaptureKeyboard() then
end
end
function love.mousemoved(x, y, dx, dy)
imgui.MouseMoved(x, y)
if not imgui.GetWantCaptureMouse() then
end
end
function love.mousepressed(x, y, button)
imgui.MousePressed(button)
if not imgui.GetWantCaptureMouse() then
end
end
function love.mousereleased(x, y, button)
imgui.MouseReleased(button)
if not imgui.GetWantCaptureMouse() then
end
end
function love.wheelmoved(x, y)
imgui.WheelMoved(y)
if not imgui.GetWantCaptureMouse() then
end
end
In the if not imgui.GetWant...
blocks you should wrap your usual code that handles these events. It's
there to ensure that the input is not propagating to the game if one of imgui's windows has focus.
Finally, we've reached the moment we can make some windows! You write the code for the interface inside
love.draw
callback. The general documentation for ImGui can be found in
imgui.cpp, each function has a short description
next to its declaration in imgui.h. One more resource
is worth reading: imgui_demo.cpp. But still,
those are for C++. Thing is Lua binding reflects all names and things like function parameters almost directly. Let's
walk through some examples to get the gist of it!
local bg = { 0.3, 0.4, 0.2 }
function love.draw()
if imgui.Button("Goodbye, world!") then
love.event.quit()
end
bg[1], bg[2], bg[3] = imgui.ColorEdit3("Background", bg[1], bg[2], bg[3])
love.graphics.clear(unpack(bg))
-- Background colour will be dynamically controlled by ColorEdit3 widget.
imgui.Render()
end
Here's an example using two simple widgets: Button
and ColorEdit3
. It should illustrate how
immediate-mode GUI works and interacts with the state. One important thing to note is imgui.Render()
just
before the function reaches the end. It does exactly what you would expect from it: renders the ImGui's windows to the
screen. Now, let's control ImGui's windows' behaviour:
local show_fps = true
function love.draw()
if show_fps then
show_fps = imgui.Begin("FPS", true, {"ImGuiWindowFlags_NoCollapse"})
imgui.Text(string.format("FPS: %.2f", love.timer.getFPS()))
imgui.SetWindowSize("FPS", 0, 0)
imgui.End()
end
imgui.Render()
end
By default if you create an element ImGui will create "Debug" window for you to hold all of your stuff. Of course,
that's not always desired and so Begin
creates a new window. It accepts parameters: title, which serves as
an id, boolean that indicates if window should contain button for closing it, and a set of flags. In C++ flags are
handled via enums and bit-wise operators. In Lua binding use a table and put full name of the enum value.
SetWindowSize
makes the window unresizable and zeroes shrinks window to the size of its content.
local is_a = false
function love.draw()
if is_a then
imgui.PushStyleColor("ImGuiCol_Button", 0.7, 0.2, 0.2, 1)
imgui.PushStyleColor("ImGuiCol_ButtonHovered", 0.8, 0.3, 0.3, 1)
imgui.PushStyleColor("ImGuiCol_ButtonActive", 0.9, 0.1, 0.1, 1)
if imgui.Button("Change to B", 90, 0) then
is_a = false
end
imgui.PopStyleColor(3)
else
imgui.PushStyleColor("ImGuiCol_Button", 0.2, 0.7, 0.2, 1)
imgui.PushStyleColor("ImGuiCol_ButtonHovered", 0.3, 0.8, 0.3, 1)
imgui.PushStyleColor("ImGuiCol_ButtonActive", 0.1, 0.9, 0.1, 1)
if imgui.Button("Change to A", 90, 0) then
is_a = true
end
imgui.PopStyleColor(3)
end
if imgui.IsItemHovered() then
imgui.SetTooltip("Switches between A and B")
end
imgui.SameLine()
imgui.PushStyleColor("ImGuiCol_Button", 0.3, 0.3, 0.3, 1)
imgui.PushStyleColor("ImGuiCol_ButtonHovered", 0.3, 0.3, 0.3, 1)
imgui.PushStyleColor("ImGuiCol_ButtonActive", 0.3, 0.3, 0.3, 1)
imgui.Button("Disabled button")
imgui.PopStyleColor(3)
imgui.Text("Well, that's colorful")
imgui.Render()
end
The ad hoc styling is handled with stack-based interface. You just need to find the style name in the source and push
a colour or other property. When it's no longer needed, you pop it.
Quite a number of functions modify either the element that comes after them or the one before. Consider the usage of
IsItemHovered
in the example above. It doesn't matter, which button will be drawn, the tool-tip will be
shown if user hovers over the last element that preceding if
statement produced. Then there's
SameLine
which makes the next element remain on the same line (surprising, isn't it?).
local windows = {true, true, true, true, true}
local
function draw_my_window(n)
if windows[n] then
windows[n] = imgui.Begin(string.format("Window #%d", n), true)
for i, v in ipairs(windows) do
windows[i] = imgui.Checkbox(string.format("Show 'Window #%d'", i), v)
end
imgui.End()
end
end
function love.draw()
for i=1, #windows do
draw_my_window(i)
end
imgui.Render()
end
That's a bit useless, but yeah, you can link the state however you like and use the function in any way you imagine.
In C++, most elements like Checkbox
take a pointer. In Lua you can't pass normal values like that in a
simple way and so you usually put a value from previous frame in the place of the pointer and you expect that the
function (the Element) will return the new values that you can use in the next frame.
Aaand, that's about it without going into examples that'd make this post twice as long.