shaper.go raw

   1  // SPDX-License-Identifier: Unlicense OR MIT
   2  
   3  package text
   4  
   5  import (
   6  	"io"
   7  	"strings"
   8  
   9  	"golang.org/x/image/math/fixed"
  10  
  11  	"github.com/p9c/p9/pkg/gel/gio/op"
  12  )
  13  
  14  // Shaper implements layout and shaping of text.
  15  type Shaper interface {
  16  	// Layout a text according to a set of options.
  17  	Layout(font Font, size fixed.Int26_6, maxWidth int, txt io.Reader) ([]Line, error)
  18  	// LayoutString is Layout for strings.
  19  	LayoutString(font Font, size fixed.Int26_6, maxWidth int, str string) []Line
  20  	// Shape a line of text and return a clipping operation for its outline.
  21  	Shape(font Font, size fixed.Int26_6, layout Layout) op.CallOp
  22  }
  23  
  24  // A FontFace is a Font and a matching Face.
  25  type FontFace struct {
  26  	Font Font
  27  	Face Face
  28  }
  29  
  30  // Cache implements cached layout and shaping of text from a set of
  31  // registered fonts.
  32  //
  33  // If a font matches no registered shape, Cache falls back to the
  34  // first registered face.
  35  //
  36  // The LayoutString and ShapeString results are cached and re-used if
  37  // possible.
  38  type Cache struct {
  39  	def   Typeface
  40  	faces map[Font]*faceCache
  41  }
  42  
  43  type faceCache struct {
  44  	face        Face
  45  	layoutCache layoutCache
  46  	pathCache   pathCache
  47  }
  48  
  49  func (c *Cache) lookup(font Font) *faceCache {
  50  	f := c.faceForStyle(font)
  51  	if f == nil {
  52  		font.Typeface = c.def
  53  		f = c.faceForStyle(font)
  54  	}
  55  	return f
  56  }
  57  
  58  func (c *Cache) faceForStyle(font Font) *faceCache {
  59  	tf := c.faces[font]
  60  	if tf == nil {
  61  		font := font
  62  		font.Weight = Normal
  63  		tf = c.faces[font]
  64  	}
  65  	if tf == nil {
  66  		font := font
  67  		font.Style = Regular
  68  		tf = c.faces[font]
  69  	}
  70  	if tf == nil {
  71  		font := font
  72  		font.Style = Regular
  73  		font.Weight = Normal
  74  		tf = c.faces[font]
  75  	}
  76  	return tf
  77  }
  78  
  79  func NewCache(collection []FontFace) *Cache {
  80  	c := &Cache{
  81  		faces: make(map[Font]*faceCache),
  82  	}
  83  	for i, ff := range collection {
  84  		if i == 0 {
  85  			c.def = ff.Font.Typeface
  86  		}
  87  		c.faces[ff.Font] = &faceCache{face: ff.Face}
  88  	}
  89  	return c
  90  }
  91  
  92  // Layout implements the Shaper interface.
  93  func (s *Cache) Layout(font Font, size fixed.Int26_6, maxWidth int, txt io.Reader) ([]Line, error) {
  94  	cache := s.lookup(font)
  95  	return cache.face.Layout(size, maxWidth, txt)
  96  }
  97  
  98  // LayoutString is a caching implementation of the Shaper interface.
  99  func (s *Cache) LayoutString(font Font, size fixed.Int26_6, maxWidth int, str string) []Line {
 100  	cache := s.lookup(font)
 101  	return cache.layout(size, maxWidth, str)
 102  }
 103  
 104  // Shape is a caching implementation of the Shaper interface. Shape assumes that the layout
 105  // argument is unchanged from a call to Layout or LayoutString.
 106  func (s *Cache) Shape(font Font, size fixed.Int26_6, layout Layout) op.CallOp {
 107  	cache := s.lookup(font)
 108  	return cache.shape(size, layout)
 109  }
 110  
 111  func (f *faceCache) layout(ppem fixed.Int26_6, maxWidth int, str string) []Line {
 112  	if f == nil {
 113  		return nil
 114  	}
 115  	lk := layoutKey{
 116  		ppem:     ppem,
 117  		maxWidth: maxWidth,
 118  		str:      str,
 119  	}
 120  	if l, ok := f.layoutCache.Get(lk); ok {
 121  		return l
 122  	}
 123  	l, _ := f.face.Layout(ppem, maxWidth, strings.NewReader(str))
 124  	f.layoutCache.Put(lk, l)
 125  	return l
 126  }
 127  
 128  func (f *faceCache) shape(ppem fixed.Int26_6, layout Layout) op.CallOp {
 129  	if f == nil {
 130  		return op.CallOp{}
 131  	}
 132  	pk := pathKey{
 133  		ppem: ppem,
 134  		str:  layout.Text,
 135  	}
 136  	if clip, ok := f.pathCache.Get(pk); ok {
 137  		return clip
 138  	}
 139  	clip := f.face.Shape(ppem, layout)
 140  	f.pathCache.Put(pk, clip)
 141  	return clip
 142  }
 143