Files
imgui-luau/imgui/backend/imgui_impl_roblox.lua
T
2026-10-07 14:47:12 -05:00

588 lines
26 KiB
Lua

if _G.__IMGUI_ENV then setfenv(1, _G.__IMGUI_ENV) end -- [Roblox] shared env, see init.lua
--- ImGui Backend for Roblox
--- Structurally mirrors imgui_impl_love.lua
--- - Renderer: software rasterizer drawing into a CanvasDraw canvas (EditableImage). CanvasDraw's own shape
--- functions can't do per-vertex colours, texture tinting or clip rects, so triangles are rasterized straight
--- into `Canvas.Buffer` (same RGBA8 layout as ImU32) and pushed with `Canvas:Render()`.
--- - Platform: UserInputService
--- - Files: an in-memory file system (VFS), pre-filled with the fonts from imgui_impl_roblox_fonts.lua
---
--- Usage (LocalScript):
--- local G = require(ReplicatedStorage.imgui)
--- G.ImGui.CreateContext()
--- G.ImGui_ImplRoblox.Init(require(ReplicatedStorage.CanvasDraw))
local UserInputService = game:GetService("UserInputService")
local Players = game:GetService("Players")
local floor, ceil, min, max = math.floor, math.ceil, math.min, math.max
local readu8, writeu8, writeu32 = buffer.readu8, buffer.writeu8, buffer.writeu32
-- EditableImage size limit. Bigger screens get a downscaled canvas that is stretched to fit.
-- Lower it to trade sharpness for speed.
local MAX_CANVAS_SIZE = 1024
local ImGui_ImplRoblox_GetBackendData
local ImGui_ImplRoblox_UpdateTexture
local ImGui_ImplRoblox_RenderDrawData
local ImGui_ImplRoblox_Shutdown
local KEY_MAP = {} -- Enum.KeyCode -> ImGuiKey
do
local names = {
Return = "Enter", Escape = "Escape", Backspace = "Backspace", Tab = "Tab", Space = "Space", KeypadEnter = "KeypadEnter",
Up = "UpArrow", Down = "DownArrow", Left = "LeftArrow", Right = "RightArrow",
Home = "Home", End = "End", PageUp = "PageUp", PageDown = "PageDown", Insert = "Insert", Delete = "Delete",
LeftControl = "LeftCtrl", RightControl = "RightCtrl", LeftShift = "LeftShift", RightShift = "RightShift",
LeftAlt = "LeftAlt", RightAlt = "RightAlt", LeftSuper = "LeftSuper", RightSuper = "RightSuper",
Quote = "Apostrophe", Comma = "Comma", Minus = "Minus", Period = "Period", Slash = "Slash", Semicolon = "Semicolon",
Equals = "Equal", LeftBracket = "LeftBracket", BackSlash = "Backslash", RightBracket = "RightBracket", Backquote = "GraveAccent",
CapsLock = "CapsLock",
KeypadPeriod = "KeypadDecimal", KeypadDivide = "KeypadDivide", KeypadMultiply = "KeypadMultiply",
KeypadMinus = "KeypadSubtract", KeypadPlus = "KeypadAdd", KeypadEquals = "KeypadEqual",
}
local digits = { "Zero", "One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine" }
for i = 0, 9 do
names[digits[i + 1]] = "K" .. i
names["Keypad" .. digits[i + 1]] = "Keypad" .. i
end
for i = 1, 12 do names["F" .. i] = "F" .. i end
for c in ("ABCDEFGHIJKLMNOPQRSTUVWXYZ"):gmatch(".") do names[c] = c end
for rbx_name, imgui_name in pairs(names) do
KEY_MAP[Enum.KeyCode[rbx_name]] = ImGuiKey[imgui_name]
end
end
-- Roblox has no "text input" event, so characters come from KeyCode.Value (ASCII for printable keys) + a US layout shift table.
local SHIFTED = {
["1"] = "!", ["2"] = "@", ["3"] = "#", ["4"] = "$", ["5"] = "%", ["6"] = "^", ["7"] = "&", ["8"] = "*", ["9"] = "(", ["0"] = ")",
["-"] = "_", ["="] = "+", ["["] = "{", ["]"] = "}", ["\\"] = "|", [";"] = ":", ["'"] = "\"", [","] = "<", ["."] = ">", ["/"] = "?", ["`"] = "~",
}
local MOUSE_BUTTONS = {
[Enum.UserInputType.MouseButton1] = 0, -- ImGuiMouseButton.Left
[Enum.UserInputType.MouseButton2] = 1, -- ImGuiMouseButton.Right
[Enum.UserInputType.MouseButton3] = 2, -- ImGuiMouseButton.Middle
}
--- @return boolean ctrl, boolean shift, boolean alt
local function ImGui_ImplRoblox_UpdateKeyModifiers()
local io = ImGui.GetIO()
local K = Enum.KeyCode
local ctrl = UserInputService:IsKeyDown(K.LeftControl) or UserInputService:IsKeyDown(K.RightControl)
local shift = UserInputService:IsKeyDown(K.LeftShift) or UserInputService:IsKeyDown(K.RightShift)
local alt = UserInputService:IsKeyDown(K.LeftAlt) or UserInputService:IsKeyDown(K.RightAlt)
io:AddKeyEvent(ImGuiMod_Ctrl, ctrl)
io:AddKeyEvent(ImGuiMod_Shift, shift)
io:AddKeyEvent(ImGuiMod_Alt, alt)
io:AddKeyEvent(ImGuiMod_Super, UserInputService:IsKeyDown(K.LeftSuper) or UserInputService:IsKeyDown(K.RightSuper))
return ctrl, shift, alt
end
--- @return table?
function ImGui_ImplRoblox_GetBackendData()
return ImGui.GetCurrentContext() and ImGui.GetIO().BackendPlatformUserData or nil
end
local function ImGui_ImplRoblox_UpdateMonitors(w, h)
local platform_io = ImGui.GetPlatformIO()
platform_io.Monitors:resize(0)
local imgui_monitor = ImGuiPlatformMonitor()
ImVec2_Copy(imgui_monitor.MainSize, ImVec2(w, h))
ImVec2_Copy(imgui_monitor.WorkSize, ImVec2(w, h))
platform_io.Monitors:push_back(imgui_monitor)
end
---------------------------------------------------------
-- PLATFORM DATA & CALLBACKS
---------------------------------------------------------
-- Games can't touch the OS clipboard, so copy/paste only works inside ImGui
local clipboard = ""
local function ImGui_ImplRoblox_PlatformSetClipboardText(ctx, text) clipboard = text end
local function ImGui_ImplRoblox_PlatformGetClipboardText(ctx) return clipboard end
---------------------------------------------------------
-- FILE SYSTEM (in-memory, Roblox has no file access)
---------------------------------------------------------
local VFS = {} -- path -> string of raw bytes
--- Registers a file so ImGui can load it, e.g. ImGui_ImplRoblox.AddFile("fonts/MyFont.ttf", bytes)
--- then io.Fonts:AddFontFromFileTTF("fonts/MyFont.ttf", 16)
local function ImGui_ImplRoblox_AddFile(path, bytes) VFS[path] = bytes end
for path, hex in pairs(IM_INCLUDE("imgui_impl_roblox_fonts.lua")) do
VFS[path] = hex:gsub("%s", ""):gsub("%x%x", function(h) return string.char(tonumber(h, 16)) end)
end
function ImStd.ImFileOpen(filename, mode)
local data = VFS[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
---------------------------------------------------------
-- TEXTURE MANAGEMENT
---------------------------------------------------------
-- Copies a region of ImTextureData pixels (1-based byte table) into our RGBA8 buffer
local function CopyTexturePixels(tex, dst, x, y, w, h)
local bpp, tex_w = tex.BytesPerPixel, tex.Width
for row = 0, h - 1 do
local src, src_base = tex:GetPixelsAt(x, y + row)
local dst_off = ((y + row) * tex_w + x) * 4
if bpp == 4 then
for i = 0, w - 1 do
local s = src_base + i * 4
writeu32(dst, dst_off + i * 4, src[s] + src[s + 1] * 256 + src[s + 2] * 65536 + src[s + 3] * 16777216)
end
else -- Alpha8: white with alpha
for i = 0, w - 1 do
writeu32(dst, dst_off + i * 4, 16777215 + src[src_base + i] * 16777216)
end
end
end
end
function ImGui_ImplRoblox_UpdateTexture(tex)
local bd = ImGui_ImplRoblox_GetBackendData()
if tex.Status == ImTextureStatus.WantCreate then
local t = { Buffer = buffer.create(tex.Width * tex.Height * 4), Width = tex.Width, Height = tex.Height }
CopyTexturePixels(tex, t.Buffer, 0, 0, tex.Width, tex.Height)
bd.TextureCount = bd.TextureCount + 1
bd.Textures[bd.TextureCount] = t
tex.BackendUserData = bd.TextureCount
tex:SetTexID(bd.TextureCount)
tex:SetStatus(ImTextureStatus.OK)
elseif tex.Status == ImTextureStatus.WantUpdates then
local t = bd.Textures[tex.BackendUserData]
IM_ASSERT(t ~= nil)
for _, r in tex.Updates:iter() do
CopyTexturePixels(tex, t.Buffer, r.x, r.y, r.w, r.h)
end
tex:SetStatus(ImTextureStatus.OK)
elseif tex.Status == ImTextureStatus.WantDestroy then
if tex.BackendUserData then
bd.Textures[tex.BackendUserData] = nil
tex.BackendUserData = nil
end
tex:SetTexID(ImTextureID_Invalid)
tex:SetStatus(ImTextureStatus.Destroyed)
end
end
---------------------------------------------------------
-- RENDERING (software rasterizer into the canvas buffer)
---------------------------------------------------------
-- Buffer layout: 4 bytes per pixel, R G B A, row-major, 0-based offsets. ImU32 colours are already R | G<<8 | B<<16 | A<<24.
-- Blending is non-premultiplied "over" with a transparent (alpha 0) background, so the game stays visible behind ImGui.
local buf, buf_w -- current canvas buffer/width, set at the start of RenderDrawData
-- Blends one pixel (r, g, b, a are 0..255)
local function BlendPixel(o, r, g, b, a)
if a >= 255 then
writeu32(buf, o, floor(r + 0.5) + floor(g + 0.5) * 256 + floor(b + 0.5) * 65536 + 4278190080)
return
end
if a <= 0 then return end
local da = readu8(buf, o + 3)
if da == 0 then
writeu32(buf, o, floor(r + 0.5) + floor(g + 0.5) * 256 + floor(b + 0.5) * 65536 + floor(a + 0.5) * 16777216)
elseif da == 255 then
local sa = a / 255
local dr, dg, db = readu8(buf, o), readu8(buf, o + 1), readu8(buf, o + 2)
writeu8(buf, o, dr + (r - dr) * sa + 0.5)
writeu8(buf, o + 1, dg + (g - dg) * sa + 0.5)
writeu8(buf, o + 2, db + (b - db) * sa + 0.5)
else
local sa = a / 255
local k = (da / 255) * (1 - sa)
local oa = sa + k
writeu8(buf, o, (r * sa + readu8(buf, o) * k) / oa + 0.5)
writeu8(buf, o + 1, (g * sa + readu8(buf, o + 1) * k) / oa + 0.5)
writeu8(buf, o + 2, (b * sa + readu8(buf, o + 2) * k) / oa + 0.5)
writeu8(buf, o + 3, oa * 255 + 0.5)
end
end
-- Fills pixels [x0, x1) of row y with one colour
local function FillSpan(y, x0, x1, r, g, b, a)
if a <= 0 then return end
local o = (y * buf_w + x0) * 4
if a >= 255 then
local packed = floor(r + 0.5) + floor(g + 0.5) * 256 + floor(b + 0.5) * 65536 + 4278190080
for x = 0, x1 - x0 - 1 do writeu32(buf, o + x * 4, packed) end
else
for x = 0, x1 - x0 - 1 do BlendPixel(o + x * 4, r, g, b, a) end
end
end
local function UnpackColor(col)
return col % 256, floor(col / 256) % 256, floor(col / 65536) % 256, floor(col / 16777216) % 256
end
-- Nearest texel at (u, v) as r, g, b, a (0..255)
local function SampleTexture(t, u, v)
local tw, th = t.Width, t.Height
local tx, ty = floor(u * tw), floor(v * th)
if tx < 0 then tx = 0 elseif tx >= tw then tx = tw - 1 end
if ty < 0 then ty = 0 elseif ty >= th then ty = th - 1 end
local o = (ty * tw + tx) * 4
local tb = t.Buffer
return readu8(tb, o), readu8(tb, o + 1), readu8(tb, o + 2), readu8(tb, o + 3)
end
-- Axis aligned, single colour quad (rects, glyphs, images). Pixel centers inside [x0, x1) x [y0, y1) are drawn.
local function DrawRect(x0, y0, x1, y1, u0, v0, u1, v1, col, t, cx0, cy0, cx1, cy1)
local r, g, b, a = UnpackColor(col)
if a == 0 then return end
local px0, py0 = max(ceil(x0 - 0.5), cx0), max(ceil(y0 - 0.5), cy0)
local px1, py1 = min(ceil(x1 - 0.5), cx1), min(ceil(y1 - 0.5), cy1)
if px0 >= px1 or py0 >= py1 then return end
if not t or (u0 == u1 and v0 == v1) then
if t then
local tr, tg, tb, ta = SampleTexture(t, u0, v0)
r, g, b, a = r * tr / 255, g * tg / 255, b * tb / 255, a * ta / 255
end
for y = py0, py1 - 1 do FillSpan(y, px0, px1, r, g, b, a) end
return
end
local du, dv = (u1 - u0) / (x1 - x0), (v1 - v0) / (y1 - y0)
local tw, th, tbuf = t.Width, t.Height, t.Buffer
local r255, g255, b255, a255 = r / 255, g / 255, b / 255, a / 255
for y = py0, py1 - 1 do
local ty = floor((v0 + (y + 0.5 - y0) * dv) * th)
if ty < 0 then ty = 0 elseif ty >= th then ty = th - 1 end
local trow = ty * tw
local o = (y * buf_w + px0) * 4
for x = px0, px1 - 1 do
local tx = floor((u0 + (x + 0.5 - x0) * du) * tw)
if tx < 0 then tx = 0 elseif tx >= tw then tx = tw - 1 end
local to = (trow + tx) * 4
local ta = readu8(tbuf, to + 3)
if ta > 0 then
BlendPixel(o, readu8(tbuf, to) * r255, readu8(tbuf, to + 1) * g255, readu8(tbuf, to + 2) * b255, ta * a255)
end
o = o + 4
end
end
end
-- Generic triangle: per-vertex colour + uv, top-left fill rule, clipped to [cx0, cx1) x [cy0, cy1)
local function DrawTriangle(va, vb, vc, t, cx0, cy0, cx1, cy1, ox, oy)
local x0, y0 = va[1][1] - ox, va[1][2] - oy
local x1, y1 = vb[1][1] - ox, vb[1][2] - oy
local x2, y2 = vc[1][1] - ox, vc[1][2] - oy
local area = (x1 - x0) * (y2 - y0) - (y1 - y0) * (x2 - x0)
if area == 0 then return end
if area < 0 then -- make the winding consistent so "inside" means all edge functions >= 0
vb, vc = vc, vb
x1, y1, x2, y2 = x2, y2, x1, y1
area = -area
end
local minx, maxx = max(floor(min(x0, x1, x2)), cx0), min(ceil(max(x0, x1, x2)), cx1 - 1)
local miny, maxy = max(floor(min(y0, y1, y2)), cy0), min(ceil(max(y0, y1, y2)), cy1 - 1)
if minx > maxx or miny > maxy then return end
-- Edge functions E(p) = A*px + B*py + C, w0 is opposite va, w1 opposite vb, w2 opposite vc
local A0, B0 = y1 - y2, x2 - x1; local C0 = -(A0 * x1 + B0 * y1)
local A1, B1 = y2 - y0, x0 - x2; local C1 = -(A1 * x2 + B1 * y2)
local A2, B2 = y0 - y1, x1 - x0; local C2 = -(A2 * x0 + B2 * y0)
local tl0 = A0 > 0 or (A0 == 0 and B0 > 0)
local tl1 = A1 > 0 or (A1 == 0 and B1 > 0)
local tl2 = A2 > 0 or (A2 == 0 and B2 > 0)
local inv = 1 / area
local ra, ga, ba, aa = UnpackColor(va[3])
local rb, gb, bb, ab = UnpackColor(vb[3])
local rc, gc, bc, ac = UnpackColor(vc[3])
local ua, vva, ub, vvb, uc, vvc = va[2][1], va[2][2], vb[2][1], vb[2][2], vc[2][1], vc[2][2]
local textured = t and not (ua == ub and ua == uc and vva == vvb and vva == vvc)
if t and not textured then -- constant uv (e.g. the atlas white pixel): fold the texel into the vertex colours
local tr, tg, tb, ta = SampleTexture(t, ua, vva)
ra, ga, ba, aa = ra * tr / 255, ga * tg / 255, ba * tb / 255, aa * ta / 255
rb, gb, bb, ab = rb * tr / 255, gb * tg / 255, bb * tb / 255, ab * ta / 255
rc, gc, bc, ac = rc * tr / 255, gc * tg / 255, bc * tb / 255, ac * ta / 255
end
local flat = not textured and ra == rb and ra == rc and ga == gb and ga == gc and ba == bb and ba == bc and aa == ab and aa == ac
if flat and aa <= 0 then return end
for y = miny, maxy do
local py = y + 0.5
-- Narrow the x range of this row using the edges (expanded by one pixel, exact test below)
local xl, xr = minx, maxx
local e0, e1, e2 = B0 * py + C0, B1 * py + C1, B2 * py + C2
if A0 ~= 0 then local xe = -e0 / A0 - 0.5; if A0 > 0 then xl = max(xl, floor(xe)) else xr = min(xr, ceil(xe)) end end
if A1 ~= 0 then local xe = -e1 / A1 - 0.5; if A1 > 0 then xl = max(xl, floor(xe)) else xr = min(xr, ceil(xe)) end end
if A2 ~= 0 then local xe = -e2 / A2 - 0.5; if A2 > 0 then xl = max(xl, floor(xe)) else xr = min(xr, ceil(xe)) end end
local row = y * buf_w
for x = xl, xr do
local px = x + 0.5
local w0, w1, w2 = A0 * px + e0, A1 * px + e1, A2 * px + e2
if (w0 > 0 or (w0 == 0 and tl0)) and (w1 > 0 or (w1 == 0 and tl1)) and (w2 > 0 or (w2 == 0 and tl2)) then
local o = (row + x) * 4
if flat then
BlendPixel(o, ra, ga, ba, aa)
else
w0, w1, w2 = w0 * inv, w1 * inv, w2 * inv
local r = w0 * ra + w1 * rb + w2 * rc
local g = w0 * ga + w1 * gb + w2 * gc
local b = w0 * ba + w1 * bb + w2 * bc
local a = w0 * aa + w1 * ab + w2 * ac
if textured then
local tr, tg, tb, ta = SampleTexture(t, w0 * ua + w1 * ub + w2 * uc, w0 * vva + w1 * vvb + w2 * vvc)
r, g, b, a = r * tr / 255, g * tg / 255, b * tb / 255, a * ta / 255
end
BlendPixel(o, r, g, b, a)
end
end
end
end
end
-- Quads emitted by PrimRect/PrimRectUV/RenderText use indices (a, b, c, a, c, d) with a=TL, b=TR, c=BR, d=BL.
-- When such a quad is axis aligned with one colour it is drawn with the much cheaper DrawRect.
local function TryDrawRect(va, vb, vc, vd, t, cx0, cy0, cx1, cy1, ox, oy)
local pa, pb, pc, pd = va[1], vb[1], vc[1], vd[1]
if not (pa[2] == pb[2] and pb[1] == pc[1] and pc[2] == pd[2] and pd[1] == pa[1] and pa[1] < pb[1] and pa[2] < pd[2]) then return false end
local col = va[3]
if vb[3] ~= col or vc[3] ~= col or vd[3] ~= col then return false end
local ta, tb, tc, td = va[2], vb[2], vc[2], vd[2]
if not (tb[1] == tc[1] and tb[2] == ta[2] and td[1] == ta[1] and td[2] == tc[2]) then return false end
DrawRect(pa[1] - ox, pa[2] - oy, pc[1] - ox, pc[2] - oy, ta[1], ta[2], tc[1], tc[2], col, t, cx0, cy0, cx1, cy1)
return true
end
function ImGui_ImplRoblox_RenderDrawData(draw_data)
local bd = ImGui_ImplRoblox_GetBackendData()
local canvas = bd.Canvas
if draw_data.Textures ~= nil then
for _, tex in draw_data.Textures:iter() do
if tex.Status ~= ImTextureStatus.OK then
ImGui_ImplRoblox_UpdateTexture(tex)
end
end
end
canvas:Clear()
buf, buf_w = canvas.Buffer, canvas.CurrentResX
local buf_h = canvas.CurrentResY
local ox, oy = draw_data.DisplayPos.x, draw_data.DisplayPos.y
if draw_data.DisplaySize.x > 0.0 and draw_data.DisplaySize.y > 0.0 then
for _, draw_list in draw_data.CmdLists:iter() do
local vtx, idx = draw_list.VtxBuffer.Data, draw_list.IdxBuffer.Data
for _, pcmd in draw_list.CmdBuffer:iter() do
if pcmd.UserCallback ~= nil then
pcmd.UserCallback(draw_list, pcmd)
elseif pcmd.ElemCount > 0 then
local clip = pcmd.ClipRect
local cx0, cy0 = max(floor(clip.x - ox), 0), max(floor(clip.y - oy), 0)
local cx1, cy1 = min(floor(clip.z - ox), buf_w), min(floor(clip.w - oy), buf_h)
if cx1 > cx0 and cy1 > cy0 then
local t = bd.Textures[pcmd:GetTexID()]
local vo = pcmd.VtxOffset
local i, last = pcmd.IdxOffset + 1, pcmd.IdxOffset + pcmd.ElemCount
while i <= last do
local ia, ic = idx[i], idx[i + 2]
local va, vb, vc = vtx[vo + ia], vtx[vo + idx[i + 1]], vtx[vo + ic]
if i + 5 <= last and idx[i + 3] == ia and idx[i + 4] == ic
and TryDrawRect(va, vb, vc, vtx[vo + idx[i + 5]], t, cx0, cy0, cx1, cy1, ox, oy) then
i = i + 6
else
DrawTriangle(va, vb, vc, t, cx0, cy0, cx1, cy1, ox, oy)
i = i + 3
end
end
end
end
end
end
end
canvas:Render()
end
---------------------------------------------------------
-- CORE LIFECYCLE
---------------------------------------------------------
-- Canvas resolution for a given screen size (capped by MAX_CANVAS_SIZE, aspect ratio kept)
local function GetCanvasSize(screen)
local scale = max(1, screen.X / MAX_CANVAS_SIZE, screen.Y / MAX_CANVAS_SIZE)
return max(1, floor(screen.X / scale)), max(1, floor(screen.Y / scale))
end
--- @param CanvasDraw table # require(path.to.CanvasDraw)
--- @param parent Instance? # where the ScreenGui goes, defaults to the local player's PlayerGui
function ImGui_ImplRoblox_Init(CanvasDraw, parent)
assert(CanvasDraw, "ImGui_ImplRoblox.Init: pass the CanvasDraw module, e.g. Init(require(ReplicatedStorage.CanvasDraw))")
local io = ImGui.GetIO()
local gui = Instance.new("ScreenGui")
gui.Name = "ImGui"
gui.IgnoreGuiInset = true -- so GetMouseLocation() and the canvas share the same origin
gui.ResetOnSpawn = false
gui.DisplayOrder = 1000
gui.Parent = parent or Players.LocalPlayer:WaitForChild("PlayerGui")
local frame = Instance.new("Frame")
frame.Size = UDim2.fromScale(1, 1)
frame.BackgroundTransparency = 1
frame.Parent = gui
local screen = gui.AbsoluteSize
if screen.X < 1 or screen.Y < 1 then screen = workspace.CurrentCamera.ViewportSize end
local w, h = GetCanvasSize(screen)
local canvas = CanvasDraw.new(frame, Vector2.new(w, h), Color3.new(0, 0, 0))
canvas.AutoRender = false
canvas:SetStretchToFit(true)
canvas:SetClearRGBA(0, 0, 0, 0)
canvas:Clear()
local bd = {
Time = os.clock(), Gui = gui, Canvas = canvas, Connections = {},
Textures = {}, TextureCount = 0, MouseX = -1, MouseY = -1,
}
io.BackendPlatformUserData = bd
io.BackendFlags = bit32.bor(io.BackendFlags, ImGuiBackendFlags.RendererHasTextures, ImGuiBackendFlags.RendererHasVtxOffset)
local platform_io = ImGui.GetPlatformIO()
platform_io.Platform_SetClipboardTextFn = ImGui_ImplRoblox_PlatformSetClipboardText
platform_io.Platform_GetClipboardTextFn = ImGui_ImplRoblox_PlatformGetClipboardText
ImGui_ImplRoblox_UpdateMonitors(w, h)
local function connect(signal, fn) table.insert(bd.Connections, signal:Connect(fn)) end
connect(UserInputService.InputBegan, function(input, game_processed)
local io = ImGui.GetIO()
local button = MOUSE_BUTTONS[input.UserInputType]
if button then
io:AddMouseSourceEvent(ImGuiMouseSource.Mouse)
io:AddMouseButtonEvent(button, true)
elseif input.UserInputType == Enum.UserInputType.Keyboard and not game_processed then
local ctrl, shift, alt = ImGui_ImplRoblox_UpdateKeyModifiers()
local key = KEY_MAP[input.KeyCode]
if key then io:AddKeyEvent(key, true) end
-- Text: printable ASCII keys, skipped while Ctrl/Alt are held (shortcuts)
local v = input.KeyCode.Value
if v >= 32 and v < 127 and not (ctrl or alt) then
local c = string.char(v)
if shift then c = SHIFTED[c] or c:upper() end
io:AddInputCharacter(string.byte(c))
end
end
end)
connect(UserInputService.InputEnded, function(input, game_processed)
local io = ImGui.GetIO()
local button = MOUSE_BUTTONS[input.UserInputType]
if button then
io:AddMouseButtonEvent(button, false)
elseif input.UserInputType == Enum.UserInputType.Keyboard then
ImGui_ImplRoblox_UpdateKeyModifiers()
local key = KEY_MAP[input.KeyCode]
if key then io:AddKeyEvent(key, false) end
end
end)
connect(UserInputService.InputChanged, function(input, game_processed)
if input.UserInputType == Enum.UserInputType.MouseWheel then
ImGui.GetIO():AddMouseWheelEvent(0, input.Position.Z)
end
end)
connect(UserInputService.WindowFocused, function() ImGui.GetIO():AddFocusEvent(true) end)
connect(UserInputService.WindowFocusReleased, function() ImGui.GetIO():AddFocusEvent(false) end)
end
function ImGui_ImplRoblox_Shutdown()
local io = ImGui.GetIO()
local bd = ImGui_ImplRoblox_GetBackendData()
for _, c in ipairs(bd.Connections) do c:Disconnect() end
bd.Canvas:Destroy()
bd.Gui:Destroy()
io.BackendPlatformUserData = nil
io.BackendFlags = bit32.band(io.BackendFlags, bit32.bnot(bit32.bor(
ImGuiBackendFlags.RendererHasTextures,
ImGuiBackendFlags.RendererHasVtxOffset
)))
end
function ImGui_ImplRoblox_NewFrame()
local io = ImGui.GetIO()
local bd = ImGui_ImplRoblox_GetBackendData()
local canvas = bd.Canvas
-- Follow screen size changes
local screen = bd.Gui.AbsoluteSize
if screen.X >= 1 and screen.Y >= 1 then
local w, h = GetCanvasSize(screen)
if w ~= canvas.CurrentResX or h ~= canvas.CurrentResY then
canvas:Resize(Vector2.new(w, h))
ImGui_ImplRoblox_UpdateMonitors(w, h)
end
end
local w, h = canvas.CurrentResX, canvas.CurrentResY
ImVec2_CopyV(io.DisplaySize, w, h)
local current_time = os.clock()
io.DeltaTime = (current_time > bd.Time) and (current_time - bd.Time) or (1.0 / 60.0)
bd.Time = current_time
-- Mouse position: screen pixels -> canvas pixels (the canvas is stretched over the whole screen)
local m = UserInputService:GetMouseLocation()
local mx, my = m.X * w / max(screen.X, 1), m.Y * h / max(screen.Y, 1)
if mx ~= bd.MouseX or my ~= bd.MouseY then
bd.MouseX, bd.MouseY = mx, my
io:AddMouseSourceEvent(ImGuiMouseSource.Mouse)
io:AddMousePosEvent(mx, my)
end
end
ImGui_ImplRoblox = {
Init = ImGui_ImplRoblox_Init,
Shutdown = ImGui_ImplRoblox_Shutdown,
NewFrame = ImGui_ImplRoblox_NewFrame,
RenderDrawData = ImGui_ImplRoblox_RenderDrawData,
AddFile = ImGui_ImplRoblox_AddFile,
}
return true -- [Roblox] ModuleScripts must return exactly one value