Handler lifetime
Which task runs a GUI handler, how long it lives, and how it should end.
The one rule
A GUI handler belongs to the task that built the widget — not to the screen it appears on. Everything on this page follows from that.
That rule is invisible while you are writing a normal console, because there the
builder is the console's main GUI task. It starts to matter the moment part of
a screen is painted by a different task — task_schedule,
sub_task_schedule or gui_sub_task_schedule — which is exactly what you
reach for when a panel gets big enough to split up.
The forms
| Form | Which task runs it | When it dies | Takes over the GUI task? | How it should end |
|---|---|---|---|---|
on gui_message(w): / on gui_click(w): block |
the builder, as an inline block | with the build that registered it | no | falls off its end, or jump |
on change <expr>: block |
the builder, as an inline block | with the build that registered it | no | falls off its end, or jump |
on signal <name>: block |
the builder, as an inline block | with the build that registered it | no | falls off its end, or jump |
on_press=<label> |
a sub-task hosted by the GUI task | when it ends, or with the build | only if it reaches await gui() |
->END |
on_press=<callable> |
no task — plain Python | n/a | no | return |
gui_message_callback(w, fn) |
no task — plain Python | n/a | no | return |
gui_message(w, label) |
a sub-task hosted by the GUI task | when it ends, or with the build | only if it reaches await gui() |
->END |
* / + inside await gui() |
the GUI task itself | when the await gui() resolves |
it is the GUI task | jump, or fall through |
The two Python forms are the only ones with no task in the path at all. When you want a handler that cannot be affected by any of this, use one of those.
Handlers are owned by the BUILD, not by the builder
An on ...: block lives as long as the GUI build that registered it, and dies
when the next build replaces it. Its position in the label does not matter
— a handler written above the first widget lives exactly as long as one
written below it.
The same is true of a handler registered by a scheduled task that has since finished. The builder is woken to run its own block, and ends again afterwards:
== console_body ==
gui_section("area: 5,5,95,95;")
await task_schedule(build_panel) # paints, then ends
await gui()
== build_panel ==
gui_row()
b = gui_button("Fire")
on gui_message(b): # still runs, after the builder ended
fire_torpedo(SHIP_ID)
->END
A builder that CRASHED is not woken
Reviving a task that failed part-way would re-run it from a state it never
reached, so a crashed or cancelled builder stays dead. When a click cannot
be delivered for that reason, a warning naming the source site goes to
mast.runtime.log. Check that file — the verdict of a headless run
will still say PASS.
signal_register(name, label) — and the //signal/<name> routes that
compile to it — is a different thing, and is not GUI-transient. It
lives as long as its task does. Register one on every visit to a screen and you
have registered it several times.
How a handler label should end
A handler that does something and returns ends with ->END:
b = gui_button("Launch", on_press=launch_fighter)
== launch_fighter ==
hangar_launch_craft(SHIP_ID)
->END
A handler that paints a new screen needs nothing special. Build the widgets
and await gui(); the console follows you there:
b = gui_button("Buy", data={"item": key}, on_press=market_buy)
== market_buy ==
market_purchase(client_id, item)
jump market_screen # paints, then `await gui()` -- that is all
->END and the deprecated is_sub_task=False
With is_sub_task=False an on_press label is a jump on the GUI task,
so the handler is that task — and ->END ends the console, not the
handler. The screen goes dead. That is the historical behavior, and the
reason is_sub_task is deprecated: it made the correct ending depend on a
flag most scripters never knew was there.
Steering the GUI task from somewhere else
Reaching await gui() sends the console to whatever you just built, so a
handler that paints does not need to do anything else. Three cases still want an
explicit redirect:
| You want to | Use |
|---|---|
| send the console somewhere without going there yourself (a watcher loop that keeps looping) | gui_task_jump(label) |
| redirect one client's console from outside it | gui_reroute_client(client_id, label) |
| redirect every client, or the server | gui_reroute_clients(label) / gui_reroute_server(label) |
--- watch
await delay_sim(1)
->END if not object_exists(ship_id)
state = get_data_set_value(ship_id, "red_alert", 0)
if state != prev_state:
gui_task_jump("repaint") # the panel repaints; this loop carries on
jump watch
A finished GUI task cannot be jumped
gui_task_jump silently discards the jump if the target task has already
ended. That is the trap behind the ->END-after-map_start warning in
map_picker.
Splitting a screen across tasks
gui_sub_task_schedule(label) paints part of a screen from another task, and
tags it so it is cancelled when a new GUI is presented. Its handlers —
buttons and on change watchers alike — belong to the build, so they keep
working after the sub-task itself has finished.
== console_body ==
gui_section("area: 0,0,100,100;")
gui_sub_task_schedule(status_panel)
gui_sub_task_schedule(target_panel)
await gui()
== status_panel ==
gui_row()
t = gui_text(f"Hull {hull_pct(SHIP_ID)}%")
on change hull_pct(SHIP_ID):
t.update(f"$text:Hull {hull_pct(SHIP_ID)}%;")
->END
Watchers need time to fire
on change is polled once per tick. In a headless --test run the
exerciser sits on each console for well under a sim-second by default, so a
watcher keyed to a one-second change never fires and everything inside it
goes unverified. Pass --exercise-dwell 30.
Reacting to a signal: three ways
| Runs on | Lives for | Use when | |
|---|---|---|---|
//signal/<name> route |
one task per connected console, plus the server | the mission | reacting anywhere, per console |
//shared/signal/<name> route |
the server only, once | the mission | anything that spawns, saves, rewards or counts |
on signal <name>: block |
the builder | the GUI build | updating the screen you are building |
The block form is the GUI-scoped one. If your handler does anything other than paint, it belongs in a route — see Signals.
Every handler on a widget runs
A widget can carry as many handlers as you attach, in any mix of forms, and they all fire in registration order (which is source order):
b = gui_button("Go", on_press=fire)
on gui_message(b): # BOTH run
log("also me")
The gui_message_callback / gui_message_label family always runs first,
because the page walks the layout tree before it looks the tag up in its tag
map. A handler that raises is logged to mast.runtime and the remaining ones
still run. To replace rather than add, call gui_message_clear(widget)
first.
See also
- Runtime & Tasks — schedulers, tasks and the tick loop
- GUI — building the layout itself
- Signals — routes vs
signal_next - execution API —
task_schedule,sub_task_schedule,gui_sub_task_schedule,gui_task_jump