~eliasnaur/gio

252e05876660b09c7bfa0b443723dc67923827af — Elias Naur 2 years ago 7b6e1ce
all: serialize ops

Pros:
- Much less per-frame garbage
- Allow future preprocessing of ops while building it
- Much fewer interface calls and pointer chasing
- Allow future serialization of ops for remote rendering

Cons:
- Slightly clumsier API

Signed-off-by: Elias Naur <mail@eliasnaur.com>
M apps/gophers/main.go => apps/gophers/main.go +64 -67
@@ 162,6 162,7 @@ func init() {

func (a *App) run() error {
	a.w.Profiling = *stats
	ops := new(ui.Ops)
	for a.w.IsAlive() {
		select {
		case users := <-a.updateUsers:


@@ 194,23 195,23 @@ func (a *App) run() error {
					}
				}
			case app.Draw:
				ops.Reset()
				a.cfg = e.Config
				a.faces.Cfg = a.cfg
				cs := layout.ExactConstraints(a.w.Size())
				root, _ := a.Layout(cs)
				a.Layout(ops, cs)
				if a.w.Profiling {
					op, _ := layout.Align(
					layout.Align(
						layout.NE,
						layout.Margin(a.cfg,
							layout.Margins{Top: ui.Dp(16)},
							text.Label{Src: textColor, Face: a.face(fonts.mono, 8), Text: a.w.Timings()},
						),
					).Layout(cs)
					root = ui.Ops{root, op}
					).Layout(ops, cs)
				}
				a.w.Draw(root)
				a.w.SetTextInput(a.kqueue.Frame(root))
				a.pqueue.Frame(root)
				a.w.Draw(ops)
				a.w.SetTextInput(a.kqueue.Frame(ops))
				a.pqueue.Frame(ops)
				a.faces.Frame()
			}
		}


@@ 361,12 362,12 @@ func (a *App) face(f *sfnt.Font, size float32) text.Face {
	return a.faces.For(f, ui.Sp(size))
}

func (a *App) Layout(cs layout.Constraints) (ui.Op, layout.Dimens) {
func (a *App) Layout(ops *ui.Ops, cs layout.Constraints) layout.Dimens {
	if a.selectedUser == nil {
		return a.layoutUsers(cs)
		return a.layoutUsers(ops, cs)
	} else {
		a.selectedUser.Update(a.cfg, a.pqueue)
		return a.selectedUser.Layout(cs)
		return a.selectedUser.Layout(ops, cs)
	}
}



@@ 387,22 388,21 @@ func (up *userPage) Update(cfg *ui.Config, pqueue pointer.Events) {
	up.commitsList.Scroll(up.cfg, pqueue)
}

func (up *userPage) Layout(cs layout.Constraints) (ui.Op, layout.Dimens) {
func (up *userPage) Layout(ops *ui.Ops, cs layout.Constraints) layout.Dimens {
	l := up.commitsList
	var ops ui.Ops
	if l.Dragging() {
		ops = append(ops, key.OpHideInput{})
		key.OpHideInput{}.Add(ops)
	}
	select {
	case commits := <-up.commitsResult:
		up.commits = commits
	default:
	}
	for i, ok := l.Init(cs, len(up.commits)); ok; i, ok = l.Index() {
	for i, ok := l.Init(ops, cs, len(up.commits)); ok; i, ok = l.Index() {
		l.Elem(up.commit(i))
	}
	op, dims := l.Layout()
	return append(ops, op), dims
	dims := l.Layout()
	return dims
}

func (up *userPage) commit(index int) layout.Widget {


@@ 414,9 414,9 @@ func (up *userPage) commit(index int) layout.Widget {
	label := text.Label{Src: textColor, Face: up.faces.For(fonts.regular, ui.Sp(12)), Text: msg}
	return layout.Margin(c,
		layout.Margins{Top: ui.Dp(16), Right: ui.Dp(8), Left: ui.Dp(8)},
		layout.F(func(cs layout.Constraints) (ui.Op, layout.Dimens) {
		layout.F(func(ops *ui.Ops, cs layout.Constraints) layout.Dimens {
			return (&layout.Flex{Axis: layout.Horizontal, MainAxisAlignment: layout.Start, CrossAxisAlignment: layout.Start}).
				Init(cs).
				Init(ops, cs).
				Rigid(avatar).
				Flexible(-1, 1, layout.Fit, layout.Margin(c, layout.Margins{Left: ui.Dp(8)}, label)).
				Layout()


@@ 446,10 446,10 @@ func (up *userPage) fetchCommits() {
	}()
}

func (a *App) layoutUsers(cs layout.Constraints) (ui.Op, layout.Dimens) {
func (a *App) layoutUsers(ops *ui.Ops, cs layout.Constraints) layout.Dimens {
	c := a.cfg
	a.fab.Update(c, a.pqueue)
	st := (&layout.Stack{Alignment: layout.Center}).Init(cs).
	st := (&layout.Stack{Alignment: layout.Center}).Init(ops, cs).
		Rigid(layout.Align(
			layout.SE,
			layout.Margin(c,


@@ 459,8 459,8 @@ func (a *App) layoutUsers(cs layout.Constraints) (ui.Op, layout.Dimens) {
		))
	a.edit.Update(c, a.pqueue, a.kqueue)
	a.edit2.Update(c, a.pqueue, a.kqueue)
	return st.Expand(0, layout.F(func(cs layout.Constraints) (ui.Op, layout.Dimens) {
		return (&layout.Flex{Axis: layout.Vertical, MainAxisAlignment: layout.Start, CrossAxisAlignment: layout.Stretch}).Init(cs).
	return st.Expand(0, layout.F(func(ops *ui.Ops, cs layout.Constraints) layout.Dimens {
		return (&layout.Flex{Axis: layout.Vertical, MainAxisAlignment: layout.Start, CrossAxisAlignment: layout.Stretch}).Init(ops, cs).
			Rigid(layout.Margin(c,
				layout.EqualMargins(ui.Dp(16)),
				layout.Sized(c, ui.Dp(0), ui.Dp(200), a.edit),


@@ 469,8 469,8 @@ func (a *App) layoutUsers(cs layout.Constraints) (ui.Op, layout.Dimens) {
				layout.Margins{Bottom: ui.Dp(16), Left: ui.Dp(16), Right: ui.Dp(16)},
				a.edit2,
			)).
			Rigid(layout.F(func(cs layout.Constraints) (ui.Op, layout.Dimens) {
				return (&layout.Stack{Alignment: layout.Center}).Init(cs).
			Rigid(layout.F(func(ops *ui.Ops, cs layout.Constraints) layout.Dimens {
				return (&layout.Stack{Alignment: layout.Center}).Init(ops, cs).
					Rigid(layout.Margin(c,
						layout.Margins{Top: ui.Dp(16), Right: ui.Dp(8), Bottom: ui.Dp(8), Left: ui.Dp(8)},
						text.Label{Src: rgb(0x888888), Face: a.face(fonts.regular, 9), Text: "GOPHERS"},


@@ 490,35 490,34 @@ func (a *ActionButton) Update(c *ui.Config, q pointer.Events) {
	a.btnClicker.Update(q)
}

func (a *ActionButton) Layout(cs layout.Constraints) (ui.Op, layout.Dimens) {
func (a *ActionButton) Layout(ops *ui.Ops, cs layout.Constraints) layout.Dimens {
	c := a.cfg
	fl := (&layout.Flex{Axis: layout.Vertical, MainAxisAlignment: layout.Start, CrossAxisAlignment: layout.End, MainAxisSize: layout.Min}).Init(cs)
	fl := (&layout.Flex{Axis: layout.Vertical, MainAxisAlignment: layout.Start, CrossAxisAlignment: layout.End, MainAxisSize: layout.Min}).Init(ops, cs)
	fabCol := brandColor
	fl.Rigid(layout.Margin(c,
		layout.Margins{Top: ui.Dp(4)},
		layout.F(func(cs layout.Constraints) (ui.Op, layout.Dimens) {
			op, dims := fab(c, a.sendIco.image(c), fabCol, ui.Dp(56)).Layout(cs)
			ops := ui.Ops{op, a.btnClicker.Op(&gesture.Ellipse{dims.Size})}
			return ops, dims
		layout.F(func(ops *ui.Ops, cs layout.Constraints) layout.Dimens {
			dims := fab(c, a.sendIco.image(c), fabCol, ui.Dp(56)).Layout(ops, cs)
			a.btnClicker.Op(ops, &gesture.Ellipse{dims.Size})
			return dims
		}),
	))
	return fl.Layout()
}

func (a *App) layoutContributors() layout.Widget {
	return layout.F(func(cs layout.Constraints) (ui.Op, layout.Dimens) {
	return layout.F(func(ops *ui.Ops, cs layout.Constraints) layout.Dimens {
		c := a.cfg
		l := a.usersList
		l.Scroll(c, a.pqueue)
		var ops ui.Ops
		if l.Dragging() {
			ops = append(ops, key.OpHideInput{})
			key.OpHideInput{}.Add(ops)
		}
		for i, ok := l.Init(cs, len(a.users)); ok; i, ok = l.Index() {
		for i, ok := l.Init(ops, cs, len(a.users)); ok; i, ok = l.Index() {
			l.Elem(a.user(c, i))
		}
		op, dims := l.Layout()
		return append(ops, op), dims
		dims := l.Layout()
		return dims
	})
}



@@ 532,13 531,13 @@ func (a *App) user(c *ui.Config, index int) layout.Widget {
		}
	}
	avatar := clipCircle(layout.Sized(a.cfg, sz, sz, widget.Image{Src: u.avatar, Rect: u.avatar.Bounds()}))
	return layout.F(func(cs layout.Constraints) (ui.Op, layout.Dimens) {
		elem := (&layout.Flex{Axis: layout.Vertical, MainAxisAlignment: layout.Start, CrossAxisAlignment: layout.Start}).Init(cs)
		elem.Rigid(layout.F(func(cs layout.Constraints) (ui.Op, layout.Dimens) {
			op, dims := layout.Margin(c,
	return layout.F(func(ops *ui.Ops, cs layout.Constraints) layout.Dimens {
		elem := (&layout.Flex{Axis: layout.Vertical, MainAxisAlignment: layout.Start, CrossAxisAlignment: layout.Start}).Init(ops, cs)
		elem.Rigid(layout.F(func(ops *ui.Ops, cs layout.Constraints) layout.Dimens {
			dims := layout.Margin(c,
				layout.EqualMargins(ui.Dp(8)),
				layout.F(func(cs layout.Constraints) (ui.Op, layout.Dimens) {
					return centerRowOpts().Init(cs).
				layout.F(func(ops *ui.Ops, cs layout.Constraints) layout.Dimens {
					return centerRowOpts().Init(ops, cs).
						Rigid(layout.Margin(c, layout.Margins{Right: ui.Dp(8)}, avatar)).
						Rigid(column(
							baseline(


@@ 555,9 554,9 @@ func (a *App) user(c *ui.Config, index int) layout.Widget {
						)).
						Layout()
				}),
			).Layout(cs)
			ops := ui.Ops{op, click.Op(&gesture.Rect{dims.Size})}
			return ops, dims
			).Layout(ops, cs)
			click.Op(ops, &gesture.Rect{dims.Size})
			return dims
		}))
		return elem.Layout()
	})


@@ 588,8 587,8 @@ func baseline(widgets ...layout.Widget) layout.Widget {
}

func flex(f *layout.Flex, widgets ...layout.Widget) layout.Widget {
	return layout.F(func(cs layout.Constraints) (ui.Op, layout.Dimens) {
		f.Init(cs)
	return layout.F(func(ops *ui.Ops, cs layout.Constraints) layout.Dimens {
		f.Init(ops, cs)
		for _, w := range widgets {
			f.Rigid(w)
		}


@@ 598,41 597,39 @@ func flex(f *layout.Flex, widgets ...layout.Widget) layout.Widget {
}

func clipCircle(w layout.Widget) layout.Widget {
	return layout.F(func(cs layout.Constraints) (ui.Op, layout.Dimens) {
		op, dims := w.Layout(cs)
	return layout.F(func(ops *ui.Ops, cs layout.Constraints) layout.Dimens {
		ops.Begin()
		dims := w.Layout(ops, cs)
		block := ops.End()
		max := dims.Size.X
		if dy := dims.Size.Y; dy > max {
			max = dy
		}
		szf := float32(max)
		rr := szf * .5
		op = gdraw.OpClip{
			Path: rrect(szf, szf, rr, rr, rr, rr),
			Op:   op,
		}
		return op, dims
		ops.Begin()
		gdraw.OpClip{Path: rrect(szf, szf, rr, rr, rr, rr)}.Add(ops)
		block.Add(ops)
		ops.End().Add(ops)
		return dims
	})
}

func fab(c *ui.Config, ico, col image.Image, size ui.Value) layout.Widget {
	return layout.F(func(cs layout.Constraints) (ui.Op, layout.Dimens) {
	return layout.F(func(ops *ui.Ops, cs layout.Constraints) layout.Dimens {
		szf := c.Pixels(size)
		sz := int(szf + .5)
		rr := szf * .5
		dp := image.Point{X: (sz - ico.Bounds().Dx()) / 2, Y: (sz - ico.Bounds().Dy()) / 2}
		dims := image.Point{X: sz, Y: sz}
		op := gdraw.OpClip{
			Path: rrect(szf, szf, rr, rr, rr, rr),
			Op: ui.Ops{
				gdraw.OpImage{Rect: f32.Rectangle{Max: f32.Point{X: float32(sz), Y: float32(sz)}}, Src: col, SrcRect: col.Bounds()},
				gdraw.OpImage{
					Rect:    toRectF(ico.Bounds().Add(dp)),
					Src:     ico,
					SrcRect: ico.Bounds(),
				},
			},
		}
		return op, layout.Dimens{Size: dims}
		gdraw.OpClip{Path: rrect(szf, szf, rr, rr, rr, rr)}.Add(ops)
		gdraw.OpImage{Rect: f32.Rectangle{Max: f32.Point{X: float32(sz), Y: float32(sz)}}, Src: col, SrcRect: col.Bounds()}.Add(ops)
		gdraw.OpImage{
			Rect:    toRectF(ico.Bounds().Add(dp)),
			Src:     ico,
			SrcRect: ico.Bounds(),
		}.Add(ops)
		return layout.Dimens{Size: dims}
	})
}


M apps/hello/hello.go => apps/hello/hello.go +4 -2
@@ 46,14 46,16 @@ func loop(w *app.Window) {
	maroon := &image.Uniform{color.RGBA{127, 0, 0, 255}}
	face := faces.For(regular, ui.Sp(72))
	message := "Hello, Gio"
	ops := new(ui.Ops)
	for w.IsAlive() {
		e := <-w.Events()
		switch e := e.(type) {
		case app.Draw:
			faces.Cfg = e.Config
			cs := layout.ExactConstraints(w.Size())
			root, _ := (text.Label{Src: maroon, Face: face, Alignment: text.Center, Text: message}).Layout(cs)
			w.Draw(root)
			ops.Reset()
			(text.Label{Src: maroon, Face: face, Alignment: text.Center, Text: message}).Layout(ops, cs)
			w.Draw(ops)
			faces.Frame()
		}
	}

M ui/app/internal/gpu/gpu.go => ui/app/internal/gpu/gpu.go +101 -75
@@ 14,6 14,7 @@ import (
	"gioui.org/ui/app/internal/gl"
	gdraw "gioui.org/ui/draw"
	"gioui.org/ui/f32"
	"gioui.org/ui/internal/ops"
	"gioui.org/ui/internal/path"
	"golang.org/x/image/draw"
)


@@ 31,13 32,13 @@ type GPU struct {
	refreshErr chan error
	stop       chan struct{}
	stopped    chan struct{}
	ops        ops
	ops        drawOps
}

type frame struct {
	collectStats bool
	viewport     image.Point
	ops          ops
	ops          drawOps
}

type frameResult struct {


@@ 54,7 55,8 @@ type renderer struct {
	intersections packer
}

type ops struct {
type drawOps struct {
	reader     ops.Reader
	cache      *resourceCache
	viewport   image.Point
	clearColor [3]float32


@@ 66,6 68,14 @@ type ops struct {
	pathOps   []*pathOp
}

type drawState struct {
	clip  f32.Rectangle
	t     ui.Transform
	cpath *pathOp
	rect  bool
	z     int
}

type pathOp struct {
	off f32.Point
	// clip is the union of all


@@ 298,12 308,13 @@ func (g *GPU) Refresh() {
	g.setErr(<-g.refreshErr)
}

func (g *GPU) Draw(profile bool, viewport image.Point, op ui.Op) {
func (g *GPU) Draw(profile bool, viewport image.Point, root *ui.Ops) {
	if g.err != nil {
		return
	}
	g.Flush()
	g.ops.collect(g.cache, op, viewport)
	g.ops.reset(g.cache, viewport)
	g.ops.collect(g.cache, root, viewport)
	g.frames <- frame{profile, viewport, g.ops}
	g.drawing = true
}


@@ 629,84 640,99 @@ func floor(v float32) int {
	}
}

func (ops *ops) collect(cache *resourceCache, op ui.Op, viewport image.Point) {
	ops.clearColor = [3]float32{1.0, 1.0, 1.0}
	ops.cache = cache
	ops.viewport = viewport
	ops.imageOps = ops.imageOps[:0]
	ops.zimageOps = ops.zimageOps[:0]
	ops.pathOps = ops.pathOps[:0]
func (d *drawOps) reset(cache *resourceCache, viewport image.Point) {
	d.clearColor = [3]float32{1.0, 1.0, 1.0}
	d.cache = cache
	d.viewport = viewport
	d.imageOps = d.imageOps[:0]
	d.zimageOps = d.zimageOps[:0]
	d.pathOps = d.pathOps[:0]
}

func (d *drawOps) collect(cache *resourceCache, root *ui.Ops, viewport image.Point) {
	d.reset(cache, viewport)
	clip := f32.Rectangle{
		Max: f32.Point{X: float32(viewport.X), Y: float32(viewport.Y)},
	}
	ops.collectOp(op, clip, ui.Transform{}, nil, true, 0)
	d.reader.Reset(root.Data(), root.Refs())
	d.collectOps(&d.reader, clip, ui.Transform{}, nil, true, 0)
}

func (ops *ops) collectOp(op ui.Op, clip f32.Rectangle, t ui.Transform, cpath *pathOp, rect bool, z int) int {
	type childOp interface {
		ChildOp() ui.Op
	}
	switch op := op.(type) {
	case ui.OpTransform:
		t := t.Mul(op.Transform)
		z = ops.collectOp(op.ChildOp(), clip, t, cpath, rect, z)
	case gdraw.OpClip:
		data := op.Path.Data().(*path.Path)
		off := t.Transform(f32.Point{})
		clip := clip.Intersect(data.Bounds.Add(off))
		if clip.Empty() {
			break
		}
		cpath := &pathOp{
			parent: cpath,
			off:    off,
		}
		if len(data.Vertices) > 0 {
			rect = false
			cpath.path = data
			ops.pathOps = append(ops.pathOps, cpath)
		}
		z = ops.collectOp(op.ChildOp(), clip, t, cpath, rect, z)
	case gdraw.OpImage:
		off := t.Transform(f32.Point{})
		clip := clip.Intersect(op.Rect.Add(off))
		if clip.Empty() {
			break
		}
		bounds := boundRectF(clip)
		mat := materialFor(ops.cache, op, off, bounds)
		if bounds.Min == (image.Point{}) && bounds.Max == ops.viewport && mat.opaque && mat.material == materialColor {
			// The image is a uniform opaque color and takes up the whole screen.
			// Scrap images up to and including this image and set clear color.
			ops.zimageOps = ops.zimageOps[:0]
			ops.imageOps = ops.imageOps[:0]
			z = 0
			copy(ops.clearColor[:], mat.color[:3])
func (d *drawOps) collectOps(r *ops.Reader, clip f32.Rectangle, t ui.Transform, cpath *pathOp, rect bool, z int) int {
loop:
	for {
		data, ok := r.Decode()
		if !ok {
			break
		}
		z++
		// Assume 16-bit depth buffer.
		const zdepth = 1 << 16
		// Convert z to window-space, assuming depth range [0;1].
		zf := float32(z)*2/zdepth - 1.0
		img := imageOp{
			z:        zf,
			path:     cpath,
			off:      off,
			clip:     bounds,
			material: mat,
		}
		if rect && img.material.opaque {
			ops.zimageOps = append(ops.zimageOps, img)
		} else {
			ops.imageOps = append(ops.imageOps, img)
		}
	case ui.Ops:
		for _, op := range op {
			z = ops.collectOp(op, clip, t, cpath, rect, z)
		switch ops.OpType(data[0]) {
		case ops.TypeTransform:
			var op ui.OpTransform
			op.Decode(data)
			t = t.Mul(op.Transform)
		case ops.TypeClip:
			var op gdraw.OpClip
			op.Decode(data, r.Refs)
			if op.Path == nil {
				clip = f32.Rectangle{}
				continue
			}
			data := op.Path.Data().(*path.Path)
			off := t.Transform(f32.Point{})
			clip = clip.Intersect(data.Bounds.Add(off))
			if clip.Empty() {
				continue
			}
			cpath = &pathOp{
				parent: cpath,
				off:    off,
			}
			if len(data.Vertices) > 0 {
				rect = false
				cpath.path = data
				d.pathOps = append(d.pathOps, cpath)
			}
		case ops.TypeImage:
			var op gdraw.OpImage
			op.Decode(data, r.Refs)
			off := t.Transform(f32.Point{})
			clip := clip.Intersect(op.Rect.Add(off))
			if clip.Empty() {
				continue
			}
			bounds := boundRectF(clip)
			mat := materialFor(d.cache, op, off, bounds)
			if bounds.Min == (image.Point{}) && bounds.Max == d.viewport && mat.opaque && mat.material == materialColor {
				// The image is a uniform opaque color and takes up the whole screen.
				// Scrap images up to and including this image and set clear color.
				d.zimageOps = d.zimageOps[:0]
				d.imageOps = d.imageOps[:0]
				z = 0
				copy(d.clearColor[:], mat.color[:3])
				continue
			}
			z++
			// Assume 16-bit depth buffer.
			const zdepth = 1 << 16
			// Convert z to window-space, assuming depth range [0;1].
			zf := float32(z)*2/zdepth - 1.0
			img := imageOp{
				z:        zf,
				path:     cpath,
				off:      off,
				clip:     bounds,
				material: mat,
			}
			if rect && img.material.opaque {
				d.zimageOps = append(d.zimageOps, img)
			} else {
				d.imageOps = append(d.imageOps, img)
			}
		case ops.TypePush:
			z = d.collectOps(r, clip, t, cpath, rect, z)
		case ops.TypePop:
			break loop
		}
	case childOp:
		z = ops.collectOp(op.ChildOp(), clip, t, cpath, rect, z)
	}
	return z
}

M ui/app/window.go => ui/app/window.go +27 -28
@@ 11,6 11,7 @@ import (

	"gioui.org/ui"
	"gioui.org/ui/app/internal/gpu"
	"gioui.org/ui/internal/ops"
	"gioui.org/ui/key"
	"gioui.org/ui/pointer"
)


@@ 41,6 42,8 @@ type Window struct {
	hasNextFrame bool
	nextFrame    time.Time
	delayedDraw  *time.Timer

	reader ops.Reader
}

// driver is the interface for the platform implementation


@@ 89,7 92,7 @@ func (w *Window) Err() error {
	return w.err
}

func (w *Window) Draw(root ui.Op) {
func (w *Window) Draw(root *ui.Ops) {
	if !w.IsAlive() {
		return
	}


@@ 132,13 135,35 @@ func (w *Window) Draw(root ui.Op) {
		w.timings = fmt.Sprintf("t:%7s %s", frameDur, w.gpu.Timings())
		w.setNextFrame(time.Time{})
	}
	if t, ok := collectRedraws(root); ok {
	w.reader.Reset(root.Data(), root.Refs())
	if t, ok := collectRedraws(&w.reader); ok {
		w.setNextFrame(t)
	}
	w.updateAnimation()
	w.gpu.Draw(w.Profiling, size, root)
}

func collectRedraws(r *ops.Reader) (time.Time, bool) {
	var t time.Time
	redraw := false
	for {
		data, ok := r.Decode()
		if !ok {
			break
		}
		switch ops.OpType(data[0]) {
		case ops.TypeRedraw:
			var op ui.OpRedraw
			op.Decode(data)
			if !redraw || op.At.Before(t) {
				redraw = true
				t = op.At
			}
		}
	}
	return t, redraw
}

func (w *Window) Redraw() {
	if !w.IsAlive() {
		return


@@ 256,29 281,3 @@ func (w *Window) event(e Event) {
		close(w.events)
	}
}

func collectRedraws(op ui.Op) (time.Time, bool) {
	type childOp interface {
		ChildOp() ui.Op
	}
	switch op := op.(type) {
	case ui.Ops:
		var earliest time.Time
		var valid bool
		for _, op := range op {
			if t, ok := collectRedraws(op); ok {
				if !valid || t.Before(earliest) {
					valid = true
					earliest = t
				}
			}
		}
		return earliest, valid
	case ui.OpRedraw:
		return op.At, true
	case childOp:
		return collectRedraws(op.ChildOp())
	default:
		return time.Time{}, false
	}
}

M ui/draw/draw.go => ui/draw/draw.go +58 -9
@@ 3,11 3,13 @@
package draw

import (
	"encoding/binary"
	"image"
	"math"

	"gioui.org/ui"
	"gioui.org/ui/f32"
	"gioui.org/ui/internal/ops"
	"gioui.org/ui/internal/path"
)



@@ 17,18 19,65 @@ type OpImage struct {
	SrcRect image.Rectangle
}

func (i OpImage) Add(o *ui.Ops) {
	data := make([]byte, ops.TypeImageLen)
	data[0] = byte(ops.TypeImage)
	bo := binary.LittleEndian
	ref := o.Ref(i.Src)
	bo.PutUint32(data[1:], uint32(ref))
	bo.PutUint32(data[5:], math.Float32bits(i.Rect.Min.X))
	bo.PutUint32(data[9:], math.Float32bits(i.Rect.Min.Y))
	bo.PutUint32(data[13:], math.Float32bits(i.Rect.Max.X))
	bo.PutUint32(data[17:], math.Float32bits(i.Rect.Max.Y))
	bo.PutUint32(data[21:], uint32(i.SrcRect.Min.X))
	bo.PutUint32(data[25:], uint32(i.SrcRect.Min.Y))
	bo.PutUint32(data[29:], uint32(i.SrcRect.Max.X))
	bo.PutUint32(data[33:], uint32(i.SrcRect.Max.Y))
	o.Write(data)
}

func (i *OpImage) Decode(d []byte, refs []interface{}) {
	bo := binary.LittleEndian
	if ops.OpType(d[0]) != ops.TypeImage {
		panic("invalid op")
	}
	ref := int(bo.Uint32(d[1:]))
	r := f32.Rectangle{
		Min: f32.Point{
			X: math.Float32frombits(bo.Uint32(d[5:])),
			Y: math.Float32frombits(bo.Uint32(d[9:])),
		},
		Max: f32.Point{
			X: math.Float32frombits(bo.Uint32(d[13:])),
			Y: math.Float32frombits(bo.Uint32(d[17:])),
		},
	}
	sr := image.Rectangle{
		Min: image.Point{
			X: int(bo.Uint32(d[21:])),
			Y: int(bo.Uint32(d[25:])),
		},
		Max: image.Point{
			X: int(bo.Uint32(d[29:])),
			Y: int(bo.Uint32(d[33:])),
		},
	}
	*i = OpImage{
		Rect:    r,
		Src:     refs[ref].(image.Image),
		SrcRect: sr,
	}
}

func (OpImage) ImplementsOp() {}

// ClipRect returns a special case of OpClip
// that clips to a pixel aligned rectangular area.
func ClipRect(r image.Rectangle, op ui.Op) OpClip {
	return OpClip{
		Path: &Path{
			data: &path.Path{
				Bounds: toRectF(r),
			},
// RectPath constructs a path corresponding to
// a pixel aligned rectangular area.
func RectPath(r image.Rectangle) *Path {
	return &Path{
		data: &path.Path{
			Bounds: toRectF(r),
		},
		Op: op,
	}
}


M ui/draw/path.go => ui/draw/path.go +21 -4
@@ 3,16 3,17 @@
package draw

import (
	"encoding/binary"
	"math"

	"gioui.org/ui"
	"gioui.org/ui/f32"
	"gioui.org/ui/internal/ops"
	"gioui.org/ui/internal/path"
)

type OpClip struct {
	Path *Path
	Op   ui.Op
}

type Path struct {


@@ 33,11 34,25 @@ func (p *Path) Data() interface{} {
	return p.data
}

func (p OpClip) ChildOp() ui.Op {
	return p.Op
func (c OpClip) Add(o *ui.Ops) {
	data := make([]byte, ops.TypeClipLen)
	data[0] = byte(ops.TypeClip)
	bo := binary.LittleEndian
	ref := o.Ref(c.Path)
	bo.PutUint32(data[1:], uint32(ref))
	o.Write(data)
}

func (p OpClip) ImplementsOp() {}
func (c *OpClip) Decode(d []byte, refs []interface{}) {
	bo := binary.LittleEndian
	if ops.OpType(d[0]) != ops.TypeClip {
		panic("invalid op")
	}
	ref := int(bo.Uint32(d[1:]))
	*c = OpClip{
		Path: refs[ref].(*Path),
	}
}

// MoveTo moves the pen to the given position.
func (p *PathBuilder) Move(to f32.Point) {


@@ 265,3 280,5 @@ func (p *PathBuilder) Path() *Path {
	}
	return data
}

func (p OpClip) ImplementsOp() {}

M ui/gesture/gestures.go => ui/gesture/gestures.go +7 -6
@@ 83,8 83,9 @@ const (
	thresholdVelocity = 1
)

func (c *Click) Op(a pointer.Area) pointer.OpHandler {
	return pointer.OpHandler{Area: a, Key: c}
func (c *Click) Op(ops *ui.Ops, a pointer.Area) {
	op := pointer.OpHandler{Area: a, Key: c}
	op.Add(ops)
}

func (c *Click) Update(q pointer.Events) []ClickEvent {


@@ 115,12 116,12 @@ func (c *Click) Update(q pointer.Events) []ClickEvent {
	return events
}

func (s *Scroll) Op(a pointer.Area) ui.Op {
func (s *Scroll) Op(ops *ui.Ops, a pointer.Area) {
	oph := pointer.OpHandler{Area: a, Key: s, Grab: s.grab}
	if !s.flinger.Active() {
		return oph
	oph.Add(ops)
	if s.flinger.Active() {
		ui.OpRedraw{}.Add(ops)
	}
	return ui.Ops{oph, ui.OpRedraw{}}
}

func (s *Scroll) Stop() {

A ui/internal/ops/ops.go => ui/internal/ops/ops.go +112 -0
@@ 0,0 1,112 @@
package ops

import (
	"encoding/binary"
)

type Reader struct {
	pc    int
	stack []block
	Refs  []interface{}
	data  []byte

	pseudoOp [1]byte
}

type block struct {
	retPC int
	endPC int
}

type OpType byte

const (
	TypeBlockDef OpType = iota
	TypeBlock
	TypeTransform
	TypeLayer
	TypeRedraw
	TypeClip
	TypeImage
	TypePointerHandler
	TypeKeyHandler
	TypeHideInput
	TypePush
	TypePop
)

const (
	TypeBlockDefLen       = 1 + 4
	TypeBlockLen          = 1 + 4
	TypeTransformLen      = 1 + 4*2
	TypeLayerLen          = 1
	TypeRedrawLen         = 1 + 8
	TypeClipLen           = 1 + 4
	TypeImageLen          = 1 + 4 + 4*4 + 4*4
	TypePointerHandlerLen = 1 + 4 + 4 + 1
	TypeKeyHandlerLen     = 1 + 4 + 1
	TypeHideInputLen      = 1
	TypePushLen           = 1
	TypePopLen            = 1
)

var typeLengths = [...]int{
	TypeBlockDefLen,
	TypeBlockLen,
	TypeTransformLen,
	TypeLayerLen,
	TypeRedrawLen,
	TypeClipLen,
	TypeImageLen,
	TypePointerHandlerLen,
	TypeKeyHandlerLen,
	TypeHideInputLen,
	TypePushLen,
	TypePopLen,
}

// Reset start reading from the op list.
func (r *Reader) Reset(data []byte, refs []interface{}) {
	r.Refs = refs
	r.data = data
	r.stack = r.stack[:0]
	r.pc = 0
}

func (r *Reader) Decode() ([]byte, bool) {
	bo := binary.LittleEndian
	for {
		if r.pc == len(r.data) {
			return nil, false
		}
		if len(r.stack) > 0 {
			b := r.stack[len(r.stack)-1]
			if r.pc == b.endPC {
				r.pc = b.retPC
				r.stack = r.stack[:len(r.stack)-1]
				r.pseudoOp[0] = byte(TypePop)
				return r.pseudoOp[:], true
			}
		}
		t := OpType(r.data[r.pc])
		n := typeLengths[t]
		data := r.data[r.pc : r.pc+n]
		switch t {
		case TypeBlock:
			blockIdx := int(bo.Uint32(data[1:]))
			if OpType(r.data[blockIdx]) != TypeBlockDef {
				panic("invalid block reference")
			}
			blockLen := int(bo.Uint32(r.data[blockIdx+1:]))
			r.stack = append(r.stack, block{r.pc + n, blockIdx + blockLen})
			r.pc = blockIdx + TypeBlockDefLen
			r.pseudoOp[0] = byte(TypePush)
			return r.pseudoOp[:], true
		case TypeBlockDef:
			r.pc += int(bo.Uint32(data[1:]))
			continue
		}
		r.pc += n
		return data, true
	}
}

M ui/key/key.go => ui/key/key.go +36 -0
@@ 2,6 2,13 @@

package key

import (
	"encoding/binary"

	"gioui.org/ui"
	"gioui.org/ui/internal/ops"
)

type OpHandler struct {
	Key   Key
	Focus bool


@@ 63,6 70,35 @@ const (
	NamePageDown       = '⇟'
)

func (h OpHandler) Add(o *ui.Ops) {
	data := make([]byte, ops.TypeKeyHandlerLen)
	data[0] = byte(ops.TypeKeyHandler)
	bo := binary.LittleEndian
	if h.Focus {
		data[1] = 1
	}
	bo.PutUint32(data[2:], uint32(o.Ref(h.Key)))
	o.Write(data)
}

func (h *OpHandler) Decode(d []byte, refs []interface{}) {
	bo := binary.LittleEndian
	if ops.OpType(d[0]) != ops.TypeKeyHandler {
		panic("invalid op")
	}
	key := int(bo.Uint32(d[2:]))
	*h = OpHandler{
		Focus: d[1] != 0,
		Key:   refs[key].(Key),
	}
}

func (h OpHideInput) Add(o *ui.Ops) {
	data := make([]byte, ops.TypeHideInputLen)
	data[0] = byte(ops.TypeHideInput)
	o.Write(data)
}

func (OpHandler) ImplementsOp()   {}
func (OpHideInput) ImplementsOp() {}


M ui/key/queue.go => ui/key/queue.go +35 -28
@@ 4,12 4,14 @@ package key

import (
	"gioui.org/ui"
	"gioui.org/ui/internal/ops"
)

type Queue struct {
	focus    Key
	events   []Event
	handlers map[Key]bool
	reader   ops.Reader
}

type listenerPriority uint8


@@ 21,9 23,10 @@ const (
	priNewFocus
)

func (q *Queue) Frame(op ui.Op) TextInputState {
func (q *Queue) Frame(root *ui.Ops) TextInputState {
	q.events = q.events[:0]
	f, pri, hide := resolveFocus(op, q.focus)
	q.reader.Reset(root.Data(), root.Refs())
	f, pri, hide := resolveFocus(&q.reader, q.focus)
	changed := f != nil && f != q.focus
	for k, active := range q.handlers {
		if !active || changed {


@@ 71,39 74,43 @@ func (q *Queue) For(k Key) []Event {
	return q.events
}

func resolveFocus(op ui.Op, focus Key) (Key, listenerPriority, bool) {
	type childOp interface {
		ChildOp() ui.Op
	}
func resolveFocus(r *ops.Reader, focus Key) (Key, listenerPriority, bool) {
	var k Key
	var pri listenerPriority
	var hide bool
	switch op := op.(type) {
	case ui.Ops:
		for i := len(op) - 1; i >= 0; i-- {
			newK, newPri, h := resolveFocus(op[i], focus)
loop:
	for {
		data, ok := r.Decode()
		if !ok {
			break
		}
		switch ops.OpType(data[0]) {
		case ops.TypeKeyHandler:
			var op OpHandler
			op.Decode(data, r.Refs)
			var newPri listenerPriority
			switch {
			case op.Focus:
				newPri = priNewFocus
			case op.Key == focus:
				newPri = priCurrentFocus
			default:
				newPri = priDefault
			}
			if newPri >= pri {
				k, pri = op.Key, newPri
			}
		case ops.TypeHideInput:
			hide = true
		case ops.TypePush:
			newK, newPri, h := resolveFocus(r, focus)
			hide = hide || h
			if newPri > pri {
			if newPri >= pri {
				k, pri = newK, newPri
			}
		case ops.TypePop:
			break loop
		}
	case OpHandler:
		var newPri listenerPriority
		switch {
		case op.Focus:
			newPri = priNewFocus
		case op.Key == focus:
			newPri = priCurrentFocus
		default:
			newPri = priDefault
		}
		if newPri > pri {
			k, pri = op.Key, newPri
		}
	case OpHideInput:
		hide = true
	case childOp:
		return resolveFocus(op.ChildOp(), focus)
	}
	return k, pri, hide
}

M ui/layout/flex.go => ui/layout/flex.go +29 -23
@@ 15,20 15,20 @@ type Flex struct {
	CrossAxisAlignment CrossAxisAlignment
	MainAxisSize       MainAxisSize

	cs Constraints
	ops *ui.Ops
	cs  Constraints

	children    []flexChild
	taken       int
	maxCross    int
	maxBaseline int

	ccache  [10]flexChild
	opCache [10]ui.Op
	ccache [10]flexChild
}

type flexChild struct {
	op   ui.Op
	dims Dimens
	block ui.OpBlock
	dims  Dimens
}

type MainAxisSize uint8


@@ 60,7 60,8 @@ const (
	Stretch
)

func (f *Flex) Init(cs Constraints) *Flex {
func (f *Flex) Init(ops *ui.Ops, cs Constraints) *Flex {
	f.ops = ops
	f.cs = cs
	if f.children == nil {
		f.children = f.ccache[:0]


@@ 78,7 79,10 @@ func (f *Flex) Rigid(w Widget) *Flex {
		mainMax -= f.taken
	}
	cs := axisConstraints(f.Axis, Constraint{Max: mainMax}, f.crossConstraintChild(f.cs))
	op, dims := w.Layout(cs)
	f.ops.Begin()
	ui.OpLayer{}.Add(f.ops)
	dims := w.Layout(f.ops, cs)
	block := f.ops.End()
	f.taken += axisMain(f.Axis, dims.Size)
	if c := axisCross(f.Axis, dims.Size); c > f.maxCross {
		f.maxCross = c


@@ 86,7 90,7 @@ func (f *Flex) Rigid(w Widget) *Flex {
	if b := dims.Baseline; b > f.maxBaseline {
		f.maxBaseline = b
	}
	f.children = append(f.children, flexChild{op, dims})
	f.children = append(f.children, flexChild{block, dims})
	return f
}



@@ 101,7 105,10 @@ func (f *Flex) Flexible(idx int, flex float32, mode FlexMode, w Widget) *Flex {
		submainc.Min = submainc.Max
	}
	cs := axisConstraints(f.Axis, submainc, f.crossConstraintChild(f.cs))
	op, dims := w.Layout(cs)
	f.ops.Begin()
	ui.OpLayer{}.Add(f.ops)
	dims := w.Layout(f.ops, cs)
	block := f.ops.End()
	f.taken += axisMain(f.Axis, dims.Size)
	if c := axisCross(f.Axis, dims.Size); c > f.maxCross {
		f.maxCross = c


@@ 109,15 116,16 @@ func (f *Flex) Flexible(idx int, flex float32, mode FlexMode, w Widget) *Flex {
	if b := dims.Baseline; b > f.maxBaseline {
		f.maxBaseline = b
	}
	f.children = append(f.children, flexChild{op, dims})
	if idx < 0 {
		idx += len(f.children)
		idx += len(f.children) + 1
	}
	f.children[idx], f.children[len(f.children)-1] = f.children[len(f.children)-1], f.children[idx]
	f.children = append(f.children, flexChild{})
	copy(f.children[idx+1:], f.children[idx:])
	f.children[idx] = flexChild{block, dims}
	return f
}

func (f *Flex) Layout() (ui.Op, Dimens) {
func (f *Flex) Layout() Dimens {
	mainc := axisMainConstraint(f.Axis, f.cs)
	crossSize := axisCrossConstraint(f.Axis, f.cs).Constrain(f.maxCross)
	var space int


@@ 140,13 148,7 @@ func (f *Flex) Layout() (ui.Op, Dimens) {
	case SpaceAround:
		mainSize += space / (len(f.children) * 2)
	}
	var ops ui.Ops
	if len(f.children) > len(f.opCache) {
		ops = make([]ui.Op, len(f.children))
	} else {
		ops = f.opCache[:len(f.children)]
	}
	for i, child := range f.children {
	for _, child := range f.children {
		dims := child.dims
		b := dims.Baseline
		var cross int


@@ 160,8 162,12 @@ func (f *Flex) Layout() (ui.Op, Dimens) {
				cross = f.maxBaseline - b
			}
		}
		off := ui.Offset(toPointF(axisPoint(f.Axis, mainSize, cross)))
		ops[i] = ui.OpLayer{Op: ui.OpTransform{Transform: off, Op: child.op}}
		f.ops.Begin()
		ui.OpTransform{
			Transform: ui.Offset(toPointF(axisPoint(f.Axis, mainSize, cross))),
		}.Add(f.ops)
		child.block.Add(f.ops)
		f.ops.End().Add(f.ops)
		mainSize += axisMain(f.Axis, dims.Size)
		switch f.MainAxisAlignment {
		case SpaceEvenly:


@@ 187,7 193,7 @@ func (f *Flex) Layout() (ui.Op, Dimens) {
	if baseline == 0 {
		baseline = sz.Y
	}
	return ops, Dimens{Size: sz, Baseline: baseline}
	return Dimens{Size: sz, Baseline: baseline}
}

func axisPoint(a Axis, main, cross int) image.Point {

M ui/layout/list.go => ui/layout/list.go +36 -24
@@ 12,8 12,8 @@ import (
)

type scrollChild struct {
	op   ui.Op
	size image.Point
	size  image.Point
	block ui.OpBlock
}

type List struct {


@@ 24,12 24,14 @@ type List struct {
	// The distance scrolled since last call to Init.
	Distance int

	area      gesture.Rect
	scroll    gesture.Scroll
	scrollDir int

	offset int
	first  int

	ops *ui.Ops
	cs  Constraints
	len int



@@ 38,7 40,6 @@ type List struct {
	elem     func(w Widget)

	size image.Point
	ops  ui.Ops
}

type Interface interface {


@@ 46,19 47,20 @@ type Interface interface {
	At(i int) Widget
}

func (l *List) Init(cs Constraints, len int) (int, bool) {
func (l *List) Init(ops *ui.Ops, cs Constraints, len int) (int, bool) {
	l.maxSize = 0
	l.children = l.children[:0]
	l.ops = ops
	l.cs = cs
	l.len = len
	l.elem = nil
	if l.first > len {
		l.first = len
	}
	l.ops = l.ops[:0]
	if len == 0 {
		return 0, false
	}
	l.scroll.Op(ops, &l.area)
	return l.Index()
}



@@ 82,9 84,9 @@ func (l *List) Index() (int, bool) {
	return i, ok
}

func (l *List) Layout() (ui.Op, Dimens) {
	ops := append(ui.Ops{l.scroll.Op(&gesture.Rect{l.size})}, l.ops...)
	return ops, Dimens{Size: l.size}
func (l *List) Layout() Dimens {
	l.area.Size = l.size
	return Dimens{Size: l.size}
}

func (l *List) next() (int, bool) {


@@ 116,21 118,28 @@ func (l *List) Elem(w Widget) {
}

func (l *List) backward(w Widget) {
	subcs := axisConstraints(l.Axis, Constraint{Max: ui.Inf}, l.crossConstraintChild(l.cs))
	l.first--
	op, dims := w.Layout(subcs)
	mainSize := axisMain(l.Axis, dims.Size)
	child := l.add(w)
	mainSize := axisMain(l.Axis, child.size)
	l.offset += mainSize
	l.maxSize += mainSize
	l.children = append([]scrollChild{{op, dims.Size}}, l.children...)
	l.children = append([]scrollChild{child}, l.children...)
}

func (l *List) forward(w Widget) {
	subcs := axisConstraints(l.Axis, Constraint{Max: ui.Inf}, l.crossConstraintChild(l.cs))
	op, dims := w.Layout(subcs)
	mainSize := axisMain(l.Axis, dims.Size)
	child := l.add(w)
	mainSize := axisMain(l.Axis, child.size)
	l.maxSize += mainSize
	l.children = append(l.children, scrollChild{op, dims.Size})
	l.children = append(l.children, child)
}

func (l *List) add(w Widget) scrollChild {
	subcs := axisConstraints(l.Axis, Constraint{Max: ui.Inf}, l.crossConstraintChild(l.cs))
	l.ops.Begin()
	ui.OpLayer{}.Add(l.ops)
	dims := w.Layout(l.ops, subcs)
	block := l.ops.End()
	return scrollChild{dims.Size, block}
}

func (l *List) draw() {


@@ 176,14 185,17 @@ func (l *List) draw() {
		if min < 0 {
			min = 0
		}
		op := draw.ClipRect(
			image.Rectangle{
				Min: axisPoint(l.Axis, min, -ui.Inf),
				Max: axisPoint(l.Axis, max, ui.Inf),
			},
			ui.OpTransform{Transform: ui.Offset(toPointF(axisPoint(l.Axis, pos, cross))), Op: child.op},
		)
		l.ops = append(l.ops, ui.OpLayer{Op: op})
		r := image.Rectangle{
			Min: axisPoint(l.Axis, min, -ui.Inf),
			Max: axisPoint(l.Axis, max, ui.Inf),
		}
		l.ops.Begin()
		draw.OpClip{Path: draw.RectPath(r)}.Add(l.ops)
		ui.OpTransform{
			Transform: ui.Offset(toPointF(axisPoint(l.Axis, pos, cross))),
		}.Add(l.ops)
		child.block.Add(l.ops)
		l.ops.End().Add(l.ops)
		pos += axisMain(l.Axis, sz)
	}
	atStart := l.first == 0 && l.offset <= 0

M ui/layout/simple.go => ui/layout/simple.go +25 -19
@@ 10,7 10,7 @@ import (
)

type Widget interface {
	Layout(cs Constraints) (ui.Op, Dimens)
	Layout(ops *ui.Ops, cs Constraints) Dimens
}

type Constraints struct {


@@ 29,7 29,7 @@ type Dimens struct {

type Axis uint8

type F func(cs Constraints) (ui.Op, Dimens)
type F func(ops *ui.Ops, cs Constraints) Dimens

const (
	Horizontal Axis = iota


@@ 73,8 73,8 @@ func ExactConstraints(size image.Point) Constraints {
	}
}

func (f F) Layout(cs Constraints) (ui.Op, Dimens) {
	return f(cs)
func (f F) Layout(ops *ui.Ops, cs Constraints) Dimens {
	return f(ops, cs)
}

type Margins struct {


@@ 82,7 82,7 @@ type Margins struct {
}

func Margin(c *ui.Config, m Margins, w Widget) Widget {
	return F(func(cs Constraints) (ui.Op, Dimens) {
	return F(func(ops *ui.Ops, cs Constraints) Dimens {
		mcs := cs
		t, r, b, l := int(c.Pixels(m.Top)+0.5), int(c.Pixels(m.Right)+0.5), int(c.Pixels(m.Bottom)+0.5), int(c.Pixels(m.Left)+0.5)
		if mcs.Width.Max != ui.Inf {


@@ 105,10 105,11 @@ func Margin(c *ui.Config, m Margins, w Widget) Widget {
				mcs.Height.Max = mcs.Height.Min
			}
		}

		op, dims := w.Layout(mcs)
		op = ui.OpTransform{Transform: ui.Offset(toPointF(image.Point{X: l, Y: t})), Op: op}
		return op, Dimens{
		ops.Begin()
		ui.OpTransform{Transform: ui.Offset(toPointF(image.Point{X: l, Y: t}))}.Add(ops)
		dims := w.Layout(ops, mcs)
		ops.End().Add(ops)
		return Dimens{
			Size:     cs.Constrain(dims.Size.Add(image.Point{X: r + l, Y: t + b})),
			Baseline: dims.Baseline + t,
		}


@@ 124,7 125,7 @@ func isInf(v ui.Value) bool {
}

func Capped(c *ui.Config, maxWidth, maxHeight ui.Value, wt Widget) Widget {
	return F(func(cs Constraints) (ui.Op, Dimens) {
	return F(func(ops *ui.Ops, cs Constraints) Dimens {
		if !isInf(maxWidth) {
			mw := int(c.Pixels(maxWidth) + .5)
			if mw < cs.Width.Min {


@@ 143,12 144,12 @@ func Capped(c *ui.Config, maxWidth, maxHeight ui.Value, wt Widget) Widget {
				cs.Height.Max = mh
			}
		}
		return wt.Layout(cs)
		return wt.Layout(ops, cs)
	})
}

func Sized(c *ui.Config, width, height ui.Value, wt Widget) Widget {
	return F(func(cs Constraints) (ui.Op, Dimens) {
	return F(func(ops *ui.Ops, cs Constraints) Dimens {
		if h := int(c.Pixels(height) + 0.5); h != 0 {
			if cs.Height.Min < h {
				cs.Height.Min = h


@@ 165,25 166,27 @@ func Sized(c *ui.Config, width, height ui.Value, wt Widget) Widget {
				cs.Width.Max = w
			}
		}
		return wt.Layout(cs)
		return wt.Layout(ops, cs)
	})
}

func Expand(w Widget) Widget {
	return F(func(cs Constraints) (ui.Op, Dimens) {
	return F(func(ops *ui.Ops, cs Constraints) Dimens {
		if cs.Height.Max != ui.Inf {
			cs.Height.Min = cs.Height.Max
		}
		if cs.Width.Max != ui.Inf {
			cs.Width.Min = cs.Width.Max
		}
		return w.Layout(cs)
		return w.Layout(ops, cs)
	})
}

func Align(alignment Direction, w Widget) Widget {
	return F(func(cs Constraints) (ui.Op, Dimens) {
		op, dims := w.Layout(cs.Loose())
	return F(func(ops *ui.Ops, cs Constraints) Dimens {
		ops.Begin()
		dims := w.Layout(ops, cs.Loose())
		block := ops.End()
		sz := dims.Size
		if cs.Width.Max != ui.Inf {
			sz.X = cs.Width.Max


@@ 204,8 207,11 @@ func Align(alignment Direction, w Widget) Widget {
		case SW, S, SE:
			p.Y = sz.Y - dims.Size.Y
		}
		op = ui.OpTransform{Transform: ui.Offset(toPointF(p)), Op: op}
		return op, Dimens{
		ops.Begin()
		ui.OpTransform{Transform: ui.Offset(toPointF(p))}.Add(ops)
		block.Add(ops)
		ops.End().Add(ops)
		return Dimens{
			Size:     sz,
			Baseline: dims.Baseline,
		}

M ui/layout/stack.go => ui/layout/stack.go +29 -22
@@ 11,18 11,18 @@ import (
type Stack struct {
	Alignment Direction

	ops      *ui.Ops
	cs       Constraints
	children []stackChild
	maxSZ    image.Point
	baseline int

	ccache  [10]stackChild
	opCache [10]ui.Op
	ccache [10]stackChild
}

type stackChild struct {
	op   ui.Op
	dims Dimens
	block ui.OpBlock
	dims  Dimens
}

type Direction uint8


@@ 38,26 38,31 @@ const (
	W
)

func (s *Stack) Init(cs Constraints) *Stack {
func (s *Stack) Init(ops *ui.Ops, cs Constraints) *Stack {
	if s.children == nil {
		s.children = s.ccache[:0]
	}
	s.children = s.children[:0]
	s.maxSZ = image.Point{}
	s.baseline = 0
	s.ops = ops
	s.cs = cs
	return s
}

func (s *Stack) Rigid(w Widget) *Stack {
	op, dims := w.Layout(s.cs)
	s.ops.Begin()
	ui.OpLayer{}.Add(s.ops)
	dims := w.Layout(s.ops, s.cs)
	b := s.ops.End()
	if w := dims.Size.X; w > s.maxSZ.X {
		s.maxSZ.X = w
	}
	if h := dims.Size.Y; h > s.maxSZ.Y {
		s.maxSZ.Y = h
	}
	s.add(op, dims)
	s.addjustBaseline(dims)
	s.children = append(s.children, stackChild{b, dims})
	return s
}



@@ 66,16 71,21 @@ func (s *Stack) Expand(idx int, w Widget) *Stack {
		Width:  Constraint{Min: s.maxSZ.X, Max: s.maxSZ.X},
		Height: Constraint{Min: s.maxSZ.Y, Max: s.maxSZ.Y},
	}
	s.add(w.Layout(cs))
	s.ops.Begin()
	ui.OpLayer{}.Add(s.ops)
	dims := w.Layout(s.ops, cs)
	b := s.ops.End()
	s.addjustBaseline(dims)
	if idx < 0 {
		idx += len(s.children)
		idx += len(s.children) + 1
	}
	s.children[idx], s.children[len(s.children)-1] = s.children[len(s.children)-1], s.children[idx]
	s.children = append(s.children, stackChild{})
	copy(s.children[idx+1:], s.children[idx:])
	s.children[idx] = stackChild{b, dims}
	return s
}

func (s *Stack) add(op ui.Op, dims Dimens) {
	s.children = append(s.children, stackChild{op, dims})
func (s *Stack) addjustBaseline(dims Dimens) {
	if s.baseline == 0 {
		if b := dims.Baseline; b != dims.Size.Y {
			s.baseline = b


@@ 83,14 93,8 @@ func (s *Stack) add(op ui.Op, dims Dimens) {
	}
}

func (s *Stack) Layout() (ui.Op, Dimens) {
	var ops ui.Ops
	if len(s.children) > len(s.opCache) {
		ops = make([]ui.Op, len(s.children))
	} else {
		ops = s.opCache[:len(s.children)]
	}
	for i, ch := range s.children {
func (s *Stack) Layout() Dimens {
	for _, ch := range s.children {
		sz := ch.dims.Size
		var p image.Point
		switch s.Alignment {


@@ 105,13 109,16 @@ func (s *Stack) Layout() (ui.Op, Dimens) {
		case SW, S, SE:
			p.Y = s.maxSZ.Y - sz.Y
		}
		ops[i] = ui.OpLayer{Op: ui.OpTransform{Transform: ui.Offset(toPointF(p)), Op: ch.op}}
		s.ops.Begin()
		ui.OpTransform{Transform: ui.Offset(toPointF(p))}.Add(s.ops)
		ch.block.Add(s.ops)
		s.ops.End().Add(s.ops)
	}
	b := s.baseline
	if b == 0 {
		b = s.maxSZ.Y
	}
	return ops, Dimens{
	return Dimens{
		Size:     s.maxSZ,
		Baseline: b,
	}

A ui/ops.go => ui/ops.go +79 -0
@@ 0,0 1,79 @@
package ui

import (
	"encoding/binary"

	"gioui.org/ui/internal/ops"
)

// Ops hold a list of serialized Ops.
type Ops struct {
	// Stack of block start indices.
	stack []int
	// Serialized ops.
	data []byte
	// Op references.
	refs []interface{}
}

type OpBlock struct {
	idx int
}

// Begin a block of ops.
func (o *Ops) Begin() {
	o.stack = append(o.stack, o.Size())
	data := make([]byte, ops.TypeBlockDefLen)
	data[0] = byte(ops.TypeBlockDef)
	o.Write(data)
}

// End the most recent block and return
// an op for invoking the completed block.
func (o *Ops) End() OpBlock {
	start := o.stack[len(o.stack)-1]
	o.stack = o.stack[:len(o.stack)-1]
	blockLen := o.Size() - start
	bo := binary.LittleEndian
	bo.PutUint32(o.data[start+1:], uint32(blockLen))
	return OpBlock{start}
}

// Reset clears the Ops.
func (o *Ops) Reset() {
	o.refs = o.refs[:0]
	o.stack = o.stack[:0]
	o.data = o.data[:0]
}

func (o *Ops) Refs() []interface{} {
	return o.refs
}

func (o *Ops) Data() []byte {
	return o.data
}

func (o *Ops) Ref(r interface{}) int {
	o.refs = append(o.refs, r)
	return len(o.refs) - 1
}

func (o *Ops) Write(op []byte) {
	o.data = append(o.data, op...)
}

// Size returns the length of the serialized Op data.
func (o *Ops) Size() int {
	return len(o.data)
}

func (b OpBlock) Add(o *Ops) {
	data := make([]byte, ops.TypeBlockLen)
	data[0] = byte(ops.TypeBlock)
	bo := binary.LittleEndian
	bo.PutUint32(data[1:], uint32(b.idx))
	o.Write(data)
}

func (OpBlock) ImplementsOp() {}

M ui/pointer/pointer.go => ui/pointer/pointer.go +29 -0
@@ 3,9 3,12 @@
package pointer

import (
	"encoding/binary"
	"time"

	"gioui.org/ui"
	"gioui.org/ui/f32"
	"gioui.org/ui/internal/ops"
)

type Event struct {


@@ 66,6 69,32 @@ const (
	Grabbed
)

func (h OpHandler) Add(o *ui.Ops) {
	data := make([]byte, ops.TypePointerHandlerLen)
	data[0] = byte(ops.TypePointerHandler)
	bo := binary.LittleEndian
	if h.Grab {
		data[1] = 1
	}
	bo.PutUint32(data[2:], uint32(o.Ref(h.Key)))
	bo.PutUint32(data[6:], uint32(o.Ref(h.Area)))
	o.Write(data)
}

func (h *OpHandler) Decode(d []byte, refs []interface{}) {
	bo := binary.LittleEndian
	if ops.OpType(d[0]) != ops.TypePointerHandler {
		panic("invalid op")
	}
	key := int(bo.Uint32(d[2:]))
	area := int(bo.Uint32(d[6:]))
	*h = OpHandler{
		Grab: d[1] != 0,
		Key:  refs[key].(Key),
		Area: refs[area].(Area),
	}
}

func (t Type) String() string {
	switch t {
	case Press:

M ui/pointer/queue.go => ui/pointer/queue.go +34 -43
@@ 5,12 5,14 @@ package pointer
import (
	"gioui.org/ui"
	"gioui.org/ui/f32"
	"gioui.org/ui/internal/ops"
)

type Queue struct {
	hitTree  []hitNode
	handlers map[Key]*handler
	pointers []pointerInfo
	reader   ops.Reader
	scratch  []Key
}



@@ 35,49 37,37 @@ type handler struct {
	wantsGrab bool
}

type childOp interface {
	ChildOp() ui.Op
}

func (q *Queue) collectHandlers(op ui.Op, t ui.Transform, layer int) ui.Op {
	switch op := op.(type) {
	case ui.Ops:
		var all ui.Ops
		for _, op := range op {
			if op := q.collectHandlers(op, t, layer); op != nil {
				if ops, ok := op.(ui.Ops); ok {
					all = append(all, ops...)
				} else {
					all = append(all, op)
				}
			}
		}
		return all
	case ui.OpLayer:
		layer++
		q.hitTree = append(q.hitTree, hitNode{level: layer})
		child := q.collectHandlers(op.ChildOp(), t, layer)
		if child == nil {
			return nil
		}
		return ui.OpLayer{Op: child}
	case ui.OpTransform:
		return q.collectHandlers(op.ChildOp(), t.Mul(op.Transform), layer)
	case OpHandler:
		q.hitTree = append(q.hitTree, hitNode{level: layer, key: op.Key})
		h, ok := q.handlers[op.Key]
func (q *Queue) collectHandlers(r *ops.Reader, t ui.Transform, layer int) {
	for {
		data, ok := r.Decode()
		if !ok {
			h = new(handler)
			q.handlers[op.Key] = h
			return
		}
		switch ops.OpType(data[0]) {
		case ops.TypePush:
			q.collectHandlers(r, t, layer)
		case ops.TypePop:
			return
		case ops.TypeLayer:
			layer++
			q.hitTree = append(q.hitTree, hitNode{level: layer})
		case ops.TypeTransform:
			var op ui.OpTransform
			op.Decode(data)
			t = t.Mul(op.Transform)
		case ops.TypePointerHandler:
			var op OpHandler
			op.Decode(data, r.Refs)
			q.hitTree = append(q.hitTree, hitNode{level: layer, key: op.Key})
			h, ok := q.handlers[op.Key]
			if !ok {
				h = new(handler)
				q.handlers[op.Key] = h
			}
			h.area = op.Area
			h.transform = t
			h.wantsGrab = h.wantsGrab || op.Grab
		}
		h.area = op.Area
		h.transform = t
		h.wantsGrab = h.wantsGrab || op.Grab
		return op
	case childOp:
		return q.collectHandlers(op.ChildOp(), t, layer)
	default:
		return nil
	}
}



@@ 115,7 105,7 @@ func (q *Queue) init() {
	}
}

func (q *Queue) Frame(op ui.Op) {
func (q *Queue) Frame(root *ui.Ops) {
	q.init()
	for k, h := range q.handlers {
		if !h.active {


@@ 126,7 116,8 @@ func (q *Queue) Frame(op ui.Op) {
		}
	}
	q.hitTree = q.hitTree[:0]
	q.collectHandlers(op, ui.Transform{}, 0)
	q.reader.Reset(root.Data(), root.Refs())
	q.collectHandlers(&q.reader, ui.Transform{}, 0)
}

func (q *Queue) For(k Key) []Event {

M ui/text/editor.go => ui/text/editor.go +14 -13
@@ 48,8 48,6 @@ type Editor struct {
	scrollOff image.Point

	clicker gesture.Click

	ops ui.Ops
}

type linePath struct {


@@ 126,7 124,7 @@ func (e *Editor) caretWidth() fixed.Int26_6 {
	return fixed.Int26_6(oneDp * 64)
}

func (e *Editor) Layout(cs layout.Constraints) (ui.Op, layout.Dimens) {
func (e *Editor) Layout(ops *ui.Ops, cs layout.Constraints) layout.Dimens {
	twoDp := int(e.cfg.Pixels(ui.Dp(2)) + 0.5)
	e.padLeft, e.padRight = twoDp, twoDp
	maxWidth := cs.Width.Max


@@ 155,8 153,7 @@ func (e *Editor) Layout(cs layout.Constraints) (ui.Op, layout.Dimens) {
		Min: image.Point{X: 0, Y: 0},
		Max: image.Point{X: e.viewSize.X, Y: e.viewSize.Y},
	}
	e.ops = e.ops[:0]
	e.ops = append(e.ops, key.OpHandler{Key: e, Focus: e.requestFocus})
	key.OpHandler{Key: e, Focus: e.requestFocus}.Add(ops)
	e.requestFocus = false
	e.it = lineIterator{
		Lines:     lines,


@@ 171,10 168,11 @@ func (e *Editor) Layout(cs layout.Constraints) (ui.Op, layout.Dimens) {
			break
		}
		path := e.Face.Path(str)
		e.ops = append(e.ops, ui.OpTransform{
			Transform: ui.Offset(lineOff),
			Op:        draw.OpClip{Path: path, Op: draw.OpImage{Rect: toRectF(clip).Sub(lineOff), Src: e.Src, SrcRect: e.Src.Bounds()}},
		})
		ops.Begin()
		ui.OpTransform{Transform: ui.Offset(lineOff)}.Add(ops)
		draw.OpClip{Path: path}.Add(ops)
		draw.OpImage{Rect: toRectF(clip).Sub(lineOff), Src: e.Src, SrcRect: e.Src.Bounds()}.Add(ops)
		ops.End().Add(ops)
	}
	if e.focused {
		now := e.cfg.Now


@@ 197,18 195,21 @@ func (e *Editor) Layout(cs layout.Constraints) (ui.Op, layout.Dimens) {
			})
			carRect = clip.Intersect(carRect)
			if !carRect.Empty() {
				e.ops = append(e.ops, draw.OpImage{Src: e.Src, Rect: toRectF(carRect), SrcRect: e.Src.Bounds()})
				img := draw.OpImage{Src: e.Src, Rect: toRectF(carRect), SrcRect: e.Src.Bounds()}
				img.Add(ops)
			}
		}
		if blinking {
			e.ops = append(e.ops, ui.OpRedraw{At: nextBlink})
			redraw := ui.OpRedraw{At: nextBlink}
			redraw.Add(ops)
		}
	}

	baseline := e.padTop + e.dims.Baseline
	area := &gesture.Rect{e.viewSize}
	e.ops = append(e.ops, e.scroller.Op(area), e.clicker.Op(area))
	return e.ops, layout.Dimens{Size: e.viewSize, Baseline: baseline}
	e.scroller.Op(ops, area)
	e.clicker.Op(ops, area)
	return layout.Dimens{Size: e.viewSize, Baseline: baseline}
}

func (e *Editor) layout() {

M ui/text/label.go => ui/text/label.go +7 -8
@@ 80,7 80,7 @@ func (l *lineIterator) Next() (String, f32.Point, bool) {
	return String{}, f32.Point{}, false
}

func (l Label) Layout(cs layout.Constraints) (ui.Op, layout.Dimens) {
func (l Label) Layout(ops *ui.Ops, cs layout.Constraints) layout.Dimens {
	textLayout := l.Face.Layout(l.Text, false, cs.Width.Max)
	lines := textLayout.Lines
	dims := linesDimens(lines)


@@ 90,7 90,6 @@ func (l Label) Layout(cs layout.Constraints) (ui.Op, layout.Dimens) {
		Min: image.Point{X: -ui.Inf, Y: -padTop},
		Max: image.Point{X: ui.Inf, Y: dims.Size.Y + padBottom},
	}
	var ops ui.Ops = make([]ui.Op, len(lines))[:0]
	l.it = lineIterator{
		Lines:     lines,
		Clip:      clip,


@@ 104,13 103,13 @@ func (l Label) Layout(cs layout.Constraints) (ui.Op, layout.Dimens) {
		}
		path := l.Face.Path(str)
		lclip := toRectF(clip).Sub(off)
		op := ui.OpTransform{
			Transform: ui.Offset(off),
			Op:        draw.OpClip{Path: path, Op: draw.OpImage{Rect: lclip, Src: l.Src, SrcRect: l.Src.Bounds()}},
		}
		ops = append(ops, op)
		ops.Begin()
		ui.OpTransform{Transform: ui.Offset(off)}.Add(ops)
		draw.OpClip{Path: path}.Add(ops)
		draw.OpImage{Rect: lclip, Src: l.Src, SrcRect: l.Src.Bounds()}.Add(ops)
		ops.End().Add(ops)
	}
	return ops, dims
	return dims
}

func itof(i int) float32 {

M ui/ui.go => ui/ui.go +59 -11
@@ 3,9 3,12 @@
package ui

import (
	"encoding/binary"
	"math"
	"time"

	"gioui.org/ui/f32"
	"gioui.org/ui/internal/ops"
)

// Config contain the context for updating and


@@ 33,7 36,7 @@ func (c *Config) Pixels(v Value) float32 {
	}
}

// Op is implemented by all known drawing and control
// Op is implemented by all drawing and control
// operations.
type Op interface {
	ImplementsOp()


@@ 41,7 44,6 @@ type Op interface {

// OpLayer represents a semantic layer of UI.
type OpLayer struct {
	Op Op
}

// OpRedraw requests a redraw at the given time. Use


@@ 50,13 52,9 @@ type OpRedraw struct {
	At time.Time
}

// Ops is the operation for a list of ops.
type Ops []Op

// OpTransform transforms an op.
type OpTransform struct {
	Transform Transform
	Op        Op
}

type Transform struct {


@@ 64,6 62,30 @@ type Transform struct {
	offset f32.Point
}

func (r OpRedraw) Add(o *Ops) {
	data := make([]byte, ops.TypeRedrawLen)
	data[0] = byte(ops.TypeRedraw)
	bo := binary.LittleEndian
	// UnixNano cannot represent the zero time.
	if t := r.At; !t.IsZero() {
		nanos := t.UnixNano()
		if nanos > 0 {
			bo.PutUint64(data[1:], uint64(nanos))
		}
	}
	o.Write(data)
}

func (r *OpRedraw) Decode(d []byte) {
	bo := binary.LittleEndian
	if ops.OpType(d[0]) != ops.TypeRedraw {
		panic("invalid op")
	}
	if nanos := bo.Uint64(d[1:]); nanos > 0 {
		r.At = time.Unix(0, int64(nanos))
	}
}

func (t Transform) InvTransform(p f32.Point) f32.Point {
	return p.Sub(t.offset)
}


@@ 78,12 100,39 @@ func (t Transform) Mul(t2 Transform) Transform {
	}
}

func (t OpTransform) ChildOp() Op {
	return t.Op
func (t OpTransform) Add(o *Ops) {
	data := make([]byte, ops.TypeTransformLen)
	data[0] = byte(ops.TypeTransform)
	bo := binary.LittleEndian
	bo.PutUint32(data[1:], math.Float32bits(t.Transform.offset.X))
	bo.PutUint32(data[5:], math.Float32bits(t.Transform.offset.Y))
	o.Write(data)
}

func (o OpLayer) ChildOp() Op {
	return o.Op
func (t *OpTransform) Decode(d []byte) {
	bo := binary.LittleEndian
	if ops.OpType(d[0]) != ops.TypeTransform {
		panic("invalid op")
	}
	*t = OpTransform{
		Transform: Offset(f32.Point{
			X: math.Float32frombits(bo.Uint32(d[1:])),
			Y: math.Float32frombits(bo.Uint32(d[5:])),
		}),
	}
}

func (l OpLayer) Add(o *Ops) {
	data := make([]byte, ops.TypeLayerLen)
	data[0] = byte(ops.TypeLayer)
	o.Write(data)
}

func (l *OpLayer) Decode(d []byte) {
	if ops.OpType(d[0]) != ops.TypeLayer {
		panic("invalid op")
	}
	*l = OpLayer{}
}

func Offset(o f32.Point) Transform {


@@ 93,7 142,6 @@ func Offset(o f32.Point) Transform {
// Inf is the int value that represents an unbounded maximum constraint.
const Inf = int(^uint(0) >> 1)

func (Ops) ImplementsOp()         {}
func (OpLayer) ImplementsOp()     {}
func (OpTransform) ImplementsOp() {}
func (OpRedraw) ImplementsOp()    {}

M ui/widget/image.go => ui/widget/image.go +3 -3
@@ 16,7 16,7 @@ type Image struct {
	Rect image.Rectangle
}

func (im Image) Layout(cs layout.Constraints) (ui.Op, layout.Dimens) {
func (im Image) Layout(ops *ui.Ops, cs layout.Constraints) layout.Dimens {
	d := image.Point{X: cs.Width.Max, Y: cs.Height.Max}
	if d.X == ui.Inf {
		d.X = cs.Width.Min


@@ 27,6 27,6 @@ func (im Image) Layout(cs layout.Constraints) (ui.Op, layout.Dimens) {
	dr := f32.Rectangle{
		Max: f32.Point{X: float32(d.X), Y: float32(d.Y)},
	}
	op := draw.OpImage{Rect: dr, Src: im.Src, SrcRect: im.Rect}
	return op, layout.Dimens{Size: d, Baseline: d.Y}
	draw.OpImage{Rect: dr, Src: im.Src, SrcRect: im.Rect}.Add(ops)
	return layout.Dimens{Size: d, Baseline: d.Y}
}