scene

package
v0.131.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 9, 2026 License: BSD-3-Clause Imports: 2 Imported by: 0

Documentation

Overview

Package scene adds an OPT-IN Evas-style damage / scene layer on top of the immediate-mode go-widgets/toolkit widget set. The core toolkit repaints the whole widget tree every frame (Window.Draw → containers recurse Draw unconditionally, with no per-widget dirty flag). That is simple and correct, but on a dense scene where a single hovered widget changes it repaints — and on a wasm/canvas host, re-blits — every pixel of every widget each frame.

This package fills that INTRA-app gap without touching the Widget contract: an app that does not construct a Scene keeps calling Draw directly and is completely unaffected. An app that opts in wraps its root widget in a Scene, calls Scene.Invalidate whenever a widget's appearance changes, and calls Scene.Render once per frame. Render:

  • coalesces every invalidation into a damage RegionSet (each invalidation contributes union(old-rect, new-rect) so a moved/resized widget damages both where it was and where it now is),
  • clips the painter to the damage via the existing painter.Clipper seam,
  • draws ONLY the nodes whose retained bounds intersect the damage — pruning whole non-intersecting subtrees in O(depth) rather than O(n),
  • skips a node that is provably, conservatively occluded by an opaque later sibling (see Opaque), and
  • returns the exact region the host must blit.

The result is pixel-identical to a full immediate-mode repaint (this is the package's headline correctness guarantee, proven by test), because the persisted framebuffer already holds the previous frame everywhere and Render repaints — in z-order, background first — every node that has pixels inside the damage. wasmbox already does cross-WINDOW damage between clients; this is the missing intra-app piece that also hands the host the blit region.

Retaining an immediate-mode tree

The scene mirrors the live widget tree through the toolkit's generic [childProvider] seam (the same Children() method CollectRuns walks), keeping a retained Node per widget with its last-drawn rect. Two OPTIONAL capabilities let a container/leaf cooperate for maximum efficiency; a widget that implements neither is still drawn correctly (a container that does not implement SelfDrawer is drawn wholesale — its own Draw recurses — whenever it intersects the damage):

  • SelfDrawer: a container that paints its own chrome separately from its children lets the scene repaint just the chrome and then descend into — and prune — its children individually.
  • Opaque: a leaf/container that fills a rectangle fully opaque lets the scene occlusion-cull lower siblings it completely covers.
Example

Example shows the opt-in damage loop: wrap a laid-out root in a Scene, then on each frame Invalidate whatever changed and Render — the returned Region is the exact rectangle set the host must blit.

// A 200x120 surface with one opaque cell the "hover" recolours.
hovered := newCell(Rect{X: 20, Y: 20, W: 40, H: 30}, blue)
root := newGroup(Rect{X: 0, Y: 0, W: 200, H: 120}, grey, hovered)

s := New(root)
buf := make([]byte, 4*200*120)
p := painter.NewPixelPainter(buf, 200, 120)
theme := toolkit.DefaultLight()

s.Render(p, theme) // first frame paints everything

// The pointer enters the cell: recolour it and damage just that widget.
hovered.col = red
s.Invalidate(hovered)
region := s.Render(p, theme)

fmt.Printf("blit %d rect(s), bounds=%v, bytes=%d\n",
	len(region.Rects()), region.Bounds(), region.Area()*4)
Output:
blit 1 rect(s), bounds={20 20 40 30}, bytes=4800

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Node

type Node struct {
	// W is the widget this node mirrors.
	W toolkit.Widget
	// contains filtered or unexported fields
}

Node is one retained entry in the scene tree, mirroring a single widget. It caches the widget's bounds at the last render (lastRect) — so an invalidation can damage both the old and the new position — plus the bounding box of the node's whole subtree (subtree), which lets Render prune a non-intersecting subtree without visiting its descendants.

func (*Node) Children

func (n *Node) Children() []*Node

Children returns the node's child nodes in draw order (earlier under later). Exposed so a consumer can introspect the retained tree; the slice is owned by the scene and must not be mutated.

func (*Node) Dirty

func (n *Node) Dirty() bool

Dirty reports whether an Invalidate has touched this node (or a descendant of it) since the last Render.

func (*Node) Widget

func (n *Node) Widget() toolkit.Widget

Widget returns the widget this node mirrors.

type Opaque

type Opaque interface {
	OpaqueRect() (Rect, bool)
}

Opaque is an OPTIONAL capability a widget implements when it paints every pixel of some rectangle fully opaque (alpha 0xFF). OpaqueRect returns that rectangle and true; a widget that cannot promise full opacity returns false (or does not implement the interface at all) and is NEVER treated as an occluder. The scene uses it ONLY to occlusion-cull: a lower sibling whose damaged area is entirely contained in a higher sibling's opaque rect is completely hidden and need not be drawn. The rule is deliberately conservative — the scene never skips a node that could show through, so a partially-covering or translucent occluder culls nothing.

type Rect

type Rect = toolkit.Rect

Rect is re-exported from the toolkit (itself an alias of painter.Rect) so a scene consumer needs only this package to describe a damage rectangle.

type Region

type Region = RegionSet

Region is the value Scene.Render returns: the coalesced set of rectangles the host must blit this frame. It aliases RegionSet.

type RegionSet

type RegionSet struct {
	// contains filtered or unexported fields
}

RegionSet is a small set of damage rectangles kept coalesced: Add drops a rectangle already covered by a member and removes members a new rectangle subsumes, so identical invalidations collapse to one rect and disjoint ones stay distinct. It is the union(old, new) damage accumulator and the blit region Render returns.

func (*RegionSet) Add

func (rs *RegionSet) Add(r Rect)

Add merges r into the set, keeping it coalesced. An empty rectangle (zero or negative extent) is ignored. If an existing member already contains r, r is dropped; every existing member r fully contains is removed. It never allocates once the backing slice has grown to the working set's steady-state size.

func (*RegionSet) Area

func (rs *RegionSet) Area() int

Area returns the summed area of the members. Because members are coalesced only by containment (not by partial-overlap merging), overlapping members would double-count; the sets the scene produces (union(old,new) damage) never partially overlap after coalescing in practice, so Area is the exact pixels-touched count the host blits — multiply by 4 for RGBA bytes.

func (*RegionSet) Bounds

func (rs *RegionSet) Bounds() Rect

Bounds returns the single rectangle bounding every member, or the zero Rect when the set is empty.

func (*RegionSet) Rects

func (rs *RegionSet) Rects() []Rect

Rects returns the coalesced rectangles. The slice is owned by the set (and, for a Render result, reused on the next frame); copy it if you need to retain it past the next Render.

type Scene

type Scene struct {
	// contains filtered or unexported fields
}

Scene retains a mirror of a widget tree and turns per-widget invalidations into a clipped, minimally-drawn frame. It is not safe for concurrent use; a UI thread owns it, exactly like the widgets it wraps.

func New

func New(root toolkit.Widget) *Scene

New builds a scene mirroring the tree rooted at root and seeds a full-surface damage so the first Render is a complete paint (matching an app's very first immediate-mode frame). root must be non-nil and already laid out (its bounds and its descendants' bounds set); New reads those bounds to size the initial damage.

func (*Scene) Invalidate

func (s *Scene) Invalidate(w toolkit.Widget)

Invalidate marks the node mirroring w dirty and records damage covering both where w was last drawn (its lastRect) and where it is now (its current Bounds), so a move or resize repaints the vacated area as well as the new one. The dirty flag propagates up w's ancestor chain. Invalidating a widget that is not in the scene tree is a no-op.

func (*Scene) Render

func (s *Scene) Render(p painter.Painter, th *toolkit.Theme) Region

Render refreshes the retained tree from the live widgets, then repaints and returns the accumulated damage. When no widget has been invalidated it draws nothing and returns an empty region. The returned Region's backing storage is reused on the next Render — copy it if you must keep it. Render performs no per-frame heap allocation in the steady state.

func (*Scene) Root

func (s *Scene) Root() *Node

Root returns the retained root node, for introspection of the scene tree.

type SelfDrawer

type SelfDrawer interface {
	DrawSelf(p painter.Painter, th *toolkit.Theme)
}

SelfDrawer is an OPTIONAL capability a container implements when it paints its own chrome (a background fill, a frame border, a title bar) separately from its children. The scene calls DrawSelf to repaint just that chrome inside the damage region and then recurses into the container's children individually, so an unchanged child of a changed container is pruned. A container that does NOT implement SelfDrawer is instead drawn wholesale (its ordinary Draw, which recurses into every child) whenever its subtree intersects the damage — still pixel-correct, just without per-child pruning inside that container.

DrawSelf MUST paint only the container's own pixels and MUST NOT recurse into children; the scene owns child traversal.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL