1package ui
2
3import (
4 "testing"
5
6 renderer "github.com/bnema/vev-vt"
7 "github.com/bnema/vev/internal/domain"
8)
9
10func cell(r rune, style renderer.Style, continuation bool) renderer.Cell {
11 return renderer.Cell{Rune: r, Style: style, Continuation: continuation}
12}
13
14func TestBlitFrameAnchorsAndClips(t *testing.T) {
15 style := renderer.Style{Foreground: 1, Background: 2}
16 tests := []struct {
17 name string
18 anchor VerticalAnchor
19 srcH, srcW, rectX, rectY, rectW, rectH int
20 want string
21 }{
22 {"top taller", VerticalAnchorTop, 4, 2, 1, 1, 2, 2, "abcd"},
23 {"bottom taller", VerticalAnchorBottom, 4, 2, 1, 1, 2, 2, "efgh"},
24 {"top shorter", VerticalAnchorTop, 1, 2, 1, 1, 2, 2, "ab "},
25 {"bottom shorter", VerticalAnchorBottom, 1, 2, 1, 1, 2, 2, " ab"},
26 }
27 for _, tt := range tests {
28 t.Run(tt.name, func(t *testing.T) {
29 src := FrameView{Width: tt.srcW, Height: tt.srcH, Rows: make([][]renderer.Cell, tt.srcH)}
30 for y := range src.Rows {
31 src.Rows[y] = make([]renderer.Cell, tt.srcW)
32 for x := range src.Rows[y] {
33 src.Rows[y][x] = cell(rune('a'+y*tt.srcW+x), style, false)
34 }
35 }
36 dst := renderer.NewFrame(5, 5)
37 BlitFrame(dst, domain.Rect{X: tt.rectX, Y: tt.rectY, Width: tt.rectW, Height: tt.rectH}, src, tt.anchor)
38 got := ""
39 for y := 1; y < 3; y++ {
40 for x := 1; x < 3; x++ {
41 got += string(dst.At(x, y).Rune)
42 }
43 }
44 if got != tt.want {
45 t.Fatalf("got %q want %q", got, tt.want)
46 }
47 })
48 }
49}
50
51func TestBlitFrameNeverSplitsWidePairsAtClippedEdges(t *testing.T) {
52 style := renderer.Style{Foreground: 7, Background: 8, Bold: true}
53 wide := []renderer.Cell{
54 cell('界', style, false),
55 cell(0, style, true),
56 cell('x', renderer.DefaultStyle(), false),
57 }
58 mark := cell('!', renderer.Style{Foreground: 3}, false)
59 tests := []struct {
60 name string
61 rect domain.Rect
62 dst renderer.Frame
63 want []renderer.Cell
64 }{
65 {
66 name: "left clipped continuation remains untouched",
67 rect: domain.Rect{X: -1, Y: 0, Width: 3, Height: 1},
68 dst: markedFrame(3, 1, mark),
69 want: []renderer.Cell{mark, wide[2], mark},
70 },
71 {
72 name: "right clipped leader remains untouched",
73 rect: domain.Rect{X: 0, Y: 0, Width: 3, Height: 1},
74 dst: markedFrame(1, 1, mark),
75 want: []renderer.Cell{mark},
76 },
77 {
78 name: "complete pair retains exact cells",
79 rect: domain.Rect{X: 1, Y: 0, Width: 3, Height: 1},
80 dst: markedFrame(4, 1, mark),
81 want: []renderer.Cell{mark, wide[0], wide[1], wide[2]},
82 },
83 }
84 for _, tt := range tests {
85 t.Run(tt.name, func(t *testing.T) {
86 BlitFrame(tt.dst, tt.rect, FrameView{Rows: [][]renderer.Cell{wide}, Width: len(wide), Height: 1}, VerticalAnchorTop)
87 assertRowEqual(t, tt.dst, 0, tt.want)
88 })
89 }
90}
91
92func TestBlitFrameBoundsExtremeRectanglesToVisibleIntersection(t *testing.T) {
93 maxInt := int(^uint(0) >> 1)
94 minInt := -maxInt - 1
95 style := renderer.Style{Foreground: 9}
96 src := FrameView{Rows: [][]renderer.Cell{{cell('a', style, false)}}, Width: 1, Height: 1}
97 mark := cell('!', renderer.Style{Background: 4}, false)
98 tests := []struct {
99 name string
100 rect domain.Rect
101 want []renderer.Cell
102 }{
103 {"huge visible width copies only source span", domain.Rect{X: 0, Y: 0, Width: maxInt, Height: maxInt}, []renderer.Cell{src.Rows[0][0], mark}},
104 {"max coordinate overflow is outside frame", domain.Rect{X: maxInt - 1, Y: maxInt - 1, Width: maxInt, Height: maxInt}, []renderer.Cell{mark, mark}},
105 {"min coordinate overflow is outside frame", domain.Rect{X: minInt, Y: minInt, Width: maxInt, Height: maxInt}, []renderer.Cell{mark, mark}},
106 }
107 for _, tt := range tests {
108 t.Run(tt.name, func(t *testing.T) {
109 dst := markedFrame(2, 1, mark)
110 BlitFrame(dst, tt.rect, src, VerticalAnchorTop)
111 assertRowEqual(t, dst, 0, tt.want)
112 })
113 }
114}
115
116func TestBlitFrameUsesLogicalDestinationRowsAndPreservesClippedSentinels(t *testing.T) {
117 mark := cell('!', renderer.Style{Foreground: 3}, false)
118 dst := renderer.NewFrame(3, 3)
119 dst.ScrollUp(0, 2, 1)
120 for y := range dst.Height {
121 for x := range dst.Width {
122 dst.Set(x, y, mark)
123 }
124 }
125 src := FrameView{Rows: [][]renderer.Cell{
126 {cell('a', renderer.Style{Foreground: 1}, false), cell('b', renderer.Style{Foreground: 2}, false)},
127 {cell('c', renderer.Style{Foreground: 4}, false), cell('d', renderer.Style{Foreground: 5}, false)},
128 }, Width: 2, Height: 2}
129
130 BlitFrame(dst, domain.Rect{X: -1, Y: 1, Width: 3, Height: 2}, src, VerticalAnchorTop)
131
132 assertRowEqual(t, dst, 0, []renderer.Cell{mark, mark, mark})
133 assertRowEqual(t, dst, 1, []renderer.Cell{src.Rows[0][1], mark, mark})
134 assertRowEqual(t, dst, 2, []renderer.Cell{src.Rows[1][1], mark, mark})
135}
136
137func TestBlitFrameClipsEveryEdgeAndPreservesUntouchedSentinels(t *testing.T) {
138 mark := cell('!', renderer.Style{Foreground: 3}, false)
139 src := FrameView{Rows: [][]renderer.Cell{
140 {cell('a', renderer.DefaultStyle(), false), cell('b', renderer.DefaultStyle(), false), cell('c', renderer.DefaultStyle(), false)},
141 {cell('d', renderer.DefaultStyle(), false), cell('e', renderer.DefaultStyle(), false), cell('f', renderer.DefaultStyle(), false)},
142 {cell('g', renderer.DefaultStyle(), false), cell('h', renderer.DefaultStyle(), false), cell('i', renderer.DefaultStyle(), false)},
143 }, Width: 3, Height: 3}
144
145 leftTop := markedFrame(3, 3, mark)
146 BlitFrame(leftTop, domain.Rect{X: -1, Y: -1, Width: 3, Height: 3}, src, VerticalAnchorTop)
147 assertRowEqual(t, leftTop, 0, []renderer.Cell{src.Rows[1][1], src.Rows[1][2], mark})
148 assertRowEqual(t, leftTop, 1, []renderer.Cell{src.Rows[2][1], src.Rows[2][2], mark})
149 assertRowEqual(t, leftTop, 2, []renderer.Cell{mark, mark, mark})
150
151 rightBottom := markedFrame(3, 3, mark)
152 BlitFrame(rightBottom, domain.Rect{X: 2, Y: 2, Width: 3, Height: 3}, src, VerticalAnchorTop)
153 assertRowEqual(t, rightBottom, 0, []renderer.Cell{mark, mark, mark})
154 assertRowEqual(t, rightBottom, 1, []renderer.Cell{mark, mark, mark})
155 assertRowEqual(t, rightBottom, 2, []renderer.Cell{mark, mark, src.Rows[0][0]})
156}
157
158func TestBlitFrameInvalidNoOp(t *testing.T) {
159 dst := renderer.NewFrame(2, 2)
160 before := dst.Clone()
161 cases := []struct {
162 r domain.Rect
163 s FrameView
164 a VerticalAnchor
165 }{
166 {domain.Rect{}, FrameView{}, VerticalAnchorTop},
167 {domain.Rect{Width: 1, Height: 1}, FrameView{Width: 0, Height: 1}, VerticalAnchorTop},
168 {domain.Rect{Width: 1, Height: 1}, FrameView{Width: 1, Height: 1, Rows: [][]renderer.Cell{{}}}, VerticalAnchor(99)},
169 }
170 for _, tc := range cases {
171 BlitFrame(dst, tc.r, tc.s, tc.a)
172 }
173 if !framesEqual(dst, before) {
174 t.Fatal("invalid blit changed destination")
175 }
176}
177
178func markedFrame(width, height int, mark renderer.Cell) renderer.Frame {
179 frame := renderer.NewFrame(width, height)
180 for y := range height {
181 for x := range width {
182 frame.Set(x, y, mark)
183 }
184 }
185 return frame
186}
187
188func assertRowEqual(t *testing.T, frame renderer.Frame, y int, want []renderer.Cell) {
189 t.Helper()
190 if len(want) != frame.Width {
191 t.Fatalf("want row length %d, frame width %d", len(want), frame.Width)
192 }
193 for x := range want {
194 if got := frame.At(x, y); !got.Equal(want[x]) {
195 t.Fatalf("cell %d,%d = %#v, want %#v", x, y, got, want[x])
196 }
197 }
198}