gioui.org@v0.6.1-0.20240506124620-7a9ce51988ce/widget/label.go (about)

     1  // SPDX-License-Identifier: Unlicense OR MIT
     2  
     3  package widget
     4  
     5  import (
     6  	"image"
     7  
     8  	"gioui.org/f32"
     9  	"gioui.org/font"
    10  	"gioui.org/io/semantic"
    11  	"gioui.org/layout"
    12  	"gioui.org/op"
    13  	"gioui.org/op/clip"
    14  	"gioui.org/op/paint"
    15  	"gioui.org/text"
    16  	"gioui.org/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  }