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:
@@ -167,6 +167,19 @@ io.Fonts:AddFontDefault()
|
||||
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
|
||||
|
||||
The API is the same function-for-function; these are the Luau-specific conventions:
|
||||
|
||||
@@ -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
@@ -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
|
||||
h = ImHashStrRaw(str, #str, seed)
|
||||
m[str] = h
|
||||
hash_memo_n += 1
|
||||
hash_memo_n = hash_memo_n + 1
|
||||
end
|
||||
return h
|
||||
end
|
||||
@@ -18717,7 +18717,7 @@ function ExampleTree_CreateDemoTree()
|
||||
uid = uid + 1
|
||||
local node_L0 = ExampleTree_CreateNode("<ROOT>", uid, nil)
|
||||
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
|
||||
local node_L1 = ExampleTree_CreateNode(name_buf, uid, node_L0)
|
||||
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
|
||||
baked._TextSizeCache[cache_key] = { text_size.x / scale, text_size.y / line_height }
|
||||
baked._TextSizeCacheN += 1
|
||||
baked._TextSizeCacheN = baked._TextSizeCacheN + 1
|
||||
end
|
||||
|
||||
return text_size, out_remaining
|
||||
@@ -51515,7 +51515,7 @@ function FormatCached(format, v)
|
||||
local r = m[v]
|
||||
if r == nil then
|
||||
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
|
||||
return r
|
||||
end
|
||||
@@ -52510,7 +52510,7 @@ function ImGui.NewTextBuffer(str, size)
|
||||
end
|
||||
function ImGui.TextBufferToString(buf)
|
||||
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)
|
||||
end
|
||||
|
||||
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
@@ -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
|
||||
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)
|
||||
- `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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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
|
||||
h = ImHashStrRaw(str, #str, seed)
|
||||
m[str] = h
|
||||
hash_memo_n += 1
|
||||
hash_memo_n = hash_memo_n + 1
|
||||
end
|
||||
return h
|
||||
end
|
||||
|
||||
+1
-1
@@ -246,7 +246,7 @@ function ExampleTree_CreateDemoTree()
|
||||
uid = uid + 1
|
||||
local node_L0 = ExampleTree_CreateNode("<ROOT>", uid, nil)
|
||||
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
|
||||
local node_L1 = ExampleTree_CreateNode(name_buf, uid, node_L0)
|
||||
local number_of_childs = #node_L1.Name
|
||||
|
||||
+1
-1
@@ -2743,7 +2743,7 @@ function ImFontCalcTextSizeEx(font, size, max_width, wrap_width, text, text_begi
|
||||
|
||||
if cache_key then
|
||||
baked._TextSizeCache[cache_key] = { text_size.x / scale, text_size.y / line_height }
|
||||
baked._TextSizeCacheN += 1
|
||||
baked._TextSizeCacheN = baked._TextSizeCacheN + 1
|
||||
end
|
||||
|
||||
return text_size, out_remaining
|
||||
|
||||
+2
-2
@@ -3069,7 +3069,7 @@ function FormatCached(format, v)
|
||||
local r = m[v]
|
||||
if r == nil then
|
||||
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
|
||||
return r
|
||||
end
|
||||
@@ -4064,7 +4064,7 @@ function ImGui.NewTextBuffer(str, size)
|
||||
end
|
||||
function ImGui.TextBufferToString(buf)
|
||||
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)
|
||||
end
|
||||
|
||||
|
||||
+20
-7
@@ -1,6 +1,8 @@
|
||||
#!/usr/bin/env python3
|
||||
# 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)
|
||||
# --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.
|
||||
# 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, ...).
|
||||
@@ -9,21 +11,31 @@ import os, re
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
import sys
|
||||
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"]
|
||||
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 = {}
|
||||
for d in (ROOT, os.path.join(ROOT, "backends"), os.path.join(ROOT, "fonts")):
|
||||
for f in sorted(os.listdir(d)):
|
||||
if f.endswith(".lua") and not (NO_DEMO and f == "imgui_demo.lua"):
|
||||
FILES[f] = open(os.path.join(d, f)).read()
|
||||
if f.endswith(".lua") and not (NO_DEMO and f == "imgui_demo.lua") and f not in SKIP:
|
||||
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()
|
||||
for src in FILES.values():
|
||||
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))
|
||||
|
||||
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 = {}, {}",
|
||||
"function IM_INCLUDE(name)",
|
||||
" local r = __LOADED[name]",
|
||||
@@ -48,7 +60,8 @@ out.append("}")
|
||||
import sys
|
||||
OUT = sys.argv[1] if len(sys.argv) > 1 else os.path.join(ROOT, "build")
|
||||
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
|
||||
json.dump(linemap, open(os.path.join(OUT, "ImGui.linemap.json"), "w"))
|
||||
print("wrote build/ImGui.luau (%d files, %d exports)" % (len(FILES), len(names)))
|
||||
json.dump(linemap, open(os.path.join(OUT, NAME.rsplit(".", 1)[0] + ".linemap.json"), "w"))
|
||||
print("wrote build/%s (%d files, %d exports)" % (NAME, len(FILES), len(names)))
|
||||
|
||||
@@ -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 },
|
||||
}
|
||||
Executable
+7
@@ -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"
|
||||
@@ -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)
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user