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.

whale whale whale, what a cutie

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.