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  }