ImGui
BeamNG ImGui Patterns & Examples
local im = ui_imgui
Basic Window
local im = ui_imgui
local M = {}
local windowOpen = im.BoolPtr(true)
local function onUpdate(dtReal, dtSim, dtRaw)
if not windowOpen[0] then return end
if im.Begin("My Tool", windowOpen, im.WindowFlags_AlwaysAutoResize) then
im.Text("Hello!")
end
im.End() -- Always call End, even if Begin returns false
end
M.onUpdate = onUpdate
return MCentered Modal Dialog
local function showDialog()
local viewport = im.GetMainViewport()
local center = im.ImVec2(
viewport.WorkPos.x + viewport.WorkSize.x * 0.5,
viewport.WorkPos.y + viewport.WorkSize.y * 0.5
)
im.SetNextWindowPos(center, im.Cond_Appearing, im.ImVec2(0.5, 0.5))
local flags = im.WindowFlags_AlwaysAutoResize + im.WindowFlags_NoCollapse + im.WindowFlags_NoResize
local isOpen = im.BoolPtr(true)
if im.Begin("Confirm Action", isOpen, flags) then
im.Text("Are you sure?")
im.Separator()
if im.Button("Yes", im.ImVec2(100, 0)) then
-- handle confirm
end
im.SameLine()
if im.Button("No", im.ImVec2(100, 0)) then
-- handle cancel
end
end
im.End()
return isOpen[0] -- false if user closed via X
endStyled Dark Window
local function drawStyledWindow()
im.PushStyleColor2(im.Col_WindowBg, im.ImVec4(0.13, 0.13, 0.13, 0.95))
im.PushStyleColor2(im.Col_Button, im.ImVec4(0.13, 0.13, 0.13, 0.9))
im.PushStyleColor2(im.Col_ButtonHovered, im.ImVec4(0.95, 0.43, 0.49, 1))
im.PushStyleColor2(im.Col_ButtonActive, im.ImVec4(0.95, 0.43, 0.49, 1))
im.PushStyleColor2(im.Col_Text, im.ImVec4(1, 1, 1, 1))
im.PushStyleColor2(im.Col_FrameBg, im.ImVec4(0.08, 0.08, 0.08, 0.9))
im.PushStyleVar2(im.StyleVar_WindowPadding, im.ImVec2(15, 15))
im.PushStyleVar2(im.StyleVar_ItemSpacing, im.ImVec2(8, 8))
im.PushStyleVar1(im.StyleVar_WindowBorderSize, 0)
if im.Begin("Styled Window") then
im.Text("Dark themed window")
end
im.End()
im.PopStyleVar(3)
im.PopStyleColor(6)
endForm with Input Fields
local ffi = require("ffi")
local nameBuffer = im.ArrayChar(128, "")
local ageValue = im.IntPtr(25)
local speedValue = im.FloatPtr(1.0)
local enabledFlag = im.BoolPtr(true)
local function drawForm()
if im.Begin("Settings") then
-- Text input
im.Text("Name:")
im.SetNextItemWidth(200)
if im.InputText("##name", nameBuffer, 128) then
local name = ffi.string(nameBuffer)
end
-- Integer input with step buttons
im.Text("Age:")
im.SetNextItemWidth(200)
im.InputInt("##age", ageValue, 1, 10)
-- Float slider
im.Text("Speed:")
im.SetNextItemWidth(200)
im.SliderFloat("##speed", speedValue, 0.0, 10.0, "%.1f")
-- Checkbox
im.Checkbox("Enabled", enabledFlag)
-- Submit button
im.Spacing()
if im.Button("Apply", im.ImVec2(200, 30)) then
print("Name: " .. ffi.string(nameBuffer))
print("Age: " .. ageValue[0])
print("Speed: " .. speedValue[0])
print("Enabled: " .. tostring(enabledFlag[0]))
end
end
im.End()
endDropdown / Combo Box
local selectedIndex = im.IntPtr(0)
local options = {"Option A", "Option B", "Option C"}
-- Simple combo
if im.BeginCombo("Select", options[selectedIndex[0] + 1] or "Choose...") then
for i, option in ipairs(options) do
local isSelected = (i - 1 == selectedIndex[0])
if im.Selectable1(option, isSelected) then
selectedIndex[0] = i - 1
end
if isSelected then
im.SetItemDefaultFocus()
end
end
im.EndCombo()
endSearchable Filtered List
local filterText = ""
local function drawFilteredList(items)
local filterBuf = im.ArrayChar(128, filterText)
if im.InputText("Filter", filterBuf, 128) then
filterText = ffi.string(filterBuf)
end
local lowerFilter = filterText:lower()
im.BeginChild1("ItemList", im.ImVec2(0, 200), true)
for _, item in ipairs(items) do
if lowerFilter == "" or item:lower():find(lowerFilter, 1, true) then
if im.Selectable1(item, false) then
print("Selected: " .. item)
end
end
end
im.EndChild()
endTables
local function drawTable(data)
local flags = im.TableFlags_Borders + im.TableFlags_RowBg + im.TableFlags_Resizable
if im.BeginTable("MyTable", 3, flags) then
-- Setup columns
im.TableSetupColumn("Name")
im.TableSetupColumn("Value")
im.TableSetupColumn("Status")
im.TableHeadersRow()
for _, row in ipairs(data) do
im.TableNextRow()
im.TableSetColumnIndex(0)
im.Text(row.name)
im.TableSetColumnIndex(1)
im.Text(tostring(row.value))
im.TableSetColumnIndex(2)
if row.active then
im.TextColored(im.ImVec4(0.2, 0.8, 0.2, 1), "Active")
else
im.TextColored(im.ImVec4(0.8, 0.2, 0.2, 1), "Inactive")
end
end
im.EndTable()
end
endCollapsible Sections
if im.CollapsingHeader1("Basic Settings") then
im.Text("Content here")
im.InputFloat("Value", myFloat, 0.1, 1.0, "%.2f")
end
-- Default open
if im.CollapsingHeader1("Advanced##adv") then
im.Text("Advanced content")
endTab Bar
if im.BeginTabBar("MyTabs") then
if im.BeginTabItem("General") then
im.Text("General settings")
im.EndTabItem()
end
if im.BeginTabItem("Advanced") then
im.Text("Advanced settings")
im.EndTabItem()
end
im.EndTabBar()
endMenu Bar
local flags = im.WindowFlags_MenuBar
if im.Begin("My Window", nil, flags) then
if im.BeginMenuBar() then
if im.BeginMenu("File") then
if im.MenuItem1("New") then print("New") end
if im.MenuItem1("Open") then print("Open") end
im.Separator()
if im.MenuItem1("Save") then print("Save") end
im.EndMenu()
end
if im.BeginMenu("Edit") then
if im.MenuItem1("Undo") then print("Undo") end
if im.MenuItem1("Redo") then print("Redo") end
im.EndMenu()
end
im.EndMenuBar()
end
im.Text("Window content")
end
im.End()Popup (Context Menu)
-- Right-click context menu
if im.Button("Right-click me") then end
if im.BeginPopupContextItem("item_context") then
if im.MenuItem1("Edit") then end
if im.MenuItem1("Delete") then end
im.EndPopup()
end
-- Manual popup
if im.Button("Open Popup") then
im.OpenPopup("MyPopup")
end
if im.BeginPopup("MyPopup") then
im.Text("Popup content")
if im.Button("Close") then
im.CloseCurrentPopup()
end
im.EndPopup()
endModal Popup
if im.Button("Confirm Delete") then
im.OpenPopup("Delete Confirmation")
end
if im.BeginPopupModal("Delete Confirmation", nil, im.WindowFlags_AlwaysAutoResize) then
im.Text("This cannot be undone!")
im.Separator()
if im.Button("Delete", im.ImVec2(120, 0)) then
-- perform delete
im.CloseCurrentPopup()
end
im.SameLine()
if im.Button("Cancel", im.ImVec2(120, 0)) then
im.CloseCurrentPopup()
end
im.EndPopup()
endTooltip
im.Button("Hover me")
if im.IsItemHovered() then
im.BeginTooltip()
im.Text("This is a tooltip")
im.Text("With multiple lines")
im.EndTooltip()
end
-- Or the simple version:
im.Button("Hover me too")
if im.IsItemHovered() then
im.SetTooltip("Simple tooltip")
end
-- Built-in helper:
im.ShowHelpMarker("Explains what this does", true) -- true = SameLine firstUnique IDs (Avoiding Conflicts)
-- Problem: Two buttons with same label
im.Button("Click") -- ID = "Click"
im.Button("Click") -- CONFLICT: same ID
-- Solution 1: Hidden ID suffix with ##
im.Button("Click##first")
im.Button("Click##second")
-- Solution 2: PushID/PopID for loops
for i, item in ipairs(items) do
im.PushID1("item_" .. tostring(i))
im.Text(item.name)
im.SameLine()
if im.Button("Delete") then -- Each gets unique ID from PushID
-- delete item
end
im.PopID()
endSplit Panel Layout
local function drawSplitLayout()
local windowWidth = im.GetContentRegionAvail().x
local leftWidth = windowWidth * 0.3
-- Left panel
im.BeginChild1("LeftPanel", im.ImVec2(leftWidth, 0), true)
im.Text("Navigation")
-- ... list items ...
im.EndChild()
im.SameLine()
-- Right panel (fills remaining space)
im.BeginChild1("RightPanel", im.ImVec2(0, 0), true)
im.Text("Details")
-- ... detail content ...
im.EndChild()
endCustom Drawing
local function drawCustom()
local drawList = im.GetWindowDrawList()
local pos = im.GetCursorScreenPos()
-- Rectangle
im.ImDrawList_AddRectFilled(drawList,
im.ImVec2(pos.x, pos.y),
im.ImVec2(pos.x + 100, pos.y + 50),
im.GetColorU322(im.ImVec4(0.2, 0.5, 1, 1)),
5, -- rounding
im.ImDrawFlags_RoundCornersAll
)
-- Line
im.ImDrawList_AddLine(drawList,
im.ImVec2(pos.x, pos.y),
im.ImVec2(pos.x + 200, pos.y + 100),
im.GetColorU322(im.ImVec4(1, 0, 0, 1)),
2 -- thickness
)
-- Circle
im.ImDrawList_AddCircleFilled(drawList,
im.ImVec2(pos.x + 50, pos.y + 50),
25, -- radius
im.GetColorU322(im.ImVec4(0, 1, 0, 1))
)
-- Text
im.ImDrawList_AddText1(drawList,
im.ImVec2(pos.x + 10, pos.y + 10),
im.GetColorU322(im.ImVec4(1, 1, 1, 1)),
"Custom text"
)
-- Reserve space so ImGui knows we drew something
im.Dummy(im.ImVec2(200, 100))
endImage / Texture
local imguiUtils = require('/common/extensions/ui/imguiUtils')
local tex = imguiUtils.texObj("/art/my_image.png")
-- Display image
im.Image(tex.texId, im.ImVec2(128, 128))
-- Image button
if im.ImageButton("myimg", tex.texId, im.ImVec2(64, 64)) then
print("Image clicked!")
endDisabled Widgets
local canEdit = someCondition()
im.BeginDisabled(not canEdit)
if im.Button("Action") then end
im.InputFloat("Value", myFloat)
im.EndDisabled()Plot Lines (Graphs)
-- PlotLines1 expects data from im.TableToArrayFloat(), NOT raw Lua tables or ffi cdata
local dataTable = {}
for i = 1, 100 do
dataTable[i] = math.sin(i * 0.1) * 50 + 50
end
local plotData = im.TableToArrayFloat(dataTable)
im.PlotLines1("##MyPlot", plotData, im.GetLengthArrayFloat(plotData),
0, -- values_offset
"Overlay Text", -- overlay_text (optional, can be nil or "")
0, -- scale_min
100, -- scale_max
im.ImVec2(im.GetContentRegionAvail().x, 100) -- graph_size
)
-- ⚠️ COMMON MISTAKE: Do NOT pass im.ArrayFloat() or ffi.new("float[?]") directly.
-- BeamNG's PlotLines1 expects the result of im.TableToArrayFloat().
-- Use im.GetLengthArrayFloat() for the count, NOT #table or a manual count.Plot Histogram
local histData = im.TableToArrayFloat({10, 25, 40, 30, 15})
im.PlotHistogram1("##Hist", histData, im.GetLengthArrayFloat(histData),
0, "Distribution", 0, 50, im.ImVec2(200, 80))Progress Bar
local progress = 0.65
im.ProgressBar(progress, im.ImVec2(-1, 0), string.format("%.0f%%", progress * 100))Radio Buttons
local selected = im.IntPtr(0)
im.RadioButton2("Option A", selected, im.Int(0))
im.SameLine()
im.RadioButton2("Option B", selected, im.Int(1))
im.SameLine()
im.RadioButton2("Option C", selected, im.Int(2))
-- selected[0] is 0, 1, or 2Color Picker
local color = im.ArrayFloat(4)
color[0] = 1.0 -- R
color[1] = 0.5 -- G
color[2] = 0.0 -- B
color[3] = 1.0 -- A
im.ColorEdit4("Color", color)
-- or without alpha:
im.ColorEdit3("Color##noalpha", color)Drag and Drop
-- Source
if im.BeginDragDropSource() then
im.SetDragDropPayload("MY_TYPE", "payload_data", #"payload_data")
im.Text("Dragging...")
im.EndDragDropSource()
end
-- Target
if im.BeginDragDropTarget() then
local payload = im.AcceptDragDropPayload("MY_TYPE")
if payload then
-- Handle drop
end
im.EndDragDropTarget()
endKnobs (BeamNG Custom)
local knobValue = im.FloatPtr(0.5)
im.Knob("Volume", knobValue, 0, 1, 0.01, "%.2f",
im.KnobVariant_WiperDot, 60,
im.KnobFlags_NoTitle + im.KnobFlags_ValueTooltip)