~eliasnaur/gio

4c220f45541627cf70144f0f674599bf4a7fa0ef — Elias Naur 1 year, 8 months ago dcacca3
text: simplify text layout and shaping API

First, replace LayoutOptions with an explicit maximum width parameter.  The
single-field option struct doesn't carry its weight, and I don't think we'll
see more global layout options in the future. Rather, I expect options to cover
spans of text or be part of a Font.

Second, replace the unit.Converter with an scaled text size. It's simpler and
allow the Editor and similar widgets to easily detect whether their cached
layouts are stale. Package text no longer depends on package unit, which is
now dealt with at the widget-level only.

Finally, remove the Size field from Font. It was a design mistake: a Font is
assumed to cover all sizes, as evidenced by the FontRegistry disregarding
Size when looking up fonts.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
M font/opentype/opentype.go => font/opentype/opentype.go +4 -4
@@ 85,12 85,12 @@ func (c *Collection) Font(i int) (*Font, error) {
	return &Font{font: fnt}, nil
}

func (f *Font) Layout(ppem fixed.Int26_6, txt io.Reader, opts text.LayoutOptions) ([]text.Line, error) {
func (f *Font) Layout(ppem fixed.Int26_6, maxWidth int, txt io.Reader) ([]text.Line, error) {
	glyphs, err := readGlyphs(txt)
	if err != nil {
		return nil, err
	}
	return layoutText(&f.buf, ppem, &opentype{Font: f.font, Hinting: font.HintingFull}, glyphs, opts)
	return layoutText(&f.buf, ppem, maxWidth, &opentype{Font: f.font, Hinting: font.HintingFull}, glyphs)
}

func (f *Font) Shape(ppem fixed.Int26_6, str []text.Glyph) op.CallOp {


@@ 102,7 102,7 @@ func (f *Font) Metrics(ppem fixed.Int26_6) font.Metrics {
	return o.Metrics(&f.buf, ppem)
}

func layoutText(sbuf *sfnt.Buffer, ppem fixed.Int26_6, f *opentype, glyphs []text.Glyph, opts text.LayoutOptions) ([]text.Line, error) {
func layoutText(sbuf *sfnt.Buffer, ppem fixed.Int26_6, maxWidth int, f *opentype, glyphs []text.Glyph) ([]text.Line, error) {
	m := f.Metrics(sbuf, ppem)
	lineTmpl := text.Line{
		Ascent: m.Ascent,


@@ 112,7 112,7 @@ func layoutText(sbuf *sfnt.Buffer, ppem fixed.Int26_6, f *opentype, glyphs []tex
		Bounds:  f.Bounds(sbuf, ppem),
	}
	var lines []text.Line
	maxDotX := fixed.I(opts.MaxWidth)
	maxDotX := fixed.I(maxWidth)
	type state struct {
		r     rune
		adv   fixed.Int26_6

M text/lru.go => text/lru.go +3 -3
@@ 30,9 30,9 @@ type path struct {
}

type layoutKey struct {
	ppem fixed.Int26_6
	str  string
	opts LayoutOptions
	ppem     fixed.Int26_6
	maxWidth int
	str      string
}

type pathKey struct {

M text/shaper.go => text/shaper.go +20 -26
@@ 9,24 9,23 @@ import (
	"golang.org/x/image/font"

	"gioui.org/op"
	"gioui.org/unit"
	"golang.org/x/image/math/fixed"
)

// Shaper implements layout and shaping of text.
type Shaper interface {
	// Layout a text according to a set of options.
	Layout(c unit.Converter, font Font, txt io.Reader, opts LayoutOptions) ([]Line, error)
	Layout(font Font, size fixed.Int26_6, maxWidth int, txt io.Reader) ([]Line, error)
	// Shape a line of text and return a clipping operation for its outline.
	Shape(c unit.Converter, font Font, layout []Glyph) op.CallOp
	Shape(font Font, size fixed.Int26_6, layout []Glyph) op.CallOp

	// LayoutString is like Layout, but for strings..
	LayoutString(c unit.Converter, font Font, str string, opts LayoutOptions) []Line
	LayoutString(font Font, size fixed.Int26_6, maxWidth int, str string) []Line
	// ShapeString is like Shape for lines previously laid out by LayoutString.
	ShapeString(c unit.Converter, font Font, str string, layout []Glyph) op.CallOp
	ShapeString(font Font, size fixed.Int26_6, str string, layout []Glyph) op.CallOp

	// Metrics returns the font metrics for font.
	Metrics(c unit.Converter, font Font) font.Metrics
	Metrics(font Font, size fixed.Int26_6) font.Metrics
}

// FontRegistry implements layout and shaping of text from a set of


@@ 53,8 52,6 @@ func (s *FontRegistry) Register(font Font, tf Face) {
		s.def = font.Typeface
		s.faces = make(map[Font]*face)
	}
	// Treat all font sizes equally.
	font.Size = unit.Value{}
	if font.Weight == 0 {
		font.Weight = Normal
	}


@@ 63,31 60,29 @@ func (s *FontRegistry) Register(font Font, tf Face) {
	}
}

func (s *FontRegistry) Layout(c unit.Converter, font Font, txt io.Reader, opts LayoutOptions) ([]Line, error) {
func (s *FontRegistry) Layout(font Font, size fixed.Int26_6, maxWidth int, txt io.Reader) ([]Line, error) {
	tf := s.faceForFont(font)
	ppem := fixed.I(c.Px(font.Size))
	return tf.face.Layout(ppem, txt, opts)
	return tf.face.Layout(size, maxWidth, txt)
}

func (s *FontRegistry) Shape(c unit.Converter, font Font, layout []Glyph) op.CallOp {
func (s *FontRegistry) Shape(font Font, size fixed.Int26_6, layout []Glyph) op.CallOp {
	tf := s.faceForFont(font)
	ppem := fixed.I(c.Px(font.Size))
	return tf.face.Shape(ppem, layout)
	return tf.face.Shape(size, layout)
}

func (s *FontRegistry) LayoutString(c unit.Converter, font Font, str string, opts LayoutOptions) []Line {
func (s *FontRegistry) LayoutString(font Font, size fixed.Int26_6, maxWidth int, str string) []Line {
	tf := s.faceForFont(font)
	return tf.layout(fixed.I(c.Px(font.Size)), str, opts)
	return tf.layout(size, maxWidth, str)
}

func (s *FontRegistry) ShapeString(c unit.Converter, font Font, str string, layout []Glyph) op.CallOp {
func (s *FontRegistry) ShapeString(font Font, size fixed.Int26_6, str string, layout []Glyph) op.CallOp {
	tf := s.faceForFont(font)
	return tf.shape(fixed.I(c.Px(font.Size)), str, layout)
	return tf.shape(size, str, layout)
}

func (s *FontRegistry) Metrics(c unit.Converter, font Font) font.Metrics {
func (s *FontRegistry) Metrics(font Font, size fixed.Int26_6) font.Metrics {
	tf := s.faceForFont(font)
	return tf.metrics(fixed.I(c.Px(font.Size)))
	return tf.metrics(size)
}

func (s *FontRegistry) faceForStyle(font Font) *face {


@@ 112,7 107,6 @@ func (s *FontRegistry) faceForStyle(font Font) *face {
}

func (s *FontRegistry) faceForFont(font Font) *face {
	font.Size = unit.Value{}
	tf := s.faceForStyle(font)
	if tf == nil {
		font.Typeface = s.def


@@ 121,19 115,19 @@ func (s *FontRegistry) faceForFont(font Font) *face {
	return tf
}

func (t *face) layout(ppem fixed.Int26_6, str string, opts LayoutOptions) []Line {
func (t *face) layout(ppem fixed.Int26_6, maxWidth int, str string) []Line {
	if t == nil {
		return nil
	}
	lk := layoutKey{
		ppem: ppem,
		str:  str,
		opts: opts,
		ppem:     ppem,
		maxWidth: maxWidth,
		str:      str,
	}
	if l, ok := t.layoutCache.Get(lk); ok {
		return l
	}
	l, _ := t.face.Layout(ppem, strings.NewReader(str), opts)
	l, _ := t.face.Layout(ppem, maxWidth, strings.NewReader(str))
	t.layoutCache.Put(lk, l)
	return l
}

M text/text.go => text/text.go +2 -10
@@ 6,7 6,6 @@ import (
	"io"

	"gioui.org/op"
	"gioui.org/unit"
	"golang.org/x/image/font"
	"golang.org/x/image/math/fixed"
)


@@ 32,23 31,16 @@ type Glyph struct {
	Advance fixed.Int26_6
}

// LayoutOptions specify the constraints of a text layout.
type LayoutOptions struct {
	// MaxWidth is the available width of the layout.
	MaxWidth int
}

// Style is the font style.
type Style int

// Weight is a font weight, in CSS units.
type Weight int

// Font specify a particular typeface, style and size.
// Font specify a particular typeface variant, style and weight.
type Font struct {
	Typeface Typeface
	Variant  Variant
	Size     unit.Value
	Style    Style
	// Weight is the text weight. If zero, Normal is used instead.
	Weight Weight


@@ 56,7 48,7 @@ type Font struct {

// Face implements text layout and shaping for a particular font.
type Face interface {
	Layout(ppem fixed.Int26_6, txt io.Reader, opts LayoutOptions) ([]Line, error)
	Layout(ppem fixed.Int26_6, maxWidth int, txt io.Reader) ([]Line, error)
	Shape(ppem fixed.Int26_6, str []Glyph) op.CallOp
	Metrics(ppem fixed.Int26_6) font.Metrics
}

M widget/editor.go => widget/editor.go +9 -7
@@ 36,6 36,7 @@ type Editor struct {
	eventKey     int
	scale        int
	font         text.Font
	textSize     fixed.Int26_6
	blinkStart   time.Time
	focused      bool
	rr           editBuffer


@@ 213,14 214,16 @@ func (e *Editor) Focus() {
}

// Layout lays out the editor.
func (e *Editor) Layout(gtx *layout.Context, sh text.Shaper, font text.Font) {
func (e *Editor) Layout(gtx *layout.Context, sh text.Shaper, font text.Font, size unit.Value) {
	// Flush events from before the previous frame.
	copy(e.events, e.events[e.prevEvents:])
	e.events = e.events[:len(e.events)-e.prevEvents]
	e.prevEvents = len(e.events)
	if e.font != font {
	textSize := fixed.I(gtx.Px(size))
	if e.font != font || e.textSize != textSize {
		e.invalidate()
		e.font = font
		e.textSize = textSize
	}
	e.processEvents(gtx)
	e.layout(gtx, sh)


@@ 245,7 248,7 @@ func (e *Editor) layout(gtx *layout.Context, sh text.Shaper) {
	}

	if !e.valid {
		e.lines, e.dims = e.layoutText(gtx, sh, e.font)
		e.lines, e.dims = e.layoutText(sh)
		e.valid = true
	}



@@ 277,7 280,7 @@ func (e *Editor) layout(gtx *layout.Context, sh text.Shaper) {
		if !ok {
			break
		}
		path := sh.Shape(gtx, e.font, layout)
		path := sh.Shape(e.font, e.textSize, layout)
		e.shapes = append(e.shapes, line{off, path})
	}



@@ 430,10 433,9 @@ func (e *Editor) moveCoord(c unit.Converter, pos image.Point) {
	e.moveToLine(x, carLine)
}

func (e *Editor) layoutText(c unit.Converter, s text.Shaper, font text.Font) ([]text.Line, layout.Dimensions) {
func (e *Editor) layoutText(s text.Shaper) ([]text.Line, layout.Dimensions) {
	e.rr.Reset()
	opts := text.LayoutOptions{MaxWidth: e.maxWidth}
	lines, _ := s.Layout(c, font, &e.rr, opts)
	lines, _ := s.Layout(e.font, e.textSize, e.maxWidth, &e.rr)
	dims := linesDimens(lines)
	for i := 0; i < len(lines)-1; i++ {
		// To avoid layout flickering while editing, assume a soft newline takes

M widget/label.go => widget/label.go +5 -3
@@ 12,6 12,7 @@ import (
	"gioui.org/op"
	"gioui.org/op/paint"
	"gioui.org/text"
	"gioui.org/unit"

	"golang.org/x/image/math/fixed"
)


@@ 83,9 84,10 @@ func (l *lineIterator) Next() (int, int, []text.Glyph, f32.Point, bool) {
	return 0, 0, nil, f32.Point{}, false
}

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, size unit.Value, txt string) {
	cs := gtx.Constraints
	lines := s.LayoutString(gtx, font, txt, text.LayoutOptions{MaxWidth: cs.Width.Max})
	textSize := fixed.I(gtx.Px(size))
	lines := s.LayoutString(font, textSize, cs.Width.Max, txt)
	if max := l.MaxLines; max > 0 && len(lines) > max {
		lines = lines[:max]
	}


@@ 109,7 111,7 @@ func (l Label) Layout(gtx *layout.Context, s text.Shaper, font text.Font, txt st
		stack.Push(gtx.Ops)
		op.TransformOp{}.Offset(off).Add(gtx.Ops)
		str := txt[start:end]
		s.ShapeString(gtx, font, str, layout).Add(gtx.Ops)
		s.ShapeString(font, textSize, str, layout).Add(gtx.Ops)
		paint.PaintOp{Rect: lclip}.Add(gtx.Ops)
		stack.Pop()
	}

M widget/material/button.go => widget/material/button.go +4 -5
@@ 22,6 22,7 @@ type Button struct {
	// Color is the text color.
	Color        color.RGBA
	Font         text.Font
	TextSize     unit.Value
	Background   color.RGBA
	CornerRadius unit.Value
	shaper       text.Shaper


@@ 40,10 41,8 @@ func (t *Theme) Button(txt string) Button {
		Text:       txt,
		Color:      rgb(0xffffff),
		Background: t.Color.Primary,
		Font: text.Font{
			Size: t.TextSize.Scale(14.0 / 16.0),
		},
		shaper: t.Shaper,
		TextSize:   t.TextSize.Scale(14.0 / 16.0),
		shaper:     t.Shaper,
	}
}



@@ 83,7 82,7 @@ func (b Button) Layout(gtx *layout.Context, button *widget.Button) {
			layout.Center.Layout(gtx, func() {
				layout.Inset{Top: unit.Dp(10), Bottom: unit.Dp(10), Left: unit.Dp(12), Right: unit.Dp(12)}.Layout(gtx, func() {
					paint.ColorOp{Color: col}.Add(gtx.Ops)
					widget.Label{}.Layout(gtx, b.shaper, b.Font, b.Text)
					widget.Label{}.Layout(gtx, b.shaper, b.Font, b.TextSize, b.Text)
				})
			})
			pointer.Rect(image.Rectangle{Max: gtx.Dimensions.Size}).Add(gtx.Ops)

M widget/material/checkable.go => widget/material/checkable.go +2 -1
@@ 18,6 18,7 @@ type checkable struct {
	Label              string
	Color              color.RGBA
	Font               text.Font
	TextSize           unit.Value
	IconColor          color.RGBA
	Size               unit.Value
	shaper             text.Shaper


@@ 56,7 57,7 @@ func (c *checkable) layout(gtx *layout.Context, checked bool) {
			layout.W.Layout(gtx, func() {
				layout.UniformInset(unit.Dp(2)).Layout(gtx, func() {
					paint.ColorOp{Color: c.Color}.Add(gtx.Ops)
					widget.Label{}.Layout(gtx, c.shaper, c.Font, c.Label)
					widget.Label{}.Layout(gtx, c.shaper, c.Font, c.TextSize, c.Label)
				})
			})
		}),

M widget/material/checkbox.go => widget/material/checkbox.go +4 -7
@@ 4,7 4,6 @@ package material

import (
	"gioui.org/layout"
	"gioui.org/text"
	"gioui.org/unit"
	"gioui.org/widget"
)


@@ 16,12 15,10 @@ type CheckBox struct {
func (t *Theme) CheckBox(label string) CheckBox {
	return CheckBox{
		checkable{
			Label:     label,
			Color:     t.Color.Text,
			IconColor: t.Color.Primary,
			Font: text.Font{
				Size: t.TextSize.Scale(14.0 / 16.0),
			},
			Label:              label,
			Color:              t.Color.Text,
			IconColor:          t.Color.Primary,
			TextSize:           t.TextSize.Scale(14.0 / 16.0),
			Size:               unit.Dp(26),
			shaper:             t.Shaper,
			checkedStateIcon:   t.checkBoxCheckedIcon,

M widget/material/editor.go => widget/material/editor.go +6 -6
@@ 9,11 9,13 @@ import (
	"gioui.org/op"
	"gioui.org/op/paint"
	"gioui.org/text"
	"gioui.org/unit"
	"gioui.org/widget"
)

type Editor struct {
	Font text.Font
	Font     text.Font
	TextSize unit.Value
	// Color is the text color.
	Color color.RGBA
	// Hint contains the text displayed when the editor is empty.


@@ 26,9 28,7 @@ type Editor struct {

func (t *Theme) Editor(hint string) Editor {
	return Editor{
		Font: text.Font{
			Size: t.TextSize,
		},
		TextSize:  t.TextSize,
		Color:     t.Color.Text,
		shaper:    t.Shaper,
		Hint:      hint,


@@ 43,7 43,7 @@ func (e Editor) Layout(gtx *layout.Context, editor *widget.Editor) {
	macro.Record(gtx.Ops)
	paint.ColorOp{Color: e.HintColor}.Add(gtx.Ops)
	tl := widget.Label{Alignment: editor.Alignment}
	tl.Layout(gtx, e.shaper, e.Font, e.Hint)
	tl.Layout(gtx, e.shaper, e.Font, e.TextSize, e.Hint)
	macro.Stop()
	if w := gtx.Dimensions.Size.X; gtx.Constraints.Width.Min < w {
		gtx.Constraints.Width.Min = w


@@ 51,7 51,7 @@ func (e Editor) Layout(gtx *layout.Context, editor *widget.Editor) {
	if h := gtx.Dimensions.Size.Y; gtx.Constraints.Height.Min < h {
		gtx.Constraints.Height.Min = h
	}
	editor.Layout(gtx, e.shaper, e.Font)
	editor.Layout(gtx, e.shaper, e.Font, e.TextSize)
	if editor.Len() > 0 {
		paint.ColorOp{Color: e.Color}.Add(gtx.Ops)
		editor.PaintText(gtx)

M widget/material/label.go => widget/material/label.go +6 -7
@@ 22,6 22,7 @@ type Label struct {
	// MaxLines limits the number of lines. Zero means no limit.
	MaxLines int
	Text     string
	TextSize unit.Value

	shaper text.Shaper
}


@@ 64,17 65,15 @@ func (t *Theme) Caption(txt string) Label {

func (t *Theme) Label(size unit.Value, txt string) Label {
	return Label{
		Text:  txt,
		Color: t.Color.Text,
		Font: text.Font{
			Size: size,
		},
		shaper: t.Shaper,
		Text:     txt,
		Color:    t.Color.Text,
		TextSize: size,
		shaper:   t.Shaper,
	}
}

func (l Label) Layout(gtx *layout.Context) {
	paint.ColorOp{Color: l.Color}.Add(gtx.Ops)
	tl := widget.Label{Alignment: l.Alignment, MaxLines: l.MaxLines}
	tl.Layout(gtx, l.shaper, l.Font, l.Text)
	tl.Layout(gtx, l.shaper, l.Font, l.TextSize, l.Text)
}

M widget/material/radiobutton.go => widget/material/radiobutton.go +3 -6
@@ 4,7 4,6 @@ package material

import (
	"gioui.org/layout"
	"gioui.org/text"
	"gioui.org/unit"
	"gioui.org/widget"
)


@@ 21,11 20,9 @@ func (t *Theme) RadioButton(key, label string) RadioButton {
		checkable: checkable{
			Label: label,

			Color:     t.Color.Text,
			IconColor: t.Color.Primary,
			Font: text.Font{
				Size: t.TextSize.Scale(14.0 / 16.0),
			},
			Color:              t.Color.Text,
			IconColor:          t.Color.Primary,
			TextSize:           t.TextSize.Scale(14.0 / 16.0),
			Size:               unit.Dp(26),
			shaper:             t.Shaper,
			checkedStateIcon:   t.radioCheckedIcon,