shiny/widget/node: split Paint into Paint and PaintBase.

Also introduce the Sheet widget.

Follow-up changes will implement smooth scrolling, where cached textures
are simply re-drawn at different offsets, instead of a window-sized
buffer being drawn on and uploaded from scratch on every paint cycle.

Change-Id: Iea291a064200cb658004846cd6cf075131644464
Reviewed-on: https://go-review.googlesource.com/25321
Reviewed-by: David Crawshaw <crawshaw@golang.org>
diff --git a/shiny/example/basicgl/main.go b/shiny/example/basicgl/main.go
index 3542e37..38442e6 100644
--- a/shiny/example/basicgl/main.go
+++ b/shiny/example/basicgl/main.go
@@ -40,13 +40,13 @@
 		defer t1.cleanup()
 		defer t2.cleanup()
 
-		body := flex.NewFlex(
+		body := widget.NewSheet(flex.NewFlex(
 			colorPatch(colornames.Green, unit.Pixels(50), unit.Pixels(50)),
 			widget.WithLayoutData(t1.w, flex.LayoutData{Grow: 1, Align: flex.AlignItemStretch}),
 			colorPatch(colornames.Blue, unit.Pixels(50), unit.Pixels(50)),
 			widget.WithLayoutData(t2.w, flex.LayoutData{MinSize: image.Point{80, 80}}),
 			colorPatch(colornames.Green, unit.Pixels(50), unit.Pixels(50)),
-		)
+		))
 
 		if err := widget.RunWindow(s, body, nil); err != nil {
 			log.Fatal(err)
diff --git a/shiny/example/gallery/main.go b/shiny/example/gallery/main.go
index f7cb66d..5cb247f 100644
--- a/shiny/example/gallery/main.go
+++ b/shiny/example/gallery/main.go
@@ -22,7 +22,6 @@
 	"golang.org/x/exp/shiny/screen"
 	"golang.org/x/exp/shiny/widget"
 	"golang.org/x/exp/shiny/widget/node"
-	"golang.org/x/exp/shiny/widget/theme"
 )
 
 var uniforms = [...]*image.Uniform{
@@ -56,21 +55,22 @@
 		if w.index == len(uniforms) {
 			w.index = 0
 		}
-		w.Mark(node.MarkNeedsPaint)
+		w.Mark(node.MarkNeedsPaintBase)
 	}
 	return node.Handled
 }
 
-func (w *custom) Paint(t *theme.Theme, dst *image.RGBA, origin image.Point) {
-	w.Marks.UnmarkNeedsPaint()
-	draw.Draw(dst, w.Rect.Add(origin), uniforms[w.index], image.Point{}, draw.Src)
+func (w *custom) PaintBase(ctx *node.PaintBaseContext, origin image.Point) error {
+	w.Marks.UnmarkNeedsPaintBase()
+	draw.Draw(ctx.Dst, w.Rect.Add(origin), uniforms[w.index], image.Point{}, draw.Src)
+	return nil
 }
 
 func main() {
 	log.SetFlags(0)
 	driver.Main(func(s screen.Screen) {
 		// TODO: create a bunch of standard widgets: buttons, labels, etc.
-		w := newCustom()
+		w := widget.NewSheet(newCustom())
 		if err := widget.RunWindow(s, w, nil); err != nil {
 			log.Fatal(err)
 		}
diff --git a/shiny/example/imageview/main.go b/shiny/example/imageview/main.go
index 4811ec8..a756d8d 100644
--- a/shiny/example/imageview/main.go
+++ b/shiny/example/imageview/main.go
@@ -57,8 +57,8 @@
 		if err != nil {
 			log.Fatal(err)
 		}
-		m := widget.NewImage(src, src.Bounds())
-		if err := widget.RunWindow(s, m, nil); err != nil {
+		w := widget.NewSheet(widget.NewImage(src, src.Bounds()))
+		if err := widget.RunWindow(s, w, nil); err != nil {
 			log.Fatal(err)
 		}
 	})
diff --git a/shiny/example/layout/main.go b/shiny/example/layout/main.go
index a04cd83..8b6d8a6 100644
--- a/shiny/example/layout/main.go
+++ b/shiny/example/layout/main.go
@@ -28,6 +28,7 @@
 
 	"golang.org/x/exp/shiny/unit"
 	"golang.org/x/exp/shiny/widget"
+	"golang.org/x/exp/shiny/widget/node"
 	"golang.org/x/exp/shiny/widget/theme"
 )
 
@@ -82,7 +83,10 @@
 	vf.Measure(t)
 	vf.Rect = rgba.Bounds()
 	vf.Layout(t)
-	vf.Paint(t, rgba, image.Point{})
+	vf.PaintBase(&node.PaintBaseContext{
+		Theme: t,
+		Dst:   rgba,
+	}, image.Point{})
 
 	// Encode to PNG.
 	out, err := os.Create("out.png")
diff --git a/shiny/example/textedit/main.go b/shiny/example/textedit/main.go
index e8bb9fb..d36152b 100644
--- a/shiny/example/textedit/main.go
+++ b/shiny/example/textedit/main.go
@@ -25,7 +25,7 @@
 func main() {
 	log.SetFlags(0)
 	driver.Main(func(s screen.Screen) {
-		w := widget.NewText(prideAndPrejudice)
+		w := widget.NewSheet(widget.NewText(prideAndPrejudice))
 		if err := widget.RunWindow(s, w, nil); err != nil {
 			log.Fatal(err)
 		}
diff --git a/shiny/screen/screen.go b/shiny/screen/screen.go
index cbc1cb4..c4b326b 100644
--- a/shiny/screen/screen.go
+++ b/shiny/screen/screen.go
@@ -248,6 +248,10 @@
 	// contents can be further modified, once all outstanding calls to Upload
 	// have returned.
 	//
+	// TODO: make it optional that a Buffer's contents is preserved after
+	// Upload? Undoing a swizzle is a non-trivial amount of work, and can be
+	// redundant if the next paint cycle starts by clearing the buffer.
+	//
 	// When uploading to a Window, there will not be any visible effect until
 	// Publish is called.
 	Upload(dp image.Point, src Buffer, sr image.Rectangle)
diff --git a/shiny/widget/glwidget/glwidget.go b/shiny/widget/glwidget/glwidget.go
index f598502..c80ea6e 100644
--- a/shiny/widget/glwidget/glwidget.go
+++ b/shiny/widget/glwidget/glwidget.go
@@ -12,7 +12,6 @@
 
 	"golang.org/x/exp/shiny/driver/gldriver"
 	"golang.org/x/exp/shiny/widget/node"
-	"golang.org/x/exp/shiny/widget/theme"
 	"golang.org/x/mobile/gl"
 )
 
@@ -81,14 +80,16 @@
 	return w
 }
 
-func (w *GL) Paint(t *theme.Theme, dst *image.RGBA, origin image.Point) {
+func (w *GL) PaintBase(ctx *node.PaintBaseContext, origin image.Point) error {
+	w.Marks.UnmarkNeedsPaintBase()
 	if w.Rect.Empty() {
-		return
+		return nil
 	}
-	w.dst = dst
+	w.dst = ctx.Dst
 	w.origin = origin
 	w.draw(w)
 	w.dst = nil
+	return nil
 }
 
 // Publish renders the default framebuffer of Ctx onto the area of the
diff --git a/shiny/widget/image.go b/shiny/widget/image.go
index b7e9e98..8a085ff 100644
--- a/shiny/widget/image.go
+++ b/shiny/widget/image.go
@@ -43,10 +43,10 @@
 	w.MeasuredSize = w.SrcRect.Size()
 }
 
-func (w *Image) Paint(t *theme.Theme, dst *image.RGBA, origin image.Point) {
-	w.Marks.UnmarkNeedsPaint()
+func (w *Image) PaintBase(ctx *node.PaintBaseContext, origin image.Point) error {
+	w.Marks.UnmarkNeedsPaintBase()
 	if w.Src == nil {
-		return
+		return nil
 	}
 
 	// wRect is the widget's layout rectangle, in dst's coordinate space.
@@ -57,5 +57,6 @@
 	// upper-left corner of wRect.
 	sRect := w.SrcRect.Add(wRect.Min.Sub(w.SrcRect.Min))
 
-	draw.Draw(dst, wRect.Intersect(sRect), w.Src, w.SrcRect.Min, draw.Over)
+	draw.Draw(ctx.Dst, wRect.Intersect(sRect), w.Src, w.SrcRect.Min, draw.Over)
+	return nil
 }
diff --git a/shiny/widget/label.go b/shiny/widget/label.go
index 858ba15..5fca977 100644
--- a/shiny/widget/label.go
+++ b/shiny/widget/label.go
@@ -40,15 +40,15 @@
 	w.MeasuredSize.Y = m.Ascent.Ceil() + m.Descent.Ceil()
 }
 
-func (w *Label) Paint(t *theme.Theme, dst *image.RGBA, origin image.Point) {
-	w.Marks.UnmarkNeedsPaint()
-	dst = dst.SubImage(w.Rect.Add(origin)).(*image.RGBA)
+func (w *Label) PaintBase(ctx *node.PaintBaseContext, origin image.Point) error {
+	w.Marks.UnmarkNeedsPaintBase()
+	dst := ctx.Dst.SubImage(w.Rect.Add(origin)).(*image.RGBA)
 	if dst.Bounds().Empty() {
-		return
+		return nil
 	}
 
-	face := t.AcquireFontFace(theme.FontFaceOptions{})
-	defer t.ReleaseFontFace(theme.FontFaceOptions{}, face)
+	face := ctx.Theme.AcquireFontFace(theme.FontFaceOptions{})
+	defer ctx.Theme.ReleaseFontFace(theme.FontFaceOptions{}, face)
 	m := face.Metrics()
 	ascent := m.Ascent.Ceil()
 
@@ -59,7 +59,7 @@
 
 	d := font.Drawer{
 		Dst:  dst,
-		Src:  tc.Uniform(t),
+		Src:  tc.Uniform(ctx.Theme),
 		Face: face,
 		Dot: fixed.Point26_6{
 			X: fixed.I(origin.X + w.Rect.Min.X),
@@ -67,4 +67,5 @@
 		},
 	}
 	d.DrawString(w.Text)
+	return nil
 }
diff --git a/shiny/widget/node/node.go b/shiny/widget/node/node.go
index dcb77ec..03ebfff 100644
--- a/shiny/widget/node/node.go
+++ b/shiny/widget/node/node.go
@@ -45,7 +45,9 @@
 	"image"
 
 	"golang.org/x/exp/shiny/gesture"
+	"golang.org/x/exp/shiny/screen"
 	"golang.org/x/exp/shiny/widget/theme"
+	"golang.org/x/image/math/f64"
 	"golang.org/x/mobile/event/mouse"
 )
 
@@ -88,15 +90,49 @@
 	// previously been set during the parent node's layout.
 	Layout(t *theme.Theme)
 
-	// Paint paints this node (and its children) onto a destination image.
+	// Paint paints this node (and its children). Painting is split into two
+	// passes: a base pass and an effects pass. The effects pass is often a
+	// no-op, and the bulk of the work is typically done in the base pass.
 	//
-	// origin is the parent widget's origin with respect to the dst image's
+	// The base pass paints onto an *image.RGBA pixel buffer and ancestor nodes
+	// may choose to re-use the result. For example, re-painting a text widget
+	// after scrolling may copy cached buffers at different offsets, instead of
+	// painting the text's glyphs onto a fresh buffer. Similarly, animating the
+	// scale and opacity of an overlay can re-use the buffer from a previous
+	// base pass.
+	//
+	// The effects pass paints that part of the widget that can not or should
+	// not be cached. For example, the border of a text widget shouldn't move
+	// on the screen when that text widget is scrolled. The effects pass does
+	// not have a destination RGBA pixel buffer, and is limited to what a
+	// screen.Drawer provides: affine-transformed textures and uniform fills.
+	//
+	// TODO: app-specific OpenGL, if available, should be part of the effects
+	// pass. Is that exposed via the screen.Drawer or by another mechanism?
+	//
+	// The Paint method may create base pass RGBA pixel buffers, by calling
+	// ctx.Screen.NewBuffer. Many implementations won't, and instead assume
+	// that PaintBase is recursively triggered by an ancestor node such as a
+	// widget.Sheet. If it does create those RGBA pixel buffers, it is also
+	// responsible for calling PaintBase on this node (and its children). In
+	// any case, the Paint method should then paint any effects. Many widgets
+	// will neither create their own buffers nor have any effects, so their
+	// Paint methods will simply be the default implemention: do nothing except
+	// call Paint on its children. As mentioned above, the bulk of the work is
+	// typically done in PaintBase.
+	//
+	// origin is the parent widget's origin with respect to the ctx.Src2Dst
+	// transformation matrix; this node's Embed.Rect.Add(origin) will be its
+	// position and size in pre-transformed coordinate space.
+	Paint(ctx *PaintContext, origin image.Point) error
+
+	// PaintBase paints the base pass of this node (and its children) onto an
+	// RGBA pixel buffer.
+	//
+	// origin is the parent widget's origin with respect to the ctx.Dst image's
 	// origin; this node's Embed.Rect.Add(origin) will be its position and size
-	// in dst's coordinate space.
-	//
-	// TODO: add a clip rectangle? Or rely on the RGBA.SubImage method to pass
-	// smaller dst images?
-	Paint(t *theme.Theme, dst *image.RGBA, origin image.Point)
+	// in ctx.Dst's coordinate space.
+	PaintBase(ctx *PaintBaseContext, origin image.Point) error
 
 	// Mark adds the given marks to this node. It calls OnChildMarked on its
 	// parent if new marks were added.
@@ -119,6 +155,27 @@
 
 }
 
+// PaintContext is the context for the Node.Paint method.
+type PaintContext struct {
+	Theme   *theme.Theme
+	Screen  screen.Screen
+	Drawer  screen.Drawer
+	Src2Dst f64.Aff3
+
+	// TODO: add a clip rectangle?
+
+	// TODO: add the DrawContext from the lifecycle event?
+}
+
+// PaintBaseContext is the context for the Node.PaintBase method.
+type PaintBaseContext struct {
+	Theme *theme.Theme
+	Dst   *image.RGBA
+
+	// TODO: add a clip rectangle? Or rely on the RGBA.SubImage method to pass
+	// smaller Dst images?
+}
+
 // LeafEmbed is designed to be embedded in struct types for nodes with no
 // children.
 type LeafEmbed struct{ Embed }
@@ -133,8 +190,14 @@
 
 func (m *LeafEmbed) Layout(t *theme.Theme) {}
 
-func (m *LeafEmbed) Paint(t *theme.Theme, dst *image.RGBA, origin image.Point) {
+func (m *LeafEmbed) Paint(ctx *PaintContext, origin image.Point) error {
 	m.Marks.UnmarkNeedsPaint()
+	return nil
+}
+
+func (m *LeafEmbed) PaintBase(ctx *PaintBaseContext, origin image.Point) error {
+	m.Marks.UnmarkNeedsPaintBase()
+	return nil
 }
 
 func (m *LeafEmbed) OnChildMarked(child Node, newMarks Marks) {}
@@ -170,11 +233,20 @@
 	}
 }
 
-func (m *ShellEmbed) Paint(t *theme.Theme, dst *image.RGBA, origin image.Point) {
+func (m *ShellEmbed) Paint(ctx *PaintContext, origin image.Point) error {
 	m.Marks.UnmarkNeedsPaint()
 	if c := m.FirstChild; c != nil {
-		c.Wrapper.Paint(t, dst, origin.Add(m.Rect.Min))
+		return c.Wrapper.Paint(ctx, origin.Add(m.Rect.Min))
 	}
+	return nil
+}
+
+func (m *ShellEmbed) PaintBase(ctx *PaintBaseContext, origin image.Point) error {
+	m.Marks.UnmarkNeedsPaintBase()
+	if c := m.FirstChild; c != nil {
+		return c.Wrapper.PaintBase(ctx, origin.Add(m.Rect.Min))
+	}
+	return nil
 }
 
 func (m *ShellEmbed) OnChildMarked(child Node, newMarks Marks) {
@@ -217,12 +289,26 @@
 	}
 }
 
-func (m *ContainerEmbed) Paint(t *theme.Theme, dst *image.RGBA, origin image.Point) {
+func (m *ContainerEmbed) Paint(ctx *PaintContext, origin image.Point) error {
 	m.Marks.UnmarkNeedsPaint()
 	origin = origin.Add(m.Rect.Min)
 	for c := m.FirstChild; c != nil; c = c.NextSibling {
-		c.Wrapper.Paint(t, dst, origin)
+		if err := c.Wrapper.Paint(ctx, origin); err != nil {
+			return err
+		}
 	}
+	return nil
+}
+
+func (m *ContainerEmbed) PaintBase(ctx *PaintBaseContext, origin image.Point) error {
+	m.Marks.UnmarkNeedsPaintBase()
+	origin = origin.Add(m.Rect.Min)
+	for c := m.FirstChild; c != nil; c = c.NextSibling {
+		if err := c.Wrapper.PaintBase(ctx, origin); err != nil {
+			return err
+		}
+	}
+	return nil
 }
 
 func (m *ContainerEmbed) OnChildMarked(child Node, newMarks Marks) {
@@ -382,11 +468,15 @@
 
 	// MarkNeedsPaint marks this node as needing a Paint call.
 	MarkNeedsPaint = Marks(1 << 1)
-	// TODO: have separate notions of 'base' and 'top' paint passes.
+
+	// MarkNeedsPaintBase marks this node as needing a PaintBase call.
+	MarkNeedsPaintBase = Marks(1 << 2)
 )
 
 func (m Marks) NeedsMeasureLayout() bool { return m&MarkNeedsMeasureLayout != 0 }
 func (m Marks) NeedsPaint() bool         { return m&MarkNeedsPaint != 0 }
+func (m Marks) NeedsPaintBase() bool     { return m&MarkNeedsPaintBase != 0 }
 
 func (m *Marks) UnmarkNeedsMeasureLayout() { *m &^= MarkNeedsMeasureLayout }
 func (m *Marks) UnmarkNeedsPaint()         { *m &^= MarkNeedsPaint }
+func (m *Marks) UnmarkNeedsPaintBase()     { *m &^= MarkNeedsPaintBase }
diff --git a/shiny/widget/sheet.go b/shiny/widget/sheet.go
new file mode 100644
index 0000000..407a9de
--- /dev/null
+++ b/shiny/widget/sheet.go
@@ -0,0 +1,105 @@
+// Copyright 2016 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package widget
+
+import (
+	"image"
+	"image/draw"
+
+	"golang.org/x/exp/shiny/screen"
+	"golang.org/x/exp/shiny/widget/node"
+)
+
+// TODO: scrolling.
+
+// Sheet is a shell widget that provides *image.RGBA pixel buffers (analogous
+// to blank sheets of paper) for its descendent widgets to paint on, via their
+// PaintBase methods. Such buffers may be cached and their contents re-used for
+// multiple paints, which can make scrolling and animation smoother and more
+// efficient.
+//
+// A simple app may have only one Sheet, near the root of its widget tree. A
+// more complicated app may have multiple Sheets. For example, consider a text
+// editor consisting of a small header bar and a large text widget. Those two
+// nodes may be backed by two separate Sheets, since scrolling the latter
+// should not scroll the former.
+type Sheet struct {
+	node.ShellEmbed
+	buf screen.Buffer
+	tex screen.Texture
+}
+
+// NewSheet returns a new Sheet widget.
+func NewSheet(inner node.Node) *Sheet {
+	w := &Sheet{}
+	w.Wrapper = w
+	if inner != nil {
+		w.Insert(inner, nil)
+	}
+	return w
+}
+
+func (w *Sheet) Paint(ctx *node.PaintContext, origin image.Point) (retErr error) {
+	w.Marks.UnmarkNeedsPaint()
+	c := w.FirstChild
+	if c == nil {
+		if w.buf != nil {
+			w.buf.Release()
+			w.buf = nil
+			w.tex.Release()
+			w.tex = nil
+		}
+		return nil
+	}
+
+	fresh, size := false, w.Rect.Size()
+	if w.buf != nil && w.buf.Size() != size {
+		w.buf.Release()
+		w.buf = nil
+		w.tex.Release()
+		w.tex = nil
+	}
+	if w.buf == nil {
+		w.buf, retErr = ctx.Screen.NewBuffer(size)
+		if retErr != nil {
+			return retErr
+		}
+		w.tex, retErr = ctx.Screen.NewTexture(size)
+		if retErr != nil {
+			w.buf.Release()
+			w.buf = nil
+			return retErr
+		}
+		fresh = true
+	}
+	if fresh || c.Marks.NeedsPaintBase() {
+		c.Wrapper.PaintBase(&node.PaintBaseContext{
+			Theme: ctx.Theme,
+			Dst:   w.buf.RGBA(),
+		}, image.Point{})
+	}
+
+	w.tex.Upload(image.Point{}, w.buf, w.buf.Bounds())
+	// TODO: should draw.Over be configurable?
+	ctx.Drawer.Draw(ctx.Src2Dst, w.tex, w.tex.Bounds(), draw.Over, nil)
+
+	return c.Wrapper.Paint(ctx, origin.Add(w.Rect.Min))
+}
+
+func (w *Sheet) PaintBase(ctx *node.PaintBaseContext, origin image.Point) error {
+	w.Marks.UnmarkNeedsPaintBase()
+	// Do not recursively call PaintBase on our children. We create our own
+	// buffers, and Sheet.Paint will call PaintBase with our PaintBaseContext
+	// instead of our ancestor's.
+	return nil
+}
+
+func (w *Sheet) OnChildMarked(child node.Node, newMarks node.Marks) {
+	if newMarks&node.MarkNeedsPaintBase != 0 {
+		newMarks &^= node.MarkNeedsPaintBase
+		newMarks |= node.MarkNeedsPaint
+	}
+	w.Mark(newMarks)
+}
diff --git a/shiny/widget/text.go b/shiny/widget/text.go
index e5b386e..9b87053 100644
--- a/shiny/widget/text.go
+++ b/shiny/widget/text.go
@@ -64,23 +64,23 @@
 	w.frame.SetMaxWidth(fixed.I(w.Rect.Dx() - 2*padding))
 }
 
-func (w *Text) Paint(t *theme.Theme, dst *image.RGBA, origin image.Point) {
-	w.Marks.UnmarkNeedsPaint()
-	dst = dst.SubImage(w.Rect.Add(origin)).(*image.RGBA)
+func (w *Text) PaintBase(ctx *node.PaintBaseContext, origin image.Point) error {
+	w.Marks.UnmarkNeedsPaintBase()
+	dst := ctx.Dst.SubImage(w.Rect.Add(origin)).(*image.RGBA)
 	if dst.Bounds().Empty() {
-		return
+		return nil
 	}
 
-	face := t.AcquireFontFace(theme.FontFaceOptions{})
-	defer t.ReleaseFontFace(theme.FontFaceOptions{}, face)
+	face := ctx.Theme.AcquireFontFace(theme.FontFaceOptions{})
+	defer ctx.Theme.ReleaseFontFace(theme.FontFaceOptions{}, face)
 	m := face.Metrics()
 	ascent := m.Ascent.Ceil()
 	descent := m.Descent.Ceil()
 	height := m.Height.Ceil()
 
-	padding := t.Pixels(unit.Ems(0.5)).Ceil()
+	padding := ctx.Theme.Pixels(unit.Ems(0.5)).Ceil()
 
-	draw.Draw(dst, dst.Bounds(), t.GetPalette().Background(), image.Point{}, draw.Src)
+	draw.Draw(dst, dst.Bounds(), ctx.Theme.GetPalette().Background(), image.Point{}, draw.Src)
 
 	minDotY := fixed.I(dst.Bounds().Min.Y - descent)
 	maxDotY := fixed.I(dst.Bounds().Max.Y + ascent)
@@ -88,7 +88,7 @@
 	x0 := fixed.I(origin.X + w.Rect.Min.X + padding)
 	d := font.Drawer{
 		Dst:  dst,
-		Src:  t.GetPalette().Foreground(),
+		Src:  ctx.Theme.GetPalette().Foreground(),
 		Face: face,
 		Dot: fixed.Point26_6{
 			X: x0,
@@ -100,7 +100,7 @@
 		for l := p.FirstLine(f); l != nil; l = l.Next(f) {
 			if d.Dot.Y > minDotY {
 				if d.Dot.Y >= maxDotY {
-					return
+					return nil
 				}
 				for b := l.FirstBox(f); b != nil; b = b.Next(f) {
 					d.DrawBytes(b.TrimmedText(f))
@@ -111,4 +111,11 @@
 			d.Dot.Y += fixed.I(height)
 		}
 	}
+	return nil
+}
+
+func (w *Text) Paint(ctx *node.PaintContext, origin image.Point) error {
+	// TODO: draw an optional border, whose color depends on whether w has the
+	// keyboard focus.
+	return w.LeafEmbed.Paint(ctx, origin)
 }
diff --git a/shiny/widget/uniform.go b/shiny/widget/uniform.go
index 175aad9..c8fadec 100644
--- a/shiny/widget/uniform.go
+++ b/shiny/widget/uniform.go
@@ -31,13 +31,15 @@
 	return w
 }
 
-func (w *Uniform) Paint(t *theme.Theme, dst *image.RGBA, origin image.Point) {
-	w.Marks.UnmarkNeedsPaint()
+func (w *Uniform) PaintBase(ctx *node.PaintBaseContext, origin image.Point) error {
+	w.Marks.UnmarkNeedsPaintBase()
 	if w.ThemeColor != nil {
+		src := w.ThemeColor.Uniform(ctx.Theme)
 		// TODO: should draw.Src be draw.Over?
-		draw.Draw(dst, w.Rect.Add(origin), w.ThemeColor.Uniform(t), image.Point{}, draw.Src)
+		draw.Draw(ctx.Dst, w.Rect.Add(origin), src, image.Point{}, draw.Src)
 	}
 	if c := w.FirstChild; c != nil {
-		c.Wrapper.Paint(t, dst, origin.Add(w.Rect.Min))
+		return c.Wrapper.PaintBase(ctx, origin.Add(w.Rect.Min))
 	}
+	return nil
 }
diff --git a/shiny/widget/widget.go b/shiny/widget/widget.go
index 374be03..4c0f3ce 100644
--- a/shiny/widget/widget.go
+++ b/shiny/widget/widget.go
@@ -15,6 +15,7 @@
 	"golang.org/x/exp/shiny/unit"
 	"golang.org/x/exp/shiny/widget/node"
 	"golang.org/x/exp/shiny/widget/theme"
+	"golang.org/x/image/math/f64"
 	"golang.org/x/mobile/event/lifecycle"
 	"golang.org/x/mobile/event/mouse"
 	"golang.org/x/mobile/event/paint"
@@ -73,7 +74,6 @@
 // A nil opts is valid and means to use the default option values.
 func RunWindow(s screen.Screen, root node.Node, opts *RunWindowOptions) error {
 	var (
-		buf screen.Buffer
 		nwo *screen.NewWindowOptions
 		t   *theme.Theme
 	)
@@ -81,12 +81,6 @@
 		nwo = &opts.NewWindowOptions
 		t = &opts.Theme
 	}
-	defer func() {
-		if buf != nil {
-			buf.Release()
-		}
-	}()
-
 	w, err := s.NewWindow(nwo)
 	if err != nil {
 		return err
@@ -115,6 +109,7 @@
 
 		switch e := e.(type) {
 		case lifecycle.Event:
+			// TODO: drop buffers and textures when we're not visible.
 			if e.To == lifecycle.StageDead {
 				return nil
 			}
@@ -123,23 +118,22 @@
 			root.OnInputEvent(e, image.Point{})
 
 		case paint.Event:
-			if buf != nil {
-				root.Paint(t, buf.RGBA(), image.Point{})
-				w.Upload(image.Point{}, buf, buf.Bounds())
+			ctx := &node.PaintContext{
+				Theme:  t,
+				Screen: s,
+				Drawer: w,
+				Src2Dst: f64.Aff3{
+					1, 0, 0,
+					0, 1, 0,
+				},
+			}
+			if err := root.Paint(ctx, image.Point{}); err != nil {
+				return err
 			}
 			w.Publish()
 			paintPending = false
 
 		case size.Event:
-			if buf != nil {
-				buf.Release()
-			}
-			var err error
-			buf, err = s.NewBuffer(e.Size())
-			if err != nil {
-				return err
-			}
-
 			if dpi := float64(e.PixelsPerPt) * unit.PointsPerInch; dpi != t.GetDPI() {
 				newT := new(theme.Theme)
 				if t != nil {