How your app runs
Your app's script runs once when the app starts; after that, DatumHue calls the callbacks your script registered, until something ends the app. What keeps an app running depends on whether it has a window.
The script and its callbacks
The script body is your app's start: it runs once, top to bottom, when
the app starts. Read the arguments it was launched with from
datumhue.args.argv. The body may wait on slow work, such as loading an
image or reading a file: it pauses there and goes on when the result is
ready (Waiting).
Everything after that happens in callbacks, functions your app hands to DatumHue to call later:
| Callback | Registered with | Runs |
|---|---|---|
| Per-frame handler | datumhue.on_update | once a frame, with the seconds since its last run |
| Timer | datumhue.time.after, datumhue.time.every | when its time comes, before that frame's per-frame handler |
| UI handler | a widget's on_click and its other on_ keys | when someone uses the widget |
| Subscription | datumhue.events.on, datumhue.messaging.subscribe | when the event or message arrives |
A timer's callback gets dt, the seconds the timer ran since its last
fire, or since it started for the first, leaving out any time it was
paused. A repeating timer that falls behind,
because a frame or a callback runs long or a callback waits for a
result, fires once, with a dt that covers the whole gap.
An app runs one callback at a time. Each runs to its end before the next one starts, so your app never sees one of its callbacks half done.
A callback that runs long goes on into the next frames. Until it ends, what someone types or presses on a gamepad waits instead of reaching your app's widgets, and DatumHue leaves your app's focus as the callback leaves it, even on a widget the callback hid. When the callback ends, the waiting input arrives in order, at the widget focused then. A call that waits for a result (Waiting) is a pause, not a long run: your widgets get input while it waits.
DatumHue picks the handlers that hear an event only between your app's
callbacks, never part-way through one, and the script body counts as a
callback here. So an event a callback causes, such as the app_spawned
of an app it starts, reaches a handler the callback subscribes after
causing it. A click someone makes while the callback runs, on a widget
the callback then removes, reaches no handler. A call that waits for a
result opens such a gap too, so subscribe before that call, not after
it.
UI apps and service apps
An app is either a UI app, with a window, or a service app, with none.
| UI app | Service app | |
|---|---|---|
| Window | a root container that holds its UI | none: the namespaces the reference marks "UI apps only" are missing |
| Started by | datumhue run app.lua, or a package | datumhue run --service app.lua, or a package with service #true |
| Ends | only when something ends it | also as soon as nothing is registered |
A UI app stays open with no callbacks at all: its window is reason enough. A service app lives only while it has something left to do. These count as registered:
- the per-frame handler;
- a timer that has fires left;
- a subscription: events, messages, document watchers, input feeds, and
anything else that returns a
Subscription; - a callback on something the app owns: a widget handler, a collision callback, an animation's callback, a route hook;
- a call that is still waiting for its result.
When the last of these is gone, the service app ends. This one ticks three times, then stops its timer, and with it the app:
local ticks = 0
local timer
timer = datumhue.time.every(0.2, function()
ticks = ticks + 1
print("tick " .. ticks)
if ticks == 3 then
timer:unsubscribe()
print("stopped: nothing is registered")
end
end)
In this example: datumhue.time.every
Run it as a service app, and it prints three ticks, then ends on its own:
$ datumhue run --service ticker.lua
How an app ends
An app ends when:
- it calls
datumhue.app.terminate; - an app that manages it calls
datumhue.wm.terminate_appon it; - the app that started it ends: ending an app ends every app it started, and every app those started;
- an error escapes one of its callbacks (below);
- one of its callbacks runs past the step limit it was started with, without pausing (Launch other apps);
- it is a service app with nothing registered.
Everything the app made goes with it: its window, its widgets, its
drawings and its timers. If it had focus, the frontmost app left takes
focus. The app that started it hears it end through the app_terminated
event (Launch other apps).
To save state on the way out, register a handler with
datumhue.on_terminate. It runs once, within a short time limit, before
the cleanup. It does not run after a crash, cannot stop the ending, and
does not keep a service app alive.
An app that ends itself then runs the __gc finalizers of the tables it
still has, once each, as Lua does when it closes, within a time limit of
their own as short as the handler's. An app that crashed, in its handler
too, or that another app ended runs none.
When a callback errors
An error that escapes any callback ends your app. The script body, the
per-frame handler, timers, UI handlers and subscriptions all follow this
one rule. The log names your app and shows the error with its traceback,
and the whole app ends as above, never just the part that failed. Other
apps are untouched, and when the app is the one datumhue run started,
the command exits with a failure code.
To survive an error, catch it where it can happen: call the risky part
with pcall inside the callback, and handle the result. A caught error
ends nothing. Some callbacks run on the spot, inside one of your own
calls, as a router's page builder does. An error in one of those raises
at that call, so a pcall around the call catches it too.
Sessions
Every launch has a session, an id that tells it apart from other
launches running at the same time. Every app of the launch reads it with
datumhue.app.session. A launch starts with a fresh session.
A launch can name its session with
--session, or take the most recent one again
with --continue. In a browser a launch is one
tab: its session lasts through reloads of the tab and ends when the tab
closes.
The app window
A UI app's window is its root container. A window manager places,
sizes and stacks it, and nothing your app shows reaches outside it. Inside
is the content area, where your UI lives: everything you build on
datumhue.ui.root, and every canvas or image you mount without naming a
parent, lands there.
root container placed by the window manager, clips its contents
+-- content area fills the root container
+-- your panels, labels, buttons, canvases ...
datumhue.screen works on the window of the app that calls it. The root
app, usually the window manager, drives the whole workstation window; any
other app drives its own root container:
| Call | From the root app | From any other app |
|---|---|---|
datumhue.screen.fullscreen | the workstation window goes full screen | asks the window manager to maximize its root container |
datumhue. | asks for a workstation window size | asks for a root container size |
datumhue.screen.width, datumhue.screen.height | the workstation window's size | its root container's size |
A request is a proposal: the window manager answers it, and the size properties report what is in effect. Fit any window size shows how to follow size changes, and Write a window manager or launcher how windows get their frames.
See also
Reference used on this page: datumhue.on_update datumhue.time.after datumhue.time.every datumhue.events.on datumhue.messaging.subscribe datumhue.screen.fullscreen datumhue.screen.request_resolution datumhue.screen.width datumhue.screen.height datumhue.args Subscription:unsubscribe Timer:unsubscribe datumhue.app.terminate datumhue.wm.terminate_app datumhue.on_terminate datumhue.app.session datumhue.ui.root datumhue.screen