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 ¶
- type HostRoot
- func (r *HostRoot) Bounds() toolkit.Rect
- func (r *HostRoot) Children() []toolkit.Widget
- func (r *HostRoot) Draw(p painter.Painter, th *toolkit.Theme)
- func (r *HostRoot) HitTest(px, py int) bool
- func (r *HostRoot) Invalidate(w toolkit.Widget)
- func (r *HostRoot) OnEvent(ev toolkit.Event)
- func (r *HostRoot) RenderDamaged(p painter.Painter, th *toolkit.Theme) []toolkit.Rect
- func (r *HostRoot) Scene() *Scene
- func (r *HostRoot) SetBounds(b toolkit.Rect)
- type Node
- type Opaque
- type Rect
- type Region
- type RegionSet
- type Scene
- type SelfDrawer
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type HostRoot ¶ added in v0.132.0
type HostRoot struct {
// contains filtered or unexported fields
}
HostRoot adapts an immediate-mode widget tree to a windowing host's damage-aware present path. It is a plain toolkit.Widget — so it can be handed to ANY host, including one with no damage support, which simply calls Draw for a full-surface repaint — and it ADDITIONALLY reports the exact per-frame damage region through HostRoot.RenderDamaged.
A host (github.com/go-widgets/window) type-asserts the root it is given for the structural capability
interface {
RenderDamaged(p painter.Painter, th *toolkit.Theme) []toolkit.Rect
}
When the assertion holds it draws the frame through RenderDamaged and packs+presents ONLY the returned rectangles' union; a plain widget keeps the full-surface path. HostRoot is that capability's reference implementation.
HostRoot owns a Scene mirroring child and fills the window background — from the theme handed to each frame — as scene chrome, so an incremental frame repaints a widget's VACATED background exactly as a full immediate-mode frame (background fill, then the tree) would. The two are pixel-identical by construction (see the package's headline correctness guarantee), which is why a host may switch to the incremental path without any risk of dropping a pixel.
Usage: build the tree, wrap the root, hand it to the host, and call HostRoot.Invalidate whenever a widget's appearance changes:
root := scene.NewHostRoot(appRoot) // in an event handler, after mutating w's appearance: root.Invalidate(w) // ... backend.Run(root) // window.Backend, drives RenderDamaged per frame
func NewHostRoot ¶ added in v0.132.0
NewHostRoot wraps child (the application's root widget) so it can drive a host's damage-aware present path. child must be non-nil. The returned root is not yet laid out; the host calls SetBounds with the client area before the first frame (which seeds a full-surface first paint).
func (*HostRoot) Bounds ¶ added in v0.132.0
Bounds returns the root's placement (the whole client area once laid out).
func (*HostRoot) Children ¶ added in v0.138.1
Children exposes the single wrapped child container so a generic tree walk descends from the host root INTO the application tree instead of stopping at it. Every other toolkit container answers the Children() []toolkit.Widget convention; HostRoot did not, so a walker built on it — the accessibility walk github.com/go-widgets/toolkit.WalkA11y the platform bridges consume, the window drag-and-drop controller, [CollectRuns] — reached nothing under a desktop app's root and consumers had to unwrap via Scene().Root().Widget() by hand.
The returned widget is exactly that: the live container Scene().Root().Widget() exposes, which itself yields the application's root. This is purely structural — it exposes the existing child for traversal and changes neither rendering nor damage.
func (*HostRoot) Draw ¶ added in v0.132.0
Draw paints the whole tree (background then children) — the full-surface path a damage-unaware host uses. It makes HostRoot a drop-in toolkit.Widget.
func (*HostRoot) HitTest ¶ added in v0.132.0
HitTest reports whether (px, py) falls on the wrapped tree.
func (*HostRoot) Invalidate ¶ added in v0.132.0
Invalidate records that widget w's appearance changed, damaging both where it was and where it now is. Call it from event handlers between frames. Invalidating a widget not in the tree is a no-op.
func (*HostRoot) OnEvent ¶ added in v0.132.0
OnEvent delivers an event to the wrapped tree. The application's own handlers are responsible for calling HostRoot.Invalidate on any widget whose appearance the event changed.
func (*HostRoot) RenderDamaged ¶ added in v0.132.0
RenderDamaged paints this frame into p (confined, per damaged rectangle, to the coalesced damage via the painter's clip seam) and returns the rectangles it repainted, in surface coordinates. The host packs+presents exactly their union. An empty result means nothing changed this frame (the host presents nothing). The returned slice is owned by the underlying Scene and is reused on the next call — the host must consume it before the next frame (it does: it presents the rects immediately).
func (*HostRoot) Scene ¶ added in v0.132.0
Scene returns the underlying retained scene, for introspection or advanced invalidation. Most callers only need HostRoot.Invalidate.
func (*HostRoot) SetBounds ¶ added in v0.132.0
SetBounds lays the tree out to b. A size/position change damages the whole old and new area (so a resize repaints correctly through the SAME incremental path — RenderDamaged then returns full-surface damage and repaints the fresh framebuffer completely); an unchanged bounds is a cheap no-op that neither re-lays-out the child nor invalidates.
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 ¶
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.
type Opaque ¶
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 ¶
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 ¶
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 ¶
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.
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 ¶
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 ¶
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 ¶
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.
type SelfDrawer ¶
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.