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// A dimmed window gets a veil over its own rect, border included, right
68// after it: windows drawn later stay bright.
69func TestSceneWindowDim(t *testing.T) {
70	r := &Renderer{width: 80, height: 60}
71	s := ports.Scene{Border: ports.Border{Width: 1, Inactive: "#ffffff"},
72		Windows: []ports.SceneWindow{
73			{ID: 1, Floating: true, Dim: 0.5, Inset: ports.SideAll, Rect: ports.Rect{X: -10, W: 20, H: 20}},
74			{ID: 2, Floating: true, Inset: ports.SideAll, Rect: ports.Rect{X: 20, W: 20, H: 20}},
75		},
76		Separators: []ports.Separator{{Window: 1, Rect: ports.Rect{X: -10, W: 20, H: 1}}},
77	}
78	ds := r.draws(s, nil, newDamage(&target{}, s, image.Rect(0, 0, 80, 60)))
79	// Window 1, its border, its veil (clipped to the output), window 2.
80	if len(ds) != 4 {
81		t.Fatalf("draws = %d, want 4", len(ds))
82	}
83	if v := ds[2].pc; v.rect != [4]int32{0, 0, 10, 20} || v.color != [4]float32{0, 0, 0, 0.5} {
84		t.Fatalf("veil = %+v", v)
85	}
86	if ds[3].pc.rect[0] != 21 {
87		t.Fatalf("window 2 not last: %+v", ds[3].pc)
88	}
89}
90
91// HDR blends linear light: the dim alpha is raised so reference white
92// lands where SDR's encoded dim puts it.
93func TestDimAlpha(t *testing.T) {
94	if got := dimAlpha(0.5, false); got != 0.5 {
95		t.Fatalf("sdr %v", got)
96	}
97	for _, a := range []float64{0, 0.3, 0.5, 1} {
98		got := dimAlpha(a, true)
99		if want := 1 - srgbToLinear(1-a); math.Abs(got-want) > 1e-12 || got < a {
100			t.Fatalf("hdr %v: %v want %v", a, got, want)
101		}
102	}
103	if dimAlpha(2, true) != 1 || dimAlpha(-1, true) != 0 {
104		t.Fatal("not clamped")
105	}
106}
107
108func TestSceneDimPartialDamage(t *testing.T) {
109	r := &Renderer{width: 80, height: 60}
110	s := ports.Scene{Seq: 8, Dim: 0.5, Windows: []ports.SceneWindow{
111		{ID: 1, Rect: ports.Rect{W: 40, H: 40}},
112		{ID: 2, Floating: true, Rect: ports.Rect{X: 50, W: 20, H: 20}},
113	}}
114	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)}}}
115	contents := map[ports.WindowID]ports.SurfaceContent{1: {Seq: 2}, 2: {Seq: 1}}
116	dmg := newDamage(tg, s, image.Rect(0, 0, 80, 60))
117	ds := r.draws(s, contents, dmg)
118	dmg.finish()
119	if dmg.all() || dmg.area != image.Rect(0, 0, 40, 40) {
120		t.Fatalf("damage = %v (full=%v)", dmg.area, dmg.all())
121	}
122	ds = dmg.clip(ds)
123	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} {
124		t.Fatalf("clipped draws = %+v", ds)
125	}
126}
127
128// The actual blend must darken already painted pixels without darkening
129// floats, window popups or top layers. Also exercise a retained target:
130// content damage beneath the veil must replay it over that region.
131func TestRendererDimBlendAndDamage(t *testing.T) {
132	r, err := New(64, 48)
133	if err != nil {
134		t.Skipf("Vulkan unavailable: %v", err)
135	}
136	defer r.Close()
137	s := ports.Scene{Seq: 10, Background: "#ffffff", Dim: 0.5,
138		Windows: []ports.SceneWindow{
139			{ID: 1, Rect: ports.Rect{W: 20, H: 20}},
140			{ID: 2, Floating: true, Rect: ports.Rect{X: 30, W: 10, H: 10}},
141			{ID: 3, Popup: true, Rect: ports.Rect{X: 42, W: 8, H: 8}},
142		},
143		Layers: []ports.SceneLayer{{ID: 4, Layer: ports.LayerTop, Rect: ports.Rect{X: 52, W: 8, H: 8}}},
144	}
145	white := solidContent(t, 20, 20, color.RGBA{255, 255, 255, 255})
146	white.Seq = 1
147	popup := solidContent(t, 8, 8, color.RGBA{255, 255, 255, 255})
148	top := solidContent(t, 8, 8, color.RGBA{255, 255, 255, 255})
149	contents := map[ports.WindowID]ports.SurfaceContent{1: white, 3: popup, 4: top}
150	check := func() {
151		t.Helper()
152		if err := render(r, s, contents); err != nil {
153			t.Fatal(err)
154		}
155		for _, tc := range []struct {
156			x, y int
157			want color.RGBA
158		}{
159			{60, 40, color.RGBA{128, 128, 128, 255}}, // background
160			{5, 5, color.RGBA{128, 128, 128, 255}},   // tile
161			{43, 5, color.RGBA{255, 255, 255, 255}},  // window popup
162			{53, 5, color.RGBA{255, 255, 255, 255}},  // top layer
163		} {
164			if got := readPixels(t, r).RGBAAt(tc.x, tc.y); !near(got, tc.want, 2) {
165				t.Errorf("pixel (%d,%d) = %v, want %v", tc.x, tc.y, got, tc.want)
166			}
167		}
168	}
169	check()
170	// Reuse the target with unchanged scene Seq, but changed tile content.
171	// Red becomes 128 after the black veil, not 255 (missing veil).
172	for y := 2; y < 5; y++ {
173		for x := 2; x < 5; x++ {
174			if _, err := white.SHM.File.WriteAt([]byte{0, 0, 255, 255}, int64((y*20+x)*4)); err != nil {
175				t.Fatal(err)
176			}
177		}
178	}
179	white.Seq, white.Version = 2, 2
180	white.DamageHistory = []ports.SeqDamage{{Seq: 1, Full: true}, {Seq: 2, Rects: []ports.Rect{{X: 2, Y: 2, W: 3, H: 3}}}}
181	contents[1] = white
182	before := r.redrawn
183	if err := render(r, s, contents); err != nil {
184		t.Fatal(err)
185	}
186	if r.redrawn-before >= 64*48 {
187		t.Fatal("content change unexpectedly triggered full redraw")
188	}
189	if got := readPixels(t, r).RGBAAt(3, 3); !near(got, color.RGBA{128, 0, 0, 255}, 2) {
190		t.Fatalf("damaged tile beneath veil = %v, want dark red", got)
191	}
192}
193
194// A per-window veil darkens that window and its border only, also when
195// its content alone is redrawn on a retained target.
196func TestRendererWindowDimBlendAndDamage(t *testing.T) {
197	r, err := New(64, 48)
198	if err != nil {
199		t.Skipf("Vulkan unavailable: %v", err)
200	}
201	defer r.Close()
202	s := ports.Scene{Seq: 20, Background: "#ffffff", Border: ports.Border{Width: 1, Inactive: "#ffffff"},
203		Windows: []ports.SceneWindow{
204			{ID: 1, Floating: true, Dim: 0.5, Inset: ports.SideAll, Rect: ports.Rect{W: 20, H: 20}},
205			{ID: 2, Floating: true, Inset: ports.SideAll, Rect: ports.Rect{X: 30, W: 20, H: 20}},
206		},
207		Separators: []ports.Separator{
208			{Window: 1, Rect: ports.Rect{W: 20, H: 1}},
209			{Window: 2, Rect: ports.Rect{X: 30, W: 20, H: 1}},
210		},
211	}
212	peek := solidContent(t, 18, 18, color.RGBA{255, 255, 255, 255})
213	peek.Seq = 1
214	sel := solidContent(t, 18, 18, color.RGBA{255, 255, 255, 255})
215	contents := map[ports.WindowID]ports.SurfaceContent{1: peek, 2: sel}
216	grey, white := color.RGBA{128, 128, 128, 255}, color.RGBA{255, 255, 255, 255}
217	if err := render(r, s, contents); err != nil {
218		t.Fatal(err)
219	}
220	for _, tc := range []struct {
221		x, y int
222		want color.RGBA
223	}{{5, 5, grey}, {5, 0, grey}, {35, 5, white}, {35, 0, white}, {25, 30, white}} {
224		if got := readPixels(t, r).RGBAAt(tc.x, tc.y); !near(got, tc.want, 2) {
225			t.Errorf("pixel (%d,%d) = %v, want %v", tc.x, tc.y, got, tc.want)
226		}
227	}
228	// Damage inside the peek replays its veil: red becomes dark red.
229	if _, err := peek.SHM.File.WriteAt([]byte{0, 0, 255, 255}, int64((3*18+3)*4)); err != nil {
230		t.Fatal(err)
231	}
232	peek.Seq, peek.Version = 2, 2
233	peek.DamageHistory = []ports.SeqDamage{{Seq: 1, Full: true}, {Seq: 2, Rects: []ports.Rect{{X: 3, Y: 3, W: 1, H: 1}}}}
234	contents[1] = peek
235	if err := render(r, s, contents); err != nil {
236		t.Fatal(err)
237	}
238	if got := readPixels(t, r).RGBAAt(4, 4); !near(got, color.RGBA{128, 0, 0, 255}, 2) {
239		t.Fatalf("damaged peek = %v, want dark red", got)
240	}
241}

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