mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-06 09:55:40 +00:00
text: add io.Reader Layout method to Shaper
use them for Editor, which is no longer required to construct a string for laying out its content. Signed-off-by: Elias Naur <mail@eliasnaur.com>
This commit is contained in:
+64
-31
@@ -85,8 +85,12 @@ func (c *Collection) Font(i int) (*Font, error) {
|
|||||||
return &Font{font: fnt}, nil
|
return &Font{font: fnt}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Font) Layout(ppem fixed.Int26_6, str string, opts text.LayoutOptions) []text.Line {
|
func (f *Font) Layout(ppem fixed.Int26_6, txt io.Reader, opts text.LayoutOptions) ([]text.Line, error) {
|
||||||
return layoutText(&f.buf, ppem, str, &opentype{Font: f.font, Hinting: font.HintingFull}, opts)
|
glyphs, err := readGlyphs(txt)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return layoutText(&f.buf, ppem, &opentype{Font: f.font, Hinting: font.HintingFull}, glyphs, opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Font) Shape(ppem fixed.Int26_6, str []text.Glyph) op.CallOp {
|
func (f *Font) Shape(ppem fixed.Int26_6, str []text.Glyph) op.CallOp {
|
||||||
@@ -98,59 +102,59 @@ func (f *Font) Metrics(ppem fixed.Int26_6) font.Metrics {
|
|||||||
return o.Metrics(&f.buf, ppem)
|
return o.Metrics(&f.buf, ppem)
|
||||||
}
|
}
|
||||||
|
|
||||||
func layoutText(buf *sfnt.Buffer, ppem fixed.Int26_6, str string, f *opentype, opts text.LayoutOptions) []text.Line {
|
func layoutText(sbuf *sfnt.Buffer, ppem fixed.Int26_6, f *opentype, glyphs []text.Glyph, opts text.LayoutOptions) ([]text.Line, error) {
|
||||||
m := f.Metrics(buf, ppem)
|
m := f.Metrics(sbuf, ppem)
|
||||||
lineTmpl := text.Line{
|
lineTmpl := text.Line{
|
||||||
Ascent: m.Ascent,
|
Ascent: m.Ascent,
|
||||||
// m.Height is equal to m.Ascent + m.Descent + linegap.
|
// m.Height is equal to m.Ascent + m.Descent + linegap.
|
||||||
// Compute the descent including the linegap.
|
// Compute the descent including the linegap.
|
||||||
Descent: m.Height - m.Ascent,
|
Descent: m.Height - m.Ascent,
|
||||||
Bounds: f.Bounds(buf, ppem),
|
Bounds: f.Bounds(sbuf, ppem),
|
||||||
}
|
}
|
||||||
var lines []text.Line
|
var lines []text.Line
|
||||||
maxDotX := fixed.I(opts.MaxWidth)
|
maxDotX := fixed.I(opts.MaxWidth)
|
||||||
type state struct {
|
type state struct {
|
||||||
r rune
|
r rune
|
||||||
layout []text.Glyph
|
adv fixed.Int26_6
|
||||||
adv fixed.Int26_6
|
x fixed.Int26_6
|
||||||
x fixed.Int26_6
|
idx int
|
||||||
idx int
|
len int
|
||||||
valid bool
|
valid bool
|
||||||
}
|
}
|
||||||
var prev, word state
|
var prev, word state
|
||||||
endLine := func() {
|
endLine := func() {
|
||||||
line := lineTmpl
|
line := lineTmpl
|
||||||
line.Layout = prev.layout
|
line.Layout = glyphs[:prev.idx:prev.idx]
|
||||||
line.Len = prev.idx
|
line.Len = prev.len
|
||||||
line.Width = prev.x + prev.adv
|
line.Width = prev.x + prev.adv
|
||||||
line.Bounds.Max.X += prev.x
|
line.Bounds.Max.X += prev.x
|
||||||
lines = append(lines, line)
|
lines = append(lines, line)
|
||||||
str = str[prev.idx:]
|
glyphs = glyphs[prev.idx:]
|
||||||
prev = state{}
|
prev = state{}
|
||||||
word = state{}
|
word = state{}
|
||||||
}
|
}
|
||||||
for prev.idx < len(str) {
|
for prev.idx < len(glyphs) {
|
||||||
c, s := utf8.DecodeRuneInString(str[prev.idx:])
|
g := &glyphs[prev.idx]
|
||||||
a, valid := f.GlyphAdvance(buf, ppem, c)
|
a, valid := f.GlyphAdvance(sbuf, ppem, g.Rune)
|
||||||
next := state{
|
next := state{
|
||||||
r: c,
|
r: g.Rune,
|
||||||
layout: prev.layout,
|
idx: prev.idx + 1,
|
||||||
idx: prev.idx + s,
|
len: prev.len + utf8.RuneLen(g.Rune),
|
||||||
x: prev.x + prev.adv,
|
x: prev.x + prev.adv,
|
||||||
adv: a,
|
adv: a,
|
||||||
valid: valid,
|
valid: valid,
|
||||||
}
|
}
|
||||||
if c == '\n' {
|
if g.Rune == '\n' {
|
||||||
// The newline is zero width; use the previous
|
// The newline is zero width; use the previous
|
||||||
// character for line measurements.
|
// character for line measurements.
|
||||||
prev.layout = append(prev.layout, text.Glyph{Rune: c, Advance: 0})
|
|
||||||
prev.idx = next.idx
|
prev.idx = next.idx
|
||||||
|
prev.len = next.len
|
||||||
endLine()
|
endLine()
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
var k fixed.Int26_6
|
var k fixed.Int26_6
|
||||||
if prev.valid {
|
if prev.valid {
|
||||||
k = f.Kern(buf, ppem, prev.r, next.r)
|
k = f.Kern(sbuf, ppem, prev.r, next.r)
|
||||||
}
|
}
|
||||||
// Break the line if we're out of space.
|
// Break the line if we're out of space.
|
||||||
if prev.idx > 0 && next.x+next.adv+k > maxDotX {
|
if prev.idx > 0 && next.x+next.adv+k > maxDotX {
|
||||||
@@ -160,21 +164,21 @@ func layoutText(buf *sfnt.Buffer, ppem fixed.Int26_6, str string, f *opentype, o
|
|||||||
}
|
}
|
||||||
next.x -= word.x + word.adv
|
next.x -= word.x + word.adv
|
||||||
next.idx -= word.idx
|
next.idx -= word.idx
|
||||||
next.layout = next.layout[len(word.layout):]
|
next.len -= word.len
|
||||||
prev = word
|
prev = word
|
||||||
endLine()
|
endLine()
|
||||||
} else if k != 0 {
|
} else if k != 0 {
|
||||||
next.layout[len(next.layout)-1].Advance += k
|
glyphs[prev.idx-1].Advance += k
|
||||||
next.x += k
|
next.x += k
|
||||||
}
|
}
|
||||||
next.layout = append(next.layout, text.Glyph{Rune: c, Advance: next.adv})
|
g.Advance = next.adv
|
||||||
if unicode.IsSpace(c) {
|
if unicode.IsSpace(g.Rune) {
|
||||||
word = next
|
word = next
|
||||||
}
|
}
|
||||||
prev = next
|
prev = next
|
||||||
}
|
}
|
||||||
endLine()
|
endLine()
|
||||||
return lines
|
return lines, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func textPath(buf *sfnt.Buffer, ppem fixed.Int26_6, f *opentype, str []text.Glyph) op.CallOp {
|
func textPath(buf *sfnt.Buffer, ppem fixed.Int26_6, f *opentype, str []text.Glyph) op.CallOp {
|
||||||
@@ -237,6 +241,35 @@ func textPath(buf *sfnt.Buffer, ppem fixed.Int26_6, f *opentype, str []text.Glyp
|
|||||||
return op.CallOp{Ops: ops}
|
return op.CallOp{Ops: ops}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func readGlyphs(r io.Reader) ([]text.Glyph, error) {
|
||||||
|
var glyphs []text.Glyph
|
||||||
|
buf := make([]byte, 0, 1024)
|
||||||
|
for {
|
||||||
|
n, err := r.Read(buf[len(buf):cap(buf)])
|
||||||
|
buf = buf[:len(buf)+n]
|
||||||
|
lim := len(buf)
|
||||||
|
// Read full runes if possible.
|
||||||
|
if err != io.EOF {
|
||||||
|
lim -= utf8.UTFMax - 1
|
||||||
|
}
|
||||||
|
i := 0
|
||||||
|
for i < lim {
|
||||||
|
c, s := utf8.DecodeRune(buf[i:])
|
||||||
|
i += s
|
||||||
|
glyphs = append(glyphs, text.Glyph{Rune: c})
|
||||||
|
}
|
||||||
|
n = copy(buf, buf[i:])
|
||||||
|
buf = buf[:n]
|
||||||
|
if err != nil {
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return glyphs, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (f *opentype) GlyphAdvance(buf *sfnt.Buffer, ppem fixed.Int26_6, r rune) (advance fixed.Int26_6, ok bool) {
|
func (f *opentype) GlyphAdvance(buf *sfnt.Buffer, ppem fixed.Int26_6, r rune) (advance fixed.Int26_6, ok bool) {
|
||||||
g, err := f.Font.GlyphIndex(buf, r)
|
g, err := f.Font.GlyphIndex(buf, r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
+33
-8
@@ -3,6 +3,9 @@
|
|||||||
package text
|
package text
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"golang.org/x/image/font"
|
"golang.org/x/image/font"
|
||||||
|
|
||||||
"gioui.org/op"
|
"gioui.org/op"
|
||||||
@@ -13,17 +16,27 @@ import (
|
|||||||
// Shaper implements layout and shaping of text.
|
// Shaper implements layout and shaping of text.
|
||||||
type Shaper interface {
|
type Shaper interface {
|
||||||
// Layout a text according to a set of options.
|
// Layout a text according to a set of options.
|
||||||
Layout(c unit.Converter, font Font, str string, opts LayoutOptions) []Line
|
Layout(c unit.Converter, font Font, txt io.Reader, opts LayoutOptions) ([]Line, error)
|
||||||
// Shape a line of text previously laid out by Layout.
|
// Shape a line of text and return a clipping operation for its outline.
|
||||||
Shape(c unit.Converter, font Font, str string, layout []Glyph) op.CallOp
|
Shape(c unit.Converter, font Font, layout []Glyph) op.CallOp
|
||||||
|
|
||||||
|
// LayoutString is like Layout, but for strings..
|
||||||
|
LayoutString(c unit.Converter, font Font, str string, opts LayoutOptions) []Line
|
||||||
|
// ShapeString is like Shape for lines previously laid out by LayoutString.
|
||||||
|
ShapeString(c unit.Converter, font Font, str string, layout []Glyph) op.CallOp
|
||||||
|
|
||||||
|
// Metrics returns the font metrics for font.
|
||||||
Metrics(c unit.Converter, font Font) font.Metrics
|
Metrics(c unit.Converter, font Font) font.Metrics
|
||||||
}
|
}
|
||||||
|
|
||||||
// FontRegistry implements layout and shaping of text and a cache of
|
// FontRegistry implements layout and shaping of text from a set of
|
||||||
// computed results.
|
// registered fonts.
|
||||||
//
|
//
|
||||||
// If a font matches no registered shape, FontRegistry falls back to the
|
// If a font matches no registered shape, FontRegistry falls back to the
|
||||||
// first registered face.
|
// first registered face.
|
||||||
|
//
|
||||||
|
// The LayoutString and ShapeString results are cached and re-used if
|
||||||
|
// possible.
|
||||||
type FontRegistry struct {
|
type FontRegistry struct {
|
||||||
def Typeface
|
def Typeface
|
||||||
faces map[Font]*face
|
faces map[Font]*face
|
||||||
@@ -50,12 +63,24 @@ func (s *FontRegistry) Register(font Font, tf Face) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *FontRegistry) Layout(c unit.Converter, font Font, str string, opts LayoutOptions) []Line {
|
func (s *FontRegistry) Layout(c unit.Converter, font Font, txt io.Reader, opts LayoutOptions) ([]Line, error) {
|
||||||
|
tf := s.faceForFont(font)
|
||||||
|
ppem := fixed.I(c.Px(font.Size))
|
||||||
|
return tf.face.Layout(ppem, txt, opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *FontRegistry) Shape(c unit.Converter, font Font, layout []Glyph) op.CallOp {
|
||||||
|
tf := s.faceForFont(font)
|
||||||
|
ppem := fixed.I(c.Px(font.Size))
|
||||||
|
return tf.face.Shape(ppem, layout)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *FontRegistry) LayoutString(c unit.Converter, font Font, str string, opts LayoutOptions) []Line {
|
||||||
tf := s.faceForFont(font)
|
tf := s.faceForFont(font)
|
||||||
return tf.layout(fixed.I(c.Px(font.Size)), str, opts)
|
return tf.layout(fixed.I(c.Px(font.Size)), str, opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *FontRegistry) Shape(c unit.Converter, font Font, str string, layout []Glyph) op.CallOp {
|
func (s *FontRegistry) ShapeString(c unit.Converter, font Font, str string, layout []Glyph) op.CallOp {
|
||||||
tf := s.faceForFont(font)
|
tf := s.faceForFont(font)
|
||||||
return tf.shape(fixed.I(c.Px(font.Size)), str, layout)
|
return tf.shape(fixed.I(c.Px(font.Size)), str, layout)
|
||||||
}
|
}
|
||||||
@@ -108,7 +133,7 @@ func (t *face) layout(ppem fixed.Int26_6, str string, opts LayoutOptions) []Line
|
|||||||
if l, ok := t.layoutCache.Get(lk); ok {
|
if l, ok := t.layoutCache.Get(lk); ok {
|
||||||
return l
|
return l
|
||||||
}
|
}
|
||||||
l := t.face.Layout(ppem, str, opts)
|
l, _ := t.face.Layout(ppem, strings.NewReader(str), opts)
|
||||||
t.layoutCache.Put(lk, l)
|
t.layoutCache.Put(lk, l)
|
||||||
return l
|
return l
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-1
@@ -3,6 +3,8 @@
|
|||||||
package text
|
package text
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"io"
|
||||||
|
|
||||||
"gioui.org/op"
|
"gioui.org/op"
|
||||||
"gioui.org/unit"
|
"gioui.org/unit"
|
||||||
"golang.org/x/image/font"
|
"golang.org/x/image/font"
|
||||||
@@ -54,7 +56,7 @@ type Font struct {
|
|||||||
|
|
||||||
// Face implements text layout and shaping for a particular font.
|
// Face implements text layout and shaping for a particular font.
|
||||||
type Face interface {
|
type Face interface {
|
||||||
Layout(ppem fixed.Int26_6, str string, opts LayoutOptions) []Line
|
Layout(ppem fixed.Int26_6, txt io.Reader, opts LayoutOptions) ([]Line, error)
|
||||||
Shape(ppem fixed.Int26_6, str []Glyph) op.CallOp
|
Shape(ppem fixed.Int26_6, str []Glyph) op.CallOp
|
||||||
Metrics(ppem fixed.Int26_6) font.Metrics
|
Metrics(ppem fixed.Int26_6) font.Metrics
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-5
@@ -95,18 +95,30 @@ func (e *editBuffer) gapLen() int {
|
|||||||
return e.gapend - e.gapstart
|
return e.gapend - e.gapstart
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e *editBuffer) Reset() {
|
||||||
|
e.pos = 0
|
||||||
|
}
|
||||||
|
|
||||||
func (e *editBuffer) Read(p []byte) (int, error) {
|
func (e *editBuffer) Read(p []byte) (int, error) {
|
||||||
if e.pos == e.len() {
|
if e.pos == e.len() {
|
||||||
return 0, io.EOF
|
return 0, io.EOF
|
||||||
}
|
}
|
||||||
var n int
|
var total int
|
||||||
if e.pos < e.gapstart {
|
if e.pos < e.gapstart {
|
||||||
n += copy(p, e.text[e.pos:e.gapstart])
|
n := copy(p, e.text[e.pos:e.gapstart])
|
||||||
p = p[n:]
|
p = p[n:]
|
||||||
|
total += n
|
||||||
|
e.pos += n
|
||||||
}
|
}
|
||||||
n += copy(p, e.text[e.gapend:])
|
if e.pos >= e.gapstart {
|
||||||
e.pos += n
|
n := copy(p, e.text[e.pos+e.gapLen():])
|
||||||
return n, nil
|
total += n
|
||||||
|
e.pos += n
|
||||||
|
}
|
||||||
|
if e.pos > e.len() {
|
||||||
|
panic("hey!")
|
||||||
|
}
|
||||||
|
return total, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *editBuffer) ReadRune() (rune, int, error) {
|
func (e *editBuffer) ReadRune() (rune, int, error) {
|
||||||
|
|||||||
+4
-6
@@ -273,13 +273,11 @@ func (e *Editor) layout(gtx *layout.Context, sh text.Shaper) {
|
|||||||
}
|
}
|
||||||
e.shapes = e.shapes[:0]
|
e.shapes = e.shapes[:0]
|
||||||
for {
|
for {
|
||||||
start, end, layout, off, ok := it.Next()
|
_, _, layout, off, ok := it.Next()
|
||||||
if !ok {
|
if !ok {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
// TODO: remove
|
path := sh.Shape(gtx, e.font, layout)
|
||||||
str := e.rr.String()[start:end]
|
|
||||||
path := sh.Shape(gtx, e.font, str, layout)
|
|
||||||
e.shapes = append(e.shapes, line{off, path})
|
e.shapes = append(e.shapes, line{off, path})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -433,9 +431,9 @@ func (e *Editor) moveCoord(c unit.Converter, pos image.Point) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (e *Editor) layoutText(c unit.Converter, s text.Shaper, font text.Font) ([]text.Line, layout.Dimensions) {
|
func (e *Editor) layoutText(c unit.Converter, s text.Shaper, font text.Font) ([]text.Line, layout.Dimensions) {
|
||||||
txt := e.rr.String()
|
e.rr.Reset()
|
||||||
opts := text.LayoutOptions{MaxWidth: e.maxWidth}
|
opts := text.LayoutOptions{MaxWidth: e.maxWidth}
|
||||||
lines := s.Layout(c, font, txt, opts)
|
lines, _ := s.Layout(c, font, &e.rr, opts)
|
||||||
dims := linesDimens(lines)
|
dims := linesDimens(lines)
|
||||||
for i := 0; i < len(lines)-1; i++ {
|
for i := 0; i < len(lines)-1; i++ {
|
||||||
// To avoid layout flickering while editing, assume a soft newline takes
|
// To avoid layout flickering while editing, assume a soft newline takes
|
||||||
|
|||||||
+2
-2
@@ -85,7 +85,7 @@ func (l *lineIterator) Next() (int, int, []text.Glyph, f32.Point, bool) {
|
|||||||
|
|
||||||
func (l Label) Layout(gtx *layout.Context, s text.Shaper, font text.Font, txt string) {
|
func (l Label) Layout(gtx *layout.Context, s text.Shaper, font text.Font, txt string) {
|
||||||
cs := gtx.Constraints
|
cs := gtx.Constraints
|
||||||
lines := s.Layout(gtx, font, txt, text.LayoutOptions{MaxWidth: cs.Width.Max})
|
lines := s.LayoutString(gtx, font, txt, text.LayoutOptions{MaxWidth: cs.Width.Max})
|
||||||
if max := l.MaxLines; max > 0 && len(lines) > max {
|
if max := l.MaxLines; max > 0 && len(lines) > max {
|
||||||
lines = lines[:max]
|
lines = lines[:max]
|
||||||
}
|
}
|
||||||
@@ -109,7 +109,7 @@ func (l Label) Layout(gtx *layout.Context, s text.Shaper, font text.Font, txt st
|
|||||||
stack.Push(gtx.Ops)
|
stack.Push(gtx.Ops)
|
||||||
op.TransformOp{}.Offset(off).Add(gtx.Ops)
|
op.TransformOp{}.Offset(off).Add(gtx.Ops)
|
||||||
str := txt[start:end]
|
str := txt[start:end]
|
||||||
s.Shape(gtx, font, str, layout).Add(gtx.Ops)
|
s.ShapeString(gtx, font, str, layout).Add(gtx.Ops)
|
||||||
paint.PaintOp{Rect: lclip}.Add(gtx.Ops)
|
paint.PaintOp{Rect: lclip}.Add(gtx.Ops)
|
||||||
stack.Pop()
|
stack.Pop()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user