LÖVE backend + build target (compat/luajit.lua shims, Roblox build unchanged), examples/example_love, examples/example_shared (same app on Roblox and LÖVE), LuaJIT checksum tests

This commit is contained in:
4DBug
2026-10-08 19:31:22 -05:00
parent 81c5ad9e43
commit 66fa061089
23 changed files with 56780 additions and 17 deletions
+13
View File
@@ -167,6 +167,19 @@ io.Fonts:AddFontDefault()
io.Fonts:AddFontFromFileTTF("fonts/Roboto-Medium.ttf", 16.0) io.Fonts:AddFontFromFileTTF("fonts/Roboto-Medium.ttf", 16.0)
``` ```
### Other backends: LÖVE
The same sources also build for [LÖVE](https://love2d.org) (LuaJIT), with a GPU renderer in [backends/imgui_impl_love.lua](backends/imgui_impl_love.lua):
```
python3 tools/bundle.py --target love # -> build/imgui_love.lua (LÖVE backend + compat/luajit.lua shims)
love examples/example_love
```
Roblox stays the primary target: the Roblox build is untouched by this (no shims, `@native` kept); the LÖVE build strips `@native` and adds small `bit32`/`buffer`/`utf8`/`table` shims ([compat/luajit.lua](compat/luajit.lua)). `tools/test/love/run.sh` checks that LuaJIT produces exactly the same draw data as Luau.
[examples/example_shared/app.lua](examples/example_shared/app.lua) is one UI that runs unchanged on both: `roblox.client.luau` (Rojo: `examples/example_shared/default.project.json`) and `main.lua` (`love examples/example_shared`) are the only platform code. Write shared UI code in plain Lua 5.1 syntax (no `+=`, `//`, `continue`).
### Differences from C++ Dear ImGui ### Differences from C++ Dear ImGui
The API is the same function-for-function; these are the Luau-specific conventions: The API is the same function-for-function; these are the Luau-specific conventions:
+397
View File
@@ -0,0 +1,397 @@
--- 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
+5 -5
View File
@@ -3779,7 +3779,7 @@ function ImHashStr(str, size, seed)
if hash_memo_n >= HASH_MEMO_MAX then hash_memo = { [seed] = m }; table.clear(m); hash_memo_n = 0 end if hash_memo_n >= HASH_MEMO_MAX then hash_memo = { [seed] = m }; table.clear(m); hash_memo_n = 0 end
h = ImHashStrRaw(str, #str, seed) h = ImHashStrRaw(str, #str, seed)
m[str] = h m[str] = h
hash_memo_n += 1 hash_memo_n = hash_memo_n + 1
end end
return h return h
end end
@@ -18717,7 +18717,7 @@ function ExampleTree_CreateDemoTree()
uid = uid + 1 uid = uid + 1
local node_L0 = ExampleTree_CreateNode("<ROOT>", uid, nil) local node_L0 = ExampleTree_CreateNode("<ROOT>", uid, nil)
for idx_L0 = 0, ROOT_ITEMS_COUNT - 1 do for idx_L0 = 0, ROOT_ITEMS_COUNT - 1 do
local name_buf = string.format("%s %d", category_names[idx_L0 // (ROOT_ITEMS_COUNT // category_count) + 1], idx_L0 % (ROOT_ITEMS_COUNT // category_count)) local name_buf = string.format("%s %d", category_names[math.floor(idx_L0 / math.floor(ROOT_ITEMS_COUNT / category_count)) + 1], idx_L0 % math.floor(ROOT_ITEMS_COUNT / category_count))
uid = uid + 1 uid = uid + 1
local node_L1 = ExampleTree_CreateNode(name_buf, uid, node_L0) local node_L1 = ExampleTree_CreateNode(name_buf, uid, node_L0)
local number_of_childs = #node_L1.Name local number_of_childs = #node_L1.Name
@@ -28499,7 +28499,7 @@ function ImFontCalcTextSizeEx(font, size, max_width, wrap_width, text, text_begi
if cache_key then if cache_key then
baked._TextSizeCache[cache_key] = { text_size.x / scale, text_size.y / line_height } baked._TextSizeCache[cache_key] = { text_size.x / scale, text_size.y / line_height }
baked._TextSizeCacheN += 1 baked._TextSizeCacheN = baked._TextSizeCacheN + 1
end end
return text_size, out_remaining return text_size, out_remaining
@@ -51515,7 +51515,7 @@ function FormatCached(format, v)
local r = m[v] local r = m[v]
if r == nil then if r == nil then
if fmt_memo_n >= 4096 then table.clear(fmt_memo); m = {}; fmt_memo[format] = m; fmt_memo_n = 0 end if fmt_memo_n >= 4096 then table.clear(fmt_memo); m = {}; fmt_memo[format] = m; fmt_memo_n = 0 end
r = ImFormatString(format, v); m[v] = r; fmt_memo_n += 1 r = ImFormatString(format, v); m[v] = r; fmt_memo_n = fmt_memo_n + 1
end end
return r return r
end end
@@ -52510,7 +52510,7 @@ function ImGui.NewTextBuffer(str, size)
end end
function ImGui.TextBufferToString(buf) function ImGui.TextBufferToString(buf)
local n = 0 local n = 0
while buf[n + 1] ~= nil and buf[n + 1] ~= 0 do n += 1 end while buf[n + 1] ~= nil and buf[n + 1] ~= 0 do n = n + 1 end
return n == 0 and "" or ImGui._ByteArrayToString(buf, 1, n + 1) return n == 0 and "" or ImGui._ByteArrayToString(buf, 1, n + 1)
end end
+1
View File
@@ -0,0 +1 @@
[[149, "imgui.lua"], [18609, "imgui_demo.lua"], [25894, "imgui_draw.lua"], [31132, "imgui_h.lua"], [33330, "imgui_impl_love.lua"], [33730, "imgui_internal.lua"], [36902, "imgui_tables.lua"], [41249, "imgui_widgets.lua"], [51004, "imstb_rectpack.lua"], [51450, "imstb_textedit.lua"], [52691, "imstb_truetype.lua"], [55159, "imstd_minstdio.lua"]]
+55904
View File
File diff suppressed because it is too large Load Diff
+137
View File
@@ -0,0 +1,137 @@
-- Luau standard library pieces the port uses, for LuaJIT (LÖVE). Prepended by `tools/bundle.py --target love` only:
-- the Roblox build uses Luau's native bit32/buffer/table/utf8 and never sees this file.
local ffi = require("ffi")
local bit = require("bit")
local floor = math.floor
local TWO32 = 4294967296
-- bit32: unsigned 32-bit results like Luau/Lua 5.2 (LuaJIT's `bit` returns signed values)
if not bit32 then
local band, bor, bxor, bnot, lshift, rshift, arshift, tobit = bit.band, bit.bor, bit.bxor, bit.bnot, bit.lshift, bit.rshift, bit.arshift, bit.tobit
local function u(x) return x % TWO32 end
local function t(x) return tobit(x % TWO32) end -- any number -> int32 (bit.* wraps doubles >= 2^31 unreliably)
bit32 = {}
function bit32.band(a, b, ...)
if a == nil then return TWO32 - 1 end
if b == nil then return u(t(a)) end
local r = band(t(a), t(b))
if ... ~= nil then for i = 1, select("#", ...) do r = band(r, t((select(i, ...)))) end end
return u(r)
end
function bit32.bor(a, b, ...)
if a == nil then return 0 end
if b == nil then return u(t(a)) end
local r = bor(t(a), t(b))
if ... ~= nil then for i = 1, select("#", ...) do r = bor(r, t((select(i, ...)))) end end
return u(r)
end
function bit32.bxor(a, b, ...)
if a == nil then return 0 end
if b == nil then return u(t(a)) end
local r = bxor(t(a), t(b))
if ... ~= nil then for i = 1, select("#", ...) do r = bxor(r, t((select(i, ...)))) end end
return u(r)
end
function bit32.bnot(a) return u(bnot(t(a))) end
function bit32.lshift(a, n) if n >= 32 or n <= -32 then return 0 end if n < 0 then return u(rshift(t(a), -n)) end return u(lshift(t(a), n)) end
function bit32.rshift(a, n) if n >= 32 or n <= -32 then return 0 end if n < 0 then return u(lshift(t(a), -n)) end return u(rshift(t(a), n)) end
function bit32.arshift(a, n) if n >= 32 then n = 31 end return u(arshift(t(a), n)) end
function bit32.btest(...) return bit32.band(...) ~= 0 end
function bit32.extract(n, field, width) width = width or 1; return u(band(rshift(t(n), field), 2 ^ width - 1)) end
end
-- buffer: fixed-size byte buffers (subset used by the port), on FFI memory. Little-endian like Luau.
if not buffer then
ffi.cdef("void *memmove(void *dst, const void *src, size_t n); void *memset(void *s, int c, size_t n);")
local C = ffi.C
local u8p, u32p, f64p = ffi.typeof("uint8_t*"), ffi.typeof("uint32_t*"), ffi.typeof("double*")
buffer = {}
function buffer.create(n)
local mem = ffi.new("uint8_t[?]", math.max(n, 1)) -- zero-filled
return { p = ffi.cast(u8p, mem), n = n, mem = mem }
end
function buffer.len(b) return b.n end
function buffer.readu8(b, o) return b.p[o] end
function buffer.writeu8(b, o, v) b.p[o] = v end
function buffer.readu32(b, o) return ffi.cast(u32p, b.p + o)[0] end
function buffer.writeu32(b, o, v) ffi.cast(u32p, b.p + o)[0] = v % TWO32 end
function buffer.readf64(b, o) return ffi.cast(f64p, b.p + o)[0] end
function buffer.writef64(b, o, v) ffi.cast(f64p, b.p + o)[0] = v end
function buffer.readstring(b, o, n) return ffi.string(b.p + o, n) end
function buffer.writestring(b, o, s, n) ffi.copy(b.p + o, s, n or #s) end
function buffer.copy(dst, doff, src, soff, n)
soff = soff or 0
n = n or (src.n - soff)
if n > 0 then C.memmove(dst.p + doff, src.p + soff, n) end
end
function buffer.fill(b, o, v, n) n = n or (b.n - o); if n > 0 then C.memset(b.p + o, v, n) end end
function buffer.tostring(b) return ffi.string(b.p, b.n) end
function buffer.fromstring(s) local b = buffer.create(#s); ffi.copy(b.p, s, #s); return b end
end
-- table / math / utf8 additions from Luau / Lua 5.3
do
local ok, clear = pcall(require, "table.clear")
table.clear = table.clear or (ok and clear) or function(t) for k in pairs(t) do t[k] = nil end end
table.unpack = table.unpack or unpack
table.find = table.find or function(t, v, init) for i = init or 1, #t do if t[i] == v then return i end end end
table.move = table.move or function(a1, f, e, t, a2)
a2 = a2 or a1
if e >= f then
if t > f or t > e or a1 ~= a2 then for i = 0, e - f do a2[t + i] = a1[f + i] end
else for i = e - f, 0, -1 do a2[t + i] = a1[f + i] end end
end
return a2
end
math.pow = math.pow or function(a, b) return a ^ b end
end
if not utf8 then
utf8 = { charpattern = "[\0-\x7F\xC2-\xFD][\x80-\xBF]*" }
function utf8.char(...)
local out = {}
for i = 1, select("#", ...) do
local c = select(i, ...)
if c < 0x80 then out[#out + 1] = string.char(c)
elseif c < 0x800 then out[#out + 1] = string.char(0xC0 + floor(c / 64), 0x80 + c % 64)
elseif c < 0x10000 then out[#out + 1] = string.char(0xE0 + floor(c / 4096), 0x80 + floor(c / 64) % 64, 0x80 + c % 64)
else out[#out + 1] = string.char(0xF0 + floor(c / 262144), 0x80 + floor(c / 4096) % 64, 0x80 + floor(c / 64) % 64, 0x80 + c % 64) end
end
return table.concat(out)
end
local function decode(s, i)
local c = s:byte(i)
if not c then return nil end
if c < 0x80 then return c, 1 end
if c < 0xE0 then return (c % 32) * 64 + s:byte(i + 1) % 64, 2 end
if c < 0xF0 then return (c % 16) * 4096 + (s:byte(i + 1) % 64) * 64 + s:byte(i + 2) % 64, 3 end
return (c % 8) * 262144 + (s:byte(i + 1) % 64) * 4096 + (s:byte(i + 2) % 64) * 64 + s:byte(i + 3) % 64, 4
end
function utf8.codes(s)
local i = 1
return function()
if i > #s then return nil end
local c, n = decode(s, i)
local p = i; i = i + n
return p, c
end
end
function utf8.codepoint(s, i, j)
i = i or 1; j = j or i
local out = {}
while i <= j do local c, n = decode(s, i); out[#out + 1] = c; i = i + n end
return unpack(out)
end
function utf8.len(s) local n = 0; for _ in utf8.codes(s) do n = n + 1 end return n end
function utf8.offset(s, n, i)
i = i or (n >= 0 and 1 or #s + 1)
if n > 0 then
n = n - 1
while n > 0 and i <= #s do i = i + 1; while i <= #s and s:byte(i) >= 0x80 and s:byte(i) < 0xC0 do i = i + 1 end; n = n - 1 end
if n > 0 then return nil end
return i
end
return i
end
end
+6
View File
@@ -18,6 +18,12 @@ there, but keep this port's existing data structures and conventions. Read the s
- Luau has a limit of 200 locals per function and 255 upvalues; split huge functions (like demo sections) into - Luau has a limit of 200 locals per function and 255 upvalues; split huge functions (like demo sections) into
several local/global functions. several local/global functions.
## Portability (LÖVE build)
- Library code must stay valid **Lua 5.1/LuaJIT syntax** apart from `@native` lines (stripped for LÖVE): no `+=`, `//`,
`continue`, type annotations, `if`-expressions or string interpolation. Luau library calls are fine only if
`compat/luajit.lua` shims them (`bit32`, `buffer`, `table.clear/find/move/unpack`, `utf8`).
- Check with `tools/test/love/run.sh` (LuaJIT draw checksums must equal the Luau ones).
## Conventions (follow what the port already does) ## Conventions (follow what the port already does)
- `ImVector`: 1-based `.Data`, `.Size`, methods `push_back/pop_back/resize/back/iter/erase/...` (imgui_h.lua). - `ImVector`: 1-based `.Data`, `.Size`, methods `push_back/pop_back/resize/back/iter/erase/...` (imgui_h.lua).
Indices that C++ stores as 0-based *values* (e.g. column index, tab order) stay 0-based unless the surrounding Indices that C++ stores as 0-based *values* (e.g. column index, tab order) stay 0-based unless the surrounding
+5
View File
@@ -0,0 +1,5 @@
function love.conf(t)
t.window.title = "Dear ImGui - LÖVE example"
t.window.width, t.window.height = 1280, 800
t.window.resizable = true
end
+62
View File
@@ -0,0 +1,62 @@
-- Dear ImGui on LÖVE: python3 tools/bundle.py --target love then love examples/example_love
local ROOT = love.filesystem.getSource() .. "/../../" -- repo root (build/, fonts/)
local f = assert(io.open(ROOT .. "build/imgui_love.lua", "rb"), "run: python3 tools/bundle.py --target love")
local G = assert(loadstring(f:read("*a"), "@imgui_love.lua"))(); f:close()
local ImGui, ImVec4 = G.ImGui, G.ImVec4
local clear_color = ImVec4(0.45, 0.55, 0.60, 1.00)
local show_demo_window, show_another_window = true, false
local value, counter = 0.0, 0
function love.load()
ImGui.CreateContext()
local io = ImGui.GetIO()
io.ConfigFlags = bit32.bor(io.ConfigFlags, G.ImGuiConfigFlags.NavEnableKeyboard, G.ImGuiConfigFlags.DockingEnable)
io.IniFilename = nil
G.ImGui_ImplLove.Init({ root = ROOT })
io.Fonts:AddFontDefault()
io.Fonts:AddFontFromFileTTF("fonts/Roboto-Medium.ttf", 16.0)
end
function love.update(dt)
G.ImGui_ImplLove.NewFrame()
ImGui.NewFrame()
if show_demo_window then show_demo_window = ImGui.ShowDemoWindow(show_demo_window) end
ImGui.Begin("Hello, world!")
ImGui.Text("This is some useful text.")
local _
_, show_demo_window = ImGui.Checkbox("Demo Window", show_demo_window)
_, show_another_window = ImGui.Checkbox("Another Window", show_another_window)
value = ImGui.SliderFloat("float", value, 0.0, 1.0)
ImGui.ColorEdit3("clear color", clear_color)
if ImGui.Button("Button") then counter = counter + 1 end
ImGui.SameLine(); ImGui.Text("counter = %d", counter)
local io = ImGui.GetIO()
ImGui.Text("Application average %.3f ms/frame (%.1f FPS)", 1000.0 / io.Framerate, io.Framerate)
ImGui.End()
if show_another_window then
show_another_window = ImGui.Begin("Another Window", show_another_window)
ImGui.Text("Hello from another window!")
if ImGui.Button("Close Me") then show_another_window = false end
ImGui.End()
end
ImGui.Render()
end
function love.draw()
love.graphics.clear(clear_color.x, clear_color.y, clear_color.z, 1)
G.ImGui_ImplLove.RenderDrawData(ImGui.GetDrawData())
end
function love.mousemoved(x, y) G.ImGui_ImplLove.MouseMoved(x, y) end
function love.mousepressed(x, y, b) G.ImGui_ImplLove.MousePressed(x, y, b) end
function love.mousereleased(x, y, b) G.ImGui_ImplLove.MouseReleased(x, y, b) end
function love.wheelmoved(x, y) G.ImGui_ImplLove.WheelMoved(x, y) end
function love.keypressed(k) G.ImGui_ImplLove.KeyPressed(k) end
function love.keyreleased(k) G.ImGui_ImplLove.KeyReleased(k) end
function love.textinput(t) G.ImGui_ImplLove.TextInput(t) end
function love.focus(f) G.ImGui_ImplLove.Focus(f) end
+53
View File
@@ -0,0 +1,53 @@
-- One UI that runs unchanged on Roblox and LÖVE: only the Dear ImGui API is used, and the code is plain Lua 5.1
-- (no Luau-only syntax), so both Luau and LuaJIT run it. The platform launchers are roblox.client.luau and main.lua.
-- local App = require(app)(G) -- G = the table returned by the ImGui bundle
-- every frame, between ImGui.NewFrame() and ImGui.Render(): App.Frame()
return function(G)
local ImGui, ImVec2, ImVec4 = G.ImGui, G.ImVec2, G.ImVec4
local App = {
Platform = "?", -- set by the launcher ("Roblox" / "LÖVE")
ClearColor = ImVec4(0.45, 0.55, 0.60, 1.00), -- launchers may use it as the background colour
}
local show_demo, show_table = false, true
local counter, speed = 0, 1.0
local name = ImGui.NewTextBuffer("Player", 64)
local samples, t = {}, 0.0
local items = { { "Sword", 1, 120 }, { "Potion", 5, 15 }, { "Arrow", 64, 1 }, { "Shield", 1, 80 } }
function App.Frame()
local io = ImGui.GetIO()
t = t + io.DeltaTime * speed
for i = 1, 90 do samples[i] = math.sin(i / 90 * math.pi * 4 + t * 3) end
ImGui.SetNextWindowPos(ImVec2(20, 20), G.ImGuiCond.FirstUseEver)
ImGui.SetNextWindowSize(ImVec2(420, 360), G.ImGuiCond.FirstUseEver)
ImGui.Begin("Shared Example")
ImGui.Text("Running on %s (%.1f FPS)", App.Platform, io.Framerate)
ImGui.Separator()
ImGui.InputText("name", name, 64)
if ImGui.Button("Click me") then counter = counter + 1 end
ImGui.SameLine(); ImGui.Text("%s clicked %d times", ImGui.TextBufferToString(name), counter)
speed = ImGui.SliderFloat("speed", speed, 0.0, 4.0)
ImGui.ColorEdit3("background", App.ClearColor)
ImGui.PlotLines("wave", samples, nil, #samples, 0, nil, -1.0, 1.0, ImVec2(0, 60))
local _
_, show_table = ImGui.Checkbox("Inventory table", show_table)
ImGui.SameLine(); _, show_demo = ImGui.Checkbox("Demo window", show_demo)
if show_table and ImGui.BeginTable("inv", 3, bit32.bor(G.ImGuiTableFlags.Borders, G.ImGuiTableFlags.RowBg, G.ImGuiTableFlags.Sortable)) then
ImGui.TableSetupColumn("Item"); ImGui.TableSetupColumn("Count"); ImGui.TableSetupColumn("Value")
ImGui.TableHeadersRow()
for _, it in ipairs(items) do
ImGui.TableNextRow()
ImGui.TableNextColumn(); ImGui.Text(it[1])
ImGui.TableNextColumn(); ImGui.Text("%d", it[2])
ImGui.TableNextColumn(); ImGui.Text("%d", it[3])
end
ImGui.EndTable()
end
ImGui.End()
if show_demo and ImGui.ShowDemoWindow then show_demo = ImGui.ShowDemoWindow(show_demo) end
end
return App
end
+5
View File
@@ -0,0 +1,5 @@
function love.conf(t)
t.window.title = "Dear ImGui - shared example (LÖVE)"
t.window.width, t.window.height = 1280, 800
t.window.resizable = true
end
@@ -0,0 +1,15 @@
{
"name": "imgui-shared-example",
"tree": {
"$className": "DataModel",
"ReplicatedStorage": {
"ImGui": { "$path": "../../build/ImGui.luau" },
"SharedApp": { "$path": "app.lua" }
},
"StarterPlayer": {
"StarterPlayerScripts": {
"SharedExample": { "$path": "roblox.client.luau" }
}
}
}
}
+43
View File
@@ -0,0 +1,43 @@
-- LÖVE launcher for app.lua: python3 tools/bundle.py --target love then love examples/example_shared
local ROOT = love.filesystem.getSource() .. "/../../" -- repo root (build/, fonts/)
local function load_bundle()
local f = assert(io.open(ROOT .. "build/imgui_love.lua", "rb"), "run: python3 tools/bundle.py --target love")
local src = f:read("*a"); f:close()
return assert(loadstring(src, "@imgui_love.lua"))()
end
local G, ImGui, App
function love.load()
G = load_bundle()
ImGui = G.ImGui
ImGui.CreateContext()
local io = ImGui.GetIO()
io.ConfigFlags = bit32.bor(io.ConfigFlags, G.ImGuiConfigFlags.NavEnableKeyboard, G.ImGuiConfigFlags.DockingEnable)
io.IniFilename = nil
G.ImGui_ImplLove.Init({ root = ROOT })
App = love.filesystem.load("app.lua")()(G)
App.Platform = "LÖVE"
end
function love.update(dt)
G.ImGui_ImplLove.NewFrame()
ImGui.NewFrame()
App.Frame()
ImGui.Render()
end
function love.draw()
local c = App.ClearColor
love.graphics.clear(c.x, c.y, c.z, 1)
G.ImGui_ImplLove.RenderDrawData(ImGui.GetDrawData())
end
function love.mousemoved(x, y) G.ImGui_ImplLove.MouseMoved(x, y) end
function love.mousepressed(x, y, b) G.ImGui_ImplLove.MousePressed(x, y, b) end
function love.mousereleased(x, y, b) G.ImGui_ImplLove.MouseReleased(x, y, b) end
function love.wheelmoved(x, y) G.ImGui_ImplLove.WheelMoved(x, y) end
function love.keypressed(k) G.ImGui_ImplLove.KeyPressed(k) end
function love.keyreleased(k) G.ImGui_ImplLove.KeyReleased(k) end
function love.textinput(t) G.ImGui_ImplLove.TextInput(t) end
function love.focus(f) G.ImGui_ImplLove.Focus(f) end
@@ -0,0 +1,21 @@
-- Roblox launcher for app.lua (sync with examples/example_shared/default.project.json).
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local G = require(ReplicatedStorage:WaitForChild("ImGui"))
local ImGui = G.ImGui
ImGui.CreateContext()
local io = ImGui.GetIO()
io.ConfigFlags = bit32.bor(io.ConfigFlags, G.ImGuiConfigFlags.NavEnableKeyboard, G.ImGuiConfigFlags.DockingEnable)
G.ImGui_ImplRoblox.Init()
local App = require(ReplicatedStorage:WaitForChild("SharedApp"))(G)
App.Platform = "Roblox"
RunService.Stepped:Connect(function()
G.ImGui_ImplRoblox.NewFrame()
ImGui.NewFrame()
App.Frame()
ImGui.Render()
G.ImGui_ImplRoblox.RenderDrawData(ImGui.GetDrawData())
end)
+1 -1
View File
@@ -3768,7 +3768,7 @@ function ImHashStr(str, size, seed)
if hash_memo_n >= HASH_MEMO_MAX then hash_memo = { [seed] = m }; table.clear(m); hash_memo_n = 0 end if hash_memo_n >= HASH_MEMO_MAX then hash_memo = { [seed] = m }; table.clear(m); hash_memo_n = 0 end
h = ImHashStrRaw(str, #str, seed) h = ImHashStrRaw(str, #str, seed)
m[str] = h m[str] = h
hash_memo_n += 1 hash_memo_n = hash_memo_n + 1
end end
return h return h
end end
+1 -1
View File
@@ -246,7 +246,7 @@ function ExampleTree_CreateDemoTree()
uid = uid + 1 uid = uid + 1
local node_L0 = ExampleTree_CreateNode("<ROOT>", uid, nil) local node_L0 = ExampleTree_CreateNode("<ROOT>", uid, nil)
for idx_L0 = 0, ROOT_ITEMS_COUNT - 1 do for idx_L0 = 0, ROOT_ITEMS_COUNT - 1 do
local name_buf = string.format("%s %d", category_names[idx_L0 // (ROOT_ITEMS_COUNT // category_count) + 1], idx_L0 % (ROOT_ITEMS_COUNT // category_count)) local name_buf = string.format("%s %d", category_names[math.floor(idx_L0 / math.floor(ROOT_ITEMS_COUNT / category_count)) + 1], idx_L0 % math.floor(ROOT_ITEMS_COUNT / category_count))
uid = uid + 1 uid = uid + 1
local node_L1 = ExampleTree_CreateNode(name_buf, uid, node_L0) local node_L1 = ExampleTree_CreateNode(name_buf, uid, node_L0)
local number_of_childs = #node_L1.Name local number_of_childs = #node_L1.Name
+1 -1
View File
@@ -2743,7 +2743,7 @@ function ImFontCalcTextSizeEx(font, size, max_width, wrap_width, text, text_begi
if cache_key then if cache_key then
baked._TextSizeCache[cache_key] = { text_size.x / scale, text_size.y / line_height } baked._TextSizeCache[cache_key] = { text_size.x / scale, text_size.y / line_height }
baked._TextSizeCacheN += 1 baked._TextSizeCacheN = baked._TextSizeCacheN + 1
end end
return text_size, out_remaining return text_size, out_remaining
+2 -2
View File
@@ -3069,7 +3069,7 @@ function FormatCached(format, v)
local r = m[v] local r = m[v]
if r == nil then if r == nil then
if fmt_memo_n >= 4096 then table.clear(fmt_memo); m = {}; fmt_memo[format] = m; fmt_memo_n = 0 end if fmt_memo_n >= 4096 then table.clear(fmt_memo); m = {}; fmt_memo[format] = m; fmt_memo_n = 0 end
r = ImFormatString(format, v); m[v] = r; fmt_memo_n += 1 r = ImFormatString(format, v); m[v] = r; fmt_memo_n = fmt_memo_n + 1
end end
return r return r
end end
@@ -4064,7 +4064,7 @@ function ImGui.NewTextBuffer(str, size)
end end
function ImGui.TextBufferToString(buf) function ImGui.TextBufferToString(buf)
local n = 0 local n = 0
while buf[n + 1] ~= nil and buf[n + 1] ~= 0 do n += 1 end while buf[n + 1] ~= nil and buf[n + 1] ~= 0 do n = n + 1 end
return n == 0 and "" or ImGui._ByteArrayToString(buf, 1, n + 1) return n == 0 and "" or ImGui._ByteArrayToString(buf, 1, n + 1)
end end
+20 -7
View File
@@ -1,6 +1,8 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# Bundles the root *.lua (library + imgui_demo.lua), backends/ and fonts/ into ONE ModuleScript: build/ImGui.luau # Bundles the root *.lua (library + imgui_demo.lua), backends/ and fonts/ into ONE ModuleScript: build/ImGui.luau
# --no-demo: library only (imgui_demo.lua left out, like not compiling imgui_demo.cpp) # --no-demo: library only (imgui_demo.lua left out, like not compiling imgui_demo.cpp)
# --target love: build/imgui_love.lua for LÖVE/LuaJIT instead (LÖVE backend, compat/luajit.lua shims, @native stripped).
# The default (roblox) build is unaffected by it.
# Why: ModuleScripts don't share globals, and the setfenv() alternative turns off Luau's fast paths / native codegen. # Why: ModuleScripts don't share globals, and the setfenv() alternative turns off Luau's fast paths / native codegen.
# Each source file becomes a function in __FILES; IM_INCLUDE(name) runs it once and caches the result. # Each source file becomes a function in __FILES; IM_INCLUDE(name) runs it once and caches the result.
# The module returns a table with every global the library defines (ImGui, ImVec2, ImGuiConfigFlags, ImGui_ImplRoblox, ...). # The module returns a table with every global the library defines (ImGui, ImVec2, ImGuiConfigFlags, ImGui_ImplRoblox, ...).
@@ -9,21 +11,31 @@ import os, re
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
import sys import sys
NO_DEMO = "--no-demo" in sys.argv NO_DEMO = "--no-demo" in sys.argv
LOVE = False
if "--target" in sys.argv:
i = sys.argv.index("--target"); LOVE = sys.argv[i + 1] == "love"; del sys.argv[i:i + 2]
sys.argv = [a for a in sys.argv if a != "--no-demo"] sys.argv = [a for a in sys.argv if a != "--no-demo"]
BACKEND = "imgui_impl_love.lua" if LOVE else "imgui_impl_roblox.lua"
SKIP = {"imgui_impl_roblox.lua", "imgui_impl_roblox_fonts.lua"} if LOVE else {"imgui_impl_love.lua"}
FILES = {} FILES = {}
for d in (ROOT, os.path.join(ROOT, "backends"), os.path.join(ROOT, "fonts")): for d in (ROOT, os.path.join(ROOT, "backends"), os.path.join(ROOT, "fonts")):
for f in sorted(os.listdir(d)): for f in sorted(os.listdir(d)):
if f.endswith(".lua") and not (NO_DEMO and f == "imgui_demo.lua"): if f.endswith(".lua") and not (NO_DEMO and f == "imgui_demo.lua") and f not in SKIP:
FILES[f] = open(os.path.join(d, f)).read() src = open(os.path.join(d, f)).read()
if LOVE: src = re.sub(r"^@native$", "", src, flags=re.M) # keep line numbers
FILES[f] = src
ENTRY = ["imgui.lua", "imgui_impl_roblox.lua"] + ([] if NO_DEMO else ["imgui_demo.lua"]) # run order ENTRY = ["imgui.lua", BACKEND] + ([] if NO_DEMO else ["imgui_demo.lua"]) # run order
names = set() names = set()
for src in FILES.values(): for src in FILES.values():
names.update(re.findall(r"^function ([A-Za-z_]\w*)\s*\(", src, re.M)) names.update(re.findall(r"^function ([A-Za-z_]\w*)\s*\(", src, re.M))
names.update(re.findall(r"^([A-Za-z_]\w*)\s*=[^=]", src, re.M)) names.update(re.findall(r"^([A-Za-z_]\w*)\s*=[^=]", src, re.M))
out = ["--!optimize 2", "-- GENERATED by tools/bundle.py from *.lua, backends/, fonts/ - do not edit", "", out = ["--!optimize 2", "-- GENERATED by tools/bundle.py from *.lua, backends/, fonts/ - do not edit", ""]
if LOVE:
out = ["-- GENERATED by tools/bundle.py --target love - do not edit", open(os.path.join(ROOT, "compat", "luajit.lua")).read(), ""]
out += [
"local __FILES, __LOADED = {}, {}", "local __FILES, __LOADED = {}, {}",
"function IM_INCLUDE(name)", "function IM_INCLUDE(name)",
" local r = __LOADED[name]", " local r = __LOADED[name]",
@@ -48,7 +60,8 @@ out.append("}")
import sys import sys
OUT = sys.argv[1] if len(sys.argv) > 1 else os.path.join(ROOT, "build") OUT = sys.argv[1] if len(sys.argv) > 1 else os.path.join(ROOT, "build")
os.makedirs(OUT, exist_ok=True) os.makedirs(OUT, exist_ok=True)
open(os.path.join(OUT, "ImGui.luau"), "w").write("\n".join(out) + "\n") NAME = "imgui_love.lua" if LOVE else "ImGui.luau"
open(os.path.join(OUT, NAME), "w").write("\n".join(out) + "\n")
import json import json
json.dump(linemap, open(os.path.join(OUT, "ImGui.linemap.json"), "w")) json.dump(linemap, open(os.path.join(OUT, NAME.rsplit(".", 1)[0] + ".linemap.json"), "w"))
print("wrote build/ImGui.luau (%d files, %d exports)" % (len(FILES), len(names))) print("wrote build/%s (%d files, %d exports)" % (NAME, len(FILES), len(names)))
+25
View File
@@ -0,0 +1,25 @@
-- Minimal headless `love` for running build/imgui_love.lua under plain LuaJIT (tools/test/love/run.sh).
local ffi = require("ffi")
local W, H = 1920, 1080
local t = 0
local function obj(extra) local o = { release = function() end }; for k, v in pairs(extra or {}) do o[k] = v end; return o end
local function bytedata(n)
local mem = ffi.new("uint8_t[?]", n)
return obj({ getFFIPointer = function() return mem end, getSize = function() return n end })
end
love = {
timer = { getTime = function() return t end, step = function(dt) t = t + dt end },
graphics = {
getDimensions = function() return W, H end,
newImage = function() return obj({ setFilter = function() end, replacePixels = function() end }) end,
newMesh = function() return obj({ setVertices = function() end, setVertexMap = function() end, setTexture = function() end, setDrawRange = function() end }) end,
push = function() end, pop = function() end, setBlendMode = function() end, setColor = function() end,
translate = function() end, setScissor = function() end, draw = function() MOCK_DRAWS = (MOCK_DRAWS or 0) + 1 end,
},
image = { newImageData = function(w, h) return bytedata(w * h * 4) end },
data = { newByteData = function(n) return bytedata(n) end },
keyboard = { isDown = function() return false end, setTextInput = function() end, setKeyRepeat = function() end },
mouse = { setCursor = function() end, getSystemCursor = function() return {} end, setVisible = function() end },
system = { getClipboardText = function() return "" end, setClipboardText = function() end, openURL = function() end },
filesystem = { getInfo = function() return nil end, read = function() return nil end },
}
+7
View File
@@ -0,0 +1,7 @@
#!/bin/sh
# LÖVE/LuaJIT checks: build the love bundle, compare draw checksums with the Luau ones, run both LÖVE examples headless.
set -e
cd "$(dirname "$0")/../../.."
python3 tools/bundle.py --target love >/dev/null
nix-shell -p luajit --run "luajit tools/test/love/t_drawhash.lua 150 Widgets,Basic && luajit tools/test/love/t_drawhash.lua 150 all && luajit tools/test/love/t_examples.lua"
echo "expected (Luau, tools/test/scripts/t_drawhash.luau): 1385106188 / 1292122782"
+41
View File
@@ -0,0 +1,41 @@
-- LuaJIT twin of tools/test/scripts/t_drawhash.luau: same input, same checksum => compat shims behave like Luau.
-- usage: luajit tools/test/love/t_drawhash.lua <frames> [Widgets,Basic | all]
dofile("tools/test/love/mock_love.lua")
local G = dofile("build/imgui_love.lua")
local ImGui = G.ImGui
local FRAMES, OPEN = tonumber(arg[1] or 150), arg[2] or ""
local open = {}
for n in OPEN:gmatch("[^,]+") do open[n] = true end
for _, fname in ipairs({ "TreeNode", "CollapsingHeader" }) do
local real = ImGui[fname]
ImGui[fname] = function(label, ...)
if open[label] or OPEN == "all" then ImGui.SetNextItemOpen(true, G.ImGuiCond.Always) end
return real(label, ...)
end
end
ImGui.CreateContext()
G.ImGui_ImplLove.Init({ root = "./" })
local io = ImGui.GetIO()
local h = 0
local function mix(v) h = (h * 31 + math.floor(v * 64 + 0.5)) % 2147483647 end
for frame = 1, FRAMES do
love.timer.step(1 / 60)
G.ImGui_ImplLove.NewFrame()
io.DeltaTime = 1 / 60
if frame == 1 then io:AddMousePosEvent(0, 0) end -- (the Roblox backend reports the initial mouse position)
io:AddMousePosEvent(100 + (frame * 37) % 500, 60 + (frame * 53) % 600)
if frame % 7 == 0 then io:AddMouseButtonEvent(0, frame % 14 == 0) end
if frame % 11 == 0 then io:AddMouseWheelEvent(0, -1) end
ImGui.NewFrame()
ImGui.ShowDemoWindow(true)
ImGui.Render()
for _, dl in ImGui.GetDrawData().CmdLists:iter() do
local vb, id = dl.VtxBuffer.Buf, dl.IdxBuffer.Data
for i = 1, dl.VtxBuffer.Size do local o = (i - 1) * 36; mix(buffer.readf64(vb, o)); mix(buffer.readf64(vb, o + 8)); mix(buffer.readf64(vb, o + 16) * 4096); mix(buffer.readf64(vb, o + 24) * 4096); mix(buffer.readu32(vb, o + 32) / 64) end
for i = 1, dl.IdxBuffer.Size do mix(id[i]) end
for _, c in dl.CmdBuffer:iter() do mix(c.ClipRect.x); mix(c.ClipRect.y); mix(c.ClipRect.z); mix(c.ClipRect.w); mix(c.ElemCount) end
end
G.ImGui_ImplLove.RenderDrawData(ImGui.GetDrawData())
if frame == FRAMES - 1 then print("DRAWHASH", string.format("%d", h)) end
end
print("draws", MOCK_DRAWS)
+15
View File
@@ -0,0 +1,15 @@
-- Runs examples/example_love and examples/example_shared main.lua headless under LuaJIT (mock love) for some frames.
for _, ex in ipairs({ "examples/example_love", "examples/example_shared" }) do
dofile("tools/test/love/mock_love.lua")
love.filesystem.getSource = function() return ex end
love.filesystem.load = function(p) return assert(loadfile(ex .. "/" .. p)) end
love.graphics.clear = function() end
dofile(ex .. "/main.lua")
love.load()
for i = 1, 30 do
love.timer.step(1 / 60)
love.mousemoved(100 + i, 100); love.update(1 / 60); love.draw()
end
love.keypressed("a"); love.textinput("a"); love.keyreleased("a"); love.update(1 / 60); love.draw()
print(ex, "OK", MOCK_DRAWS)
end