Files
imgui-luau/backends/imgui_impl_love.lua
T

398 lines
17 KiB
Lua

--- Dear ImGui backend for LÖVE (11.x, LuaJIT). Platform (input, clipboard, cursors, IME) + renderer (Mesh per draw list).
--- Built into build/imgui_love.lua by `python3 tools/bundle.py --target love`.
--- Usage (see examples/example_love/main.lua):
--- love.load: ImGui.CreateContext(); ImGui_ImplLove.Init({ root = "<repo root>/" })
--- love.update: ImGui_ImplLove.NewFrame(); ImGui.NewFrame(); ...your UI...; ImGui.Render()
--- love.draw: ImGui_ImplLove.RenderDrawData(ImGui.GetDrawData())
--- forward love.mousemoved/mousepressed/mousereleased/wheelmoved/keypressed/keyreleased/textinput to ImGui_ImplLove.*
local ffi = require("ffi")
local floor, min = math.floor, math.min
local ImGui_ImplLOVE_GetBackendData
local CURSOR_MAP = {
[ImGuiMouseCursor.Arrow] = "arrow",
[ImGuiMouseCursor.TextInput] = "ibeam",
[ImGuiMouseCursor.ResizeAll] = "sizeall",
[ImGuiMouseCursor.ResizeNS] = "sizens",
[ImGuiMouseCursor.ResizeEW] = "sizewe",
[ImGuiMouseCursor.ResizeNESW] = "sizenesw",
[ImGuiMouseCursor.ResizeNWSE] = "sizenwse",
[ImGuiMouseCursor.Hand] = "hand",
[ImGuiMouseCursor.Wait] = "wait",
[ImGuiMouseCursor.Progress] = "waitarrow",
[ImGuiMouseCursor.NotAllowed] = "no",
}
local KEY_MAP = {
["return"] = ImGuiKey.Enter, ["escape"] = ImGuiKey.Escape,
["backspace"] = ImGuiKey.Backspace, ["tab"] = ImGuiKey.Tab,
["space"] = ImGuiKey.Space, ["kpenter"] = ImGuiKey.KeypadEnter,
["up"] = ImGuiKey.UpArrow, ["down"] = ImGuiKey.DownArrow,
["left"] = ImGuiKey.LeftArrow, ["right"] = ImGuiKey.RightArrow,
["home"] = ImGuiKey.Home, ["end"] = ImGuiKey.End,
["pageup"] = ImGuiKey.PageUp, ["pagedown"] = ImGuiKey.PageDown,
["insert"] = ImGuiKey.Insert, ["delete"] = ImGuiKey.Delete,
["lctrl"] = ImGuiKey.LeftCtrl, ["rctrl"] = ImGuiKey.RightCtrl,
["lshift"] = ImGuiKey.LeftShift, ["rshift"] = ImGuiKey.RightShift,
["lalt"] = ImGuiKey.LeftAlt, ["ralt"] = ImGuiKey.RightAlt,
["lgui"] = ImGuiKey.LeftSuper, ["rgui"] = ImGuiKey.RightSuper,
["-"] = ImGuiKey.Minus, ["="] = ImGuiKey.Equal, ["["] = ImGuiKey.LeftBracket, ["]"] = ImGuiKey.RightBracket,
[";"] = ImGuiKey.Semicolon, ["'"] = ImGuiKey.Apostrophe, [","] = ImGuiKey.Comma, ["."] = ImGuiKey.Period,
["/"] = ImGuiKey.Slash, ["\\"] = ImGuiKey.Backslash, ["`"] = ImGuiKey.GraveAccent,
}
for i = 0, 9 do KEY_MAP[tostring(i)] = ImGuiKey["K" .. i]; KEY_MAP["kp" .. i] = ImGuiKey["Keypad" .. i] end
for i = 1, 12 do KEY_MAP["f" .. i] = ImGuiKey["F" .. i] end
for c in ("abcdefghijklmnopqrstuvwxyz"):gmatch(".") do KEY_MAP[c] = ImGuiKey[c:upper()] end
local function UpdateKeyModifiers()
local io = ImGui.GetIO()
io:AddKeyEvent(ImGuiMod_Ctrl, love.keyboard.isDown("lctrl", "rctrl"))
io:AddKeyEvent(ImGuiMod_Shift, love.keyboard.isDown("lshift", "rshift"))
io:AddKeyEvent(ImGuiMod_Alt, love.keyboard.isDown("lalt", "ralt"))
io:AddKeyEvent(ImGuiMod_Super, love.keyboard.isDown("lgui", "rgui"))
end
function ImGui_ImplLOVE_GetBackendData()
return ImGui.GetCurrentContext() and ImGui.GetIO().BackendPlatformUserData or nil
end
local function UpdateMonitors(w, h)
local platform_io = ImGui.GetPlatformIO()
platform_io.Monitors:resize(0)
local m = ImGuiPlatformMonitor()
ImVec2_Copy(m.MainSize, ImVec2(w, h))
ImVec2_Copy(m.WorkSize, ImVec2(w, h))
platform_io.Monitors:push_back(m)
end
---------------------------------------------------------
-- PLATFORM DATA & CALLBACKS
---------------------------------------------------------
local function PlatformSetImeData(ctx, vp, data)
if data.WantVisible or data.WantTextInput then
love.keyboard.setTextInput(true, data.InputPos.x, data.InputPos.y, 1, data.InputLineHeight)
else
love.keyboard.setTextInput(false)
end
end
local function PlatformSetClipboardText(ctx, text) love.system.setClipboardText(text) end
local function PlatformGetClipboardText(ctx) return love.system.getClipboardText() end
local function PlatformOpenInShell(ctx, path) return love.system.openURL(path) end
---------------------------------------------------------
-- FILE SYSTEM: love.filesystem (game folder), then io.open(<root> .. path) so fonts/ etc. load from the repo
---------------------------------------------------------
local function ReadFile(filename)
local data = love.filesystem.getInfo(filename) and love.filesystem.read(filename)
if data then return data end
local bd = ImGui_ImplLOVE_GetBackendData()
local f = io.open(((bd and bd.Root) or "") .. filename, "rb")
if not f then return nil end
data = f:read("*a"); f:close()
return data
end
function ImStd.ImFileOpen(filename, mode)
local data = ReadFile(filename)
return data and { Data = data, Pos = 0 } or nil
end
function ImStd.ImFileClose(f) end
function ImStd.ImFileGetSize(f) return #f.Data end
function ImStd.ImFileRead(f, data, count)
local s, pos = f.Data, f.Pos
count = min(count, #s - pos)
for i = 1, count do data[i] = string.byte(s, pos + i) end
f.Pos = pos + count
return count
end
function ImStd.ImFileLoadToMemory(filename, mode)
local f = ImStd.ImFileOpen(filename, mode)
if not f or #f.Data == 0 then return end
local file_data = {}
local size = ImStd.ImFileRead(f, file_data, #f.Data)
return file_data, size
end
---------------------------------------------------------
-- TEXTURES
---------------------------------------------------------
local function CopyToImageData(tex, x, y, w, h)
local img = love.image.newImageData(w, h, "rgba8")
local dst = ffi.cast("uint8_t*", img:getFFIPointer())
local bpp = tex.BytesPerPixel
for row = 0, h - 1 do
local src, base = tex:GetPixelsAt(x, y + row)
local d = row * w * 4
if bpp == 4 then
for i = 0, w * 4 - 1 do dst[d + i] = src[base + i] end
else -- Alpha8: white with alpha
for i = 0, w - 1 do
local o = d + i * 4
dst[o], dst[o + 1], dst[o + 2], dst[o + 3] = 255, 255, 255, src[base + i]
end
end
end
return img
end
function ImGui_ImplLOVE_UpdateTexture(tex)
local bd = ImGui_ImplLOVE_GetBackendData()
if tex.Status == ImTextureStatus.WantCreate then
local data = CopyToImageData(tex, 0, 0, tex.Width, tex.Height)
local image = love.graphics.newImage(data)
image:setFilter("nearest", "nearest")
bd.TextureCount = bd.TextureCount + 1
bd.Textures[bd.TextureCount] = image
tex.BackendUserData = bd.TextureCount
tex:SetTexID(bd.TextureCount)
tex:SetStatus(ImTextureStatus.OK)
elseif tex.Status == ImTextureStatus.WantUpdates then
local image = bd.Textures[tex.BackendUserData]
for _, r in tex.Updates:iter() do
local data = CopyToImageData(tex, r.x, r.y, r.w, r.h)
image:replacePixels(data, nil, 1, r.x, r.y, false)
data:release()
end
tex:SetStatus(ImTextureStatus.OK)
elseif tex.Status == ImTextureStatus.WantDestroy then
local image = tex.BackendUserData and bd.Textures[tex.BackendUserData]
if image then image:release(); bd.Textures[tex.BackendUserData] = nil end
tex.BackendUserData = nil
tex:SetTexID(ImTextureID_Invalid)
tex:SetStatus(ImTextureStatus.Destroyed)
end
end
--- User texture from RGBA8 pixels (a `buffer` of w*h*4 bytes, or a love ImageData). Returns an ImTextureRef.
function ImGui_ImplLOVE_CreateTexture(pixels, w, h)
local bd = ImGui_ImplLOVE_GetBackendData()
local data = pixels
if type(pixels) == "table" and pixels.p then
data = love.image.newImageData(w, h, "rgba8")
ffi.copy(data:getFFIPointer(), pixels.p, w * h * 4)
end
bd.TextureCount = bd.TextureCount + 1
bd.Textures[bd.TextureCount] = love.graphics.newImage(data)
return ImTextureRef(bd.TextureCount)
end
function ImGui_ImplLOVE_DestroyTexture(tex_ref)
local bd = ImGui_ImplLOVE_GetBackendData()
local image = bd.Textures[tex_ref._TexID]
if image then image:release(); bd.Textures[tex_ref._TexID] = nil end
end
---------------------------------------------------------
-- RENDERING: one streamed Mesh per draw list; vertices converted from the port's flat VtxBuffer (f64 x, y, u, v,
-- u32 col; 36 bytes) to LÖVE's float x, y, u, v + byte rgba (20 bytes); indices -> 0-based uint32 vertex map.
---------------------------------------------------------
ffi.cdef("typedef struct { float x, y, u, v; uint8_t r, g, b, a; } ImGuiLoveVertex;")
local VERTEX_FORMAT = { { "VertexPosition", "float", 2 }, { "VertexTexCoord", "float", 2 }, { "VertexColor", "byte", 4 } }
local vtx_ct, u32p, f64p = ffi.typeof("ImGuiLoveVertex*"), ffi.typeof("uint32_t*"), ffi.typeof("double*")
local function GetMesh(bd, draw_list, nvtx, nidx)
local m = bd.Meshes[draw_list]
if not m or m.VtxCap < nvtx or m.IdxCap < nidx then
local vcap = math.max(nvtx, m and m.VtxCap * 2 or 256)
local icap = math.max(nidx, m and m.IdxCap * 2 or 512)
if m then m.Mesh:release() end
m = {
Mesh = love.graphics.newMesh(VERTEX_FORMAT, vcap, "triangles", "stream"),
VtxData = love.data.newByteData(vcap * 20), IdxData = love.data.newByteData(icap * 4),
VtxCap = vcap, IdxCap = icap,
}
bd.Meshes[draw_list] = m
end
m.Used = bd.FrameNo
return m
end
function ImGui_ImplLOVE_RenderDrawData(draw_data)
local bd = ImGui_ImplLOVE_GetBackendData()
if draw_data.Textures ~= nil then
for _, tex in draw_data.Textures:iter() do
if tex.Status ~= ImTextureStatus.OK then ImGui_ImplLOVE_UpdateTexture(tex) end
end
end
local fb_w, fb_h = draw_data.DisplaySize.x, draw_data.DisplaySize.y
if fb_w <= 0 or fb_h <= 0 then return end
bd.FrameNo = bd.FrameNo + 1
love.graphics.push("all")
love.graphics.setBlendMode("alpha", "alphamultiply")
love.graphics.setColor(1, 1, 1, 1)
local ox, oy = draw_data.DisplayPos.x, draw_data.DisplayPos.y
love.graphics.translate(-ox, -oy)
for _, draw_list in draw_data.CmdLists:iter() do
local nvtx, nidx = draw_list.VtxBuffer.Size, draw_list.IdxBuffer.Size
if nvtx > 0 and nidx > 0 then
local m = GetMesh(bd, draw_list, nvtx, nidx)
local base = draw_list.VtxBuffer.Buf.p
local srcu = ffi.cast(u32p, base)
local dst = ffi.cast(vtx_ct, m.VtxData:getFFIPointer())
for i = 0, nvtx - 1 do
local v, src = dst[i], ffi.cast(f64p, base + i * 36) -- 36-byte stride: cast per vertex
v.x, v.y, v.u, v.v = src[0], src[1], src[2], src[3]
local c = srcu[i * 9 + 8]
v.r, v.g, v.b, v.a = c % 256, floor(c / 256) % 256, floor(c / 65536) % 256, floor(c / 16777216)
end
m.Mesh:setVertices(m.VtxData, 1, nvtx)
-- vertex map: per command, 0-based vertex = VtxOffset + idx - 1 (port indices are 1-based)
local map = ffi.cast(u32p, m.IdxData:getFFIPointer())
local idx = draw_list.IdxBuffer.Data
for _, pcmd in draw_list.CmdBuffer:iter() do
local vo = pcmd.VtxOffset
for i = pcmd.IdxOffset + 1, pcmd.IdxOffset + pcmd.ElemCount do map[i - 1] = vo + idx[i] - 1 end
end
m.Mesh:setVertexMap(m.IdxData, "uint32")
for _, pcmd in draw_list.CmdBuffer:iter() do
if pcmd.UserCallback ~= nil then
pcmd.UserCallback(draw_list, pcmd)
elseif pcmd.ElemCount > 0 then
local c = pcmd.ClipRect
local x0, y0 = math.max(c.x - ox, 0), math.max(c.y - oy, 0)
local x1, y1 = min(c.z - ox, fb_w), min(c.w - oy, fb_h)
if x1 > x0 and y1 > y0 then
love.graphics.setScissor(x0, y0, x1 - x0, y1 - y0)
m.Mesh:setTexture(bd.Textures[pcmd:GetTexID()])
m.Mesh:setDrawRange(pcmd.IdxOffset + 1, pcmd.ElemCount)
love.graphics.draw(m.Mesh)
end
end
end
end
end
-- free meshes of draw lists that went away
for dl, m in pairs(bd.Meshes) do
if bd.FrameNo - m.Used > 120 then m.Mesh:release(); bd.Meshes[dl] = nil end
end
love.graphics.setScissor()
love.graphics.pop()
end
---------------------------------------------------------
-- LIFECYCLE
---------------------------------------------------------
--- opts.root: path prefix for files not in the LÖVE game folder (e.g. the repo root, for fonts/)
function ImGui_ImplLOVE_Init(opts)
opts = opts or {}
local io = ImGui.GetIO()
local bd = { Time = love.timer.getTime(), Root = opts.root or "", Textures = {}, TextureCount = 0, Meshes = {}, FrameNo = 0 }
io.BackendPlatformUserData = bd
io.BackendFlags = bit32.bor(io.BackendFlags, ImGuiBackendFlags.RendererHasTextures, ImGuiBackendFlags.RendererHasVtxOffset, ImGuiBackendFlags.HasMouseCursors)
local platform_io = ImGui.GetPlatformIO()
platform_io.Platform_SetImeDataFn = PlatformSetImeData
platform_io.Platform_SetClipboardTextFn = PlatformSetClipboardText
platform_io.Platform_GetClipboardTextFn = PlatformGetClipboardText
platform_io.Platform_OpenInShellFn = PlatformOpenInShell
local w, h = love.graphics.getDimensions()
UpdateMonitors(w, h)
love.keyboard.setKeyRepeat(true)
end
function ImGui_ImplLOVE_Shutdown()
local io = ImGui.GetIO()
local bd = ImGui_ImplLOVE_GetBackendData()
for _, m in pairs(bd.Meshes) do m.Mesh:release() end
for _, t in pairs(bd.Textures) do t:release() end
io.BackendPlatformUserData = nil
io.BackendFlags = bit32.band(io.BackendFlags, bit32.bnot(bit32.bor(ImGuiBackendFlags.RendererHasTextures, ImGuiBackendFlags.RendererHasVtxOffset, ImGuiBackendFlags.HasMouseCursors)))
end
function ImGui_ImplLOVE_NewFrame()
local io = ImGui.GetIO()
local bd = ImGui_ImplLOVE_GetBackendData()
local w, h = love.graphics.getDimensions()
if w ~= bd.ScreenW or h ~= bd.ScreenH then bd.ScreenW, bd.ScreenH = w, h; UpdateMonitors(w, h) end
ImVec2_CopyV(io.DisplaySize, w, h)
local now = love.timer.getTime()
io.DeltaTime = (now > bd.Time) and (now - bd.Time) or (1.0 / 60.0)
bd.Time = now
if bit32.band(io.ConfigFlags, ImGuiConfigFlags.NoMouseCursorChange) == 0 then
local cursor = ImGui.GetMouseCursor()
if cursor ~= bd.LastCursor then
bd.LastCursor = cursor
local name = CURSOR_MAP[cursor]
love.mouse.setVisible(cursor ~= ImGuiMouseCursor.None and not io.MouseDrawCursor)
love.mouse.setCursor(name and love.mouse.getSystemCursor(name) or nil)
end
end
end
---------------------------------------------------------
-- LÖVE EVENT HOOKS (forward from love.* callbacks)
---------------------------------------------------------
local MOUSE_BUTTONS = { [1] = 0, [2] = 1, [3] = 2, [4] = 3, [5] = 4 } -- LÖVE: 1 left, 2 right, 3 middle
local function MouseMoved(x, y)
local io = ImGui.GetIO()
io:AddMouseSourceEvent(ImGuiMouseSource.Mouse)
io:AddMousePosEvent(x, y)
end
local function MousePressed(x, y, button)
local b = MOUSE_BUTTONS[button]
if b then local io = ImGui.GetIO(); io:AddMouseSourceEvent(ImGuiMouseSource.Mouse); io:AddMouseButtonEvent(b, true) end
end
local function MouseReleased(x, y, button)
local b = MOUSE_BUTTONS[button]
if b then ImGui.GetIO():AddMouseButtonEvent(b, false) end
end
local function WheelMoved(x, y) ImGui.GetIO():AddMouseWheelEvent(-x, y) end
local function KeyPressed(key)
UpdateKeyModifiers()
local k = KEY_MAP[key]
if k then ImGui.GetIO():AddKeyEvent(k, true) end
end
local function KeyReleased(key)
UpdateKeyModifiers()
local k = KEY_MAP[key]
if k then ImGui.GetIO():AddKeyEvent(k, false) end
end
local function TextInput(text)
local io = ImGui.GetIO()
for _, c in utf8.codes(text) do io:AddInputCharacter(c) end
end
local function Focus(f) ImGui.GetIO():AddFocusEvent(f) end
--- True when ImGui wants the mouse / keyboard (don't pass the event to your game)
local function WantCaptureMouse() return ImGui.GetIO().WantCaptureMouse end
local function WantCaptureKeyboard() return ImGui.GetIO().WantCaptureKeyboard end
ImGui_ImplLove = {
Init = ImGui_ImplLOVE_Init,
Shutdown = ImGui_ImplLOVE_Shutdown,
NewFrame = ImGui_ImplLOVE_NewFrame,
RenderDrawData = ImGui_ImplLOVE_RenderDrawData,
CreateTexture = ImGui_ImplLOVE_CreateTexture,
DestroyTexture = ImGui_ImplLOVE_DestroyTexture,
MouseMoved = MouseMoved,
MousePressed = MousePressed,
MouseReleased = MouseReleased,
WheelMoved = WheelMoved,
KeyPressed = KeyPressed,
KeyReleased = KeyReleased,
TextInput = TextInput,
Focus = Focus,
WantCaptureMouse = WantCaptureMouse,
WantCaptureKeyboard = WantCaptureKeyboard,
}
return true