github.com/gop9/olt@v0.0.0-20200202132135-d956aad50b08/gio/widget/label.go (about) 1 // SPDX-License-Identifier: Unlicense OR MIT 2 3 package widget 4 5 import ( 6 "fmt" 7 "image" 8 "unicode/utf8" 9 10 "github.com/gop9/olt/gio/f32" 11 "github.com/gop9/olt/gio/layout" 12 "github.com/gop9/olt/gio/op" 13 "github.com/gop9/olt/gio/op/paint" 14 "github.com/gop9/olt/gio/text" 15 16 "golang.org/x/image/math/fixed" 17 ) 18 19 // Label is a widget for laying out and drawing text. 20 type Label struct { 21 // Alignment specify the text alignment. 22 Alignment text.Alignment 23 // MaxLines limits the number of lines. Zero means no limit. 24 MaxLines int 25 } 26 27 type lineIterator struct { 28 Lines []text.Line 29 Clip image.Rectangle 30 Alignment text.Alignment 31 Width int 32 Offset image.Point 33 34 y, prevDesc fixed.Int26_6 35 } 36 37 const inf = 1e6 38 39 func (l *lineIterator) Next() (text.String, f32.Point, bool) { 40 for len(l.Lines) > 0 { 41 line := l.Lines[0] 42 l.Lines = l.Lines[1:] 43 x := align(l.Alignment, line.Width, l.Width) + fixed.I(l.Offset.X) 44 l.y += l.prevDesc + line.Ascent 45 l.prevDesc = line.Descent 46 // Align baseline and line start to the pixel grid. 47 off := fixed.Point26_6{X: fixed.I(x.Floor()), Y: fixed.I(l.y.Ceil())} 48 l.y = off.Y 49 off.Y += fixed.I(l.Offset.Y) 50 if (off.Y + line.Bounds.Min.Y).Floor() > l.Clip.Max.Y { 51 break 52 } 53 if (off.Y + line.Bounds.Max.Y).Ceil() < l.Clip.Min.Y { 54 continue 55 } 56 str := line.Text 57 for len(str.Advances) > 0 { 58 adv := str.Advances[0] 59 if (off.X + adv + line.Bounds.Max.X - line.Width).Ceil() >= l.Clip.Min.X { 60 break 61 } 62 off.X += adv 63 _, s := utf8.DecodeRuneInString(str.String) 64 str.String = str.String[s:] 65 str.Advances = str.Advances[1:] 66 } 67 n := 0 68 endx := off.X 69 for i, adv := range str.Advances { 70 if (endx + line.Bounds.Min.X).Floor() > l.Clip.Max.X { 71 str.String = str.String[:n] 72 str.Advances = str.Advances[:i] 73 break 74 } 75 _, s := utf8.DecodeRuneInString(str.String[n:]) 76 n += s 77 endx += adv 78 } 79 offf := f32.Point{X: float32(off.X) / 64, Y: float32(off.Y) / 64} 80 return str, offf, true 81 } 82 return text.String{}, f32.Point{}, false 83 } 84 85 func (l Label) Layout(gtx *layout.Context, s *text.Shaper, font text.Font, txt string) { 86 cs := gtx.Constraints 87 textLayout := s.Layout(gtx, font, txt, text.LayoutOptions{MaxWidth: cs.Width.Max}) 88 lines := textLayout.Lines 89 if max := l.MaxLines; max > 0 && len(lines) > max { 90 lines = lines[:max] 91 } 92 dims := linesDimens(lines) 93 dims.Size = cs.Constrain(dims.Size) 94 clip := textPadding(lines) 95 clip.Max = clip.Max.Add(dims.Size) 96 it := lineIterator{ 97 Lines: lines, 98 Clip: clip, 99 Alignment: l.Alignment, 100 Width: dims.Size.X, 101 } 102 for { 103 str, off, ok := it.Next() 104 if !ok { 105 break 106 } 107 lclip := toRectF(clip).Sub(off) 108 var stack op.StackOp 109 stack.Push(gtx.Ops) 110 op.TransformOp{}.Offset(off).Add(gtx.Ops) 111 s.Shape(gtx, font, str).Add(gtx.Ops) 112 paint.PaintOp{Rect: lclip}.Add(gtx.Ops) 113 stack.Pop() 114 } 115 gtx.Dimensions = dims 116 } 117 118 func toRectF(r image.Rectangle) f32.Rectangle { 119 return f32.Rectangle{ 120 Min: f32.Point{X: float32(r.Min.X), Y: float32(r.Min.Y)}, 121 Max: f32.Point{X: float32(r.Max.X), Y: float32(r.Max.Y)}, 122 } 123 } 124 125 func textPadding(lines []text.Line) (padding image.Rectangle) { 126 if len(lines) == 0 { 127 return 128 } 129 first := lines[0] 130 if d := first.Ascent + first.Bounds.Min.Y; d < 0 { 131 padding.Min.Y = d.Ceil() 132 } 133 last := lines[len(lines)-1] 134 if d := last.Bounds.Max.Y - last.Descent; d > 0 { 135 padding.Max.Y = d.Ceil() 136 } 137 if d := first.Bounds.Min.X; d < 0 { 138 padding.Min.X = d.Ceil() 139 } 140 if d := first.Bounds.Max.X - first.Width; d > 0 { 141 padding.Max.X = d.Ceil() 142 } 143 return 144 } 145 146 func linesDimens(lines []text.Line) layout.Dimensions { 147 var width fixed.Int26_6 148 var h int 149 var baseline int 150 if len(lines) > 0 { 151 baseline = lines[0].Ascent.Ceil() 152 var prevDesc fixed.Int26_6 153 for _, l := range lines { 154 h += (prevDesc + l.Ascent).Ceil() 155 prevDesc = l.Descent 156 if l.Width > width { 157 width = l.Width 158 } 159 } 160 h += lines[len(lines)-1].Descent.Ceil() 161 } 162 w := width.Ceil() 163 return layout.Dimensions{ 164 Size: image.Point{ 165 X: w, 166 Y: h, 167 }, 168 Baseline: h - baseline, 169 } 170 } 171 172 func align(align text.Alignment, width fixed.Int26_6, maxWidth int) fixed.Int26_6 { 173 mw := fixed.I(maxWidth) 174 switch align { 175 case text.Middle: 176 return fixed.I(((mw - width) / 2).Floor()) 177 case text.End: 178 return fixed.I((mw - width).Floor()) 179 case text.Start: 180 return 0 181 default: 182 panic(fmt.Errorf("unknown alignment %v", align)) 183 } 184 }