github.com/gop9/olt@v0.0.0-20200202132135-d956aad50b08/gio/widget/label.go (about)

     1  // SPDX-License-Identifier: Unlicense OR MIT
     2  
     3  package widget
     4  
     5  import (
     6  	"fmt"
     7  	"image"
     8  	"unicode/utf8"
     9  
    10  	"github.com/gop9/olt/gio/f32"
    11  	"github.com/gop9/olt/gio/layout"
    12  	"github.com/gop9/olt/gio/op"
    13  	"github.com/gop9/olt/gio/op/paint"
    14  	"github.com/gop9/olt/gio/text"
    15  
    16  	"golang.org/x/image/math/fixed"
    17  )
    18  
    19  // Label is a widget for laying out and drawing text.
    20  type Label struct {
    21  	// Alignment specify the text alignment.
    22  	Alignment text.Alignment
    23  	// MaxLines limits the number of lines. Zero means no limit.
    24  	MaxLines int
    25  }
    26  
    27  type lineIterator struct {
    28  	Lines     []text.Line
    29  	Clip      image.Rectangle
    30  	Alignment text.Alignment
    31  	Width     int
    32  	Offset    image.Point
    33  
    34  	y, prevDesc fixed.Int26_6
    35  }
    36  
    37  const inf = 1e6
    38  
    39  func (l *lineIterator) Next() (text.String, f32.Point, bool) {
    40  	for len(l.Lines) > 0 {
    41  		line := l.Lines[0]
    42  		l.Lines = l.Lines[1:]
    43  		x := align(l.Alignment, line.Width, l.Width) + fixed.I(l.Offset.X)
    44  		l.y += l.prevDesc + line.Ascent
    45  		l.prevDesc = line.Descent
    46  		// Align baseline and line start to the pixel grid.
    47  		off := fixed.Point26_6{X: fixed.I(x.Floor()), Y: fixed.I(l.y.Ceil())}
    48  		l.y = off.Y
    49  		off.Y += fixed.I(l.Offset.Y)
    50  		if (off.Y + line.Bounds.Min.Y).Floor() > l.Clip.Max.Y {
    51  			break
    52  		}
    53  		if (off.Y + line.Bounds.Max.Y).Ceil() < l.Clip.Min.Y {
    54  			continue
    55  		}
    56  		str := line.Text
    57  		for len(str.Advances) > 0 {
    58  			adv := str.Advances[0]
    59  			if (off.X + adv + line.Bounds.Max.X - line.Width).Ceil() >= l.Clip.Min.X {
    60  				break
    61  			}
    62  			off.X += adv
    63  			_, s := utf8.DecodeRuneInString(str.String)
    64  			str.String = str.String[s:]
    65  			str.Advances = str.Advances[1:]
    66  		}
    67  		n := 0
    68  		endx := off.X
    69  		for i, adv := range str.Advances {
    70  			if (endx + line.Bounds.Min.X).Floor() > l.Clip.Max.X {
    71  				str.String = str.String[:n]
    72  				str.Advances = str.Advances[:i]
    73  				break
    74  			}
    75  			_, s := utf8.DecodeRuneInString(str.String[n:])
    76  			n += s
    77  			endx += adv
    78  		}
    79  		offf := f32.Point{X: float32(off.X) / 64, Y: float32(off.Y) / 64}
    80  		return str, offf, true
    81  	}
    82  	return text.String{}, f32.Point{}, false
    83  }
    84  
    85  func (l Label) Layout(gtx *layout.Context, s *text.Shaper, font text.Font, txt string) {
    86  	cs := gtx.Constraints
    87  	textLayout := s.Layout(gtx, font, txt, text.LayoutOptions{MaxWidth: cs.Width.Max})
    88  	lines := textLayout.Lines
    89  	if max := l.MaxLines; max > 0 && len(lines) > max {
    90  		lines = lines[:max]
    91  	}
    92  	dims := linesDimens(lines)
    93  	dims.Size = cs.Constrain(dims.Size)
    94  	clip := textPadding(lines)
    95  	clip.Max = clip.Max.Add(dims.Size)
    96  	it := lineIterator{
    97  		Lines:     lines,
    98  		Clip:      clip,
    99  		Alignment: l.Alignment,
   100  		Width:     dims.Size.X,
   101  	}
   102  	for {
   103  		str, off, ok := it.Next()
   104  		if !ok {
   105  			break
   106  		}
   107  		lclip := toRectF(clip).Sub(off)
   108  		var stack op.StackOp
   109  		stack.Push(gtx.Ops)
   110  		op.TransformOp{}.Offset(off).Add(gtx.Ops)
   111  		s.Shape(gtx, font, str).Add(gtx.Ops)
   112  		paint.PaintOp{Rect: lclip}.Add(gtx.Ops)
   113  		stack.Pop()
   114  	}
   115  	gtx.Dimensions = dims
   116  }
   117  
   118  func toRectF(r image.Rectangle) f32.Rectangle {
   119  	return f32.Rectangle{
   120  		Min: f32.Point{X: float32(r.Min.X), Y: float32(r.Min.Y)},
   121  		Max: f32.Point{X: float32(r.Max.X), Y: float32(r.Max.Y)},
   122  	}
   123  }
   124  
   125  func textPadding(lines []text.Line) (padding image.Rectangle) {
   126  	if len(lines) == 0 {
   127  		return
   128  	}
   129  	first := lines[0]
   130  	if d := first.Ascent + first.Bounds.Min.Y; d < 0 {
   131  		padding.Min.Y = d.Ceil()
   132  	}
   133  	last := lines[len(lines)-1]
   134  	if d := last.Bounds.Max.Y - last.Descent; d > 0 {
   135  		padding.Max.Y = d.Ceil()
   136  	}
   137  	if d := first.Bounds.Min.X; d < 0 {
   138  		padding.Min.X = d.Ceil()
   139  	}
   140  	if d := first.Bounds.Max.X - first.Width; d > 0 {
   141  		padding.Max.X = d.Ceil()
   142  	}
   143  	return
   144  }
   145  
   146  func linesDimens(lines []text.Line) layout.Dimensions {
   147  	var width fixed.Int26_6
   148  	var h int
   149  	var baseline int
   150  	if len(lines) > 0 {
   151  		baseline = lines[0].Ascent.Ceil()
   152  		var prevDesc fixed.Int26_6
   153  		for _, l := range lines {
   154  			h += (prevDesc + l.Ascent).Ceil()
   155  			prevDesc = l.Descent
   156  			if l.Width > width {
   157  				width = l.Width
   158  			}
   159  		}
   160  		h += lines[len(lines)-1].Descent.Ceil()
   161  	}
   162  	w := width.Ceil()
   163  	return layout.Dimensions{
   164  		Size: image.Point{
   165  			X: w,
   166  			Y: h,
   167  		},
   168  		Baseline: h - baseline,
   169  	}
   170  }
   171  
   172  func align(align text.Alignment, width fixed.Int26_6, maxWidth int) fixed.Int26_6 {
   173  	mw := fixed.I(maxWidth)
   174  	switch align {
   175  	case text.Middle:
   176  		return fixed.I(((mw - width) / 2).Floor())
   177  	case text.End:
   178  		return fixed.I((mw - width).Floor())
   179  	case text.Start:
   180  		return 0
   181  	default:
   182  		panic(fmt.Errorf("unknown alignment %v", align))
   183  	}
   184  }