Building an interface
A game’s interface in Orblit is a UiNode tree with a style string on each
node, built into real Flutter widgets: laid out by Flutter, hit-tested by
Flutter, drawn by Impeller. Not a second widget system pretending to be one.
The smallest one
Section titled “The smallest one”return UiSurface( description: const UiNode( type: 'column', classes: 'p-6 gap-4 items-center bg-slate-900', children: [ UiNode(type: 'text', text: 'Paused', classes: 'text-2xl text-slate-100'), UiNode( type: 'button', text: 'Resume', classes: 'px-4 py-2 bg-orange-600 rounded-lg', props: {'onPressed': 'resume'}, ), ], ), onEvent: (handler, payload) => debugPrint('$handler $payload'),);That’s a widget. Put it in a Stack over an OrblitView and it’s a heads-up
display. Put it in a Scaffold and it’s a pause menu.
An element does something by naming a handler in its props: onPressed on a
button, onChanged on a field, onTap on anything. onEvent then gets
called with that name and whatever the element has to say, whether that’s the
text in a field, the value of a slider, or null. What the host does with it is
its own business, and that’s what lets the same layer serve a game’s HUD, an
editor panel and a unit test without any of them knowing about the others.
Why the class names look familiar
Section titled “Why the class names look familiar”p-4 flex-1 items-center bg-slate-800 rounded-lg means what somebody who has
written a web page expects it to mean, and padding: 8px 12px; border-radius: 6px means the same thing in the other notation. Both are accepted, on the same
node.
This is borrowed deliberately. It’s a way in, rather than a second box model to keep aligned forever, because every class resolves to Flutter’s own layout. There’s no separate measure-and-arrange pass, no separate hit test, and nothing to keep in step with Flutter as Flutter changes.
Two ways to author, one document
Section titled “Two ways to author, one document”The editor lays a canvas out visually and writes a .oui file. A script
describes the same tree in TypeScript and sends it. Both arrive as a UiNode
tree.
That’s a preference rather than a fork. A canvas laid out by hand can be handed to a script to change, and a script’s tree can be saved as a canvas and edited by hand. It’s the same shape as the scripting boundary: several authoring front ends onto one model.
Responsiveness
Section titled “Responsiveness”A game runs on a phone and on a 32-inch monitor, and the interface has to hold on both.
Breakpoint prefixes work on any class:
classes: 'flex-col md:flex-row gap-2 lg:gap-6 text-base lg:text-2xl'The breakpoints are sm 640, md 900, lg 1280 and xl 1680, applied
narrowest-first, so a later, wider one wins.
The canvas decides how the whole document scales:
return const UiCanvas( width: 1920, height: 1080, fit: CanvasFit.responsive, columns: 12, minScale: 0.8, maxScale: 1.5,);CanvasFit.contain scales the whole design to fit and letterboxes it. That’s
right for a console game and wrong for a phone, because it makes the text tiny
rather than making the layout narrower. CanvasFit.responsive re-lays out
instead, which is what you want on a device whose shape you didn’t design for.
Testing it
Section titled “Testing it”Because it’s widgets, flutter_test works on it with no engine and no window:
await tester.pumpWidget(MaterialApp(home: UiSurface(description: pauseMenu)));expect(find.text('Resume'), findsOneWidget);await tester.tap(find.text('Resume'));That’s the argument for the whole approach in one paragraph. An interface built in most engines can only be exercised by launching the game.
