Files
gio-patched/widget/label.go
T
Elias Naur a63e0cb44a all: [API] change op.Offset to take integer coordinates
op.Offset is a convenience function most often used by layouts. Layouts
usually operate in integer coordinates, and the float32 version of op.Offset
needlessly force conversions from int to float32. This change makes op.Offset
take integer coordinates, to better match its intended use.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-05-31 10:24:09 +02:00

207 lines
5.4 KiB
Go

// SPDX-License-Identifier: Unlicense OR MIT
package widget
import (
"fmt"
"image"
"gioui.org/io/semantic"
"gioui.org/io/system"
"gioui.org/layout"
"gioui.org/op"
"gioui.org/op/clip"
"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
}
// screenPos describes a character position (in text line and column numbers,
// not pixels): Y = line number, X = rune column.
type screenPos image.Point
const inf = 1e6
func posIsAbove(lines []text.Line, pos combinedPos, y int) bool {
line := lines[pos.lineCol.Y]
return pos.y+line.Bounds.Max.Y.Ceil() < y
}
func posIsBelow(lines []text.Line, pos combinedPos, y int) bool {
line := lines[pos.lineCol.Y]
return pos.y+line.Bounds.Min.Y.Floor() > y
}
func clipLine(lines []text.Line, alignment text.Alignment, width int, clip image.Rectangle, linePos combinedPos) (start combinedPos, end combinedPos) {
// Seek to first (potentially) visible column.
lineIdx := linePos.lineCol.Y
line := lines[lineIdx]
// runeWidth is the width of the widest rune in line.
runeWidth := (line.Bounds.Max.X - line.Width).Ceil()
lineStart := fixed.I(clip.Min.X - runeWidth)
lineEnd := fixed.I(clip.Max.X + runeWidth)
flip := line.Layout.Direction.Progression() == system.TowardOrigin
if flip {
lineStart, lineEnd = lineEnd, lineStart
}
q := combinedPos{y: start.y, x: lineStart}
start, _ = seekPosition(lines, alignment, width, linePos, q, 0)
// Seek to first invisible column after start.
q = combinedPos{y: start.y, x: lineEnd}
end, _ = seekPosition(lines, alignment, width, start, q, 0)
if flip {
start, end = end, start
}
return start, end
}
func subLayout(line text.Line, start, end combinedPos) text.Layout {
if start.lineCol.X == line.Layout.Runes.Count {
return text.Layout{}
}
startCluster := clusterIndexFor(line, start.lineCol.X, start.clusterIndex)
endCluster := clusterIndexFor(line, end.lineCol.X, end.clusterIndex)
if startCluster > endCluster {
startCluster, endCluster = endCluster, startCluster
}
return line.Layout.Slice(startCluster, endCluster)
}
func firstPos(line text.Line, alignment text.Alignment, width int) combinedPos {
p := combinedPos{
x: align(alignment, line.Layout.Direction, line.Width, width),
y: line.Ascent.Ceil(),
}
if line.Layout.Direction.Progression() == system.TowardOrigin {
p.x += line.Width
}
return p
}
func (p1 screenPos) Less(p2 screenPos) bool {
return p1.Y < p2.Y || (p1.Y == p2.Y && p1.X < p2.X)
}
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, gtx.Locale, txt)
if max := l.MaxLines; max > 0 && len(lines) > max {
lines = lines[:max]
}
dims := linesDimens(lines)
dims.Size = cs.Constrain(dims.Size)
if len(lines) == 0 {
return dims
}
cl := textPadding(lines)
cl.Max = cl.Max.Add(dims.Size)
defer clip.Rect(cl).Push(gtx.Ops).Pop()
semantic.LabelOp(txt).Add(gtx.Ops)
pos := firstPos(lines[0], l.Alignment, dims.Size.X)
for !posIsBelow(lines, pos, cl.Max.Y) {
start, end := clipLine(lines, l.Alignment, dims.Size.X, cl, pos)
line := lines[start.lineCol.Y]
lt := subLayout(line, start, end)
off := image.Point{X: start.x.Floor(), Y: start.y}
t := op.Offset(off).Push(gtx.Ops)
op := clip.Outline{Path: s.Shape(font, textSize, lt)}.Op().Push(gtx.Ops)
paint.PaintOp{}.Add(gtx.Ops)
op.Pop()
t.Pop()
if pos.lineCol.Y == len(lines)-1 {
break
}
pos, _ = seekPosition(lines, l.Alignment, dims.Size.X, pos, combinedPos{lineCol: screenPos{Y: pos.lineCol.Y + 1}}, 0)
}
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,
}
}
// align returns the x offset that should be applied to text with width so that it
// appears correctly aligned within a space of size maxWidth and with the primary
// text direction dir.
func align(align text.Alignment, dir system.TextDirection, width fixed.Int26_6, maxWidth int) fixed.Int26_6 {
mw := fixed.I(maxWidth)
if dir.Progression() == system.TowardOrigin {
switch align {
case text.Start:
align = text.End
case text.End:
align = text.Start
}
}
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))
}
}