Platform · guide
Running a UI application
UiApplication.Run(options) is the whole of a Vixen interface's Main — a window, a device and the four steps of a frame — and this is what the options say, when the three events fire relative to the layout, and why the loop redraws every frame instead of when something changed.
Edit this page on GitHubDocuments
What it is
UiApplication is the host for an application whose whole content is a user interface. It opens a
window, creates a device, mounts a component into a UiDocument, and runs the four steps of a frame
until the window closes.
UiApplication.Run(options) is the entry point, and it is the only public way to start one — the
constructor is internal, because an application that has been constructed and not run is a window
nobody opened.
static int Main(string[] arguments) => UiApplication.Run( new UiApplicationOptions { Title = "Vixen — Hello UI", Organisation = "Vixen", Application = "HelloUi", Content = () => new Shell() }, arguments );That is the whole of a Vixen application's Main.
⚠ It knows nothing about the engine. Vixen.Ui.Desktop is a Platform/ assembly holding a
window, a device and a frame loop; it does not reference Vixen.Engine or Vixen.App, and neither
does an application that uses it. The boundary is what keeps a tool from dragging a renderer, a scene
graph and an asset database behind it.
What it is for
Because there used to be three of these and they had diverged. Samples/02-HelloUi carried a
five-hundred-line Program.cs with a Vulkan device, a swapchain, a render graph, an atlas upload,
resize coalescing and a suboptimal-present rule in it; Vixen.Templates' AppHost and
Vixen.Editor.Host's EditorHost each carried their own copy of the same thing, and the three did
not agree about any of it. Every one of them was written because the alternative was referencing a
host that drags an engine behind it.
⚠ A frame is drawn every pass, not when something changed. The loop is unconditional, and that is
a decision rather than an omission: an interface that repainted on change needs something to be the
authority on what "changed" means, and the reactive graph deliberately is not that — an Effect can
assign a property that no draw depends on, and a caret blinks with nothing assigned at all. The cost
is a device that is always busy on a laptop, which is real; the alternative is a class of bug where
the screen is correct only if somebody remembered to invalidate.
Using it
UiApplicationOptions has a shippable default for everything except one. The shortest useful set
is Title and Content; Content is a Func<Component> and is the one thing there is no sensible
default for.
| Option | Default | What it settles |
|---|---|---|
Title | "Vixen" | What the window is called |
Size | 1280 × 800 | Its size, in device-independent pixels |
IsResizable | true | Whether the user may resize it |
Organisation / Application | "Vixen" | The two halves of where settings are kept |
Content | — | Builds the interface |
Mount | the default | Puts the interface into the document, for hot reload |
Styles | [] | Stylesheets to load, in order, as author sheets |
InstallControlTheme | true | Whether the control set's theme goes in under everything |
RootClasses | [] | Classes to put on the document's root |
Ground | a dark blue-grey | What the window is cleared to |
InstallSystemFont | true | Whether a face is borrowed from the OS when none is registered |
Configure | — | Run once against the document, after the sheets and before the content |
Frames | 0 | How many frames to run, or zero for "until it is closed" |
Offscreen | false | Draw on a device with no surface, with the window hidden |
CapturePath | null | A directory to write the last frame into, as frame.png |
Platform | SDL | Where the window comes from |
⚠ Styles cannot be inferred, and the generated utility sheet is the one people forget.
VixenUtilityStyles.Css is a string in the assembly the build produced; nothing walks a manifest
looking for it, so an application whose classes silently do nothing is nearly always one that did not
name it here. See making a project compile markup for where that string
comes from.
⚠ Ground is used twice and the two must not disagree. It is the clear colour and the colour
UiRenderer.Compose composites against, so a window whose two halves were given different values
gets a fringe wherever the interface is translucent.
Three events, and where each one is is the whole of what distinguishes them.
* Started fires once, after the interface is built and before the first frame is pumped.
* Frame fires once a frame, after the events are pumped and before UiDocument.Update — so a
signal a handler writes is laid out and drawn in the same frame rather than the next one.
* Stopping fires once, after the loop stops and while the document is still alive. That is why
it is not Dispose: a handler that wants to read what is on screen before it goes still can.
Each has a matching property on the options, for an application that has nowhere to hold the instance.
UiFrame is what Frame carries: Now, the time since the application started, and Delta, how
long the previous frame took.
Tasks is a BackgroundTaskManager, pumped once a frame before Frame fires. An application
with long work to report does not need a timer of its own; see
background tasks.
Run(options, arguments) is the overload to use from Main. It reads these arguments before
starting. Each takes its value as the next argument or after =:
| Argument | Sets | What it does |
|---|---|---|
--vixen-frames N (or --frames N) | Frames | Runs N frames and exits. This is what makes a smoke test stop |
--vixen-size WxH | Size | Replaces the application's own window size |
--vixen-offscreen | Offscreen | Draws on a device with no surface and hides the window |
--vixen-capture <dir> | CapturePath | The same, and writes the last frame to <dir>/frame.png |
Platform is how an application reaches the operating system. The clipboard, the native file
pickers, the displays and the process lifecycle live on IPlatform, which Vixen.Ui is not allowed
to name — so this property is the only route to them from application code, and Started is where
to read it.
⚠ Ask Capabilities before using most of it. A headless run has no displays; a Linux session
with no zenity or kdialog has no file picker; and a picker that is not there answers "nothing
chosen", which is exactly what the user pressing Cancel answers. platform.Pickers() is that
question spelled once: it hands back INativeDialogs when PlatformCapabilities.NativeDialogs is
present and null otherwise, which is what an Open… menu item's enablement should read.
ShowMessageAsync is the exception and is always safe to call.
⚠ Everything on it belongs to the loop thread, so call it from Started, Frame, Stopping or
an event handler — never from a continuation that resumed on a pool thread. Win32 delivers messages
to the thread that made the window and AppKit refuses to be touched from anywhere else; that is the
operating systems' rule, not the framework's.
⚠ Hot reload arrives by reference and there is no flag. A Debug-only reference to
Vixen.Ui.Desktop.HotReload is the whole of turning it on: UiApplication's static constructor
loads that assembly by name and runs its module initializer, so a Release build does not resolve it
and nothing in Main changes. Mount exists for the same reason — it lets the reload host own the
mounting without Vixen.Ui.HotReload being linked into a shipped application.
Capturing the whole application
⚠ --frames N on its own is not a screenshot. It runs the loop and presents to the window, and
nothing is written anywhere. Until #1367 there was no way to picture a Vixen.Ui application whole:
controls were pictured one at a time, in a test document, and never together with the application's
own sheet.
--vixen-capture <dir> renders into a texture the size of the window's framebuffer, on a real device,
through the same passes a presented frame takes. It copies the last frame out and writes it as
frame.png. The window is created hidden, because it is still what gives the document its size,
its DPI scale and its event stream.
MyTool --vixen-frames 3 --vixen-size 1280x1800 --vixen-capture ./shotsThree rules keep it from quietly producing nothing:
* ⚠ It needs a frame count. A hidden window cannot be closed, so --vixen-capture or
--vixen-offscreen without --vixen-frames throws instead of running for ever.
* ⚠ It refuses to run without a Vulkan device. The windowed path draws nothing on a machine with no
GPU, which is fine for a smoke test. A run that asked for a picture throws with the reason instead,
and never falls back to drawing nothing.
* ⚠ A run that wrote no picture exits 1. Exiting 0 would look like a run that worked.
The run prints the adapter, Vulkan device created on '<name>', and Captured the frame to <path>..
Check both before trusting the picture.
⚠ It still needs a display server. The window is hidden but it is a real SDL window, so a Linux
runner with no X server or Wayland compositor needs Xvfb. --vixen-headless is not read by this host.
Quitting, and refusing to
UiApplication.Quit() asks before it stops. The request is routed from the focus outwards as a
CloseRequestEvent, and anything on the route may Cancel() it; UiDocument.CloseRequested fires
afterwards for a head that is not in the element tree.
The prompt an application with a document wants is written already — it is
DocumentClosePrompt.Install, and one line is the whole of it:
using var prompt = DocumentClosePrompt.Install( application.Document.Root, dialogs, () => application.Quit());Something that is not a document — an unfinished upload, a running job — answers the request itself:
document.Root.AddHandler<CloseRequestEvent>((_, args) => { if (job.IsFinished) { return; } args.Cancel(); job.Cancelled += () => application.Quit();});⚠ A refusal is "not now", not "never". A Save / Don't Save / Cancel prompt is a dialog and a
dialog is answered frames later, so a synchronous veto cannot wait for one without blocking the loop
that draws it. The handler cancels, opens the prompt, and calls Quit() again when it has an
answer — which is the shape EditorHost has used since save-on-close was built, and the shape
DocumentClosePrompt packages. ⚠ Calling Quit() again runs every handler again, so a handler that
refuses on a condition its own answer did not change has to latch over the second pass or the
application cannot be quit at all.
⚠ Cancel() is not Handled. They are two questions. A document that saved silently has dealt
with the request and is content to go, and a handler forced to say so by leaving Handled false
could not be told apart from one that had refused.
Stop() is the unconditional form, for the handler that has finished asking. Platform is the
platform the application is running on — the clipboard, the native dialogs, the lifecycle and the
display list all hang off it.
Examples
The application template's Main, which is the shape to copy:
static int Main(string[] arguments) => UiApplication.Run( new UiApplicationOptions { Title = "My Tool", Organisation = "Acme", Application = "MyTool", Size = new Int2(1280, 800), // The generated sheet. Nothing finds it for you. Styles = { VixenUtilityStyles.Css }, RootClasses = { "dark" }, Content = () => new Shell() }, arguments );Reporting a frame time into the interface, using the event that runs before the layout:
application.Frame += (_, frame) => model.FrameTime.Value = frame.Delta.TotalMilliseconds;Because Frame runs before UiDocument.Update, the number written there is laid out and drawn in
that same frame.
A run of a fixed length, which is what a smoke test wants. It presents to a window and writes nothing:
MyTool --frames 4A picture of the whole application, drawn on the GPU with the window hidden:
MyTool --vixen-frames 3 --vixen-capture ./shotsSee also
* Making a project compile markup — where VixenUtilityStyles.Css and
the .vxml classes come from.
* Background tasks — what UiApplication.Tasks is pumping.
* Docking panels — the arrangement an editor-shaped application puts inside the
window, and the scrolling contract a panel keeps to.
* UiDocument — the tree the loop lays out, draws and dispatches
into.