text.go raw

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