Files
gio/widget/label.go
T
Elias Naur aee87baefe text: represent laid out text as strings to facilitate caching of layouts
Commit https://gioui.org/commit/b331407e81456 added text layout and shaping
based on io.Reader and changed Editor to use it. Unfortunately, as ~inkeliz
discovered, caching of shapes were also lost.

~inkeliz suggested fix,

https://lists.sr.ht/~eliasnaur/gio-patches/patches/15059

adds caching of shapes to Editor to regain lost performance.

This change repairs the cache to work on io.Reader API, in hope that the
already complicated Editor won't need additional caching.

Before this change, text layouts were represented as a slice of (rune, advance)
pairs. Unfortunately, this representation doesn't lend itself to caching of
shaping results, so change the representation of a line of text to be a pair
of text and advances:

	package text

	type Layout {
		Text string
		Advances []fixed.Int26_6
	}

The Text field can then be used in a cache key, assuming Advances is
consistent with it.

The end result is that the two shaper variants of text.Shaper is reduced to
just one, and the Len field field of text.Line is no longer needed.

The changed representation adds a bit of extra work to package opentype.
Cleaning that up is left as a future TODO.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2020-11-16 16:02:30 +01:00

182 lines
4.1 KiB
Go

// SPDX-License-Identifier: Unlicense OR MIT
package widget
import (
"fmt"
"image"
"unicode/utf8"
"gioui.org/f32"
"gioui.org/layout"
"gioui.org/op"
"gioui.org/op/paint"
"gioui.org/text"
"gioui.org/unit"
"golang.org/x/image/math/fixed"
)
// Label is a widget for laying out and drawing text.
type Label struct {
// Alignment specify the text alignment.
Alignment text.Alignment
// MaxLines limits the number of lines. Zero means no limit.
MaxLines int
}
type lineIterator struct {
Lines []text.Line
Clip image.Rectangle
Alignment text.Alignment
Width int
Offset image.Point
y, prevDesc fixed.Int26_6
txtOff int
}
const inf = 1e6
func (l *lineIterator) Next() (text.Layout, f32.Point, bool) {
for len(l.Lines) > 0 {
line := l.Lines[0]
l.Lines = l.Lines[1:]
x := align(l.Alignment, line.Width, l.Width) + fixed.I(l.Offset.X)
l.y += l.prevDesc + line.Ascent
l.prevDesc = line.Descent
// Align baseline and line start to the pixel grid.
off := fixed.Point26_6{X: fixed.I(x.Floor()), Y: fixed.I(l.y.Ceil())}
l.y = off.Y
off.Y += fixed.I(l.Offset.Y)
if (off.Y + line.Bounds.Min.Y).Floor() > l.Clip.Max.Y {
break
}
layout := line.Layout
start := l.txtOff
l.txtOff += len(line.Layout.Text)
if (off.Y + line.Bounds.Max.Y).Ceil() < l.Clip.Min.Y {
continue
}
for len(layout.Advances) > 0 {
_, n := utf8.DecodeRuneInString(layout.Text)
adv := layout.Advances[0]
if (off.X + adv + line.Bounds.Max.X - line.Width).Ceil() >= l.Clip.Min.X {
break
}
off.X += adv
layout.Text = layout.Text[n:]
layout.Advances = layout.Advances[1:]
start += n
}
end := start
endx := off.X
rune := 0
for n, r := range layout.Text {
if (endx + line.Bounds.Min.X).Floor() > l.Clip.Max.X {
layout.Advances = layout.Advances[:rune]
layout.Text = layout.Text[:n]
break
}
end += utf8.RuneLen(r)
endx += layout.Advances[rune]
rune++
}
offf := f32.Point{X: float32(off.X) / 64, Y: float32(off.Y) / 64}
return layout, offf, true
}
return text.Layout{}, f32.Point{}, false
}
func (l Label) Layout(gtx layout.Context, s text.Shaper, font text.Font, size unit.Value, txt string) layout.Dimensions {
cs := gtx.Constraints
textSize := fixed.I(gtx.Px(size))
lines := s.LayoutString(font, textSize, cs.Max.X, txt)
if max := l.MaxLines; max > 0 && len(lines) > max {
lines = lines[:max]
}
dims := linesDimens(lines)
dims.Size = cs.Constrain(dims.Size)
clip := textPadding(lines)
clip.Max = clip.Max.Add(dims.Size)
it := lineIterator{
Lines: lines,
Clip: clip,
Alignment: l.Alignment,
Width: dims.Size.X,
}
for {
l, off, ok := it.Next()
if !ok {
break
}
stack := op.Push(gtx.Ops)
op.Offset(off).Add(gtx.Ops)
s.Shape(font, textSize, l).Add(gtx.Ops)
paint.PaintOp{}.Add(gtx.Ops)
stack.Pop()
}
return dims
}
func textPadding(lines []text.Line) (padding image.Rectangle) {
if len(lines) == 0 {
return
}
first := lines[0]
if d := first.Ascent + first.Bounds.Min.Y; d < 0 {
padding.Min.Y = d.Ceil()
}
last := lines[len(lines)-1]
if d := last.Bounds.Max.Y - last.Descent; d > 0 {
padding.Max.Y = d.Ceil()
}
if d := first.Bounds.Min.X; d < 0 {
padding.Min.X = d.Ceil()
}
if d := first.Bounds.Max.X - first.Width; d > 0 {
padding.Max.X = d.Ceil()
}
return
}
func linesDimens(lines []text.Line) layout.Dimensions {
var width fixed.Int26_6
var h int
var baseline int
if len(lines) > 0 {
baseline = lines[0].Ascent.Ceil()
var prevDesc fixed.Int26_6
for _, l := range lines {
h += (prevDesc + l.Ascent).Ceil()
prevDesc = l.Descent
if l.Width > width {
width = l.Width
}
}
h += lines[len(lines)-1].Descent.Ceil()
}
w := width.Ceil()
return layout.Dimensions{
Size: image.Point{
X: w,
Y: h,
},
Baseline: h - baseline,
}
}
func align(align text.Alignment, width fixed.Int26_6, maxWidth int) fixed.Int26_6 {
mw := fixed.I(maxWidth)
switch align {
case text.Middle:
return fixed.I(((mw - width) / 2).Floor())
case text.End:
return fixed.I((mw - width).Floor())
case text.Start:
return 0
default:
panic(fmt.Errorf("unknown alignment %v", align))
}
}