~eliasnaur/gio

719278bb36d46f2759e6db148559b6e4c11b7d10 — Chris Waldon 5 months ago b7d126e
widget: unify text painting and fix premature termination

This commit unifies all widget text painting to use a single function
and fixes two bugs that could result in visible glyphs failing to be
painted.

The first bug was that we checked whether a particular glyph's
outline was visible within the viewport and terminated iteration the
first time that we found a glyph that wasn't visible. If the very top
of the next line of text was visible within the viewport, taller glyphs
should be painted since part of them is visible. We would stop as soon
as we got to a short glyph, preventing the rest of the line (and any
tall glyphs it contained) from being painted.

I fixed this first problem by using the ascent/descent of the line containing
a glyph to determine whether it's "visible". While this will conclude that
a small glyph is visible when it may be entirely off-screen, the net result
will be that we will paint the entire line containing the glyph rather than
constructing a special version of the line with only the tall glyphs. This
has better path caching performance, as we don't need a bespoke path for when
the line is partially visible.

The second bug was that when the glyph iterator concluded that the
current glyph was out of the viewport, we would immediately terminate
the loop for painting glyphs without painting any buffered glyphs that
had been determined to be visible.

This second bug was easily fixed by ensuring that we always paint all buffered
glyphs when terminating iteration.

As part of this work, I pulled the (fairly complex) logic of buffering and
painting glyphs into the glyph iterator so that label and editor can share
a single implementation.

I was unable to completely encapsulate the array storing buffered glyphs within
the iterator without it being moved to the heap, so the current glyph iteration
API requires the caller to juggle a slice of glyphs. Hopefully someone in
the future can find a structure that the compiler's escape analysis understands.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2 files changed, 83 insertions(+), 69 deletions(-)

M widget/editor.go
M widget/label.go
M widget/editor.go => widget/editor.go +10 -23
@@ 735,9 735,6 @@ func (e *Editor) PaintSelection(gtx layout.Context) {
}

func (e *Editor) PaintText(gtx layout.Context) {
	var gs [32]text.Glyph
	line := gs[:0]
	var lineOff image.Point
	m := op.Record(gtx.Ops)
	viewport := image.Rectangle{
		Min: e.scrollOff,


@@ 752,26 749,15 @@ func (e *Editor) PaintText(gtx layout.Context) {
		}
		startGlyph += line.glyphs
	}
	var glyphs [32]text.Glyph
	line := glyphs[:0]
	for _, g := range e.index.glyphs[startGlyph:] {
		if !it.Glyph(g, true) {
		var ok bool
		if line, ok = it.paintGlyph(gtx, e.shaper, g, line); !ok {
			break
		}
		if it.visible {
			if len(line) == 0 {
				lineOff = image.Point{X: it.g.X.Floor(), Y: int(it.g.Y)}.Sub(e.scrollOff)
			}
			line = append(line, g)
		}
		if g.Flags&text.FlagLineBreak > 0 || cap(line)-len(line) == 0 {
			t := op.Offset(lineOff).Push(gtx.Ops)
			op := clip.Outline{Path: e.shaper.Shape(line)}.Op().Push(gtx.Ops)
			paint.PaintOp{}.Add(gtx.Ops)
			op.Pop()
			t.Pop()
			line = line[:0]
		}

	}

	call := m.Stop()
	viewport.Min = viewport.Min.Add(it.padding.Min)
	viewport.Max = viewport.Max.Add(it.padding.Max)


@@ 906,14 892,15 @@ func (e *Editor) layoutText(lt *text.Shaper) {
			PxPerEm:   e.textSize,
			Alignment: e.Alignment,
		}, 0, e.maxWidth, e.locale, r)
		for it.Glyph(lt.NextGlyph()) {
			e.index.Glyph(it.g)
		for glyph, ok := it.processGlyph(lt.NextGlyph()); ok; glyph, ok = it.processGlyph(lt.NextGlyph()) {
			e.index.Glyph(glyph)
		}
	} else {
		// Make a fake glyph for every rune in the reader.
		for _, _, err := r.ReadRune(); err != io.EOF; _, _, err = r.ReadRune() {
			it.Glyph(text.Glyph{Runes: 1, Flags: text.FlagClusterBreak}, true)
			e.index.Glyph(it.g)
			g, _ := it.processGlyph(text.Glyph{Runes: 1, Flags: text.FlagClusterBreak}, true)
			e.index.Glyph(g)

		}
	}
	dims := layout.Dimensions{Size: it.bounds.Size()}

M widget/label.go => widget/label.go +73 -46
@@ 37,23 37,12 @@ func (l Label) Layout(gtx layout.Context, lt *text.Shaper, font text.Font, size 
	viewport := image.Rectangle{Max: cs.Max}
	it := textIterator{viewport: viewport}
	semantic.LabelOp(txt).Add(gtx.Ops)
	var gs [32]text.Glyph
	line := gs[:0]
	var lineOff image.Point
	for it.Glyph(lt.NextGlyph()) {
		if it.visible {
			if len(line) == 0 {
				lineOff = image.Point{X: it.g.X.Floor(), Y: int(it.g.Y)}
			}
			line = append(line, it.g)
		}
		if it.g.Flags&text.FlagLineBreak > 0 || cap(line)-len(line) == 0 {
			t := op.Offset(lineOff).Push(gtx.Ops)
			op := clip.Outline{Path: lt.Shape(line)}.Op().Push(gtx.Ops)
			paint.PaintOp{}.Add(gtx.Ops)
			op.Pop()
			t.Pop()
			line = line[:0]
	var glyphs [32]text.Glyph
	line := glyphs[:0]
	for g, ok := lt.NextGlyph(); ok; g, ok = lt.NextGlyph() {
		var ok bool
		if line, ok = it.paintGlyph(gtx, lt, g, line); !ok {
			break
		}
	}
	call := m.Stop()


@@ 72,52 61,90 @@ func r2p(r clip.Rect) clip.Op {
	return clip.Stroke{Path: r.Path(), Width: 1}.Op()
}

// textIterator computes the bounding box of and paints text.
type textIterator struct {
	g        text.Glyph
	// viewport is the rectangle of document coordinates that the iterator is
	// trying to fill with text.
	viewport image.Rectangle
	padding  image.Rectangle
	bounds   image.Rectangle
	visible  bool
	first    bool

	// lineOff tracks the origin for the glyphs in the current line.
	lineOff image.Point
	// padding is the space needed outside of the bounds of the text to ensure no
	// part of a glyph is clipped.
	padding image.Rectangle
	// bounds is the logical bounding box of the text.
	bounds image.Rectangle
	// visible tracks whether the most recently iterated glyph is visible within
	// the viewport.
	visible bool
	// first tracks whether the iterator has processed a glyph yet.
	first bool
	// baseline tracks the location of the first line of text's baseline.
	baseline int
}

func (t *textIterator) Glyph(g text.Glyph, ok bool) bool {
	t.g = g
// processGlyph checks whether the glyph is visible within the iterator's configured
// viewport and (if so) updates the iterator's text dimensions to include the glyph.
func (it *textIterator) processGlyph(g text.Glyph, ok bool) (_ text.Glyph, visibleOrBefore bool) {
	bounds := image.Rectangle{
		Min: image.Pt(g.Bounds.Min.X.Floor(), g.Bounds.Min.Y.Floor()),
		Max: image.Pt(g.Bounds.Max.X.Ceil(), g.Bounds.Max.Y.Ceil()),
	}
	// Compute the maximum extent to which glyphs overhang on the horizontal
	// axis.
	if d := g.Bounds.Min.X.Floor(); d < t.padding.Min.X {
		t.padding.Min.X = d
	if d := g.Bounds.Min.X.Floor(); d < it.padding.Min.X {
		it.padding.Min.X = d
	}
	if d := (g.Bounds.Max.X - g.Advance).Ceil(); d > t.padding.Max.X {
		t.padding.Max.X = d
	if d := (g.Bounds.Max.X - g.Advance).Ceil(); d > it.padding.Max.X {
		it.padding.Max.X = d
	}
	// Convert the bounds from dot-relative coordinates to document coordinates.
	bounds = bounds.Add(image.Pt(g.X.Round(), int(g.Y)))
	if !t.first {
		t.first = true
		t.baseline = int(g.Y)
		t.bounds = bounds
	if !it.first {
		it.first = true
		it.baseline = int(g.Y)
		it.bounds = bounds
	}

	lineTop := int(g.Y) - g.Ascent.Ceil()
	lineBottom := int(g.Y) + g.Descent.Ceil()
	above := lineBottom < it.viewport.Min.Y
	below := lineTop > it.viewport.Max.Y
	left := bounds.Max.X < it.viewport.Min.X
	right := bounds.Min.X > it.viewport.Max.X
	it.visible = !above && !below && !left && !right
	if it.visible {
		it.bounds.Min.X = min(it.bounds.Min.X, bounds.Min.X)
		it.bounds.Min.Y = min(it.bounds.Min.Y, lineTop)
		it.bounds.Max.X = max(it.bounds.Max.X, bounds.Max.X)
		it.bounds.Max.Y = max(it.bounds.Max.Y, lineBottom)
	}
	return g, ok && !below
}

	above := bounds.Max.Y < t.viewport.Min.Y
	below := bounds.Min.Y > t.viewport.Max.Y
	left := bounds.Max.X < t.viewport.Min.X
	right := bounds.Min.X > t.viewport.Max.X
	t.visible = !above && !below && !left && !right
	if t.visible {
		t.bounds.Min.X = min(t.bounds.Min.X, bounds.Min.X)
		t.bounds.Min.Y = min(t.bounds.Min.Y, int(g.Y)-g.Ascent.Ceil())
		t.bounds.Max.X = max(t.bounds.Max.X, bounds.Max.X)
		t.bounds.Max.Y = max(t.bounds.Max.Y, int(g.Y)+g.Descent.Ceil())
// paintGlyph buffers up and paints text glyphs. It should be invoked iteratively upon each glyph
// until it returns false. The line parameter should be a slice with
// a backing array of sufficient size to buffer multiple glyphs.
// A modified slice will be returned with each invocation, and is
// expected to be passed back in on the following invocation.
// This design is awkward, but prevents the line slice from escaping
// to the heap.
func (it *textIterator) paintGlyph(gtx layout.Context, shaper *text.Shaper, glyph text.Glyph, line []text.Glyph) ([]text.Glyph, bool) {
	_, visibleOrBefore := it.processGlyph(glyph, true)
	done := !visibleOrBefore
	if it.visible {
		if len(line) == 0 {
			it.lineOff = image.Point{X: glyph.X.Floor(), Y: int(glyph.Y)}.Sub(it.viewport.Min)
		}
		line = append(line, glyph)
	}
	if t.bounds.Dy() == 0 {
		t.bounds.Min.Y = -g.Ascent.Ceil()
		t.bounds.Max.Y = g.Descent.Ceil()
	if glyph.Flags&text.FlagLineBreak > 0 || cap(line)-len(line) == 0 || done {
		t := op.Offset(it.lineOff).Push(gtx.Ops)
		op := clip.Outline{Path: shaper.Shape(line)}.Op().Push(gtx.Ops)
		paint.PaintOp{}.Add(gtx.Ops)
		op.Pop()
		t.Pop()
		line = line[:0]
	}
	return ok && !below
	return line, !done
}