label.go raw

   1  package gel
   2  
   3  import (
   4  	"fmt"
   5  	"image"
   6  	"image/color"
   7  	"unicode/utf8"
   8  
   9  	l "github.com/p9c/gio/layout"
  10  	"github.com/p9c/gio/op"
  11  	"github.com/p9c/gio/op/clip"
  12  	"github.com/p9c/gio/op/paint"
  13  	"github.com/p9c/gio/text"
  14  	"github.com/p9c/gio/unit"
  15  	"golang.org/x/image/math/fixed"
  16  )
  17  
  18  // Label is text drawn inside an empty box
  19  type Label struct {
  20  	*Window
  21  	// Face defines the text style.
  22  	font text.Font
  23  	// Color is the text color.
  24  	color color.NRGBA
  25  	// Alignment specify the text alignment.
  26  	alignment text.Alignment
  27  	// MaxLines limits the number of lines. Zero means no limit.
  28  	maxLines int
  29  	text     string
  30  	textSize unit.Value
  31  	shaper   text.Shaper
  32  }
  33  
  34  // screenPos describes a character position (in text line and column numbers,
  35  // not pixels): Y = line number, X = rune column.
  36  type screenPos image.Point
  37  
  38  type segmentIterator struct {
  39  	Lines     []text.Line
  40  	Clip      image.Rectangle
  41  	Alignment text.Alignment
  42  	Width     int
  43  	Offset    image.Point
  44  	startSel  screenPos
  45  	endSel    screenPos
  46  
  47  	pos    screenPos   // current position
  48  	line   text.Line   // current line
  49  	layout text.Layout // current line's Layout
  50  
  51  	// pixel positions
  52  	off         fixed.Point26_6
  53  	y, prevDesc fixed.Int26_6
  54  }
  55  
  56  func (l *segmentIterator) Next() (text.Layout, image.Point, bool, int, image.Point, bool) {
  57  	for l.pos.Y < len(l.Lines) {
  58  		if l.pos.X == 0 {
  59  			l.line = l.Lines[l.pos.Y]
  60  
  61  			// Calculate X & Y pixel coordinates of left edge of line. We need y
  62  			// for the next line, so it's in l, but we only need x here, so it's
  63  			// not.
  64  			x := align(l.Alignment, l.line.Width, l.Width) + fixed.I(l.Offset.X)
  65  			l.y += l.prevDesc + l.line.Ascent
  66  			l.prevDesc = l.line.Descent
  67  			// Align baseline and line start to the pixel grid.
  68  			l.off = fixed.Point26_6{X: fixed.I(x.Floor()), Y: fixed.I(l.y.Ceil())}
  69  			l.y = l.off.Y
  70  			l.off.Y += fixed.I(l.Offset.Y)
  71  			if (l.off.Y + l.line.Bounds.Min.Y).Floor() > l.Clip.Max.Y {
  72  				break
  73  			}
  74  
  75  			if (l.off.Y + l.line.Bounds.Max.Y).Ceil() < l.Clip.Min.Y {
  76  				// This line is outside/before the clip area; go on to the next line.
  77  				l.pos.Y++
  78  				continue
  79  			}
  80  
  81  			// Copy the line's Layout, since we slice it up later.
  82  			l.layout = l.line.Layout
  83  
  84  			// Find the left edge of the text visible in the l.Clip clipping
  85  			// area.
  86  			for len(l.layout.Advances) > 0 {
  87  				_, n := utf8.DecodeRuneInString(l.layout.Text)
  88  				adv := l.layout.Advances[0]
  89  				if (l.off.X + adv + l.line.Bounds.Max.X - l.line.Width).Ceil() >= l.Clip.Min.X {
  90  					break
  91  				}
  92  				l.off.X += adv
  93  				l.layout.Text = l.layout.Text[n:]
  94  				l.layout.Advances = l.layout.Advances[1:]
  95  				l.pos.X++
  96  			}
  97  		}
  98  
  99  		selected := l.inSelection()
 100  		endx := l.off.X
 101  		rune := 0
 102  		nextLine := true
 103  		retLayout := l.layout
 104  		for n := range l.layout.Text {
 105  			selChanged := selected != l.inSelection()
 106  			beyondClipEdge := (endx + l.line.Bounds.Min.X).Floor() > l.Clip.Max.X
 107  			if selChanged || beyondClipEdge {
 108  				retLayout.Advances = l.layout.Advances[:rune]
 109  				retLayout.Text = l.layout.Text[:n]
 110  				if selChanged {
 111  					// Save the rest of the line
 112  					l.layout.Advances = l.layout.Advances[rune:]
 113  					l.layout.Text = l.layout.Text[n:]
 114  					nextLine = false
 115  				}
 116  				break
 117  			}
 118  			endx += l.layout.Advances[rune]
 119  			rune++
 120  			l.pos.X++
 121  		}
 122  		offFloor := image.Point{X: l.off.X.Floor(), Y: l.off.Y.Floor()}
 123  
 124  		// Calculate the width & height if the returned text.
 125  		//
 126  		// If there's a better way to do this, I'm all ears.
 127  		var d fixed.Int26_6
 128  		for _, adv := range retLayout.Advances {
 129  			d += adv
 130  		}
 131  		size := image.Point{
 132  			X: d.Ceil(),
 133  			Y: (l.line.Ascent + l.line.Descent).Ceil(),
 134  		}
 135  
 136  		if nextLine {
 137  			l.pos.Y++
 138  			l.pos.X = 0
 139  		} else {
 140  			l.off.X = endx
 141  		}
 142  
 143  		return retLayout, offFloor, selected, l.prevDesc.Ceil() - size.Y, size, true
 144  	}
 145  	return text.Layout{}, image.Point{}, false, 0, image.Point{}, false
 146  }
 147  
 148  func (l *segmentIterator) inSelection() bool {
 149  	return l.startSel.LessOrEqual(l.pos) &&
 150  		l.pos.Less(l.endSel)
 151  }
 152  
 153  func (p1 screenPos) LessOrEqual(p2 screenPos) bool {
 154  	return p1.Y < p2.Y || (p1.Y == p2.Y && p1.X <= p2.X)
 155  }
 156  
 157  func (p1 screenPos) Less(p2 screenPos) bool {
 158  	return p1.Y < p2.Y || (p1.Y == p2.Y && p1.X < p2.X)
 159  }
 160  
 161  // Fn renders the label as specified
 162  func (l *Label) Fn(gtx l.Context) l.Dimensions {
 163  	cs := gtx.Constraints
 164  	textSize := fixed.I(gtx.Px(l.textSize))
 165  	lines := l.shaper.LayoutString(l.font, textSize, cs.Max.X, l.text)
 166  	if max := l.maxLines; max > 0 && len(lines) > max {
 167  		lines = lines[:max]
 168  	}
 169  	dims := linesDimens(lines)
 170  	dims.Size = cs.Constrain(dims.Size)
 171  	cl := textPadding(lines)
 172  	cl.Max = cl.Max.Add(dims.Size)
 173  	it := segmentIterator{
 174  		Lines:     lines,
 175  		Clip:      cl,
 176  		Alignment: l.alignment,
 177  		Width:     dims.Size.X,
 178  	}
 179  	for {
 180  		lb, off, _, _, _, ok := it.Next()
 181  		if !ok {
 182  			break
 183  		}
 184  		stack := op.Save(gtx.Ops)
 185  		op.Offset(Fpt(off)).Add(gtx.Ops)
 186  		l.shaper.Shape(l.font, textSize, lb).Add(gtx.Ops)
 187  		clip.Rect(cl.Sub(off)).Add(gtx.Ops)
 188  		paint.ColorOp{Color: l.color}.Add(gtx.Ops)
 189  		paint.PaintOp{}.Add(gtx.Ops)
 190  		stack.Load()
 191  	}
 192  	return dims
 193  }
 194  
 195  func textPadding(lines []text.Line) (padding image.Rectangle) {
 196  	if len(lines) == 0 {
 197  		return
 198  	}
 199  	first := lines[0]
 200  	if d := first.Ascent + first.Bounds.Min.Y; d < 0 {
 201  		padding.Min.Y = d.Ceil()
 202  	}
 203  	last := lines[len(lines)-1]
 204  	if d := last.Bounds.Max.Y - last.Descent; d > 0 {
 205  		padding.Max.Y = d.Ceil()
 206  	}
 207  	if d := first.Bounds.Min.X; d < 0 {
 208  		padding.Min.X = d.Ceil()
 209  	}
 210  	if d := first.Bounds.Max.X - first.Width; d > 0 {
 211  		padding.Max.X = d.Ceil()
 212  	}
 213  	return
 214  }
 215  
 216  func linesDimens(lines []text.Line) l.Dimensions {
 217  	var width fixed.Int26_6
 218  	var h int
 219  	var baseline int
 220  	if len(lines) > 0 {
 221  		baseline = lines[0].Ascent.Ceil()
 222  		var prevDesc fixed.Int26_6
 223  		for _, l := range lines {
 224  			h += (prevDesc + l.Ascent).Ceil()
 225  			prevDesc = l.Descent
 226  			if l.Width > width {
 227  				width = l.Width
 228  			}
 229  		}
 230  		h += lines[len(lines)-1].Descent.Ceil()
 231  	}
 232  	w := width.Ceil()
 233  	return l.Dimensions{
 234  		Size: image.Point{
 235  			X: w,
 236  			Y: h,
 237  		},
 238  		Baseline: h - baseline,
 239  	}
 240  }
 241  
 242  func align(align text.Alignment, width fixed.Int26_6, maxWidth int) fixed.Int26_6 {
 243  	mw := fixed.I(maxWidth)
 244  	switch align {
 245  	case text.Middle:
 246  		return fixed.I(((mw - width) / 2).Floor())
 247  	case text.End:
 248  		return fixed.I((mw - width).Floor())
 249  	case text.Start:
 250  		return 0
 251  	default:
 252  		panic(fmt.Errorf("unknown alignment %v", align))
 253  	}
 254  }
 255  
 256  // ScaleType is a map of the set of label txsizes
 257  type ScaleType map[string]float32
 258  
 259  // Scales is the ratios against
 260  //
 261  // TODO: shouldn't that 16.0 be the text size in the theme?
 262  var Scales = ScaleType{
 263  	"H1":      96.0 / 16.0,
 264  	"H2":      60.0 / 16.0,
 265  	"H3":      48.0 / 16.0,
 266  	"H4":      34.0 / 16.0,
 267  	"H5":      24.0 / 16.0,
 268  	"H6":      20.0 / 16.0,
 269  	"Body1":   1,
 270  	"Body2":   14.0 / 16.0,
 271  	"Caption": 12.0 / 16.0,
 272  }
 273  
 274  // Label creates a label that prints a block of text
 275  func (w *Window) Label() (l *Label) {
 276  	var f text.Font
 277  	var e error
 278  	var fon text.Font
 279  	if fon, e = w.Theme.collection.Font("plan9"); !E.Chk(e) {
 280  		f = fon
 281  	}
 282  	return &Label{
 283  		Window:   w,
 284  		text:     "",
 285  		font:     f,
 286  		color:    w.Colors.GetNRGBAFromName("DocText"),
 287  		textSize: unit.Sp(1),
 288  		shaper:   w.shaper,
 289  	}
 290  }
 291  
 292  // Text sets the text to render in the label
 293  func (l *Label) Text(text string) *Label {
 294  	l.text = text
 295  	return l
 296  }
 297  
 298  // TextScale sets the size of the text relative to the base font size
 299  func (l *Label) TextScale(scale float32) *Label {
 300  	l.textSize = l.Theme.TextSize.Scale(scale)
 301  	return l
 302  }
 303  
 304  // MaxLines sets the maximum number of lines to render
 305  func (l *Label) MaxLines(maxLines int) *Label {
 306  	l.maxLines = maxLines
 307  	return l
 308  }
 309  
 310  // Alignment sets the text alignment, left, right or centered
 311  func (l *Label) Alignment(alignment text.Alignment) *Label {
 312  	l.alignment = alignment
 313  	return l
 314  }
 315  
 316  // Color sets the color of the label font
 317  func (l *Label) Color(color string) *Label {
 318  	l.color = l.Theme.Colors.GetNRGBAFromName(color)
 319  	return l
 320  }
 321  
 322  // Font sets the font out of the available font collection
 323  func (l *Label) Font(font string) *Label {
 324  	var e error
 325  	var fon text.Font
 326  	if fon, e = l.Theme.collection.Font(font); !E.Chk(e) {
 327  		l.font = fon
 328  	}
 329  	return l
 330  }
 331  
 332  // H1 header 1
 333  func (w *Window) H1(txt string) (l *Label) {
 334  	l = w.Label().TextScale(Scales["H1"]).Font("plan9").Text(txt)
 335  	return
 336  }
 337  
 338  // H2 header 2
 339  func (w *Window) H2(txt string) (l *Label) {
 340  	l = w.Label().TextScale(Scales["H2"]).Font("plan9").Text(txt)
 341  	return
 342  }
 343  
 344  // H3 header 3
 345  func (w *Window) H3(txt string) (l *Label) {
 346  	l = w.Label().TextScale(Scales["H3"]).Font("plan9").Text(txt)
 347  	return
 348  }
 349  
 350  // H4 header 4
 351  func (w *Window) H4(txt string) (l *Label) {
 352  	l = w.Label().TextScale(Scales["H4"]).Font("plan9").Text(txt)
 353  	return
 354  }
 355  
 356  // H5 header 5
 357  func (w *Window) H5(txt string) (l *Label) {
 358  	l = w.Label().TextScale(Scales["H5"]).Font("plan9").Text(txt)
 359  	return
 360  }
 361  
 362  // H6 header 6
 363  func (w *Window) H6(txt string) (l *Label) {
 364  	l = w.Label().TextScale(Scales["H6"]).Font("plan9").Text(txt)
 365  	return
 366  }
 367  
 368  // Body1 normal body text 1
 369  func (w *Window) Body1(txt string) (l *Label) {
 370  	l = w.Label().TextScale(Scales["Body1"]).Font("bariol regular").Text(txt)
 371  	return
 372  }
 373  
 374  // Body2 normal body text 2
 375  func (w *Window) Body2(txt string) (l *Label) {
 376  	l = w.Label().TextScale(Scales["Body2"]).Font("bariol regular").Text(txt)
 377  	return
 378  }
 379  
 380  // Caption caption text
 381  func (w *Window) Caption(txt string) (l *Label) {
 382  	l = w.Label().TextScale(Scales["Caption"]).Font("bariol regular").Text(txt)
 383  	return
 384  }
 385