A page is a screen drawn by OneRender: an HTML and CSS document (or a tree built in C#) that OneRender lays out, styles, animates and hit-tests, and that onegame draws through the render graph over your game. OneGame.UI stays: a game can use either, per screen, or both at once. Solitaire's settings screen is a page (samples/solitaire/Views/SettingsPage.cs); its menu, statistics and table are still OneGame.UI.
Pages are view code. A handler reads state and sends commands, never writes state, and showing a page changes no hash.
Page or OneGame.UI?
| Reach for a page when | Reach for OneGame.UI when |
|---|---|
| you want real CSS: grid, gradients, shadows, filters, transitions, animations, blend modes | you want the component kit (tables, tabs, tooltips, dialogs, lists, text input with its focus rules) |
| the screen is mostly layout and text, or you already have the HTML | the screen needs focus navigation by controller across custom widgets |
| you want native form controls (radio buttons, checkboxes, sliders, text fields) with no code | you want the kit's focus rules and widgets ready made |
An HTML page
Build the page when its screen opens. Show replaces a page of the same name, so a screen that is built again simply remakes it.
using OneGame.Host; using OneGame.Pages; using OneGame.UI; using OneRender.Interaction; using PagesGame; // Meter: the game's own entity, with an Add command public static class MenuScreen { private const string Html = """ <body style="margin:0;font:16px Body;color:#e8ecf4"> <h1 style="margin:10px">Menu</h1> <button id="start" style="width:120px;height:36px">Start</button> </body> """; public static Element Build(GameInstance game) { var fonts = new PageFonts(game.Fonts); // the same faces OneGame.Text draws with fonts.Add(File.ReadAllBytes("fonts/Body.ttf"), "Body"); // the page says: font: 16px Body game.Pages.Show("menu", PageScreen.FromHtml(Html, new PageOptions(fonts) { // Runs every time the page is made, which includes after a window resize or a UI scale change. Setup = s => s.Interactive!.Events.AddEventListener( EventTarget.Of(s.Interactive.FindElement("start")), "click", _ => Meter.Of(game.World).Send.Add(1)), })); return Ui.Column(); // the screen's OneGame.UI side: empty, or the felt behind the page } }
Things worth knowing:
- Sizes are CSS pixels at the UI scale: a 620 px wide card is 620
OneGame.UIpixels wide, and a 1.5 scale draws it at 930 device pixels. - The page is transparent where you give it no background, and clicks on a transparent part pass through to what is under it.
- An HTML page cannot resize in place, so it is made again when the window size or UI scale changes. Focus, scroll position and form values are lost then. Put what the page shows in your own state (the profile, the settings object) and have
Setupapply it: set control values withSetCheckedandSetRangeValue, and focus the first control withFocus.Setup's changes are painted before the first frame. - Hide a page by calling
game.Pages.Remove("menu")when another screen opens (the screen builders of the other screens are the place), orSetVisibleto keep it. - Structure is fixed once the page is made: handlers change text, control state and classes, not the element tree. Change text with
SetTextContent(see Limits), or use a builder page for a HUD.
Handlers send commands
A DOM listener registered with page.Events.AddEventListener is view code. It may read state, send commands, and change the player's own settings and profile. It may not write synced state or call sim-only methods; the analyzer says so at build time:
error OG0008: View code cannot change simulation state: 'Table.NewGame' changes state. Views read state and send commands; use entity.Send.<Command>(...) instead.
That holds for lambdas, method groups and local functions passed to AddEventListener. A command sent from a handler runs on the next tick, like any command from a view.
Solitaire's handlers save the profile as the player changes a control:
using OneGame.Pages; using OneRender.Interaction; public static class Handlers { public static void Wire(InteractivePage page, Action<int> setMusic, Action back) { page.Events.AddEventListener(EventTarget.Of(page.FindElement("music")), "input", _ => setMusic((int)page.GetRangeValue(page.FindElement("music")))); page.Events.AddEventListener(EventTarget.Of(page.FindElement("back")), "click", _ => back()); } }
A builder page
A page can also be a StyledTree built in C#, with no HTML. It suits a HUD that changes every frame: edit the tree and the page redraws on the next frame, and the usual edits (text, state, style) allocate nothing.
using OneGame.Host; using OneGame.Pages; using OneRender.Style; using OneRender.Tree; public static class HudScreen { public static void Show(GameInstance game, PageFonts fonts) { var tree = new StyledTree(); var root = tree.AddRoot("div", new StyleDeclaration().Set("width", "320px").Set("height", "40px").Set("font", "18px Body").Set("color", "#ffd040")); var gold = tree.AddElement(root, "span"); tree.AddText(gold, "Gold 0"); tree.SetName(gold, "gold"); game.Pages.Show("hud", PageScreen.FromTree(tree, new PageOptions(fonts))); } public static void Update(PageSet pages, int gold) { var tree = pages.Get("hud")!.Live!.Styled; tree.SetText(tree.FindByName("gold"), $"Gold {gold}"); } }
A builder page draws and can be found by name, but it takes no pointer, key or text input: the game sends input to its nodes itself.
Images and SVG
PageImages puts raster and SVG files in the game's art atlas and gives pages addresses to name. SVG is drawn at the size the page shows it, with currentColor following the CSS color.
using OneGame.Host; using OneGame.Pages; public static class Art { public static PageOptions Options(GameInstance game, PageFonts fonts) { var images = new PageImages(game.Art); images.Add("coin.png", File.ReadAllBytes("art/coin.png")); images.Add("heart.svg", File.ReadAllBytes("art/heart.svg")); return new PageOptions(fonts) { Images = images }; // <img src="coin.png"> and background-image: url(heart.svg) } }
Finding pages in scenarios and the harness
ui_query, ui_click and ui_invoke, and the scenario steps click:, invoke: and ui: that use them, look in the OneGame.UI tree first and then in the shown pages, by name: an element's id, a form control's name, or #name. A click sends real pointer events through the platform queue at the element's centre, so hover, focus, transitions and handlers run as for a player. A controller button with a page focused acts like the keyboard: d-pad up and down move between controls (as Shift+Tab and Tab), left and right move along sliders and radio groups, and the south button presses.
name: settings-pad steps: - screen: settings - step: 3 - nav: down # focus moves from Draw 1 to Draw 3 - nav: south # the south button presses it - nav: down - nav: south # the checkbox - nav: down - nav: dpad_right # Music volume goes up one step - nav: dpad_right - step: 3 - expect: capture: { reference: refs/settings-pad-720p.png, size: 1280x720, tolerance: 2, max-diff: 0.001 }
click: draw-3 and expect: { ui: { id: start, text: Start } } work the same way, as in tests/PagesGame/scenarios/menu.yaml.
A capture draws the page. Tolerances for text edges follow the rest of the game (lavapipe is the reference); look at the frame before keeping it as the reference.
Limits and what is not there yet
- Platforms. Pages run on Linux (lavapipe) and in headless Chrome (WebGPU on SwiftShader), where solitaire's settings captures match the desktop references within the web tolerance. Metal, Windows, phones and real GPUs in a browser have not run them.
- Native control looks. Radio buttons, checkboxes and sliders take
color-scheme: dark(Chrome's dark control palette) andaccent-colorfor the fill, ring and dot, as solitaire's settings do. The slider track and thumb shapes cannot be styled until OneRender adds range pseudo-elements. For a custom look useappearance: none;:checked,:disabledand:indeterminatefollow the live state. - Live changes.
InteractivePage.SetTextContentchanges an element's text with no allocation, so a readout can follow a slider every frame (solitaire's volume numbers do).SetValueandSetCheckedchange a control's state. Class edits andSetAttribute/RemoveAttributere-style in place for about 835 KB; changingvalue,type,checked,srcornamethat way rebuilds at about 7.4 MB, so useSetValue/SetCheckedfor those. Per-frame changes should be text, transform and opacity; keep class and attribute edits for events (a click, a screen change). - No text-input method yet. Typing works; composed text from an input method and clipboard paste do not reach page fields.
- Tab order. A radio group is one tab stop (OneRender 1.0.1), so the controller steps over the group and arrows move inside it.
- Cost. OneRender's cost is in its own notes; a steady page allocates nothing in the adapter. Bounds on SVG keys, images, instances and clips are in
docs/interfaces/onerender.md.