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 <mail@eliasnaur.com>
This commit is contained in:
Elias Naur
2019-10-08 21:29:04 +02:00
parent ea404bc8fc
commit bef7c39e4c
8 changed files with 233 additions and 198 deletions
+10 -31
View File
@@ -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++ {
+4 -16
View File
@@ -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))
}
+15 -19
View File
@@ -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
+2 -2
View File
@@ -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{})
@@ -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
}
-61
View File
@@ -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
}
+109
View File
@@ -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
}
+16 -15
View File
@@ -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