Every rule on this page exists elsewhere in the docs. This page puts them in one ordered timeline, because most first-project bugs are lifecycle bugs: querying too early, binding data at the wrong moment, or treating a render state as a logical one.
A destination mounted by a NavHost goes through these moments, in this order:
| Moment | When it fires | Use it for | Do not |
|---|---|---|---|
Awake | On spawn. NavHost pre-spawns every destination in the graph, hidden, at boot | Cache components, register non-UI callbacks | Query UXML elements, assume UIView exists, or run per-visit logic: hidden destinations spawn long before the player opens them |
OnUILoaded(root) / WhenUILoaded | The first PanelRenderer delivery once the view root resolves | Element queries, Fade event subscriptions, one-time wiring | Treat it as a per-visit data refresh: it fires once, not per push |
| Born visibility | Synchronously inside that same reload, before the first paint | Nothing: automatic. Start Visible nodes show, everything else hides | Synthesize show/fade callbacks for born-visible views. The host's boot seeding push already runs one real fade cycle; firing your own double-counts |
OnStateReset(args) | Before a reopen push (the 2nd and later), when the node sets Reset State On Reopen. The first-ever push does not fire it | Reset transient view state so the screen opens fresh | Assume it fires on the first push, or use it as the args-consume hook |
Fade.OnFadeInStart | Every fade-in, including the first | The per-visit refresh: read GetArgs<T>(), update labels, repopulate lists. See Typed Destinations | One-time wiring |
Fade.OnFadeIn | Fade-in complete | Focus side effects, analytics, entrance juice | Required data binding: too late, the player already saw the screen |
Fade.OnFadeOutStart / Fade.OnFadeOut | Closing | Tear down per-visit state | Treat a render occlusion as a close |
OnViewRootChanged(root) | A later reload whose resolved root differs (UXML live reload, enable cycles) | Re-query and re-attach: everything wired in OnUILoaded died with the old tree | Keep references to old VisualElements |
| Render occlusion | SetRenderOccluded from Overlay Opaque or tab switches | IUIViewRenderAware.OnRenderShown / OnRenderHidden on registered UI Actions, for controls that need repair after display: none (Luna's GridView does this) | Treat it as a logical close. The view stays open on its stack the whole time; see Occlusion |
There are two subscription surfaces, and mixing them up is a common source of confusion:
UIViewComponent subclass, or any external controller) subscribes to the events on component.Fade: OnFadeInStart, OnFadeIn, OnFadeOutStart, OnFadeOut. This is the pattern every built-in Luna controller uses.UIView subclasses override the protected virtuals of the same names on UIView. You rarely need this.Because OnUILoaded can run again after a live reload (via OnViewRootChanged re-wiring), always unsubscribe before subscribing:
protected override void OnUILoaded(VisualElement root)
{
_title = root.Q<Label>("title");
Fade.OnFadeInStart -= Refresh;
Fade.OnFadeInStart += Refresh;
}
protected override void OnViewRootChanged(VisualElement root) => OnUILoaded(root);UIViewComponent: it logs the resolved PanelRenderer chain, every fade start/complete, and enable/disable transitions.OnUILoaded; data that appears only on the second visit means args were read in OnUILoaded instead of Fade.OnFadeInStart; a screen that silently never appears usually means an unresolvable Parent Name (the console now logs this as an error).Settings
Theme
Light
Contrast
Material
Dark
Dim
Material Dark
System
Sidebar(Light & Contrast only)
Font Family
DM Sans
Wix
Inclusive Sans
AR One Sans
Direction