mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-01 07:35:40 +00:00
b7d126e24c
This commit restructures the entire text shaping stack to enable lines of shaped text to
have non-homogeneous properties like which font face they belong to and which direction
a segment of text is going.
The text package now provides a concrete type text.Shaper which can be used to convert
strings into sequences of renderable text.Glyphs. At a high level, the API is used
like this:
// Prepare some fonts.
var collection []text.FontFace
// Make a shaper with those fonts loaded.
shaper := text.NewShaper(collection)
// Shape a string.
shaper.LayoutString(text.Parameters{
PxPerEm: fixed.I(12),
}, 0, 100, system.Locale{}, "Hello")
// Iterate the glyphs from that string.
for glyph, ok := shaper.NextGlyph(); ok; glyph, ok = shaper.NextGlyph() {
// Convert the glyph data into a path. In real uses, convert batches of glyphs
// rather than single glyphs to reduce the number of individual paths and offsets
// required to display your text.
shape := shaper.Shape([]text.Glyph{glyph})
// Offset the glyph to the position it declares within its fields. This will
// automatically handle correct bidirectional text glyph positioning.
offset := op.Offset(image.Pt(glyph.X.Floor(), int(glyph.Y))).Push(gtx.Ops)
// Create a clip area from the shape of the glyph.
area := clip.Outline{Path: shape}.Push(gtx.Ops)
// Paint whatever the current color is within the glyph's shape.
paint.PaintOp{}.Add(gtx.Ops)
area.Pop()
offset.Pop()
}
This API will transparently handle both font fallback (choosing appropriate fonts
from those loaded when the primary font doesn't contain a required glyph) and
bidirectional text (mixed left-to-right and right-to-left text). Glyphs are
iterated in order of the input runes, not their visual order, but proper use
of the provided offsets will ensure that text always displays correctly.
Thanks to Elias Naur for suggesting this glyph iterator strategy. It let us cut
through a lot of accumulated complexity from trying to match our old text APIs,
meaning that this change actually is a net negative change in lines of code.
This commit consumes the upstream github.com/go-text/typesetting/shaping API
now that my prior work is merged there, removing the need for the font/opentype/internal
package entirely.
As part of my efforts, I fuzzed both the low-level text shaping stack and the
editor widget extensively. I've committed regression tests found that way into
the appropriate testdata files to ensure the fuzzer re-checks them.
Fixes: https://todo.sr.ht/~eliasnaur/gio/425
Fixes: https://todo.sr.ht/~eliasnaur/gio/211
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
149 lines
2.9 KiB
Go
149 lines
2.9 KiB
Go
// SPDX-License-Identifier: Unlicense OR MIT
|
|
|
|
package text
|
|
|
|
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 (
|
|
Start Alignment = iota
|
|
End
|
|
Middle
|
|
)
|
|
|
|
const (
|
|
Regular Style = iota
|
|
Italic
|
|
)
|
|
|
|
const (
|
|
Thin Weight = 100 - 400
|
|
Hairline Weight = Thin
|
|
ExtraLight Weight = 200 - 400
|
|
UltraLight Weight = ExtraLight
|
|
Light Weight = 300 - 400
|
|
Normal Weight = 400 - 400
|
|
Medium Weight = 500 - 400
|
|
SemiBold Weight = 600 - 400
|
|
DemiBold Weight = SemiBold
|
|
Bold Weight = 700 - 400
|
|
ExtraBold Weight = 800 - 400
|
|
UltraBold Weight = ExtraBold
|
|
Black Weight = 900 - 400
|
|
Heavy Weight = Black
|
|
ExtraBlack Weight = 950 - 400
|
|
UltraBlack Weight = ExtraBlack
|
|
)
|
|
|
|
func (a Alignment) String() string {
|
|
switch a {
|
|
case Start:
|
|
return "Start"
|
|
case End:
|
|
return "End"
|
|
case Middle:
|
|
return "Middle"
|
|
default:
|
|
panic("invalid Alignment")
|
|
}
|
|
}
|
|
|
|
// Align returns the x offset that should be applied to text with width so that it
|
|
// appears correctly aligned within a space of size maxWidth and with the primary
|
|
// text direction dir.
|
|
func (a Alignment) Align(dir system.TextDirection, width fixed.Int26_6, maxWidth int) fixed.Int26_6 {
|
|
mw := fixed.I(maxWidth)
|
|
if dir.Progression() == system.TowardOrigin {
|
|
switch a {
|
|
case Start:
|
|
a = End
|
|
case End:
|
|
a = Start
|
|
}
|
|
}
|
|
switch a {
|
|
case Middle:
|
|
return fixed.I(((mw - width) / 2).Floor())
|
|
case End:
|
|
return fixed.I((mw - width).Floor())
|
|
case Start:
|
|
return 0
|
|
default:
|
|
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")
|
|
}
|
|
}
|