system_tools
The editor's dock SERVICE. A visibility-driven Z.shell.dockApp host that
renders whatever panels are registered in the editor registry
(modules/api/editor/registry), sorted by each panel's order. It holds NO
hardcoded panel list — the panel set is entirely what has been registered, and
the dock seeds from the registry at mount and stays in sync via onChanged
(add/remove is live, no remount).
The shell's per-panel refresh scheduler gates every data fetch on
(window_visible AND panel_open AND refresh_due), so the dock costs zero engine
work when hidden / closed and polls only the panels the user opened.
The service owns a single named app "system_tools" with one main screen
(system_tools/main) plus an extraScreens.details floating window — a fixed
entity inspector that opens on selection from the entities panel (not a registry
tab).
Where panels come from
- Built-in tabs —
modules/api/editor/system_tools/panelsregisters the engine's own menus + tabs through the publicaddPanel/addMenuAPI. The editor entrypoint requires that module; the require IS the registration. - World panels — an
editorPanelasset whoseonRegister(self)hook callsaddPanel(seeassetTypes/editorPanel.assetType). Write a panel asset into the world → it appears, no restart.
Both paths end at editor.addPanel({ id, build, label?, menu?, order?, refresh?, onCallback?, onMount?, tick?, float? }). The dock renders each registered panel
as a dock tab; the top bar lists it under its menu in order.
Exports
M.mount(opts: MountOpts?) -> SystemToolsHandle— mount the dock service and return an opaque handle whoseupdate/onCallback/destroy/setVisiblethe hosting component drives each frame. On mount it publishes its open/close/toggle/isOpen controller into the registry (EditorRegistry.dock()) so the top bar + viewport camera can drive it; destroy clears it.
Module-level introspection:
M._APP_NAME: string—"system_tools".M._MAIN_SCREEN: string—"main".
The registered panel set is introspected through the registry itself
(EditorRegistry.panelsSorted() / EditorRegistry.list()), not through fields
on this module.
Types:
MountOpts = { include_agent: boolean?, include_graphics: boolean?, layer: string? }SystemToolsHandle = { state: any, update: (dt) -> (), onCallback: (id, data) -> boolean, destroy: () -> (), setVisible: (b) -> (), screenName: string }
A panel spec
editor.addPanel({
id = "profiler", -- unique key (dock tab + routing id)
label = "Profiler", -- dock tab title + menu entry label
menu = "debug", -- top-bar menu it opens from
order = 10, -- position within the menu / dock
refresh = 0.1, -- seconds between rebuilds when open
build = function(state) ... end, -- pure: returns widget
onMount = function(state) ... end, -- optional, run once on first open
onCallback = function(id, data, state) -> bool, -- optional, returns handled
})
build(state) should be pure — derive everything from state plus external
sources (use Cache.tracked for those). Per-panel persistent state lives at
state.tabs[<id>].
Mount API
local SystemTools = require("@builtin::modules.api.editor.system_tools")
local handle = SystemTools.mount({ include_graphics = true })
-- In your component:
function update(dt) handle.update(dt) end
function onCallback(id, data) return handle.onCallback(id, data) end
function onDestroy() handle.destroy() end
-- Visibility:
handle.setVisible(true) -- equivalent to ui.showScreen(handle.screenName)
handle.setVisible(false) -- equivalent to ui.hideScreen(handle.screenName)
Notes
- Work-gating compounds: window visibility, panel open-state, and per-section
collapse. Hidden window → zero engine work. Closed panel → no
buildand no fetches. Collapsed section → useZ.dataSource(fn, { gate, ttl }). - The handle's
statefield is the live reactive State. Treat it as opaque except where documented. - Adding a built-in panel: add a row to
panels.modulepointing at the tab module. Adding a world panel: write aneditorPanelasset. The next mount (or the liveonChangedreconcile) picks it up.
Scoped to this part · feeds back into the world's score.