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. Flutter lays them out, Flutter
hit-tests them, and Impeller draws them. It is 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-ember-600 rounded-lg', props: {'onPressed': 'resume'}, ), ], ), onEvent: (handler, payload) => debugPrint('$handler $payload'),);That is a widget. Put it in a Stack over an OrblitView and it is a
heads-up display. Put it in a Scaffold and it is 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 is then called
with that name and whatever the element has to say, which might be the text in
a field or null. What the host does with it is up to the host, and that is
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 someone 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 work, on the same node.
This is borrowed on purpose. It is a way in, not a second box model to keep in step forever, because every class turns into Flutter’s own layout. There is no separate measure-and-arrange pass, no separate hit test, and nothing to keep up to date 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 is a preference, not 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 is the same shape as the scripting boundary: several ways to author, one model.
Responsiveness
Section titled “Responsiveness”A game runs on a phone and on a 32-inch monitor, and the interface has to work on both.
Breakpoint prefixes work on any class:
classes: 'col md:row gap-2 lg:gap-6 text-base lg:text-2xl'The breakpoints are sm 640, md 900, lg 1280 and xl 1680. They apply
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 is
right for a console game and wrong for a phone, because it makes the text tiny
instead of making the layout narrower. CanvasFit.responsive lays the
document out again, which is what you want on a device whose shape you didn’t
design for.
Testing it
Section titled “Testing it”Because it is 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 is the argument for the whole approach in one paragraph. An interface built in most engines can only be exercised by launching the game.
