BeamNG ImGui Style Guide
Visual patterns and conventions extracted from the Freeroam Event Editor. Use these patterns for consistent, polished editor UIs.
Visual patterns and conventions extracted from the Freeroam Event Editor. Use these patterns for consistent, polished editor UIs.
Semantic Color Palette
Define colors at the top of your file for consistent use throughout:
local colors = {
success = im.ImVec4(0.2, 0.8, 0.2, 1.0), -- Green - valid, complete, enabled
warning = im.ImVec4(1.0, 0.8, 0.2, 1.0), -- Yellow/Orange - caution, pending
error = im.ImVec4(0.9, 0.2, 0.2, 1.0), -- Red - invalid, missing, danger
info = im.ImVec4(0.4, 0.7, 1.0, 1.0), -- Blue - informational, selected
dimmed = im.ImVec4(0.6, 0.6, 0.6, 1.0), -- Gray - secondary, labels
highlight = im.ImVec4(0.95, 0.43, 0.49, 1.0), -- Pink - accent, attention
}Use with im.TextColored(colors.success, "Valid!").
Section Separators
Use im.SeparatorText() for major sections:
im.SeparatorText("Basic Information")
-- inputs here...
im.SeparatorText("Event Options")
-- more inputs...This creates a horizontal line with centered text - much cleaner than plain im.Separator().
Help Markers
Add contextual help with a (?) tooltip pattern:
local function helpMarker(text, sameLine)
if sameLine then im.SameLine() end
im.TextDisabled("(?)")
if im.IsItemHovered() then
im.BeginTooltip()
im.PushTextWrapPos(im.GetFontSize() * 25)
im.TextUnformatted(text)
im.PopTextWrapPos()
im.EndTooltip()
end
end
-- Usage:
im.Checkbox("Running Start", runningStart)
helpMarker("Timer starts when player crosses start line at speed.", true)Two-Panel Layout
Standard editor layout with list on left, details on right:
local windowWidth = im.GetContentRegionAvail().x
local leftPanelWidth = windowWidth * 0.3 -- 30% for list
-- Left panel
im.BeginChild1("ItemList", im.ImVec2(leftPanelWidth, im.GetContentRegionAvail().y), true)
-- list items here
im.EndChild()
im.SameLine()
-- Right panel
im.BeginChild1("ItemDetails", im.ImVec2(0, im.GetContentRegionAvail().y), true)
-- detail form here
im.EndChild()State-Colored Buttons
Color buttons based on item state:
local exists = checkIfExists()
local isSelected = (item == currentItem)
if not exists then
-- Red for missing/incomplete
im.PushStyleColor2(im.Col_Button, im.ImVec4(0.5, 0.2, 0.2, 1.0))
im.PushStyleColor2(im.Col_ButtonHovered, im.ImVec4(0.6, 0.3, 0.3, 1.0))
im.PushStyleColor2(im.Col_ButtonActive, im.ImVec4(0.7, 0.4, 0.4, 1.0))
elseif isSelected then
-- Blue for selected
im.PushStyleColor2(im.Col_Button, im.ImVec4(0.2, 0.4, 0.6, 1.0))
im.PushStyleColor2(im.Col_ButtonHovered, im.ImVec4(0.3, 0.5, 0.7, 1.0))
im.PushStyleColor2(im.Col_ButtonActive, im.ImVec4(0.4, 0.6, 0.8, 1.0))
end
if im.Button(itemLabel .. "##" .. itemId, im.ImVec2(im.GetContentRegionAvail().x, 0)) then
currentItem = item
end
if not exists or isSelected then
im.PopStyleColor(3)
endTrigger Button Pattern
For create/select buttons that toggle state:
local function triggerButton(type, displayName, id)
local exists = triggerExists(type, id)
local isPending = (pendingType == type)
local buttonText = exists and ("Select " .. displayName) or ("Create " .. displayName)
if isPending then buttonText = "Cancel " .. displayName .. " Placement" end
if isPending then
im.PushStyleColor2(im.Col_Button, im.ImVec4(0.6, 0.4, 0.1, 1)) -- Orange for pending
elseif exists then
im.PushStyleColor2(im.Col_Button, im.ImVec4(0.1, 0.4, 0.2, 1)) -- Green for exists
else
im.PushStyleColor2(im.Col_Button, im.ImVec4(0.4, 0.1, 0.1, 1)) -- Red for missing
end
if im.Button(buttonText .. "##" .. type, im.ImVec2(im.GetContentRegionAvail().x, 0)) then
-- toggle action
end
im.PopStyleColor()
endProgress Indicators
Show completion status with progress bar:
local done, total = getCompleteness(item)
local progressColor = done == total and colors.success or colors.warning
im.TextColored(progressColor, string.format("(%d/%d)", done, total))
im.ProgressBar(done / total, im.ImVec2(-1, 0), string.format("%d/%d components", done, total))Filter Inputs
For searchable lists, use InputTextWithHint:
local filterText = ""
local filterBuf = im.ArrayChar(128, filterText)
im.SetNextItemWidth(im.GetContentRegionAvail().x)
if im.InputTextWithHint("##Filter", "Filter items...", filterBuf, 128) then
filterText = ffi.string(filterBuf)
end
-- In your list loop:
local filterLower = filterText:lower()
for _, item in pairs(items) do
if filterLower ~= "" and not string.find(item.name:lower(), filterLower) then
goto continue
end
-- render item
::continue::
endInput Validation Feedback
Show invalid state with red background:
local isValid = validateInput(value)
if not isValid then
im.PushStyleColor2(im.Col_FrameBg, im.ImVec4(0.5, 0.1, 0.1, 1))
end
if im.InputText("Field", valueBuf) then
-- handle input
end
if not isValid then
im.PopStyleColor()
im.SameLine()
im.TextColored(colors.error, "!")
if im.IsItemHovered() then
im.SetTooltip("Validation error message here")
end
endTables for Data Display
Use tables for organized key-value or columnar data:
if im.BeginTable("DataTable", 2, im.TableFlags_BordersInnerV) then
im.TableSetupColumn("Label", im.TableColumnFlags_WidthStretch)
im.TableSetupColumn("Value", im.TableColumnFlags_WidthStretch)
im.TableNextRow()
im.TableSetColumnIndex(0)
im.Text("Average Gap")
im.TableSetColumnIndex(1)
im.Text(formatValue(avgGap))
-- more rows...
im.EndTable()
endModal Confirmations
For destructive actions, use a confirmation modal:
-- Trigger
im.PushStyleColor2(im.Col_Button, im.ImVec4(0.6, 0.1, 0.1, 1))
if im.Button("Delete Item", im.ImVec2(im.GetContentRegionAvail().x, 30)) then
im.OpenPopup("Delete Confirmation")
end
im.PopStyleColor()
-- Modal
if im.BeginPopupModal("Delete Confirmation", nil, im.WindowFlags_AlwaysAutoResize) then
im.Text("Are you sure you want to delete this?")
im.TextColored(colors.warning, itemName)
im.Text("This action cannot be undone.")
im.Separator()
im.PushStyleColor2(im.Col_Button, im.ImVec4(0.6, 0.1, 0.1, 1))
if im.Button("Yes, Delete", im.ImVec2(im.GetContentRegionAvail().x * 0.48, 0)) then
deleteItem()
im.CloseCurrentPopup()
end
im.PopStyleColor()
im.SameLine()
if im.Button("Cancel", im.ImVec2(im.GetContentRegionAvail().x, 0)) then
im.CloseCurrentPopup()
end
im.EndPopup()
endIndentation for Nested Options
Use indent/unindent for conditional sub-options:
local hasOption = im.BoolPtr(data.option ~= nil)
if im.Checkbox("Enable Option", hasOption) then
if hasOption[0] then
data.option = defaultValue
else
data.option = nil
end
end
if hasOption[0] then
im.Indent()
local optionValue = im.FloatPtr(data.option)
if im.InputFloat("Option Value", optionValue) then
data.option = optionValue[0]
end
im.Unindent()
endCollapsible Sections
Use CollapsingHeader for optional/advanced details:
if im.CollapsingHeader1("Advanced Options") then
-- detailed content here
end
-- With default open:
if im.CollapsingHeader1("Details##section", im.TreeNodeFlags_DefaultOpen) then
-- content
endRich Tooltips on Hover
Add detailed tooltips to list items:
if im.Button(itemLabel) then
selectItem(item)
end
if im.IsItemHovered() then
im.BeginTooltip()
im.Text("ID: " .. item.id)
local done, total = getProgress(item)
if done == total then
im.TextColored(colors.success, "Complete!")
else
im.TextColored(colors.warning, string.format("Progress: %d/%d", done, total))
im.Separator()
for _, component in ipairs(getMissing(item)) do
im.TextColored(colors.error, "✗ " .. component)
end
end
im.EndTooltip()
endMoney/Number Formatting
Format large numbers for readability:
local function formatMoney(amount)
amount = math.floor(amount + 0.5)
if amount >= 1000000 then
return string.format("$%.1fM", amount / 1000000)
elseif amount >= 1000 then
return string.format("$%dk", math.floor(amount / 1000))
else
return string.format("$%d", amount)
end
endStatus Line Pattern
Show save state and context at top of window:
if modified then
im.TextColored(colors.warning, "* Modified (unsaved)")
else
im.TextColored(colors.success, "Saved")
end
im.SameLine()
im.TextColored(colors.dimmed, "| Level: " .. currentLevel)
im.Separator()Full-Width Buttons
For action buttons, use full width:
-- Single button
if im.Button("Action", im.ImVec2(im.GetContentRegionAvail().x, 0)) then
doAction()
end
-- Two buttons side by side
if im.Button("Left", im.ImVec2(im.GetContentRegionAvail().x * 0.48, 0)) then
leftAction()
end
im.SameLine()
if im.Button("Right", im.ImVec2(im.GetContentRegionAvail().x, 0)) then
rightAction()
end
-- Tall action button
if im.Button("Important Action", im.ImVec2(im.GetContentRegionAvail().x, 30)) then
doImportantThing()
endAdd/Remove List Pattern
For dynamic lists with add/remove:
for i, item in ipairs(items) do
im.PushID1("item_" .. tostring(i))
-- Remove button (except first item)
if i > 1 then
if im.Button("X##remove", im.ImVec2(24, 0)) then
table.remove(items, i)
im.PopID()
break -- exit loop, list modified
end
im.SameLine()
end
-- Item content
drawItemContent(item, i)
im.PopID()
end
-- Add button at bottom
if im.Button("+ Add Item", im.ImVec2(im.GetContentRegionAvail().x, 0)) then
table.insert(items, createNewItem())
endGraph Visualization
For PlotLines with readable tooltips:
local function getScaleInfo(maxVal)
if maxVal >= 1000000 then return 1000000, "M"
elseif maxVal >= 1000 then return 1000, "k"
else return 1, ""
end
end
local function drawPlot(label, dataTable, maxVal, height)
local divisor, suffix = getScaleInfo(maxVal)
local scaledTable = {}
for i, v in ipairs(dataTable) do
scaledTable[i] = v / divisor
end
local data = im.TableToArrayFloat(scaledTable)
local dataLen = im.GetLengthArrayFloat(data)
local scaledMax = math.max((maxVal / divisor) * 1.1, 0.01)
local overlayText = suffix ~= "" and ("$" .. suffix) or "$"
im.PlotLines1(label, data, dataLen, 0, overlayText, 0, scaledMax,
im.ImVec2(im.GetContentRegionAvail().x, height))
endComplete Example Structure
local M = {}
local im = ui_imgui
local ffi = require("ffi")
-- ============================================================================
-- COLORS
-- ============================================================================
local colors = {
success = im.ImVec4(0.2, 0.8, 0.2, 1.0),
warning = im.ImVec4(1.0, 0.8, 0.2, 1.0),
error = im.ImVec4(0.9, 0.2, 0.2, 1.0),
info = im.ImVec4(0.4, 0.7, 1.0, 1.0),
dimmed = im.ImVec4(0.6, 0.6, 0.6, 1.0),
}
-- ============================================================================
-- HELPERS
-- ============================================================================
local function helpMarker(text, sameLine)
if sameLine then im.SameLine() end
im.TextDisabled("(?)")
if im.IsItemHovered() then
im.BeginTooltip()
im.PushTextWrapPos(im.GetFontSize() * 25)
im.TextUnformatted(text)
im.PopTextWrapPos()
im.EndTooltip()
end
end
-- ============================================================================
-- MAIN GUI
-- ============================================================================
local function onEditorGui()
if editor.beginWindow(toolWindowName, "My Editor", im.WindowFlags_MenuBar) then
-- Menu bar
if im.BeginMenuBar() then
if im.BeginMenu("File") then
if im.MenuItem1("Save") then saveData() end
im.EndMenu()
end
im.EndMenuBar()
end
-- Status line
if modified then
im.TextColored(colors.warning, "* Modified")
else
im.TextColored(colors.success, "Saved")
end
im.Separator()
-- Two-panel layout
local leftWidth = im.GetContentRegionAvail().x * 0.3
im.BeginChild1("List", im.ImVec2(leftWidth, -1), true)
-- list content
im.EndChild()
im.SameLine()
im.BeginChild1("Details", im.ImVec2(0, -1), true)
im.SeparatorText("Section 1")
-- form content
im.SeparatorText("Section 2")
-- more content
im.EndChild()
editor.endWindow()
end
end
M.onEditorGui = onEditorGui
return M