Skip to content

Commit

Permalink
Merge 4058126 into 5fd79c5
Browse files Browse the repository at this point in the history
  • Loading branch information
mum4k committed Jun 4, 2018
2 parents 5fd79c5 + 4058126 commit 6960e12
Show file tree
Hide file tree
Showing 13 changed files with 382 additions and 89 deletions.
9 changes: 5 additions & 4 deletions align/align.go
Expand Up @@ -19,7 +19,8 @@ import (
"fmt"
"image"
"strings"
"unicode/utf8"

runewidth "github.com/mattn/go-runewidth"
)

// Horizontal indicates the type of horizontal alignment.
Expand Down Expand Up @@ -147,10 +148,10 @@ func Text(rect image.Rectangle, text string, h Horizontal, v Vertical) (image.Po
return image.ZP, fmt.Errorf("the provided text contains a newline character: %q", text)
}

runes := utf8.RuneCountInString(text)
cells := runewidth.StringWidth(text)
var textLen int
if runes < rect.Dx() {
textLen = runes
if cells < rect.Dx() {
textLen = cells
} else {
textLen = rect.Dx()
}
Expand Down
10 changes: 9 additions & 1 deletion align/align_test.go
Expand Up @@ -275,13 +275,21 @@ func TestText(t *testing.T) {
want: image.Point{1, 2},
},
{
desc: "aligns text middle and center",
desc: "aligns half-width text rune middle and center",
rect: image.Rect(1, 1, 4, 4),
text: "a",
hAlign: HorizontalCenter,
vAlign: VerticalMiddle,
want: image.Point{2, 2},
},
{
desc: "aligns full-width text rune middle and center",
rect: image.Rect(1, 1, 4, 4),
text: "界",
hAlign: HorizontalCenter,
vAlign: VerticalMiddle,
want: image.Point{1, 2},
},
{
desc: "aligns text middle and right",
rect: image.Rect(1, 1, 4, 4),
Expand Down
24 changes: 18 additions & 6 deletions draw/border_test.go
Expand Up @@ -35,15 +35,21 @@ func TestBorder(t *testing.T) {
wantErr bool
}{
{
desc: "border is larger than canvas",
canvas: image.Rect(0, 0, 1, 1),
border: image.Rect(0, 0, 2, 2),
desc: "border is larger than canvas",
canvas: image.Rect(0, 0, 1, 1),
border: image.Rect(0, 0, 2, 2),
want: func(size image.Point) *faketerm.Terminal {
return faketerm.MustNew(size)
},
wantErr: true,
},
{
desc: "border is too small",
canvas: image.Rect(0, 0, 2, 2),
border: image.Rect(0, 0, 1, 1),
desc: "border is too small",
canvas: image.Rect(0, 0, 2, 2),
border: image.Rect(0, 0, 1, 1),
want: func(size image.Point) *faketerm.Terminal {
return faketerm.MustNew(size)
},
wantErr: true,
},
{
Expand All @@ -53,6 +59,9 @@ func TestBorder(t *testing.T) {
opts: []BorderOption{
BorderLineStyle(LineStyle(-1)),
},
want: func(size image.Point) *faketerm.Terminal {
return faketerm.MustNew(size)
},
wantErr: true,
},
{
Expand Down Expand Up @@ -195,6 +204,9 @@ func TestBorder(t *testing.T) {
opts: []BorderOption{
BorderTitle("abc", OverrunModeStrict),
},
want: func(size image.Point) *faketerm.Terminal {
return faketerm.MustNew(size)
},
wantErr: true,
},
{
Expand Down
6 changes: 4 additions & 2 deletions draw/rectangle.go
Expand Up @@ -51,7 +51,7 @@ func RectCellOpts(opts ...cell.Option) RectangleOption {
})
}

// DefaultRectChar is the default value for the RectChar option.
// DefaultRectChar is the default value for the RectChar option.
const DefaultRectChar = ' '

// RectChar sets the character used in each of the cells of the rectangle.
Expand All @@ -63,7 +63,9 @@ func RectChar(c rune) RectangleOption {

// Rectangle draws a filled rectangle on the canvas.
func Rectangle(c *canvas.Canvas, r image.Rectangle, opts ...RectangleOption) error {
opt := &rectOptions{}
opt := &rectOptions{
char: DefaultRectChar,
}
for _, o := range opts {
o.set(opt)
}
Expand Down
57 changes: 29 additions & 28 deletions draw/text.go
Expand Up @@ -104,47 +104,48 @@ func TextOverrunMode(om OverrunMode) TextOption {
})
}

// trimToCells trims the provided text so that it fits the specified amount of cells.
func trimToCells(text string, maxCells int, om OverrunMode) string {
// TrimText trims the provided text so that it fits the specified amount of cells.
func TrimText(text string, maxCells int, om OverrunMode) (string, error) {
if maxCells < 1 {
return ""
return "", fmt.Errorf("maxCells(%d) cannot be less than one", maxCells)
}

textCells := runewidth.StringWidth(text)
if textCells <= maxCells {
// Nothing to do if the text fits.
return text, nil
}

switch om {
case OverrunModeStrict:
return "", fmt.Errorf("the requested text %q takes %d cells to draw, space is available for only %d cells and overrun mode is %v", text, textCells, maxCells, om)
case OverrunModeTrim, OverrunModeThreeDot:
default:
return "", fmt.Errorf("unsupported overrun mode %d", om)
}

var b bytes.Buffer
cells := 0
cur := 0
for _, r := range text {
rw := runewidth.RuneWidth(r)
if cells+rw >= maxCells {
if cur+rw >= maxCells {
switch {
case om == OverrunModeTrim && rw == 1:
b.WriteRune(r)
case om == OverrunModeTrim:
// Only write the rune if it still fits, i.e. don't cut
// full-width runes in halt.
if cur+rw == maxCells {
b.WriteRune(r)
}
case om == OverrunModeThreeDot:
b.WriteRune('…')
}
break
}
b.WriteRune(r)
cells += rw
}
return b.String()
}

// bounds enforces the text bounds based on the specified overrun mode.
// Returns test that can be safely drawn within the bounds.
func bounds(text string, maxCells int, om OverrunMode) (string, error) {
cells := runewidth.StringWidth(text)
if cells <= maxCells {
return text, nil
}

switch om {
case OverrunModeStrict:
return "", fmt.Errorf("the requested text %q takes %d cells to draw, space is available for only %d cells and overrun mode is %v", text, cells, maxCells, om)
case OverrunModeTrim, OverrunModeThreeDot:
return trimToCells(text, maxCells, om), nil
default:
return "", fmt.Errorf("unsupported overrun mode %v", om)
b.WriteRune(r)
cur += rw
}
return b.String(), nil
}

// Text prints the provided text on the canvas starting at the provided point.
Expand All @@ -171,7 +172,7 @@ func Text(c *canvas.Canvas, text string, start image.Point, opts ...TextOption)
}

maxCells := wantMaxX - start.X
trimmed, err := bounds(text, maxCells, opt.overrunMode)
trimmed, err := TrimText(text, maxCells, opt.overrunMode)
if err != nil {
return err
}
Expand Down
182 changes: 182 additions & 0 deletions draw/text_test.go
Expand Up @@ -24,6 +24,188 @@ import (
"github.com/mum4k/termdash/terminal/faketerm"
)

func TestTrimText(t *testing.T) {
tests := []struct {
desc string
text string
maxCells int
om OverrunMode
want string
wantErr bool
}{
{
desc: "text is empty",
text: "",
maxCells: 1,
om: OverrunModeTrim,
want: "",
},
{
desc: "zero max cells",
text: "ab",
maxCells: 0,
om: OverrunModeTrim,
wantErr: true,
},
{
desc: "unsupported overrun mode",
text: "ab",
maxCells: 1,
om: OverrunMode(-1),
wantErr: true,
},
{
desc: "half-width runes, OverrunModeStrict, text fits exactly",
text: "ab",
maxCells: 2,
om: OverrunModeStrict,
want: "ab",
},
{
desc: "half-width runes, OverrunModeTrim, text fits exactly",
text: "ab",
maxCells: 2,
om: OverrunModeTrim,
want: "ab",
},
{
desc: "half-width runes, OverrunModeThreeDot, text fits exactly",
text: "ab",
maxCells: 2,
om: OverrunModeThreeDot,
want: "ab",
},
{
desc: "full-width runes, OverrunModeStrict, text fits exactly",
text: "你好",
maxCells: 4,
om: OverrunModeStrict,
want: "你好",
},
{
desc: "full-width runes, OverrunModeTrim, text fits exactly",
text: "你好",
maxCells: 4,
om: OverrunModeTrim,
want: "你好",
},
{
desc: "full-width runes, OverrunModeThreeDot, text fits exactly",
text: "你好",
maxCells: 4,
om: OverrunModeThreeDot,
want: "你好",
},
{
desc: "half-width runes, OverrunModeStrict, text overruns",
text: "ab",
maxCells: 1,
om: OverrunModeStrict,
wantErr: true,
},
{
desc: "half-width runes, OverrunModeTrim, text overruns",
text: "ab",
maxCells: 1,
om: OverrunModeTrim,
want: "a",
},
{
desc: "half-width runes, OverrunModeThreeDot, text overruns at the beginning",
text: "ab",
maxCells: 1,
om: OverrunModeThreeDot,
want: "…",
},
{
desc: "half-width runes, OverrunModeThreeDot, text overruns in the middle",
text: "abc",
maxCells: 2,
om: OverrunModeThreeDot,
want: "a…",
},
{
desc: "full-width runes, OverrunModeStrict, text overruns",
text: "你好",
maxCells: 1,
om: OverrunModeStrict,
wantErr: true,
},
{
desc: "full-width runes, OverrunModeTrim, text overruns at the beginning on rune boundary",
text: "你好",
maxCells: 2,
om: OverrunModeTrim,
want: "你",
},
{
desc: "full-width runes, OverrunModeThreeDot, text overruns at the beginning on rune boundary",
text: "你好",
maxCells: 2,
om: OverrunModeThreeDot,
want: "…",
},
{
desc: "full-width runes, OverrunModeTrim, text overruns in the middle on rune boundary",
text: "你好你好",
maxCells: 4,
om: OverrunModeTrim,
want: "你好",
},
{
desc: "full-width runes, OverrunModeThreeDot, text overruns in the middle on rune boundary",
text: "你好你好",
maxCells: 4,
om: OverrunModeThreeDot,
want: "你…",
},
{
desc: "full-width runes, OverrunModeTrim, text overruns at the beginning, cuts rune in half",
text: "你好",
maxCells: 1,
om: OverrunModeTrim,
want: "",
},
{
desc: "full-width runes, OverrunModeThreeDot, text overruns at the beginning, cuts rune in half",
text: "你好",
maxCells: 1,
om: OverrunModeThreeDot,
want: "…",
},
{
desc: "full-width runes, OverrunModeTrim, text overruns in the middle, cuts rune in half",
text: "你好你好",
maxCells: 3,
om: OverrunModeTrim,
want: "你",
},
{
desc: "full-width runes, OverrunModeThreeDot, text overruns in the middle, cuts rune in half",
text: "你好你好",
maxCells: 3,
om: OverrunModeThreeDot,
want: "你…",
},
}

for _, tc := range tests {
t.Run(tc.desc, func(t *testing.T) {
got, err := TrimText(tc.text, tc.maxCells, tc.om)
if (err != nil) != tc.wantErr {
t.Fatalf("TrimText => unexpected error: %v, wantErr: %v", err, tc.wantErr)
}
if err != nil {
return
}

if got != tc.want {
t.Errorf("TrimText =>\n got: %q\nwant: %q", got, tc.want)
}
})
}
}

func TestText(t *testing.T) {
tests := []struct {
desc string
Expand Down
Binary file modified images/gaugedemo.gif
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 6960e12

Please sign in to comment.