From bef7c39e4ce3a9e26a06f422c5933c511d5889ad Mon Sep 17 00:00:00 2001 From: Elias Naur Date: Tue, 8 Oct 2019 21:29:04 +0200 Subject: [PATCH] text: replace Family with Shaper, add Font, Face There is now a single shaping implementation, Shaper, for all fonts, replacing Family that only covered a single typeface. A typeface is identified by a name, where the empty string denotes the default typeface. Font is introduced to specify a particular font from the typeface, style, weight and size. Face is changed to an interface for a particular layout and shaping method. The text/shape package is renamed to text/opentype and contains a Face implementation based on golang.org/x/image/font/sfnt. Signed-off-by: Elias Naur --- text/editor.go | 41 ++---- text/label.go | 20 +-- text/{shape => }/lru.go | 34 ++--- text/{shape => }/lru_test.go | 4 +- .../measure.go => opentype/opentype.go} | 131 ++++++++++-------- text/shape/opentype.go | 61 -------- text/shaper.go | 109 +++++++++++++++ text/{measure.go => text.go} | 31 +++-- 8 files changed, 233 insertions(+), 198 deletions(-) rename text/{shape => }/lru.go (74%) rename text/{shape => }/lru_test.go (95%) rename text/{shape/measure.go => opentype/opentype.go} (63%) delete mode 100644 text/shape/opentype.go create mode 100644 text/shaper.go rename text/{measure.go => text.go} (83%) diff --git a/text/editor.go b/text/editor.go index e9dca39e..26dd18d1 100644 --- a/text/editor.go +++ b/text/editor.go @@ -21,23 +21,9 @@ import ( "golang.org/x/image/math/fixed" ) -type editorFont struct { - // Family defines the font and style of the text. - Family Family - // Face specifies the font family configuration. - Face Face - // Size is the text size. - Size unit.Value -} - // Editor implements an editable and scrollable text area. type Editor struct { - // Family defines the font and style of the text. - Family Family - // Face specifies the font family configuration. - Face Face - // Size is the text size. - Size unit.Value + Font Font // Material for drawing the text. Material op.MacroOp // Hint contains the text displayed to the user when the @@ -184,13 +170,8 @@ func (e *Editor) Focus() { } // Layout flushes any remaining events and lays out the editor. -func (e *Editor) Layout(gtx *layout.Context) { - font := editorFont{ - e.Family, - e.Face, - e.Size, - } - e.layout(gtx, font) +func (e *Editor) Layout(gtx *layout.Context, s *Shaper) { + e.layout(gtx, s, e.Font) var stack op.StackOp stack.Push(gtx.Ops) if e.Len() > 0 { @@ -200,14 +181,14 @@ func (e *Editor) Layout(gtx *layout.Context) { paint.ColorOp{Color: color.RGBA{A: 0xaa}}.Add(gtx.Ops) e.HintMaterial.Add(gtx.Ops) } - e.draw(gtx, font) + e.draw(gtx, s, e.Font) paint.ColorOp{Color: color.RGBA{A: 0xff}}.Add(gtx.Ops) e.Material.Add(gtx.Ops) e.drawCaret(gtx) stack.Pop() } -func (e *Editor) layout(gtx *layout.Context, font editorFont) { +func (e *Editor) layout(gtx *layout.Context, s *Shaper, font Font) { for _, ok := e.Event(gtx); ok; _, ok = e.Event(gtx) { } if e.font != font { @@ -237,7 +218,7 @@ func (e *Editor) layout(gtx *layout.Context, font editorFont) { } if !e.valid { - e.layoutText(gtx, font) + e.layoutText(gtx, s, font) e.valid = true } @@ -278,7 +259,7 @@ func (e *Editor) layout(gtx *layout.Context, font editorFont) { } } -func (e *Editor) draw(gtx *layout.Context, font editorFont) { +func (e *Editor) draw(gtx *layout.Context, s *Shaper, font Font) { var stack op.StackOp stack.Push(gtx.Ops) off := image.Point{ @@ -295,7 +276,6 @@ func (e *Editor) draw(gtx *layout.Context, font editorFont) { Width: e.viewWidth(), Offset: off, } - fsize := float32(gtx.Px(font.Size)) for { str, lineOff, ok := it.Next() if !ok { @@ -304,7 +284,7 @@ func (e *Editor) draw(gtx *layout.Context, font editorFont) { var stack op.StackOp stack.Push(gtx.Ops) op.TransformOp{}.Offset(lineOff).Add(gtx.Ops) - font.Family.Shape(font.Face, fsize, str).Add(gtx.Ops) + s.Shape(gtx, font, str).Add(gtx.Ops) paint.PaintOp{Rect: toRectF(clip).Sub(lineOff)}.Add(gtx.Ops) stack.Pop() } @@ -407,14 +387,13 @@ func (e *Editor) moveCoord(c unit.Converter, pos image.Point) { e.moveToLine(x, carLine) } -func (e *Editor) layoutText(c unit.Converter, font editorFont) { +func (e *Editor) layoutText(c unit.Converter, s *Shaper, font Font) { txt := e.rr.String() if txt == "" { txt = e.Hint } opts := LayoutOptions{SingleLine: e.SingleLine, MaxWidth: e.maxWidth} - fsize := float32(c.Px(font.Size)) - textLayout := font.Family.Layout(font.Face, fsize, txt, opts) + textLayout := s.Layout(c, font, txt, opts) lines := textLayout.Lines dims := linesDimens(lines) for i := 0; i < len(lines)-1; i++ { diff --git a/text/label.go b/text/label.go index 84c8c9c3..9ba40209 100644 --- a/text/label.go +++ b/text/label.go @@ -12,17 +12,13 @@ import ( "gioui.org/layout" "gioui.org/op" "gioui.org/op/paint" - "gioui.org/unit" "golang.org/x/image/math/fixed" ) // Label is a widget for laying out and drawing text. type Label struct { - // Face defines the style of the text. - Face Face - // Size is the text size. If zero, a default size is used. - Size unit.Value + Font Font // Material is a macro recording the material to draw the // text. Use a ColorOp for colored text. @@ -93,10 +89,9 @@ func (l *lineIterator) Next() (String, f32.Point, bool) { return String{}, f32.Point{}, false } -func (l Label) Layout(gtx *layout.Context, family Family) { +func (l Label) Layout(gtx *layout.Context, s *Shaper) { cs := gtx.Constraints - tsize := textSize(gtx, l.Size) - textLayout := family.Layout(l.Face, tsize, l.Text, LayoutOptions{MaxWidth: cs.Width.Max}) + textLayout := s.Layout(gtx, l.Font, l.Text, LayoutOptions{MaxWidth: cs.Width.Max}) lines := textLayout.Lines if max := l.MaxLines; max > 0 && len(lines) > max { lines = lines[:max] @@ -123,7 +118,7 @@ func (l Label) Layout(gtx *layout.Context, family Family) { var stack op.StackOp stack.Push(gtx.Ops) op.TransformOp{}.Offset(off).Add(gtx.Ops) - family.Shape(l.Face, tsize, str).Add(gtx.Ops) + s.Shape(gtx, l.Font, str).Add(gtx.Ops) // Set a default color in case the material is empty. paint.ColorOp{Color: color.RGBA{A: 0xff}}.Add(gtx.Ops) l.Material.Add(gtx.Ops) @@ -153,10 +148,3 @@ func textPadding(lines []Line) (padTop int, padBottom int) { } return } - -func textSize(c unit.Converter, s unit.Value) float32 { - if s.V == 0 { - s = unit.Sp(12) - } - return float32(c.Px(s)) -} diff --git a/text/shape/lru.go b/text/lru.go similarity index 74% rename from text/shape/lru.go rename to text/lru.go index 7977e159..f3c769c8 100644 --- a/text/shape/lru.go +++ b/text/lru.go @@ -1,17 +1,15 @@ // SPDX-License-Identifier: Unlicense OR MIT -package shape +package text import ( "gioui.org/op/paint" - "gioui.org/text" - "golang.org/x/image/font/sfnt" "golang.org/x/image/math/fixed" ) type layoutCache struct { - m map[layoutKey]*layout - head, tail *layout + m map[layoutKey]*layoutElem + head, tail *layoutElem } type pathCache struct { @@ -19,10 +17,10 @@ type pathCache struct { head, tail *path } -type layout struct { - next, prev *layout +type layoutElem struct { + next, prev *layoutElem key layoutKey - layout *text.Layout + layout *Layout } type path struct { @@ -32,21 +30,19 @@ type path struct { } type layoutKey struct { - f *sfnt.Font ppem fixed.Int26_6 str string - opts text.LayoutOptions + opts LayoutOptions } type pathKey struct { - f *sfnt.Font ppem fixed.Int26_6 str string } const maxSize = 1000 -func (l *layoutCache) Get(k layoutKey) (*text.Layout, bool) { +func (l *layoutCache) Get(k layoutKey) (*Layout, bool) { if lt, ok := l.m[k]; ok { l.remove(lt) l.insert(lt) @@ -55,15 +51,15 @@ func (l *layoutCache) Get(k layoutKey) (*text.Layout, bool) { return nil, false } -func (l *layoutCache) Put(k layoutKey, lt *text.Layout) { +func (l *layoutCache) Put(k layoutKey, lt *Layout) { if l.m == nil { - l.m = make(map[layoutKey]*layout) - l.head = new(layout) - l.tail = new(layout) + l.m = make(map[layoutKey]*layoutElem) + l.head = new(layoutElem) + l.tail = new(layoutElem) l.head.prev = l.tail l.tail.next = l.head } - val := &layout{key: k, layout: lt} + val := &layoutElem{key: k, layout: lt} l.m[k] = val l.insert(val) if len(l.m) > maxSize { @@ -73,12 +69,12 @@ func (l *layoutCache) Put(k layoutKey, lt *text.Layout) { } } -func (l *layoutCache) remove(lt *layout) { +func (l *layoutCache) remove(lt *layoutElem) { lt.next.prev = lt.prev lt.prev.next = lt.next } -func (l *layoutCache) insert(lt *layout) { +func (l *layoutCache) insert(lt *layoutElem) { lt.next = l.head lt.prev = l.head.prev lt.prev.next = lt diff --git a/text/shape/lru_test.go b/text/lru_test.go similarity index 95% rename from text/shape/lru_test.go rename to text/lru_test.go index 4b35bf79..6d9acdb5 100644 --- a/text/shape/lru_test.go +++ b/text/lru_test.go @@ -1,6 +1,6 @@ // SPDX-License-Identifier: Unlicense OR MIT -package shape +package text import ( "strconv" @@ -21,7 +21,7 @@ func TestLayoutLRU(t *testing.T) { testLRU(t, put, get) } -func TestuPathLRU(t *testing.T) { +func TestPathLRU(t *testing.T) { c := new(pathCache) put := func(i int) { c.Put(pathKey{str: strconv.Itoa(i)}, paint.ClipOp{}) diff --git a/text/shape/measure.go b/text/opentype/opentype.go similarity index 63% rename from text/shape/measure.go rename to text/opentype/opentype.go index bb73af9a..72ae175f 100644 --- a/text/shape/measure.go +++ b/text/opentype/opentype.go @@ -1,9 +1,8 @@ // SPDX-License-Identifier: Unlicense OR MIT -/* -Package shape implements text layout and shaping. -*/ -package shape +// Package opentype implements text layout and shaping for OpenType +// files. +package opentype import ( "math" @@ -19,65 +18,42 @@ import ( "golang.org/x/image/math/fixed" ) -// Family is an implementation of text.Family. It caches -// layouts and paths. -// A Family must specify at least the Regular font to be useful. -type Family struct { - Regular *sfnt.Font - Italic *sfnt.Font - Bold *sfnt.Font - - layoutCache layoutCache - pathCache pathCache - buf sfnt.Buffer +// Font implementats text.Face. +type Font struct { + font *sfnt.Font + buf sfnt.Buffer } -// for returns a font for the given face. -func (f *Family) fontFor(face text.Face) *sfnt.Font { - var font *sfnt.Font - switch { - case face.Style == text.Italic: - font = f.Italic - case face.Weight >= 600: - font = f.Bold +type opentype struct { + Font *sfnt.Font + Hinting font.Hinting +} + +// NewFont parses an SFNT font, such as TTF or OTF data, from a []byte +// data source. +func Parse(src []byte) (*Font, error) { + fnt, err := sfnt.Parse(src) + if err != nil { + return nil, err } - if font == nil { - font = f.Regular + return &Font{font: fnt}, nil +} + +// Must is a helper that wraps a call to a function returning (*Font, +// error) and panics if the error is non-nil. +func Must(font *Font, err error) *Font { + if err != nil { + panic(err) } return font } -func (f *Family) Layout(face text.Face, size float32, str string, opts text.LayoutOptions) *text.Layout { - fnt := f.fontFor(face) - ppem := fixed.Int26_6(size * 64) - lk := layoutKey{ - f: fnt, - ppem: ppem, - str: str, - opts: opts, - } - if l, ok := f.layoutCache.Get(lk); ok { - return l - } - l := layoutText(&f.buf, ppem, str, &opentype{Font: fnt, Hinting: font.HintingFull}, opts) - f.layoutCache.Put(lk, l) - return l +func (f *Font) Layout(ppem fixed.Int26_6, str string, opts text.LayoutOptions) *text.Layout { + return layoutText(&f.buf, ppem, str, &opentype{Font: f.font, Hinting: font.HintingFull}, opts) } -func (f *Family) Shape(face text.Face, size float32, str text.String) paint.ClipOp { - fnt := f.fontFor(face) - ppem := fixed.Int26_6(size * 64) - pk := pathKey{ - f: fnt, - ppem: ppem, - str: str.String, - } - if p, ok := f.pathCache.Get(pk); ok { - return p - } - p := textPath(&f.buf, ppem, &opentype{Font: fnt, Hinting: font.HintingFull}, str) - f.pathCache.Put(pk, p) - return p +func (f *Font) Shape(ppem fixed.Int26_6, str text.String) paint.ClipOp { + return textPath(&f.buf, ppem, &opentype{Font: f.font, Hinting: font.HintingFull}, str) } func layoutText(buf *sfnt.Buffer, ppem fixed.Int26_6, str string, f *opentype, opts text.LayoutOptions) *text.Layout { @@ -229,3 +205,50 @@ func textPath(buf *sfnt.Buffer, ppem fixed.Int26_6, f *opentype, str text.String } return builder.End() } + +func (f *opentype) GlyphAdvance(buf *sfnt.Buffer, ppem fixed.Int26_6, r rune) (advance fixed.Int26_6, ok bool) { + g, err := f.Font.GlyphIndex(buf, r) + if err != nil { + return 0, false + } + adv, err := f.Font.GlyphAdvance(buf, g, ppem, f.Hinting) + return adv, err == nil +} + +func (f *opentype) Kern(buf *sfnt.Buffer, ppem fixed.Int26_6, r0, r1 rune) fixed.Int26_6 { + g0, err := f.Font.GlyphIndex(buf, r0) + if err != nil { + return 0 + } + g1, err := f.Font.GlyphIndex(buf, r1) + if err != nil { + return 0 + } + adv, err := f.Font.Kern(buf, g0, g1, ppem, f.Hinting) + if err != nil { + return 0 + } + return adv +} + +func (f *opentype) Metrics(buf *sfnt.Buffer, ppem fixed.Int26_6) font.Metrics { + m, _ := f.Font.Metrics(buf, ppem, f.Hinting) + return m +} + +func (f *opentype) Bounds(buf *sfnt.Buffer, ppem fixed.Int26_6) fixed.Rectangle26_6 { + r, _ := f.Font.Bounds(buf, ppem, f.Hinting) + return r +} + +func (f *opentype) LoadGlyph(buf *sfnt.Buffer, ppem fixed.Int26_6, r rune) ([]sfnt.Segment, bool) { + g, err := f.Font.GlyphIndex(buf, r) + if err != nil { + return nil, false + } + segs, err := f.Font.LoadGlyph(buf, g, ppem, nil) + if err != nil { + return nil, false + } + return segs, true +} diff --git a/text/shape/opentype.go b/text/shape/opentype.go deleted file mode 100644 index b0b670a1..00000000 --- a/text/shape/opentype.go +++ /dev/null @@ -1,61 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package shape - -import ( - "golang.org/x/image/font" - "golang.org/x/image/font/sfnt" - "golang.org/x/image/math/fixed" -) - -type opentype struct { - Font *sfnt.Font - Hinting font.Hinting -} - -func (f *opentype) GlyphAdvance(buf *sfnt.Buffer, ppem fixed.Int26_6, r rune) (advance fixed.Int26_6, ok bool) { - g, err := f.Font.GlyphIndex(buf, r) - if err != nil { - return 0, false - } - adv, err := f.Font.GlyphAdvance(buf, g, ppem, f.Hinting) - return adv, err == nil -} - -func (f *opentype) Kern(buf *sfnt.Buffer, ppem fixed.Int26_6, r0, r1 rune) fixed.Int26_6 { - g0, err := f.Font.GlyphIndex(buf, r0) - if err != nil { - return 0 - } - g1, err := f.Font.GlyphIndex(buf, r1) - if err != nil { - return 0 - } - adv, err := f.Font.Kern(buf, g0, g1, ppem, f.Hinting) - if err != nil { - return 0 - } - return adv -} - -func (f *opentype) Metrics(buf *sfnt.Buffer, ppem fixed.Int26_6) font.Metrics { - m, _ := f.Font.Metrics(buf, ppem, f.Hinting) - return m -} - -func (f *opentype) Bounds(buf *sfnt.Buffer, ppem fixed.Int26_6) fixed.Rectangle26_6 { - r, _ := f.Font.Bounds(buf, ppem, f.Hinting) - return r -} - -func (f *opentype) LoadGlyph(buf *sfnt.Buffer, ppem fixed.Int26_6, r rune) ([]sfnt.Segment, bool) { - g, err := f.Font.GlyphIndex(buf, r) - if err != nil { - return nil, false - } - segs, err := f.Font.LoadGlyph(buf, g, ppem, nil) - if err != nil { - return nil, false - } - return segs, true -} diff --git a/text/shaper.go b/text/shaper.go new file mode 100644 index 00000000..f30ef3cc --- /dev/null +++ b/text/shaper.go @@ -0,0 +1,109 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package text + +import ( + "gioui.org/op/paint" + "gioui.org/unit" + "golang.org/x/image/math/fixed" +) + +// Shaper implements layout and shaping of text and a cache of +// computed results. +// +// Specify the default and fallback font by calling Register with the +// empty Font. +type Shaper struct { + faces map[Font]*face +} + +type face struct { + face Face + layoutCache layoutCache + pathCache pathCache +} + +func (s *Shaper) Register(font Font, tf Face) { + if s.faces == nil { + s.faces = make(map[Font]*face) + } + // Treat all font sizes equally. + font.Size = unit.Value{} + if font.Weight == 0 { + font.Weight = Normal + } + s.faces[font] = &face{ + face: tf, + } +} + +func (s *Shaper) Layout(c unit.Converter, font Font, str string, opts LayoutOptions) *Layout { + tf := s.faceForFont(font) + return tf.layout(fixed.I(c.Px(font.Size)), str, opts) +} + +func (s *Shaper) Shape(c unit.Converter, font Font, str String) paint.ClipOp { + tf := s.faceForFont(font) + return tf.shape(fixed.I(c.Px(font.Size)), str) +} + +func (s *Shaper) faceForStyle(font Font) *face { + tf := s.faces[font] + if tf == nil { + font := font + font.Weight = Normal + tf = s.faces[font] + } + if tf == nil { + font := font + font.Style = Regular + tf = s.faces[font] + } + if tf == nil { + font := font + font.Style = Regular + font.Weight = Normal + tf = s.faces[font] + } + return tf +} + +func (s *Shaper) faceForFont(font Font) *face { + font.Size = unit.Value{} + tf := s.faceForStyle(font) + if tf == nil { + font.Typeface = "" + tf = s.faceForStyle(font) + } + if tf == nil { + panic("no default typeface defined") + } + return tf +} + +func (t *face) layout(ppem fixed.Int26_6, str string, opts LayoutOptions) *Layout { + lk := layoutKey{ + ppem: ppem, + str: str, + opts: opts, + } + if l, ok := t.layoutCache.Get(lk); ok { + return l + } + l := t.face.Layout(ppem, str, opts) + t.layoutCache.Put(lk, l) + return l +} + +func (t *face) shape(ppem fixed.Int26_6, str String) paint.ClipOp { + pk := pathKey{ + ppem: ppem, + str: str.String, + } + if clip, ok := t.pathCache.Get(pk); ok { + return clip + } + clip := t.face.Shape(ppem, str) + t.pathCache.Put(pk, clip) + return clip +} diff --git a/text/measure.go b/text/text.go similarity index 83% rename from text/measure.go rename to text/text.go index 092a70e2..01b5fd32 100644 --- a/text/measure.go +++ b/text/text.go @@ -8,6 +8,7 @@ import ( "gioui.org/layout" "gioui.org/op/paint" + "gioui.org/unit" "golang.org/x/image/math/fixed" ) @@ -45,27 +46,27 @@ type LayoutOptions struct { SingleLine bool } -// Face specify a particular configuration of a Family. -type Face struct { - // Weight is the text weight. If zero, Normal is used instead. - Weight Weight - Style Style -} - // Style is the font style. type Style int // Weight is a font weight, in CSS units. type Weight int -// Family implements a font family. It can layout and shape text from -// a Face and size. -type Family interface { - // Layout returns the text layout for a string given a set of - // options. - Layout(face Face, size float32, s string, opts LayoutOptions) *Layout - // Path returns the ClipOp outline of a text. - Shape(face Face, size float32, s String) paint.ClipOp +// Font specify a particular typeface, style and size. +type Font struct { + // Typeface identifies a particular typeface design. The empty + // string denotes the default typeface. + Typeface string + Size unit.Value + Style Style + // Weight is the text weight. If zero, Normal is used instead. + Weight Weight +} + +// Face implements text layout and shaping for a particular font. +type Face interface { + Layout(ppem fixed.Int26_6, str string, opts LayoutOptions) *Layout + Shape(ppem fixed.Int26_6, str String) paint.ClipOp } type Alignment uint8