RLS Studios
ProjectsPatreonCommunityDocsAbout
Join Patreon
BeamNG Modding Docs

Guides

Reference

UI

BeamNG ImGui API ReferenceBeamNG ImGui Cheat SheetBeamNG ImGui Data TypesBeamNG World Editor ImGui WindowsBeamNG ImGui Flags & ConstantsBeamNG ImGui Patterns & ExamplesBeamNG ImGui Style Guide

Resources

BeamNG Game Engine Lua Cheat SheetGE Developer RecipesMCP Server Setup

// RLS.STUDIOS=true

Premium Mods for BeamNG.drive. Career systems, custom vehicles, and immersive gameplay experiences.

Index

HomeProjectsPatreon

Socials

DiscordPatreon (RLS)Patreon (Vehicles)

© 2026 RLS Studios. All rights reserved.

Modding since 2024

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 M

Centered 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
end

Styled 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)
end

Form 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()
end

Dropdown / 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()
end

Searchable 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()
end

Tables

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
end

Collapsible 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")
end

Tab 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()
end

Menu 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()
end

Modal 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()
end

Tooltip

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 first

Unique 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()
end

Split 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()
end

Custom 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))
end

Image / 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!")
end

Disabled 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 2

Color 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()
end

Knobs (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)

BeamNG ImGui Flags & Constants

All flags are accessed as `im.FlagName` where `im = ui_imgui`. Combine with `+` operator.

BeamNG ImGui Style Guide

Visual patterns and conventions extracted from the Freeroam Event Editor. Use these patterns for consistent, polished editor UIs.

On this page

Basic WindowCentered Modal DialogStyled Dark WindowForm with Input FieldsDropdown / Combo BoxSearchable Filtered ListTablesCollapsible SectionsTab BarMenu BarPopup (Context Menu)Modal PopupTooltipUnique IDs (Avoiding Conflicts)Split Panel LayoutCustom DrawingImage / TextureDisabled WidgetsPlot Lines (Graphs)Plot HistogramProgress BarRadio ButtonsColor PickerDrag and DropKnobs (BeamNG Custom)