Recursive AI

Change a page after it’s created

All onerender recipes

Update a live OneRender page from C#: set the text of a score readout, toggle a class and disable a button, without building the page again and without losing focus, hover or form state.

C#
using OneRender;
using OneRender.Image;
using OneRender.Interaction;
using OneRender.Painting;
using RecursiveAI.OneHtml;
 
string html = """
<!doctype html>
<html lang="en">
<style>
  body { margin: 0; font: 16px sans-serif; background: #16202c; color: #e8edf3 }
  .hud { display: flex; align-items: center; gap: 12px; padding: 12px 16px }
  #score { font-weight: bold; min-width: 4em }
  button { font: inherit; color: inherit; padding: 6px 12px; border: 0; border-radius: 6px; background: #2a3644 }
  button.on { background: #3b82f6 }
  button:disabled { opacity: .4 }
</style>
<div class="hud">
  <span>Score</span><span id="score">0</span>
  <button id="sound">Sound</button>
  <button id="save">Save</button>
</div>
</html>
""";
 
InteractivePage ui = InteractivePage.Create(html,
    new HtmlPageOptions(FontProviders.CreateDefault()) { ViewportWidth = 360, ViewportHeight = 60 });
Node score = ui.FindElement("score");
Node sound = ui.FindElement("sound");
Node save = ui.FindElement("save");
ui.Focus(save);
 
// Text: cheap enough to change every frame.
InputOutcome o = ui.SetTextContent(score, "1250");
Console.WriteLine($"score: {o}, \"{ui.GetTextContent(score)}\"");
 
// Classes and attributes: for state changes, not every frame.
o = ui.ToggleClass(sound, "on", force: true);
Console.WriteLine($"sound: {o}, class \"{ui.GetClassName(sound)}\"");
o = ui.SetAttribute(save, "disabled", "");
Console.WriteLine($"save: {o}, disabled \"{ui.GetAttribute(save, "disabled")}\", focused: {(ui.FocusedElement.IsNull ? "none" : ui.FocusedElement.Id)}");
Console.WriteLine($"the Node handle still reads the original: {save.GetAttribute("disabled") ?? "null"}");
 
var list = new DisplayList();
ui.Page.Paint(list);
File.WriteAllBytes("hud.png", DisplayListRasterizer.Render(list, 360, 60).ToPng());
Text
score: Relaid, "1250"
sound: Relaid, class "on"
save: StatesChanged, Relaid, FocusChanged, disabled "", focused: none
the Node handle still reads the original: null

In hud.png the score reads 1250, Sound is blue and Save is dimmed by its :disabled rule.

How it works

  • Ten calls on InteractivePage. SetTextContent, SetClassName, AddClass, RemoveClass, ToggleClass, SetAttribute and RemoveAttribute change the page; GetTextContent, GetClassName and GetAttribute read it back. Each change returns an InputOutcome: Relaid when something changed (call Repaint as in Hit testing and events), None for a no-op.
  • The result equals a fresh page. A page created from the edited HTML lays out and paints the same. Focus, hover, scroll positions, form state and listeners stay as they were, and a focused element that stops being focusable loses focus, with the usual blur events.
  • Your Node handles stay valid, but they point into the document the page was created from, which never changes. Read current values through the page.
  • SetTextContent takes an element that holds exactly one text node, like a label or a readout. It throws on style, script, textarea, title, option and an element with more than one child.

What each change costs

  • Text doesn’t touch the cascade: only the changed text is laid out and painted again. Once warm, a change that fits the room the old text had allocates nothing, so it’s safe to do every frame.
  • Classes and attributes restyle from a new snapshot of the document: about 0.8 MB per change on a 1,800-node page. Attributes a control reads when its boxes are made (value, checked, type, src) rebuild the page, about 7 MB there.
  • So change text, transform and opacity per frame, and classes and attributes when something happens, like a click or a state change.

The full rules, including what checked and value do after the user has changed a control, are in Game hosts.