Builtin Library
README.md
edui.app (module)
The edui reactive core. An App owns one ui.* screen and turns the
string-callback-id protocol into ordinary closures.
ui.* dispatches interaction through string ids because closures cannot cross
the FFI boundary: a widget carries props.onClick = "<id>", and the screen's
registered env receives onCallback(id, data). An App hides that — during a
build it allocates an id per closure and records id→closure, and its single
onCallback looks the id up, calls the closure, and rebuilds.
Surface
new(screenName, opts?) -> App—opts.layeris a render-layer mask applied viaui.setScreenRenderLayeron first register;opts.callbackKeyoverrides the env key.App:mount(builderFn)— register the callback env, run the first build.builderFn(app) -> widgetTreeis re-run on every rebuild.App:cb(closure, key?) -> id— the id to place in a widget'son*prop. Pass a stablekeyso the id survives a rebuild, which is required for anything the renderer tracks by id (focus, drag,ui.widgetState).App:dispatch(id, data) -> handled— what the broadcast calls. Returns false for an id belonging to another surface so a host can keep routing.App:markDirty()/App:rebuild()— a rebuild requested during a build is coalesced into a single follow-up, so a handler that mutates state and a builder that reads it never recurse.App:unmount()— release the env and unregister the screen.current()— the app whose builder is running right now, or nil outside a build. Theeduiprimitives read this, which is why an author passes a closure and never threads the app through every call.
Builder errors are logged rather than thrown, so one bad build never wedges the editor.
local edui = require("modules.edui")
local app = edui.createApp("myPanel")
app:mount(function(a)
return {
type = "vertical",
children = {
{ type = "button", props = {
text = "Save",
onClick = a:cb(function() save() end, "save"),
} },
},
}
end)