View Lifecycle
This page describes the current public lifecycle contract. The production backend is in-process Ultralight 1.4.0. Older legacy-runtime shell and subprocess behavior does not apply.
States
A normal view moves through these application-visible states:
[not created]
|
| CreateView("page.html", onDomReady)
v
[loading]
|
| document becomes ready -> onDomReady is queued to the game thread
v
[ready + visible]
|
| Hide()
v
[ready + hidden]
|
| Show()
v
[visible + unfocused]
|
| Focus(...) or FocusOverlay(...)
v
[visible + focused]
|
| Unfocus()
v
[visible + unfocused]
|
| Hide()
v
[ready + hidden]
|
| Destroy()
v
[destroyed]
CreateView returns a PrismaView handle immediately. Loading continues asynchronously. Views begin visible, so menus that should not appear immediately normally call Hide() after creation.
Creation
PrismaView view = api->CreateView("MyPlugin/page.html", OnDomReady);
if (view != 0) {
api->Hide(view);
}
Create the view when your plugin is ready to own it, commonly from kPostLoadGame / kNewGame or another bounded game-state event after you have acquired the Prisma API.
case F4SE::MessagingInterface::kPostLoadGame:
case F4SE::MessagingInterface::kNewGame:
if (g_api && (g_view == 0 || !g_api->IsValid(g_view))) {
g_view = g_api->CreateView("MyPlugin/page.html", OnDomReady);
if (g_view) {
g_api->Hide(g_view);
g_api->RegisterConsoleCallback(g_view, ConsoleCallback);
g_api->SetViewRole(g_view, PRISMA_UI_API::ViewRole::kPanel);
}
}
break;
Do not assume an old handle remains valid across arbitrary world/session transitions. Use IsValid() when reusing a stored handle.
DOM-ready callback
static void OnDomReady(PrismaView view)
{
g_api->RegisterJSListener(view, "onClose", OnClose);
g_api->RegisterJSListener(view, "requestData", OnDataRequest);
// Legacy RegisterTranslations injects into the current live document.
g_api->RegisterTranslations(view, "MyPlugin_F4");
// Run translation/listener-dependent page initialization after registration.
g_api->Invoke(view, "window.init && window.init()");
}
The framework queues OnDomReadyCallback onto the main game thread.
For consumer code, registering page-facing listeners in OnDomReady is the safest pattern because the document and JS environment are known to exist. The runtime accepts listener registration for a valid view, but registering after DOM readiness avoids depending on backend timing.
RegisterTranslations belongs here as well. The legacy path builds the translation helper script and sends it to the current document through Invoke; it is not a pre-document injection hook.
Do not depend on Invoke() succeeding before the page has a live JavaScript context.
Callback threading
The public callback paths are marshalled through the verified framework callback path:
OnDomReadyCallbackJSCallbackreturned byInvokeJSListenerCallbackregistered withRegisterJSListenerJSListenerCallbackregistered withBindUIEventConsoleMessageCallback
That means normal RE::* access is safe inside those callbacks without adding a second F4SE::GetTaskInterface()->AddTask solely to reach the game thread.
g_api->RegisterJSListener(view, "queryPlayer", [](const char*) {
auto* player = RE::PlayerCharacter::GetSingleton();
if (!player) return;
const auto hp = player->GetActorValue(RE::ActorValue::kHealth);
g_api->InteropCall(g_view, "setHealth", std::to_string(hp).c_str());
});
If your own worker/network thread calls into game state, you still must dispatch that work appropriately. The guarantee above applies to callbacks delivered by PrismaUI.
Show, Hide, Focus, and Unfocus
Visibility and focus are separate:
| Operation | Meaning |
|---|---|
Show(view) | Include the view in normal on-screen presentation |
Hide(view) | Stop on-screen presentation while keeping the view alive |
Focus(view, ...) | Give the view normal Prisma input ownership |
FocusOverlay(view, ...) | V10 overlay-focus path used with selective input regions |
Unfocus(view) | Release Prisma input ownership from that view |
Typical panel toggle:
void OpenPanel()
{
if (!g_api || !g_api->IsValid(g_view)) return;
g_api->SetViewRole(g_view, PRISMA_UI_API::ViewRole::kPanel);
g_api->Show(g_view);
g_api->Focus(g_view, true, false);
}
void ClosePanel()
{
if (!g_api || !g_api->IsValid(g_view)) return;
g_api->Unfocus(g_view);
g_api->Hide(g_view);
}
Only one Prisma view owns normal framework focus at a time. GetFocusedView() identifies it and HasAnyActiveFocus() gives the older yes/no form.
View roles and panel coordination
Any view that behaves as an interactive panel should declare a V10 role:
g_api->SetViewRole(g_view, PRISMA_UI_API::ViewRole::kPanel);
Passive HUD-style views should use kWidget. Undeclared views are not counted as panels by IsAnyPanelVisible(), so leaving an interactive panel at kUnspecified can cause other Prisma mods to open over it.
Before opening your own panel:
if (g_api->IsAnyPanelVisible(g_view)) {
return;
}
V10 selective input regions
FocusOverlay and SetInputRegions support interaction with only selected rectangles instead of making the entire view surface an input blocker.
This behavior is capability-gated. Check it first:
if (PRISMA_UI_API::HasPrismaCapability(
PRISMA_UI_API::PrismaCapability::InputRegions)) {
const PRISMA_UI_API::InputRegion regions[] = {
{ 80, 80, 500, 300 },
};
if (g_api->SetInputRegions(g_view, regions, 1)) {
g_api->FocusOverlay(g_view, false, false);
}
}
See FocusOverlay and SetInputRegions.
Escape ownership
SetViewOwnsEscape(view, true) opts a focused view into receiving Escape instead of leaving it to the game's normal path.
g_api->SetViewOwnsEscape(g_view, true);
Then handle it in the page and call your normal close listener:
document.addEventListener('keydown', (event) => {
if (event.key === 'Escape') {
event.preventDefault();
window.onClose();
}
});
Only enable this on a view that actually handles Escape.
Multiple views
Each Prisma view has its own handle and page state. Do not use the retired shared-legacy-runtime-shell model to reason about current PrismaUI.
Ordering is controlled with SetOrder / GetOrder:
api->SetOrder(backgroundView, 0);
api->SetOrder(popupView, 10);
For most plugins, prefer one view with internal routing for related screens. Use multiple views for genuinely separate surfaces such as a persistent widget plus an independent panel.
Hidden and offscreen views
Hide() controls normal 2D presentation. V5 offscreen rendering is a separate mode used for texture handoff / mesh-oriented consumers.
Use SetViewOffscreen(view, true) and the related V5/V8 APIs only when you specifically need an offscreen texture. Do not confuse offscreen mode with an ordinary hidden panel.
Mesh binding remains an explicit advanced integration; on-mesh behavior should be treated as an explicit advanced integration rather than a requirement for ordinary HTML overlays.
Health and recovery
V8 exposes GetViewHealth() for framework-observed view state:
kCreatingkDomReadykLivekLoadFailedkDomReadyTimeoutkUnresponsivekJsError
Use it when you need diagnostics or a bounded recovery policy. If a consumer decides a view should be recreated, destroy the old handle and create a new view rather than continuing to call a dead handle.
Inspector compatibility methods
Current builds ship a packaged in-game Ultralight inspector opened with F12 when DevTools are enabled.
The V1 inspector methods remain in the ABI for compatibility and are not the control surface for the current F12 DevTools workflow. Do not use retired remote-debugging-port or external legacy-runtime instructions.
For diagnostics, combine the F12 inspector with RegisterConsoleCallback, PrismaUI_F4.log, and view-health reporting.
Scrolling-size compatibility methods
GetScrollingPixelSize and SetScrollingPixelSize remain in the V1 ABI but remain compatibility methods and should not be used as a primary layout API.
GetScrollingPixelSizereturns0and logs a warning.SetScrollingPixelSizeleaves the view unchanged and logs a warning.
Normal browser/page scrolling still comes from the page and input events. Do not build new behavior around the V1 pixel-size compatibility calls.
Destruction
Destroy() tears down the view and invalidates its handle.
if (g_api && g_api->IsValid(g_view)) {
g_api->Unfocus(g_view);
g_api->Hide(g_view);
g_api->Destroy(g_view);
}
g_view = 0;
The framework also clears per-view bookkeeping such as roles, input regions, escape ownership, callbacks, and offscreen tracking when the view is forgotten.