337 lines
16 KiB
Luau
Executable File
337 lines
16 KiB
Luau
Executable File
-- LocalScript. Build the library first: `python3 tools/bundle.py` -> build/ImGui.luau (see default.project.json)
|
|
|
|
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
|
local RunService = game:GetService("RunService")
|
|
|
|
local G = require(ReplicatedStorage:WaitForChild("ImGui"))
|
|
local ExampleImageViewerData, ExampleImageViewer_DrawOptions, ExampleImageViewer_DrawCanvas = G.ExampleImageViewerData, G.ExampleImageViewer_DrawOptions, G.ExampleImageViewer_DrawCanvas
|
|
|
|
local ImGui, ImVec4, ImGuiConfigFlags, ImGui_ImplRoblox = G.ImGui, G.ImVec4, G.ImGuiConfigFlags, G.ImGui_ImplRoblox
|
|
|
|
local main_scale = 1.0
|
|
-- Roblox UI points are multiplied by the OS display scale (e.g. 150%), which makes imgui look large.
|
|
-- Set this to your display scale to render imgui at physical-pixel resolution (same size as a desktop imgui app).
|
|
local render_scale = 1.0
|
|
local clear_color = ImVec4(0.45, 0.55, 0.60, 1.00)
|
|
local show_demo_window = true
|
|
local show_another_window = false
|
|
local f = 0.0
|
|
local counter = 0
|
|
|
|
ImGui.CreateContext()
|
|
|
|
local io = ImGui.GetIO()
|
|
io.ConfigFlags = bit32.bor(io.ConfigFlags, ImGuiConfigFlags.NavEnableKeyboard, ImGuiConfigFlags.DockingEnable)
|
|
|
|
-- Catppuccin Mocha, Mauve accent (https://catppuccin.com/palette)
|
|
local function StyleColorsCatppuccinMocha(style)
|
|
local function hex(h, a)
|
|
return ImVec4(bit32.extract(h, 16, 8) / 255, bit32.extract(h, 8, 8) / 255, bit32.extract(h, 0, 8) / 255, a or 1.0)
|
|
end
|
|
local crust, mantle, base = 0x11111b, 0x181825, 0x1e1e2e
|
|
local surface0, surface1, surface2 = 0x313244, 0x45475a, 0x585b70
|
|
local overlay0, overlay2, text = 0x6c7086, 0x9399b2, 0xcdd6f4
|
|
local mauve, lavender = 0xcba6f7, 0xb4befe
|
|
local C, c = G.ImGuiCol, style.Colors
|
|
local function set(idx, h, a) c[idx] = hex(h, a) end
|
|
set(C.Text, text); set(C.TextDisabled, overlay0)
|
|
set(C.WindowBg, base); set(C.ChildBg, base, 0.0)
|
|
set(C.PopupBg, mantle, 0.98); set(C.Border, surface1); set(C.BorderShadow, crust, 0.0)
|
|
set(C.FrameBg, surface0); set(C.FrameBgHovered, surface1); set(C.FrameBgActive, surface2)
|
|
set(C.TitleBg, mantle); set(C.TitleBgActive, crust); set(C.TitleBgCollapsed, mantle, 0.75)
|
|
set(C.MenuBarBg, mantle)
|
|
set(C.ScrollbarBg, mantle); set(C.ScrollbarGrab, surface1)
|
|
set(C.ScrollbarGrabHovered, surface2); set(C.ScrollbarGrabActive, overlay0)
|
|
set(C.CheckMark, mauve); set(C.SliderGrab, mauve, 0.8); set(C.SliderGrabActive, mauve)
|
|
set(C.Button, surface0); set(C.ButtonHovered, surface1); set(C.ButtonActive, mauve, 0.6)
|
|
set(C.Header, surface0); set(C.HeaderHovered, surface1); set(C.HeaderActive, mauve, 0.5)
|
|
set(C.Separator, surface1); set(C.SeparatorHovered, mauve, 0.7); set(C.SeparatorActive, mauve)
|
|
set(C.ResizeGrip, mauve, 0.2); set(C.ResizeGripHovered, mauve, 0.6); set(C.ResizeGripActive, mauve, 0.9)
|
|
set(C.InputTextCursor, text)
|
|
set(C.TabHovered, surface2); set(C.Tab, surface0)
|
|
set(C.TabSelected, surface1); set(C.TabSelectedOverline, mauve)
|
|
set(C.TabDimmed, mantle); set(C.TabDimmedSelected, surface0); set(C.TabDimmedSelectedOverline, overlay0)
|
|
set(C.DockingPreview, mauve, 0.7); set(C.DockingEmptyBg, crust)
|
|
set(C.PlotLines, overlay2); set(C.PlotLinesHovered, mauve)
|
|
set(C.PlotHistogram, mauve, 0.8); set(C.PlotHistogramHovered, lavender)
|
|
set(C.TableHeaderBg, surface0); set(C.TableBorderStrong, surface1); set(C.TableBorderLight, surface0)
|
|
set(C.TableRowBg, base, 0.0); set(C.TableRowBgAlt, surface0, 0.3)
|
|
set(C.TextLink, mauve); set(C.TextSelectedBg, mauve, 0.35)
|
|
set(C.TreeLines, surface2)
|
|
set(C.DragDropTarget, mauve, 0.9)
|
|
set(C.NavCursor, lavender); set(C.NavWindowingHighlight, lavender, 0.7)
|
|
set(C.NavWindowingDimBg, crust, 0.5); set(C.ModalWindowDimBg, crust, 0.6)
|
|
end
|
|
|
|
local style = ImGui.GetStyle()
|
|
StyleColorsCatppuccinMocha(style)
|
|
style:ScaleAllSizes(main_scale)
|
|
style.FontScaleDpi = main_scale
|
|
|
|
-- Fonts: ProggyClean stays the default (added first); the rest show up in Style Editor > Fonts
|
|
io.Fonts:AddFontDefault()
|
|
for _, name in ipairs({ "Cousine-Regular", "DroidSans", "Karla-Regular", "ProggyTiny", "Roboto-Medium" }) do
|
|
io.Fonts:AddFontFromFileTTF("fonts/" .. name .. ".ttf", 13.0)
|
|
end
|
|
|
|
ImGui_ImplRoblox.Init(nil, render_scale)
|
|
ImGui_ImplRoblox.SetRenderRate(60) -- tile redraws per second; imgui logic + your code still run every frame
|
|
-- ImGui_ImplRoblox.SetBusyRenderRate(30) -- optional: lower redraw rate while scrolling/resizing/moving windows
|
|
ImGui_ImplRoblox.SetProfiling(true) -- MicroProfiler (Ctrl+F6) labels; turn off when not profiling
|
|
|
|
local function P(name) if ImGui_ImplRoblox.IsProfiling() then debug.profilebegin(name) end end
|
|
local function PE() if ImGui_ImplRoblox.IsProfiling() then debug.profileend() end end
|
|
|
|
--------------------------------------------------------------------------------------------------------------------
|
|
-- Custom widget: ImGui.Embed(id, size, flags, init) -> Frame
|
|
-- Reserves a widget-sized area in the current window and shows a real Frame there, so any Instances (UI, a
|
|
-- ViewportFrame, ...) can live inside an imgui window. Immediate-mode friendly:
|
|
-- * one Frame per id (per window, through the ID stack, like any widget); `init(frame)` runs only when it is created
|
|
-- * call it every frame the Frame should be visible; when it isn't called (window closed/collapsed, tab or tree node
|
|
-- closed, scrolled fully out of view) the Frame is hidden, never destroyed, so your Instances keep their state
|
|
-- * size uses imgui sizing (ImVec2(-1, h) = fill width, 0 = default), style gives padding/rounding/background/border
|
|
-- * returns the Frame: edit properties, add or remove children any time
|
|
-- ImGui.DestroyEmbed(id) removes it for good (call while the same window is current, like the widget itself).
|
|
--------------------------------------------------------------------------------------------------------------------
|
|
local EmbedFlags = {
|
|
None = 0,
|
|
NoClip = 1, -- don't clip to the window's content area (still moves/hides/stacks with the window)
|
|
NoBackground = 2, -- no FrameBg behind the Frame
|
|
NoBorder = 4, -- no style border
|
|
NoPadding = 8, -- no FramePadding inside the Frame (UIPadding)
|
|
NoRounding = 16, -- no FrameRounding corners (UICorner)
|
|
}
|
|
G.ImGuiEmbedFlags = EmbedFlags
|
|
|
|
local embeds = {} -- [ImGuiID] = { Frame, Padding, Corner }
|
|
|
|
function ImGui.Embed(id, size, flags, init)
|
|
flags = flags or EmbedFlags.None
|
|
local window = ImGui.GetCurrentWindow()
|
|
local gid = window:GetID(id)
|
|
local e = embeds[gid]
|
|
if not e then
|
|
local frame = Instance.new("Frame")
|
|
frame.Name = tostring(id)
|
|
frame.BackgroundTransparency = 1
|
|
frame.BorderSizePixel = 0
|
|
frame.Visible = false
|
|
e = { Frame = frame, Padding = Instance.new("UIPadding"), Corner = Instance.new("UICorner") }
|
|
e.Padding.Parent = frame
|
|
e.Corner.Parent = frame
|
|
embeds[gid] = e
|
|
if init then init(frame) end
|
|
end
|
|
if window.SkipItems then return e.Frame end
|
|
|
|
local style = ImGui.GetStyle()
|
|
local sz = ImGui.CalcItemSize(size or G.ImVec2(0, 0), ImGui.CalcItemWidth(), ImGui.GetFrameHeight() * 4)
|
|
local p = window.DC.CursorPos
|
|
local bb = G.ImRect(p.x, p.y, p.x + sz.x, p.y + sz.y)
|
|
ImGui.ItemSize(sz)
|
|
if not ImGui.ItemAdd(bb, gid) then return e.Frame end -- fully clipped: not submitted -> hidden this frame
|
|
|
|
local rounding = bit32.band(flags, EmbedFlags.NoRounding) == 0 and style.FrameRounding or 0
|
|
if bit32.band(flags, EmbedFlags.NoBackground) == 0 then
|
|
ImGui.RenderFrame(bb.Min, bb.Max, ImGui.GetColorU32(G.ImGuiCol.FrameBg), bit32.band(flags, EmbedFlags.NoBorder) == 0, rounding)
|
|
elseif bit32.band(flags, EmbedFlags.NoBorder) == 0 then
|
|
ImGui.RenderFrameBorder(bb.Min, bb.Max, rounding)
|
|
end
|
|
|
|
-- style -> Instances (only written when changed)
|
|
local scale = ImGui_ImplRoblox.GetRenderScale()
|
|
local px, py = 0, 0
|
|
if bit32.band(flags, EmbedFlags.NoPadding) == 0 then px, py = style.FramePadding.x / scale, style.FramePadding.y / scale end
|
|
if e.PX ~= px or e.PY ~= py then
|
|
e.PX, e.PY = px, py
|
|
e.Padding.PaddingLeft, e.Padding.PaddingRight = UDim.new(0, px), UDim.new(0, px)
|
|
e.Padding.PaddingTop, e.Padding.PaddingBottom = UDim.new(0, py), UDim.new(0, py)
|
|
end
|
|
if e.R ~= rounding then e.R = rounding; e.Corner.CornerRadius = UDim.new(0, rounding / scale) end
|
|
|
|
ImGui_ImplRoblox.SetOverlay(e.Frame, bb.Min.x, bb.Min.y, sz.x, sz.y, bit32.band(flags, EmbedFlags.NoClip) == 0)
|
|
return e.Frame
|
|
end
|
|
|
|
function ImGui.DestroyEmbed(id)
|
|
local gid = ImGui.GetCurrentWindow():GetID(id)
|
|
local e = embeds[gid]
|
|
if e then e.Frame:Destroy(); embeds[gid] = nil end
|
|
end
|
|
|
|
-- Example: Decal Viewer. Loads a decal/image asset's pixels into an imgui texture and shows it in the demo's
|
|
-- image viewer (zoom with the wheel, drag to pan, toggleable pixel grid).
|
|
local AssetService = game:GetService("AssetService")
|
|
local MarketplaceService = game:GetService("MarketplaceService")
|
|
local decal = { input = ImGui.NewTextBuffer("", 32), status = "Enter a decal or image id", tex = nil, w = 0, h = 0, loading = false,
|
|
viewer = ExampleImageViewerData(), show = true }
|
|
|
|
-- A Decal asset wraps an Image asset with a different id; the image is almost always uploaded just before it
|
|
-- (same creator, id slightly lower). ponytail: heuristic scan of 10 ids, InsertService:LoadAsset when owned is exact.
|
|
local function ResolveImageId(id)
|
|
local ok, info = pcall(MarketplaceService.GetProductInfo, MarketplaceService, id)
|
|
if not ok then return nil, "GetProductInfo failed: " .. tostring(info) end
|
|
if info.AssetTypeId == 1 then return id end -- already an Image
|
|
if info.AssetTypeId ~= 13 then return nil, "asset is not a decal or image" end
|
|
for cand = id - 1, id - 10, -1 do
|
|
local ok2, ci = pcall(MarketplaceService.GetProductInfo, MarketplaceService, cand)
|
|
if ok2 and ci.AssetTypeId == 1 and ci.Creator and info.Creator and ci.Creator.Id == info.Creator.Id then return cand end
|
|
end
|
|
return nil, "couldn't find the decal's image id (try the image id directly)"
|
|
end
|
|
|
|
local function LoadDecal(id)
|
|
decal.loading = true
|
|
decal.status = "Loading " .. id .. "..."
|
|
task.spawn(function()
|
|
local image_id, err = ResolveImageId(id)
|
|
decal.thumb = nil
|
|
if image_id then
|
|
-- 1) the image itself (needs permission), 2) its 420x420 thumbnail (may be allowed when 1 isn't)
|
|
local ok, img = pcall(AssetService.CreateEditableImageAsync, AssetService, Content.fromUri("rbxassetid://" .. image_id))
|
|
if not ok then
|
|
local ok2, img2 = pcall(AssetService.CreateEditableImageAsync, AssetService, Content.fromUri("rbxthumb://type=Asset&id=" .. id .. "&w=420&h=420"))
|
|
if ok2 then ok, img = true, img2 end
|
|
end
|
|
if ok then
|
|
local size = img.Size
|
|
local pixels = img:ReadPixelsBuffer(Vector2.zero, size)
|
|
img:Destroy()
|
|
if decal.tex then ImGui_ImplRoblox.DestroyTexture(decal.tex) end
|
|
decal.thumb = nil
|
|
decal.tex, decal.w, decal.h = ImGui_ImplRoblox.CreateTexture(pixels, size.X, size.Y), size.X, size.Y
|
|
decal.viewer.ViewReset = true
|
|
decal.status = string.format("Image %d: %dx%d", image_id, size.X, size.Y)
|
|
else
|
|
-- 3) no pixel access: Roblox still displays any asset, so show its thumbnail in an ImageLabel (no grid)
|
|
decal.thumb = "rbxthumb://type=Asset&id=" .. id .. "&w=420&h=420"
|
|
decal.status = "No pixel access (asset not owned by this experience's creator): display only, no zoom/grid"
|
|
end
|
|
else
|
|
decal.thumb = "rbxthumb://type=Asset&id=" .. id .. "&w=420&h=420"
|
|
decal.status = (err or "") .. " - showing thumbnail"
|
|
end
|
|
decal.loading = false
|
|
end)
|
|
end
|
|
|
|
local function ShowDecalViewer()
|
|
if not decal.show then return end
|
|
decal.show = ImGui.Begin("Decal Viewer", decal.show)
|
|
ImGui.SetNextItemWidth(ImGui.GetFontSize() * 12)
|
|
local enter = ImGui.InputTextWithHint("##id", "decal id", decal.input, 32, G.ImGuiInputTextFlags.CharsDecimal + G.ImGuiInputTextFlags.EnterReturnsTrue)
|
|
ImGui.SameLine()
|
|
ImGui.BeginDisabled(decal.loading)
|
|
if (ImGui.Button("Load") or enter) and not decal.loading then
|
|
local id = tonumber(ImGui.TextBufferToString(decal.input))
|
|
if id then LoadDecal(id) else decal.status = "Not a number" end
|
|
end
|
|
ImGui.EndDisabled()
|
|
ImGui.SameLine(); ImGui.TextDisabled(decal.status)
|
|
if decal.thumb then
|
|
local avail = ImGui.GetContentRegionAvail()
|
|
local frame = ImGui.Embed("decal_thumb", G.ImVec2(-1, math.max(avail.y, 50)), G.ImGuiEmbedFlags.None, function(f)
|
|
local img = Instance.new("ImageLabel")
|
|
img.Name = "Image"
|
|
img.Size = UDim2.fromScale(1, 1)
|
|
img.BackgroundTransparency = 1
|
|
img.ScaleType = Enum.ScaleType.Fit
|
|
img.Parent = f
|
|
end)
|
|
if frame.Image.Image ~= decal.thumb then frame.Image.Image = decal.thumb end
|
|
elseif decal.tex then
|
|
ExampleImageViewer_DrawOptions(decal.viewer)
|
|
local avail = ImGui.GetContentRegionAvail()
|
|
ExampleImageViewer_DrawCanvas(decal.viewer, G.ImVec2(math.max(avail.x, 50), math.max(avail.y, 50)), decal.tex, decal.w, decal.h)
|
|
end
|
|
ImGui.End()
|
|
end
|
|
|
|
-- Example: a ViewportFrame with a spinning part, edited live from imgui widgets
|
|
local spin_speed = 1.0
|
|
local part_color = ImVec4(0.80, 0.65, 0.97, 1.0)
|
|
local spin_angle = 0.0
|
|
local show_viewport_window = true
|
|
|
|
RunService.Stepped:Connect(function()
|
|
P("ImGui frame")
|
|
ImGui_ImplRoblox.NewFrame()
|
|
P("ImGui.NewFrame"); ImGui.NewFrame(); PE()
|
|
P("ImGui UI")
|
|
|
|
-- Dockspace covering the whole screen; the centre stays transparent so the game shows through
|
|
ImGui.DockSpaceOverViewport(0, nil, G.ImGuiDockNodeFlags.PassthruCentralNode)
|
|
|
|
if show_demo_window then
|
|
ImGui.ShowDemoWindow()
|
|
end
|
|
|
|
do
|
|
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)
|
|
|
|
f = ImGui.SliderFloat("float", f, 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)
|
|
|
|
ImGui.Text("Application average %.3f ms/frame (%.1f FPS)", 1000.0 / io.Framerate, io.Framerate)
|
|
|
|
ImGui.End()
|
|
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
|
|
|
|
PE()
|
|
ShowDecalViewer()
|
|
|
|
if show_viewport_window then
|
|
show_viewport_window = ImGui.Begin("Viewport", show_viewport_window)
|
|
spin_speed = ImGui.SliderFloat("speed", spin_speed, 0.0, 5.0)
|
|
ImGui.ColorEdit3("color", part_color)
|
|
local frame = ImGui.Embed("viewport", G.ImVec2(-1, 220), G.ImGuiEmbedFlags.None, function(f)
|
|
-- runs once
|
|
local vpf = Instance.new("ViewportFrame")
|
|
vpf.Size = UDim2.fromScale(1, 1)
|
|
vpf.BackgroundTransparency = 1
|
|
vpf.Parent = f
|
|
local cam = Instance.new("Camera")
|
|
cam.CFrame = CFrame.lookAt(Vector3.new(0, 2, 6), Vector3.new(0, 0, 0))
|
|
cam.Parent = vpf
|
|
vpf.CurrentCamera = cam
|
|
local part = Instance.new("Part")
|
|
part.Name = "Spinner"
|
|
part.Size = Vector3.new(2, 2, 2)
|
|
part.Parent = vpf
|
|
end)
|
|
-- every frame: edit what was created once
|
|
spin_angle += spin_speed * io.DeltaTime
|
|
local part = frame.ViewportFrame.Spinner
|
|
part.CFrame = CFrame.Angles(0.4, spin_angle, 0)
|
|
part.Color = Color3.new(part_color.x, part_color.y, part_color.z)
|
|
ImGui.Text("Frames created: 1 (same Frame every frame)")
|
|
ImGui.End()
|
|
end
|
|
|
|
P("ImGui.Render"); ImGui.EndFrame(); ImGui.Render(); PE()
|
|
ImGui_ImplRoblox.RenderDrawData(ImGui.GetDrawData())
|
|
PE()
|
|
end)
|