diff --git a/app/ime_test.go b/app/ime_test.go index c510c353..20c14cd8 100644 --- a/app/ime_test.go +++ b/app/ime_test.go @@ -35,7 +35,7 @@ func FuzzIME(f *testing.F) { var r router.Router gtx := layout.Context{Ops: new(op.Ops), Queue: &r} // Layout once to register focus. - e.Layout(gtx, cache, text.Font{}, unit.Sp(10), nil) + e.Layout(gtx, cache, text.Font{}, unit.Sp(10), op.CallOp{}, op.CallOp{}) r.Frame(gtx.Ops) var state editorState @@ -103,7 +103,7 @@ func FuzzIME(f *testing.F) { } } cmds = cmds[cmdLen:] - e.Layout(gtx, cache, text.Font{}, unit.Sp(10), nil) + e.Layout(gtx, cache, text.Font{}, unit.Sp(10), op.CallOp{}, op.CallOp{}) r.Frame(gtx.Ops) newState := r.EditorState() // We don't track caret position. diff --git a/font/opentype/opentype.go b/font/opentype/opentype.go index 23d9964a..6287d026 100644 --- a/font/opentype/opentype.go +++ b/font/opentype/opentype.go @@ -2,11 +2,18 @@ // Package opentype implements text layout and shaping for OpenType // files. +// +// NOTE: the OpenType specification allows for fonts to include bitmap images +// in a variety of formats. In the interest of small binary sizes, the opentype +// package only automatically imports the PNG image decoder. If you have a font +// with glyphs in JPEG or TIFF formats, register those decoders with the image +// package in order to ensure those glyphs are visible in text. package opentype import ( "bytes" "fmt" + _ "image/png" "github.com/go-text/typesetting/font" ) diff --git a/text/gotext.go b/text/gotext.go index 7ee59840..62e2a8b7 100644 --- a/text/gotext.go +++ b/text/gotext.go @@ -3,6 +3,8 @@ package text import ( + "bytes" + "image" "io" "sort" @@ -19,6 +21,7 @@ import ( "gioui.org/io/system" "gioui.org/op" "gioui.org/op/clip" + "gioui.org/op/paint" ) // document holds a collection of shaped lines and alignment information for @@ -504,12 +507,13 @@ func alignWidth(minWidth int, lines []line) int { return minWidth } -// Shape converts the provided glyphs into a path. -func (s *shaperImpl) Shape(ops *op.Ops, gs []Glyph) clip.PathSpec { +// Shape converts the provided glyphs into a path. The path will enclose the forms +// of all vector glyphs. +func (s *shaperImpl) Shape(pathOps *op.Ops, gs []Glyph) clip.PathSpec { var lastPos f32.Point var x fixed.Int26_6 var builder clip.Path - builder.Begin(ops) + builder.Begin(pathOps) for i, g := range gs { if i == 0 { x = g.X @@ -570,6 +574,68 @@ func (s *shaperImpl) Shape(ops *op.Ops, gs []Glyph) clip.PathSpec { return builder.End() } +// Bitmaps returns an op.CallOp that will display all bitmap glyphs within gs. +// The positioning of the bitmaps uses the same logic as Shape(), so the returned +// CallOp can be added at the same offset as the path data returned by Shape() +// and will align correctly. +func (s *shaperImpl) Bitmaps(ops *op.Ops, gs []Glyph) op.CallOp { + var x fixed.Int26_6 + bitmapMacro := op.Record(ops) + for i, g := range gs { + if i == 0 { + x = g.X + } + _, faceIdx, gid := splitGlyphID(g.ID) + face := s.orderer.faceFor(faceIdx) + glyphData := face.GlyphData(gid) + switch glyphData := glyphData.(type) { + case api.GlyphBitmap: + var imgOp paint.ImageOp + var imgSize image.Point + var img image.Image + switch glyphData.Format { + case api.PNG, api.JPG, api.TIFF: + img, _, _ = image.Decode(bytes.NewReader(glyphData.Data)) + case api.BlackAndWhite: + // This is a complex family of uncompressed bitmaps that don't seem to be + // very common in practice. We can try adding support later if needed. + fallthrough + default: + // Unknown format. + continue + } + imgOp = paint.NewImageOp(img) + imgSize = img.Bounds().Size() + off := op.Offset(image.Point{ + X: ((g.X - x) - g.Offset.X).Round(), + Y: g.Offset.Y.Round() - g.Ascent.Round(), + }).Push(ops) + cl := clip.Rect{Max: imgSize}.Push(ops) + + glyphSize := image.Rectangle{ + Min: image.Point{ + X: g.Bounds.Min.X.Round(), + Y: g.Bounds.Min.Y.Round(), + }, + Max: image.Point{ + X: g.Bounds.Max.X.Round(), + Y: g.Bounds.Max.Y.Round(), + }, + }.Size() + aff := op.Affine(f32.Affine2D{}.Scale(f32.Point{}, f32.Point{ + X: float32(glyphSize.X) / float32(imgSize.X), + Y: float32(glyphSize.Y) / float32(imgSize.Y), + })).Push(ops) + imgOp.Add(ops) + paint.PaintOp{}.Add(ops) + aff.Pop() + cl.Pop() + off.Pop() + } + } + return bitmapMacro.Stop() +} + // langConfig describes the language and writing system of a body of text. type langConfig struct { // Language the text is written in. diff --git a/text/lru.go b/text/lru.go index ef8e065e..bbf7d412 100644 --- a/text/lru.go +++ b/text/lru.go @@ -5,74 +5,50 @@ package text import ( "encoding/binary" "hash/maphash" + "image" "gioui.org/io/system" + "gioui.org/op" "gioui.org/op/clip" + "gioui.org/op/paint" "golang.org/x/image/math/fixed" ) -type layoutCache struct { - m map[layoutKey]*layoutElem - head, tail *layoutElem +// entry holds a single key-value pair for an LRU cache. +type entry[K comparable, V any] struct { + next, prev *entry[K, V] + key K + v V } -type pathCache struct { - seed maphash.Seed - m map[uint64]*path - head, tail *path +// lru is a generic least-recently-used cache. +type lru[K comparable, V any] struct { + m map[K]*entry[K, V] + head, tail *entry[K, V] } -type layoutElem struct { - next, prev *layoutElem - key layoutKey - layout document -} - -type path struct { - next, prev *path - key uint64 - val clip.PathSpec - glyphs []glyphInfo -} - -type glyphInfo struct { - ID GlyphID - X fixed.Int26_6 -} - -type layoutKey struct { - ppem fixed.Int26_6 - maxWidth, minWidth int - maxLines int - str string - locale system.Locale - font Font -} - -type pathKey struct { - gidHash uint64 -} - -const maxSize = 1000 - -func (l *layoutCache) Get(k layoutKey) (document, bool) { +// Get fetches the value associated with the given key, if any. +func (l *lru[K, V]) Get(k K) (V, bool) { if lt, ok := l.m[k]; ok { l.remove(lt) l.insert(lt) - return lt.layout, true + return lt.v, true } - return document{}, false + var v V + return v, false } -func (l *layoutCache) Put(k layoutKey, lt document) { +// Put inserts the given value with the given key, evicting old +// cache entries if necessary. +func (l *lru[K, V]) Put(k K, v V) { if l.m == nil { - l.m = make(map[layoutKey]*layoutElem) - l.head = new(layoutElem) - l.tail = new(layoutElem) + l.m = make(map[K]*entry[K, V]) + l.head = new(entry[K, V]) + l.tail = new(entry[K, V]) l.head.prev = l.tail l.tail.next = l.head } - val := &layoutElem{key: k, layout: lt} + val := &entry[K, V]{key: k, v: v} l.m[k] = val l.insert(val) if len(l.m) > maxSize { @@ -82,21 +58,42 @@ func (l *layoutCache) Put(k layoutKey, lt document) { } } -func (l *layoutCache) remove(lt *layoutElem) { - lt.next.prev = lt.prev - lt.prev.next = lt.next +// remove cuts e out of the lru linked list. +func (l *lru[K, V]) remove(e *entry[K, V]) { + e.next.prev = e.prev + e.prev.next = e.next } -func (l *layoutCache) insert(lt *layoutElem) { - lt.next = l.head - lt.prev = l.head.prev - lt.prev.next = lt - lt.next.prev = lt +// insert adds e to the lru linked list. +func (l *lru[K, V]) insert(e *entry[K, V]) { + e.next = l.head + e.prev = l.head.prev + e.prev.next = e + e.next.prev = e +} + +type bitmapCache = lru[GlyphID, bitmap] + +type bitmap struct { + img paint.ImageOp + size image.Point +} + +type layoutCache = lru[layoutKey, document] + +type glyphValue[V any] struct { + v V + glyphs []glyphInfo +} + +type glyphLRU[V any] struct { + seed maphash.Seed + cache lru[uint64, glyphValue[V]] } // hashGlyphs computes a hash key based on the ID and X offset of // every glyph in the slice. -func (c *pathCache) hashGlyphs(gs []Glyph) uint64 { +func (c *glyphLRU[V]) hashGlyphs(gs []Glyph) uint64 { if c.seed == (maphash.Seed{}) { c.seed = maphash.MakeSeed() } @@ -118,6 +115,55 @@ func (c *pathCache) hashGlyphs(gs []Glyph) uint64 { return sum } +func (c *glyphLRU[V]) Get(key uint64, gs []Glyph) (V, bool) { + if v, ok := c.cache.Get(key); ok && gidsEqual(v.glyphs, gs) { + return v.v, true + } + var v V + return v, false +} + +func (c *glyphLRU[V]) Put(key uint64, glyphs []Glyph, v V) { + gids := make([]glyphInfo, len(glyphs)) + firstX := fixed.I(0) + for i, glyph := range glyphs { + if i == 0 { + firstX = glyph.X + } + // Cache glyph X offsets relative to the first glyph. + gids[i] = glyphInfo{ID: glyph.ID, X: glyph.X - firstX} + } + val := glyphValue[V]{ + glyphs: gids, + v: v, + } + c.cache.Put(key, val) +} + +type pathCache = glyphLRU[clip.PathSpec] + +type bitmapShapeCache = glyphLRU[op.CallOp] + +type glyphInfo struct { + ID GlyphID + X fixed.Int26_6 +} + +type layoutKey struct { + ppem fixed.Int26_6 + maxWidth, minWidth int + maxLines int + str string + locale system.Locale + font Font +} + +type pathKey struct { + gidHash uint64 +} + +const maxSize = 1000 + func gidsEqual(a []glyphInfo, glyphs []Glyph) bool { if len(a) != len(glyphs) { return false @@ -134,51 +180,3 @@ func gidsEqual(a []glyphInfo, glyphs []Glyph) bool { } return true } - -func (c *pathCache) Get(key uint64, gs []Glyph) (clip.PathSpec, bool) { - if v, ok := c.m[key]; ok && gidsEqual(v.glyphs, gs) { - c.remove(v) - c.insert(v) - return v.val, true - } - return clip.PathSpec{}, false -} - -func (c *pathCache) Put(key uint64, glyphs []Glyph, v clip.PathSpec) { - if c.m == nil { - c.m = make(map[uint64]*path) - c.head = new(path) - c.tail = new(path) - c.head.prev = c.tail - c.tail.next = c.head - } - gids := make([]glyphInfo, len(glyphs)) - firstX := fixed.I(0) - for i, glyph := range glyphs { - if i == 0 { - firstX = glyph.X - } - // Cache glyph X offsets relative to the first glyph. - gids[i] = glyphInfo{ID: glyph.ID, X: glyph.X - firstX} - } - val := &path{key: key, val: v, glyphs: gids} - c.m[key] = val - c.insert(val) - if len(c.m) > maxSize { - oldest := c.tail.next - c.remove(oldest) - delete(c.m, oldest.key) - } -} - -func (c *pathCache) remove(v *path) { - v.next.prev = v.prev - v.prev.next = v.next -} - -func (c *pathCache) insert(v *path) { - v.next = c.head - v.prev = c.head.prev - v.prev.next = v - v.next.prev = v -} diff --git a/text/shaper.go b/text/shaper.go index e81a9994..1b633e25 100644 --- a/text/shaper.go +++ b/text/shaper.go @@ -146,10 +146,11 @@ type GlyphID uint64 // Shaper converts strings of text into glyphs that can be displayed. type Shaper struct { - shaper shaperImpl - pathCache pathCache - layoutCache layoutCache - paragraph []rune + shaper shaperImpl + pathCache pathCache + bitmapShapeCache bitmapShapeCache + layoutCache layoutCache + paragraph []rune reader strings.Reader @@ -440,17 +441,33 @@ func splitGlyphID(g GlyphID) (fixed.Int26_6, int, font.GID) { return ppem, faceIdx, gid } -// Shape converts a slice of glyphs into a path describing their collective -// shape. All glyphs are expected to be from a single line of text (their -// Y offsets are ignored). +// Shape converts the provided glyphs into a path. The path will enclose the forms +// of all vector glyphs. +// All glyphs are expected to be from a single line of text (their Y offsets are ignored). func (l *Shaper) Shape(gs []Glyph) clip.PathSpec { key := l.pathCache.hashGlyphs(gs) shape, ok := l.pathCache.Get(key, gs) if ok { return shape } - ops := new(op.Ops) - shape = l.shaper.Shape(ops, gs) + pathOps := new(op.Ops) + shape = l.shaper.Shape(pathOps, gs) l.pathCache.Put(key, gs, shape) return shape } + +// Bitmaps extracts bitmap glyphs from the provided slice and creates an op.CallOp to present +// them. The returned op.CallOp will align correctly with the return value of Shape() for the +// same gs slice. +// All glyphs are expected to be from a single line of text (their Y offsets are ignored). +func (l *Shaper) Bitmaps(gs []Glyph) op.CallOp { + key := l.bitmapShapeCache.hashGlyphs(gs) + call, ok := l.bitmapShapeCache.Get(key, gs) + if ok { + return call + } + callOps := new(op.Ops) + call = l.shaper.Bitmaps(callOps, gs) + l.bitmapShapeCache.Put(key, gs, call) + return call +} diff --git a/widget/editor.go b/widget/editor.go index a05c3ce7..ce067541 100644 --- a/widget/editor.go +++ b/widget/editor.go @@ -18,6 +18,7 @@ import ( "gioui.org/io/event" "gioui.org/io/key" "gioui.org/io/pointer" + "gioui.org/io/semantic" "gioui.org/io/system" "gioui.org/layout" "gioui.org/op" @@ -504,12 +505,14 @@ func (e *Editor) initBuffer() { e.text.Mask = e.Mask } -// Layout lays out the editor. If content is not nil, it is laid out on top. -func (e *Editor) Layout(gtx layout.Context, lt *text.Shaper, font text.Font, size unit.Sp, content layout.Widget) layout.Dimensions { +// Layout lays out the editor using the provided textMaterial as the paint material +// for the text glyphs+caret and the selectMaterial as the paint material for the +// selection rectangle. +func (e *Editor) Layout(gtx layout.Context, lt *text.Shaper, font text.Font, size unit.Sp, textMaterial, selectMaterial op.CallOp) layout.Dimensions { e.initBuffer() e.text.Update(gtx, lt, font, size, e.processEvents) - dims := e.layout(gtx, content) + dims := e.layout(gtx, textMaterial, selectMaterial) if e.focused { // Notify IME of selection if it changed. @@ -586,7 +589,7 @@ func (e *Editor) updateSnippet(gtx layout.Context, start, end int) { }.Add(gtx.Ops) } -func (e *Editor) layout(gtx layout.Context, content layout.Widget) layout.Dimensions { +func (e *Editor) layout(gtx layout.Context, textMaterial, selectMaterial op.CallOp) layout.Dimensions { // Adjust scrolling for new viewport and layout. e.text.ScrollRel(0, 0) @@ -659,33 +662,44 @@ func (e *Editor) layout(gtx layout.Context, content layout.Widget) layout.Dimens } e.showCaret = e.focused && (!blinking || dt%timePerBlink < timePerBlink/2) } + disabled := gtx.Queue == nil - if content != nil { - content(gtx) + semantic.Editor.Add(gtx.Ops) + if e.Len() > 0 { + e.paintSelection(gtx, selectMaterial) + e.paintText(gtx, textMaterial) + } + if !disabled { + e.paintCaret(gtx, textMaterial) } return visibleDims } -// PaintSelection paints the contrasting background for selected text. -func (e *Editor) PaintSelection(gtx layout.Context) { +// paintSelection paints the contrasting background for selected text using the provided +// material to set the painting material for the selection. +func (e *Editor) paintSelection(gtx layout.Context, material op.CallOp) { e.initBuffer() if !e.focused { return } - e.text.PaintSelection(gtx) + e.text.PaintSelection(gtx, material) } -func (e *Editor) PaintText(gtx layout.Context) { +// paintText paints the text glyphs using the provided material to set the fill of the +// glyphs. +func (e *Editor) paintText(gtx layout.Context, material op.CallOp) { e.initBuffer() - e.text.PaintText(gtx) + e.text.PaintText(gtx, material) } -func (e *Editor) PaintCaret(gtx layout.Context) { +// paintCaret paints the text glyphs using the provided material to set the fill material +// of the caret rectangle. +func (e *Editor) paintCaret(gtx layout.Context, material op.CallOp) { e.initBuffer() if !e.showCaret || e.ReadOnly { return } - e.text.PaintCaret(gtx) + e.text.PaintCaret(gtx, material) } // Len is the length of the editor contents, in runes. diff --git a/widget/editor_test.go b/widget/editor_test.go index debe07c3..a896c7c4 100644 --- a/widget/editor_test.go +++ b/widget/editor_test.go @@ -117,12 +117,12 @@ func TestEditorReadOnly(t *testing.T) { if cStart != cEnd { t.Errorf("unexpected initial caret positions") } - e.Layout(gtx, cache, font, fontSize, nil) + e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{}) // Select everything. gtx.Ops.Reset() gtx.Queue = &testQueue{events: []event.Event{key.Event{Name: "A", Modifiers: key.ModShortcut}}} - e.Layout(gtx, cache, font, fontSize, nil) + e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{}) textContent := e.Text() cStart2, cEnd2 := e.Selection() if cStart2 > cEnd2 { @@ -138,7 +138,7 @@ func TestEditorReadOnly(t *testing.T) { // Type some new characters. gtx.Ops.Reset() gtx.Queue = &testQueue{events: []event.Event{key.EditEvent{Range: key.Range{Start: cStart2, End: cEnd2}, Text: "something else"}}} - e.Layout(gtx, cache, font, fontSize, nil) + e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{}) textContent2 := e.Text() if textContent2 != textContent { t.Errorf("readonly editor modified by key.EditEvent") @@ -147,7 +147,7 @@ func TestEditorReadOnly(t *testing.T) { // Try to delete selection. gtx.Ops.Reset() gtx.Queue = &testQueue{events: []event.Event{key.Event{Name: key.NameDeleteBackward}}} - dims := e.Layout(gtx, cache, font, fontSize, nil) + dims := e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{}) textContent2 = e.Text() if textContent2 != textContent { t.Errorf("readonly editor modified by delete key.Event") @@ -173,7 +173,7 @@ func TestEditorReadOnly(t *testing.T) { Position: layout.FPt(dims.Size).Mul(.5), }, }} - e.Layout(gtx, cache, font, fontSize, nil) + e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{}) cStart3, cEnd3 := e.Selection() if cStart3 == cStart2 || cEnd3 == cEnd2 { t.Errorf("expected mouse interaction to change selection.") @@ -213,7 +213,7 @@ func TestEditorConfigurations(t *testing.T) { e.Alignment = alignment e.SetText(sentence) e.SetCaret(0, 0) - dims := e.Layout(gtx, cache, font, fontSize, nil) + dims := e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{}) if dims.Size.X < gtx.Constraints.Min.X || dims.Size.Y < gtx.Constraints.Min.Y { t.Errorf("expected min size %#+v, got %#+v", gtx.Constraints.Min, dims.Size) } @@ -222,7 +222,7 @@ func TestEditorConfigurations(t *testing.T) { t.Errorf("expected caret X to be %f, got %f", halfway, coords.X) } e.SetCaret(runes, runes) - e.Layout(gtx, cache, font, fontSize, nil) + e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{}) coords = e.CaretCoords() if int(coords.X) > gtx.Constraints.Max.X || int(coords.Y) > gtx.Constraints.Max.Y { t.Errorf("caret coordinates %v exceed constraints %v", coords, gtx.Constraints.Max) @@ -246,7 +246,7 @@ func TestEditor(t *testing.T) { // Regression test for bad in-cluster rune offset math. e.SetText("æbc") - e.Layout(gtx, cache, font, fontSize, nil) + e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{}) e.text.MoveEnd(selectionClear) assertCaret(t, e, 0, 3, len("æbc")) @@ -257,7 +257,7 @@ func TestEditor(t *testing.T) { if got, exp := e.Len(), utf8.RuneCountInString(e.Text()); got != exp { t.Errorf("got length %d, expected %d", got, exp) } - e.Layout(gtx, cache, font, fontSize, nil) + e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{}) assertCaret(t, e, 0, 0, 0) e.text.MoveEnd(selectionClear) assertCaret(t, e, 0, 3, len("æbc")) @@ -284,7 +284,7 @@ func TestEditor(t *testing.T) { e.MoveCaret(-3, -3) assertCaret(t, e, 1, 1, len("æbc\na")) e.text.Mask = '*' - e.Layout(gtx, cache, font, fontSize, nil) + e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{}) assertCaret(t, e, 1, 1, len("æbc\na")) e.MoveCaret(-3, -3) assertCaret(t, e, 0, 2, len("æb")) @@ -292,7 +292,7 @@ func TestEditor(t *testing.T) { NOTE(whereswaldon): it isn't possible to check the raw glyph data like this anymore. How should we handle this? e.Mask = '\U0001F92B' - e.Layout(gtx, cache, font, fontSize, nil) + e.Layout(gtx, cache, font, fontSize, op.CallOp{},op.CallOp{}) e.moveEnd(selectionClear) assertCaret(t, e, 0, 3, len("æbc")) @@ -358,7 +358,7 @@ func TestEditorRTL(t *testing.T) { // Set the text to a single RTL word. The caret should start at 0 column // zero, but this is the first column on the right. e.SetText("الحب") - e.Layout(gtx, cache, font, fontSize, nil) + e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{}) assertCaret(t, e, 0, 0, 0) e.MoveCaret(+1, +1) assertCaret(t, e, 0, 1, len("ا")) @@ -372,7 +372,7 @@ func TestEditorRTL(t *testing.T) { sentence := "الحب سماء لا\nتمط غير الأحلام" e.SetText(sentence) - e.Layout(gtx, cache, font, fontSize, nil) + e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{}) assertCaret(t, e, 0, 0, 0) e.text.MoveEnd(selectionClear) assertCaret(t, e, 0, 12, len("الحب سماء لا")) @@ -440,7 +440,7 @@ func TestEditorLigature(t *testing.T) { e.SetCaret(0, 0) // shouldn't panic assertCaret(t, e, 0, 0, 0) e.SetText("fl") // just a ligature - e.Layout(gtx, cache, font, fontSize, nil) + e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{}) e.text.MoveEnd(selectionClear) assertCaret(t, e, 0, 2, len("fl")) e.MoveCaret(-1, -1) @@ -450,7 +450,7 @@ func TestEditorLigature(t *testing.T) { e.MoveCaret(+2, +2) assertCaret(t, e, 0, 2, len("fl")) e.SetText("flaffl•ffi\n•fflfi") // 3 ligatures on line 0, 2 on line 1 - e.Layout(gtx, cache, font, fontSize, nil) + e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{}) assertCaret(t, e, 0, 0, 0) e.text.MoveEnd(selectionClear) assertCaret(t, e, 0, 10, len("ffaffl•ffi")) @@ -502,7 +502,7 @@ func TestEditorLigature(t *testing.T) { assertCaret(t, e, 0, 0, 0) gtx.Constraints = layout.Exact(image.Pt(50, 50)) e.SetText("fflffl fflffl fflffl fflffl") // Many ligatures broken across lines. - e.Layout(gtx, cache, font, fontSize, nil) + e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{}) // Ensure that all runes in the final cluster of a line are properly // decoded when moving to the end of the line. This is a regression test. e.text.MoveEnd(selectionClear) @@ -517,7 +517,7 @@ func TestEditorLigature(t *testing.T) { // Absurdly narrow constraints to force each ligature onto its own line. gtx.Constraints = layout.Exact(image.Pt(10, 10)) e.SetText("ffl ffl") // Two ligatures on separate lines. - e.Layout(gtx, cache, font, fontSize, nil) + e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{}) assertCaret(t, e, 0, 0, 0) e.MoveCaret(1, 1) // Move the caret into the first ligature. assertCaret(t, e, 0, 1, len("f")) @@ -541,7 +541,7 @@ func TestEditorDimensions(t *testing.T) { cache := text.NewShaper(gofont.Collection()) fontSize := unit.Sp(10) font := text.Font{} - dims := e.Layout(gtx, cache, font, fontSize, nil) + dims := e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{}) if dims.Size.X == 0 { t.Errorf("EditEvent was not reflected in Editor width") } @@ -591,7 +591,7 @@ func TestEditorCaretConsistency(t *testing.T) { for _, a := range []text.Alignment{text.Start, text.Middle, text.End} { e := &Editor{} e.Alignment = a - e.Layout(gtx, cache, font, fontSize, nil) + e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{}) consistent := func() error { t.Helper() @@ -615,7 +615,7 @@ func TestEditorCaretConsistency(t *testing.T) { switch mutation { case setText: e.SetText(str) - e.Layout(gtx, cache, font, fontSize, nil) + e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{}) case moveRune: e.MoveCaret(int(distance), int(distance)) case moveLine: @@ -681,7 +681,7 @@ func TestEditorMoveWord(t *testing.T) { fontSize := unit.Sp(10) font := text.Font{} e.SetText(t) - e.Layout(gtx, cache, font, fontSize, nil) + e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{}) return e } for ii, tt := range tests { @@ -786,7 +786,7 @@ func TestEditorInsert(t *testing.T) { fontSize := unit.Sp(10) font := text.Font{} e.SetText(t) - e.Layout(gtx, cache, font, fontSize, nil) + e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{}) return e } for ii, tt := range tests { @@ -876,7 +876,7 @@ func TestEditorDeleteWord(t *testing.T) { fontSize := unit.Sp(10) font := text.Font{} e.SetText(t) - e.Layout(gtx, cache, font, fontSize, nil) + e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{}) return e } for ii, tt := range tests { @@ -934,7 +934,7 @@ g 2 4 6 8 g selected := func(start, end int) string { // Layout once with no events; populate e.lines. gtx.Queue = nil - e.Layout(gtx, cache, font, fontSize, nil) + e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{}) _ = e.Events() // throw away any events from this layout // Build the selection events @@ -960,7 +960,7 @@ g 2 4 6 8 g tim += time.Second // Avoid multi-clicks. gtx.Queue = tq - e.Layout(gtx, cache, font, fontSize, nil) + e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{}) for _, evt := range e.Events() { switch evt.(type) { case SelectEvent: @@ -1006,7 +1006,7 @@ g 2 4 6 8 g gtx.Constraints = layout.Exact(image.Pt(36, 36)) // Keep existing selection gtx.Queue = nil - e.Layout(gtx, cache, font, fontSize, nil) + e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{}) caretStart := e.text.closestToRune(e.text.caret.start) caretEnd := e.text.closestToRune(e.text.caret.end) @@ -1030,7 +1030,7 @@ func TestSelectMove(t *testing.T) { // Layout once to populate e.lines and get focus. gtx.Queue = newQueue(key.FocusEvent{Focus: true}) - e.Layout(gtx, cache, font, fontSize, nil) + e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{}) testKey := func(keyName string) { // Select 345 @@ -1041,7 +1041,7 @@ func TestSelectMove(t *testing.T) { // Press the key gtx.Queue = newQueue(key.Event{State: key.Press, Name: keyName}) - e.Layout(gtx, cache, font, fontSize, nil) + e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{}) if expected, got := "", e.SelectedText(); expected != got { t.Errorf("KeyName %s, expected %q, got %q", keyName, expected, got) @@ -1115,7 +1115,7 @@ func TestEditor_MaxLen(t *testing.T) { cache := text.NewShaper(gofont.Collection()) fontSize := unit.Sp(10) font := text.Font{} - e.Layout(gtx, cache, font, fontSize, nil) + e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{}) if got, want := e.Text(), "12345678"; got != want { t.Errorf("editor failed to cap EditEvent") @@ -1146,7 +1146,7 @@ func TestEditor_Filter(t *testing.T) { cache := text.NewShaper(gofont.Collection()) fontSize := unit.Sp(10) font := text.Font{} - e.Layout(gtx, cache, font, fontSize, nil) + e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{}) if got, want := e.Text(), "12345678"; got != want { t.Errorf("editor failed to filter EditEvent") @@ -1170,7 +1170,7 @@ func TestEditor_Submit(t *testing.T) { cache := text.NewShaper(gofont.Collection()) fontSize := unit.Sp(10) font := text.Font{} - e.Layout(gtx, cache, font, fontSize, nil) + e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{}) if got, want := e.Text(), "ab1"; got != want { t.Errorf("editor failed to filter newline") diff --git a/widget/label.go b/widget/label.go index 2b1b836a..d0b56456 100644 --- a/widget/label.go +++ b/widget/label.go @@ -27,22 +27,22 @@ type Label struct { Selectable *Selectable } -// Layout the label with the given shaper, font, size, and text. Content is a function that will be invoked -// with the label's clip area applied, and should be used to set colors and paint the text/selection. -// content will only be invoked for labels with a non-nil Selectable. For stateless labels, the paint color -// should be set prior to calling Layout. -func (l Label) LayoutSelectable(gtx layout.Context, lt *text.Shaper, font text.Font, size unit.Sp, txt string, content layout.Widget) layout.Dimensions { +// Layout the label with the given shaper, font, size, text, and materials. If the Selectable field is +// populated, the label will support text selection. Otherwise, it will be non-interactive. The textMaterial +// and selectionMaterial op.CallOps are responsible for setting the painting material for the text glyphs +// and the text selection rectangles, respectively. +func (l Label) Layout(gtx layout.Context, lt *text.Shaper, font text.Font, size unit.Sp, txt string, textMaterial, selectionMaterial op.CallOp) layout.Dimensions { if l.Selectable == nil { - return l.Layout(gtx, lt, font, size, txt) + return l.layout(gtx, lt, font, size, txt, textMaterial, selectionMaterial) } l.Selectable.text.Alignment = l.Alignment l.Selectable.text.MaxLines = l.MaxLines l.Selectable.SetText(txt) - return l.Selectable.Layout(gtx, lt, font, size, content) + return l.Selectable.Layout(gtx, lt, font, size, textMaterial, selectionMaterial) } -// Layout the text as non-interactive. -func (l Label) Layout(gtx layout.Context, lt *text.Shaper, font text.Font, size unit.Sp, txt string) layout.Dimensions { +// layout the text as non-interactive. +func (l Label) layout(gtx layout.Context, lt *text.Shaper, font text.Font, size unit.Sp, txt string, textMaterial, selectionMaterial op.CallOp) layout.Dimensions { cs := gtx.Constraints textSize := fixed.I(gtx.Sp(size)) lt.LayoutString(text.Parameters{ @@ -53,7 +53,11 @@ func (l Label) Layout(gtx layout.Context, lt *text.Shaper, font text.Font, size }, cs.Min.X, cs.Max.X, gtx.Locale, txt) m := op.Record(gtx.Ops) viewport := image.Rectangle{Max: cs.Max} - it := textIterator{viewport: viewport, maxLines: l.MaxLines} + it := textIterator{ + viewport: viewport, + maxLines: l.MaxLines, + material: textMaterial, + } semantic.LabelOp(txt).Add(gtx.Ops) var glyphs [32]text.Glyph line := glyphs[:0] @@ -86,6 +90,9 @@ type textIterator struct { viewport image.Rectangle // maxLines is the maximum number of text lines that should be displayed. maxLines int + // material sets the paint material for the text glyphs. If none is provided + // the glyphs will be invisible. + material op.CallOp // linesSeen tracks the quantity of line endings this iterator has seen. linesSeen int @@ -165,9 +172,14 @@ func (it *textIterator) paintGlyph(gtx layout.Context, shaper *text.Shaper, glyp } if glyph.Flags&text.FlagLineBreak != 0 || cap(line)-len(line) == 0 || !visibleOrBefore { t := op.Offset(it.lineOff).Push(gtx.Ops) - op := clip.Outline{Path: shaper.Shape(line)}.Op().Push(gtx.Ops) + path := shaper.Shape(line) + outline := clip.Outline{Path: path}.Op().Push(gtx.Ops) + it.material.Add(gtx.Ops) paint.PaintOp{}.Add(gtx.Ops) - op.Pop() + outline.Pop() + if call := shaper.Bitmaps(line); call != (op.CallOp{}) { + call.Add(gtx.Ops) + } t.Pop() line = line[:0] } diff --git a/widget/material/button.go b/widget/material/button.go index 290b5476..156d967c 100644 --- a/widget/material/button.go +++ b/widget/material/button.go @@ -117,8 +117,9 @@ func (b ButtonStyle) Layout(gtx layout.Context) layout.Dimensions { Button: b.Button, }.Layout(gtx, func(gtx layout.Context) layout.Dimensions { return b.Inset.Layout(gtx, func(gtx layout.Context) layout.Dimensions { + colMacro := op.Record(gtx.Ops) paint.ColorOp{Color: b.Color}.Add(gtx.Ops) - return widget.Label{Alignment: text.Middle}.Layout(gtx, b.shaper, b.Font, b.TextSize, b.Text) + return widget.Label{Alignment: text.Middle}.Layout(gtx, b.shaper, b.Font, b.TextSize, b.Text, colMacro.Stop(), op.CallOp{}) }) }) } diff --git a/widget/material/checkable.go b/widget/material/checkable.go index 8312d99c..76bb7d49 100644 --- a/widget/material/checkable.go +++ b/widget/material/checkable.go @@ -8,6 +8,7 @@ import ( "gioui.org/internal/f32color" "gioui.org/layout" + "gioui.org/op" "gioui.org/op/clip" "gioui.org/op/paint" "gioui.org/text" @@ -73,8 +74,9 @@ func (c *checkable) layout(gtx layout.Context, checked, hovered bool) layout.Dim layout.Rigid(func(gtx layout.Context) layout.Dimensions { return layout.UniformInset(2).Layout(gtx, func(gtx layout.Context) layout.Dimensions { + colMacro := op.Record(gtx.Ops) paint.ColorOp{Color: c.Color}.Add(gtx.Ops) - return widget.Label{}.Layout(gtx, c.shaper, c.Font, c.TextSize, c.Label) + return widget.Label{}.Layout(gtx, c.shaper, c.Font, c.TextSize, c.Label, colMacro.Stop(), op.CallOp{}) }) }), ) diff --git a/widget/material/editor.go b/widget/material/editor.go index 9b25cf9f..2d3c486a 100644 --- a/widget/material/editor.go +++ b/widget/material/editor.go @@ -6,7 +6,6 @@ import ( "image/color" "gioui.org/internal/f32color" - "gioui.org/io/semantic" "gioui.org/layout" "gioui.org/op" "gioui.org/op/paint" @@ -44,38 +43,37 @@ func Editor(th *Theme, editor *widget.Editor, hint string) EditorStyle { } func (e EditorStyle) Layout(gtx layout.Context) layout.Dimensions { - macro := op.Record(gtx.Ops) + // Choose colors. + textColorMacro := op.Record(gtx.Ops) + paint.ColorOp{Color: e.Color}.Add(gtx.Ops) + textColor := textColorMacro.Stop() + hintColorMacro := op.Record(gtx.Ops) paint.ColorOp{Color: e.HintColor}.Add(gtx.Ops) + hintColor := hintColorMacro.Stop() + selectionColorMacro := op.Record(gtx.Ops) + paint.ColorOp{Color: blendDisabledColor(gtx.Queue == nil, e.SelectionColor)}.Add(gtx.Ops) + selectionColor := selectionColorMacro.Stop() + var maxlines int if e.Editor.SingleLine { maxlines = 1 } + + macro := op.Record(gtx.Ops) tl := widget.Label{Alignment: e.Editor.Alignment, MaxLines: maxlines} - dims := tl.Layout(gtx, e.shaper, e.Font, e.TextSize, e.Hint) + dims := tl.Layout(gtx, e.shaper, e.Font, e.TextSize, e.Hint, hintColor, selectionColor) call := macro.Stop() + if w := dims.Size.X; gtx.Constraints.Min.X < w { gtx.Constraints.Min.X = w } if h := dims.Size.Y; gtx.Constraints.Min.Y < h { gtx.Constraints.Min.Y = h } - dims = e.Editor.Layout(gtx, e.shaper, e.Font, e.TextSize, func(gtx layout.Context) layout.Dimensions { - semantic.Editor.Add(gtx.Ops) - disabled := gtx.Queue == nil - if e.Editor.Len() > 0 { - paint.ColorOp{Color: blendDisabledColor(disabled, e.SelectionColor)}.Add(gtx.Ops) - e.Editor.PaintSelection(gtx) - paint.ColorOp{Color: blendDisabledColor(disabled, e.Color)}.Add(gtx.Ops) - e.Editor.PaintText(gtx) - } else { - call.Add(gtx.Ops) - } - if !disabled { - paint.ColorOp{Color: e.Color}.Add(gtx.Ops) - e.Editor.PaintCaret(gtx) - } - return dims - }) + dims = e.Editor.Layout(gtx, e.shaper, e.Font, e.TextSize, textColor, selectionColor) + if e.Editor.Len() == 0 { + call.Add(gtx.Ops) + } return dims } diff --git a/widget/material/label.go b/widget/material/label.go index 2a9ae9b5..28ced81c 100644 --- a/widget/material/label.go +++ b/widget/material/label.go @@ -7,6 +7,7 @@ import ( "gioui.org/internal/f32color" "gioui.org/layout" + "gioui.org/op" "gioui.org/op/paint" "gioui.org/text" "gioui.org/unit" @@ -98,16 +99,13 @@ func Label(th *Theme, size unit.Sp, txt string) LabelStyle { } func (l LabelStyle) Layout(gtx layout.Context) layout.Dimensions { + textColorMacro := op.Record(gtx.Ops) paint.ColorOp{Color: l.Color}.Add(gtx.Ops) + textColor := textColorMacro.Stop() + selectColorMacro := op.Record(gtx.Ops) + paint.ColorOp{Color: l.SelectionColor}.Add(gtx.Ops) + selectColor := selectColorMacro.Stop() + tl := widget.Label{Alignment: l.Alignment, MaxLines: l.MaxLines, Selectable: l.State} - if l.State == nil { - return tl.Layout(gtx, l.shaper, l.Font, l.TextSize, l.Text) - } - return tl.LayoutSelectable(gtx, l.shaper, l.Font, l.TextSize, l.Text, func(gtx layout.Context) layout.Dimensions { - paint.ColorOp{Color: l.SelectionColor}.Add(gtx.Ops) - l.State.PaintSelection(gtx) - paint.ColorOp{Color: l.Color}.Add(gtx.Ops) - l.State.PaintText(gtx) - return layout.Dimensions{} - }) + return tl.Layout(gtx, l.shaper, l.Font, l.TextSize, l.Text, textColor, selectColor) } diff --git a/widget/selectable.go b/widget/selectable.go index 162156a2..83b2a2ad 100644 --- a/widget/selectable.go +++ b/widget/selectable.go @@ -12,6 +12,7 @@ import ( "gioui.org/io/pointer" "gioui.org/io/system" "gioui.org/layout" + "gioui.org/op" "gioui.org/op/clip" "gioui.org/text" "gioui.org/unit" @@ -91,18 +92,19 @@ func (l *Selectable) Focused() bool { return l.focused } -// PaintSelection paints the contrasting background for selected text. -func (l *Selectable) PaintSelection(gtx layout.Context) { +// paintSelection paints the contrasting background for selected text. +func (l *Selectable) paintSelection(gtx layout.Context, material op.CallOp) { l.initialize() if !l.focused { return } - l.text.PaintSelection(gtx) + l.text.PaintSelection(gtx, material) } -func (l *Selectable) PaintText(gtx layout.Context) { +// paintText paints the text glyphs with the provided material. +func (l *Selectable) paintText(gtx layout.Context, material op.CallOp) { l.initialize() - l.text.PaintText(gtx) + l.text.PaintText(gtx, material) } // SelectionLen returns the length of the selection, in runes; it is @@ -158,10 +160,10 @@ func (l *Selectable) SetText(s string) { } } -// Layout clips to the dimensions of the selectable, updates the shaped text, configures input handling, and invokes -// content. content is expected to set colors and invoke the Paint methods. content may be nil, in which case nothing -// will be displayed. -func (l *Selectable) Layout(gtx layout.Context, lt *text.Shaper, font text.Font, size unit.Sp, content layout.Widget) layout.Dimensions { +// Layout clips to the dimensions of the selectable, updates the shaped text, configures input handling, and paints +// the text and selection rectangles. The provided textMaterial and selectionMaterial ops are used to set the +// paint material for the text and selection rectangles, respectively. +func (l *Selectable) Layout(gtx layout.Context, lt *text.Shaper, font text.Font, size unit.Sp, textMaterial, selectionMaterial op.CallOp) layout.Dimensions { l.initialize() l.text.Update(gtx, lt, font, size, l.handleEvents) dims := l.text.Dimensions() @@ -182,9 +184,8 @@ func (l *Selectable) Layout(gtx layout.Context, lt *text.Shaper, font text.Font, l.clicker.Add(gtx.Ops) l.dragger.Add(gtx.Ops) - if content != nil { - content(gtx) - } + l.paintSelection(gtx, selectionMaterial) + l.paintText(gtx, textMaterial) return dims } diff --git a/widget/selectable_test.go b/widget/selectable_test.go index 732cf8b8..1941ebe6 100644 --- a/widget/selectable_test.go +++ b/widget/selectable_test.go @@ -46,10 +46,9 @@ func TestSelectableMove(t *testing.T) { gtx.Queue = newQueue(key.FocusEvent{Focus: true}) s := new(Selectable) - w := func(layout.Context) layout.Dimensions { return layout.Dimensions{} } Label{ Selectable: s, - }.LayoutSelectable(gtx, cache, text.Font{}, fontSize, str, w) + }.Layout(gtx, cache, text.Font{}, fontSize, str, op.CallOp{}, op.CallOp{}) testKey := func(keyName string) { // Select 345 @@ -65,7 +64,7 @@ func TestSelectableMove(t *testing.T) { gtx.Queue = newQueue(key.Event{State: key.Press, Name: keyName}) Label{ Selectable: s, - }.LayoutSelectable(gtx, cache, font, fontSize, str, w) + }.Layout(gtx, cache, font, fontSize, str, op.CallOp{}, op.CallOp{}) if expected, got := "", s.SelectedText(); expected != got { t.Errorf("KeyName %s, expected %q, got %q", keyName, expected, got) @@ -88,7 +87,6 @@ func TestSelectableConfigurations(t *testing.T) { fontSize := unit.Sp(10) font := text.Font{} sentence := "\n\n\n\n\n\n\n\n\n\n\n\nthe quick brown fox jumps over the lazy dog" - w := func(layout.Context) layout.Dimensions { return layout.Dimensions{} } for _, alignment := range []text.Alignment{text.Start, text.Middle, text.End} { for _, zeroMin := range []bool{true, false} { @@ -108,8 +106,8 @@ func TestSelectableConfigurations(t *testing.T) { Alignment: alignment, Selectable: s, } - interactiveDims := label.LayoutSelectable(gtx, cache, font, fontSize, sentence, w) - staticDims := label.Layout(gtx, cache, font, fontSize, sentence) + interactiveDims := label.Layout(gtx, cache, font, fontSize, sentence, op.CallOp{}, op.CallOp{}) + staticDims := label.Layout(gtx, cache, font, fontSize, sentence, op.CallOp{}, op.CallOp{}) if interactiveDims != staticDims { t.Errorf("expected consistent dimensions, static returned %#+v, interactive returned %#+v", staticDims, interactiveDims) diff --git a/widget/text.go b/widget/text.go index 94010b20..687c5ccd 100644 --- a/widget/text.go +++ b/widget/text.go @@ -234,31 +234,33 @@ func (e *textView) Update(gtx layout.Context, lt *text.Shaper, font text.Font, s e.makeValid() } -// PaintSelection clips and paints the visible text selection rectangles. Callers -// are expected to apply an appropriate paint material with a paint.ColorOp or -// similar prior to calling PaintSelection. -func (e *textView) PaintSelection(gtx layout.Context) { +// PaintSelection clips and paints the visible text selection rectangles using +// the provided material to fill the rectangles. +func (e *textView) PaintSelection(gtx layout.Context, material op.CallOp) { localViewport := image.Rectangle{Max: e.viewSize} docViewport := image.Rectangle{Max: e.viewSize}.Add(e.scrollOff) defer clip.Rect(localViewport).Push(gtx.Ops).Pop() e.regions = e.index.locate(docViewport, e.caret.start, e.caret.end, e.regions) for _, region := range e.regions { area := clip.Rect(region.Bounds).Push(gtx.Ops) + material.Add(gtx.Ops) paint.PaintOp{}.Add(gtx.Ops) area.Pop() } } -// PaintText clips and paints the visible text glyph outlines. Callers -// are expected to apply an appropriate paint material with a paint.ColorOp or -// similar prior to calling PaintSelection. -func (e *textView) PaintText(gtx layout.Context) { +// PaintText clips and paints the visible text glyph outlines using the provided +// material to fill the glyphs. +func (e *textView) PaintText(gtx layout.Context, material op.CallOp) { m := op.Record(gtx.Ops) viewport := image.Rectangle{ Min: e.scrollOff, Max: e.viewSize.Add(e.scrollOff), } - it := textIterator{viewport: viewport} + it := textIterator{ + viewport: viewport, + material: material, + } startGlyph := 0 for _, line := range e.index.lines { @@ -293,10 +295,9 @@ func (e *textView) caretWidth(gtx layout.Context) int { return carWidth2 } -// PaintCaret clips and paints the caret rectangle. Callers -// are expected to apply an appropriate paint material with a paint.ColorOp or -// similar prior to calling PaintSelection. -func (e *textView) PaintCaret(gtx layout.Context) { +// PaintCaret clips and paints the caret rectangle, adding material immediately +// before painting to set the appropriate paint material. +func (e *textView) PaintCaret(gtx layout.Context, material op.CallOp) { carWidth2 := e.caretWidth(gtx) caretPos, carAsc, carDesc := e.CaretInfo() @@ -308,6 +309,7 @@ func (e *textView) PaintCaret(gtx layout.Context) { carRect = cl.Intersect(carRect) if !carRect.Empty() { defer clip.Rect(carRect).Push(gtx.Ops).Pop() + material.Add(gtx.Ops) paint.PaintOp{}.Add(gtx.Ops) } } diff --git a/widget/text_bench_test.go b/widget/text_bench_test.go index 03d834cd..3a73df69 100644 --- a/widget/text_bench_test.go +++ b/widget/text_bench_test.go @@ -83,7 +83,7 @@ func BenchmarkLabelStatic(b *testing.B) { l := Label{} b.ResetTimer() for i := 0; i < b.N; i++ { - l.Layout(gtx, cache, font, fontSize, runesStr) + l.Layout(gtx, cache, font, fontSize, runesStr, op.CallOp{}, op.CallOp{}) if render { win.Frame(gtx.Ops) } @@ -118,7 +118,7 @@ func BenchmarkLabelDynamic(b *testing.B) { a := rand.Intn(len(runes)) b := rand.Intn(len(runes)) runes[a], runes[b] = runes[b], runes[a] - l.Layout(gtx, cache, font, fontSize, string(runes)) + l.Layout(gtx, cache, font, fontSize, string(runes), op.CallOp{}, op.CallOp{}) if render { win.Frame(gtx.Ops) } @@ -151,12 +151,7 @@ func BenchmarkEditorStatic(b *testing.B) { e.SetText(runesStr) b.ResetTimer() for i := 0; i < b.N; i++ { - e.Layout(gtx, cache, font, fontSize, func(gtx layout.Context) layout.Dimensions { - e.PaintSelection(gtx) - e.PaintText(gtx) - e.PaintCaret(gtx) - return layout.Dimensions{Size: gtx.Constraints.Min} - }) + e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{}) if render { win.Frame(gtx.Ops) } @@ -196,12 +191,7 @@ func BenchmarkEditorDynamic(b *testing.B) { e.Insert("") e.SetCaret(b, b) e.Insert(takeStr) - e.Layout(gtx, cache, font, fontSize, func(gtx layout.Context) layout.Dimensions { - e.PaintSelection(gtx) - e.PaintText(gtx) - e.PaintCaret(gtx) - return layout.Dimensions{Size: gtx.Constraints.Min} - }) + e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{}) if render { win.Frame(gtx.Ops) } @@ -225,12 +215,7 @@ func FuzzEditorEditing(f *testing.F) { e := Editor{} f.Fuzz(func(t *testing.T, txt string, replaceFrom, replaceTo int16) { e.SetText(txt) - e.Layout(gtx, cache, font, fontSize, func(gtx layout.Context) layout.Dimensions { - e.PaintSelection(gtx) - e.PaintText(gtx) - e.PaintCaret(gtx) - return layout.Dimensions{Size: gtx.Constraints.Min} - }) + e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{}) // simulate a constantly changing string if e.Len() > 0 { a := int(replaceFrom) % e.Len() @@ -241,12 +226,7 @@ func FuzzEditorEditing(f *testing.F) { e.SetCaret(b, b) e.Insert(takeStr) } - e.Layout(gtx, cache, font, fontSize, func(gtx layout.Context) layout.Dimensions { - e.PaintSelection(gtx) - e.PaintText(gtx) - e.PaintCaret(gtx) - return layout.Dimensions{Size: gtx.Constraints.Min} - }) + e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{}) gtx.Ops.Reset() }) }