Files
gio/text/label.go
T
Elias Naur f35fe407b3 text: tighten clip bounds for Editor and Label
We're about to remove the extra padding from Editor. To do that, the
clipping must account for text drawing that lie outside the viewport.

With accurate clip bounds, use it for Label as well.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2019-10-12 14:04:34 +02:00

155 lines
3.7 KiB
Go

// SPDX-License-Identifier: Unlicense OR MIT
// Package text implements text widgets.
package text
import (
"image"
"image/color"
"unicode/utf8"
"gioui.org/f32"
"gioui.org/layout"
"gioui.org/op"
"gioui.org/op/paint"
"golang.org/x/image/math/fixed"
)
// Label is a widget for laying out and drawing text.
type Label struct {
Font Font
// Material is a macro recording the material to draw the
// text. Use a ColorOp for colored text.
Material op.MacroOp
// Alignment specify the text alignment.
Alignment Alignment
// Text is the string to draw.
Text string
// MaxLines limits the number of lines. Zero means no limit.
MaxLines int
}
type lineIterator struct {
Lines []Line
Clip image.Rectangle
Alignment Alignment
Width int
Offset image.Point
y, prevDesc fixed.Int26_6
}
const inf = 1e6
func (l *lineIterator) Next() (String, 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
}
if (off.Y + line.Bounds.Max.Y).Ceil() < l.Clip.Min.Y {
continue
}
str := line.Text
for len(str.Advances) > 0 {
adv := str.Advances[0]
if (off.X + adv + line.Bounds.Max.X - line.Width).Ceil() >= l.Clip.Min.X {
break
}
off.X += adv
_, s := utf8.DecodeRuneInString(str.String)
str.String = str.String[s:]
str.Advances = str.Advances[1:]
}
n := 0
endx := off.X
for i, adv := range str.Advances {
if (endx + line.Bounds.Min.X).Floor() > l.Clip.Max.X {
str.String = str.String[:n]
str.Advances = str.Advances[:i]
break
}
_, s := utf8.DecodeRuneInString(str.String[n:])
n += s
endx += adv
}
offf := f32.Point{X: float32(off.X) / 64, Y: float32(off.Y) / 64}
return str, offf, true
}
return String{}, f32.Point{}, false
}
func (l Label) Layout(gtx *layout.Context, s *Shaper) {
cs := gtx.Constraints
textLayout := s.Layout(gtx, l.Font, l.Text, LayoutOptions{MaxWidth: cs.Width.Max})
lines := textLayout.Lines
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 {
str, off, ok := it.Next()
if !ok {
break
}
lclip := toRectF(clip).Sub(off)
var stack op.StackOp
stack.Push(gtx.Ops)
op.TransformOp{}.Offset(off).Add(gtx.Ops)
s.Shape(gtx, l.Font, str).Add(gtx.Ops)
// Set a default color in case the material is empty.
paint.ColorOp{Color: color.RGBA{A: 0xff}}.Add(gtx.Ops)
l.Material.Add(gtx.Ops)
paint.PaintOp{Rect: lclip}.Add(gtx.Ops)
stack.Pop()
}
gtx.Dimensions = dims
}
func toRectF(r image.Rectangle) f32.Rectangle {
return f32.Rectangle{
Min: f32.Point{X: float32(r.Min.X), Y: float32(r.Min.Y)},
Max: f32.Point{X: float32(r.Max.X), Y: float32(r.Max.Y)},
}
}
func textPadding(lines []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
}