github.com/Seikaijyu/gio@v0.0.1/widget/label.go (about) 1 // SPDX-License-Identifier: Unlicense OR MIT 2 3 package widget 4 5 import ( 6 "image" 7 8 "github.com/Seikaijyu/gio/f32" 9 "github.com/Seikaijyu/gio/font" 10 "github.com/Seikaijyu/gio/io/semantic" 11 "github.com/Seikaijyu/gio/layout" 12 "github.com/Seikaijyu/gio/op" 13 "github.com/Seikaijyu/gio/op/clip" 14 "github.com/Seikaijyu/gio/op/paint" 15 "github.com/Seikaijyu/gio/text" 16 "github.com/Seikaijyu/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 func r2p(r clip.Rect) clip.Op { 101 return clip.Stroke{Path: r.Path(), Width: 1}.Op() 102 } 103 104 // textIterator computes the bounding box of and paints text. 105 type textIterator struct { 106 // viewport is the rectangle of document coordinates that the iterator is 107 // trying to fill with text. 108 viewport image.Rectangle 109 // maxLines is the maximum number of text lines that should be displayed. 110 maxLines int 111 // material sets the paint material for the text glyphs. If none is provided 112 // the color of the glyphs is undefined and may change unpredictably if the 113 // text contains color glyphs. 114 material op.CallOp 115 // truncated tracks the count of truncated runes in the text. 116 truncated int 117 // linesSeen tracks the quantity of line endings this iterator has seen. 118 linesSeen int 119 // lineOff tracks the origin for the glyphs in the current line. 120 lineOff f32.Point 121 // padding is the space needed outside of the bounds of the text to ensure no 122 // part of a glyph is clipped. 123 padding image.Rectangle 124 // bounds is the logical bounding box of the text. 125 bounds image.Rectangle 126 // visible tracks whether the most recently iterated glyph is visible within 127 // the viewport. 128 visible bool 129 // first tracks whether the iterator has processed a glyph yet. 130 first bool 131 // baseline tracks the location of the first line of text's baseline. 132 baseline int 133 } 134 135 // processGlyph checks whether the glyph is visible within the iterator's configured 136 // viewport and (if so) updates the iterator's text dimensions to include the glyph. 137 func (it *textIterator) processGlyph(g text.Glyph, ok bool) (visibleOrBefore bool) { 138 if it.maxLines > 0 { 139 if g.Flags&text.FlagTruncator != 0 && g.Flags&text.FlagClusterBreak != 0 { 140 // A glyph carrying both of these flags provides the count of truncated runes. 141 it.truncated = int(g.Runes) 142 } 143 if g.Flags&text.FlagLineBreak != 0 { 144 it.linesSeen++ 145 } 146 if it.linesSeen == it.maxLines && g.Flags&text.FlagParagraphBreak != 0 { 147 return false 148 } 149 } 150 // Compute the maximum extent to which glyphs overhang on the horizontal 151 // axis. 152 if d := g.Bounds.Min.X.Floor(); d < it.padding.Min.X { 153 // If the distance between the dot and the left edge of this glyph is 154 // less than the current padding, increase the left padding. 155 it.padding.Min.X = d 156 } 157 if d := (g.Bounds.Max.X - g.Advance).Ceil(); d > it.padding.Max.X { 158 // If the distance between the dot and the right edge of this glyph 159 // minus the logical advance of this glyph is greater than the current 160 // padding, increase the right padding. 161 it.padding.Max.X = d 162 } 163 if d := (g.Bounds.Min.Y + g.Ascent).Floor(); d < it.padding.Min.Y { 164 // If the distance between the dot and the top of this glyph is greater 165 // than the ascent of the glyph, increase the top padding. 166 it.padding.Min.Y = d 167 } 168 if d := (g.Bounds.Max.Y - g.Descent).Ceil(); d > it.padding.Max.Y { 169 // If the distance between the dot and the bottom of this glyph is greater 170 // than the descent of the glyph, increase the bottom padding. 171 it.padding.Max.Y = d 172 } 173 logicalBounds := image.Rectangle{ 174 Min: image.Pt(g.X.Floor(), int(g.Y)-g.Ascent.Ceil()), 175 Max: image.Pt((g.X + g.Advance).Ceil(), int(g.Y)+g.Descent.Ceil()), 176 } 177 if !it.first { 178 it.first = true 179 it.baseline = int(g.Y) 180 it.bounds = logicalBounds 181 } 182 183 above := logicalBounds.Max.Y < it.viewport.Min.Y 184 below := logicalBounds.Min.Y > it.viewport.Max.Y 185 left := logicalBounds.Max.X < it.viewport.Min.X 186 right := logicalBounds.Min.X > it.viewport.Max.X 187 it.visible = !above && !below && !left && !right 188 if it.visible { 189 it.bounds.Min.X = min(it.bounds.Min.X, logicalBounds.Min.X) 190 it.bounds.Min.Y = min(it.bounds.Min.Y, logicalBounds.Min.Y) 191 it.bounds.Max.X = max(it.bounds.Max.X, logicalBounds.Max.X) 192 it.bounds.Max.Y = max(it.bounds.Max.Y, logicalBounds.Max.Y) 193 } 194 return ok && !below 195 } 196 197 func fixedToFloat(i fixed.Int26_6) float32 { 198 return float32(i) / 64.0 199 } 200 201 // paintGlyph buffers up and paints text glyphs. It should be invoked iteratively upon each glyph 202 // until it returns false. The line parameter should be a slice with 203 // a backing array of sufficient size to buffer multiple glyphs. 204 // A modified slice will be returned with each invocation, and is 205 // expected to be passed back in on the following invocation. 206 // This design is awkward, but prevents the line slice from escaping 207 // to the heap. 208 func (it *textIterator) paintGlyph(gtx layout.Context, shaper *text.Shaper, glyph text.Glyph, line []text.Glyph) ([]text.Glyph, bool) { 209 visibleOrBefore := it.processGlyph(glyph, true) 210 if it.visible { 211 if len(line) == 0 { 212 it.lineOff = f32.Point{X: fixedToFloat(glyph.X), Y: float32(glyph.Y)}.Sub(layout.FPt(it.viewport.Min)) 213 } 214 line = append(line, glyph) 215 } 216 if glyph.Flags&text.FlagLineBreak != 0 || cap(line)-len(line) == 0 || !visibleOrBefore { 217 t := op.Affine(f32.Affine2D{}.Offset(it.lineOff)).Push(gtx.Ops) 218 path := shaper.Shape(line) 219 outline := clip.Outline{Path: path}.Op().Push(gtx.Ops) 220 it.material.Add(gtx.Ops) 221 paint.PaintOp{}.Add(gtx.Ops) 222 outline.Pop() 223 if call := shaper.Bitmaps(line); call != (op.CallOp{}) { 224 call.Add(gtx.Ops) 225 } 226 t.Pop() 227 line = line[:0] 228 } 229 return line, visibleOrBefore 230 }