label.go 8.1 KB

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