font/opentype,text,widget{,/material}: [API] support bitmap glyph rendering

This commit supports rendering opentype glyphs containing bitmap data instead of
color data. In order to support returning the shaped bitmap glyphs from the Shaper's
Shape() method, it has gained a second return parameter, an op.CallOp. Adding
that CallOp immediately after or immediately before painting the returned path
will display the bitmap glyphs.

The consequences of supporting colored glyphs forced changes upon the widget APIs
for widgets that display text. Previously text always had a fixed paint material,
so we could rely upon the caller setting the material (e.g. adding a paint.ColorOp)
before painting the glyphs and everything would work. Now that we display image-
based glyphs, we end up changing the painting material to an image midway through
displaying text. This is an awkward consequence of how we currently manage the
painting material, and to work around it widgets now accept an op.CallOp that
is expected to set the proper paint material. Text widgets will use that op.CallOp
before painting text (or other paint operations) to ensure that they are painting
with the proper materials.

This, in turn, changed the APIs for laying out widget.Editor, widget.Label, and
widget.Selectable, and eliminated the need for them to accept a callback (the
callback was only really to set the colors). Dropping that callback function
allowed me to consolidate widget.Label to only need one exported Layout method,
and allowed me to unexport the PaintText, PaintCaret, and PaintSelection methods
from widget.Editor and widget.Selectable. Those methods are useless in the public
API now that they don't need to be invoked after applying a color operation.

Callers of the raw text shaper API will need to make the following changes:

- Where before you used:

	var ops *op.Ops // Assume we have an operation list.
	var shaper *text.Shaper // Assume we have a shaper.
	var col color.NRGBA // Assume we have a text color.
	var glyphs []text.Glyph // Assume we have already filled a slice of glyphs.

	shape := shaper.Shape(glyphs)
	paint.FillShape(ops, col, clip.Outline{Path:shape}.Op())

- Now you should do:

	shape, call := shaper.Shape(glyphs)
	paint.FillShape(ops, col, clip.Outline{Path:shape}.Op())
	call.Add(ops)

Callers of the widget.{Label,Selectable,Editor} APIs will need to make the
following changes:

- Where before you used:

	var gtx layout.Context // Assume we have an operation list.
	var shaper *text.Shaper // Assume we have a shaper.
	var textCol color.NRGBA // Assume we have a text color.
	var selectCol color.NRGBA // Assume we have a selection color.
	var ed widget.Editor // Assume we have an editor.
	var sel widget.Selectable // Assume we have a selectable.

	// Lay out an editor.
	ed.Layout(gtx, shaper, text.Font{}, unit.Sp(30), func(layout.Context) layout.Dimensions {
		// Paint the editor.
	})
	// Lay out a selectable.
	sel.Layout(gtx, shaper, text.Font{}, unit.Sp(30), func(layout.Context) layout.Dimensions {
		// Paint the selectable.
	})
	// Lay out an interactive label.
	widget.Label{}.LayoutSelectable(gtx, shaper, text.Font{}, unit.Sp(30), "hello", func(layout.Context) layout.Dimensions {
		// Paint the label.
	})
	// Lay out a non-interactive label.
	widget.Label{}.Layout(gtx, shaper, text.Font{}, unit.Sp(30), "hello")

- Now you should do:

	// Capture setting the text paint material in a macro.
	textColMacro := op.Record(gtx.Ops)
	paint.ColorOp{Color: textCol}.Add(gtx.Ops)
	textMaterial := textColMacro.Stop()
	// Capture setting the selection paint material in a macro.
	selectColMacro := op.Record(gtx.Ops)
	paint.ColorOp{Color: selectCol}.Add(gtx.Ops)
	selectMaterial := selectColMacro.Stop()

	// Lay out an editor.
	ed.Layout(gtx, shaper, text.Font{}, unit.Sp(30), textMaterial, selectMaterial)
	// Lay out a selectable.
	sel.Layout(gtx, shaper, text.Font{}, unit.Sp(30), textMaterial, selectMaterial)
	// Lay out a label (no difference between interactive and non-interactive)
	widget.Label{}.Layout(gtx, shaper, text.Font{}, unit.Sp(30), "hello", textMaterial, selectMaterial)

Callers of the material package API do not need to make any changes.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
This commit is contained in:
Chris Waldon
2023-03-07 13:02:05 -05:00
committed by Elias Naur
parent 47d25c1394
commit 6ab3ff40a6
16 changed files with 358 additions and 264 deletions
+2 -2
View File
@@ -35,7 +35,7 @@ func FuzzIME(f *testing.F) {
var r router.Router var r router.Router
gtx := layout.Context{Ops: new(op.Ops), Queue: &r} gtx := layout.Context{Ops: new(op.Ops), Queue: &r}
// Layout once to register focus. // 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) r.Frame(gtx.Ops)
var state editorState var state editorState
@@ -103,7 +103,7 @@ func FuzzIME(f *testing.F) {
} }
} }
cmds = cmds[cmdLen:] 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) r.Frame(gtx.Ops)
newState := r.EditorState() newState := r.EditorState()
// We don't track caret position. // We don't track caret position.
+7
View File
@@ -2,11 +2,18 @@
// Package opentype implements text layout and shaping for OpenType // Package opentype implements text layout and shaping for OpenType
// files. // 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 package opentype
import ( import (
"bytes" "bytes"
"fmt" "fmt"
_ "image/png"
"github.com/go-text/typesetting/font" "github.com/go-text/typesetting/font"
) )
+69 -3
View File
@@ -3,6 +3,8 @@
package text package text
import ( import (
"bytes"
"image"
"io" "io"
"sort" "sort"
@@ -19,6 +21,7 @@ import (
"gioui.org/io/system" "gioui.org/io/system"
"gioui.org/op" "gioui.org/op"
"gioui.org/op/clip" "gioui.org/op/clip"
"gioui.org/op/paint"
) )
// document holds a collection of shaped lines and alignment information for // document holds a collection of shaped lines and alignment information for
@@ -504,12 +507,13 @@ func alignWidth(minWidth int, lines []line) int {
return minWidth return minWidth
} }
// Shape converts the provided glyphs into a path. // Shape converts the provided glyphs into a path. The path will enclose the forms
func (s *shaperImpl) Shape(ops *op.Ops, gs []Glyph) clip.PathSpec { // of all vector glyphs.
func (s *shaperImpl) Shape(pathOps *op.Ops, gs []Glyph) clip.PathSpec {
var lastPos f32.Point var lastPos f32.Point
var x fixed.Int26_6 var x fixed.Int26_6
var builder clip.Path var builder clip.Path
builder.Begin(ops) builder.Begin(pathOps)
for i, g := range gs { for i, g := range gs {
if i == 0 { if i == 0 {
x = g.X x = g.X
@@ -570,6 +574,68 @@ func (s *shaperImpl) Shape(ops *op.Ops, gs []Glyph) clip.PathSpec {
return builder.End() 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. // langConfig describes the language and writing system of a body of text.
type langConfig struct { type langConfig struct {
// Language the text is written in. // Language the text is written in.
+103 -105
View File
@@ -5,74 +5,50 @@ package text
import ( import (
"encoding/binary" "encoding/binary"
"hash/maphash" "hash/maphash"
"image"
"gioui.org/io/system" "gioui.org/io/system"
"gioui.org/op"
"gioui.org/op/clip" "gioui.org/op/clip"
"gioui.org/op/paint"
"golang.org/x/image/math/fixed" "golang.org/x/image/math/fixed"
) )
type layoutCache struct { // entry holds a single key-value pair for an LRU cache.
m map[layoutKey]*layoutElem type entry[K comparable, V any] struct {
head, tail *layoutElem next, prev *entry[K, V]
key K
v V
} }
type pathCache struct { // lru is a generic least-recently-used cache.
seed maphash.Seed type lru[K comparable, V any] struct {
m map[uint64]*path m map[K]*entry[K, V]
head, tail *path head, tail *entry[K, V]
} }
type layoutElem struct { // Get fetches the value associated with the given key, if any.
next, prev *layoutElem func (l *lru[K, V]) Get(k K) (V, bool) {
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) {
if lt, ok := l.m[k]; ok { if lt, ok := l.m[k]; ok {
l.remove(lt) l.remove(lt)
l.insert(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 { if l.m == nil {
l.m = make(map[layoutKey]*layoutElem) l.m = make(map[K]*entry[K, V])
l.head = new(layoutElem) l.head = new(entry[K, V])
l.tail = new(layoutElem) l.tail = new(entry[K, V])
l.head.prev = l.tail l.head.prev = l.tail
l.tail.next = l.head l.tail.next = l.head
} }
val := &layoutElem{key: k, layout: lt} val := &entry[K, V]{key: k, v: v}
l.m[k] = val l.m[k] = val
l.insert(val) l.insert(val)
if len(l.m) > maxSize { if len(l.m) > maxSize {
@@ -82,21 +58,42 @@ func (l *layoutCache) Put(k layoutKey, lt document) {
} }
} }
func (l *layoutCache) remove(lt *layoutElem) { // remove cuts e out of the lru linked list.
lt.next.prev = lt.prev func (l *lru[K, V]) remove(e *entry[K, V]) {
lt.prev.next = lt.next e.next.prev = e.prev
e.prev.next = e.next
} }
func (l *layoutCache) insert(lt *layoutElem) { // insert adds e to the lru linked list.
lt.next = l.head func (l *lru[K, V]) insert(e *entry[K, V]) {
lt.prev = l.head.prev e.next = l.head
lt.prev.next = lt e.prev = l.head.prev
lt.next.prev = lt 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 // hashGlyphs computes a hash key based on the ID and X offset of
// every glyph in the slice. // 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{}) { if c.seed == (maphash.Seed{}) {
c.seed = maphash.MakeSeed() c.seed = maphash.MakeSeed()
} }
@@ -118,6 +115,55 @@ func (c *pathCache) hashGlyphs(gs []Glyph) uint64 {
return sum 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 { func gidsEqual(a []glyphInfo, glyphs []Glyph) bool {
if len(a) != len(glyphs) { if len(a) != len(glyphs) {
return false return false
@@ -134,51 +180,3 @@ func gidsEqual(a []glyphInfo, glyphs []Glyph) bool {
} }
return true 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
}
+26 -9
View File
@@ -146,10 +146,11 @@ type GlyphID uint64
// Shaper converts strings of text into glyphs that can be displayed. // Shaper converts strings of text into glyphs that can be displayed.
type Shaper struct { type Shaper struct {
shaper shaperImpl shaper shaperImpl
pathCache pathCache pathCache pathCache
layoutCache layoutCache bitmapShapeCache bitmapShapeCache
paragraph []rune layoutCache layoutCache
paragraph []rune
reader strings.Reader reader strings.Reader
@@ -440,17 +441,33 @@ func splitGlyphID(g GlyphID) (fixed.Int26_6, int, font.GID) {
return ppem, faceIdx, gid return ppem, faceIdx, gid
} }
// Shape converts a slice of glyphs into a path describing their collective // Shape converts the provided glyphs into a path. The path will enclose the forms
// shape. All glyphs are expected to be from a single line of text (their // of all vector glyphs.
// Y offsets are ignored). // 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 { func (l *Shaper) Shape(gs []Glyph) clip.PathSpec {
key := l.pathCache.hashGlyphs(gs) key := l.pathCache.hashGlyphs(gs)
shape, ok := l.pathCache.Get(key, gs) shape, ok := l.pathCache.Get(key, gs)
if ok { if ok {
return shape return shape
} }
ops := new(op.Ops) pathOps := new(op.Ops)
shape = l.shaper.Shape(ops, gs) shape = l.shaper.Shape(pathOps, gs)
l.pathCache.Put(key, gs, shape) l.pathCache.Put(key, gs, shape)
return 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
}
+27 -13
View File
@@ -18,6 +18,7 @@ import (
"gioui.org/io/event" "gioui.org/io/event"
"gioui.org/io/key" "gioui.org/io/key"
"gioui.org/io/pointer" "gioui.org/io/pointer"
"gioui.org/io/semantic"
"gioui.org/io/system" "gioui.org/io/system"
"gioui.org/layout" "gioui.org/layout"
"gioui.org/op" "gioui.org/op"
@@ -504,12 +505,14 @@ func (e *Editor) initBuffer() {
e.text.Mask = e.Mask e.text.Mask = e.Mask
} }
// Layout lays out the editor. If content is not nil, it is laid out on top. // Layout lays out the editor using the provided textMaterial as the paint material
func (e *Editor) Layout(gtx layout.Context, lt *text.Shaper, font text.Font, size unit.Sp, content layout.Widget) layout.Dimensions { // 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.initBuffer()
e.text.Update(gtx, lt, font, size, e.processEvents) e.text.Update(gtx, lt, font, size, e.processEvents)
dims := e.layout(gtx, content) dims := e.layout(gtx, textMaterial, selectMaterial)
if e.focused { if e.focused {
// Notify IME of selection if it changed. // Notify IME of selection if it changed.
@@ -586,7 +589,7 @@ func (e *Editor) updateSnippet(gtx layout.Context, start, end int) {
}.Add(gtx.Ops) }.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. // Adjust scrolling for new viewport and layout.
e.text.ScrollRel(0, 0) 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) e.showCaret = e.focused && (!blinking || dt%timePerBlink < timePerBlink/2)
} }
disabled := gtx.Queue == nil
if content != nil { semantic.Editor.Add(gtx.Ops)
content(gtx) if e.Len() > 0 {
e.paintSelection(gtx, selectMaterial)
e.paintText(gtx, textMaterial)
}
if !disabled {
e.paintCaret(gtx, textMaterial)
} }
return visibleDims return visibleDims
} }
// PaintSelection paints the contrasting background for selected text. // paintSelection paints the contrasting background for selected text using the provided
func (e *Editor) PaintSelection(gtx layout.Context) { // material to set the painting material for the selection.
func (e *Editor) paintSelection(gtx layout.Context, material op.CallOp) {
e.initBuffer() e.initBuffer()
if !e.focused { if !e.focused {
return 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.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() e.initBuffer()
if !e.showCaret || e.ReadOnly { if !e.showCaret || e.ReadOnly {
return return
} }
e.text.PaintCaret(gtx) e.text.PaintCaret(gtx, material)
} }
// Len is the length of the editor contents, in runes. // Len is the length of the editor contents, in runes.
+31 -31
View File
@@ -117,12 +117,12 @@ func TestEditorReadOnly(t *testing.T) {
if cStart != cEnd { if cStart != cEnd {
t.Errorf("unexpected initial caret positions") 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. // Select everything.
gtx.Ops.Reset() gtx.Ops.Reset()
gtx.Queue = &testQueue{events: []event.Event{key.Event{Name: "A", Modifiers: key.ModShortcut}}} 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() textContent := e.Text()
cStart2, cEnd2 := e.Selection() cStart2, cEnd2 := e.Selection()
if cStart2 > cEnd2 { if cStart2 > cEnd2 {
@@ -138,7 +138,7 @@ func TestEditorReadOnly(t *testing.T) {
// Type some new characters. // Type some new characters.
gtx.Ops.Reset() gtx.Ops.Reset()
gtx.Queue = &testQueue{events: []event.Event{key.EditEvent{Range: key.Range{Start: cStart2, End: cEnd2}, Text: "something else"}}} 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() textContent2 := e.Text()
if textContent2 != textContent { if textContent2 != textContent {
t.Errorf("readonly editor modified by key.EditEvent") t.Errorf("readonly editor modified by key.EditEvent")
@@ -147,7 +147,7 @@ func TestEditorReadOnly(t *testing.T) {
// Try to delete selection. // Try to delete selection.
gtx.Ops.Reset() gtx.Ops.Reset()
gtx.Queue = &testQueue{events: []event.Event{key.Event{Name: key.NameDeleteBackward}}} 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() textContent2 = e.Text()
if textContent2 != textContent { if textContent2 != textContent {
t.Errorf("readonly editor modified by delete key.Event") 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), 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() cStart3, cEnd3 := e.Selection()
if cStart3 == cStart2 || cEnd3 == cEnd2 { if cStart3 == cStart2 || cEnd3 == cEnd2 {
t.Errorf("expected mouse interaction to change selection.") t.Errorf("expected mouse interaction to change selection.")
@@ -213,7 +213,7 @@ func TestEditorConfigurations(t *testing.T) {
e.Alignment = alignment e.Alignment = alignment
e.SetText(sentence) e.SetText(sentence)
e.SetCaret(0, 0) 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 { 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) 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) t.Errorf("expected caret X to be %f, got %f", halfway, coords.X)
} }
e.SetCaret(runes, runes) e.SetCaret(runes, runes)
e.Layout(gtx, cache, font, fontSize, nil) e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
coords = e.CaretCoords() coords = e.CaretCoords()
if int(coords.X) > gtx.Constraints.Max.X || int(coords.Y) > gtx.Constraints.Max.Y { 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) 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. // Regression test for bad in-cluster rune offset math.
e.SetText("æbc") e.SetText("æbc")
e.Layout(gtx, cache, font, fontSize, nil) e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
e.text.MoveEnd(selectionClear) e.text.MoveEnd(selectionClear)
assertCaret(t, e, 0, 3, len("æbc")) 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 { if got, exp := e.Len(), utf8.RuneCountInString(e.Text()); got != exp {
t.Errorf("got length %d, expected %d", 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) assertCaret(t, e, 0, 0, 0)
e.text.MoveEnd(selectionClear) e.text.MoveEnd(selectionClear)
assertCaret(t, e, 0, 3, len("æbc")) assertCaret(t, e, 0, 3, len("æbc"))
@@ -284,7 +284,7 @@ func TestEditor(t *testing.T) {
e.MoveCaret(-3, -3) e.MoveCaret(-3, -3)
assertCaret(t, e, 1, 1, len("æbc\na")) assertCaret(t, e, 1, 1, len("æbc\na"))
e.text.Mask = '*' 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")) assertCaret(t, e, 1, 1, len("æbc\na"))
e.MoveCaret(-3, -3) e.MoveCaret(-3, -3)
assertCaret(t, e, 0, 2, len("æb")) 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 NOTE(whereswaldon): it isn't possible to check the raw glyph data
like this anymore. How should we handle this? like this anymore. How should we handle this?
e.Mask = '\U0001F92B' e.Mask = '\U0001F92B'
e.Layout(gtx, cache, font, fontSize, nil) e.Layout(gtx, cache, font, fontSize, op.CallOp{},op.CallOp{})
e.moveEnd(selectionClear) e.moveEnd(selectionClear)
assertCaret(t, e, 0, 3, len("æbc")) 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 // 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. // zero, but this is the first column on the right.
e.SetText("الحب") 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) assertCaret(t, e, 0, 0, 0)
e.MoveCaret(+1, +1) e.MoveCaret(+1, +1)
assertCaret(t, e, 0, 1, len("ا")) assertCaret(t, e, 0, 1, len("ا"))
@@ -372,7 +372,7 @@ func TestEditorRTL(t *testing.T) {
sentence := "الحب سماء لا\nتمط غير الأحلام" sentence := "الحب سماء لا\nتمط غير الأحلام"
e.SetText(sentence) 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) assertCaret(t, e, 0, 0, 0)
e.text.MoveEnd(selectionClear) e.text.MoveEnd(selectionClear)
assertCaret(t, e, 0, 12, len("الحب سماء لا")) assertCaret(t, e, 0, 12, len("الحب سماء لا"))
@@ -440,7 +440,7 @@ func TestEditorLigature(t *testing.T) {
e.SetCaret(0, 0) // shouldn't panic e.SetCaret(0, 0) // shouldn't panic
assertCaret(t, e, 0, 0, 0) assertCaret(t, e, 0, 0, 0)
e.SetText("fl") // just a ligature 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) e.text.MoveEnd(selectionClear)
assertCaret(t, e, 0, 2, len("fl")) assertCaret(t, e, 0, 2, len("fl"))
e.MoveCaret(-1, -1) e.MoveCaret(-1, -1)
@@ -450,7 +450,7 @@ func TestEditorLigature(t *testing.T) {
e.MoveCaret(+2, +2) e.MoveCaret(+2, +2)
assertCaret(t, e, 0, 2, len("fl")) assertCaret(t, e, 0, 2, len("fl"))
e.SetText("flaffl•ffi\n•fflfi") // 3 ligatures on line 0, 2 on line 1 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) assertCaret(t, e, 0, 0, 0)
e.text.MoveEnd(selectionClear) e.text.MoveEnd(selectionClear)
assertCaret(t, e, 0, 10, len("ffaffl•ffi")) assertCaret(t, e, 0, 10, len("ffaffl•ffi"))
@@ -502,7 +502,7 @@ func TestEditorLigature(t *testing.T) {
assertCaret(t, e, 0, 0, 0) assertCaret(t, e, 0, 0, 0)
gtx.Constraints = layout.Exact(image.Pt(50, 50)) gtx.Constraints = layout.Exact(image.Pt(50, 50))
e.SetText("fflffl fflffl fflffl fflffl") // Many ligatures broken across lines. 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 // 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. // decoded when moving to the end of the line. This is a regression test.
e.text.MoveEnd(selectionClear) e.text.MoveEnd(selectionClear)
@@ -517,7 +517,7 @@ func TestEditorLigature(t *testing.T) {
// Absurdly narrow constraints to force each ligature onto its own line. // Absurdly narrow constraints to force each ligature onto its own line.
gtx.Constraints = layout.Exact(image.Pt(10, 10)) gtx.Constraints = layout.Exact(image.Pt(10, 10))
e.SetText("ffl ffl") // Two ligatures on separate lines. 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) assertCaret(t, e, 0, 0, 0)
e.MoveCaret(1, 1) // Move the caret into the first ligature. e.MoveCaret(1, 1) // Move the caret into the first ligature.
assertCaret(t, e, 0, 1, len("f")) assertCaret(t, e, 0, 1, len("f"))
@@ -541,7 +541,7 @@ func TestEditorDimensions(t *testing.T) {
cache := text.NewShaper(gofont.Collection()) cache := text.NewShaper(gofont.Collection())
fontSize := unit.Sp(10) fontSize := unit.Sp(10)
font := text.Font{} 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 { if dims.Size.X == 0 {
t.Errorf("EditEvent was not reflected in Editor width") 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} { for _, a := range []text.Alignment{text.Start, text.Middle, text.End} {
e := &Editor{} e := &Editor{}
e.Alignment = a e.Alignment = a
e.Layout(gtx, cache, font, fontSize, nil) e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
consistent := func() error { consistent := func() error {
t.Helper() t.Helper()
@@ -615,7 +615,7 @@ func TestEditorCaretConsistency(t *testing.T) {
switch mutation { switch mutation {
case setText: case setText:
e.SetText(str) e.SetText(str)
e.Layout(gtx, cache, font, fontSize, nil) e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
case moveRune: case moveRune:
e.MoveCaret(int(distance), int(distance)) e.MoveCaret(int(distance), int(distance))
case moveLine: case moveLine:
@@ -681,7 +681,7 @@ func TestEditorMoveWord(t *testing.T) {
fontSize := unit.Sp(10) fontSize := unit.Sp(10)
font := text.Font{} font := text.Font{}
e.SetText(t) e.SetText(t)
e.Layout(gtx, cache, font, fontSize, nil) e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
return e return e
} }
for ii, tt := range tests { for ii, tt := range tests {
@@ -786,7 +786,7 @@ func TestEditorInsert(t *testing.T) {
fontSize := unit.Sp(10) fontSize := unit.Sp(10)
font := text.Font{} font := text.Font{}
e.SetText(t) e.SetText(t)
e.Layout(gtx, cache, font, fontSize, nil) e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
return e return e
} }
for ii, tt := range tests { for ii, tt := range tests {
@@ -876,7 +876,7 @@ func TestEditorDeleteWord(t *testing.T) {
fontSize := unit.Sp(10) fontSize := unit.Sp(10)
font := text.Font{} font := text.Font{}
e.SetText(t) e.SetText(t)
e.Layout(gtx, cache, font, fontSize, nil) e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
return e return e
} }
for ii, tt := range tests { for ii, tt := range tests {
@@ -934,7 +934,7 @@ g 2 4 6 8 g
selected := func(start, end int) string { selected := func(start, end int) string {
// Layout once with no events; populate e.lines. // Layout once with no events; populate e.lines.
gtx.Queue = nil 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 _ = e.Events() // throw away any events from this layout
// Build the selection events // Build the selection events
@@ -960,7 +960,7 @@ g 2 4 6 8 g
tim += time.Second // Avoid multi-clicks. tim += time.Second // Avoid multi-clicks.
gtx.Queue = tq 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() { for _, evt := range e.Events() {
switch evt.(type) { switch evt.(type) {
case SelectEvent: case SelectEvent:
@@ -1006,7 +1006,7 @@ g 2 4 6 8 g
gtx.Constraints = layout.Exact(image.Pt(36, 36)) gtx.Constraints = layout.Exact(image.Pt(36, 36))
// Keep existing selection // Keep existing selection
gtx.Queue = nil 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) caretStart := e.text.closestToRune(e.text.caret.start)
caretEnd := e.text.closestToRune(e.text.caret.end) 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. // Layout once to populate e.lines and get focus.
gtx.Queue = newQueue(key.FocusEvent{Focus: true}) 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) { testKey := func(keyName string) {
// Select 345 // Select 345
@@ -1041,7 +1041,7 @@ func TestSelectMove(t *testing.T) {
// Press the key // Press the key
gtx.Queue = newQueue(key.Event{State: key.Press, Name: keyName}) 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 { if expected, got := "", e.SelectedText(); expected != got {
t.Errorf("KeyName %s, expected %q, got %q", keyName, 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()) cache := text.NewShaper(gofont.Collection())
fontSize := unit.Sp(10) fontSize := unit.Sp(10)
font := text.Font{} 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 { if got, want := e.Text(), "12345678"; got != want {
t.Errorf("editor failed to cap EditEvent") t.Errorf("editor failed to cap EditEvent")
@@ -1146,7 +1146,7 @@ func TestEditor_Filter(t *testing.T) {
cache := text.NewShaper(gofont.Collection()) cache := text.NewShaper(gofont.Collection())
fontSize := unit.Sp(10) fontSize := unit.Sp(10)
font := text.Font{} 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 { if got, want := e.Text(), "12345678"; got != want {
t.Errorf("editor failed to filter EditEvent") t.Errorf("editor failed to filter EditEvent")
@@ -1170,7 +1170,7 @@ func TestEditor_Submit(t *testing.T) {
cache := text.NewShaper(gofont.Collection()) cache := text.NewShaper(gofont.Collection())
fontSize := unit.Sp(10) fontSize := unit.Sp(10)
font := text.Font{} 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 { if got, want := e.Text(), "ab1"; got != want {
t.Errorf("editor failed to filter newline") t.Errorf("editor failed to filter newline")
+24 -12
View File
@@ -27,22 +27,22 @@ type Label struct {
Selectable *Selectable Selectable *Selectable
} }
// Layout the label with the given shaper, font, size, and text. Content is a function that will be invoked // Layout the label with the given shaper, font, size, text, and materials. If the Selectable field is
// with the label's clip area applied, and should be used to set colors and paint the text/selection. // populated, the label will support text selection. Otherwise, it will be non-interactive. The textMaterial
// content will only be invoked for labels with a non-nil Selectable. For stateless labels, the paint color // and selectionMaterial op.CallOps are responsible for setting the painting material for the text glyphs
// should be set prior to calling Layout. // and the text selection rectangles, respectively.
func (l Label) LayoutSelectable(gtx layout.Context, lt *text.Shaper, font text.Font, size unit.Sp, txt string, content layout.Widget) layout.Dimensions { 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 { 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.Alignment = l.Alignment
l.Selectable.text.MaxLines = l.MaxLines l.Selectable.text.MaxLines = l.MaxLines
l.Selectable.SetText(txt) 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. // 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 { 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 cs := gtx.Constraints
textSize := fixed.I(gtx.Sp(size)) textSize := fixed.I(gtx.Sp(size))
lt.LayoutString(text.Parameters{ 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) }, cs.Min.X, cs.Max.X, gtx.Locale, txt)
m := op.Record(gtx.Ops) m := op.Record(gtx.Ops)
viewport := image.Rectangle{Max: cs.Max} 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) semantic.LabelOp(txt).Add(gtx.Ops)
var glyphs [32]text.Glyph var glyphs [32]text.Glyph
line := glyphs[:0] line := glyphs[:0]
@@ -86,6 +90,9 @@ type textIterator struct {
viewport image.Rectangle viewport image.Rectangle
// maxLines is the maximum number of text lines that should be displayed. // maxLines is the maximum number of text lines that should be displayed.
maxLines int 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 tracks the quantity of line endings this iterator has seen.
linesSeen int 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 { if glyph.Flags&text.FlagLineBreak != 0 || cap(line)-len(line) == 0 || !visibleOrBefore {
t := op.Offset(it.lineOff).Push(gtx.Ops) 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) paint.PaintOp{}.Add(gtx.Ops)
op.Pop() outline.Pop()
if call := shaper.Bitmaps(line); call != (op.CallOp{}) {
call.Add(gtx.Ops)
}
t.Pop() t.Pop()
line = line[:0] line = line[:0]
} }
+2 -1
View File
@@ -117,8 +117,9 @@ func (b ButtonStyle) Layout(gtx layout.Context) layout.Dimensions {
Button: b.Button, Button: b.Button,
}.Layout(gtx, func(gtx layout.Context) layout.Dimensions { }.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return b.Inset.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) 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{})
}) })
}) })
} }
+3 -1
View File
@@ -8,6 +8,7 @@ import (
"gioui.org/internal/f32color" "gioui.org/internal/f32color"
"gioui.org/layout" "gioui.org/layout"
"gioui.org/op"
"gioui.org/op/clip" "gioui.org/op/clip"
"gioui.org/op/paint" "gioui.org/op/paint"
"gioui.org/text" "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 { layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return layout.UniformInset(2).Layout(gtx, 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) 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{})
}) })
}), }),
) )
+18 -20
View File
@@ -6,7 +6,6 @@ import (
"image/color" "image/color"
"gioui.org/internal/f32color" "gioui.org/internal/f32color"
"gioui.org/io/semantic"
"gioui.org/layout" "gioui.org/layout"
"gioui.org/op" "gioui.org/op"
"gioui.org/op/paint" "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 { 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) 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 var maxlines int
if e.Editor.SingleLine { if e.Editor.SingleLine {
maxlines = 1 maxlines = 1
} }
macro := op.Record(gtx.Ops)
tl := widget.Label{Alignment: e.Editor.Alignment, MaxLines: maxlines} 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() call := macro.Stop()
if w := dims.Size.X; gtx.Constraints.Min.X < w { if w := dims.Size.X; gtx.Constraints.Min.X < w {
gtx.Constraints.Min.X = w gtx.Constraints.Min.X = w
} }
if h := dims.Size.Y; gtx.Constraints.Min.Y < h { if h := dims.Size.Y; gtx.Constraints.Min.Y < h {
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 { dims = e.Editor.Layout(gtx, e.shaper, e.Font, e.TextSize, textColor, selectionColor)
semantic.Editor.Add(gtx.Ops) if e.Editor.Len() == 0 {
disabled := gtx.Queue == nil call.Add(gtx.Ops)
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
})
return dims return dims
} }
+8 -10
View File
@@ -7,6 +7,7 @@ import (
"gioui.org/internal/f32color" "gioui.org/internal/f32color"
"gioui.org/layout" "gioui.org/layout"
"gioui.org/op"
"gioui.org/op/paint" "gioui.org/op/paint"
"gioui.org/text" "gioui.org/text"
"gioui.org/unit" "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 { func (l LabelStyle) Layout(gtx layout.Context) layout.Dimensions {
textColorMacro := op.Record(gtx.Ops)
paint.ColorOp{Color: l.Color}.Add(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} 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, textColor, selectColor)
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{}
})
} }
+13 -12
View File
@@ -12,6 +12,7 @@ import (
"gioui.org/io/pointer" "gioui.org/io/pointer"
"gioui.org/io/system" "gioui.org/io/system"
"gioui.org/layout" "gioui.org/layout"
"gioui.org/op"
"gioui.org/op/clip" "gioui.org/op/clip"
"gioui.org/text" "gioui.org/text"
"gioui.org/unit" "gioui.org/unit"
@@ -91,18 +92,19 @@ func (l *Selectable) Focused() bool {
return l.focused return l.focused
} }
// PaintSelection paints the contrasting background for selected text. // paintSelection paints the contrasting background for selected text.
func (l *Selectable) PaintSelection(gtx layout.Context) { func (l *Selectable) paintSelection(gtx layout.Context, material op.CallOp) {
l.initialize() l.initialize()
if !l.focused { if !l.focused {
return 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.initialize()
l.text.PaintText(gtx) l.text.PaintText(gtx, material)
} }
// SelectionLen returns the length of the selection, in runes; it is // 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 // Layout clips to the dimensions of the selectable, updates the shaped text, configures input handling, and paints
// content. content is expected to set colors and invoke the Paint methods. content may be nil, in which case nothing // the text and selection rectangles. The provided textMaterial and selectionMaterial ops are used to set the
// will be displayed. // 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, content layout.Widget) layout.Dimensions { 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.initialize()
l.text.Update(gtx, lt, font, size, l.handleEvents) l.text.Update(gtx, lt, font, size, l.handleEvents)
dims := l.text.Dimensions() 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.clicker.Add(gtx.Ops)
l.dragger.Add(gtx.Ops) l.dragger.Add(gtx.Ops)
if content != nil { l.paintSelection(gtx, selectionMaterial)
content(gtx) l.paintText(gtx, textMaterial)
}
return dims return dims
} }
+4 -6
View File
@@ -46,10 +46,9 @@ func TestSelectableMove(t *testing.T) {
gtx.Queue = newQueue(key.FocusEvent{Focus: true}) gtx.Queue = newQueue(key.FocusEvent{Focus: true})
s := new(Selectable) s := new(Selectable)
w := func(layout.Context) layout.Dimensions { return layout.Dimensions{} }
Label{ Label{
Selectable: s, 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) { testKey := func(keyName string) {
// Select 345 // Select 345
@@ -65,7 +64,7 @@ func TestSelectableMove(t *testing.T) {
gtx.Queue = newQueue(key.Event{State: key.Press, Name: keyName}) gtx.Queue = newQueue(key.Event{State: key.Press, Name: keyName})
Label{ Label{
Selectable: s, 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 { if expected, got := "", s.SelectedText(); expected != got {
t.Errorf("KeyName %s, expected %q, got %q", keyName, 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) fontSize := unit.Sp(10)
font := text.Font{} font := text.Font{}
sentence := "\n\n\n\n\n\n\n\n\n\n\n\nthe quick brown fox jumps over the lazy dog" 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 _, alignment := range []text.Alignment{text.Start, text.Middle, text.End} {
for _, zeroMin := range []bool{true, false} { for _, zeroMin := range []bool{true, false} {
@@ -108,8 +106,8 @@ func TestSelectableConfigurations(t *testing.T) {
Alignment: alignment, Alignment: alignment,
Selectable: s, Selectable: s,
} }
interactiveDims := label.LayoutSelectable(gtx, cache, font, fontSize, sentence, w) interactiveDims := label.Layout(gtx, cache, font, fontSize, sentence, op.CallOp{}, op.CallOp{})
staticDims := label.Layout(gtx, cache, font, fontSize, sentence) staticDims := label.Layout(gtx, cache, font, fontSize, sentence, op.CallOp{}, op.CallOp{})
if interactiveDims != staticDims { if interactiveDims != staticDims {
t.Errorf("expected consistent dimensions, static returned %#+v, interactive returned %#+v", staticDims, interactiveDims) t.Errorf("expected consistent dimensions, static returned %#+v, interactive returned %#+v", staticDims, interactiveDims)
+15 -13
View File
@@ -234,31 +234,33 @@ func (e *textView) Update(gtx layout.Context, lt *text.Shaper, font text.Font, s
e.makeValid() e.makeValid()
} }
// PaintSelection clips and paints the visible text selection rectangles. Callers // PaintSelection clips and paints the visible text selection rectangles using
// are expected to apply an appropriate paint material with a paint.ColorOp or // the provided material to fill the rectangles.
// similar prior to calling PaintSelection. func (e *textView) PaintSelection(gtx layout.Context, material op.CallOp) {
func (e *textView) PaintSelection(gtx layout.Context) {
localViewport := image.Rectangle{Max: e.viewSize} localViewport := image.Rectangle{Max: e.viewSize}
docViewport := image.Rectangle{Max: e.viewSize}.Add(e.scrollOff) docViewport := image.Rectangle{Max: e.viewSize}.Add(e.scrollOff)
defer clip.Rect(localViewport).Push(gtx.Ops).Pop() defer clip.Rect(localViewport).Push(gtx.Ops).Pop()
e.regions = e.index.locate(docViewport, e.caret.start, e.caret.end, e.regions) e.regions = e.index.locate(docViewport, e.caret.start, e.caret.end, e.regions)
for _, region := range e.regions { for _, region := range e.regions {
area := clip.Rect(region.Bounds).Push(gtx.Ops) area := clip.Rect(region.Bounds).Push(gtx.Ops)
material.Add(gtx.Ops)
paint.PaintOp{}.Add(gtx.Ops) paint.PaintOp{}.Add(gtx.Ops)
area.Pop() area.Pop()
} }
} }
// PaintText clips and paints the visible text glyph outlines. Callers // PaintText clips and paints the visible text glyph outlines using the provided
// are expected to apply an appropriate paint material with a paint.ColorOp or // material to fill the glyphs.
// similar prior to calling PaintSelection. func (e *textView) PaintText(gtx layout.Context, material op.CallOp) {
func (e *textView) PaintText(gtx layout.Context) {
m := op.Record(gtx.Ops) m := op.Record(gtx.Ops)
viewport := image.Rectangle{ viewport := image.Rectangle{
Min: e.scrollOff, Min: e.scrollOff,
Max: e.viewSize.Add(e.scrollOff), Max: e.viewSize.Add(e.scrollOff),
} }
it := textIterator{viewport: viewport} it := textIterator{
viewport: viewport,
material: material,
}
startGlyph := 0 startGlyph := 0
for _, line := range e.index.lines { for _, line := range e.index.lines {
@@ -293,10 +295,9 @@ func (e *textView) caretWidth(gtx layout.Context) int {
return carWidth2 return carWidth2
} }
// PaintCaret clips and paints the caret rectangle. Callers // PaintCaret clips and paints the caret rectangle, adding material immediately
// are expected to apply an appropriate paint material with a paint.ColorOp or // before painting to set the appropriate paint material.
// similar prior to calling PaintSelection. func (e *textView) PaintCaret(gtx layout.Context, material op.CallOp) {
func (e *textView) PaintCaret(gtx layout.Context) {
carWidth2 := e.caretWidth(gtx) carWidth2 := e.caretWidth(gtx)
caretPos, carAsc, carDesc := e.CaretInfo() caretPos, carAsc, carDesc := e.CaretInfo()
@@ -308,6 +309,7 @@ func (e *textView) PaintCaret(gtx layout.Context) {
carRect = cl.Intersect(carRect) carRect = cl.Intersect(carRect)
if !carRect.Empty() { if !carRect.Empty() {
defer clip.Rect(carRect).Push(gtx.Ops).Pop() defer clip.Rect(carRect).Push(gtx.Ops).Pop()
material.Add(gtx.Ops)
paint.PaintOp{}.Add(gtx.Ops) paint.PaintOp{}.Add(gtx.Ops)
} }
} }
+6 -26
View File
@@ -83,7 +83,7 @@ func BenchmarkLabelStatic(b *testing.B) {
l := Label{} l := Label{}
b.ResetTimer() b.ResetTimer()
for i := 0; i < b.N; i++ { 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 { if render {
win.Frame(gtx.Ops) win.Frame(gtx.Ops)
} }
@@ -118,7 +118,7 @@ func BenchmarkLabelDynamic(b *testing.B) {
a := rand.Intn(len(runes)) a := rand.Intn(len(runes))
b := rand.Intn(len(runes)) b := rand.Intn(len(runes))
runes[a], runes[b] = runes[b], runes[a] 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 { if render {
win.Frame(gtx.Ops) win.Frame(gtx.Ops)
} }
@@ -151,12 +151,7 @@ func BenchmarkEditorStatic(b *testing.B) {
e.SetText(runesStr) e.SetText(runesStr)
b.ResetTimer() b.ResetTimer()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
e.Layout(gtx, cache, font, fontSize, func(gtx layout.Context) layout.Dimensions { e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
e.PaintSelection(gtx)
e.PaintText(gtx)
e.PaintCaret(gtx)
return layout.Dimensions{Size: gtx.Constraints.Min}
})
if render { if render {
win.Frame(gtx.Ops) win.Frame(gtx.Ops)
} }
@@ -196,12 +191,7 @@ func BenchmarkEditorDynamic(b *testing.B) {
e.Insert("") e.Insert("")
e.SetCaret(b, b) e.SetCaret(b, b)
e.Insert(takeStr) e.Insert(takeStr)
e.Layout(gtx, cache, font, fontSize, func(gtx layout.Context) layout.Dimensions { e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
e.PaintSelection(gtx)
e.PaintText(gtx)
e.PaintCaret(gtx)
return layout.Dimensions{Size: gtx.Constraints.Min}
})
if render { if render {
win.Frame(gtx.Ops) win.Frame(gtx.Ops)
} }
@@ -225,12 +215,7 @@ func FuzzEditorEditing(f *testing.F) {
e := Editor{} e := Editor{}
f.Fuzz(func(t *testing.T, txt string, replaceFrom, replaceTo int16) { f.Fuzz(func(t *testing.T, txt string, replaceFrom, replaceTo int16) {
e.SetText(txt) e.SetText(txt)
e.Layout(gtx, cache, font, fontSize, func(gtx layout.Context) layout.Dimensions { e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
e.PaintSelection(gtx)
e.PaintText(gtx)
e.PaintCaret(gtx)
return layout.Dimensions{Size: gtx.Constraints.Min}
})
// simulate a constantly changing string // simulate a constantly changing string
if e.Len() > 0 { if e.Len() > 0 {
a := int(replaceFrom) % e.Len() a := int(replaceFrom) % e.Len()
@@ -241,12 +226,7 @@ func FuzzEditorEditing(f *testing.F) {
e.SetCaret(b, b) e.SetCaret(b, b)
e.Insert(takeStr) e.Insert(takeStr)
} }
e.Layout(gtx, cache, font, fontSize, func(gtx layout.Context) layout.Dimensions { e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
e.PaintSelection(gtx)
e.PaintText(gtx)
e.PaintCaret(gtx)
return layout.Dimensions{Size: gtx.Constraints.Min}
})
gtx.Ops.Reset() gtx.Ops.Reset()
}) })
} }