diff --git a/widget/editor.go b/widget/editor.go index f9c693d3..e8e7d7a6 100644 --- a/widget/editor.go +++ b/widget/editor.go @@ -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()} diff --git a/widget/label.go b/widget/label.go index 9799dfc1..6e5cae85 100644 --- a/widget/label.go +++ b/widget/label.go @@ -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 } - 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()) + 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) } - if t.bounds.Dy() == 0 { - t.bounds.Min.Y = -g.Ascent.Ceil() - t.bounds.Max.Y = g.Descent.Ceil() - } - return ok && !below + return g, ok && !below +} + +// 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 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 line, !done }