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:
+918
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user