gioui.org/ui@v0.0.0-20190926171558-ce74bc0cbaea/measure/measure.go (about)

     1  // SPDX-License-Identifier: Unlicense OR MIT
     2  
     3  /*
     4  Package measure implements text layout and shaping.
     5  */
     6  package measure
     7  
     8  import (
     9  	"math"
    10  	"unicode"
    11  	"unicode/utf8"
    12  
    13  	"gioui.org/ui"
    14  	"gioui.org/ui/f32"
    15  	"gioui.org/ui/paint"
    16  	"gioui.org/ui/text"
    17  	"golang.org/x/image/font"
    18  	"golang.org/x/image/font/sfnt"
    19  	"golang.org/x/image/math/fixed"
    20  )
    21  
    22  // Faces is a cache of text layouts and paths.
    23  type Faces struct {
    24  	config      ui.Config
    25  	faceCache   map[faceKey]*Face
    26  	layoutCache map[layoutKey]cachedLayout
    27  	pathCache   map[pathKey]cachedPath
    28  }
    29  
    30  type cachedLayout struct {
    31  	active bool
    32  	layout *text.Layout
    33  }
    34  
    35  type cachedPath struct {
    36  	active bool
    37  	path   ui.MacroOp
    38  }
    39  
    40  type layoutKey struct {
    41  	f    *sfnt.Font
    42  	ppem fixed.Int26_6
    43  	str  string
    44  	opts text.LayoutOptions
    45  }
    46  
    47  type pathKey struct {
    48  	f    *sfnt.Font
    49  	ppem fixed.Int26_6
    50  	str  string
    51  }
    52  
    53  type faceKey struct {
    54  	font *sfnt.Font
    55  	size ui.Value
    56  }
    57  
    58  // Face is a cached implementation of text.Face.
    59  type Face struct {
    60  	faces *Faces
    61  	size  ui.Value
    62  	font  *opentype
    63  }
    64  
    65  // Reset the cache, discarding any measures or paths that
    66  // haven't been used since the last call to Reset.
    67  func (f *Faces) Reset(c ui.Config) {
    68  	f.config = c
    69  	f.init()
    70  	for pk, p := range f.pathCache {
    71  		if !p.active {
    72  			delete(f.pathCache, pk)
    73  			continue
    74  		}
    75  		p.active = false
    76  		f.pathCache[pk] = p
    77  	}
    78  	for lk, l := range f.layoutCache {
    79  		if !l.active {
    80  			delete(f.layoutCache, lk)
    81  			continue
    82  		}
    83  		l.active = false
    84  		f.layoutCache[lk] = l
    85  	}
    86  }
    87  
    88  // For returns a Face for the given font and size.
    89  func (f *Faces) For(fnt *sfnt.Font, size ui.Value) *Face {
    90  	f.init()
    91  	fk := faceKey{fnt, size}
    92  	if f, exist := f.faceCache[fk]; exist {
    93  		return f
    94  	}
    95  	face := &Face{
    96  		faces: f,
    97  		size:  size,
    98  		font:  &opentype{Font: fnt, Hinting: font.HintingFull},
    99  	}
   100  	f.faceCache[fk] = face
   101  	return face
   102  }
   103  
   104  func (f *Faces) init() {
   105  	if f.faceCache != nil {
   106  		return
   107  	}
   108  	f.faceCache = make(map[faceKey]*Face)
   109  	f.pathCache = make(map[pathKey]cachedPath)
   110  	f.layoutCache = make(map[layoutKey]cachedLayout)
   111  }
   112  
   113  func (f *Face) Layout(str string, opts text.LayoutOptions) *text.Layout {
   114  	ppem := fixed.Int26_6(f.faces.config.Px(f.size) * 64)
   115  	lk := layoutKey{
   116  		f:    f.font.Font,
   117  		ppem: ppem,
   118  		str:  str,
   119  		opts: opts,
   120  	}
   121  	if l, ok := f.faces.layoutCache[lk]; ok {
   122  		l.active = true
   123  		f.faces.layoutCache[lk] = l
   124  		return l.layout
   125  	}
   126  	l := layoutText(ppem, str, f.font, opts)
   127  	f.faces.layoutCache[lk] = cachedLayout{active: true, layout: l}
   128  	return l
   129  }
   130  
   131  func (f *Face) Path(str text.String) ui.MacroOp {
   132  	ppem := fixed.Int26_6(f.faces.config.Px(f.size) * 64)
   133  	pk := pathKey{
   134  		f:    f.font.Font,
   135  		ppem: ppem,
   136  		str:  str.String,
   137  	}
   138  	if p, ok := f.faces.pathCache[pk]; ok {
   139  		p.active = true
   140  		f.faces.pathCache[pk] = p
   141  		return p.path
   142  	}
   143  	p := textPath(ppem, f.font, str)
   144  	f.faces.pathCache[pk] = cachedPath{active: true, path: p}
   145  	return p
   146  }
   147  
   148  func layoutText(ppem fixed.Int26_6, str string, f *opentype, opts text.LayoutOptions) *text.Layout {
   149  	m := f.Metrics(ppem)
   150  	lineTmpl := text.Line{
   151  		Ascent: m.Ascent,
   152  		// m.Height is equal to m.Ascent + m.Descent + linegap.
   153  		// Compute the descent including the linegap.
   154  		Descent: m.Height - m.Ascent,
   155  		Bounds:  f.Bounds(ppem),
   156  	}
   157  	var lines []text.Line
   158  	maxDotX := fixed.Int26_6(math.MaxInt32)
   159  	maxDotX = fixed.I(opts.MaxWidth)
   160  	type state struct {
   161  		r     rune
   162  		advs  []fixed.Int26_6
   163  		adv   fixed.Int26_6
   164  		x     fixed.Int26_6
   165  		idx   int
   166  		valid bool
   167  	}
   168  	var prev, word state
   169  	endLine := func() {
   170  		line := lineTmpl
   171  		line.Text.Advances = prev.advs
   172  		line.Text.String = str[:prev.idx]
   173  		line.Width = prev.x + prev.adv
   174  		line.Bounds.Max.X += prev.x
   175  		lines = append(lines, line)
   176  		str = str[prev.idx:]
   177  		prev = state{}
   178  		word = state{}
   179  	}
   180  	for prev.idx < len(str) {
   181  		c, s := utf8.DecodeRuneInString(str[prev.idx:])
   182  		nl := c == '\n'
   183  		if opts.SingleLine && nl {
   184  			nl = false
   185  			c = ' '
   186  			s = 1
   187  		}
   188  		a, ok := f.GlyphAdvance(ppem, c)
   189  		if !ok {
   190  			prev.idx += s
   191  			continue
   192  		}
   193  		next := state{
   194  			r:     c,
   195  			advs:  prev.advs,
   196  			idx:   prev.idx + s,
   197  			x:     prev.x + prev.adv,
   198  			valid: true,
   199  		}
   200  		if nl {
   201  			// The newline is zero width; use the previous
   202  			// character for line measurements.
   203  			prev.advs = append(prev.advs, 0)
   204  			prev.idx = next.idx
   205  			endLine()
   206  			continue
   207  		}
   208  		next.adv = a
   209  		var k fixed.Int26_6
   210  		if prev.valid {
   211  			k = f.Kern(ppem, prev.r, next.r)
   212  		}
   213  		// Break the line if we're out of space.
   214  		if prev.idx > 0 && next.x+next.adv+k >= maxDotX {
   215  			// If the line contains no word breaks, break off the last rune.
   216  			if word.idx == 0 {
   217  				word = prev
   218  			}
   219  			next.x -= word.x + word.adv
   220  			next.idx -= word.idx
   221  			next.advs = next.advs[len(word.advs):]
   222  			prev = word
   223  			endLine()
   224  		} else {
   225  			next.adv += k
   226  		}
   227  		next.advs = append(next.advs, next.adv)
   228  		if unicode.IsSpace(next.r) {
   229  			word = next
   230  		}
   231  		prev = next
   232  	}
   233  	endLine()
   234  	return &text.Layout{Lines: lines}
   235  }
   236  
   237  func textPath(ppem fixed.Int26_6, f *opentype, str text.String) ui.MacroOp {
   238  	var lastPos f32.Point
   239  	var builder paint.PathBuilder
   240  	ops := new(ui.Ops)
   241  	builder.Init(ops)
   242  	var x fixed.Int26_6
   243  	var advIdx int
   244  	var m ui.MacroOp
   245  	m.Record(ops)
   246  	for _, r := range str.String {
   247  		if !unicode.IsSpace(r) {
   248  			segs, ok := f.LoadGlyph(ppem, r)
   249  			if !ok {
   250  				continue
   251  			}
   252  			// Move to glyph position.
   253  			pos := f32.Point{
   254  				X: float32(x) / 64,
   255  			}
   256  			builder.Move(pos.Sub(lastPos))
   257  			lastPos = pos
   258  			var lastArg f32.Point
   259  			// Convert sfnt.Segments to relative segments.
   260  			for _, fseg := range segs {
   261  				nargs := 1
   262  				switch fseg.Op {
   263  				case sfnt.SegmentOpQuadTo:
   264  					nargs = 2
   265  				case sfnt.SegmentOpCubeTo:
   266  					nargs = 3
   267  				}
   268  				var args [3]f32.Point
   269  				for i := 0; i < nargs; i++ {
   270  					a := f32.Point{
   271  						X: float32(fseg.Args[i].X) / 64,
   272  						Y: float32(fseg.Args[i].Y) / 64,
   273  					}
   274  					args[i] = a.Sub(lastArg)
   275  					if i == nargs-1 {
   276  						lastArg = a
   277  					}
   278  				}
   279  				switch fseg.Op {
   280  				case sfnt.SegmentOpMoveTo:
   281  					builder.Move(args[0])
   282  				case sfnt.SegmentOpLineTo:
   283  					builder.Line(args[0])
   284  				case sfnt.SegmentOpQuadTo:
   285  					builder.Quad(args[0], args[1])
   286  				case sfnt.SegmentOpCubeTo:
   287  					builder.Cube(args[0], args[1], args[2])
   288  				default:
   289  					panic("unsupported segment op")
   290  				}
   291  			}
   292  			lastPos = lastPos.Add(lastArg)
   293  		}
   294  		x += str.Advances[advIdx]
   295  		advIdx++
   296  	}
   297  	builder.End()
   298  	m.Stop()
   299  	return m
   300  }