NarraLeaf

Surfaces

Screens. The two kinds, the navigation stack, the enter and exit lifecycle, and how the game stage is composed from them.

A Surface is one screen: a root element, a design size, and the element tree underneath. Every screen in a game is a Surface — the title menu, the settings panel, the dialog box the story speaks through. A project keeps all of them in one UI document, and the runtime shows them two ways: stacked on a navigation stack, or mounted into a slot of the running game stage.

The order things are drawn in on screen, and how it changes as a game starts, a page opens and the game ends, is laid out step by step in Interfaces at run time. This page is the reference for the rules behind it.

The two kinds

KindhostWhere it showsReach for this when
appSurfaceappas a page on the page stack, or as a layer over the pagesyou need a screen the player opens and leaves: title, settings, saves, gallery.
stageSurfaceplayerin one slot of the running game stageyou need to replace part of the running game's own interface: the dialog box, the choice menu, the notification stack.

There is no third kind. Both records carry id, name, designSize, rootElementId, optional settings (background color, background image and page animation) and optional actions (the project's input actions it answers); a stageSurface additionally carries a mount naming its slot, and is created with a transparent background because it sits over the stage.

Studio's UI manager labels them Page and Game UI, and the node names follow that labelling — Go Page opens an appSurface.

Design size

designSize belongs to each Surface, not to the project. It is the coordinate system you author in: layouts are written in design pixels and the runtime scales the whole tree to the window, letterboxing to keep the aspect ratio.

  • The active Surface's design size sets the viewport box. The entry Surface's design size is also the game window's initial size.
  • Creating a Page offers a canvas size, prefilled from a Surface already in the document. Creating a Game UI offers no size field — it takes the project's design size, because it has to share the stage's coordinate system.

The NarraLeaf stage is built at the design size of whichever Surface is active at the moment the game session mounts — not from the project setting, and not from the Game UI surfaces filling its slots. Whatever design size the Page your player presses Start Game on happens to have, the stage gets.

The navigation stack

Navigation is two stacks: pages, opened by Go Page, and layers, shown by Show Layer. Every layer is drawn above every page. Each entry on either stack is an instance of a Surface, not the Surface itself: it carries its own props and its own runtime scope. Open the same Surface twice and you get two independent instances that share nothing.

  • On boot the page stack holds a single page: the entry page.
  • Opening pushes. Go Page pushes the selected Page onto the page stack with the params and props its inputs give, and the new page replaces the one on screen. Replace Page opens a page the same way and then removes the page it replaced from the stack, so neither Go back nor Go Page with None returns to it. Show Layer pushes onto the layer stack and hands back a handle naming that showing of it: the value Hide Layer, Wait For Layer and Is Layer Mounted address. Nothing else names a layer: stacking order is mount order, and no screen can be written to depend on sitting at a particular depth. A layer sits above every page, including pages opened after it.
  • Modal layers. A modal layer takes input from everything beneath it and holds the keyboard. A layer that is not modal leaves the page beneath it working. Layers that claim the same group queue rather than stack, and a layer closes on its own when the Surface that showed it closes.
  • Five ways to close. Go back closes the top layer when that layer is dismissible, and otherwise pops exactly one page. Go Page with its Page selector set to None empties the page stack down to its first page. Clear Page closes only the pages opened during a running game, and does nothing outside a game. Hide Layer closes the layer its handle names, and Close This Layer closes the layer the running blueprint is inside, optionally with a result for whoever is waiting on it.
  • The floor stays. Go back, Go Page with None and Clear Page never close the first page on the page stack. When the game launches it is the entry page; after Quit Game it is the page Quit Game opened. Replace Page run while it is the page on screen puts the page it opens in its place, which is how a splash page hands over to the title page. Go back at the bottom of the stack is a no-op rather than an error. Signatures are on App nodes.

Presentation

Every page is either appPage or gameOverlay. The runtime picks, not the author:

  • Before a game is running, Go Page opens appPage pages. Only the top page is drawn; the pages beneath it stay on the stack for Go back.
  • Start Game and Load Save hide every page currently on the stack, close every layer, and reveal the game stage. The stack still holds the pages; they simply stop rendering.
  • While the stack is hidden, Go Page opens gameOverlay pages, drawn above the stage. Underneath one of these is the game, not the pages you came from: this is what a pause menu, an in-game save screen, or mid-scene settings are. While one of these pages is on screen, the dialog, nvl, choice and onStage slot Surfaces fade out, staying mounted, and fade back when it closes; the notification slot stays on screen. A layer does not hide them.
  • A layer is stamped the same way when it is shown: Show Layer while the stack is hidden shows a gameOverlay layer, and at any other time an appPage one.
  • A gameOverlay page or layer draws its own background slightly transparent, so the scene shows through. Its widgets keep their opacity.
  • Quit Game tears down the session, opens its return Page as appPage again, and closes every page beneath it. It sits with the other Game nodes. Quit Application ends the runtime instead; in Dev Mode it stops the Dev Mode session rather than closing Studio, and it deliberately does not fire the window-close event, which is only for the player closing the window.

Is Game Overlay reads this flag in a page's or a layer's own blueprints. It is fixed to the instance, so an exiting pause menu keeps its overlay answer through its whole exit animation even though the session is already being cleared. Every stage-slot Surface answers true, and a Page embedded inside an overlay inherits the answer from the page holding it. App logic belongs to no page, and there the node answers whether a page or a modal layer covers the running game; see Is Game Overlay.

Closing a gameOverlay when the page beneath is one of the hidden ones plays the exit animation and reveals the stage. Nothing enters, because nothing underneath is rendering.

Lifecycle

One page or layer, in order:

  1. It mounts hidden and paints once. Widget Init runs during that hidden pass, so first-frame changes settle before anything is visible.
  2. Prepaint finishes → Surface Init. It fires once per runtime scope; opening the same Surface again is a new scope, so it fires again.
  3. The enter animation plays.
  4. After Surface Enter — after the animation completes. With no animation, or under reduced motion, it follows prepaint immediately. Exactly once per enter, no matter which internal path reported completion.
  5. Before Surface Exit — before the exit animation starts.
  6. The exit animation plays, and it unmounts → Surface Unmount.

Three pure nodes report the same thing as state instead of as an edge:

Nodetrue while
Is Surface Enteringfrom mount until the enter animation completes
Is Surface Exitingfrom Before Surface Exit until it unmounts
Is Surface Transitioningeither of the above

The state flips before the matching event head runs. Inside After Surface Enter, Is Surface Entering already reads false; inside Before Surface Exit, Is Surface Exiting already reads true. Read them to ask what is happening now; use the event heads for when it happened. In an environment with no animation layer at all, all three read false.

Before Surface Exit and After Surface Enter also reach the private blueprints of mounted widgets — only those alive at that instant. Surface Init and Surface Unmount do not; they belong to the Surface. A widget's own pair is Init and Unmount. All the heads are listed under Events nodes.

A page or layer takes pointer and keyboard input from the moment it is revealed, through its enter animation, and stops taking both when its exit animation begins. A graph that must ignore input while it is still entering has to branch on Is Surface Entering itself. While a page or layer plays its exit, a press that no other page or layer takes is dropped rather than reaching the game stage; see Event bubbling.

Surface, Page, and Frame

nl.frame is a widget that renders another app Surface inside the current one. What it shows is a Page.

Page is a role, not a separate document type. The same appSurface can be a page or a layer in one place and a Page inside a Frame in another. A Frame is not an iframe either: the target renders through the same renderer, in the same document.

  • The target must be another app Surface. Pointing a Frame at itself, at a Game UI, at a missing Surface, or at anything that leads back to the Surface holding the Frame is rejected, and the Frame shows a placeholder instead of breaking the render.
  • Each Frame instance gets its own runtime scope, derived from the parent scope, the Frame element, and the target. Two Frames showing the same Page share no surface state, no widget locals, and no lifecycle. The embedded Page fires its own Surface Init when the Frame first mounts it, and Surface Unmount when the target changes or the Frame goes away.

Values reach a Page through the params it declares in its properties; see Page params:

  • For a page or a layer, the values are what Go Page, Replace Page, Show Layer or Show Confirm gave. Each of the first three grows one input per param of the page it picks.
  • A Frame's params prop is the embedded Page's props. Its Properties show one field per param of the target Page.
  • Inside the Page, a text or button shows a Text param under Page param, a List shows a List param, and Get Page Param reads any param. A param that was not given reads its default. The same page reads its params the same way as a page, a layer or inside a Frame.
  • Get Page Props reads the whole object: the declared params with their defaults, and any other field a Page props input or the Frame gave. A non-object value normalizes to {}, and a field that is not there reads as null.
  • Set Frame Page switches the target and can write params in the same call; with no param given and its props input unwired, the existing params are kept. See Frame nodes.
  • The child talks back with Emit Page Event, received by the Page Event head on the Frame element's own blueprint. A page or layer shown on its own has no parent Frame, so emitting there reaches nobody.

Stage slots

While a story runs, NarraLeaf owns the stage and Studio hands it your Game UI surfaces. There are five slots, and one active Game UI each.

SlotTakes overBuild here
dialogthe dialog boxthe nametag and a Sentence, plus whatever advances the line
choicethe choice menua Choice List over { text, index, disabled }, with Item Click wired to Select Choice
notificationthe notification stacka Notification List over { id, message }
nvlfull-screen novel-mode dialogan NVL List over { nametag, isActive, index }, plus NVL Texts
onStagenothing — it is an extra layera HUD over the running stage

A slot you leave empty keeps NarraLeaf's own built-in component, so deleting your Dialog surface does not leave the game without a dialog box. The slot picker disables slots already taken; if a document somehow holds two Surfaces for one slot, the runtime uses the first in document order and warns.

onStage is the exception in the list. NarraLeaf has no injection point for it, so it mounts as a child of the Player, above the stage. It renders only while the stage is visible, and its shell is click-through on purpose — empty space must still advance the story. A full-screen interactive container placed in it will swallow those clicks.

The notification slot is display-only: nothing on it takes a press, a press over a toast goes to what is beneath it, and its Surface blueprint hears no keys or input actions.

Each slot Surface runs in its own runtime scope, keyed by the game session, the slot, and the Surface, and Is Game Overlay is true inside all of them.

The Surface blueprint

Each Surface has one blueprint, owner surfaceMain. It lives exactly as long as the page, layer or Frame instance that mounted it: open a Surface twice and its blueprint runs twice, with separate locals and separate surface state.

What it can reach:

  • Its own elements. The Element literal stores { surfaceId, elementId, elementType } and is same-Surface by construction. Nothing reaches into another Surface's tree.
  • Its own surface state, keyed by the instance's runtime scope, so two instances of the same Surface never collide. Scopes and lifetimes are on Variables.
  • Its own params and props, through Get Page Param and Get Page Props.
  • Everything that is not per-Surface anyway: persistent variables, global state, saves, the running game.

Its event heads: Surface Init, Surface Unmount, Before Surface Exit, After Surface Enter, Mouse Click, Right Click, On Action, the keyboard heads, the game-preference heads, fullscreen and window focus changes, the window-close request, the broadcast receivers, and the two bound-element heads.

Mouse Click and Right Click behave differently here than on a widget: on a Surface they fire for any click that lands on it and output coordinates in that Surface's design space, where the widget version fires for its own element and the elements inside it. A Page takes a press only where one of its elements is drawn, so a click on an empty part of a Page is not the Page's. A Page shown in a Frame runs its own Surface Mouse Click and Right Click for a press on one of its elements, in its own design space, and the Surface holding the Frame then hears the same press. A press on an empty part of the shown Page is heard by the Surface holding the Frame only.

Against widget-level blueprints:

  • A widgetMain blueprint belongs to one widget instance and mounts and unmounts with it. Init and Unmount are its lifecycle; the Surface's is Surface Init and Surface Unmount.
  • Keyboard heads dispatch to App logic, then to the Surface that owns the keyboard and every widget on it that declares one. Put the same head in all three and all three run. Which Surface owns the keyboard is described under Keyboard.
  • Element nodes are how a Surface blueprint acts on a widget it does not own — that is the whole reason the Self/Element split exists.

On this page