github.com/utopiagio/gio@v0.0.8/widget/label.go (about) 1 // SPDX-License-Identifier: Unlicense OR MIT 2 3 package widget 4 5 import ( 6 "image" 7 8 "github.com/utopiagio/gio/f32" 9 "github.com/utopiagio/gio/font" 10 "github.com/utopiagio/gio/io/semantic" 11 "github.com/utopiagio/gio/layout" 12 "github.com/utopiagio/gio/op" 13 "github.com/utopiagio/gio/op/clip" 14 "github.com/utopiagio/gio/op/paint" 15 "github.com/utopiagio/gio/text" 16 "github.com/utopiagio/gio/unit" 17 18 "golang.org/x/image/math/fixed" 19 ) 20 21 // Label is a widget for laying out and drawing text. Labels are always 22 // non-interactive text. They cannot be selected or copied. 23 type Label struct { 24 // Alignment specifies the text alignment. 25 Alignment text.Alignment 26 // MaxLines limits the number of lines. Zero means no limit. 27 MaxLines int 28 // Truncator is the text that will be shown at the end of the final 29 // line if MaxLines is exceeded. Defaults to "…" if empty. 30 Truncator string 31 // WrapPolicy configures how displayed text will be broken into lines. 32 WrapPolicy text.WrapPolicy 33 // LineHeight controls the distance between the baselines of lines of text. 34 // If zero, a sensible default will be used. 35 LineHeight unit.Sp 36 // LineHeightScale applies a scaling factor to the LineHeight. If zero, a 37 // sensible default will be used. 38 LineHeightScale float32 39 } 40 41 // Layout the label with the given shaper, font, size, text, and material. 42 func (l Label) Layout(gtx layout.Context, lt *text.Shaper, font font.Font, size unit.Sp, txt string, textMaterial op.CallOp) layout.Dimensions { 43 dims, _ := l.LayoutDetailed(gtx, lt, font, size, txt, textMaterial) 44 return dims 45 } 46 47 // TextInfo provides metadata about shaped text. 48 type TextInfo struct { 49 // Truncated contains the number of runes of text that are represented by a truncator 50 // symbol in the text. If zero, there is no truncator symbol. 51 Truncated int 52 } 53 54 // Layout the label with the given shaper, font, size, text, and material, returning metadata about the shaped text. 55 func (l Label) LayoutDetailed(gtx layout.Context, lt *text.Shaper, font font.Font, size unit.Sp, txt string, textMaterial op.CallOp) (layout.Dimensions, TextInfo) { 56 cs := gtx.Constraints 57 textSize := fixed.I(gtx.Sp(size)) 58 lineHeight := fixed.I(gtx.Sp(l.LineHeight)) 59 lt.LayoutString(text.Parameters{ 60 Font: font, 61 PxPerEm: textSize, 62 MaxLines: l.MaxLines, 63 Truncator: l.Truncator, 64 Alignment: l.Alignment, 65 WrapPolicy: l.WrapPolicy, 66 MaxWidth: cs.Max.X, 67 MinWidth: cs.Min.X, 68 Locale: gtx.Locale, 69 LineHeight: lineHeight, 70 LineHeightScale: l.LineHeightScale, 71 }, txt) 72 m := op.Record(gtx.Ops) 73 viewport := image.Rectangle{Max: cs.Max} 74 it := textIterator{ 75 viewport: viewport, 76 maxLines: l.MaxLines, 77 material: textMaterial, 78 } 79 semantic.LabelOp(txt).Add(gtx.Ops) 80 var glyphs [32]text.Glyph 81 line := glyphs[:0] 82 for g, ok := lt.NextGlyph(); ok; g, ok = lt.NextGlyph() { 83 var ok bool 84 if line, ok = it.paintGlyph(gtx, lt, g, line); !ok { 85 break 86 } 87 } 88 call := m.Stop() 89 viewport.Min = viewport.Min.Add(it.padding.Min) 90 viewport.Max = viewport.Max.Add(it.padding.Max) 91 clipStack := clip.Rect(viewport).Push(gtx.Ops) 92 call.Add(gtx.Ops) 93 dims := layout.Dimensions{Size: it.bounds.Size()} 94 dims.Size = cs.Constrain(dims.Size) 95 dims.Baseline = dims.Size.Y - it.baseline 96 clipStack.Pop() 97 return dims, TextInfo{Truncated: it.truncated} 98 } 99 100 // textIterator computes the bounding box of and paints text. 101 type textIterator struct { 102 // viewport is the rectangle of document coordinates that the iterator is 103 // trying to fill with text. 104 viewport image.Rectangle 105 // maxLines is the maximum number of text lines that should be displayed. 106 maxLines int 107 // material sets the paint material for the text glyphs. If none is provided 108 // the color of the glyphs is undefined and may change unpredictably if the 109 // text contains color glyphs. 110 material op.CallOp 111 // truncated tracks the count of truncated runes in the text. 112 truncated int 113 // linesSeen tracks the quantity of line endings this iterator has seen. 114 linesSeen int 115 // lineOff tracks the origin for the glyphs in the current line. 116 lineOff f32.Point 117 // padding is the space needed outside of the bounds of the text to ensure no 118 // part of a glyph is clipped. 119 padding image.Rectangle 120 // bounds is the logical bounding box of the text. 121 bounds image.Rectangle 122 // visible tracks whether the most recently iterated glyph is visible within 123 // the viewport. 124 visible bool 125 // first tracks whether the iterator has processed a glyph yet. 126 first bool 127 // baseline tracks the location of the first line of text's baseline. 128 baseline int 129 } 130 131 // processGlyph checks whether the glyph is visible within the iterator's configured 132 // viewport and (if so) updates the iterator's text dimensions to include the glyph. 133 func (it *textIterator) processGlyph(g text.Glyph, ok bool) (visibleOrBefore bool) { 134 if it.maxLines > 0 { 135 if g.Flags&text.FlagTruncator != 0 && g.Flags&text.FlagClusterBreak != 0 { 136 // A glyph carrying both of these flags provides the count of truncated runes. 137 it.truncated = int(g.Runes) 138 } 139 if g.Flags&text.FlagLineBreak != 0 { 140 it.linesSeen++ 141 } 142 if it.linesSeen == it.maxLines && g.Flags&text.FlagParagraphBreak != 0 { 143 return false 144 } 145 } 146 // Compute the maximum extent to which glyphs overhang on the horizontal 147 // axis. 148 if d := g.Bounds.Min.X.Floor(); d < it.padding.Min.X { 149 // If the distance between the dot and the left edge of this glyph is 150 // less than the current padding, increase the left padding. 151 it.padding.Min.X = d 152 } 153 if d := (g.Bounds.Max.X - g.Advance).Ceil(); d > it.padding.Max.X { 154 // If the distance between the dot and the right edge of this glyph 155 // minus the logical advance of this glyph is greater than the current 156 // padding, increase the right padding. 157 it.padding.Max.X = d 158 } 159 if d := (g.Bounds.Min.Y + g.Ascent).Floor(); d < it.padding.Min.Y { 160 // If the distance between the dot and the top of this glyph is greater 161 // than the ascent of the glyph, increase the top padding. 162 it.padding.Min.Y = d 163 } 164 if d := (g.Bounds.Max.Y - g.Descent).Ceil(); d > it.padding.Max.Y { 165 // If the distance between the dot and the bottom of this glyph is greater 166 // than the descent of the glyph, increase the bottom padding. 167 it.padding.Max.Y = d 168 } 169 logicalBounds := image.Rectangle{ 170 Min: image.Pt(g.X.Floor(), int(g.Y)-g.Ascent.Ceil()), 171 Max: image.Pt((g.X + g.Advance).Ceil(), int(g.Y)+g.Descent.Ceil()), 172 } 173 if !it.first { 174 it.first = true 175 it.baseline = int(g.Y) 176 it.bounds = logicalBounds 177 } 178 179 above := logicalBounds.Max.Y < it.viewport.Min.Y 180 below := logicalBounds.Min.Y > it.viewport.Max.Y 181 left := logicalBounds.Max.X < it.viewport.Min.X 182 right := logicalBounds.Min.X > it.viewport.Max.X 183 it.visible = !above && !below && !left && !right 184 if it.visible { 185 it.bounds.Min.X = min(it.bounds.Min.X, logicalBounds.Min.X) 186 it.bounds.Min.Y = min(it.bounds.Min.Y, logicalBounds.Min.Y) 187 it.bounds.Max.X = max(it.bounds.Max.X, logicalBounds.Max.X) 188 it.bounds.Max.Y = max(it.bounds.Max.Y, logicalBounds.Max.Y) 189 } 190 return ok && !below 191 } 192 193 func fixedToFloat(i fixed.Int26_6) float32 { 194 return float32(i) / 64.0 195 } 196 197 // paintGlyph buffers up and paints text glyphs. It should be invoked iteratively upon each glyph 198 // until it returns false. The line parameter should be a slice with 199 // a backing array of sufficient size to buffer multiple glyphs. 200 // A modified slice will be returned with each invocation, and is 201 // expected to be passed back in on the following invocation. 202 // This design is awkward, but prevents the line slice from escaping 203 // to the heap. 204 func (it *textIterator) paintGlyph(gtx layout.Context, shaper *text.Shaper, glyph text.Glyph, line []text.Glyph) ([]text.Glyph, bool) { 205 visibleOrBefore := it.processGlyph(glyph, true) 206 if it.visible { 207 if len(line) == 0 { 208 it.lineOff = f32.Point{X: fixedToFloat(glyph.X), Y: float32(glyph.Y)}.Sub(layout.FPt(it.viewport.Min)) 209 } 210 line = append(line, glyph) 211 } 212 if glyph.Flags&text.FlagLineBreak != 0 || cap(line)-len(line) == 0 || !visibleOrBefore { 213 t := op.Affine(f32.Affine2D{}.Offset(it.lineOff)).Push(gtx.Ops) 214 path := shaper.Shape(line) 215 outline := clip.Outline{Path: path}.Op().Push(gtx.Ops) 216 it.material.Add(gtx.Ops) 217 paint.PaintOp{}.Add(gtx.Ops) 218 outline.Pop() 219 if call := shaper.Bitmaps(line); call != (op.CallOp{}) { 220 call.Add(gtx.Ops) 221 } 222 t.Pop() 223 line = line[:0] 224 } 225 return line, visibleOrBefore 226 }