M app/ime_test.go => app/ime_test.go +2 -2
@@ 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.
M font/opentype/opentype.go => font/opentype/opentype.go +7 -0
@@ 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"
)
M text/gotext.go => text/gotext.go +69 -3
@@ 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.
M text/lru.go => text/lru.go +101 -103
@@ 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
+}
+
+// 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
}
-func (l *layoutCache) insert(lt *layoutElem) {
- lt.next = l.head
- lt.prev = l.head.prev
- lt.prev.next = lt
- lt.next.prev = lt
+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,40 115,15 @@ func (c *pathCache) hashGlyphs(gs []Glyph) uint64 {
return sum
}
-func gidsEqual(a []glyphInfo, glyphs []Glyph) bool {
- if len(a) != len(glyphs) {
- return false
+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
}
- firstX := fixed.Int26_6(0)
- for i := range a {
- if i == 0 {
- firstX = glyphs[i].X
- }
- // Cache glyph X offsets relative to the first glyph.
- if a[i].ID != glyphs[i].ID || a[i].X != (glyphs[i].X-firstX) {
- return false
- }
- }
- 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
+ var v V
+ return v, 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
- }
+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 {
@@ 161,24 133,50 @@ func (c *pathCache) Put(key uint64, glyphs []Glyph, v clip.PathSpec) {
// 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)
+ 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
}
-func (c *pathCache) remove(v *path) {
- v.next.prev = v.prev
- v.prev.next = v.next
+type pathKey struct {
+ gidHash uint64
}
-func (c *pathCache) insert(v *path) {
- v.next = c.head
- v.prev = c.head.prev
- v.prev.next = v
- v.next.prev = v
+const maxSize = 1000
+
+func gidsEqual(a []glyphInfo, glyphs []Glyph) bool {
+ if len(a) != len(glyphs) {
+ return false
+ }
+ firstX := fixed.Int26_6(0)
+ for i := range a {
+ if i == 0 {
+ firstX = glyphs[i].X
+ }
+ // Cache glyph X offsets relative to the first glyph.
+ if a[i].ID != glyphs[i].ID || a[i].X != (glyphs[i].X-firstX) {
+ return false
+ }
+ }
+ return true
}
M text/shaper.go => text/shaper.go +26 -9
@@ 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
+}
M widget/editor.go => widget/editor.go +27 -13
@@ 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.
M widget/editor_test.go => widget/editor_test.go +31 -31
@@ 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")
M widget/label.go => widget/label.go +24 -12
@@ 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]
}
M widget/material/button.go => widget/material/button.go +2 -1
@@ 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{})
})
})
}
M widget/material/checkable.go => widget/material/checkable.go +3 -1
@@ 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{})
})
}),
)
M widget/material/editor.go => widget/material/editor.go +18 -20
@@ 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
}
M widget/material/label.go => widget/material/label.go +8 -10
@@ 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)
}
M widget/selectable.go => widget/selectable.go +13 -12
@@ 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
}
M widget/selectable_test.go => widget/selectable_test.go +4 -6
@@ 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)
M widget/text.go => widget/text.go +15 -13
@@ 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)
}
}
M widget/text_bench_test.go => widget/text_bench_test.go +6 -26
@@ 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()
})
}