Benjis.UI Framework
This wiki documents the current Benjis.UI clientside framework, including the theme system, windowing, panels, buttons, fonts, labels, tabs, model panels, scroll panels, list helpers, and text entry controls. It is written against the new framework files, not the older version of the wiki.
What this framework provides
A small themed VGUI layer built around factory functions such as CreateFrame, CreatePanel, CreateButton, CreateScroll, CreateTabs, CreateTextEntry, CreateModelPanel, and list/tile helpers. The framework centralises colours, font presets, animation timing, and common panel styles.
Design goals visible in the code
The new version heavily reduces repeated per-frame work by caching fonts, using local theme aliases, precomputing paint handlers, and avoiding avoidable allocations inside Paint and Think callbacks. That matters when you open lots of panels at once, such as F4 pages, inventories, admin tools, or shop interfaces.
Suggested File Load Order
Several modules depend on shared state created by earlier files, especially the theme and font systems. A safe include order for a typical clientside loader is shown below.
-- lua/autorun/client/cl_benjis_ui_loader.lua
Benjis = Benjis or {}
Benjis.UI = Benjis.UI or {}
include("benjis_ui/cl_theme.lua")
include("benjis_ui/cl_fonts.lua")
include("benjis_ui/cl_panel.lua")
include("benjis_ui/cl_window.lua")
include("benjis_ui/cl_button.lua")
include("benjis_ui/cl_scroll.lua")
include("benjis_ui/cl_list.lua")
include("benjis_ui/cl_model.lua")
include("benjis_ui/cl_tabs.lua")
include("benjis_ui/cl_textentry.lua")
cl_theme.lua defines the palette and layout constants. cl_fonts.lua sets up font APIs and label creation. cl_panel.lua depends on the theme existing. cl_window.lua, cl_button.lua, cl_list.lua, and the other modules all build on those shared APIs.Quick Start
The example below creates a centered window with a card container, a header label, a search field, tabs, and a button row. It demonstrates how the framework is intended to be composed.
local frame = Benjis.UI:CreateFrame("Example Menu", 960, 640, "center")
local root = Benjis.UI:CreatePanel(frame.Content, "bg")
root:Dock(FILL)
root:DockMargin(Benjis.UI.Padding, Benjis.UI.Padding, Benjis.UI.Padding, Benjis.UI.Padding)
local headerCard = Benjis.UI:CreatePanel(root, "card")
headerCard:Dock(TOP)
headerCard:SetTall(88)
headerCard:DockMargin(0, 0, 0, 12)
local title = Benjis.UI:CreateLabel(headerCard, "Framework Demo", "Header")
title:Dock(TOP)
title:DockMargin(12, 12, 12, 4)
local subtitle = Benjis.UI:CreateLabel(
headerCard,
"This page shows how multiple Benjis.UI controls fit together.",
"Text"
)
subtitle:Dock(TOP)
subtitle:DockMargin(12, 0, 12, 12)
local search = Benjis.UI:CreateSearchEntry(root, {
placeholder = "Search items, players, or categories...",
tall = 38
})
search:Dock(TOP)
search:DockMargin(0, 0, 0, 12)
local tabs = Benjis.UI:CreateTabs(root, {
tabHeight = 36,
tabSpacing = 8,
contentStyle = "transparent"
})
tabs:Dock(FILL)
tabs:AddTab("dashboard", "Dashboard", function(page)
local panel = Benjis.UI:CreatePanel(page, "card")
panel:Dock(FILL)
local label = Benjis.UI:CreateLabel(page, "Dashboard content goes here.", "Text")
label:Dock(TOP)
label:DockMargin(12, 12, 12, 0)
end)
tabs:AddTab("settings", "Settings", function(page)
local save = Benjis.UI:CreateButton(page, "Save Settings")
save:Dock(TOP)
save:DockMargin(0, 12, 0, 0)
save.DoClick = function()
chat.AddText(Color(0, 200, 0), "Saved settings")
end
end)
Theme System
The theme file defines the shared palette and the two main global layout constants used throughout the framework: Benjis.UI.AnimTime and Benjis.UI.Padding.
Benjis.UI.Theme = {
BG = Color(18, 18, 18, 235),
Panel = Color(25, 25, 25, 150),
Hover = Color(35, 35, 35, 220),
Accent = Color(200, 60, 60),
Divider = Color(0, 0, 0, 180),
Text = color_white,
Muted = Color(160, 160, 160),
_CardOutline = Color(0, 0, 0, 120),
}
Benjis.UI.AnimTime = 0.25
Benjis.UI.Padding = 12
Theme fields
| Key | Purpose | Used by |
|---|---|---|
BG | Main background colour for windows and some text entries. | Frames, panel style bg, text entries. |
Panel | Default panel/body surface. | Buttons, content panels, rows, tabs, item tiles. |
Hover | Hover/active surface colour. | Buttons, tabs, text entry states, tile hovers. |
Accent | Highlight colour. | Scroll grip, tab underline, cursor/highlight states. |
Divider | Outline or separator colour. | Text entry border. |
Text | Primary foreground text colour. | Labels, buttons, tabs, rows, tiles. |
Muted | Secondary foreground text colour. | Close button, placeholder text, tile amount text. |
_CardOutline | Internal helper colour for the card outline. | CreatePanel(..., "card"). |
Best practice for theme overrides
Do not mutate the existing colour tables in place if you want to override the theme. Replace entries with new Color(...) values instead. This matches the framework’s own guidance and prevents hard-to-track issues caused by shared colour references.
-- Good
Benjis.UI.Theme.Accent = Color(52, 152, 219)
Benjis.UI.Theme.Panel = Color(22, 22, 26, 190)
-- Avoid in-place mutation patterns like this
-- Benjis.UI.Theme.Accent.r = 52
Theme override example for a police terminal
local oldAccent = Benjis.UI.Theme.Accent
local oldHover = Benjis.UI.Theme.Hover
Benjis.UI.Theme.Accent = Color(40, 120, 255)
Benjis.UI.Theme.Hover = Color(32, 42, 72, 220)
local frame = Benjis.UI:CreateFrame("Police MDT", 1100, 700, "sidebar_right")
-- restore later if you only want temporary styling
Benjis.UI.Theme.Accent = oldAccent
Benjis.UI.Theme.Hover = oldHover
Fonts and Labels
The font module handles font creation, caching, preset registration, preset lookup, and the CreateLabel helper. Fonts are resolution-scaled using a cached factor derived from ScrH(), clamped between 0.85 and 1.25. When the screen size changes, the cache is reset and rebuilt on demand.
Available APIs
| Function | Purpose |
|---|---|
Benjis.UI:GetFont(name, size, opts) | Creates or returns a cached surface font ID. |
Benjis.UI:RegisterFontPreset(id, data) | Registers a preset name for reuse. |
Benjis.UI:GetFontPreset(id) | Returns the font ID for a preset, with fallback behaviour if the preset is missing. |
Benjis.UI:CreateLabel(parent, text, preset) | Creates a wrapped DLabel using a preset and the theme text colour. |
Built-in font presets
Title = { font = "Roboto", size = 20, options = { weight = 800 } }
Header = { font = "Roboto", size = 18, options = { weight = 700 } }
Text = { font = "Roboto", size = 16, options = { weight = 500 } }
Small = { font = "Roboto", size = 13, options = { weight = 500 } }
Button = { font = "Roboto", size = 16, options = { weight = 600 } }
Mono = { font = "Courier New", size = 14, options = { weight = 500 } }
Manual font creation
local fontID = Benjis.UI:GetFont("Tahoma", 18, {
weight = 700,
outline = false,
antialias = true
})
hook.Add("HUDPaint", "ExampleFontUsage", function()
draw.SimpleText("Benjis.UI custom font", fontID, 40, 40, color_white)
end)
Registering a new preset
Benjis.UI:RegisterFontPreset("JobCardTitle", {
font = "Roboto",
size = 22,
options = {
weight = 900,
antialias = true
}
})
local lbl = Benjis.UI:CreateLabel(parent, "Civil Protection", "JobCardTitle")
Using labels correctly
CreateLabel creates a wrapped label, enables vertical auto-stretch, sets its alignment to left/vertical-center, and disables mouse input. It is suited for descriptions, body text, and titles where you want standardised typography with minimal setup.
local desc = Benjis.UI:CreateLabel(panel,
"This shipment can only be bought by Gun Dealers and requires a valid license.",
"Text"
)
desc:Dock(TOP)
desc:DockMargin(12, 0, 12, 12)
GetFontPreset does not hard-crash when a preset does not exist. It logs through ErrorNoHalt and falls back to the Text preset if available, or Default as a final fallback.Frames and Windows
CreateFrame(title, w, h, style) creates an animated DFrame with a custom header, close button, blur-backed paint, and a themed frame.Content panel for your body layout.
Supported styles
| Style | Open animation | Close animation | Typical use |
|---|---|---|---|
sidebar | Slides in from the left edge. | Slides back out to the left. | F4 menus, side inventories, admin drawers. |
sidebar_right | Slides in from the right edge. | Slides back out to the right. | Context panels, secondary inspectors, side tools. |
center | Starts lower and transparent, then moves upward while fading in. | Moves downward slightly while fading out. | Confirm dialogs, settings windows, modal pages. |
Minimal example
local frame = Benjis.UI:CreateFrame("F4 Menu", 1100, 700, "sidebar")
local content = frame.Content
local body = Benjis.UI:CreatePanel(content, "transparent")
body:Dock(FILL)
body:DockMargin(Benjis.UI.Padding, Benjis.UI.Padding, Benjis.UI.Padding, Benjis.UI.Padding)
Closing a frame safely
local frame = Benjis.UI:CreateFrame("Example", 700, 500, "center")
local btn = Benjis.UI:CreateButton(frame.Content, "Close")
btn:Dock(TOP)
btn:DockMargin(12, 12, 12, 0)
btn.DoClick = function()
if frame.CloseAnimated then
frame:CloseAnimated()
else
frame:Remove()
end
end
Header and content structure
The frame internally creates a 56px tall header with the title on the left and a custom close button on the right. Everything below that header is exposed as frame.Content, which is itself a themed DPanel. In practice, you should treat frame.Content as your main container and build all layout inside it.
Frame used as a DarkRP command menu
concommand.Add("open_job_terminal", function()
if IsValid(BenjisJobTerminal) then
BenjisJobTerminal:Remove()
end
local frame = Benjis.UI:CreateFrame("Job Terminal", 980, 640, "sidebar_right")
BenjisJobTerminal = frame
local left = Benjis.UI:CreatePanel(frame.Content, "panel")
left:Dock(LEFT)
left:SetWide(260)
left:DockMargin(12, 12, 12, 12)
local right = Benjis.UI:CreatePanel(frame.Content, "card")
right:Dock(FILL)
right:DockMargin(0, 12, 12, 12)
end)
Panels
CreatePanel(parent, paintStyle) returns a themed DPanel with a pre-selected paint function. The style is resolved once when the panel is created rather than on every frame.
Supported paint styles
| Style | Effect | When to use it |
|---|---|---|
panel | Draws a flat box with Theme.Panel. | Default content containers and standard group boxes. |
bg | Draws a flat box with Theme.BG. | Deeper backgrounds and inner window roots. |
transparent | Does not draw anything. | Pure layout containers, docking wrappers, spacing shells. |
card | Draws a rounded card with outline. | Feature blocks, setting groups, stats cards, inventory sections. |
Examples
local root = Benjis.UI:CreatePanel(frame.Content, "transparent")
root:Dock(FILL)
local sidebar = Benjis.UI:CreatePanel(root, "panel")
sidebar:Dock(LEFT)
sidebar:SetWide(240)
local content = Benjis.UI:CreatePanel(root, "bg")
content:Dock(FILL)
content:DockMargin(12, 0, 0, 0)
local card = Benjis.UI:CreatePanel(content, "card")
card:Dock(TOP)
card:SetTall(120)
card:DockMargin(12, 12, 12, 0)
Unknown styles
If you pass an unknown style name, the framework falls back to the default panel painter. That gives safe behaviour instead of a broken panel.
Buttons and Confirm Dialogs
CreateButton(parent, text, tall) creates a themed DButton with a hover animation. The button stores the visible label in btn.Label, and also exposes btn:SetLabel(txt) so you can update the display text later.
Basic button
local btn = Benjis.UI:CreateButton(parent, "Save Settings")
btn:Dock(TOP)
btn:DockMargin(0, 0, 0, 8)
btn.DoClick = function()
chat.AddText(Color(0, 255, 0), "Settings saved")
end
Changing label text dynamically
local ready = false
local btn = Benjis.UI:CreateButton(parent, "Start")
btn:Dock(TOP)
btn.DoClick = function(self)
ready = not ready
self:SetLabel(ready and "Stop" or "Start")
end
Button list example
for i = 1, 5 do
local btn = Benjis.UI:CreateButton(parent, "Option " .. i)
btn:Dock(TOP)
btn:DockMargin(0, 0, 0, 6)
btn.DoClick = function()
print("Clicked option", i)
end
end
How hover rendering works
The button smoothly lerps an internal hover value toward 1 while hovered and back toward 0 otherwise. That value is then passed into Benjis.UI:LerpColor to blend between Theme.Panel and Theme.Hover.
LerpColor caveat
Benjis.UI:LerpColor. The framework uses one shared mutable colour buffer for performance. Consume the result immediately in a draw call. Storing it for later will produce incorrect colours because the next LerpColor call overwrites that same buffer.-- Correct
local function PaintButton(self, w, h)
draw.RoundedBox(0, 0, 0, w, h,
Benjis.UI:LerpColor(self.Hover, Benjis.UI.Theme.Panel, Benjis.UI.Theme.Hover)
)
end
-- Wrong
local col = Benjis.UI:LerpColor(0.5, color_white, color_black)
-- storing this and reusing it later is unsafe
Confirm dialog
Benjis.UI:Confirm(title, text, yesText, noText, yesFunc, noFunc) is the framework’s styled replacement for Derma_Query. It creates a centered animated frame, a message label, and two side-by-side buttons.
Benjis.UI:Confirm(
"Delete Shipment",
"Are you sure you want to delete this shipment preset?",
"Delete",
"Cancel",
function()
print("Confirmed delete")
end,
function()
print("Cancelled")
end
)
Using Confirm before a DarkRP action
deleteButton.DoClick = function()
Benjis.UI:Confirm(
"Clear Warrants",
"This will request a warrant reset for all players. Continue?",
"Proceed",
"Abort",
function()
net.Start("BenjisPolice_RequestWarrantReset")
net.SendToServer()
end
)
end
Scroll Panels
CreateScroll(parent) returns a styled DScrollPanel with a slim 6px scrollbar. The up and down buttons, plus the bar background, are intentionally not painted. Only the grip is drawn, using Theme.Accent.
Simple scroll list
local scroll = Benjis.UI:CreateScroll(parent)
scroll:Dock(FILL)
for i = 1, 25 do
local btn = Benjis.UI:CreateButton(scroll, "Entry " .. i)
btn:Dock(TOP)
btn:DockMargin(0, 0, 0, 8)
end
Scrollable card stack
local scroll = Benjis.UI:CreateScroll(parent)
scroll:Dock(FILL)
for i = 1, 8 do
local card = Benjis.UI:CreatePanel(scroll, "card")
card:Dock(TOP)
card:SetTall(100)
card:DockMargin(0, 0, 8, 12)
local lbl = Benjis.UI:CreateLabel(card, "Card #" .. i, "Header")
lbl:Dock(TOP)
lbl:DockMargin(12, 12, 12, 0)
end
Lists and Item Tiles
The list module provides four helpers: CreateList, AddListRow, CreateGrid, and AddItemTile. These cover common scrollable rows and grid-style item tiles.
CreateList(parent)
This is a convenience wrapper around CreateScroll that docks the scroll panel to fill its parent.
local list = Benjis.UI:CreateList(parent)
AddListRow(list, text, stripColor, iconPath)
This adds a clickable row implemented as a DButton. It supports an optional coloured strip on the left and an optional icon image. The text offset automatically changes if an icon is supplied.
local list = Benjis.UI:CreateList(parent)
for _, ply in ipairs(player.GetAll()) do
local row = Benjis.UI:AddListRow(
list,
ply:Nick(),
team.GetColor(ply:Team()),
"icon16/user.png"
)
row.DoClick = function()
print("Selected player:", ply:Nick())
end
end
Player list without icons
for _, ply in ipairs(player.GetAll()) do
local row = Benjis.UI:AddListRow(list, ply:Nick(), team.GetColor(ply:Team()))
row.DoClick = function()
chat.AddText(color_white, "Viewing ", Color(255, 200, 0), ply:Nick())
end
end
CreateGrid(parent, spacing)
This returns two values: a scroll panel and a DIconLayout. The scroll panel is docked to fill its parent, and the icon layout is created inside it. Use the layout object for adding tiles.
local scroll, grid = Benjis.UI:CreateGrid(parent, 12)
scroll:Dock(FILL)
AddItemTile(grid, data)
This creates a 110x140 button tile with a hover state, model preview, item name, and quantity string. The data table is expected to contain at least name, amount, and model.
local _, grid = Benjis.UI:CreateGrid(parent, 12)
local tile = Benjis.UI:AddItemTile(grid, {
name = "Pistol Ammo",
amount = 3,
model = "models/items/boxsrounds.mdl"
})
tile.DoClick = function()
print("Clicked ammo tile")
end
Updating a tile after creation
The tile exposes tile:SetName(str) and tile:SetAmount(n), which update the prebuilt display strings without rebuilding the panel.
local tile = Benjis.UI:AddItemTile(grid, {
name = "Lockpick",
amount = 1,
model = "models/weapons/w_crowbar.mdl"
})
-- later
tile:SetAmount(2)
tile:SetName("Advanced Lockpick")
Inventory grid example
local scroll, grid = Benjis.UI:CreateGrid(parent, 10)
scroll:Dock(FILL)
local items = {
{ name = "Health Kit", amount = 2, model = "models/items/healthkit.mdl" },
{ name = "Armor Battery", amount = 1, model = "models/items/battery.mdl" },
{ name = "Lockpick", amount = 3, model = "models/weapons/w_crowbar.mdl" }
}
for _, item in ipairs(items) do
local tile = Benjis.UI:AddItemTile(grid, item)
tile.DoClick = function()
print("Use item:", item.name)
end
end
Model Panels
The model module provides a static preview panel and an interactive preview panel.
CreateModelPanel(parent, model)
This creates a non-interactive DModelPanel with fixed camera settings, disabled idle rotation, and visibility-aware manual painting toggling. It is intended for cheap static previews in tiles, cards, and lists.
local mdl = Benjis.UI:CreateModelPanel(parent, "models/player/kleiner.mdl")
mdl:SetSize(220, 320)
mdl:Dock(LEFT)
mdl:DockMargin(0, 0, 12, 0)
CreateInteractiveModelPanel(parent, model)
This version allows drag-to-rotate and mousewheel zoom. It tracks the current yaw in pnl.Angles and the camera distance in pnl.Zoom.
local mdl = Benjis.UI:CreateInteractiveModelPanel(parent, LocalPlayer():GetModel())
mdl:Dock(FILL)
Character preview example
local card = Benjis.UI:CreatePanel(parent, "card")
card:Dock(FILL)
local mdl = Benjis.UI:CreateInteractiveModelPanel(card, LocalPlayer():GetModel())
mdl:Dock(FILL)
local apply = Benjis.UI:CreateButton(card, "Apply Outfit")
apply:Dock(BOTTOM)
apply:DockMargin(12, 12, 12, 12)
Static tile preview example
local tile = Benjis.UI:AddItemTile(grid, {
name = "MP5",
amount = 1,
model = "models/weapons/w_smg1.mdl"
})
Tabs
CreateTabs(parent, opts) provides a lightweight tab/page switcher for interfaces such as F4 subpages, inventories, admin panels, chatboxes, and settings menus. It does not use DPropertySheet. Instead, it manages a button strip and one page panel per tab.
Options
| Option | Purpose | Default |
|---|---|---|
tabHeight | Height of the tab button strip. | 34 |
tabSpacing | Spacing between tab buttons. | 6 |
contentStyle | Paint style used for the content area panel. | "transparent" |
onTabChanged | Callback triggered after switching tabs. | nil |
Main methods
| Method | Purpose |
|---|---|
tabs:AddTab(id, title, buildFunc) | Creates a button and page for a new tab. |
tabs:SetTabLabel(id, title) | Changes the display text and button width. |
tabs:SetActiveTab(id) | Shows the target page and hides all others. |
tabs:GetActiveTab() | Returns the active tab ID. |
tabs:GetPage(id) | Returns the page panel for a tab ID. |
Basic tabs example
local tabs = Benjis.UI:CreateTabs(parent, {
tabHeight = 36,
tabSpacing = 8,
contentStyle = "transparent"
})
tabs:Dock(FILL)
tabs:AddTab("jobs", "Jobs", function(page)
local lbl = Benjis.UI:CreateLabel(page, "Available jobs go here.", "Text")
lbl:Dock(TOP)
end)
tabs:AddTab("shipments", "Shipments", function(page)
local lbl = Benjis.UI:CreateLabel(page, "Shipment listing goes here.", "Text")
lbl:Dock(TOP)
end)
Using onTabChanged
local tabs = Benjis.UI:CreateTabs(parent, {
onTabChanged = function(self, newID, oldID, page, button)
print("Changed tab from", oldID, "to", newID)
end
})
Changing a tab title later
tabs:SetTabLabel("jobs", "Jobs (12)")
DarkRP F4 style page layout example
local frame = Benjis.UI:CreateFrame("City Menu", 1180, 720, "sidebar")
local tabs = Benjis.UI:CreateTabs(frame.Content, {
tabHeight = 40,
tabSpacing = 10,
contentStyle = "transparent"
})
tabs:Dock(FILL)
tabs:DockMargin(12, 12, 12, 12)
tabs:AddTab("jobs", "Jobs", function(page)
local scroll = Benjis.UI:CreateScroll(page)
scroll:Dock(FILL)
end)
tabs:AddTab("entities", "Entities", function(page)
local scroll = Benjis.UI:CreateScroll(page)
scroll:Dock(FILL)
end)
tabs:AddTab("weapons", "Weapons", function(page)
local scroll = Benjis.UI:CreateScroll(page)
scroll:Dock(FILL)
end)
tabs:AddTab("settings", "Settings", function(page)
local entry = Benjis.UI:CreateTextEntry(page, { placeholder = "Search settings..." })
entry:Dock(TOP)
end)
Text Entries and Search Boxes
The text entry module provides CreateTextEntry(parent, opts) and CreateSearchEntry(parent, opts). These are themed wrappers around DTextEntry with hover/focus interpolation, placeholder rendering, custom colours, and state setters.
Supported options
| Option | Purpose |
|---|---|
tall | Control height. Default 36. |
font | Explicit font ID to use. |
preset | Font preset name used if font is not supplied. |
textColor | Typed text colour. |
cursorColor | Cursor colour. |
highlightColor | Selection highlight colour. |
placeholder | Placeholder text shown while empty and unfocused. |
placeholderColor | Placeholder text colour. |
bgColor | Idle background colour. |
hoverColor | Hover background colour. |
activeColor | Focus background colour. |
outlineColor | Border colour. |
textInsetX, textInsetY | Text inset/padding. |
rounded | Rounded corner size. Defaults to 6. |
updateOnType | Whether text updates on each typed character. |
enterAllowed | Whether pressing Enter is allowed. |
numeric | Numeric-only input. |
multiline | Enable multiline mode. |
value | Initial value. |
Basic text entry
local entry = Benjis.UI:CreateTextEntry(parent, {
placeholder = "Enter title...",
tall = 38
})
entry:Dock(TOP)
entry:DockMargin(0, 0, 0, 12)
Numeric entry
local amountEntry = Benjis.UI:CreateTextEntry(parent, {
placeholder = "Amount",
numeric = true,
value = 1,
tall = 36
})
Multiline notes field
local notes = Benjis.UI:CreateTextEntry(parent, {
placeholder = "Internal notes...",
multiline = true,
enterAllowed = true,
tall = 140,
textInsetY = 8
})
notes:Dock(TOP)
Search entry
CreateSearchEntry is a small wrapper that sets the default placeholder to Search..., increases left text inset to make space for an icon, and paints a magnifier icon on the left.
local search = Benjis.UI:CreateSearchEntry(parent, {
placeholder = "Search warrants...",
updateOnType = true
})
search:Dock(TOP)
search.OnValueChange = function(self, value)
print("Search changed:", value)
end
Changing state colours at runtime
local entry = Benjis.UI:CreateTextEntry(parent, {
placeholder = "Custom palette"
})
entry:SetStateColors(
Color(12, 16, 22, 235),
Color(18, 28, 42, 235),
Color(24, 36, 58, 235),
Color(0, 0, 0, 180)
)
entry:SetPlaceholderColor(Color(140, 165, 190))
Recommended Usage Patterns
1. Use layout wrappers aggressively
Use transparent panels as layout shells. Keep visible paint styles for actual visual sections only. That keeps your structure readable and prevents unnecessary paint work.
local shell = Benjis.UI:CreatePanel(frame.Content, "transparent")
shell:Dock(FILL)
shell:DockMargin(12, 12, 12, 12)
local left = Benjis.UI:CreatePanel(shell, "panel")
left:Dock(LEFT)
left:SetWide(260)
local right = Benjis.UI:CreatePanel(shell, "transparent")
right:Dock(FILL)
right:DockMargin(12, 0, 0, 0)
2. Keep UI creation separate from action logic
Build the panel structure in one function and keep data loading, filtering, and net message handling in separate functions. That makes the framework easy to reuse across DarkRP jobs, entity menus, and admin tools.
local function BuildPlayerListPage(parent)
local list = Benjis.UI:CreateList(parent)
for _, ply in ipairs(player.GetAll()) do
local row = Benjis.UI:AddListRow(list, ply:Nick(), team.GetColor(ply:Team()))
row.DoClick = function()
OpenPlayerInspector(ply)
end
end
end
3. Use theme and presets, not hardcoded styling
If every page uses the same theme and font presets, you can rebrand or rebalance contrast later without rewriting every menu.
4. Prefer static model panels in dense grids
Use CreateModelPanel for tiles and list entries. Reserve CreateInteractiveModelPanel for one or two prominent previews such as character customisation or item inspection.
Important Caveats
LerpColor returns shared state
Do not cache it. Pass it directly into draw.RoundedBox, draw.SimpleText, or another immediate draw call.
Theme aliases are captured
Some modules localise theme references when controls are created. If you replace the theme after panels already exist, existing controls may still be using earlier references until rebuilt.
Text entries do not secure anything
Clients can still spoof input. Validate values server-side, clamp lengths, check permissions, and rate-limit requests.
Interactive model panels cost more
They are optimised, but they still do more work than static previews. Avoid filling huge scrolling inventories with interactive model panels.
Extended Examples
Example 1: Searchable command list
local frame = Benjis.UI:CreateFrame("Command Browser", 900, 620, "center")
local root = Benjis.UI:CreatePanel(frame.Content, "transparent")
root:Dock(FILL)
root:DockMargin(12, 12, 12, 12)
local search = Benjis.UI:CreateSearchEntry(root, {
placeholder = "Search chat commands...",
updateOnType = true
})
search:Dock(TOP)
search:DockMargin(0, 0, 0, 12)
local list = Benjis.UI:CreateList(root)
local commands = {
{ name = "/dropmoney", color = Color(46, 204, 113) },
{ name = "/lockdown", color = Color(231, 76, 60) },
{ name = "/wanted", color = Color(241, 196, 15) },
{ name = "/job", color = Color(52, 152, 219) }
}
local function RebuildList(filter)
list:Clear()
filter = string.Trim(string.lower(filter or ""))
for _, cmd in ipairs(commands) do
if filter == "" or string.find(string.lower(cmd.name), filter, 1, true) then
local row = Benjis.UI:AddListRow(list, cmd.name, cmd.color, "icon16/script.png")
row.DoClick = function()
SetClipboardText(cmd.name)
end
end
end
end
search.OnValueChange = function(self, value)
RebuildList(value)
end
RebuildList()
Example 2: Simple inventory viewer
local frame = Benjis.UI:CreateFrame("Inventory", 1000, 700, "sidebar_right")
local wrap = Benjis.UI:CreatePanel(frame.Content, "transparent")
wrap:Dock(FILL)
wrap:DockMargin(12, 12, 12, 12)
local search = Benjis.UI:CreateSearchEntry(wrap, {
placeholder = "Search inventory...",
updateOnType = true
})
search:Dock(TOP)
search:DockMargin(0, 0, 0, 12)
local scroll, grid = Benjis.UI:CreateGrid(wrap, 10)
scroll:Dock(FILL)
local items = {
{ name = "Lockpick", amount = 2, model = "models/weapons/w_crowbar.mdl" },
{ name = "Medkit", amount = 1, model = "models/items/healthkit.mdl" },
{ name = "Battery", amount = 4, model = "models/items/battery.mdl" }
}
local function RebuildInventory(filter)
grid:Clear()
filter = string.Trim(string.lower(filter or ""))
for _, item in ipairs(items) do
if filter == "" or string.find(string.lower(item.name), filter, 1, true) then
local tile = Benjis.UI:AddItemTile(grid, item)
tile.DoClick = function()
print("Selected item:", item.name)
end
end
end
end
search.OnValueChange = function(self, value)
RebuildInventory(value)
end
RebuildInventory()
Example 3: Administrative inspector with tabs
local frame = Benjis.UI:CreateFrame("Admin Inspector", 1180, 760, "sidebar")
local tabs = Benjis.UI:CreateTabs(frame.Content, {
tabHeight = 40,
tabSpacing = 8,
contentStyle = "transparent",
onTabChanged = function(self, newID, oldID)
print("Switched from", oldID, "to", newID)
end
})
tabs:Dock(FILL)
tabs:DockMargin(12, 12, 12, 12)
tabs:AddTab("players", "Players", function(page)
local list = Benjis.UI:CreateList(page)
for _, ply in ipairs(player.GetAll()) do
local row = Benjis.UI:AddListRow(page, ply:Nick(), team.GetColor(ply:Team()), "icon16/user.png")
row.DoClick = function()
print("Inspect player", ply:SteamID())
end
end
end)
tabs:AddTab("logs", "Logs", function(page)
local box = Benjis.UI:CreateTextEntry(page, {
multiline = true,
tall = 400,
value = "Recent moderation actions will appear here..."
})
box:Dock(FILL)
end)
tabs:AddTab("preview", "Character Preview", function(page)
local mdl = Benjis.UI:CreateInteractiveModelPanel(page, LocalPlayer():GetModel())
mdl:Dock(FILL)
end)
players tab should create list rows inside the list panel, not directly on the page, when used in production. Keep parent references precise when building reusable pages.