mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-01 07:35:40 +00:00
font/opentype: [API] support font collection loading
This commit adds back support for loading font collections, which we
lost when switching to the harfbuzz-based shaper last January. In
addition, this commit takes advantage of our new font loading library's
metadata facilities to automatically construct text.FontFaces for all
fonts within a collection. This is significantly more ergonomic for
users, and can be used to load single fonts with automatic metadata
detection as well.
I've exposed a opentype.Face.Font() method that can be used to get the
font metadata for a given face as well, though you have to type assert to
see it:
var myFace text.Face
if asOpentype, ok := myFace.(opentype.Face); ok {
myFont := asOpentype.Font()
}
The one problem with this approach is that the font variant field always
be automatically populated. Mono font detection is supported, but
other variants like SmallCaps are more complicated and may need to be
expressed differently in the future (smallcaps is a feature that any font
file can have, not necessarily a separate font file). See this [0] upstream
issue for details.
Additionally, in order to avoid import cycles, I've moved the declarations
of font attributes to package font. You can fix your code automatically to
refer to the new definitions by running the following:
gofmt -w -r 'text.FontFace -> font.FontFace' .
gofmt -w -r 'text.Variant -> font.Variant' .
gofmt -w -r 'text.Style -> font.Style' .
gofmt -w -r 'text.Typeface -> font.Typeface' .
gofmt -w -r 'text.Font -> font.Font' .
gofmt -w -r 'text.Regular -> font.Regular' .
gofmt -w -r 'text.Italic -> font.Italic' .
gofmt -w -r 'text.Thin -> font.Thin' .
gofmt -w -r 'text.ExtraLight -> font.ExtraLight' .
gofmt -w -r 'text.Light -> font.Light' .
gofmt -w -r 'text.Normal -> font.Normal' .
gofmt -w -r 'text.Medium -> font.Medium' .
gofmt -w -r 'text.SemiBold -> font.SemiBold' .
gofmt -w -r 'text.Bold -> font.Bold' .
gofmt -w -r 'text.ExtraBold -> font.ExtraBold' .
gofmt -w -r 'text.Black -> font.Black' .
gofmt -w -r 'text.Hairline -> font.Thin' .
gofmt -w -r 'text.UltraLight -> font.ExtraLight' .
gofmt -w -r 'text.DemiBold -> font.SemiBold' .
gofmt -w -r 'text.UltraBold -> font.ExtraBold' .
gofmt -w -r 'text.Heavy -> font.Black' .
gofmt -w -r 'text.ExtraBlack -> font.Black+50' .
gofmt -w -r 'text.UltraBlack -> font.ExtraBlack' .
Make sure each affected file imports gioui.org/font.
[0] https://github.com/go-text/typesetting/issues/57
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
This commit is contained in:
+16
-15
@@ -18,6 +18,7 @@ import (
|
||||
"golang.org/x/text/unicode/bidi"
|
||||
|
||||
"gioui.org/f32"
|
||||
giofont "gioui.org/font"
|
||||
"gioui.org/io/system"
|
||||
"gioui.org/op"
|
||||
"gioui.org/op/clip"
|
||||
@@ -150,24 +151,24 @@ type runLayout struct {
|
||||
|
||||
// faceOrderer chooses the order in which faces should be applied to text.
|
||||
type faceOrderer struct {
|
||||
def Font
|
||||
def giofont.Font
|
||||
faceScratch []font.Face
|
||||
fontDefaultOrder map[Font]int
|
||||
defaultOrderedFonts []Font
|
||||
faces map[Font]font.Face
|
||||
fontDefaultOrder map[giofont.Font]int
|
||||
defaultOrderedFonts []giofont.Font
|
||||
faces map[giofont.Font]font.Face
|
||||
faceToIndex map[font.Face]int
|
||||
fonts []Font
|
||||
fonts []giofont.Font
|
||||
}
|
||||
|
||||
func (f *faceOrderer) insert(fnt Font, face font.Face) {
|
||||
func (f *faceOrderer) insert(fnt giofont.Font, face font.Face) {
|
||||
if len(f.fonts) == 0 {
|
||||
f.def = fnt
|
||||
}
|
||||
if f.fontDefaultOrder == nil {
|
||||
f.fontDefaultOrder = make(map[Font]int)
|
||||
f.fontDefaultOrder = make(map[giofont.Font]int)
|
||||
}
|
||||
if f.faces == nil {
|
||||
f.faces = make(map[Font]font.Face)
|
||||
f.faces = make(map[giofont.Font]font.Face)
|
||||
f.faceToIndex = make(map[font.Face]int)
|
||||
}
|
||||
f.fontDefaultOrder[fnt] = len(f.faceScratch)
|
||||
@@ -198,7 +199,7 @@ func (c *faceOrderer) faceFor(idx int) font.Face {
|
||||
// TODO(whereswaldon): this function could sort all faces by appropriateness for the
|
||||
// given font characteristics. This would ensure that (if possible) text using a
|
||||
// fallback font would select similar weights and emphases to the primary font.
|
||||
func (c *faceOrderer) sortedFacesForStyle(font Font) []font.Face {
|
||||
func (c *faceOrderer) sortedFacesForStyle(font giofont.Font) []font.Face {
|
||||
c.resetFontOrder()
|
||||
primary, ok := c.fontForStyle(font)
|
||||
if !ok {
|
||||
@@ -213,11 +214,11 @@ func (c *faceOrderer) sortedFacesForStyle(font Font) []font.Face {
|
||||
|
||||
// fontForStyle returns the closest existing font to the requested font within the
|
||||
// same typeface.
|
||||
func (c *faceOrderer) fontForStyle(font Font) (Font, bool) {
|
||||
func (c *faceOrderer) fontForStyle(font giofont.Font) (giofont.Font, bool) {
|
||||
if closest, ok := closestFont(font, c.fonts); ok {
|
||||
return closest, true
|
||||
}
|
||||
font.Style = Regular
|
||||
font.Style = giofont.Regular
|
||||
if closest, ok := closestFont(font, c.fonts); ok {
|
||||
return closest, true
|
||||
}
|
||||
@@ -226,7 +227,7 @@ func (c *faceOrderer) fontForStyle(font Font) (Font, bool) {
|
||||
|
||||
// faces returns a slice of faces with primary as the first element and
|
||||
// the remaining faces ordered by insertion order.
|
||||
func (f *faceOrderer) sorted(primary Font) []font.Face {
|
||||
func (f *faceOrderer) sorted(primary giofont.Font) []font.Face {
|
||||
// If we find primary, put it first, and omit it from the below sort.
|
||||
lowest := 0
|
||||
for i := range f.fonts {
|
||||
@@ -875,9 +876,9 @@ func computeVisualOrder(l *line) {
|
||||
|
||||
// closestFont returns the closest Font in available by weight.
|
||||
// In case of equality the lighter weight will be returned.
|
||||
func closestFont(lookup Font, available []Font) (Font, bool) {
|
||||
func closestFont(lookup giofont.Font, available []giofont.Font) (giofont.Font, bool) {
|
||||
found := false
|
||||
var match Font
|
||||
var match giofont.Font
|
||||
for _, cf := range available {
|
||||
if cf == lookup {
|
||||
return lookup, true
|
||||
@@ -902,7 +903,7 @@ func closestFont(lookup Font, available []Font) (Font, bool) {
|
||||
}
|
||||
|
||||
// weightDistance returns the distance value between two font weights.
|
||||
func weightDistance(wa Weight, wb Weight) int {
|
||||
func weightDistance(wa giofont.Weight, wb giofont.Weight) int {
|
||||
// Avoid dealing with negative Weight values.
|
||||
a := int(wa) + 400
|
||||
b := int(wb) + 400
|
||||
|
||||
+44
-43
@@ -10,6 +10,7 @@ import (
|
||||
"golang.org/x/image/font/gofont/goregular"
|
||||
"golang.org/x/image/math/fixed"
|
||||
|
||||
giofont "gioui.org/font"
|
||||
"gioui.org/font/opentype"
|
||||
"gioui.org/io/system"
|
||||
)
|
||||
@@ -24,7 +25,7 @@ var arabic = system.Locale{
|
||||
Direction: system.RTL,
|
||||
}
|
||||
|
||||
func testShaper(faces ...Face) *shaperImpl {
|
||||
func testShaper(faces ...giofont.Face) *shaperImpl {
|
||||
shaper := shaperImpl{}
|
||||
for _, face := range faces {
|
||||
shaper.Load(FontFace{Face: face})
|
||||
@@ -268,12 +269,12 @@ func makeTestText(shaper *shaperImpl, primaryDir system.TextDirection, fontSize,
|
||||
rtlSource = string(complexRunes[:runeLimit])
|
||||
}
|
||||
}
|
||||
simpleText, _ := shaper.shapeAndWrapText(shaper.orderer.sortedFacesForStyle(Font{}), Parameters{
|
||||
simpleText, _ := shaper.shapeAndWrapText(shaper.orderer.sortedFacesForStyle(giofont.Font{}), Parameters{
|
||||
PxPerEm: fixed.I(fontSize),
|
||||
MaxWidth: lineWidth,
|
||||
Locale: locale,
|
||||
}, []rune(simpleSource))
|
||||
complexText, _ := shaper.shapeAndWrapText(shaper.orderer.sortedFacesForStyle(Font{}), Parameters{
|
||||
complexText, _ := shaper.shapeAndWrapText(shaper.orderer.sortedFacesForStyle(giofont.Font{}), Parameters{
|
||||
PxPerEm: fixed.I(fontSize),
|
||||
MaxWidth: lineWidth,
|
||||
Locale: locale,
|
||||
@@ -642,33 +643,33 @@ func TestTextAppend(t *testing.T) {
|
||||
|
||||
func TestClosestFontByWeight(t *testing.T) {
|
||||
const (
|
||||
testTF1 Typeface = "MockFace"
|
||||
testTF2 Typeface = "TestFace"
|
||||
testTF3 Typeface = "AnotherFace"
|
||||
testTF1 giofont.Typeface = "MockFace"
|
||||
testTF2 giofont.Typeface = "TestFace"
|
||||
testTF3 giofont.Typeface = "AnotherFace"
|
||||
)
|
||||
fonts := []Font{
|
||||
{Typeface: testTF1, Style: Regular, Weight: Normal},
|
||||
{Typeface: testTF1, Style: Regular, Weight: Light},
|
||||
{Typeface: testTF1, Style: Regular, Weight: Bold},
|
||||
{Typeface: testTF1, Style: Italic, Weight: Thin},
|
||||
fonts := []giofont.Font{
|
||||
{Typeface: testTF1, Style: giofont.Regular, Weight: giofont.Normal},
|
||||
{Typeface: testTF1, Style: giofont.Regular, Weight: giofont.Light},
|
||||
{Typeface: testTF1, Style: giofont.Regular, Weight: giofont.Bold},
|
||||
{Typeface: testTF1, Style: giofont.Italic, Weight: giofont.Thin},
|
||||
}
|
||||
weightOnlyTests := []struct {
|
||||
Lookup Weight
|
||||
Expected Weight
|
||||
Lookup giofont.Weight
|
||||
Expected giofont.Weight
|
||||
}{
|
||||
// Test for existing weights.
|
||||
{Lookup: Normal, Expected: Normal},
|
||||
{Lookup: Light, Expected: Light},
|
||||
{Lookup: Bold, Expected: Bold},
|
||||
{Lookup: giofont.Normal, Expected: giofont.Normal},
|
||||
{Lookup: giofont.Light, Expected: giofont.Light},
|
||||
{Lookup: giofont.Bold, Expected: giofont.Bold},
|
||||
// Test for missing weights.
|
||||
{Lookup: Thin, Expected: Light},
|
||||
{Lookup: ExtraLight, Expected: Light},
|
||||
{Lookup: Medium, Expected: Normal},
|
||||
{Lookup: SemiBold, Expected: Bold},
|
||||
{Lookup: ExtraBlack, Expected: Bold},
|
||||
{Lookup: giofont.Thin, Expected: giofont.Light},
|
||||
{Lookup: giofont.ExtraLight, Expected: giofont.Light},
|
||||
{Lookup: giofont.Medium, Expected: giofont.Normal},
|
||||
{Lookup: giofont.SemiBold, Expected: giofont.Bold},
|
||||
{Lookup: giofont.ExtraBold, Expected: giofont.Bold},
|
||||
}
|
||||
for _, test := range weightOnlyTests {
|
||||
got, ok := closestFont(Font{Typeface: testTF1, Weight: test.Lookup}, fonts)
|
||||
got, ok := closestFont(giofont.Font{Typeface: testTF1, Weight: test.Lookup}, fonts)
|
||||
if !ok {
|
||||
t.Errorf("expected closest font for %v to exist", test.Lookup)
|
||||
}
|
||||
@@ -676,49 +677,49 @@ func TestClosestFontByWeight(t *testing.T) {
|
||||
t.Errorf("got weight %v, expected %v", got.Weight, test.Expected)
|
||||
}
|
||||
}
|
||||
fonts = []Font{
|
||||
{Typeface: testTF1, Style: Regular, Weight: Light},
|
||||
{Typeface: testTF1, Style: Regular, Weight: Bold},
|
||||
{Typeface: testTF1, Style: Italic, Weight: Normal},
|
||||
{Typeface: testTF3, Style: Italic, Weight: Bold},
|
||||
fonts = []giofont.Font{
|
||||
{Typeface: testTF1, Style: giofont.Regular, Weight: giofont.Light},
|
||||
{Typeface: testTF1, Style: giofont.Regular, Weight: giofont.Bold},
|
||||
{Typeface: testTF1, Style: giofont.Italic, Weight: giofont.Normal},
|
||||
{Typeface: testTF3, Style: giofont.Italic, Weight: giofont.Bold},
|
||||
}
|
||||
otherTests := []struct {
|
||||
Lookup Font
|
||||
Expected Font
|
||||
Lookup giofont.Font
|
||||
Expected giofont.Font
|
||||
ExpectedToFail bool
|
||||
}{
|
||||
// Test for existing fonts.
|
||||
{
|
||||
Lookup: Font{Typeface: testTF1, Weight: Light},
|
||||
Expected: Font{Typeface: testTF1, Style: Regular, Weight: Light},
|
||||
Lookup: giofont.Font{Typeface: testTF1, Weight: giofont.Light},
|
||||
Expected: giofont.Font{Typeface: testTF1, Style: giofont.Regular, Weight: giofont.Light},
|
||||
},
|
||||
{
|
||||
Lookup: Font{Typeface: testTF1, Style: Italic, Weight: Normal},
|
||||
Expected: Font{Typeface: testTF1, Style: Italic, Weight: Normal},
|
||||
Lookup: giofont.Font{Typeface: testTF1, Style: giofont.Italic, Weight: giofont.Normal},
|
||||
Expected: giofont.Font{Typeface: testTF1, Style: giofont.Italic, Weight: giofont.Normal},
|
||||
},
|
||||
// Test for missing fonts.
|
||||
{
|
||||
Lookup: Font{Typeface: testTF1, Weight: Normal},
|
||||
Expected: Font{Typeface: testTF1, Style: Regular, Weight: Light},
|
||||
Lookup: giofont.Font{Typeface: testTF1, Weight: giofont.Normal},
|
||||
Expected: giofont.Font{Typeface: testTF1, Style: giofont.Regular, Weight: giofont.Light},
|
||||
},
|
||||
{
|
||||
Lookup: Font{Typeface: testTF3, Style: Italic, Weight: Normal},
|
||||
Expected: Font{Typeface: testTF3, Style: Italic, Weight: Bold},
|
||||
Lookup: giofont.Font{Typeface: testTF3, Style: giofont.Italic, Weight: giofont.Normal},
|
||||
Expected: giofont.Font{Typeface: testTF3, Style: giofont.Italic, Weight: giofont.Bold},
|
||||
},
|
||||
{
|
||||
Lookup: Font{Typeface: testTF1, Style: Italic, Weight: Thin},
|
||||
Expected: Font{Typeface: testTF1, Style: Italic, Weight: Normal},
|
||||
Lookup: giofont.Font{Typeface: testTF1, Style: giofont.Italic, Weight: giofont.Thin},
|
||||
Expected: giofont.Font{Typeface: testTF1, Style: giofont.Italic, Weight: giofont.Normal},
|
||||
},
|
||||
{
|
||||
Lookup: Font{Typeface: testTF1, Style: Italic, Weight: Bold},
|
||||
Expected: Font{Typeface: testTF1, Style: Italic, Weight: Normal},
|
||||
Lookup: giofont.Font{Typeface: testTF1, Style: giofont.Italic, Weight: giofont.Bold},
|
||||
Expected: giofont.Font{Typeface: testTF1, Style: giofont.Italic, Weight: giofont.Normal},
|
||||
},
|
||||
{
|
||||
Lookup: Font{Typeface: testTF2, Weight: Normal},
|
||||
Lookup: giofont.Font{Typeface: testTF2, Weight: giofont.Normal},
|
||||
ExpectedToFail: true,
|
||||
},
|
||||
{
|
||||
Lookup: Font{Typeface: testTF2, Style: Italic, Weight: Normal},
|
||||
Lookup: giofont.Font{Typeface: testTF2, Style: giofont.Italic, Weight: giofont.Normal},
|
||||
ExpectedToFail: true,
|
||||
},
|
||||
}
|
||||
|
||||
+2
-1
@@ -7,6 +7,7 @@ import (
|
||||
"hash/maphash"
|
||||
"image"
|
||||
|
||||
giofont "gioui.org/font"
|
||||
"gioui.org/io/system"
|
||||
"gioui.org/op"
|
||||
"gioui.org/op/clip"
|
||||
@@ -156,7 +157,7 @@ type layoutKey struct {
|
||||
str string
|
||||
truncator string
|
||||
locale system.Locale
|
||||
font Font
|
||||
font giofont.Font
|
||||
forceTruncate bool
|
||||
}
|
||||
|
||||
|
||||
+3
-6
@@ -9,6 +9,7 @@ import (
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
giofont "gioui.org/font"
|
||||
"gioui.org/io/system"
|
||||
"gioui.org/op"
|
||||
"gioui.org/op/clip"
|
||||
@@ -19,7 +20,7 @@ import (
|
||||
// Parameters are static text shaping attributes applied to the entire shaped text.
|
||||
type Parameters struct {
|
||||
// Font describes the preferred typeface.
|
||||
Font Font
|
||||
Font giofont.Font
|
||||
// Alignment characterizes the positioning of text within the line. It does not directly
|
||||
// impact shaping, but is provided in order to allow efficient offset computation.
|
||||
Alignment Alignment
|
||||
@@ -44,11 +45,7 @@ type Parameters struct {
|
||||
forceTruncate bool
|
||||
}
|
||||
|
||||
// A FontFace is a Font and a matching Face.
|
||||
type FontFace struct {
|
||||
Font Font
|
||||
Face Face
|
||||
}
|
||||
type FontFace = giofont.FontFace
|
||||
|
||||
// Glyph describes a shaped font glyph. Many fields are distances relative
|
||||
// to the "dot", which is a point on the baseline (the line upon which glyphs
|
||||
|
||||
@@ -6,39 +6,9 @@ import (
|
||||
"fmt"
|
||||
|
||||
"gioui.org/io/system"
|
||||
"github.com/go-text/typesetting/font"
|
||||
"golang.org/x/image/math/fixed"
|
||||
)
|
||||
|
||||
// Style is the font style.
|
||||
type Style int
|
||||
|
||||
// Weight is a font weight, in CSS units subtracted 400 so the zero value
|
||||
// is normal text weight.
|
||||
type Weight int
|
||||
|
||||
// Font specify a particular typeface variant, style and weight.
|
||||
type Font struct {
|
||||
Typeface Typeface
|
||||
Variant Variant
|
||||
Style Style
|
||||
// Weight is the text weight. If zero, Normal is used instead.
|
||||
Weight Weight
|
||||
}
|
||||
|
||||
// Face is an opaque handle to a typeface. The concrete implementation depends
|
||||
// upon the kind of font and shaper in use.
|
||||
type Face interface {
|
||||
Face() font.Face
|
||||
}
|
||||
|
||||
// Typeface identifies a particular typeface design. The empty
|
||||
// string denotes the default typeface.
|
||||
type Typeface string
|
||||
|
||||
// Variant denotes a typeface variant such as "Mono" or "Smallcaps".
|
||||
type Variant string
|
||||
|
||||
type Alignment uint8
|
||||
|
||||
const (
|
||||
@@ -47,31 +17,6 @@ const (
|
||||
Middle
|
||||
)
|
||||
|
||||
const (
|
||||
Regular Style = iota
|
||||
Italic
|
||||
)
|
||||
|
||||
const (
|
||||
Thin Weight = -300
|
||||
ExtraLight Weight = -200
|
||||
Light Weight = -100
|
||||
Normal Weight = 0
|
||||
Medium Weight = 100
|
||||
SemiBold Weight = 200
|
||||
Bold Weight = 300
|
||||
ExtraBold Weight = 400
|
||||
Black Weight = 500
|
||||
|
||||
Hairline = Thin
|
||||
UltraLight = ExtraLight
|
||||
DemiBold = SemiBold
|
||||
UltraBold = ExtraBold
|
||||
Heavy = Black
|
||||
ExtraBlack = Black + 50
|
||||
UltraBlack = ExtraBlack
|
||||
)
|
||||
|
||||
func (a Alignment) String() string {
|
||||
switch a {
|
||||
case Start:
|
||||
@@ -109,41 +54,3 @@ func (a Alignment) Align(dir system.TextDirection, width fixed.Int26_6, maxWidth
|
||||
panic(fmt.Errorf("unknown alignment %v", a))
|
||||
}
|
||||
}
|
||||
|
||||
func (s Style) String() string {
|
||||
switch s {
|
||||
case Regular:
|
||||
return "Regular"
|
||||
case Italic:
|
||||
return "Italic"
|
||||
default:
|
||||
panic("invalid Style")
|
||||
}
|
||||
}
|
||||
|
||||
func (w Weight) String() string {
|
||||
switch w {
|
||||
case Thin:
|
||||
return "Thin"
|
||||
case ExtraLight:
|
||||
return "ExtraLight"
|
||||
case Light:
|
||||
return "Light"
|
||||
case Normal:
|
||||
return "Normal"
|
||||
case Medium:
|
||||
return "Medium"
|
||||
case SemiBold:
|
||||
return "SemiBold"
|
||||
case Bold:
|
||||
return "Bold"
|
||||
case ExtraBold:
|
||||
return "ExtraBold"
|
||||
case Black:
|
||||
return "Black"
|
||||
case ExtraBlack:
|
||||
return "ExtraBlack"
|
||||
default:
|
||||
panic("invalid Weight")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user