mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-04 17:05:38 +00:00
font/{gofont,opentype},text,widget{,/material}: [API] add font fallback and bidi support
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>
This commit is contained in:
+29
-135
@@ -3,131 +3,13 @@
|
||||
package text
|
||||
|
||||
import (
|
||||
"io"
|
||||
"fmt"
|
||||
|
||||
"gioui.org/io/system"
|
||||
"gioui.org/op/clip"
|
||||
"github.com/go-text/typesetting/font"
|
||||
"golang.org/x/image/math/fixed"
|
||||
)
|
||||
|
||||
// A Line contains the measurements of a line of text.
|
||||
type Line struct {
|
||||
Layout Layout
|
||||
// Width is the width of the line.
|
||||
Width fixed.Int26_6
|
||||
// Ascent is the height above the baseline.
|
||||
Ascent fixed.Int26_6
|
||||
// Descent is the height below the baseline, including
|
||||
// the line gap.
|
||||
Descent fixed.Int26_6
|
||||
// Bounds is the visible bounds of the line.
|
||||
Bounds fixed.Rectangle26_6
|
||||
}
|
||||
|
||||
// Range describes the position and quantity of a range of text elements
|
||||
// within a larger slice. The unit is usually runes of unicode data or
|
||||
// glyphs of shaped font data.
|
||||
type Range struct {
|
||||
// Count describes the number of items represented by the Range.
|
||||
Count int
|
||||
// Offset describes the start position of the represented
|
||||
// items within a larger list.
|
||||
Offset int
|
||||
}
|
||||
|
||||
// GlyphID uniquely identifies a glyph within a specific font.
|
||||
type GlyphID = font.GID
|
||||
|
||||
// Glyph contains the metadata needed to render a glyph.
|
||||
type Glyph struct {
|
||||
// ID is this glyph's identifier within the font it was shaped with.
|
||||
ID GlyphID
|
||||
// ClusterIndex is the identifier for the text shaping cluster that
|
||||
// this glyph is part of.
|
||||
ClusterIndex int
|
||||
// GlyphCount is the number of glyphs in the same cluster as this glyph.
|
||||
GlyphCount int
|
||||
// RuneCount is the quantity of runes in the source text that this glyph
|
||||
// corresponds to.
|
||||
RuneCount int
|
||||
// XAdvance and YAdvance describe the distance the dot moves when
|
||||
// laying out the glyph on the X or Y axis.
|
||||
XAdvance, YAdvance fixed.Int26_6
|
||||
// XOffset and YOffset describe offsets from the dot that should be
|
||||
// applied when rendering the glyph.
|
||||
XOffset, YOffset fixed.Int26_6
|
||||
}
|
||||
|
||||
// GlyphCluster provides metadata about a sequence of indivisible shaped
|
||||
// glyphs.
|
||||
type GlyphCluster struct {
|
||||
// Advance is the cumulative advance of all glyphs in the cluster.
|
||||
Advance fixed.Int26_6
|
||||
// Runes indicates the position and quantity of the runes represented by
|
||||
// this cluster within the text.
|
||||
Runes Range
|
||||
// Glyphs indicates the position and quantity of the glyphs within this
|
||||
// cluster in a Layout's Glyphs slice.
|
||||
Glyphs Range
|
||||
}
|
||||
|
||||
// RuneWidth returns the effective width of one rune for this cluster.
|
||||
// If the cluster contains multiple runes, the width of the glyphs of
|
||||
// the cluster is divided evenly among the runes.
|
||||
func (c GlyphCluster) RuneWidth() fixed.Int26_6 {
|
||||
if c.Runes.Count == 0 {
|
||||
return 0
|
||||
}
|
||||
return c.Advance / fixed.Int26_6(c.Runes.Count)
|
||||
}
|
||||
|
||||
type Layout struct {
|
||||
// Glyphs are the actual font characters for the text. They are ordered
|
||||
// from left to right regardless of the text direction of the underlying
|
||||
// text.
|
||||
Glyphs []Glyph
|
||||
// Clusters are metadata about the shaped glyphs. They are mostly useful for
|
||||
// interactive text widgets like editors. The order of clusters is logical,
|
||||
// so the first cluster will describe the beginning of the text and may
|
||||
// refer to the final glyphs in the Glyphs field if the text is RTL.
|
||||
Clusters []GlyphCluster
|
||||
// Runes describes the position of the text data this layout represents
|
||||
// within the overall body of text being shaped.
|
||||
Runes Range
|
||||
// Direction is the layout direction of the text.
|
||||
Direction system.TextDirection
|
||||
}
|
||||
|
||||
// Slice returns a layout starting at the glyph cluster index start
|
||||
// and running through the glyph cluster index end. The Offsets field
|
||||
// of the returned layout is adjusted to reflect the new rune range
|
||||
// covered by the layout. The returned layout will have no Clusters.
|
||||
func (l Layout) Slice(start, end int) Layout {
|
||||
if start == end || end == 0 || start == len(l.Clusters) {
|
||||
return Layout{}
|
||||
}
|
||||
newRuneStart := l.Clusters[start].Runes.Offset
|
||||
runesBefore := newRuneStart - l.Runes.Offset
|
||||
endCluster := l.Clusters[end-1]
|
||||
startCluster := l.Clusters[start]
|
||||
runesAfter := l.Runes.Offset + l.Runes.Count - (endCluster.Runes.Offset + endCluster.Runes.Count)
|
||||
|
||||
if l.Direction.Progression() == system.TowardOrigin {
|
||||
startCluster, endCluster = endCluster, startCluster
|
||||
}
|
||||
glyphStart := startCluster.Glyphs.Offset
|
||||
glyphEnd := endCluster.Glyphs.Offset + endCluster.Glyphs.Count
|
||||
|
||||
out := l
|
||||
out.Clusters = nil
|
||||
out.Glyphs = out.Glyphs[glyphStart:glyphEnd]
|
||||
out.Runes.Offset = newRuneStart
|
||||
out.Runes.Count -= runesBefore + runesAfter
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
// Style is the font style.
|
||||
type Style int
|
||||
|
||||
@@ -144,11 +26,10 @@ type Font struct {
|
||||
Weight Weight
|
||||
}
|
||||
|
||||
// Face implements text layout and shaping for a particular font. All
|
||||
// methods must be safe for concurrent use.
|
||||
// Face is an opaque handle to a typeface. The concrete implementation depends
|
||||
// upon the kind of font and shaper in use.
|
||||
type Face interface {
|
||||
Layout(ppem fixed.Int26_6, maxWidth int, lc system.Locale, txt io.RuneReader) ([]Line, error)
|
||||
Shape(ppem fixed.Int26_6, str Layout) clip.PathSpec
|
||||
Face() font.Face
|
||||
}
|
||||
|
||||
// Typeface identifies a particular typeface design. The empty
|
||||
@@ -203,6 +84,31 @@ func (a Alignment) String() string {
|
||||
}
|
||||
}
|
||||
|
||||
// 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:
|
||||
@@ -240,15 +146,3 @@ func (w Weight) String() string {
|
||||
panic("invalid Weight")
|
||||
}
|
||||
}
|
||||
|
||||
// weightDistance returns the distance value between two font weights.
|
||||
func weightDistance(wa Weight, wb Weight) int {
|
||||
// Avoid dealing with negative Weight values.
|
||||
a := int(wa) + 400
|
||||
b := int(wb) + 400
|
||||
diff := a - b
|
||||
if diff < 0 {
|
||||
return -diff
|
||||
}
|
||||
return diff
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user