chore(v2): vendor dependencies for offline/China builds

go mod vendor pins onnxruntime_go v1.12.1, Gio and the rest into v2/vendor
so go run/build work without hitting proxy.golang.org (blocked/slow in
China). Verified: CGO_ENABLED=1 go build -mod=vendor ./internal/spike and
GOOS=windows go build -mod=vendor ./internal/ui both pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
ila
2026-07-23 16:35:01 +08:00
co-authored by Claude Opus 4.8
parent 97c1c4a974
commit f58728cddd
972 changed files with 597802 additions and 0 deletions
+246
View File
@@ -0,0 +1,246 @@
package text
import (
"fmt"
"strings"
"unicode"
"unicode/utf8"
)
type tokenKind uint8
const (
tokenStr tokenKind = iota
tokenComma
tokenEOF
)
type token struct {
kind tokenKind
value string
}
func (t token) String() string {
switch t.kind {
case tokenStr:
return t.value
case tokenComma:
return ","
case tokenEOF:
return "EOF"
default:
return "unknown"
}
}
type lexState func(*lexer) lexState
func lexText(l *lexer) lexState {
for {
switch r := l.next(); {
case r == -1:
l.ignore()
l.emit(tokenEOF)
return nil
case unicode.IsSpace(r):
continue
case r == ',':
l.ignore()
l.emit(tokenComma)
case r == '"':
l.ignore()
return lexDquote
case r == '\'':
l.ignore()
return lexSquote
default:
return lexBareStr
}
}
}
func lexBareStr(l *lexer) lexState {
defer l.emitProcessed(tokenStr, func(s string) (string, error) {
return strings.TrimSpace(s), nil
})
for {
if strings.HasPrefix(l.input[l.pos:], `,`) {
return lexText
}
switch r := l.next(); {
case r == -1:
return lexText
}
}
}
func lexDquote(l *lexer) lexState {
return lexQuote(l, `"`)
}
func lexSquote(l *lexer) lexState {
return lexQuote(l, `'`)
}
func unescape(s string, quote rune) (string, error) {
var b strings.Builder
hitNonSpace := false
var wb strings.Builder
for i := 0; i < len(s); {
r, sz := utf8.DecodeRuneInString(s[i:])
i += sz
if unicode.IsSpace(r) {
if !hitNonSpace {
continue
}
wb.WriteRune(r)
continue
}
hitNonSpace = true
// If we get here, we're not looking at whitespace.
// Insert any buffered up whitespace characters from
// the gap between words.
b.WriteString(wb.String())
wb.Reset()
if r == '\\' {
r, sz := utf8.DecodeRuneInString(s[i:])
i += sz
switch r {
case '\\', quote:
b.WriteRune(r)
default:
return "", fmt.Errorf("illegal escape sequence \\%c", r)
}
} else {
b.WriteRune(r)
}
}
return b.String(), nil
}
func lexQuote(l *lexer, mark string) lexState {
escaping := false
for {
if isQuote := strings.HasPrefix(l.input[l.pos:], mark); isQuote && !escaping {
err := l.emitProcessed(tokenStr, func(s string) (string, error) {
return unescape(s, []rune(mark)[0])
})
if err != nil {
l.err = err
return nil
}
l.next()
l.ignore()
return lexText
}
escaped := escaping
switch r := l.next(); {
case r == -1:
l.err = fmt.Errorf("unexpected EOF while parsing %s-quoted family", mark)
return lexText
case r == '\\':
if !escaped {
escaping = true
}
}
if escaped {
escaping = false
}
}
}
type lexer struct {
input string
pos int
tokens []token
err error
}
func (l *lexer) ignore() {
l.input = l.input[l.pos:]
l.pos = 0
}
// next decodes the next rune in the input and returns it.
func (l *lexer) next() int32 {
if l.pos >= len(l.input) {
return -1
}
r, w := utf8.DecodeRuneInString(l.input[l.pos:])
l.pos += w
return r
}
// emit adds a token of the given kind.
func (l *lexer) emit(t tokenKind) {
l.emitProcessed(t, func(s string) (string, error) { return s, nil })
}
// emitProcessed adds a token of the given kind, but transforms its value
// with the provided closure first.
func (l *lexer) emitProcessed(t tokenKind, f func(string) (string, error)) error {
val, err := f(l.input[:l.pos])
l.tokens = append(l.tokens, token{
kind: t,
value: val,
})
l.ignore()
return err
}
// run executes the lexer on the given input.
func (l *lexer) run(input string) ([]token, error) {
l.input = input
l.tokens = l.tokens[:0]
l.pos = 0
for state := lexText; state != nil; {
state = state(l)
}
return l.tokens, l.err
}
// parser implements a simple recursive descent parser for font family fallback
// expressions.
type parser struct {
faces []string
lexer lexer
tokens []token
}
// parse the provided rule and return the extracted font families. The returned families
// are valid only until the next call to parse. If parsing fails, an error describing the
// failure is returned instead.
func (p *parser) parse(rule string) ([]string, error) {
var err error
p.tokens, err = p.lexer.run(rule)
if err != nil {
return nil, err
}
p.faces = p.faces[:0]
return p.faces, p.parseList()
}
// parse implements the production:
//
// LIST ::= <FACE> <COMMA> <LIST> | <FACE>
func (p *parser) parseList() error {
if len(p.tokens) == 0 {
return fmt.Errorf("expected family name, got EOF")
}
if head := p.tokens[0]; head.kind != tokenStr {
return fmt.Errorf("expected family name, got %s", head)
} else {
p.faces = append(p.faces, head.value)
p.tokens = p.tokens[1:]
}
switch head := p.tokens[0]; head.kind {
case tokenEOF:
return nil
case tokenComma:
p.tokens = p.tokens[1:]
return p.parseList()
default:
return fmt.Errorf("unexpected token %s", head)
}
}
+918
View File
@@ -0,0 +1,918 @@
// SPDX-License-Identifier: Unlicense OR MIT
package text
import (
"bytes"
"fmt"
"image"
"io"
"log"
"os"
"slices"
"github.com/go-text/typesetting/di"
"github.com/go-text/typesetting/font"
gotextot "github.com/go-text/typesetting/font/opentype"
"github.com/go-text/typesetting/fontscan"
"github.com/go-text/typesetting/language"
"github.com/go-text/typesetting/shaping"
"golang.org/x/image/math/fixed"
"golang.org/x/text/unicode/bidi"
"gioui.org/f32"
giofont "gioui.org/font"
"gioui.org/font/opentype"
"gioui.org/internal/debug"
"gioui.org/io/system"
"gioui.org/op"
"gioui.org/op/clip"
"gioui.org/op/paint"
)
// document holds a collection of shaped lines and alignment information for
// those lines.
type document struct {
lines []line
alignment Alignment
// alignWidth is the width used when aligning text.
alignWidth int
unreadRuneCount int
}
// append adds the lines of other to the end of l and ensures they
// are aligned to the same width.
func (l *document) append(other document) {
l.lines = append(l.lines, other.lines...)
l.alignWidth = max(l.alignWidth, other.alignWidth)
calculateYOffsets(l.lines)
}
// reset empties the document in preparation to reuse its memory.
func (l *document) reset() {
l.lines = l.lines[:0]
l.alignment = Start
l.alignWidth = 0
l.unreadRuneCount = 0
}
// A line contains the measurements of a line of text.
type line struct {
// runs contains sequences of shaped glyphs with common attributes. The order
// of runs is logical, meaning that the first run will contain the glyphs
// corresponding to the first runes of data in the original text.
runs []runLayout
// visualOrder is a slice of indices into Runs that describes the visual positions
// of each run of text. Iterating this slice and accessing Runs at each
// of the values stored in this slice traverses the runs in proper visual
// order from left to right.
visualOrder []int
// 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
// lineHeight captures the gap that should exist between the baseline of this
// line and the previous (if any).
lineHeight fixed.Int26_6
// direction is the dominant direction of the line. This direction will be
// used to align the text content of the line, but may not match the actual
// direction of the runs of text within the line (such as an RTL sentence
// within an LTR paragraph).
direction system.TextDirection
// runeCount is the number of text runes represented by this line's runs.
runeCount int
yOffset int
}
// insertTrailingSyntheticNewline adds a synthetic newline to the final logical run of the line
// with the given shaping cluster index.
func (l *line) insertTrailingSyntheticNewline(newLineClusterIdx int) {
// If there was a newline at the end of this paragraph, insert a synthetic glyph representing it.
finalContentRun := len(l.runs) - 1
// If there was a trailing newline update the rune counts to include
// it on the last line of the paragraph.
l.runeCount += 1
l.runs[finalContentRun].Runes.Count += 1
syntheticGlyph := glyph{
id: 0,
clusterIndex: newLineClusterIdx,
glyphCount: 0,
runeCount: 1,
advance: 0,
xOffset: 0,
yOffset: 0,
}
// Inset the synthetic newline glyph on the proper end of the run.
if l.runs[finalContentRun].Direction.Progression() == system.FromOrigin {
l.runs[finalContentRun].Glyphs = append(l.runs[finalContentRun].Glyphs, syntheticGlyph)
} else {
// Ensure capacity.
l.runs[finalContentRun].Glyphs = append(l.runs[finalContentRun].Glyphs, glyph{})
copy(l.runs[finalContentRun].Glyphs[1:], l.runs[finalContentRun].Glyphs)
l.runs[finalContentRun].Glyphs[0] = syntheticGlyph
}
}
func (l *line) setTruncatedCount(truncatedCount int) {
// If we've truncated the text with a truncator, adjust the rune counts within the
// truncator to make it represent the truncated text.
finalRunIdx := len(l.runs) - 1
l.runs[finalRunIdx].truncator = true
finalGlyphIdx := len(l.runs[finalRunIdx].Glyphs) - 1
// The run represents all of the truncated text.
l.runs[finalRunIdx].Runes.Count = truncatedCount
// Only the final glyph represents any runes, and it represents all truncated text.
for i := range l.runs[finalRunIdx].Glyphs {
if i == finalGlyphIdx {
l.runs[finalRunIdx].Glyphs[finalGlyphIdx].runeCount = truncatedCount
} else {
l.runs[finalRunIdx].Glyphs[finalGlyphIdx].runeCount = 0
}
}
}
// 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
}
// 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
// advance is the distance the dot moves when laying out the glyph along
// the run's primary axis.
advance fixed.Int26_6
// xOffset and yOffset describe offsets from the dot that should be
// applied when rendering the glyph.
xOffset, yOffset fixed.Int26_6
// bounds describes the visual bounding box of the glyph relative to
// its dot.
bounds fixed.Rectangle26_6
}
type runLayout struct {
// VisualPosition describes the relative position of this run of text within
// its line. It should be a valid index into the containing line's VisualOrder
// slice.
VisualPosition int
// X is the visual offset of the dot for the first glyph in this run
// relative to the beginning of the line.
X fixed.Int26_6
// 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
// Runes describes the position of the text data this layout represents
// within the containing text.Line.
Runes Range
// Advance is the sum of the advances of all clusters in the Layout.
Advance fixed.Int26_6
// PPEM is the pixels-per-em scale used to shape this run.
PPEM fixed.Int26_6
// Direction is the layout direction of the glyphs.
Direction system.TextDirection
// face is the font face that the ID of each Glyph in the Layout refers to.
face *font.Face
// truncator indicates that this run is a text truncator standing in for remaining
// text.
truncator bool
}
// shaperImpl implements the shaping and line-wrapping of opentype fonts.
type shaperImpl struct {
// Fields for tracking fonts/faces.
fontMap *fontscan.FontMap
faces []*font.Face
faceToIndex map[*font.Font]int
faceMeta []giofont.Font
defaultFaces []string
logger interface {
Printf(format string, args ...any)
}
parser parser
// Shaping and wrapping state.
shaper shaping.HarfbuzzShaper
wrapper shaping.LineWrapper
bidiParagraph bidi.Paragraph
// Scratch buffers used to avoid re-allocating slices during routine internal
// shaping operations.
splitScratch1, splitScratch2 []shaping.Input
outScratchBuf []shaping.Output
scratchRunes []rune
// bitmapGlyphCache caches extracted bitmap glyph images.
bitmapGlyphCache bitmapCache
}
// debugLogger only logs messages if debug.Text is true.
type debugLogger struct {
*log.Logger
}
func newDebugLogger() debugLogger {
return debugLogger{Logger: log.New(log.Writer(), "[text] ", log.Default().Flags())}
}
func (d debugLogger) Printf(format string, args ...any) {
if debug.Text.Load() {
d.Logger.Printf(format, args...)
}
}
func newShaperImpl(systemFonts bool, collection []FontFace) *shaperImpl {
var shaper shaperImpl
shaper.logger = newDebugLogger()
shaper.fontMap = fontscan.NewFontMap(shaper.logger)
shaper.faceToIndex = make(map[*font.Font]int)
if systemFonts {
str, err := os.UserCacheDir()
if err != nil {
shaper.logger.Printf("failed resolving font cache dir: %v", err)
shaper.logger.Printf("skipping system font load")
}
if err := shaper.fontMap.UseSystemFonts(str); err != nil {
shaper.logger.Printf("failed loading system fonts: %v", err)
}
}
for _, f := range collection {
shaper.Load(f)
shaper.defaultFaces = append(shaper.defaultFaces, string(f.Font.Typeface))
}
shaper.shaper.SetFontCacheSize(32)
return &shaper
}
// Load registers the provided FontFace with the shaper, if it is compatible.
// It returns whether the face is now available for use. FontFaces are prioritized
// in the order in which they are loaded, with the first face being the default.
func (s *shaperImpl) Load(f FontFace) {
desc := opentype.FontToDescription(f.Font)
face := f.Face.Face()
s.fontMap.AddFace(face, fontscan.Location{File: fmt.Sprint(desc)}, desc)
s.addFace(face, f.Font)
}
func (s *shaperImpl) addFace(f *font.Face, md giofont.Font) {
if _, ok := s.faceToIndex[f.Font]; ok {
return
}
s.logger.Printf("loaded face %s(style:%s, weight:%d)", md.Typeface, md.Style, md.Weight)
idx := len(s.faces)
s.faceToIndex[f.Font] = idx
s.faces = append(s.faces, f)
s.faceMeta = append(s.faceMeta, md)
}
// splitByScript divides the inputs into new, smaller inputs on script boundaries
// and correctly sets the text direction per-script. It will
// use buf as the backing memory for the returned slice if buf is non-nil.
func splitByScript(inputs []shaping.Input, documentDir di.Direction, buf []shaping.Input) []shaping.Input {
var splitInputs []shaping.Input
if buf == nil {
splitInputs = make([]shaping.Input, 0, len(inputs))
} else {
splitInputs = buf
}
for _, input := range inputs {
currentInput := input
if input.RunStart == input.RunEnd {
return []shaping.Input{input}
}
firstNonCommonRune := input.RunStart
for i := firstNonCommonRune; i < input.RunEnd; i++ {
if language.LookupScript(input.Text[i]) != language.Common {
firstNonCommonRune = i
break
}
}
currentInput.Script = language.LookupScript(input.Text[firstNonCommonRune])
for i := firstNonCommonRune + 1; i < input.RunEnd; i++ {
r := input.Text[i]
runeScript := language.LookupScript(r)
if runeScript == language.Common || runeScript == language.Inherited || runeScript == currentInput.Script {
continue
}
if i != input.RunStart {
currentInput.RunEnd = i
splitInputs = append(splitInputs, currentInput)
}
currentInput = input
currentInput.RunStart = i
currentInput.Script = runeScript
// In the future, it may make sense to try to guess the language of the text here as well,
// but this is a complex process.
}
// close and add the last input
currentInput.RunEnd = input.RunEnd
splitInputs = append(splitInputs, currentInput)
}
return splitInputs
}
func (s *shaperImpl) splitBidi(input shaping.Input) []shaping.Input {
var splitInputs []shaping.Input
if input.Direction.Axis() != di.Horizontal || input.RunStart == input.RunEnd {
return []shaping.Input{input}
}
def := bidi.LeftToRight
if input.Direction.Progression() == di.TowardTopLeft {
def = bidi.RightToLeft
}
s.bidiParagraph.SetString(string(input.Text), bidi.DefaultDirection(def))
out, err := s.bidiParagraph.Order()
if err != nil {
return []shaping.Input{input}
}
for i := range out.NumRuns() {
currentInput := input
run := out.Run(i)
dir := run.Direction()
_, endRune := run.Pos()
currentInput.RunEnd = endRune + 1
if dir == bidi.RightToLeft {
currentInput.Direction = di.DirectionRTL
} else {
currentInput.Direction = di.DirectionLTR
}
splitInputs = append(splitInputs, currentInput)
input.RunStart = currentInput.RunEnd
}
return splitInputs
}
// ResolveFace allows shaperImpl to implement shaping.FontMap, wrapping its fontMap
// field and ensuring that any faces loaded as part of the search are registered with
// ids so that they can be referred to by a GlyphID.
func (s *shaperImpl) ResolveFace(r rune) *font.Face {
face := s.fontMap.ResolveFace(r)
if face != nil {
family, aspect := s.fontMap.FontMetadata(face.Font)
md := opentype.DescriptionToFont(font.Description{
Family: family,
Aspect: aspect,
})
s.addFace(face, md)
return face
}
return nil
}
// splitByFaces divides the inputs by font coverage in the provided faces. It will use the slice provided in buf
// as the backing storage of the returned slice if buf is non-nil.
func (s *shaperImpl) splitByFaces(inputs []shaping.Input, buf []shaping.Input) []shaping.Input {
var split []shaping.Input
if buf == nil {
split = make([]shaping.Input, 0, len(inputs))
} else {
split = buf
}
for _, input := range inputs {
split = append(split, shaping.SplitByFace(input, s)...)
}
return split
}
// shapeText invokes the text shaper and returns the raw text data in the shaper's native
// format. It does not wrap lines.
func (s *shaperImpl) shapeText(ppem fixed.Int26_6, lc system.Locale, txt []rune) []shaping.Output {
lcfg := langConfig{
Language: language.NewLanguage(lc.Language),
Direction: mapDirection(lc.Direction),
}
// Create an initial input.
input := toInput(nil, ppem, lcfg, txt)
if input.RunStart == input.RunEnd && len(s.faces) > 0 {
// Give the empty string a face. This is a necessary special case because
// the face splitting process works by resolving faces for each rune, and
// the empty string contains no runes.
input.Face = s.faces[0]
}
// Break input on font glyph coverage.
inputs := s.splitBidi(input)
inputs = s.splitByFaces(inputs, s.splitScratch1[:0])
inputs = splitByScript(inputs, lcfg.Direction, s.splitScratch2[:0])
// Shape all inputs.
if needed := len(inputs) - len(s.outScratchBuf); needed > 0 {
s.outScratchBuf = slices.Grow(s.outScratchBuf, needed)
}
s.outScratchBuf = s.outScratchBuf[:0]
for _, input := range inputs {
if input.Face != nil {
s.outScratchBuf = append(s.outScratchBuf, s.shaper.Shape(input))
} else {
s.outScratchBuf = append(s.outScratchBuf, shaping.Output{
// Use the text size as the advance of the entire fake run so that
// it doesn't occupy zero space.
Advance: input.Size,
Size: input.Size,
Glyphs: []shaping.Glyph{
{
Width: input.Size,
Height: input.Size,
XBearing: 0,
YBearing: 0,
Advance: input.Size,
XOffset: 0,
YOffset: 0,
ClusterIndex: input.RunStart,
RuneCount: input.RunEnd - input.RunStart,
GlyphCount: 1,
GlyphID: 0,
Mask: 0,
},
},
LineBounds: shaping.Bounds{
Ascent: input.Size,
Descent: 0,
Gap: 0,
},
GlyphBounds: shaping.Bounds{
Ascent: input.Size,
Descent: 0,
Gap: 0,
},
Direction: input.Direction,
Runes: shaping.Range{
Offset: input.RunStart,
Count: input.RunEnd - input.RunStart,
},
})
}
}
return s.outScratchBuf
}
func wrapPolicyToGoText(p WrapPolicy) shaping.LineBreakPolicy {
switch p {
case WrapGraphemes:
return shaping.Always
case WrapWords:
return shaping.Never
default:
return shaping.WhenNecessary
}
}
// shapeAndWrapText invokes the text shaper and returns wrapped lines in the shaper's native format.
func (s *shaperImpl) shapeAndWrapText(params Parameters, txt []rune) (_ []shaping.Line, truncated int) {
wc := shaping.WrapConfig{
Direction: mapDirection(params.Locale.Direction),
TruncateAfterLines: params.MaxLines,
TextContinues: params.forceTruncate,
BreakPolicy: wrapPolicyToGoText(params.WrapPolicy),
DisableTrailingWhitespaceTrim: params.DisableSpaceTrim,
}
families := s.defaultFaces
if params.Font.Typeface != "" {
parsed, err := s.parser.parse(string(params.Font.Typeface))
if err != nil {
s.logger.Printf("Unable to parse typeface %q: %v", params.Font.Typeface, err)
} else {
families = parsed
}
}
s.fontMap.SetQuery(fontscan.Query{
Families: families,
Aspect: opentype.FontToDescription(params.Font).Aspect,
})
if wc.TruncateAfterLines > 0 {
if len(params.Truncator) == 0 {
params.Truncator = "…"
}
// We only permit a single run as the truncator, regardless of whether more were generated.
// Just use the first one.
wc.Truncator = s.shapeText(params.PxPerEm, params.Locale, []rune(params.Truncator))[0]
}
// Wrap outputs into lines.
return s.wrapper.WrapParagraph(wc, params.MaxWidth, txt, shaping.NewSliceIterator(s.shapeText(params.PxPerEm, params.Locale, txt)))
}
// replaceControlCharacters replaces problematic unicode
// code points with spaces to ensure proper rune accounting.
func replaceControlCharacters(in []rune) []rune {
for i, r := range in {
switch r {
// ASCII File separator.
case '\u001C':
// ASCII Group separator.
case '\u001D':
// ASCII Record separator.
case '\u001E':
case '\r':
case '\n':
// Unicode "next line" character.
case '\u0085':
// Unicode "paragraph separator".
case '\u2029':
default:
continue
}
in[i] = ' '
}
return in
}
// Layout shapes and wraps the text, and returns the result in Gio's shaped text format.
func (s *shaperImpl) LayoutString(params Parameters, txt string) document {
return s.LayoutRunes(params, []rune(txt))
}
// Layout shapes and wraps the text, and returns the result in Gio's shaped text format.
func (s *shaperImpl) Layout(params Parameters, txt io.RuneReader) document {
s.scratchRunes = s.scratchRunes[:0]
for r, _, err := txt.ReadRune(); err != nil; r, _, err = txt.ReadRune() {
s.scratchRunes = append(s.scratchRunes, r)
}
return s.LayoutRunes(params, s.scratchRunes)
}
func calculateYOffsets(lines []line) {
if len(lines) < 1 {
return
}
// Ceil the first value to ensure that we don't baseline it too close to the top of the
// viewport and cut off the top pixel.
currentY := lines[0].ascent.Ceil()
for i := range lines {
if i > 0 {
currentY += lines[i].lineHeight.Round()
}
lines[i].yOffset = currentY
}
}
// LayoutRunes shapes and wraps the text, and returns the result in Gio's shaped text format.
func (s *shaperImpl) LayoutRunes(params Parameters, txt []rune) document {
hasNewline := len(txt) > 0 && txt[len(txt)-1] == '\n'
var ls []shaping.Line
var truncated int
if hasNewline {
txt = txt[:len(txt)-1]
}
if params.MaxLines != 0 && hasNewline {
// If we might end up truncating a trailing newline, we must insert the truncator symbol
// on the final line (if we hit the limit).
params.forceTruncate = true
}
ls, truncated = s.shapeAndWrapText(params, replaceControlCharacters(txt))
hasTruncator := truncated > 0 || (params.forceTruncate && params.MaxLines == len(ls))
if hasTruncator && hasNewline {
// We have a truncator at the end of the line, so the newline is logically
// truncated as well.
truncated++
hasNewline = false
}
// Convert to Lines.
textLines := make([]line, len(ls))
maxHeight := fixed.Int26_6(0)
for i := range ls {
otLine := toLine(s.faceToIndex, ls[i], params.Locale.Direction)
if otLine.lineHeight > maxHeight {
maxHeight = otLine.lineHeight
}
if isFinalLine := i == len(ls)-1; isFinalLine {
if hasNewline {
otLine.insertTrailingSyntheticNewline(len(txt))
}
if hasTruncator {
otLine.setTruncatedCount(truncated)
}
}
textLines[i] = otLine
}
if params.LineHeight != 0 {
maxHeight = params.LineHeight
}
if params.LineHeightScale == 0 {
params.LineHeightScale = 1.2
}
maxHeight = floatToFixed(fixedToFloat(maxHeight) * params.LineHeightScale)
for i := range textLines {
textLines[i].lineHeight = maxHeight
}
calculateYOffsets(textLines)
return document{
lines: textLines,
alignment: params.Alignment,
alignWidth: alignWidth(params.MinWidth, textLines),
}
}
func alignWidth(minWidth int, lines []line) int {
for _, l := range lines {
minWidth = max(minWidth, l.width.Ceil())
}
return minWidth
}
// Shape converts the provided glyphs into a path. The path will enclose the forms
// of all vector glyphs.
func (s *shaperImpl) Shape(pathOps *op.Ops, gs []Glyph) clip.PathSpec {
var lastPos f32.Point
var x fixed.Int26_6
var builder clip.Path
builder.Begin(pathOps)
for i, g := range gs {
if i == 0 {
x = g.X
}
ppem, faceIdx, gid := splitGlyphID(g.ID)
if faceIdx >= len(s.faces) {
continue
}
face := s.faces[faceIdx]
if face == nil {
continue
}
scaleFactor := fixedToFloat(ppem) / float32(face.Upem())
glyphData := face.GlyphData(gid)
var outline font.GlyphOutline
switch glyphData := glyphData.(type) {
case font.GlyphOutline:
outline = glyphData
case font.GlyphSVG:
outline = glyphData.Outline
default:
continue
}
// Move to glyph position.
pos := f32.Point{
X: fixedToFloat((g.X - x) - g.Offset.X),
Y: -fixedToFloat(g.Offset.Y),
}
builder.Move(pos.Sub(lastPos))
lastPos = pos
var lastArg f32.Point
// Convert fonts.Segments to relative segments.
for _, fseg := range outline.Segments {
nargs := 1
switch fseg.Op {
case gotextot.SegmentOpQuadTo:
nargs = 2
case gotextot.SegmentOpCubeTo:
nargs = 3
}
var args [3]f32.Point
for i := range nargs {
a := f32.Point{
X: fseg.Args[i].X * scaleFactor,
Y: -fseg.Args[i].Y * scaleFactor,
}
args[i] = a.Sub(lastArg)
if i == nargs-1 {
lastArg = a
}
}
switch fseg.Op {
case gotextot.SegmentOpMoveTo:
builder.Move(args[0])
case gotextot.SegmentOpLineTo:
builder.Line(args[0])
case gotextot.SegmentOpQuadTo:
builder.Quad(args[0], args[1])
case gotextot.SegmentOpCubeTo:
builder.Cube(args[0], args[1], args[2])
default:
panic("unsupported segment op")
}
}
lastPos = lastPos.Add(lastArg)
}
return builder.End()
}
func fixedToFloat(i fixed.Int26_6) float32 {
return float32(i) / 64.0
}
func floatToFixed(f float32) fixed.Int26_6 {
return fixed.Int26_6(f * 64)
}
// Bitmaps returns an op.CallOp that will display all bitmap glyphs within gs.
// The positioning of the bitmaps uses the same logic as Shape(), so the returned
// CallOp can be added at the same offset as the path data returned by Shape()
// and will align correctly.
func (s *shaperImpl) Bitmaps(ops *op.Ops, gs []Glyph) op.CallOp {
var x fixed.Int26_6
bitmapMacro := op.Record(ops)
for i, g := range gs {
if i == 0 {
x = g.X
}
_, faceIdx, gid := splitGlyphID(g.ID)
if faceIdx >= len(s.faces) {
continue
}
face := s.faces[faceIdx]
if face == nil {
continue
}
glyphData := face.GlyphData(gid)
switch glyphData := glyphData.(type) {
case font.GlyphBitmap:
var imgOp paint.ImageOp
var imgSize image.Point
bitmapData, ok := s.bitmapGlyphCache.Get(g.ID)
if !ok {
var img image.Image
switch glyphData.Format {
case font.PNG, font.JPG, font.TIFF:
img, _, _ = image.Decode(bytes.NewReader(glyphData.Data))
case font.BlackAndWhite:
// This is a complex family of uncompressed bitmaps that don't seem to be
// very common in practice. We can try adding support later if needed.
fallthrough
default:
// Unknown format.
continue
}
imgOp = paint.NewImageOp(img)
imgSize = img.Bounds().Size()
s.bitmapGlyphCache.Put(g.ID, bitmap{img: imgOp, size: imgSize})
} else {
imgOp = bitmapData.img
imgSize = bitmapData.size
}
off := op.Affine(f32.AffineId().Offset(f32.Point{
X: fixedToFloat((g.X - x) + g.Offset.X),
Y: fixedToFloat(g.Offset.Y + g.Bounds.Min.Y),
})).Push(ops)
cl := clip.Rect{Max: imgSize}.Push(ops)
glyphSize := image.Rectangle{
Min: image.Point{
X: g.Bounds.Min.X.Round(),
Y: g.Bounds.Min.Y.Round(),
},
Max: image.Point{
X: g.Bounds.Max.X.Round(),
Y: g.Bounds.Max.Y.Round(),
},
}.Size()
aff := op.Affine(f32.AffineId().Scale(f32.Point{}, f32.Point{
X: float32(glyphSize.X) / float32(imgSize.X),
Y: float32(glyphSize.Y) / float32(imgSize.Y),
})).Push(ops)
imgOp.Add(ops)
paint.PaintOp{}.Add(ops)
aff.Pop()
cl.Pop()
off.Pop()
}
}
return bitmapMacro.Stop()
}
// langConfig describes the language and writing system of a body of text.
type langConfig struct {
// Language the text is written in.
language.Language
// Writing system used to represent the text.
language.Script
// Direction of the text, usually driven by the writing system.
di.Direction
}
// toInput converts its parameters into a shaping.Input.
func toInput(face *font.Face, ppem fixed.Int26_6, lc langConfig, runes []rune) shaping.Input {
var input shaping.Input
input.Direction = lc.Direction
input.Text = runes
input.Size = ppem
input.Face = face
input.Language = lc.Language
input.Script = lc.Script
input.RunStart = 0
input.RunEnd = len(runes)
return input
}
func mapDirection(d system.TextDirection) di.Direction {
switch d {
case system.LTR:
return di.DirectionLTR
case system.RTL:
return di.DirectionRTL
}
return di.DirectionLTR
}
func unmapDirection(d di.Direction) system.TextDirection {
switch d {
case di.DirectionLTR:
return system.LTR
case di.DirectionRTL:
return system.RTL
}
return system.LTR
}
// toGioGlyphs converts text shaper glyphs into the minimal representation
// that Gio needs.
func toGioGlyphs(in []shaping.Glyph, ppem fixed.Int26_6, faceIdx int) []glyph {
out := make([]glyph, 0, len(in))
for _, g := range in {
// To better understand how to calculate the bounding box, see here:
// https://freetype.org/freetype2/docs/glyphs/glyph-metrics-3.svg
var bounds fixed.Rectangle26_6
bounds.Min.X = g.XBearing
bounds.Min.Y = -g.YBearing
bounds.Max = bounds.Min.Add(fixed.Point26_6{X: g.Width, Y: -g.Height})
out = append(out, glyph{
id: newGlyphID(ppem, faceIdx, g.GlyphID),
clusterIndex: g.TextIndex(),
runeCount: g.RunesCount(),
glyphCount: g.GlyphsCount(),
advance: g.Advance,
xOffset: g.XOffset,
yOffset: g.YOffset,
bounds: bounds,
})
}
return out
}
// toLine converts the output into a Line with the provided dominant text direction.
func toLine(faceToIndex map[*font.Font]int, o shaping.Line, dir system.TextDirection) line {
if len(o) < 1 {
return line{}
}
line := line{
runs: make([]runLayout, len(o)),
direction: dir,
visualOrder: make([]int, len(o)),
}
maxSize := fixed.Int26_6(0)
for i := range o {
run := o[i]
if run.Size > maxSize {
maxSize = run.Size
}
var font *font.Font
if run.Face != nil {
font = run.Face.Font
}
line.runs[i] = runLayout{
Glyphs: toGioGlyphs(run.Glyphs, run.Size, faceToIndex[font]),
Runes: Range{
Count: run.Runes.Count,
Offset: line.runeCount,
},
Direction: unmapDirection(run.Direction),
face: run.Face,
Advance: run.Advance,
PPEM: run.Size,
VisualPosition: int(run.VisualIndex),
}
line.visualOrder[run.VisualIndex] = i
line.runeCount += run.Runes.Count
line.width += run.Advance
if line.ascent < run.LineBounds.Ascent {
line.ascent = run.LineBounds.Ascent
}
if line.descent < -run.LineBounds.Descent+run.LineBounds.Gap {
line.descent = -run.LineBounds.Descent + run.LineBounds.Gap
}
}
line.lineHeight = maxSize
// Iterate and resolve the X of each run.
x := fixed.Int26_6(0)
for _, runIdx := range line.visualOrder {
line.runs[runIdx].X = x
x += line.runs[runIdx].Advance
}
return line
}
+183
View File
@@ -0,0 +1,183 @@
// SPDX-License-Identifier: Unlicense OR MIT
package text
import (
"image"
"sync/atomic"
giofont "gioui.org/font"
"gioui.org/io/system"
"gioui.org/op"
"gioui.org/op/clip"
"gioui.org/op/paint"
"golang.org/x/image/math/fixed"
)
// entry holds a single key-value pair for an LRU cache.
type entry[K comparable, V any] struct {
next, prev *entry[K, V]
key K
v V
}
// lru is a generic least-recently-used cache.
type lru[K comparable, V any] struct {
m map[K]*entry[K, V]
head, tail *entry[K, V]
}
// Get fetches the value associated with the given key, if any.
func (l *lru[K, V]) Get(k K) (V, bool) {
if lt, ok := l.m[k]; ok {
l.remove(lt)
l.insert(lt)
return lt.v, true
}
var v V
return v, false
}
// Put inserts the given value with the given key, evicting old
// cache entries if necessary.
func (l *lru[K, V]) Put(k K, v V) {
if l.m == nil {
l.m = make(map[K]*entry[K, V])
l.head = new(entry[K, V])
l.tail = new(entry[K, V])
l.head.prev = l.tail
l.tail.next = l.head
}
val := &entry[K, V]{key: k, v: v}
l.m[k] = val
l.insert(val)
if len(l.m) > maxSize {
oldest := l.tail.next
l.remove(oldest)
delete(l.m, oldest.key)
}
}
// remove cuts e out of the lru linked list.
func (l *lru[K, V]) remove(e *entry[K, V]) {
e.next.prev = e.prev
e.prev.next = e.next
}
// insert adds e to the lru linked list.
func (l *lru[K, V]) insert(e *entry[K, V]) {
e.next = l.head
e.prev = l.head.prev
e.prev.next = e
e.next.prev = e
}
type bitmapCache = lru[GlyphID, bitmap]
type bitmap struct {
img paint.ImageOp
size image.Point
}
type layoutCache = lru[layoutKey, document]
type glyphValue[V any] struct {
v V
glyphs []glyphInfo
}
type glyphLRU[V any] struct {
seed uint64
cache lru[uint64, glyphValue[V]]
}
var seed uint32
// hashGlyphs computes a hash key based on the ID and X offset of
// every glyph in the slice.
func (c *glyphLRU[V]) hashGlyphs(gs []Glyph) uint64 {
if c.seed == 0 {
c.seed = uint64(atomic.AddUint32(&seed, 3900798947))
}
if len(gs) == 0 {
return 0
}
h := c.seed
firstX := gs[0].X
for _, g := range gs {
h += uint64(g.X - firstX)
h *= 6585573582091643
h += uint64(g.ID)
h *= 3650802748644053
}
return h
}
func (c *glyphLRU[V]) Get(key uint64, gs []Glyph) (V, bool) {
if v, ok := c.cache.Get(key); ok && gidsEqual(v.glyphs, gs) {
return v.v, true
}
var v V
return v, false
}
func (c *glyphLRU[V]) Put(key uint64, glyphs []Glyph, v V) {
gids := make([]glyphInfo, len(glyphs))
firstX := fixed.I(0)
for i, glyph := range glyphs {
if i == 0 {
firstX = glyph.X
}
// Cache glyph X offsets relative to the first glyph.
gids[i] = glyphInfo{ID: glyph.ID, X: glyph.X - firstX}
}
val := glyphValue[V]{
glyphs: gids,
v: v,
}
c.cache.Put(key, val)
}
type pathCache = glyphLRU[clip.PathSpec]
type bitmapShapeCache = glyphLRU[op.CallOp]
type glyphInfo struct {
ID GlyphID
X fixed.Int26_6
}
type layoutKey struct {
ppem fixed.Int26_6
maxWidth, minWidth int
maxLines int
str string
truncator string
locale system.Locale
font giofont.Font
forceTruncate bool
wrapPolicy WrapPolicy
lineHeight fixed.Int26_6
lineHeightScale float32
}
const maxSize = 1000
func gidsEqual(a []glyphInfo, glyphs []Glyph) bool {
if len(a) != len(glyphs) {
return false
}
firstX := fixed.Int26_6(0)
for i := range a {
if i == 0 {
firstX = glyphs[i].X
}
// Cache glyph X offsets relative to the first glyph.
if a[i].ID != glyphs[i].ID || a[i].X != (glyphs[i].X-firstX) {
return false
}
}
return true
}
+612
View File
@@ -0,0 +1,612 @@
// SPDX-License-Identifier: Unlicense OR MIT
package text
import (
"bufio"
"io"
"strings"
"unicode/utf8"
giofont "gioui.org/font"
"gioui.org/io/system"
"gioui.org/op"
"gioui.org/op/clip"
"github.com/go-text/typesetting/font"
"golang.org/x/image/math/fixed"
)
// WrapPolicy configures strategies for choosing where to break lines of text for line
// wrapping.
type WrapPolicy uint8
const (
// WrapHeuristically tries to minimize breaking within words (UAX#14 text segments)
// while also ensuring that text fits within the given MaxWidth. It will only break
// a line within a word (on a UAX#29 grapheme cluster boundary) when that word cannot
// fit on a line by itself. Additionally, when the final word of a line is being
// truncated, this policy will preserve as many symbols of that word as
// possible before the truncator.
WrapHeuristically WrapPolicy = iota
// WrapWords does not permit words (UAX#14 text segments) to be broken across lines.
// This means that sometimes long words will exceed the MaxWidth they are wrapped with.
WrapWords
// WrapGraphemes will maximize the amount of text on each line at the expense of readability,
// breaking any word across lines on UAX#29 grapheme cluster boundaries to maximize the number of
// grapheme clusters on each line.
WrapGraphemes
)
// Parameters are static text shaping attributes applied to the entire shaped text.
type Parameters struct {
// Font describes the preferred typeface.
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
// PxPerEm is the pixels-per-em to shape the text with.
PxPerEm fixed.Int26_6
// MaxLines limits the quantity of shaped lines. Zero means no limit.
MaxLines int
// Truncator is a string of text to insert where the shaped text was truncated, which
// can currently ohly happen if MaxLines is nonzero and the text on the final line is
// truncated.
Truncator string
// WrapPolicy configures how line breaks will be chosen when wrapping text across lines.
WrapPolicy WrapPolicy
// MinWidth and MaxWidth provide the minimum and maximum horizontal space constraints
// for the shaped text.
MinWidth, MaxWidth int
// Locale provides primary direction and language information for the shaped text.
Locale system.Locale
// LineHeightScale is a scaling factor applied to the LineHeight of a paragraph. If zero, a default
// value of 1.2 will be used.
LineHeightScale float32
// LineHeight is the distance between the baselines of two lines of text. If zero, the PxPerEm
// of the any given paragraph will set the LineHeight of that paragraph. This value will be
// scaled by LineHeightScale, so applications desiring a specific fixed value
// should set LineHeightScale to 1.
LineHeight fixed.Int26_6
// forceTruncate controls whether the truncator string is inserted on the final line of
// text with a MaxLines. It is unexported because this behavior only makes sense for the
// shaper to control when it iterates paragraphs of text.
forceTruncate bool
// DisableSpaceTrim prevents the width of the final whitespace glyph on a line from being zeroed.
// This is desirable for text editors (so that the whitespace can be selected), but is undesirable
// for ordinary display text.
DisableSpaceTrim bool
}
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
// visually rest) for the line of text containing the glyph.
//
// Glyphs are organized into "glyph clusters," which are sequences that
// may represent an arbitrary number of runes.
//
// Sequences of glyph clusters that share style parameters are grouped into "runs."
//
// "Document coordinates" are pixel values relative to the text's origin at (0,0)
// in the upper-left corner" Displaying each shaped glyph at the document
// coordinates of its dot will correctly visualize the text.
type Glyph struct {
// ID is a unique, per-shaper identifier for the shape of the glyph.
// Glyphs from the same shaper will share an ID when they are from
// the same face and represent the same glyph at the same size.
ID GlyphID
// X is the x coordinate of the dot for this glyph in document coordinates.
X fixed.Int26_6
// Y is the y coordinate of the dot for this glyph in document coordinates.
Y int32
// Advance is the logical width of the glyph. The glyph may be visually
// wider than this.
Advance fixed.Int26_6
// Ascent is the distance from the dot to the logical top of glyphs in
// this glyph's face. The specific glyph may be shorter than this.
Ascent fixed.Int26_6
// Descent is the distance from the dot to the logical bottom of glyphs
// in this glyph's face. The specific glyph may descend less than this.
Descent fixed.Int26_6
// Offset encodes the origin of the drawing coordinate space for this glyph
// relative to the dot. This value is used when converting glyphs to paths.
Offset fixed.Point26_6
// Bounds encodes the visual dimensions of the glyph relative to the dot.
Bounds fixed.Rectangle26_6
// Runes is the number of runes represented by the glyph cluster this glyph
// belongs to. If Flags does not contain FlagClusterBreak, this value will
// always be zero. The final glyph in the cluster contains the runes count
// for the entire cluster.
Runes uint16
// Flags encode special properties of this glyph.
Flags Flags
}
type Flags uint16
const (
// FlagTowardOrigin is set for glyphs in runs that flow
// towards the origin (RTL).
FlagTowardOrigin Flags = 1 << iota
// FlagLineBreak is set for the last glyph in a line.
FlagLineBreak
// FlagRunBreak is set for the last glyph in a run. A run is a sequence of
// glyphs sharing constant style properties (same size, same face, same
// direction, etc...).
FlagRunBreak
// FlagClusterBreak is set for the last glyph in a glyph cluster. A glyph cluster is a
// sequence of glyphs which are logically a single unit, but require multiple
// symbols from a font to display.
FlagClusterBreak
// FlagParagraphBreak indicates that the glyph cluster does not represent actual
// font glyphs, but was inserted by the shaper to represent line-breaking
// whitespace characters. After a glyph with FlagParagraphBreak set, the shaper
// will always return a glyph with FlagParagraphStart providing the X and Y
// coordinates of the start of the next line, even if that line has no contents.
FlagParagraphBreak
// FlagParagraphStart indicates that the glyph starts a new paragraph.
FlagParagraphStart
// FlagTruncator indicates that the glyph is part of a special truncator run that
// represents the portion of text removed due to truncation. A glyph with both
// FlagTruncator and FlagClusterBreak will have a Runes field accounting for all
// runes truncated.
FlagTruncator
)
func (f Flags) String() string {
var b strings.Builder
if f&FlagParagraphStart != 0 {
b.WriteString("S")
} else {
b.WriteString("_")
}
if f&FlagParagraphBreak != 0 {
b.WriteString("P")
} else {
b.WriteString("_")
}
if f&FlagTowardOrigin != 0 {
b.WriteString("T")
} else {
b.WriteString("_")
}
if f&FlagLineBreak != 0 {
b.WriteString("L")
} else {
b.WriteString("_")
}
if f&FlagRunBreak != 0 {
b.WriteString("R")
} else {
b.WriteString("_")
}
if f&FlagClusterBreak != 0 {
b.WriteString("C")
} else {
b.WriteString("_")
}
if f&FlagTruncator != 0 {
b.WriteString("…")
} else {
b.WriteString("_")
}
return b.String()
}
type GlyphID uint64
// Shaper converts strings of text into glyphs that can be displayed. The same
// Shaper should not be used in different goroutines.
//
// The Shaper controls text layout and has a cache, implemented as a map, and
// so laying out text in two different goroutines can easily result in
// concurrent access to said map, resulting in a panic.
//
// Practically speaking, this means you should use different Shapers for
// different top-level windows.
type Shaper struct {
config struct {
disableSystemFonts bool
collection []FontFace
}
initialized bool
shaper shaperImpl
pathCache pathCache
bitmapShapeCache bitmapShapeCache
layoutCache layoutCache
reader *bufio.Reader
paragraph []byte
// Iterator state.
brokeParagraph bool
pararagraphStart Glyph
txt document
line int
run int
glyph int
// advance is the width of glyphs from the current run that have already been displayed.
advance fixed.Int26_6
// done tracks whether iteration is over.
done bool
err error
}
// ShaperOptions configure text shapers.
type ShaperOption func(*Shaper)
// NoSystemFonts can be used to disable system font loading.
func NoSystemFonts() ShaperOption {
return func(s *Shaper) {
s.config.disableSystemFonts = true
}
}
// WithCollection can be used to provide a collection of pre-loaded fonts to the shaper.
func WithCollection(collection []FontFace) ShaperOption {
return func(s *Shaper) {
s.config.collection = collection
}
}
// NewShaper constructs a shaper with the provided options.
func NewShaper(options ...ShaperOption) *Shaper {
l := &Shaper{}
for _, opt := range options {
opt(l)
}
l.init()
return l
}
func (l *Shaper) init() {
if l.initialized {
return
}
l.initialized = true
l.reader = bufio.NewReader(nil)
l.shaper = *newShaperImpl(!l.config.disableSystemFonts, l.config.collection)
}
// Layout text from an io.Reader according to a set of options. Results can be retrieved by
// iteratively calling NextGlyph.
func (l *Shaper) Layout(params Parameters, txt io.Reader) {
l.init()
l.layoutText(params, txt, "")
}
// LayoutString is Layout for strings.
func (l *Shaper) LayoutString(params Parameters, str string) {
l.init()
l.layoutText(params, nil, str)
}
func (l *Shaper) reset(align Alignment) {
l.line, l.run, l.glyph, l.advance = 0, 0, 0, 0
l.done = false
l.txt.reset()
l.txt.alignment = align
}
// layoutText lays out a large text document by breaking it into paragraphs and laying
// out each of them separately. This allows the shaping results to be cached independently
// by paragraph. Only one of txt and str should be provided.
func (l *Shaper) layoutText(params Parameters, txt io.Reader, str string) {
l.reset(params.Alignment)
if txt == nil && len(str) == 0 {
l.txt.append(l.layoutParagraph(params, "", nil))
return
}
l.reader.Reset(txt)
truncating := params.MaxLines > 0
var done bool
var endByte int
for !done {
l.paragraph = l.paragraph[:0]
if txt != nil {
for {
b, err := l.reader.ReadByte()
if err != nil {
// EOF or any other error ends processing here.
done = true
break
}
l.paragraph = append(l.paragraph, b)
if b == '\n' {
break
}
}
if !done {
_, re := l.reader.ReadByte()
done = re != nil
if !done {
_ = l.reader.UnreadByte()
}
}
} else {
idx := strings.IndexByte(str, '\n')
if idx == -1 {
done = true
endByte = len(str)
} else {
endByte = idx + 1
done = endByte == len(str)
}
}
if len(str[:endByte]) > 0 || (len(l.paragraph) > 0 || len(l.txt.lines) == 0) {
params.forceTruncate = truncating && !done
lines := l.layoutParagraph(params, str[:endByte], l.paragraph)
if truncating {
params.MaxLines -= len(lines.lines)
if params.MaxLines == 0 {
done = true
// We've truncated the text, but we need to account for all of the runes we never
// decoded in the truncator.
var unreadRunes int
if txt == nil {
unreadRunes = utf8.RuneCountInString(str[endByte:])
} else {
for {
_, _, e := l.reader.ReadRune()
if e != nil {
break
}
unreadRunes++
}
}
l.txt.unreadRuneCount = unreadRunes
}
}
l.txt.append(lines)
}
if done {
return
}
str = str[endByte:]
}
}
// layoutParagraph shapes and wraps a paragraph using the provided parameters.
// It accepts the paragraph data in either string or rune format, preferring the
// string in order to hit the shaper cache more quickly.
func (l *Shaper) layoutParagraph(params Parameters, asStr string, asBytes []byte) document {
if l == nil {
return document{}
}
if len(asStr) == 0 && len(asBytes) > 0 {
asStr = string(asBytes)
}
// Alignment is not part of the cache key because changing it does not impact shaping.
lk := layoutKey{
ppem: params.PxPerEm,
maxWidth: params.MaxWidth,
minWidth: params.MinWidth,
maxLines: params.MaxLines,
truncator: params.Truncator,
locale: params.Locale,
font: params.Font,
forceTruncate: params.forceTruncate,
wrapPolicy: params.WrapPolicy,
str: asStr,
lineHeight: params.LineHeight,
lineHeightScale: params.LineHeightScale,
}
if l, ok := l.layoutCache.Get(lk); ok {
return l
}
lines := l.shaper.LayoutRunes(params, []rune(asStr))
l.layoutCache.Put(lk, lines)
return lines
}
// NextGlyph returns the next glyph from the most recent shaping operation, if
// any. If there are no more glyphs, ok will be false.
func (l *Shaper) NextGlyph() (_ Glyph, ok bool) {
l.init()
if l.done {
return Glyph{}, false
}
for {
if l.line == len(l.txt.lines) {
if l.brokeParagraph {
l.brokeParagraph = false
return l.pararagraphStart, true
}
if l.err == nil {
l.err = io.EOF
}
return Glyph{}, false
}
line := l.txt.lines[l.line]
if l.run == len(line.runs) {
l.line++
l.run = 0
continue
}
run := line.runs[l.run]
align := l.txt.alignment.Align(line.direction, line.width, l.txt.alignWidth)
if l.line == 0 && l.run == 0 && len(run.Glyphs) == 0 {
// The very first run is empty, which will only happen when the
// entire text is a shaped empty string. Return a single synthetic
// glyph to provide ascent/descent information to the caller.
l.done = true
return Glyph{
X: align,
Y: int32(line.yOffset),
Runes: 0,
Flags: FlagLineBreak | FlagClusterBreak | FlagRunBreak,
Ascent: line.ascent,
Descent: line.descent,
}, true
}
if l.glyph == len(run.Glyphs) {
l.run++
l.glyph = 0
l.advance = 0
continue
}
glyphIdx := l.glyph
rtl := run.Direction.Progression() == system.TowardOrigin
if rtl {
// If RTL, traverse glyphs backwards to ensure rune order.
glyphIdx = len(run.Glyphs) - 1 - glyphIdx
}
g := run.Glyphs[glyphIdx]
if rtl {
// Modify the advance prior to computing runOffset to ensure that the
// current glyph's width is subtracted in RTL.
l.advance += g.advance
}
// runOffset computes how far into the run the dot should be positioned.
runOffset := l.advance
if rtl {
runOffset = run.Advance - l.advance
}
glyph := Glyph{
ID: g.id,
X: align + run.X + runOffset,
Y: int32(line.yOffset),
Ascent: line.ascent,
Descent: line.descent,
Advance: g.advance,
Runes: uint16(g.runeCount),
Offset: fixed.Point26_6{
X: g.xOffset,
Y: g.yOffset,
},
Bounds: g.bounds,
}
if run.truncator {
glyph.Flags |= FlagTruncator
}
l.glyph++
if !rtl {
l.advance += g.advance
}
endOfRun := l.glyph == len(run.Glyphs)
if endOfRun {
glyph.Flags |= FlagRunBreak
}
endOfLine := endOfRun && l.run == len(line.runs)-1
if endOfLine {
glyph.Flags |= FlagLineBreak
}
endOfText := endOfLine && l.line == len(l.txt.lines)-1
nextGlyph := l.glyph
if rtl {
nextGlyph = len(run.Glyphs) - 1 - nextGlyph
}
endOfCluster := endOfRun || run.Glyphs[nextGlyph].clusterIndex != g.clusterIndex
if run.truncator {
// Only emit a single cluster for the entire truncator sequence.
endOfCluster = endOfRun
}
if endOfCluster {
glyph.Flags |= FlagClusterBreak
if run.truncator {
glyph.Runes += uint16(l.txt.unreadRuneCount)
}
} else {
glyph.Runes = 0
}
if run.Direction.Progression() == system.TowardOrigin {
glyph.Flags |= FlagTowardOrigin
}
if l.brokeParagraph {
glyph.Flags |= FlagParagraphStart
l.brokeParagraph = false
}
if g.glyphCount == 0 {
glyph.Flags |= FlagParagraphBreak
l.brokeParagraph = true
if endOfText {
l.pararagraphStart = Glyph{
Ascent: glyph.Ascent,
Descent: glyph.Descent,
Flags: FlagParagraphStart | FlagLineBreak | FlagRunBreak | FlagClusterBreak,
}
// If a glyph is both a paragraph break and the final glyph, it's a newline
// at the end of the text. We must inform widgets like the text editor
// of a valid cursor position they can use for "after" such a newline,
// taking text alignment into account.
l.pararagraphStart.X = l.txt.alignment.Align(line.direction, 0, l.txt.alignWidth)
l.pararagraphStart.Y = glyph.Y + int32(line.lineHeight.Round())
}
}
return glyph, true
}
}
const (
facebits = 16
sizebits = 16
gidbits = 64 - facebits - sizebits
)
// newGlyphID encodes a face and a glyph id into a GlyphID.
func newGlyphID(ppem fixed.Int26_6, faceIdx int, gid font.GID) GlyphID {
if gid&^((1<<gidbits)-1) != 0 {
panic("glyph id out of bounds")
}
if faceIdx&^((1<<facebits)-1) != 0 {
panic("face index out of bounds")
}
if ppem&^((1<<sizebits)-1) != 0 {
panic("ppem out of bounds")
}
// Mask off the upper 16 bits of ppem. This still allows values up to
// 1023.
ppem &= ((1 << sizebits) - 1)
return GlyphID(faceIdx)<<(gidbits+sizebits) | GlyphID(ppem)<<(gidbits) | GlyphID(gid)
}
// splitGlyphID is the opposite of newGlyphID.
func splitGlyphID(g GlyphID) (fixed.Int26_6, int, font.GID) {
faceIdx := int(uint64(g) >> (gidbits + sizebits))
ppem := fixed.Int26_6((g & ((1<<sizebits - 1) << gidbits)) >> gidbits)
gid := font.GID(g) & (1<<gidbits - 1)
return ppem, faceIdx, gid
}
// Shape converts the provided glyphs into a path. The path will enclose the forms
// of all vector glyphs.
// All glyphs are expected to be from a single line of text (their Y offsets are ignored).
func (l *Shaper) Shape(gs []Glyph) clip.PathSpec {
l.init()
key := l.pathCache.hashGlyphs(gs)
shape, ok := l.pathCache.Get(key, gs)
if ok {
return shape
}
pathOps := new(op.Ops)
shape = l.shaper.Shape(pathOps, gs)
l.pathCache.Put(key, gs, shape)
return shape
}
// Bitmaps extracts bitmap glyphs from the provided slice and creates an op.CallOp to present
// them. The returned op.CallOp will align correctly with the return value of Shape() for the
// same gs slice.
// All glyphs are expected to be from a single line of text (their Y offsets are ignored).
func (l *Shaper) Bitmaps(gs []Glyph) op.CallOp {
l.init()
key := l.bitmapShapeCache.hashGlyphs(gs)
call, ok := l.bitmapShapeCache.Get(key, gs)
if ok {
return call
}
callOps := new(op.Ops)
call = l.shaper.Bitmaps(callOps, gs)
l.bitmapShapeCache.Put(key, gs, call)
return call
}
+56
View File
@@ -0,0 +1,56 @@
// SPDX-License-Identifier: Unlicense OR MIT
package text
import (
"fmt"
"gioui.org/io/system"
"golang.org/x/image/math/fixed"
)
type Alignment uint8
const (
Start Alignment = iota
End
Middle
)
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 (mw - width) / 2
case End:
return (mw - width)
case Start:
return 0
default:
panic(fmt.Errorf("unknown alignment %v", a))
}
}