Recursive AI

Build a styled tree in C#

All onerender recipes

Lay out and draw a UI with no HTML at all: build the element tree in C# with CSS property values, render it with LivePage, then change text and hover state and redraw only what changed.

C#
using OneRender;
using OneRender.Image;
using OneRender.Interaction;
using OneRender.Style;
using OneRender.Tree;
 
var tree = new StyledTree();
StyledNode root = tree.AddRoot("div", new StyleDeclaration()
    .Set("width", "480px").Set("height", "200px").Set("padding", "16px")
    .Set("display", "flex").Set("flex-direction", "column").Set("gap", "12px")
    .Set("background", "#16202c").Set("font", "16px sans-serif").Set("color", "#e8edf3"));
 
StyledNode title = tree.AddElement(root, "h1", new StyleDeclaration().Set("font-size", "22px").Set("margin", "0"));
tree.AddText(title, "Inventory");
 
StyledNode bar = tree.AddElement(root, "div", new StyleDeclaration().Set("display", "flex").Set("gap", "8px"));
(StyledNode gold, StyledNode goldText) = AddButton("Gold 120");
(StyledNode gems, _) = AddButton("Gems 4");
 
var hover = new StyleDeclaration().Set("background", "#3b82f6");
tree.SetStateStyle(gems, ElementStates.Hover, hover);
 
var page = new LivePage(tree, new LivePageOptions(FontProviders.CreateDefault()) { ViewportWidth = 512, ViewportHeight = 232 });
FlushResult first = page.Flush();
Console.WriteLine($"first flush: {first.Work}, {page.Display.Current.Count} display items");
File.WriteAllBytes("ui-1.png", DisplayListRasterizer.Render(page.Display.Current, 512, 232).ToPng());
page.Display.ClearDamage();
 
// A frame later: new text and a hover state. Only what changed is restyled, laid out and repainted.
tree.SetText(goldText, "Gold 135");
tree.SetStates(gems, ElementStates.Hover);
FlushResult next = page.Flush();
Console.WriteLine($"second flush: {next.Work}, reused {next.ReusedSubtrees} subtrees, {page.Display.Damage.Count} damaged areas");
for (int i = 0; i < page.Display.Damage.Count; i++) Console.WriteLine($"  damage {page.Display.Damage[i]}");
File.WriteAllBytes("ui-2.png", DisplayListRasterizer.Render(page.Display.Current, 512, 232).ToPng());
 
(StyledNode Button, StyledNode Text) AddButton(string label)
{
    StyledNode b = tree.AddElement(bar, "div", new StyleDeclaration()
        .Set("padding", "8px 14px").Set("border-radius", "6px").Set("background", "#2a3644"));
    return (b, tree.AddText(b, label));
}
Text
first flush: None, 6 display items
second flush: Restyled, BoxesPatched, Relaid, Repainted, reused 2 subtrees, 2 damaged areas
  damage RectF { X = 26, Y = 60, Width = 72.9375, Height = 24, Right = 98.9375, Bottom = 84, IsEmpty = False }
  damage RectF { X = 117, Y = 54, Width = 84, Height = 34, Right = 201, Bottom = 88, IsEmpty = False }

ui-1.png shows a dark panel with the “Inventory” heading and two buttons. In ui-2.png the first button reads “Gold 135” and the second is blue.

How it works

  • Values are CSS text. StyleDeclaration.Set goes through the same parsers as a style sheet, so a builder tree and an HTML page compute alike, flexbox and grid included. A value the parser rejects is ignored; TrySet returns false for it. var() isn’t available here.
  • No user-agent style sheet. Tag names are just names here: an h1 gets no bold or margin unless you give it some.
  • The constructor does the first layout and paint, so the first Flush has nothing to do (None).
  • Edits are logged, not applied. SetText, SetDeclaration, SetProperty, SetStates, InsertElement, Remove and Move record a change. Flush then restyles, relays out and repaints only what changed, and the result equals a full rebuild.
  • State styles. SetStateStyle attaches a declaration for :hover, :active or :focus; SetStates turns the state on or off.
  • Damage. page.Display.Damage lists the rectangles that changed since ClearDamage. Here, only the first button’s text and the second button need redrawing.

In a game loop

Each frame, apply the game state to the tree and call Flush. If Repainted is set, draw page.Display.Current (or just its damaged areas) and clear the damage. Steady frames, and the usual changes such as a hover, a text change or a transform or opacity animation, allocate nothing. A game engine with its own GPU renderer reads the display list directly: see Game hosts and The display list.

For HTML pages with input, events and focus instead, see Hit testing and events.