Tier 3 — UI and layout¶
Replacing the EMS window with a layout of your own takes four pieces, all on GRIPEMS.API.UI:
- a layout provider that positions EMS's host frames and owns the chrome,
MountPanelto pull EMS's built-in panels — the sequence list, the editor, and the rest — into those hosts,SetClassicChrome(false)to hide the default two-panel chrome so yours shows instead,RegisterView/SetActiveViewfor navigation between screens.
A provider on its own gets you seven empty host frames and nothing to put in them. That was the limit of the first version of this API, and it's why a real reskin used to mean editing EMS's source. The panel-mount, chrome, and view methods close that gap: you position the hosts, mount the built-in content into them, hide the classic chrome, and drive your own nav.
EMS keeps its built-in "classic" provider — the two-panel window you see by default — and never removes it, so a user can always switch back.
One thing never moves: EMS's single in-window secure button (the Simplified Mode run button) stays parented to the window root, outside every host, so a layout can't taint it. Two panel ids, vehiclePet and metadata, are reserved with nothing to mount yet. Everything else is yours to place.
Host frames¶
The main window (GRIPEMS_MainFrame) carries seven plain, empty, hidden host frames. The classic layout ignores them and drives its own two panels; your provider positions and shows the ones it needs and mounts content into them.
| Host | Intended for |
|---|---|
navHost |
a navigation rail / sidebar |
listHost |
the sequence list |
metadataHost |
per-sequence metadata |
editorHost |
the editor body |
configHost |
a config sidebar |
globalViewHost |
global / about-style views |
iconFooterHost |
the preview icon strip / footer |
These are intended roles, not enforcement — a host is just a Frame. Use the ones that fit your layout.
Hosts are non-secure — keep them that way
Host frames are plain Frame objects, and a provider may reparent them, so no secure frame or secure button may ever live under a host. EMS keeps its one in-window secure button (the Simplified Mode run button) on the window root, outside every host, exactly so a layout plugin can't taint it. If you create secure templates in your plugin, parent them outside the hosts. This is the single most important safety rule on this tier.
API.UI:GetHost(name)¶
Returns the named host frame, or nil if the window hasn't been built yet (it's lazy-created the first time the user opens EMS). Resolve hosts inside your provider callbacks — by the time OnInitPanels runs, all seven exist.
The layout provider¶
A provider is a table with an id, a name, and a set of optional lifecycle callbacks. EMS calls the callbacks as plain functions with the arguments shown — they are not method-style, so don't expect self.
local provider = {
id = "acme_modern", -- must equal the id you register under
name = "Acme Modern", -- shown to the user
OnRegister = function(self) end, -- called once, at register time
OnInitMainFrame = function(mainFrame, hosts) end, -- before panels are built
OnInitPanels = function(mainFrame, hosts) -- build your chrome into hosts
local nav = hosts.navHost
nav:ClearAllPoints()
nav:SetPoint("TOPLEFT", mainFrame, "TOPLEFT", 0, -40)
nav:SetPoint("BOTTOMLEFT", mainFrame, "BOTTOMLEFT", 0, 0)
nav:SetWidth(160)
nav:Show()
-- ... build and parent your widgets into nav, hosts.editorHost, etc.
end,
OnApplyLayout = function(context) end, -- reposition on a layout pass
OnMinimize = function(mainFrame) end, -- collapse to title bar
OnRestore = function(mainFrame) end, -- expand again
}
Every callback is optional, but each must be a function if present. The id and name are required and must be strings, and the id field inside the table must equal the id you register under.
API.UI:RegisterLayoutProvider(id, provider)¶
Validates the provider against the contract above, stores it under id, and runs its OnRegister. Returns true, or false plus a reason — a malformed contract, a non-function callback, an id mismatch, or a duplicate id (it never overwrites an existing provider, including the built-in "classic").
API.UI:SetActiveLayoutProvider(id)¶
Makes id the active provider and persists it to the uiLayout setting. Returns false plus a reason for an unknown id. Call this when the user opts into your layout — not automatically on load, or you'll override their choice.
API.UI:GetActiveLayoutProvider()¶
Returns the active provider id. Falls back to the stored uiLayout setting, then to "classic".
Mount a built-in panel¶
API.UI:MountPanel(panelId, host) pulls one of EMS's built-in panels out of the classic chrome and into a host you control. This is what fills your layout with EMS's real content instead of leaving you to rebuild the sequence list and editor yourself.
Content panels reparent their content root into the host and pin it to fill. The mountable content panels:
| Panel id | What it is |
|---|---|
sequenceList |
the sequence list |
editor |
the sequence editor body |
variables |
the variables panel |
conditions |
the conditions panel |
macros |
the macros panel |
source |
the raw source view |
A few ids behave differently:
previewhands off to the Preview facade, the same asAPI.Preview:MountSidebar(host).options,import,export, andaboutare dialog-class. They open their existing standalone window and ignore thehostargument.vehiclePetandmetadataare reserved — known ids, but no panel is built for them yet, so mounting one returnsfalse.
Returns true, or false plus a reason for an unknown id, a reserved id, or a non-table host on a content panel.
Mount order doesn't matter
If you call MountPanel before the panel has been built (it's lazy, like the window), EMS records your host as a pending mount and applies it the moment the panel comes up. So you can mount from OnInitPanels without checking whether a given panel exists yet.
Disable un-mounts every panel your plugin mounted, returning each to the parent it had before — part of the reversibility guarantee.
Hide the classic chrome¶
API.UI:SetClassicChrome(enabled) shows or hides the default two-panel chrome — the left panel, the divider, the right panel, and the title bar — so your layout presents its own instead of sitting on top of EMS's.
API.UI:SetClassicChrome(false) -- hide the classic chrome
API.UI:SetClassicChrome(true) -- show it again
false hides; true (or nil) shows. The frames are only toggled, never reparented or destroyed, and their children hide and show with them. Returns false with a reason only when the window isn't built yet. If your plugin hid the chrome, disable shows it again for you.
Views and navigation¶
Classic EMS has no nav region — it's one window with two panels. A modern layout usually wants named screens (Sequences, Editor, Variables, and so on) with a way to switch between them. Views are that: you register the screens your layout offers, then tell EMS which one is active.
API.UI:RegisterView(id, def)¶
Registers a named view. def needs an id that matches the registry id and a name. A duplicate id is rejected. Classic registers no views; the registry is empty until your provider fills it.
API.UI:SetActiveView(id)¶
Makes id the active view and fires GEMS_UI_NAV_CHANGED with the id, so the rest of your UI (and any listener) can react. Rejects an unknown id. The active view is in-memory only — your provider rebuilds it each load, so it isn't persisted.
API.UI:GetActiveView()¶
Returns the active view id, or nil when none is set, which is the classic default.
Lifecycle and dispatch¶
The window is lazy-created the first time the user opens it. On that first open, EMS:
- builds
GRIPEMS_MainFrameand the seven hosts (empty, hidden), - resolves the active provider (default
"classic"), - calls
OnInitMainFrame(mainFrame, hosts), thenOnInitPanels(mainFrame, hosts), - fires
GEMS_UI_READY, - builds a read-only
contextand callsOnApplyLayout(context), then firesGEMS_UI_LAYOUT_APPLY.
The minimize button routes through your OnMinimize / OnRestore. If your OnInitPanels throws, EMS logs it and falls back to the classic layout, so a broken provider degrades to the stock UI rather than an empty window.
The context passed to OnApplyLayout¶
A read-only snapshot — scalars and IsShown booleans, no live frame handles:
| Field | Type | Meaning |
|---|---|---|
navId |
string | nil | active nav region (nil under classic) |
editorTab |
any | nil | the editor's active tab |
selectedSequence |
string | nil | currently selected sequence |
panelVisibility.left |
boolean | is the left panel shown |
panelVisibility.right |
boolean | is the right panel shown |
previewMode |
string | nil | current preview mode |
frameWidth / frameHeight |
number | nil | current window size |
Taking over the first paint (load order)¶
The active provider is resolved when the window is built, which is the first time the user opens it. So to own the first paint, your provider must be registered and active before that happens:
- Register at
PLAYER_LOGIN(EMS has loaded; the window usually hasn't been opened yet). - Persist the user's opt-in with
SetActiveLayoutProvider— because it writesuiLayout, the next time the window builds, EMS resolves your provider on its own.
If you register after the window already exists this session, EMS won't rebuild it underneath the user. Apply your layout on the next GEMS_UI_LAYOUT_APPLY, or let it take effect when they reopen or /reload.
Theme integration¶
API.UI:RegisterPanelFrame(frame, category, class) also lives on this sub-table. It's the Tier 5 hook for making your frames inherit the active EMS skin — see Tier 5 - Theme.