diff --git a/apps/gophers/main.go b/apps/gophers/main.go index b13ade4e..f0805be5 100644 --- a/apps/gophers/main.go +++ b/apps/gophers/main.go @@ -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} }) } diff --git a/apps/hello/hello.go b/apps/hello/hello.go index 89fabb0d..bd108c7b 100644 --- a/apps/hello/hello.go +++ b/apps/hello/hello.go @@ -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() } } diff --git a/ui/app/internal/gpu/gpu.go b/ui/app/internal/gpu/gpu.go index d0ae58b7..bc4fedc0 100644 --- a/ui/app/internal/gpu/gpu.go +++ b/ui/app/internal/gpu/gpu.go @@ -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() { +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 } - cpath := &pathOp{ - parent: cpath, - off: off, + 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 } - 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]) - 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) - } - case childOp: - z = ops.collectOp(op.ChildOp(), clip, t, cpath, rect, z) } return z } diff --git a/ui/app/window.go b/ui/app/window.go index 829adb9d..001ffc85 100644 --- a/ui/app/window.go +++ b/ui/app/window.go @@ -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 - } -} diff --git a/ui/draw/draw.go b/ui/draw/draw.go index 59920485..989c44a1 100644 --- a/ui/draw/draw.go +++ b/ui/draw/draw.go @@ -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, } } diff --git a/ui/draw/path.go b/ui/draw/path.go index 7b105dbd..787d4bc7 100644 --- a/ui/draw/path.go +++ b/ui/draw/path.go @@ -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() {} diff --git a/ui/gesture/gestures.go b/ui/gesture/gestures.go index 83f04764..0b91f407 100644 --- a/ui/gesture/gestures.go +++ b/ui/gesture/gestures.go @@ -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() { diff --git a/ui/internal/ops/ops.go b/ui/internal/ops/ops.go new file mode 100644 index 00000000..3070286e --- /dev/null +++ b/ui/internal/ops/ops.go @@ -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 + } +} diff --git a/ui/key/key.go b/ui/key/key.go index 2c5e4577..e69566c0 100644 --- a/ui/key/key.go +++ b/ui/key/key.go @@ -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() {} diff --git a/ui/key/queue.go b/ui/key/queue.go index 1445868b..0ed02817 100644 --- a/ui/key/queue.go +++ b/ui/key/queue.go @@ -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 } diff --git a/ui/layout/flex.go b/ui/layout/flex.go index 491ff0f9..9911dc59 100644 --- a/ui/layout/flex.go +++ b/ui/layout/flex.go @@ -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 { diff --git a/ui/layout/list.go b/ui/layout/list.go index 81e03b03..08edb946 100644 --- a/ui/layout/list.go +++ b/ui/layout/list.go @@ -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 diff --git a/ui/layout/simple.go b/ui/layout/simple.go index 858dc57d..d866e826 100644 --- a/ui/layout/simple.go +++ b/ui/layout/simple.go @@ -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, } diff --git a/ui/layout/stack.go b/ui/layout/stack.go index 71a51fa3..71a7dd3c 100644 --- a/ui/layout/stack.go +++ b/ui/layout/stack.go @@ -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, } diff --git a/ui/ops.go b/ui/ops.go new file mode 100644 index 00000000..0830df5f --- /dev/null +++ b/ui/ops.go @@ -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() {} diff --git a/ui/pointer/pointer.go b/ui/pointer/pointer.go index 061debe3..25edc7fe 100644 --- a/ui/pointer/pointer.go +++ b/ui/pointer/pointer.go @@ -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: diff --git a/ui/pointer/queue.go b/ui/pointer/queue.go index c7723c37..28f0bc0c 100644 --- a/ui/pointer/queue.go +++ b/ui/pointer/queue.go @@ -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 { diff --git a/ui/text/editor.go b/ui/text/editor.go index f32820c7..fb07d04f 100644 --- a/ui/text/editor.go +++ b/ui/text/editor.go @@ -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() { diff --git a/ui/text/label.go b/ui/text/label.go index 2374ac5a..ac02f4d2 100644 --- a/ui/text/label.go +++ b/ui/text/label.go @@ -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 { diff --git a/ui/ui.go b/ui/ui.go index 579b0e47..77a8303a 100644 --- a/ui/ui.go +++ b/ui/ui.go @@ -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() {} diff --git a/ui/widget/image.go b/ui/widget/image.go index 5d6f42c0..3eaef624 100644 --- a/ui/widget/image.go +++ b/ui/widget/image.go @@ -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} }