diff --git a/internal/wrap/wrap.go b/internal/wrap/wrap.go index 6e0511a..2ce48b5 100644 --- a/internal/wrap/wrap.go +++ b/internal/wrap/wrap.go @@ -34,7 +34,11 @@ func (m Mode) String() string { } // modeNames maps Mode values to human readable names. -var modeNames = map[Mode]string{} +var modeNames = map[Mode]string{ + Never: "WrapModeNever", + AtRunes: "WrapModeAtRunes", + AtWords: "WrapModeAtWords", +} const ( // Never is the default wrapping mode, which disables line wrapping. @@ -49,11 +53,11 @@ const ( AtWords ) -// Needed returns true if wrapping is needed for the rune at the horizontal +// needed returns true if wrapping is needed for the rune at the horizontal // position on the canvas that has the specified width. // This will always return false if no options are provided, since the default // behavior is to not wrap the text. -func Needed(r rune, posX, width int, m Mode) bool { +func needed(r rune, posX, width int, m Mode) bool { if r == '\n' { // Don't wrap for newline characters as they aren't printed on the // canvas, i.e. they take no horizontal space. @@ -82,7 +86,7 @@ func Lines(text string, width int, m Mode) []int { // input text or when the canvas width and configuration requires line // wrapping. type lineScanner struct { - // scanner lexes the input text. + // scanner is a lexer of the input text. scanner *scanner.Scanner // width is the width of the canvas the text will be drawn on. @@ -142,7 +146,7 @@ func scanLine(ls *lineScanner) scannerState { case tok == scanner.Ident: return scanLineBreak - case Needed(tok, ls.posX, ls.width, ls.mode): + case needed(tok, ls.posX, ls.width, ls.mode): return scanLineWrap default: diff --git a/internal/wrap/wrap_test.go b/internal/wrap/wrap_test.go index 6a0be28..3ef28fa 100644 --- a/internal/wrap/wrap_test.go +++ b/internal/wrap/wrap_test.go @@ -93,9 +93,9 @@ func TestNeeded(t *testing.T) { for _, tc := range tests { t.Run(tc.desc, func(t *testing.T) { - got := Needed(tc.r, tc.posX, tc.width, tc.mode) + got := needed(tc.r, tc.posX, tc.width, tc.mode) if got != tc.want { - t.Errorf("Needed => got %v, want %v", got, tc.want) + t.Errorf("needed => got %v, want %v", got, tc.want) } }) } diff --git a/widgets/text/text.go b/widgets/text/text.go index 50a6114..be09096 100644 --- a/widgets/text/text.go +++ b/widgets/text/text.go @@ -60,7 +60,13 @@ type Text struct { contentChanged bool // lines stores the starting locations in bytes of all the lines in the // buffer. I.e. positions of newline characters and of any calculated line wraps. + // The indexes in this slice are the line numbers. lines []int + // lineStartToIdx maps the rune positions where line starts are to indexes, + // the line numbers. + // This is the same data as in lines, but available for quick lookup based + // on character index. + lineStartToIdx map[int]int // mu protects the Text widget. mu sync.Mutex @@ -98,6 +104,7 @@ func (t *Text) reset() { t.lastWidth = 0 t.contentChanged = true t.lines = nil + t.lineStartToIdx = map[int]int{} } // Write writes text for the widget to display. Multiple calls append @@ -173,6 +180,16 @@ func (t *Text) drawScrollDown(cvs *canvas.Canvas, cur image.Point, fromLine int) return false, nil } +// isLineStart asserts whether a rune from the text at the specified position +// should be placed on a new line. +// Argument fromLine indicates the starting line we are drawing the text from +// and is needed, because this function must return false for the very first +// line drawn. The first line is already a new line. +func (t *Text) isLineStart(pos, fromLine int) bool { + idx, ok := t.lineStartToIdx[pos] + return ok && idx != fromLine +} + // draw draws the text context on the canvas starting at the specified line. func (t *Text) draw(text string, cvs *canvas.Canvas) error { var cur image.Point // Tracks the current drawing position on the canvas. @@ -183,6 +200,7 @@ func (t *Text) draw(text string, cvs *canvas.Canvas) error { return err } startPos := t.lines[fromLine] + var drawnScrollUp bool // Indicates if a scroll up marker was drawn. for i, r := range text { if i < startPos { continue @@ -196,11 +214,18 @@ func (t *Text) draw(text string, cvs *canvas.Canvas) error { if scrlUp { cur = image.Point{0, cur.Y + 1} // Move to the next line. startPos = t.lines[fromLine+1] // Skip one line of text, the marker replaced it. + drawnScrollUp = true continue } // Line wrapping. - if r == '\n' || wrap.Needed(r, cur.X, cvs.Area().Dx(), t.opts.wrapMode) { + fr := fromLine + if drawnScrollUp { + // The scroll marker inserted a line so we are off-by-one when + // looking up new lines. + fr++ + } + if t.isLineStart(i, fr) { cur = image.Point{0, cur.Y + 1} // Move to the next line. } @@ -255,6 +280,10 @@ func (t *Text) Draw(cvs *canvas.Canvas) error { // The previous text preprocessing (line wrapping) is invalidated when // new text is added or the width of the canvas changed. t.lines = wrap.Lines(text, width, t.opts.wrapMode) + t.lineStartToIdx = map[int]int{} + for idx, start := range t.lines { + t.lineStartToIdx[start] = idx + } } t.lastWidth = width