UI Controls - RGXMods/RGX-Framework GitHub Wiki
UI Controls & Options Panel
The UI module (RGXUI) provides widget factories for common interface controls and a full options panel builder with tab system and scroll container.
Widget Factories
Shared frame and alignment model
A frame is a styled layout host: a page, tab content, card, or popout uses the same Design surfaces and UI geometry. Layout owns placement; Design owns theme colors/textures/corners. Pages can combine one/two columns, lists, rows and pagination without introducing a second layout/scroll owner.
- Standard row gap: 8px. Control-row left/right inset: 8px.
AnchorRowReset(row, reset, control, inset?)centers Reset on the actual track/trigger/swatch and keeps the shared right inset, independent of labels.- Flow rows vertically center elements and allocate flexible widths after reserving fixed/right-aligned controls; natural dimensions are remeasured.
CreateCard(parent, { columns = 1|2, columnGap = 8 })exposes.columnsand.flows;.flowaliases the first flow.AutoHeight()uses the tallest column. Once enabled, width/show changes reflow it automatically; an explicit fixed-height card remains fixed until the caller asks for AutoHeight.
local card = UI:CreateCard(column, { title = "Display", columns = 2 })
card.flows[1]:Add(firstControl, { fill = true })
card.flows[2]:AddRow({ { child = secondControl, fill = true },
{ child = actionButton, align = "right" } })
card:AutoHeight()
UI:CreateConfigDialog also accepts resizable = true, minWidth/minHeight,
maxWidth/maxHeight and onResize(width, height, dialog). Resizing is opt-in;
the shared widget adapter applies bounds and a grip is created only when
supported. Dialog surfaces use Design, including its corner preference;
square explicitly overrides it. Consumers own any requested size persistence.
UI:CreateSwitch(parent, opts) returns a db-bound on/off button. Its entire
element, including the label and surrounding hit area, toggles on click; the
smaller inset square is visual only. UI:CreateCheckbox(parent, text) returns
an unbound 18px checkbox row with .checkbox and .label, for consumers that
manage their own state callbacks. Clicking the text calls the checkbox's current
native click path once, including its checked-state change and OnClick
handler. Disabled checkboxes reject text clicks. The text hit target follows
the label's bounds and does not cover adjacent inline controls.
UI:CreateButton(parent, opts) → Button
The table form accepts text, width, height, tooltip, tooltipBody,
onClick, onEnter, and onLeave. Hover callbacks run after the framework's
skin/tooltip behavior and are failure-isolated. The positional form
UI:CreateButton(parent, text, width, height) remains supported.
local button = UI:CreateButton(parent, {
text = "Preview",
onClick = function(button, mouseButton) SelectPage() end,
onEnter = function(button) ShowPreview() end,
onLeave = function(button) RestorePreview() end,
})
Script-composition quirk: WoW SetScript("OnEnter", ...) or
SetScript("OnLeave", ...) replaces the framework's hover styling and tooltip
handler. Use the table callbacks above, or HookScript when extending an
existing button. SetScript("OnClick", ...) remains available for legacy
click behavior; table onClick adds failure isolation.
UI:SetResizeBounds(frame, minWidth, minHeight, maxWidth, maxHeight) → boolean
Apply resize limits to a caller-owned frame through the client's supported
widget API. Prefer the combined SetResizeBounds; older clients can use the
paired setters when both maximum dimensions are supplied. Returns false
when the requested limits cannot be applied. Does not enable resizing, create
a grip, persist dimensions, or grant permission to modify protected frames.
Options panels with a banner expose panel.bannerFrame.divider and
panel.bannerFrame.dividerGap. Banner controls can anchor above the divider
using that gap to mirror the first tab row below it, independently of any
consumer content-frame insets.
UI:CreateSlider(parent, opts) → Frame
Create a horizontal slider control bound to a storage table — it saves and restores its value.
Parameters:
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
parent |
Frame | Yes | — | Parent frame |
opts.key |
string | Yes | — | Storage key the slider reads/writes |
opts.storage |
table | No | {} |
Table holding storage[key] (usually your db) |
opts.label |
string | No | key | Label text |
opts.min / opts.max |
number | No | 0 / 100 | Range |
opts.step |
number | No | 1 | Step increment |
opts.default |
number | No | min | Value when storage is empty; Reset target |
opts.suffix |
string | No | "" |
Appended to the displayed value, e.g. "%" |
opts.width |
number | No | 200 | Track width |
opts.progress |
boolean | No | true |
Show the primary-colored fill behind the thumb; false for a bare track |
opts.valueDisplay |
string | No | "always" |
"always" keeps the value above the track, "hover" shows only the hover value, "none" hides both |
opts.onChange |
function | No | — | onChange(value) |
The thumb re-positions itself on
OnShow, so a slider built on a panel that is still hidden (login/load) lands at the correct spot the first time the panel opens — no set/reset needed.
local slider = UI:CreateSlider(parent, {
key = "scale", storage = MyAddonDB,
label = "Scale", min = 50, max = 150, step = 5,
default = 100, suffix = "%",
onChange = function(val) MyFrame:SetScale(val / 100) end,
})
UI:CreateToggle(parent, opts) → Frame
Create a checkbox toggle bound to a storage table. Both the checkbox and its text activate the same persisted change callback; Reset restores the default.
Parameters:
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
parent |
Frame | Yes | — | Parent frame |
opts.key |
string | Yes | — | Storage key |
opts.storage |
table | No | {} |
Table holding storage[key] |
opts.label |
string | No | key | Label text beside the checkbox |
opts.default |
bool | No | — | Value when storage is empty; Reset target |
opts.onChange |
function | No | — | onChange(checked) |
local toggle = UI:CreateToggle(parent, {
key = "notifications", storage = MyAddonDB,
label = "Enable Notifications", default = true,
onChange = function(checked) ... end,
})
UI:CreateProfilesPanel(parent, opts) → Frame (card)
A complete profile manager bound to an existing framework database owner:
title band with icon, intro line, and a card with an active-profile highlight
box (name + current character), a profile dropdown with count, and stacked
Create/Rename/Reset/Copy buttons, with a confirmed Delete action beside each
non-Default profile in the dropdown. Create and Rename enter the name in
a small dialog; Copy auto-generates a name; duplicates are rejected, the
Default profile cannot be renamed or deleted, and failures are isolated
to panel feedback instead of breaking the UI.
Parameters:
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
parent |
Frame | Yes | — | Parent frame |
opts.db |
database | Yes | — | Framework database owner |
opts.title |
string | No | "Profiles" |
Title band and card title |
opts.height |
number | No | 214 |
Profile-management card height; allow room for all four action buttons |
opts.icon |
string | No | — | Title-band texture path |
opts.description |
string | No | "Create, switch, and manage your saved profiles." |
Intro line |
opts.onChange |
function | No | — | onChange(activeName, db) after any change |
opts.createFromCurrent |
boolean | No | false |
Create deep-copies the current profile, including nested colors; otherwise creates from defaults |
opts.pagedPresets |
boolean | No | false |
Paginates only the Presets card; navigation lives in its header; exposes card.presetsCard.pager |
opts.presetsPerPage |
number | No | 3 |
Presets per page, a positive multiple of three; 6 gives two rows per page |
opts.presets |
table | No | — | Array of { name, description?, disabled?, ... }; disabled entries are future slots that cannot apply settings; hidden when empty |
opts.onPreset |
function | No | — | Required with presets: onPreset(preset, db) applies one; failures isolated |
local card = UI:CreateProfilesPanel(tab, {
db = MyAddon.db, title = "Profiles", icon = MY_ICON,
onChange = function(active) MyAddon:Refresh() end,
presets = { { name = "Raid", mode = "raid" } },
onPreset = function(preset, db) MyAddon:ApplyPreset(preset) end,
})
-- card:Refresh() re-syncs the panel; card.dropdown/status/buttons,
-- card.activeInfo, and card.openNameDialog expose the seams for tests
-- and advanced use.
UI:CreateLabel(parent, opts) → FontString
Create a styled label using the theme's named sizes and colors.
Parameters:
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
parent |
Frame | Yes | — | Parent frame |
opts.text |
string | Yes | — | Label text |
opts.size |
string | No | "normal" |
"small" | "normal" | "large" |
opts.color |
string | No | "normal" |
"normal" | "muted" | "accent" | "red" | "green" | "yellow" (theme tokens) |
opts.width |
number | No | — | Enables word wrap at this fixed width — required for long text, which otherwise renders past the parent frame's edge on a single line |
opts.wrap |
boolean | No | false |
Enables word wrap without a fixed width — flow layouts clamp the label to the row and supply the width |
opts.justify |
string | No | "LEFT" |
Horizontal justify (only with width or wrap) |
local hint = UI:CreateLabel(parent, {
text = "A long descriptive sentence that needs to wrap inside the panel.",
size = "small", color = "muted", width = 340,
})
UI:CreateColorPicker(parent, opts) → table
Create a color swatch control that opens the ColorPicker on click.
Parameters:
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
parent |
Frame | Yes | — | Parent frame |
opts.label |
string | No | — | Label text |
opts.value |
table | No | {1,1,1,1} | Initial color {r,g,b,a} |
opts.onChange |
function | No | — | onChange(r, g, b, a) callback |
Returns: { frame, swatch, label }
local cp = UI:CreateColorPicker(parent, {
label = "Background Color",
value = {0.1, 0.1, 0.2, 1.0},
onChange = function(r, g, b, a)
myFrame:SetBackdropColor(r, g, b, a)
end,
})
The returned control exposes :Refresh(), which re-reads storage[key] and
repaints the swatch; it runs before the picker opens and on show, so a peer
that wrote storage directly is reflected without a stale swatch.
UI:CreateColorPickerCard(parent, opts) → Frame
The embeddable color picker — the full honeycomb spectrum + vertical
brightness bar + live Class Colors row + preview + hex inline as a card, for
placing directly in an options tab (instead of the popup swatch). Multi-instance
with its own state; bound to storage[key] = {r,g,b}.
Click or drag the honeycomb and brightness bar to pick.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
opts.key |
string | No | — | Storage key holding {r,g,b} |
opts.storage |
table | No | {} |
Table the widget reads/writes |
opts.default |
table | No | white | {r,g,b} when storage is empty |
opts.width |
number | No | 220 | Card width |
opts.border |
table | No | design token | Brand edge {r,g,b} or {r=,g=,b=} |
opts.onChange |
function | No | — | onChange(r, g, b) on every change |
Returns the widget frame, with :SetColor(r,g,b) / :GetColor(). Call
:Refresh() to re-read storage[key] and repaint without writing storage or
firing onChange (it also re-projects on show), so peer or theme changes are
reflected.
local card = UI:CreateColorPickerCard(container, {
key = "accent", storage = MyAddonDB, default = { r = 1, g = 0, b = 0 },
onChange = function(r, g, b) MyAddon:SetAccent(r, g, b) end,
})
UI:CreateColorSettingControl(parent, opts) → table
Color swatch + label bound to a saved variable. Changes write directly to storage[key].
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
parent |
Frame | Yes | Parent frame |
opts.label |
string | Yes | Label text |
opts.storage |
table | Yes | Saved variable table |
opts.key |
string | Yes | Key within storage |
opts.onChange |
function | No | Additional change callback |
local ctrl = UI:CreateColorSettingControl(parent, {
label = "Bar Color",
storage = MyAddonDB.profile,
key = "barColor",
})
UI:CreateStatusBarDropdown(parent, opts) → table
Statusbar texture selection dropdown. Delegates to the Textures module.
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
parent |
Frame | Yes | Parent frame |
opts.label |
string | No | Label text |
opts.value |
string | No | Initial statusbar name |
opts.onChange |
function | No | onChange(barName) callback |
UI:CreateFontDropdown(parent, opts) → table
Font family selection dropdown. Delegates to the Fonts module.
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
parent |
Frame | Yes | Parent frame |
opts.label |
string | No | Label text |
opts.value |
string | No | Initial font name |
opts.onChange |
function | No | onChange(fontName) callback |
UI:CreateFontSettingControl(parent, opts) → table
Font dropdown + reset button bound to storage[key]. Delegates to the Fonts module.
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
parent |
Frame | Yes | Parent frame |
opts.label |
string | Yes | Label text |
opts.storage |
table | Yes | Saved variable table |
opts.key |
string | Yes | Key within storage |
opts.onChange |
function | No | Additional change callback |
Options Panel Builder
UI:CreateScrollPage(parent, height) → canvas, scrollFrame
Create a clipped, scrollable canvas for option cards taller than the tab's
visible area. Position sections on the canvas; the canvas tracks the scroll
frame width. Classic-era's UIPanelScrollFrameTemplate supplies the scrollbar.
local page = UI:CreateScrollPage(content, 620)
local section = UI:CreateSection(page, { title = "General", height = 140 })
UI:CreateSection uses the same RGXDesign:CreateSection frame and
primary-bordered header that BLU uses; RGXUI handles geometry while RGXDesign owns textures
and themed colors. After adding controls to section.content, call
section:FitContent() to measure visible direct children and FontStrings
and size the card to them. Stacked cards anchored to its bottom follow the
resulting height. UI:CreateColumns(parent, 2, { titles = { "Options", "Layout" } })
returns card content frames; call left.card:FitContent() and
right.card:FitContent() once both columns are built.
UI:CreateCard(parent, { title = "Font" }) provides a section with a flow
layout. Anchor stacked cards to the previous card's bottom so FitContent()
or AutoHeight() can reflow the column. Use
UI:CreateColumns(parent, 2, { card = false }) for plain column hosts when
the cards themselves supply the section borders.
UI:CreateButtonGroup(parent, { "Left", "Right" }, { y = -8 }) centers a
styled button pair in its parent (default 84px buttons, 8px gap). Buttons
are available as group.buttons[1] and group.buttons[2].
UI:AnchorRowReset(row, reset, control) puts every reset at the same right
inset (8px) and centers it vertically on the control rather than its label.
Framework sliders and font dropdowns use this automatically.
UI:CreateCloseButton(parent, opts) → button
The framework's standard window close ("X") button, attachable to any
framework-built window, dialog, or panel so consumers never hand-roll window
chrome. Uses the native UIPanelCloseButton art on every supported flavor.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
parent |
frame | Yes | — | Frame the button attaches to (also default hide target) |
opts.width / opts.height |
number | No | 30 | Button size |
opts.point / opts.relativePoint |
string | No | "TOPRIGHT" |
Anchor points |
opts.relativeTo |
frame | No | parent | Anchor target |
opts.x / opts.y |
number | No | 0 | Anchor offsets |
opts.onClick |
function | No | hides parent |
Click handler |
opts.tooltip |
string | No | — | Hover tooltip text |
opts.hidden |
boolean | No | false | Start hidden (host owns the chrome) |
The button is also stored as parent.closeButton. CreateOptionsPanel uses
this factory for its own close button (hidden while the panel is embedded in
the Settings canvas, which owns its chrome there); pass
opts.closeButton = false to that panel to skip it.
UI:CreateCloseButton(myDialog, { point = "TOPRIGHT", x = -6, y = -6,
tooltip = "Close", onClick = function() myDialog:Hide() end })
UI:CreateConfigButton(anchor, opts) → button and UI:CreateConfigDialog(parent, opts) → dialog
The sub-menu configuration affordance: a small gear button attached to a control that opens a configuration dialog, so consumers never hand-roll "advanced settings" chrome.
UI:CreateConfigButton(anchor, opts) — gear icon (desaturated until hovered)
next to the anchor control:
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
anchor |
frame | Yes | — | Control the gear sits beside |
opts.point / opts.relativePoint |
string | No | "LEFT" / "RIGHT" |
Anchor points |
opts.relativeTo |
frame | No | anchor | Anchor target |
opts.x / opts.y |
number | No | 6 / 0 | Anchor offsets |
opts.dialog |
frame | No | — | Dialog shown by the default click action |
opts.onClick |
function | No | show opts.dialog |
Custom click handler |
opts.tooltip |
string | No | — | Hover tooltip text |
opts.width / opts.height |
number | No | 18 | Gear size |
opts.hidden |
boolean | No | false | Start hidden |
The button is published as anchor.configButton.
UI:CreateConfigDialog(parent, opts) — movable, design-skinned dialog frame
with the framework close button and an optional Reset action; consumers
populate the returned frame and show it from a config button:
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
parent |
frame | No | UIParent | Dialog parent |
opts.title |
string | No | "Configuration" | Header title |
opts.width / opts.height |
number | No | 420 / 260 | Dialog size |
opts.onReset |
function | No | — | Adds a Reset button when present |
opts.onShow |
function | No | — | Hooked to the dialog's OnShow |
opts.strata |
string | No | "FULLSCREEN_DIALOG" |
Frame strata |
opts.hidden |
boolean | No | true | Start hidden |
local dialog = UI:CreateConfigDialog(myPanel, { title = "Advanced", onReset = ResetAdvanced })
UI:CreateConfigButton(myToggle, { dialog = dialog, tooltip = "Advanced settings" })
UI:Confirm(opts) → dialog
The themed StaticPopup replacement: one shared confirm dialog with a
message plus Confirm/Cancel, ESC-closable, reconfigured on every call so no
consumer needs a StaticPopupDialogs table:
UI:Confirm({
title = "Reset All Settings", confirm = "Yes", cancel = "No",
message = "Are you sure you want to reset all settings to defaults?",
onConfirm = function() SQP:ResetSettings() end,
})
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
opts.title |
string | No | "Confirm" | Header title |
opts.message |
string | No | "" | Wrapped message text |
opts.confirm / opts.cancel |
string | No | "Confirm" / "Cancel" | Button labels |
opts.onConfirm |
function | No | — | Fires once on confirm (failure-isolated); dialog hides first |
opts.width / opts.height |
number | No | 340 / 160 | Dialog size |
For a single informational page without tab chrome, pass content = function(frame) ... end instead of tabs. Existing tabs = { ... } panels retain their tab
row and page lifecycle. The framework's AddOns settings category uses this
single-page form to link to RGX-Hello and list the available modules and tools.
UI:CreateOptionsPanel(name, opts) → panel
Create a full options panel with tab system, scroll container, and header.
Parameters:
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
name |
string | Yes | — | Panel name (used as frame name) |
opts.title |
string | No | name | Title text in header |
opts.subtitle |
string | No | "" | Subtitle text |
opts.width |
number | No | 800 | Panel width |
opts.height |
number | No | 600 | Panel height |
opts.version |
string | No | — | Version string shown in header |
opts.author |
string | No | — | Author string shown in header |
opts.website |
string | No | — | Website URL shown in header |
opts.closeButton |
boolean | No | true | Standard window close button (hidden while embedded in Settings) |
Returns: Panel object with methods below.
Panel Methods
Providing tabs
Tabs are supplied at creation through opts.tabs; each content(container)
builder runs once, the first time its tab is shown:
local panel = UI:CreateOptionsPanel({
addonName = "MyAddon",
tabs = {
{ text = "General", content = function(container)
UI:CreateToggle(container, { key = "enabled", label = "Enabled", storage = MyAddonDB })
UI:CreateSlider(container, { key = "scale", label = "Scale", min = 50, max = 150, storage = MyAddonDB })
end },
{ text = "Colors", content = function(container)
UI:CreateColorPicker(container, { key = "primary", label = "Primary Color", storage = MyAddonDB })
end },
},
})
Extending another addon's panel — RGX:AddOptionsTab(addonName, text, builder[, geom])
Register extra tabs onto an addon's panel by name, before it is built (i.e.
at file-parse time, from a second file). The declarative panel builder appends
them after the addon's own options tabs, so a bundled dev/test suite can live
on the addon's own panel instead of a separate window. geom optionally hints
panel width/height/maxPerRow; the largest hint across all registrations
wins. Panels are not rebuilt after creation, so this must run before the addon's
ADDON_LOADED.
RGX:AddOptionsTab("MyAddon", "Debug", function(container)
UI:CreateButton(container, "Dump State", 120, 24, DumpState)
end, { maxPerRow = 5 })
RGX:GetAddon(name) returns the addon object (with .panel) if you need it.
panel:Open()
Open the panel and navigate to it in Interface Options:
panel:Open()
panel:Close() / panel:Toggle()
Close() hides the floating panel or its Settings host through the framework's
safe-hide boundary. Toggle() closes a visible panel and otherwise opens it;
visibility includes the host so a hidden Settings window can reopen normally.
panel:SelectTab(index)
Switch to a tab by 1-based index:
panel:SelectTab(2) -- switch to Fonts tab
panel:SelectTabByName(name)
Switch to a tab by its name:
panel:SelectTabByName("Fonts")
panel:InvalidateAllTabs()
Mark all tabs for rebuild. Next time each tab is shown, its buildFn will be re-executed:
panel:InvalidateAllTabs()
panel:Refresh()
Force-refresh the currently visible tab:
panel:Refresh()
panel:SetTheme(config)
Apply the panel's colors at runtime. Repaints the accent line (accent color
only when the panel theme opts in with accentHeader = true, else primary),
version label and tab styling (primary color) from panel.theme, and
rebuilds/refreshes tab content under
that theme, without mutating the shared global theme (it is scoped and
restored, exactly like construction). Accepts the same named or array color
forms as Design:SetTheme:
panel:SetTheme({ primary = { r = 0.35, g = 0.75, b = 0.51 } })
Complete Options Panel Example
local UI = RGX:GetUI()
local panel = UI:CreateOptionsPanel("MyAddonOptions", {
title = "My Addon",
subtitle = "v1.0.0 by Me",
width = 800,
height = 600,
})
panel:AddTab("General", function(container)
UI:CreateToggle(container, {
label = "Enable Addon",
value = MyAddonDB.profile.enabled,
onChange = function(v) MyAddonDB.profile.enabled = v end,
})
UI:CreateSlider(container, {
label = "Update Interval",
min = 0.1,
max = 5.0,
step = 0.1,
value = MyAddonDB.profile.interval,
onChange = function(v) MyAddonDB.profile.interval = v end,
})
end)
panel:AddTab("Appearance", function(container)
UI:CreateFontDropdown(container, {
label = "Font Family",
value = MyAddonDB.profile.fontFamily,
onChange = function(v) MyAddonDB.profile.fontFamily = v end,
})
UI:CreateSlider(container, {
label = "Font Size",
min = 8,
max = 24,
step = 1,
value = MyAddonDB.profile.fontSize,
onChange = function(v) MyAddonDB.profile.fontSize = v end,
})
UI:CreateColorPicker(container, {
label = "Text Color",
value = MyAddonDB.profile.textColor,
onChange = function(r, g, b, a)
MyAddonDB.profile.textColor = {r, g, b, a}
end,
})
UI:CreateStatusBarDropdown(container, {
label = "Bar Texture",
value = MyAddonDB.profile.barTexture,
onChange = function(v) MyAddonDB.profile.barTexture = v end,
})
end)
-- Register with WoW
InterfaceOptions_AddCategory(panel.frame)
-- Open from slash command
SLASH_MYADDON1 = "/myaddon"
SlashCmdList.MYADDON = function()
panel:Open()
end
Layout Notes
- Framework scroll pages are clipping viewports with mouse-wheel scrolling; native scrollbar widgets are not created or shown. Canvas width uses the content area rather than reserving a visible-bar gutter.
- Numeric and discrete volume slider thumbs refresh after show and geometry changes, reading the current bound storage; redraw does not overwrite settings.
- Controls are positioned automatically within the scroll container
- Each control is anchored below the previous one
- Use
container(the scroll child) as the parent for all controls - The scroll container handles overflow automatically
- Tab content is built lazily on first show and cached unless invalidated
Definition Round Trip (Source Development Slice)
This first editor slice is available on its feature source branch, not in the
published v2.7.9 addon. It edits one plain-text label definition, using one
canonical schema (contract/schemas/rgx-definition.schema.json) and equivalent guarded
Lua/runtime and pure JavaScript/tooling implementations. It does not implement
the complete Trigger/Conditions/Display/Actions/Load model, a Studio shell, or
Blizzard Edit Mode registration.
local definition = {
version = 1, kind = "label", id = "example", text = "Hello RGX",
enabled = true, x = 0, y = 0, scale = 100,
}
local UI = RGX:GetUI()
local editor, err = UI:CreateDefinitionEditor(parent, {
definition = definition,
onSave = function(saved)
addon.db.labelDefinition = saved -- persist through the consumer's RGX DB
end,
})
All fields are required. id is an ASCII identifier (letter followed by up to
47 letters/digits/underscores/hyphens); text is valid UTF-8 of at most 256 bytes
without ASCII controls; enabled preserves explicit false; x/y are integer
offsets in -500..500 (positive y is upward); scale is integer percent in 25..300.
The model owns coordinates and scale, not pixel-identical fonts across platforms.
CreateDefinitionSession owns independent saved and draft copies. Patch/Import
change only a successfully validated draft. Save calls a synchronous onSave
with an independent copy: nil/no return or true accepts; false, another return,
or an error rejects. Callback side effects are the callback's responsibility;
the session cannot roll back arbitrary consumer writes. Cancel and closing the
in-game control restore the last saved draft. Invalid visible fields block Save.
Getters return independent copies, not mutable session storage.
Import/export uses the data-only envelope
RGXD1|label|id|enabled-bit|scale|x|y|percent-encoded-UTF8-text.
Every text byte is encoded as %XX; no Lua parser, loadstring, code execution,
or live-game bridge is involved in imports. Unknown versions/fields, malformed
UTF-8/numbers, and inaccessible values are rejected without replacing the saved
definition. This is a transfer encoding, not a second authoring DSL.
Try Both Adapters
- Run
node tools/editor/serve.mjsfrom a source checkout, then openhttp://127.0.0.1:18790. The allowlisted static server is loopback-only and read-only. The browser stores an explicitly saved definition in localStorage. - Install a manifest-built feature runtime in the intended test client; run
/rgx editor. Save uses the framework's own DB leafRGXFrameworkDB.definitionExample; closing/reloading must restore the saved definition. Unsupported stored definitions are preserved and reported. - Change and export a browser draft; paste/import it in-game. Change it there, export, and import back into the browser. Both directions must preserve text, false, position and scale. Import is a draft operation; Save remains explicit.
- Test Save/Cancel, invalid fields, malformed imports, reopen/reload persistence, overflow/preview clipping, and unaffected panels in each affected client.
The in-game preview uses the existing RGX scroll viewport, sections, controls,
and options lifecycle. The ordinary InputBox template and user-input distinction
are verified in active Retail/Classic source mirrors under
Blizzard_SharedXML/Shared/InputBox/InputBoxTemplates.xml and .lua; legacy
Cataclysm/Forever visual behavior still needs client validation.
npm --prefix tools/ci run definition-check crosses the actual JS/Lua validators
and sessions in both directions, checks the JSON schema, exercises the real Lua
editor adapter with explicit widget seams, and verifies the static server routes.
Headless widget seams do not prove WoW geometry, protected-action behavior, or
actual browser rendering. Record those manual results under framework #27.
For AI-assisted authoring, rgx_edit_definition uses the same pure engine and
returns normalized data plus an interoperable export string. Its schema resource
is rgx://schemas/definition. It is optional source tooling and labels this slice
sourceOnly; it never writes files or executes generated actions.