okami.mom/ultraviolet

git clone https://git.nkp.pet/okami.mom/ultraviolet

Drew Smirnofffix: draw graphene clusters wider than 4 columns (#150)006e29f

upstream
3.3 KiB112 linesraw
1package uv
2
3import (
4	"testing"
5
6	"github.com/charmbracelet/x/ansi"
7)
8
9// TestDrawWcWidthCells checks that drawing lays cells out over the same number
10// of columns the terminal advances over. A cell holds a whole grapheme
11// cluster, but under wcwidth its width is the sum of the cluster's codepoints,
12// which is what a terminal without Unicode core mode (DEC mode 2027) advances
13// by. Get that wrong and every cell after it lands in the wrong column.
14func TestDrawWcWidthCells(t *testing.T) {
15	tests := []struct {
16		name   string
17		str    string
18		widths []int // width of each non-placeholder cell, in order
19	}{
20		{"ascii", "abc", []int{1, 1, 1}},
21		{"combining acute", "éx", []int{1, 1}},
22		{"devanagari", "नमस्ते", []int{1, 1, 2}},
23		{"cjk", "世界", []int{2, 2}},
24		{"vs16", "⚠️", []int{1}},
25		{"zwj pair", "👨‍💻", []int{4}},
26		{"zwj flag", "🏳️‍🌈", []int{3}},
27		{"skin tone", "👍🏽", []int{4}},
28		// Wider than any glyph, and wider than the four columns cells used to
29		// be capped at. A capped cluster ends up zero width, which the
30		// renderer skips as a wide-cell placeholder, so it never gets drawn.
31		{"zwj family", "👨‍👩‍👧‍👦", []int{8}},
32	}
33
34	for _, tc := range tests {
35		t.Run(tc.name, func(t *testing.T) {
36			buf := NewScreenBuffer(20, 1)
37			buf.Method = ansi.WcWidth
38			NewStyledString(tc.str).Draw(buf, buf.Bounds())
39
40			var x int
41			for i, want := range tc.widths {
42				cell := buf.CellAt(x, 0)
43				if cell == nil {
44					t.Fatalf("cell %d: no cell at column %d", i, x)
45				}
46				if cell.Width != want {
47					t.Errorf("cell %d: width %d, want %d", i, cell.Width, want)
48				}
49				// Every column the cell covers past the first has to be a
50				// placeholder, otherwise the buffer and the terminal disagree
51				// about what occupies them.
52				for j := 1; j < cell.Width; j++ {
53					if p := buf.CellAt(x+j, 0); p == nil || !p.isWidePlaceholder() {
54						t.Errorf("column %d: got %+v, want a wide placeholder", x+j, p)
55					}
56				}
57				x += max(cell.Width, 1)
58			}
59
60			if got := ansi.StringWidthWc(tc.str); got != x {
61				t.Errorf("cells cover %d columns, but the string measures %d", x, got)
62			}
63		})
64	}
65}
66
67// TestDrawGraphemeWidthCells covers the same strings under the width model of
68// a terminal in Unicode core mode, which measures a cluster as the one glyph
69// it draws.
70func TestDrawGraphemeWidthCells(t *testing.T) {
71	tests := []struct {
72		str   string
73		width int // width of the first cell
74	}{
75		{"नमस्ते", 1},
76		{"⚠️", 2},
77		{"👨‍👩‍👧‍👦", 2},
78		{"👍🏽", 2},
79	}
80
81	for _, tc := range tests {
82		buf := NewScreenBuffer(20, 1)
83		buf.Method = ansi.GraphemeWidth
84		NewStyledString(tc.str).Draw(buf, buf.Bounds())
85
86		cell := buf.CellAt(0, 0)
87		if cell == nil {
88			t.Fatalf("%q: no cell at column 0", tc.str)
89		}
90		if cell.Width != tc.width {
91			t.Errorf("%q: width %d, want %d", tc.str, cell.Width, tc.width)
92		}
93	}
94}
95
96// TestNewCellWidths checks that cells built directly agree with the cells
97// [StyledString] lays out.
98func TestNewCellWidths(t *testing.T) {
99	for _, tc := range []struct {
100		gr   string
101		want int
102	}{
103		{"a", 1},
104		{" ", 1},
105		{"स्ते", 2},
106		{"👨‍💻", 4},
107	} {
108		if got := NewCell(ansi.WcWidth, tc.gr).Width; got != tc.want {
109			t.Errorf("NewCell(WcWidth, %q).Width = %d, want %d", tc.gr, got, tc.want)
110		}
111	}
112}