feat/data-dnd e05cb15

scene_dim_test.go RawHistoryFolder

1package vulkan
2
3import (
4	"image"
5	"image/color"
6	"math"
7	"testing"
8
9	"github.com/bnema/neferwl/internal/ports"
10)
11
12// These walk tests need no Vulkan device: placeholder windows and solid
13// separators produce draw commands without importing client buffers.
14func TestSceneDimOrder(t *testing.T) {
15	r := &Renderer{width: 80, height: 60}
16	s := ports.Scene{
17		Dim: 0.4, Border: ports.Border{Active: "#ffffff"},
18		Windows: []ports.SceneWindow{
19			{ID: 1, Rect: ports.Rect{W: 20, H: 20}},
20			{ID: 2, Floating: true, Hidden: true, Rect: ports.Rect{W: 20, H: 20}},
21			{ID: 3, Floating: true, Popup: true, Rect: ports.Rect{W: 20, H: 20}},
22			{ID: 4, Floating: true, Rect: ports.Rect{W: 0, H: 20}},
23			{ID: 5, Floating: true, Rect: ports.Rect{X: 30, W: 20, H: 20}},
24			{ID: 6, Floating: true, Rect: ports.Rect{X: 50, W: 20, H: 20}},
25		},
26		Separators: []ports.Separator{{Rect: ports.Rect{W: 80, H: 1}, Active: true}},
27	}
28	walk := func(s ports.Scene) []draw {
29		return r.draws(s, nil, newDamage(&target{}, s, image.Rect(0, 0, 80, 60)))
30	}
31	got := walk(s)
32	if len(got) != 5 { // tile, tile lines, veil, two floats
33		t.Fatalf("draws = %d, want 5", len(got))
34	}
35	veil := got[2]
36	if veil.pc.rect != [4]int32{0, 0, 80, 60} || veil.pc.color != [4]float32{0, 0, 0, float32(s.Dim)} || veil.pc.misc[0] != modeSolid || veil.pc.misc[1] != 0 {
37		t.Fatalf("veil = %+v", veil.pc)
38	}
39	if got[1].pc.color != [4]float32{1, 1, 1, 1} || got[3].pc.rect[0] != 30 || got[4].pc.rect[0] != 50 {
40		t.Fatalf("tile line / float order: %+v", got)
41	}
42	for _, tc := range []struct {
43		name      string
44		dim       float64
45		windows   []ports.SceneWindow
46		wantCount int
47		wantAlpha float32
48	}{
49		{"zero", 0, s.Windows, 4, 0},
50		{"negative", -1, s.Windows, 4, 0},
51		{"clamped", 2, s.Windows, 5, 1},
52		{"no float", 0.4, s.Windows[:4], 2, 0},
53	} {
54		t.Run(tc.name, func(t *testing.T) {
55			s.Dim, s.Windows = tc.dim, tc.windows
56			ds := walk(s)
57			if len(ds) != tc.wantCount {
58				t.Fatalf("draws = %d, want %d", len(ds), tc.wantCount)
59			}
60			if tc.wantAlpha != 0 && ds[2].pc.color[3] != tc.wantAlpha {
61				t.Fatalf("alpha = %v, want %v", ds[2].pc.color[3], tc.wantAlpha)
62			}
63		})
64	}
65}
66
67// DimBehind puts the veil first, under every window, floats included, and
68// only once.
69func TestSceneDimBehind(t *testing.T) {
70	r := &Renderer{width: 80, height: 60}
71	s := ports.Scene{Dim: 0.3, DimBehind: true, Windows: []ports.SceneWindow{
72		{ID: 1, Preview: 0.5, Rect: ports.Rect{X: 10, W: 20, H: 20}},
73		{ID: 2, Floating: true, Rect: ports.Rect{X: 40, W: 20, H: 20}},
74	}}
75	ds := r.draws(s, nil, newDamage(&target{}, s, image.Rect(0, 0, 80, 60)))
76	if len(ds) != 3 { // veil, two windows
77		t.Fatalf("draws = %d, want 3", len(ds))
78	}
79	if v := ds[0].pc; v.rect != [4]int32{0, 0, 80, 60} || v.color != [4]float32{0, 0, 0, 0.3} {
80		t.Fatalf("veil = %+v", v)
81	}
82	if ds[1].pc.rect[0] != 10 || ds[2].pc.rect[0] != 40 {
83		t.Fatalf("windows not over the veil: %+v", ds)
84	}
85}
86
87// A dimmed window gets a veil over its own rect, border included, right
88// after it: windows drawn later stay bright.
89func TestSceneWindowDim(t *testing.T) {
90	r := &Renderer{width: 80, height: 60}
91	s := ports.Scene{Border: ports.Border{Width: 1, Inactive: "#ffffff"},
92		Windows: []ports.SceneWindow{
93			{ID: 1, Floating: true, Dim: 0.5, Inset: ports.SideAll, Rect: ports.Rect{X: -10, W: 20, H: 20}},
94			{ID: 2, Floating: true, Inset: ports.SideAll, Rect: ports.Rect{X: 20, W: 20, H: 20}},
95		},
96		Separators: []ports.Separator{{Window: 1, Rect: ports.Rect{X: -10, W: 20, H: 1}}},
97	}
98	ds := r.draws(s, nil, newDamage(&target{}, s, image.Rect(0, 0, 80, 60)))
99	// Window 1, its border, its veil (clipped to the output), window 2.
100	if len(ds) != 4 {
101		t.Fatalf("draws = %d, want 4", len(ds))
102	}
103	if v := ds[2].pc; v.rect != [4]int32{0, 0, 10, 20} || v.color != [4]float32{0, 0, 0, 0.5} {
104		t.Fatalf("veil = %+v", v)
105	}
106	if ds[3].pc.rect[0] != 21 {
107		t.Fatalf("window 2 not last: %+v", ds[3].pc)
108	}
109}
110
111// HDR blends linear light: the dim alpha is raised so reference white
112// lands where SDR's encoded dim puts it.
113func TestDimAlpha(t *testing.T) {
114	if got := dimAlpha(0.5, false); got != 0.5 {
115		t.Fatalf("sdr %v", got)
116	}
117	for _, a := range []float64{0, 0.3, 0.5, 1} {
118		got := dimAlpha(a, true)
119		if want := 1 - srgbToLinear(1-a); math.Abs(got-want) > 1e-12 || got < a {
120			t.Fatalf("hdr %v: %v want %v", a, got, want)
121		}
122	}
123	if dimAlpha(2, true) != 1 || dimAlpha(-1, true) != 0 {
124		t.Fatal("not clamped")
125	}
126}
127
128func TestSceneDimPartialDamage(t *testing.T) {
129	r := &Renderer{width: 80, height: 60}
130	s := ports.Scene{Seq: 8, Dim: 0.5, Windows: []ports.SceneWindow{
131		{ID: 1, Rect: ports.Rect{W: 40, H: 40}},
132		{ID: 2, Floating: true, Rect: ports.Rect{X: 50, W: 20, H: 20}},
133	}}
134	tg := &target{valid: true, sceneSeq: 8, windows: map[ports.WindowID]heldWindow{1: {seq: 1, rect: image.Rect(0, 0, 40, 40)}, 2: {seq: 1, rect: image.Rect(50, 0, 70, 20)}}}
135	contents := map[ports.WindowID]ports.SurfaceContent{1: {Seq: 2}, 2: {Seq: 1}}
136	dmg := newDamage(tg, s, image.Rect(0, 0, 80, 60))
137	ds := r.draws(s, contents, dmg)
138	dmg.finish()
139	if dmg.all() || dmg.area != image.Rect(0, 0, 40, 40) {
140		t.Fatalf("damage = %v (full=%v)", dmg.area, dmg.all())
141	}
142	ds = dmg.clip(ds)
143	if len(ds) != 2 || ds[1].pc.color != [4]float32{0, 0, 0, 0.5} || ds[1].pc.rect != [4]int32{0, 0, 40, 40} {
144		t.Fatalf("clipped draws = %+v", ds)
145	}
146}
147
148// The actual blend must darken already painted pixels without darkening
149// floats, window popups or top layers. Also exercise a retained target:
150// content damage beneath the veil must replay it over that region.
151func TestRendererDimBlendAndDamage(t *testing.T) {
152	r, err := New(64, 48)
153	if err != nil {
154		t.Skipf("Vulkan unavailable: %v", err)
155	}
156	defer r.Close()
157	s := ports.Scene{Seq: 10, Background: "#ffffff", Dim: 0.5,
158		Windows: []ports.SceneWindow{
159			{ID: 1, Rect: ports.Rect{W: 20, H: 20}},
160			{ID: 2, Floating: true, Rect: ports.Rect{X: 30, W: 10, H: 10}},
161			{ID: 3, Popup: true, Rect: ports.Rect{X: 42, W: 8, H: 8}},
162		},
163		Layers: []ports.SceneLayer{{ID: 4, Layer: ports.LayerTop, Rect: ports.Rect{X: 52, W: 8, H: 8}}},
164	}
165	white := solidContent(t, 20, 20, color.RGBA{255, 255, 255, 255})
166	white.Seq = 1
167	popup := solidContent(t, 8, 8, color.RGBA{255, 255, 255, 255})
168	top := solidContent(t, 8, 8, color.RGBA{255, 255, 255, 255})
169	contents := map[ports.WindowID]ports.SurfaceContent{1: white, 3: popup, 4: top}
170	check := func() {
171		t.Helper()
172		if err := render(r, s, contents); err != nil {
173			t.Fatal(err)
174		}
175		for _, tc := range []struct {
176			x, y int
177			want color.RGBA
178		}{
179			{60, 40, color.RGBA{128, 128, 128, 255}}, // background
180			{5, 5, color.RGBA{128, 128, 128, 255}},   // tile
181			{43, 5, color.RGBA{255, 255, 255, 255}},  // window popup
182			{53, 5, color.RGBA{255, 255, 255, 255}},  // top layer
183		} {
184			if got := readPixels(t, r).RGBAAt(tc.x, tc.y); !near(got, tc.want, 2) {
185				t.Errorf("pixel (%d,%d) = %v, want %v", tc.x, tc.y, got, tc.want)
186			}
187		}
188	}
189	check()
190	// Reuse the target with unchanged scene Seq, but changed tile content.
191	// Red becomes 128 after the black veil, not 255 (missing veil).
192	for y := 2; y < 5; y++ {
193		for x := 2; x < 5; x++ {
194			if _, err := white.SHM.File.WriteAt([]byte{0, 0, 255, 255}, int64((y*20+x)*4)); err != nil {
195				t.Fatal(err)
196			}
197		}
198	}
199	white.Seq, white.Version = 2, 2
200	white.DamageHistory = []ports.SeqDamage{{Seq: 1, Full: true}, {Seq: 2, Rects: []ports.Rect{{X: 2, Y: 2, W: 3, H: 3}}}}
201	contents[1] = white
202	before := r.redrawn
203	if err := render(r, s, contents); err != nil {
204		t.Fatal(err)
205	}
206	if r.redrawn-before >= 64*48 {
207		t.Fatal("content change unexpectedly triggered full redraw")
208	}
209	if got := readPixels(t, r).RGBAAt(3, 3); !near(got, color.RGBA{128, 0, 0, 255}, 2) {
210		t.Fatalf("damaged tile beneath veil = %v, want dark red", got)
211	}
212}
213
214// A per-window veil darkens that window and its border only, also when
215// its content alone is redrawn on a retained target.
216func TestRendererWindowDimBlendAndDamage(t *testing.T) {
217	r, err := New(64, 48)
218	if err != nil {
219		t.Skipf("Vulkan unavailable: %v", err)
220	}
221	defer r.Close()
222	s := ports.Scene{Seq: 20, Background: "#ffffff", Border: ports.Border{Width: 1, Inactive: "#ffffff"},
223		Windows: []ports.SceneWindow{
224			{ID: 1, Floating: true, Dim: 0.5, Inset: ports.SideAll, Rect: ports.Rect{W: 20, H: 20}},
225			{ID: 2, Floating: true, Inset: ports.SideAll, Rect: ports.Rect{X: 30, W: 20, H: 20}},
226		},
227		Separators: []ports.Separator{
228			{Window: 1, Rect: ports.Rect{W: 20, H: 1}},
229			{Window: 2, Rect: ports.Rect{X: 30, W: 20, H: 1}},
230		},
231	}
232	peek := solidContent(t, 18, 18, color.RGBA{255, 255, 255, 255})
233	peek.Seq = 1
234	sel := solidContent(t, 18, 18, color.RGBA{255, 255, 255, 255})
235	contents := map[ports.WindowID]ports.SurfaceContent{1: peek, 2: sel}
236	grey, white := color.RGBA{128, 128, 128, 255}, color.RGBA{255, 255, 255, 255}
237	if err := render(r, s, contents); err != nil {
238		t.Fatal(err)
239	}
240	for _, tc := range []struct {
241		x, y int
242		want color.RGBA
243	}{{5, 5, grey}, {5, 0, grey}, {35, 5, white}, {35, 0, white}, {25, 30, white}} {
244		if got := readPixels(t, r).RGBAAt(tc.x, tc.y); !near(got, tc.want, 2) {
245			t.Errorf("pixel (%d,%d) = %v, want %v", tc.x, tc.y, got, tc.want)
246		}
247	}
248	// Damage inside the peek replays its veil: red becomes dark red.
249	if _, err := peek.SHM.File.WriteAt([]byte{0, 0, 255, 255}, int64((3*18+3)*4)); err != nil {
250		t.Fatal(err)
251	}
252	peek.Seq, peek.Version = 2, 2
253	peek.DamageHistory = []ports.SeqDamage{{Seq: 1, Full: true}, {Seq: 2, Rects: []ports.Rect{{X: 3, Y: 3, W: 1, H: 1}}}}
254	contents[1] = peek
255	if err := render(r, s, contents); err != nil {
256		t.Fatal(err)
257	}
258	if got := readPixels(t, r).RGBAAt(4, 4); !near(got, color.RGBA{128, 0, 0, 255}, 2) {
259		t.Fatalf("damaged peek = %v, want dark red", got)
260	}
261}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 25

Tags 10

↑↓ moveenter openesc close