// 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" "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 } const inf = 1e6 func (l *lineIterator) Next() (text.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 text.String{}, f32.Point{}, false } func (l Label) Layout(gtx *layout.Context, s *text.Shaper, font text.Font, txt string) { cs := gtx.Constraints textLayout := s.Layout(gtx, font, txt, 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, font, str).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 []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)) } }