From 41489fb7321f14bff0041c4fca6b4dc691f6dc66 Mon Sep 17 00:00:00 2001 From: Elias Naur Date: Wed, 16 Feb 2022 22:26:02 +0100 Subject: [PATCH] widget: replace segmentIterator with simpler functions The replacement functions all use the single seeking function, seekPosition. Signed-off-by: Elias Naur --- widget/editor.go | 125 ++++++++++++++++++------------ widget/label.go | 192 ++++++++++++++++------------------------------- 2 files changed, 140 insertions(+), 177 deletions(-) diff --git a/widget/editor.go b/widget/editor.go index 1b87190b..e722a10c 100644 --- a/widget/editor.go +++ b/widget/editor.go @@ -61,7 +61,6 @@ type Editor struct { viewSize image.Point valid bool lines []text.Line - shapes []line dims layout.Dimensions requestFocus bool @@ -620,34 +619,6 @@ func (e *Editor) layout(gtx layout.Context, content layout.Widget) layout.Dimens e.scrollToCaret() } - off := image.Point{ - X: -e.scrollOff.X, - Y: -e.scrollOff.Y, - } - cl := textPadding(e.lines) - cl.Max = cl.Max.Add(e.viewSize) - caretStart := e.closestPosition(combinedPos{runes: e.caret.start}) - caretEnd := e.closestPosition(combinedPos{runes: e.caret.end}) - startSel, endSel := sortPoints(caretStart.lineCol, caretEnd.lineCol) - it := segmentIterator{ - startSel: startSel, - endSel: endSel, - Lines: e.lines, - Clip: cl, - Alignment: e.Alignment, - Width: e.viewSize.X, - Offset: off, - } - e.shapes = e.shapes[:0] - for { - layout, off, selected, yOffs, size, ok := it.Next() - if !ok { - break - } - op := clip.Outline{Path: e.shaper.Shape(e.font, e.textSize, layout)}.Op() - e.shapes = append(e.shapes, line{off, op, selected, yOffs, size}) - } - key.InputOp{Tag: &e.eventKey, Hint: e.InputHint}.Add(gtx.Ops) if e.requestFocus { key.FocusOp{Tag: &e.eventKey}.Add(gtx.Ops) @@ -700,17 +671,50 @@ func (e *Editor) PaintSelection(gtx layout.Context) { cl := textPadding(e.lines) cl.Max = cl.Max.Add(e.viewSize) defer clip.Rect(cl).Push(gtx.Ops).Pop() - for _, shape := range e.shapes { - if !shape.selected { - continue + selStart, selEnd := e.caret.start, e.caret.end + if selStart > selEnd { + selStart, selEnd = selEnd, selStart + } + caretStart := e.closestPosition(combinedPos{runes: selStart}) + caretEnd := e.closestPosition(combinedPos{runes: selEnd}) + scroll := image.Point{ + X: e.scrollOff.X, + Y: e.scrollOff.Y, + } + cl = cl.Add(scroll) + pos := e.seekFirstVisibleLine(cl.Min.Y) + for !posIsBelow(e.lines, pos, cl.Max.Y) { + start, end := clipLine(e.lines, e.Alignment, e.viewSize.X, cl, pos) + lineIdx := start.lineCol.Y + if lineIdx > caretEnd.lineCol.Y { + // Line is after selection end; we're done. + return } - offset := shape.offset - offset.Y += shape.selectionYOffs - t := op.Offset(layout.FPt(offset)).Push(gtx.Ops) - cl := clip.Rect(image.Rectangle{Max: shape.selectionSize}).Push(gtx.Ops) + // Clamp start, end to selection. + if start.runes < selStart { + start = caretStart + } + if end.runes > selEnd { + end = caretEnd + } + + line := e.lines[start.lineCol.Y] + dotStart := image.Pt(start.x.Round(), start.y) + dotEnd := image.Pt(end.x.Round(), end.y) + t := op.Offset(layout.FPt(scroll.Mul(-1))).Push(gtx.Ops) + size := image.Rectangle{ + Min: dotStart.Sub(image.Point{Y: line.Ascent.Ceil()}), + Max: dotEnd.Add(image.Point{Y: line.Descent.Ceil()}), + } + op := clip.Rect(size).Push(gtx.Ops) paint.PaintOp{}.Add(gtx.Ops) - cl.Pop() + op.Pop() t.Pop() + + if pos.lineCol.Y == len(e.lines)-1 { + break + } + pos = e.closestPosition(combinedPos{lineCol: screenPos{Y: pos.lineCol.Y + 1}}) } } @@ -718,12 +722,28 @@ func (e *Editor) PaintText(gtx layout.Context) { cl := textPadding(e.lines) cl.Max = cl.Max.Add(e.viewSize) defer clip.Rect(cl).Push(gtx.Ops).Pop() - for _, shape := range e.shapes { - t := op.Offset(layout.FPt(shape.offset)).Push(gtx.Ops) - cl := shape.clip.Push(gtx.Ops) + scroll := image.Point{ + X: e.scrollOff.X, + Y: e.scrollOff.Y, + } + cl = cl.Add(scroll) + pos := e.seekFirstVisibleLine(cl.Min.Y) + for !posIsBelow(e.lines, pos, cl.Max.Y) { + start, end := clipLine(e.lines, e.Alignment, e.viewSize.X, cl, pos) + line := e.lines[start.lineCol.Y] + l := subLayout(line, start.lineCol.X, end.lineCol.X) + + off := image.Point{X: start.x.Floor(), Y: start.y}.Sub(scroll) + t := op.Offset(layout.FPt(off)).Push(gtx.Ops) + op := clip.Outline{Path: e.shaper.Shape(e.font, e.textSize, l)}.Op().Push(gtx.Ops) paint.PaintOp{}.Add(gtx.Ops) - cl.Pop() + op.Pop() t.Pop() + + if pos.lineCol.Y == len(e.lines)-1 { + break + } + pos = e.closestPosition(combinedPos{lineCol: screenPos{Y: pos.lineCol.Y + 1}}) } } @@ -757,6 +777,19 @@ func (e *Editor) PaintCaret(gtx layout.Context) { } } +func (e *Editor) seekFirstVisibleLine(y int) combinedPos { + pos := e.closestPosition(combinedPos{y: y}) + for pos.lineCol.Y > 0 { + prevLine := pos.lineCol.Y - 1 + prev := e.closestPosition(combinedPos{lineCol: screenPos{Y: prevLine}}) + if posIsAbove(e.lines, prev, y) { + break + } + pos = prev + } + return pos +} + func (e *Editor) caretInfo() (pos image.Point, ascent, descent int) { caretStart := e.closestPosition(combinedPos{runes: e.caret.start}) carX := caretStart.x @@ -889,11 +922,7 @@ func (e *Editor) indexPosition(pos combinedPos) combinedPos { e.makeValid() // Initialize index with first caret position. if len(e.index) == 0 { - l := e.lines[0] - e.index = append(e.index, combinedPos{ - x: align(e.Alignment, l.Width, e.viewSize.X), - y: l.Ascent.Ceil(), - }) + e.index = append(e.index, firstPos(e.lines[0], e.Alignment, e.viewSize.X)) } i := sort.Search(len(e.index), func(i int) bool { return positionGreaterOrEqual(e.lines, e.index[i], pos) @@ -949,7 +978,7 @@ func (e *Editor) closestPosition(pos combinedPos) combinedPos { const runesPerIndexEntry = 50 for { var done bool - closest, done = seekPosition(e.lines, closest, pos, e.Alignment, e.viewSize.X, runesPerIndexEntry) + closest, done = seekPosition(e.lines, e.Alignment, e.viewSize.X, closest, pos, runesPerIndexEntry) if done { return closest } @@ -959,7 +988,7 @@ func (e *Editor) closestPosition(pos combinedPos) combinedPos { // seekPosition seeks to the position closest to needle, starting at start and returns true. // If limit is non-zero, seekPosition stops seeks after limit runes and returns false. -func seekPosition(lines []text.Line, start, needle combinedPos, alignment text.Alignment, width, limit int) (combinedPos, bool) { +func seekPosition(lines []text.Line, alignment text.Alignment, width int, start, needle combinedPos, limit int) (combinedPos, bool) { l := lines[start.lineCol.Y] count := 0 // Advance next and prev until next is greater than or equal to pos. diff --git a/widget/label.go b/widget/label.go index c45a89da..067196dd 100644 --- a/widget/label.go +++ b/widget/label.go @@ -30,125 +30,57 @@ type Label struct { // not pixels): Y = line number, X = rune column. type screenPos image.Point -type segmentIterator struct { - Lines []text.Line - Clip image.Rectangle - Alignment text.Alignment - Width int - Offset image.Point - startSel screenPos - endSel screenPos - - pos screenPos // current position - line text.Line // current line - layout text.Layout // current line's Layout - - // pixel positions - off fixed.Point26_6 - y, prevDesc fixed.Int26_6 -} - const inf = 1e6 -func (l *segmentIterator) Next() (text.Layout, image.Point, bool, int, image.Point, bool) { - for l.pos.Y < len(l.Lines) { - if l.pos.X == 0 { - l.line = l.Lines[l.pos.Y] +func posIsAbove(lines []text.Line, pos combinedPos, y int) bool { + line := lines[pos.lineCol.Y] + return pos.y+line.Bounds.Max.Y.Ceil() < y +} - // Calculate X & Y pixel coordinates of left edge of line. We need y - // for the next line, so it's in l, but we only need x here, so it's - // not. - x := align(l.Alignment, l.line.Width, l.Width) + fixed.I(l.Offset.X) - l.y += l.prevDesc + l.line.Ascent - l.prevDesc = l.line.Descent - // Align baseline and line start to the pixel grid. - l.off = fixed.Point26_6{X: fixed.I(x.Floor()), Y: fixed.I(l.y.Ceil())} - l.y = l.off.Y - l.off.Y += fixed.I(l.Offset.Y) - if (l.off.Y + l.line.Bounds.Min.Y).Floor() > l.Clip.Max.Y { - break - } +func posIsBelow(lines []text.Line, pos combinedPos, y int) bool { + line := lines[pos.lineCol.Y] + return pos.y+line.Bounds.Min.Y.Floor() > y +} - if (l.off.Y + l.line.Bounds.Max.Y).Ceil() < l.Clip.Min.Y { - // This line is outside/before the clip area; go on to the next line. - l.pos.Y++ - continue - } +func clipLine(lines []text.Line, alignment text.Alignment, width int, clip image.Rectangle, linePos combinedPos) (start combinedPos, end combinedPos) { + // Seek to first (potentially) visible column. + lineIdx := linePos.lineCol.Y + line := lines[lineIdx] + // runeWidth is the width of the widest rune in line. + runeWidth := (line.Bounds.Max.X - line.Width).Ceil() + q := combinedPos{y: start.y, x: fixed.I(clip.Min.X - runeWidth)} + start, _ = seekPosition(lines, alignment, width, linePos, q, 0) + // Seek to first invisible column after start. + q = combinedPos{y: start.y, x: fixed.I(clip.Max.X + runeWidth)} + end, _ = seekPosition(lines, alignment, width, start, q, 0) + return start, end +} - // Copy the line's Layout, since we slice it up later. - l.layout = l.line.Layout - - // Find the left edge of the text visible in the l.Clip clipping - // area. - for len(l.layout.Advances) > 0 { - _, n := utf8.DecodeRuneInString(l.layout.Text) - adv := l.layout.Advances[0] - if (l.off.X + adv + l.line.Bounds.Max.X - l.line.Width).Ceil() >= l.Clip.Min.X { - break - } - l.off.X += adv - l.layout.Text = l.layout.Text[n:] - l.layout.Advances = l.layout.Advances[1:] - l.pos.X++ - } - } - - selected := l.inSelection() - endx := l.off.X - rune := 0 - nextLine := true - retLayout := l.layout - for n := range l.layout.Text { - selChanged := selected != l.inSelection() - beyondClipEdge := (endx + l.line.Bounds.Min.X).Floor() > l.Clip.Max.X - if selChanged || beyondClipEdge { - retLayout.Advances = l.layout.Advances[:rune] - retLayout.Text = l.layout.Text[:n] - if selChanged { - // Save the rest of the line - l.layout.Advances = l.layout.Advances[rune:] - l.layout.Text = l.layout.Text[n:] - nextLine = false - } - break - } - endx += l.layout.Advances[rune] - rune++ - l.pos.X++ - } - offFloor := image.Point{X: l.off.X.Floor(), Y: l.off.Y.Floor()} - - // Calculate the width & height if the returned text. - // - // If there's a better way to do this, I'm all ears. - var d fixed.Int26_6 - for _, adv := range retLayout.Advances { - d += adv - } - size := image.Point{ - X: d.Ceil(), - Y: (l.line.Ascent + l.line.Descent).Ceil(), - } - - if nextLine { - l.pos.Y++ - l.pos.X = 0 - } else { - l.off.X = endx - } - - return retLayout, offFloor, selected, l.prevDesc.Ceil() - size.Y, size, true +func subLayout(line text.Line, startCol, endCol int) text.Layout { + adv := line.Layout.Advances + if startCol == len(adv) { + return text.Layout{} } - return text.Layout{}, image.Point{}, false, 0, image.Point{}, false + adv = adv[startCol:endCol] + txt := line.Layout.Text + for i := 0; i < startCol; i++ { + _, s := utf8.DecodeRuneInString(txt) + txt = txt[s:] + } + n := 0 + for i := startCol; i < endCol; i++ { + _, s := utf8.DecodeRuneInString(txt[n:]) + n += s + } + txt = txt[:n] + return text.Layout{Text: txt, Advances: adv} } -func (l *segmentIterator) inSelection() bool { - return l.startSel.LessOrEqual(l.pos) && - l.pos.Less(l.endSel) -} - -func (p1 screenPos) LessOrEqual(p2 screenPos) bool { - return p1.Y < p2.Y || (p1.Y == p2.Y && p1.X <= p2.X) +func firstPos(line text.Line, alignment text.Alignment, width int) combinedPos { + return combinedPos{ + x: align(alignment, line.Width, width), + y: line.Ascent.Ceil(), + } } func (p1 screenPos) Less(p2 screenPos) bool { @@ -164,29 +96,31 @@ func (l Label) Layout(gtx layout.Context, s text.Shaper, font text.Font, size un } dims := linesDimens(lines) dims.Size = cs.Constrain(dims.Size) + if len(lines) == 0 { + return dims + } cl := textPadding(lines) cl.Max = cl.Max.Add(dims.Size) - it := segmentIterator{ - Lines: lines, - Clip: cl, - Alignment: l.Alignment, - Width: dims.Size.X, - } - for { - l, off, _, _, _, ok := it.Next() - if !ok { + defer clip.Rect(cl).Push(gtx.Ops).Pop() + semantic.LabelOp(txt).Add(gtx.Ops) + pos := firstPos(lines[0], l.Alignment, dims.Size.X) + for !posIsBelow(lines, pos, cl.Max.Y) { + start, end := clipLine(lines, l.Alignment, dims.Size.X, cl, pos) + line := lines[start.lineCol.Y] + lt := subLayout(line, start.lineCol.X, end.lineCol.X) + + off := image.Point{X: start.x.Floor(), Y: start.y} + t := op.Offset(layout.FPt(off)).Push(gtx.Ops) + op := clip.Outline{Path: s.Shape(font, textSize, lt)}.Op().Push(gtx.Ops) + paint.PaintOp{}.Add(gtx.Ops) + op.Pop() + t.Pop() + + if pos.lineCol.Y == len(lines)-1 { break } - t := op.Offset(layout.FPt(off)).Push(gtx.Ops) - rcl := clip.Rect(cl.Sub(off)).Push(gtx.Ops) - cl := clip.Outline{Path: s.Shape(font, textSize, l)}.Op().Push(gtx.Ops) - paint.PaintOp{}.Add(gtx.Ops) - cl.Pop() - rcl.Pop() - t.Pop() + pos, _ = seekPosition(lines, l.Alignment, dims.Size.X, pos, combinedPos{lineCol: screenPos{Y: pos.lineCol.Y + 1}}, 0) } - defer clip.Rect(image.Rectangle{Max: dims.Size}).Push(gtx.Ops).Pop() - semantic.LabelOp(txt).Add(gtx.Ops) return dims }