github.com/cybriq/giocore@v0.0.7-0.20210703034601-cfb9cb5f3900/text/shaper.go (about)

     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/cybriq/giocore/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  }