Skip to content

Rendering

Checked against e2d8fb4 on 2026-11-10. The files it covers are unchanged since.

niso’s core lays the page out; hosts only draw. Between them is the render stream: once per frame, the host calls niso_render_frame and gets back what changed in the laid-out box tree since the last frame, as bytes. The first frame (and the first after niso_render_reset) describes the whole tree.

The ops are about boxes, not elements: each box has a render node id (the DOM handle of the element or anonymous box that generates it), and the host keeps one native view per render node.

  • Structure: CREATE, DESTROY, and CHILDREN with a node’s children in paint order, back to front.
  • Geometry: FRAME, each box’s border box relative to its parent’s, in CSS pixels. Hosts place views exactly there and never run a layout of their own.
  • Visuals: BOX (background, opacity, transform, overflow, borders with their styles and colors, corner radii), EFFECTS (shadows, filters, backdrop filters, drawn with the platform’s own effects), IMAGE.
  • Text: TEXT carries shaped glyph runs with their positions, fonts, colors and decorations, plus the text each run was shaped from, so a host that draws text rather than glyphs (Android) gets the same result. Fonts come once, by id (FONT, niso_render_font). See Text.
  • Native controls: CONTROL and TEXT_FIELD say where to draw a native switch, slider, button, radio group or text field and with what state (Native controls).
  • Scrolling: ROOT and SCROLL give each scroll container’s scrolling area and offset. Scrolling is native: the host scrolls in its own scroll view and reports the offset back with niso_dom_scroll, so scrolling never waits for script.
  • Media: MEDIA and NOW_PLAYING for media elements and the media session.

The byte format of every op is generated from the header that defines it: Render stream format. The stream is versioned (niso_render_version), because hosts and widget extensions can be built separately from the core.

It’s a real wire format, not shared structs, on purpose: a host in Swift, Kotlin or Rust reads the same bytes, a renderer can run in another process, and a recording of frames (niso-run --render FILE) can be replayed and compared. Each frame is one batch of changes, applied in order, so a host never sees a half-updated tree.

Live targets (the app’s own window) apply the stream every frame. Template targets (widgets, Live Activities, notifications, CarPlay) take only native views and lay text out themselves, so they get templates instead: the same boxes with text as strings (Surfaces).

How each host turns ops into views is in Hosts. The code that writes the stream is crates/niso-core/src/render.rs.