Builtin Library
README.md
app
Layer 3 lifecycle wrapper for Z.app. Collapses the
register-or-update dance, owns multiple named screens, ticks reactive
state, and routes callbacks via an embedded Router. Per
docs/specs/ui-v3-architecture.md Layer 3. The instance is a callable
{ buildFn } plus a State reactive store and a Router callback
dispatcher; mount → tick → unmount drives ui.registerScreen /
ui.updateScreen / ui.unregisterScreen under the hood.
Exports
App.create(name: string, builderFn: BuilderFn) -> AppInstance— construct an App.App.current() -> AppInstance?— the App whose builder is currently rendering (used by widget builders that auto-stash state).
Per-instance methods (on AppInstance):
:mount(ctx: any) -> AppInstance— initial register; idempotent across re-mounts.:tick(dt: number?)— re-render dirty screens from the host'supdate(dt).:unmount()— unregister every owned screen and drop the token claims.:markDirty()/:isDirty() -> boolean— dirty bit control.:on(idOrPattern, handler) -> AppInstance— router subscription (chainable).:cb(id, handler) -> id— router callback registration; returns the id.:dispatch(callbackId, data?)— router dispatch.:set(key, value) -> AppInstance/:update(updates) -> AppInstance— state shortcuts.:_screensForTest()/:_routerForTest()— test introspection.- Internals (
_nextAnonId,_runBuilder,_register,_renderOne,_isRegistered,_isFirstRegister,_forgetScreen,_showAll) are exposed for use byZ.app's helpers but should not be called externally.
Types:
AppInstance— opaque App table carryingname,state, the embedded router, and registered-screen bookkeeping.BuilderFn = (state: any, ctx: any) -> { [screenName] = buildFn | dynamic }RenderOpts = { layer: number?, tags: { string }? }
Usage
local Z = require("@builtin::modules.zui")
local app = Z.app("inventory", function(state, ctx)
state:default("count", 0)
return {
main = function()
return Z.btn("count=" .. state:get("count"), "inc")
end,
}
end)
app:on("inc", function() app:set("count", app.state:get("count") + 1) end)
app:mount(self)
-- in update(dt):
app:tick(dt)
-- on destroy:
app:unmount()
Notes
- A per-process token-based stale-screen guard keeps a second App with
the same screen name from doubling-up
ui.updateScreenpushes. The newest mounted App wins; older instances skip their tick silently. - Closure-as-callback widgets (
Z.btn("Save", fn)) get stable per-screen ids via_nextAnonIdso re-ticks update the existing router entry instead of accumulating handlers. Z.dynamic(list, fn)values in the builder's return table are exploded into per-item screens named<base>-<item.id>; vanished items are unregistered automatically on the next tick.{ build, layer, tags }is the supported sugar for screens that needui.registerScreen's optional layer orZ.tags.setentries. Tags are written immediately after register so the editor toggles andZ.tags.findByTagsee them on the same frame.- A build that returns
nilmeans "this screen has nothing to draw this frame": the App hides the screen and shows it again on the first tick the build produces a tree. That is the whole of the App's claim on visibility — a screen hidden by anyone else (ui.hideScreen,Z.screens, the editor's F1 toggle viaZ.tags.hideByTag("editor")) stays hidden while the App keeps pushing tree updates on its refresh clock, and becomes visible again when that caller shows it. - Errors thrown by a builder propagate through
pcalland re-raise witherror(..., 0)so the screen name is preserved in the traceback.