mirror of
https://github.com/mum4k/termdash.git
synced 2025-04-25 13:48:50 +08:00
The text widget now supports full-width runes.
Refactoring the draw() implementation to make it more readable: - taking out the line trimming logic. - taking out the drawing of the scroll up/down markers.
This commit is contained in:
parent
c2dab55b50
commit
929bf2b8fc
@ -19,6 +19,8 @@ package text
|
||||
import (
|
||||
"strings"
|
||||
"text/scanner"
|
||||
|
||||
runewidth "github.com/mattn/go-runewidth"
|
||||
)
|
||||
|
||||
// wrapNeeded returns true if wrapping is needed for the rune at the horizontal
|
||||
@ -29,7 +31,8 @@ func wrapNeeded(r rune, cvsPosX, cvsWidth int, opts *options) bool {
|
||||
// canvas, i.e. they take no horizontal space.
|
||||
return false
|
||||
}
|
||||
return cvsPosX >= cvsWidth && opts.wrapAtRunes
|
||||
rw := runewidth.RuneWidth(r)
|
||||
return cvsPosX > cvsWidth-rw && opts.wrapAtRunes
|
||||
}
|
||||
|
||||
// findLines finds the starting positions of all lines in the text when the
|
||||
@ -103,9 +106,7 @@ func scanStart(ls *lineScanner) scannerState {
|
||||
// scanLine scans a line until it finds its end.
|
||||
func scanLine(ls *lineScanner) scannerState {
|
||||
for {
|
||||
tok := ls.scanner.Scan()
|
||||
//switch tok := ls.scanner.Scan(); {
|
||||
switch {
|
||||
switch tok := ls.scanner.Scan(); {
|
||||
case tok == scanner.EOF:
|
||||
return nil
|
||||
|
||||
@ -117,7 +118,7 @@ func scanLine(ls *lineScanner) scannerState {
|
||||
|
||||
default:
|
||||
// Move horizontally within the line for each scanned character.
|
||||
ls.cvsPosX++
|
||||
ls.cvsPosX += runewidth.RuneWidth(tok)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -136,7 +137,7 @@ func scanLineBreak(ls *lineScanner) scannerState {
|
||||
func scanLineWrap(ls *lineScanner) scannerState {
|
||||
// The character on which we wrapped will be printed and is the start of
|
||||
// new line.
|
||||
ls.cvsPosX = 1
|
||||
ls.cvsPosX = runewidth.StringWidth(ls.scanner.TokenText())
|
||||
ls.lines = append(ls.lines, ls.scanner.Position.Offset)
|
||||
return scanLine
|
||||
}
|
||||
|
@ -31,7 +31,7 @@ func TestWrapNeeded(t *testing.T) {
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
desc: "point within canvas",
|
||||
desc: "half-width rune, falls within canvas",
|
||||
r: 'a',
|
||||
point: image.Point{2, 0},
|
||||
width: 3,
|
||||
@ -39,7 +39,15 @@ func TestWrapNeeded(t *testing.T) {
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
desc: "point outside of canvas, wrapping not configured",
|
||||
desc: "full-width rune, falls within canvas",
|
||||
r: '世',
|
||||
point: image.Point{1, 0},
|
||||
width: 3,
|
||||
opts: &options{},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
desc: "half-width rune, falls outside of canvas, wrapping not configured",
|
||||
r: 'a',
|
||||
point: image.Point{3, 0},
|
||||
width: 3,
|
||||
@ -47,7 +55,15 @@ func TestWrapNeeded(t *testing.T) {
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
desc: "point outside of canvas, wrapping configured",
|
||||
desc: "full-width rune, starts outside of canvas, wrapping not configured",
|
||||
r: '世',
|
||||
point: image.Point{3, 0},
|
||||
width: 3,
|
||||
opts: &options{},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
desc: "half-width rune, falls outside of canvas, wrapping configured",
|
||||
r: 'a',
|
||||
point: image.Point{3, 0},
|
||||
width: 3,
|
||||
@ -56,6 +72,26 @@ func TestWrapNeeded(t *testing.T) {
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
desc: "full-width rune, starts in and falls outside of canvas, wrapping configured",
|
||||
r: '世',
|
||||
point: image.Point{2, 0},
|
||||
width: 3,
|
||||
opts: &options{
|
||||
wrapAtRunes: true,
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
desc: "full-width rune, starts outside of canvas, wrapping configured",
|
||||
r: '世',
|
||||
point: image.Point{3, 0},
|
||||
width: 3,
|
||||
opts: &options{
|
||||
wrapAtRunes: true,
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
desc: "doesn't wrap for newline characters",
|
||||
r: '\n',
|
||||
@ -174,15 +210,32 @@ func TestFindLines(t *testing.T) {
|
||||
want: []int{0, 4, 8},
|
||||
},
|
||||
{
|
||||
desc: "wrapping enabled, newlines, doesn't fit in width, wide unicode characters",
|
||||
desc: "wrapping enabled, newlines, doesn't fit in width, full-width unicode characters",
|
||||
text: "你好\n世界",
|
||||
width: 1,
|
||||
width: 2,
|
||||
opts: &options{
|
||||
wrapAtRunes: true,
|
||||
},
|
||||
want: []int{0, 3, 7, 10},
|
||||
},
|
||||
|
||||
{
|
||||
desc: "wraps before a full-width character that starts in and falls out",
|
||||
text: "a你b",
|
||||
width: 2,
|
||||
opts: &options{
|
||||
wrapAtRunes: true,
|
||||
},
|
||||
want: []int{0, 1, 4},
|
||||
},
|
||||
{
|
||||
desc: "wraps before a full-width character that falls out",
|
||||
text: "ab你b",
|
||||
width: 2,
|
||||
opts: &options{
|
||||
wrapAtRunes: true,
|
||||
},
|
||||
want: []int{0, 2, 5},
|
||||
},
|
||||
{
|
||||
desc: "handles leading and trailing newlines",
|
||||
text: "\n\n\nhello\n\n\n",
|
||||
|
102
widgets/text/line_trim.go
Normal file
102
widgets/text/line_trim.go
Normal file
@ -0,0 +1,102 @@
|
||||
package text
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
|
||||
runewidth "github.com/mattn/go-runewidth"
|
||||
"github.com/mum4k/termdash/canvas"
|
||||
)
|
||||
|
||||
// line_trim.go contains code that trims lines that are too long.
|
||||
|
||||
type trimResult struct {
|
||||
// trimmed is set to true if the current and the following runes on this
|
||||
// line are trimmed.
|
||||
trimmed bool
|
||||
|
||||
// curPoint is the updated current point the drawing should continue on.
|
||||
curPoint image.Point
|
||||
}
|
||||
|
||||
// drawTrimChar draws the horizontal ellipsis '…' character as the last
|
||||
// character in the canvas on the specified line.
|
||||
func drawTrimChar(cvs *canvas.Canvas, line int) error {
|
||||
lastPoint := image.Point{cvs.Area().Dx() - 1, line}
|
||||
// If the penultimate cell contains a full-width rune, we need to clear it
|
||||
// first. Otherwise the trim char would cover just half of it.
|
||||
if width := cvs.Area().Dx(); width > 1 {
|
||||
penUlt := image.Point{width - 2, line}
|
||||
prev, err := cvs.Cell(penUlt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if runewidth.RuneWidth(prev.Rune) == 2 {
|
||||
if _, err := cvs.SetCell(penUlt, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cells, err := cvs.SetCell(lastPoint, '…')
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if cells != 1 {
|
||||
panic(fmt.Errorf("invalid trim character, it occupies %d cells, the implementation only supports scroll markers that occupy exactly one cell", cells))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// lineTrim determines if the current line needs to be trimmed. The cvs is the
|
||||
// canvas assigned to the widget, the curPoint is the current point the widget
|
||||
// is going to place the curRune at. If line trimming is needed, this function
|
||||
// replaces the last character with the horizontal ellipsis '…' character.
|
||||
func lineTrim(cvs *canvas.Canvas, curPoint image.Point, curRune rune, opts *options) (*trimResult, error) {
|
||||
if opts.wrapAtRunes {
|
||||
// Don't trim if the widget is configured to wrap lines.
|
||||
return &trimResult{
|
||||
trimmed: false,
|
||||
curPoint: curPoint,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Newline characters are never trimmed, they start the next line.
|
||||
if curRune == '\n' {
|
||||
return &trimResult{
|
||||
trimmed: false,
|
||||
curPoint: curPoint,
|
||||
}, nil
|
||||
}
|
||||
|
||||
width := cvs.Area().Dx()
|
||||
rw := runewidth.RuneWidth(curRune)
|
||||
switch {
|
||||
case rw == 1:
|
||||
if curPoint.X == width {
|
||||
if err := drawTrimChar(cvs, curPoint.Y); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
case rw == 2:
|
||||
if curPoint.X == width || curPoint.X == width-1 {
|
||||
if err := drawTrimChar(cvs, curPoint.Y); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("unable to decide line trimming at position %v for rune %q which has an unsupported width %d", curPoint, curRune, rw)
|
||||
}
|
||||
|
||||
trimmed := curPoint.X > width-rw
|
||||
if trimmed {
|
||||
curPoint = image.Point{curPoint.X + rw, curPoint.Y}
|
||||
}
|
||||
return &trimResult{
|
||||
trimmed: trimmed,
|
||||
curPoint: curPoint,
|
||||
}, nil
|
||||
}
|
268
widgets/text/line_trim_test.go
Normal file
268
widgets/text/line_trim_test.go
Normal file
@ -0,0 +1,268 @@
|
||||
package text
|
||||
|
||||
import (
|
||||
"image"
|
||||
"testing"
|
||||
|
||||
"github.com/kylelemons/godebug/pretty"
|
||||
"github.com/mum4k/termdash/canvas"
|
||||
"github.com/mum4k/termdash/canvas/testcanvas"
|
||||
"github.com/mum4k/termdash/draw/testdraw"
|
||||
"github.com/mum4k/termdash/terminal/faketerm"
|
||||
)
|
||||
|
||||
func TestLineTrim(t *testing.T) {
|
||||
cvsArea := image.Rect(0, 0, 10, 1)
|
||||
tests := []struct {
|
||||
desc string
|
||||
cvs *canvas.Canvas
|
||||
curPoint image.Point
|
||||
curRune rune
|
||||
opts *options
|
||||
wantRes *trimResult
|
||||
want func(size image.Point) *faketerm.Terminal
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
desc: "half-width rune, beginning of the canvas",
|
||||
cvs: testcanvas.MustNew(cvsArea),
|
||||
curPoint: image.Point{0, 0},
|
||||
curRune: 'A',
|
||||
opts: &options{
|
||||
wrapAtRunes: false,
|
||||
},
|
||||
wantRes: &trimResult{
|
||||
trimmed: false,
|
||||
curPoint: image.Point{0, 0},
|
||||
},
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
return faketerm.MustNew(size)
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "half-width rune, end of the canvas, fits",
|
||||
cvs: testcanvas.MustNew(cvsArea),
|
||||
curPoint: image.Point{9, 0},
|
||||
curRune: 'A',
|
||||
opts: &options{
|
||||
wrapAtRunes: false,
|
||||
},
|
||||
wantRes: &trimResult{
|
||||
trimmed: false,
|
||||
curPoint: image.Point{9, 0},
|
||||
},
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
return faketerm.MustNew(size)
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "full-width rune, end of the canvas, fits",
|
||||
cvs: testcanvas.MustNew(cvsArea),
|
||||
curPoint: image.Point{8, 0},
|
||||
curRune: '世',
|
||||
opts: &options{
|
||||
wrapAtRunes: false,
|
||||
},
|
||||
wantRes: &trimResult{
|
||||
trimmed: false,
|
||||
curPoint: image.Point{8, 0},
|
||||
},
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
return faketerm.MustNew(size)
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "half-width rune, falls out of the canvas, not configured to trim",
|
||||
cvs: testcanvas.MustNew(cvsArea),
|
||||
curPoint: image.Point{10, 0},
|
||||
curRune: 'A',
|
||||
opts: &options{
|
||||
wrapAtRunes: true,
|
||||
},
|
||||
wantRes: &trimResult{
|
||||
trimmed: false,
|
||||
curPoint: image.Point{10, 0},
|
||||
},
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
return faketerm.MustNew(size)
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "half-width rune, first that falls out of the canvas, trimmed and marked",
|
||||
cvs: testcanvas.MustNew(cvsArea),
|
||||
curPoint: image.Point{10, 0},
|
||||
curRune: 'A',
|
||||
opts: &options{
|
||||
wrapAtRunes: false,
|
||||
},
|
||||
wantRes: &trimResult{
|
||||
trimmed: true,
|
||||
curPoint: image.Point{11, 0},
|
||||
},
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
c := testcanvas.MustNew(ft.Area())
|
||||
testdraw.MustText(c, "…", image.Point{9, 0})
|
||||
testcanvas.MustApply(c, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "full-width rune, starts in and falls out, trimmed and marked",
|
||||
cvs: testcanvas.MustNew(cvsArea),
|
||||
curPoint: image.Point{9, 0},
|
||||
curRune: '世',
|
||||
opts: &options{
|
||||
wrapAtRunes: false,
|
||||
},
|
||||
wantRes: &trimResult{
|
||||
trimmed: true,
|
||||
curPoint: image.Point{11, 0},
|
||||
},
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
c := testcanvas.MustNew(ft.Area())
|
||||
testdraw.MustText(c, "…", image.Point{9, 0})
|
||||
testcanvas.MustApply(c, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "full-width rune, starts out, trimmed and marked",
|
||||
cvs: testcanvas.MustNew(cvsArea),
|
||||
curPoint: image.Point{10, 0},
|
||||
curRune: '世',
|
||||
opts: &options{
|
||||
wrapAtRunes: false,
|
||||
},
|
||||
wantRes: &trimResult{
|
||||
trimmed: true,
|
||||
curPoint: image.Point{12, 0},
|
||||
},
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
c := testcanvas.MustNew(ft.Area())
|
||||
testdraw.MustText(c, "…", image.Point{9, 0})
|
||||
testcanvas.MustApply(c, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "newline rune, first that falls out of the canvas, not trimmed or marked",
|
||||
cvs: testcanvas.MustNew(cvsArea),
|
||||
curPoint: image.Point{10, 0},
|
||||
curRune: '\n',
|
||||
opts: &options{
|
||||
wrapAtRunes: false,
|
||||
},
|
||||
wantRes: &trimResult{
|
||||
trimmed: false,
|
||||
curPoint: image.Point{10, 0},
|
||||
},
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
return faketerm.MustNew(size)
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "half-width rune, n-th that falls out of the canvas, trimmed and not marked",
|
||||
cvs: testcanvas.MustNew(cvsArea),
|
||||
curPoint: image.Point{11, 0},
|
||||
curRune: 'A',
|
||||
opts: &options{
|
||||
wrapAtRunes: false,
|
||||
},
|
||||
wantRes: &trimResult{
|
||||
trimmed: true,
|
||||
curPoint: image.Point{12, 0},
|
||||
},
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
return faketerm.MustNew(size)
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "full-width rune, n-th that falls out of the canvas, trimmed and not marked",
|
||||
cvs: testcanvas.MustNew(cvsArea),
|
||||
curPoint: image.Point{11, 0},
|
||||
curRune: '世',
|
||||
opts: &options{
|
||||
wrapAtRunes: false,
|
||||
},
|
||||
wantRes: &trimResult{
|
||||
trimmed: true,
|
||||
curPoint: image.Point{13, 0},
|
||||
},
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
return faketerm.MustNew(size)
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "newline rune, n-th that falls out of the canvas, not trimmed or marked",
|
||||
cvs: testcanvas.MustNew(cvsArea),
|
||||
curPoint: image.Point{11, 0},
|
||||
curRune: '\n',
|
||||
opts: &options{
|
||||
wrapAtRunes: false,
|
||||
},
|
||||
wantRes: &trimResult{
|
||||
trimmed: false,
|
||||
curPoint: image.Point{11, 0},
|
||||
},
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
return faketerm.MustNew(size)
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "full-width rune, starts out, previous is also full, trimmed and marked",
|
||||
cvs: func() *canvas.Canvas {
|
||||
cvs := testcanvas.MustNew(cvsArea)
|
||||
testcanvas.MustSetCell(cvs, image.Point{8, 0}, '世')
|
||||
return cvs
|
||||
}(),
|
||||
curPoint: image.Point{10, 0},
|
||||
curRune: '世',
|
||||
opts: &options{
|
||||
wrapAtRunes: false,
|
||||
},
|
||||
wantRes: &trimResult{
|
||||
trimmed: true,
|
||||
curPoint: image.Point{12, 0},
|
||||
},
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
c := testcanvas.MustNew(ft.Area())
|
||||
testdraw.MustText(c, "…", image.Point{9, 0})
|
||||
testcanvas.MustApply(c, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
gotRes, err := lineTrim(tc.cvs, tc.curPoint, tc.curRune, tc.opts)
|
||||
if (err != nil) != tc.wantErr {
|
||||
t.Errorf("lineTrim => unexpected error: %v, wantErr: %v", err, tc.wantErr)
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if diff := pretty.Compare(tc.wantRes, gotRes); diff != "" {
|
||||
t.Errorf("lineTrim => unexpected result, diff (-want, +got):\n%s", diff)
|
||||
}
|
||||
|
||||
got, err := faketerm.New(tc.cvs.Size())
|
||||
if err != nil {
|
||||
t.Fatalf("faketerm.New => unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if err := tc.cvs.Apply(got); err != nil {
|
||||
t.Fatalf("Apply => unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if diff := faketerm.Diff(tc.want(tc.cvs.Size()), got); diff != "" {
|
||||
t.Errorf("lineTrim => %v", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -110,84 +110,105 @@ func (t *Text) Write(text string, wOpts ...WriteOption) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// lineTrimNeeded returns true if the text on this line needs to be trimmed.
|
||||
// I.e. if the Text gadget was configured to trim lines, the current point falls
|
||||
// outside of the canvas and the current rune doesn't start a new line.
|
||||
func (t *Text) lineTrimNeeded(cur image.Point, cvs *canvas.Canvas, r rune) bool {
|
||||
if cur.X < cvs.Area().Dx() || r == '\n' {
|
||||
return false
|
||||
}
|
||||
return !t.opts.wrapAtRunes
|
||||
}
|
||||
|
||||
// minLinesForMarkers are the minimum amount of lines required on the canvas in
|
||||
// order to draw the scroll markers ('⇧' and '⇩').
|
||||
const minLinesForMarkers = 3
|
||||
|
||||
// draw draws the text context on the canvas starting at the specified line.
|
||||
// Argument starts are the starting positions of all the lines in the text.
|
||||
func (t *Text) draw(text string, cvs *canvas.Canvas, starts []int, fromLine int) error {
|
||||
var cur image.Point // Tracks the current drawing position on the canvas.
|
||||
lines := len(starts)
|
||||
// drawScrollUp draws the scroll up marker on the first line if there is more
|
||||
// text "above" the canvas due to the scrolling position. Returns true if the
|
||||
// marker was drawn.
|
||||
func (t *Text) drawScrollUp(cvs *canvas.Canvas, cur image.Point, fromLine int) (bool, error) {
|
||||
height := cvs.Area().Dy()
|
||||
if cur.Y == 0 && height >= minLinesForMarkers && fromLine > 0 {
|
||||
cells, err := cvs.SetCell(cur, '⇧')
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if cells != 1 {
|
||||
panic(fmt.Errorf("invalid scroll up marker, it occupies %d cells, the implementation only supports scroll markers that occupy exactly one cell", cells))
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// drawScrollDown draws the scroll down marker on the last line if there is
|
||||
// more text "below" the canvas due to the scrolling position. Returns true if
|
||||
// the marker was drawn.
|
||||
func (t *Text) drawScrollDown(cvs *canvas.Canvas, cur image.Point, fromLine int) (bool, error) {
|
||||
height := cvs.Area().Dy()
|
||||
lines := len(t.lines)
|
||||
if cur.Y == height-1 && height >= minLinesForMarkers && height < lines-fromLine {
|
||||
cells, err := cvs.SetCell(cur, '⇩')
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if cells != 1 {
|
||||
panic(fmt.Errorf("invalid scroll down marker, it occupies %d cells, the implementation only supports scroll markers that occupy exactly one cell", cells))
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// 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.
|
||||
height := cvs.Area().Dy()
|
||||
fromLine := t.scroll.firstLine(len(t.lines), height)
|
||||
optRange := t.givenWOpts.forPosition(0) // Text options for the current byte.
|
||||
startPos := starts[fromLine]
|
||||
startPos := t.lines[fromLine]
|
||||
for i, r := range text {
|
||||
if i < startPos {
|
||||
continue
|
||||
}
|
||||
|
||||
// Draw the scroll up marker on the first line if there is more text
|
||||
// above the canvas.
|
||||
if cur.Y == 0 && height >= minLinesForMarkers && fromLine > 0 {
|
||||
if err := cvs.SetCell(cur, '⇧'); err != nil {
|
||||
return err
|
||||
}
|
||||
// Scroll up marker.
|
||||
scrlUp, err := t.drawScrollUp(cvs, cur, fromLine)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if scrlUp {
|
||||
cur = image.Point{0, cur.Y + 1} // Move to the next line.
|
||||
startPos = starts[fromLine+1] // Skip one line of text, the marker replaced it.
|
||||
startPos = t.lines[fromLine+1] // Skip one line of text, the marker replaced it.
|
||||
continue
|
||||
}
|
||||
|
||||
// Line wrapping.
|
||||
if r == '\n' || wrapNeeded(r, cur.X, cvs.Area().Dx(), t.opts) {
|
||||
cur = image.Point{0, cur.Y + 1} // Move to the next line.
|
||||
}
|
||||
|
||||
// Draw the scroll down marker on the last line if there is more text
|
||||
// below the canvas.
|
||||
if cur.Y == height-1 && height >= minLinesForMarkers && height < lines-fromLine {
|
||||
if height >= minLinesForMarkers {
|
||||
if err := cvs.SetCell(cur, '⇩'); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
break
|
||||
// Scroll down marker.
|
||||
scrlDown, err := t.drawScrollDown(cvs, cur, fromLine)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if scrlDown || cur.Y >= height {
|
||||
break // Trim all lines falling after the canvas.
|
||||
}
|
||||
|
||||
tr, err := lineTrim(cvs, cur, r, t.opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cur = tr.curPoint
|
||||
if tr.trimmed {
|
||||
continue // Skip over any characters trimmed on the current line.
|
||||
}
|
||||
|
||||
if r == '\n' {
|
||||
continue // Don't print the newline runes, just interpret them above.
|
||||
}
|
||||
|
||||
if t.lineTrimNeeded(cur, cvs, r) {
|
||||
// Trim by replacing the last printed rune.
|
||||
prev := image.Point{cur.X - 1, cur.Y}
|
||||
if prev.In(cvs.Area()) {
|
||||
if err := cvs.SetCell(prev, '…'); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !cur.In(cvs.Area()) {
|
||||
continue // Skip any runes belonging to the trimmed area on a line.
|
||||
}
|
||||
|
||||
if i >= optRange.high { // Get the next write options.
|
||||
optRange = t.givenWOpts.forPosition(i)
|
||||
}
|
||||
if err := cvs.SetCell(cur, r, optRange.opts.cellOpts); err != nil {
|
||||
cells, err := cvs.SetCell(cur, r, optRange.opts.cellOpts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cur = image.Point{cur.X + 1, cur.Y} // Move within the same line.
|
||||
cur = image.Point{cur.X + cells, cur.Y} // Move within the same line.
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@ -211,9 +232,7 @@ func (t *Text) Draw(cvs *canvas.Canvas) error {
|
||||
return nil // Nothing to draw if there's no text.
|
||||
}
|
||||
|
||||
height := cvs.Area().Dy()
|
||||
fromLine := t.scroll.firstLine(len(t.lines), height)
|
||||
if err := t.draw(text, cvs, t.lines, fromLine); err != nil {
|
||||
if err := t.draw(text, cvs); err != nil {
|
||||
return err
|
||||
}
|
||||
t.newText = false
|
||||
@ -254,6 +273,7 @@ func (t *Text) Mouse(m *terminalapi.Mouse) error {
|
||||
|
||||
func (t *Text) Options() widgetapi.Options {
|
||||
return widgetapi.Options{
|
||||
// At least one line with at least one full-width rune.
|
||||
MinimumSize: image.Point{1, 1},
|
||||
WantMouse: !t.opts.disableScrolling,
|
||||
WantKeyboard: !t.opts.disableScrolling,
|
||||
|
@ -74,6 +74,21 @@ func TestTextDraws(t *testing.T) {
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "draws line of full-width runes",
|
||||
canvas: image.Rect(0, 0, 10, 1),
|
||||
writes: func(widget *Text) error {
|
||||
return widget.Write("你好,世界")
|
||||
},
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
c := testcanvas.MustNew(ft.Area())
|
||||
|
||||
testdraw.MustText(c, "你好,世界", image.Point{0, 0})
|
||||
testcanvas.MustApply(c, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "multiple writes append",
|
||||
canvas: image.Rect(0, 0, 12, 1),
|
||||
@ -167,9 +182,9 @@ func TestTextDraws(t *testing.T) {
|
||||
},
|
||||
{
|
||||
desc: "trims long lines",
|
||||
canvas: image.Rect(0, 0, 10, 3),
|
||||
canvas: image.Rect(0, 0, 10, 4),
|
||||
writes: func(widget *Text) error {
|
||||
return widget.Write("hello world\nshort\nand long again")
|
||||
return widget.Write("hello world\nshort\nexactly 10\nand long again")
|
||||
},
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
@ -177,6 +192,24 @@ func TestTextDraws(t *testing.T) {
|
||||
|
||||
testdraw.MustText(c, "hello wor…", image.Point{0, 0})
|
||||
testdraw.MustText(c, "short", image.Point{0, 1})
|
||||
testdraw.MustText(c, "exactly 10", image.Point{0, 2})
|
||||
testdraw.MustText(c, "and long …", image.Point{0, 3})
|
||||
testcanvas.MustApply(c, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "trims long lines with full-width runes",
|
||||
canvas: image.Rect(0, 0, 10, 3),
|
||||
writes: func(widget *Text) error {
|
||||
return widget.Write("hello wor你\nhello wor你d\nand long 世")
|
||||
},
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
c := testcanvas.MustNew(ft.Area())
|
||||
|
||||
testdraw.MustText(c, "hello wor…", image.Point{0, 0})
|
||||
testdraw.MustText(c, "hello wor…", image.Point{0, 1})
|
||||
testdraw.MustText(c, "and long …", image.Point{0, 2})
|
||||
testcanvas.MustApply(c, ft)
|
||||
return ft
|
||||
@ -378,7 +411,7 @@ func TestTextDraws(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "wraps lines at rune boundaries",
|
||||
desc: "wraps lines at half-width rune boundaries",
|
||||
canvas: image.Rect(0, 0, 10, 5),
|
||||
opts: []Option{
|
||||
WrapAtRunes(),
|
||||
@ -399,6 +432,29 @@ func TestTextDraws(t *testing.T) {
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "wraps lines at full-width rune boundaries",
|
||||
canvas: image.Rect(0, 0, 10, 6),
|
||||
opts: []Option{
|
||||
WrapAtRunes(),
|
||||
},
|
||||
writes: func(widget *Text) error {
|
||||
return widget.Write("hello wor你\nhello wor你d\nand long 世")
|
||||
},
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
c := testcanvas.MustNew(ft.Area())
|
||||
|
||||
testdraw.MustText(c, "hello wor", image.Point{0, 0})
|
||||
testdraw.MustText(c, "你", image.Point{0, 1})
|
||||
testdraw.MustText(c, "hello wor", image.Point{0, 2})
|
||||
testdraw.MustText(c, "你d", image.Point{0, 3})
|
||||
testdraw.MustText(c, "and long ", image.Point{0, 4})
|
||||
testdraw.MustText(c, "世", image.Point{0, 5})
|
||||
testcanvas.MustApply(c, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "rolls content upwards and trims lines",
|
||||
canvas: image.Rect(0, 0, 10, 2),
|
||||
|
Loading…
x
Reference in New Issue
Block a user