scene_fade_test.go RawHistoryFolder

1package vulkan
2
3import (
4	"image"
5	"math"
6	"slices"
7	"testing"
8
9	"github.com/bnema/neferwl/internal/ports"
10)
11
12// A window's Fade scales its body fill, its own border lines, its dim and
13// its content by 1-Fade (premultiplied); at 1 the window draws nothing. The
14// walk needs no Vulkan device: a solid single-pixel buffer imports nothing.
15func TestSceneWindowFade(t *testing.T) {
16	near := func(a, b float32) bool { return math.Abs(float64(a-b)) < 1e-6 }
17	walk := func(r *Renderer, fade float64) []draw {
18		s := ports.Scene{
19			Background: "#ffffff", Dim: 0, Border: ports.Border{Width: 1, Active: "#ffffff", Inactive: "#808080"},
20			Windows: []ports.SceneWindow{
21				{ID: 1, Rect: ports.Rect{W: 40, H: 60}},
22				{ID: 2, Floating: true, Rect: ports.Rect{X: 40, W: 20, H: 20}, Inset: ports.SideLeft | ports.SideRight | ports.SideTop | ports.SideBottom, Fade: fade, Dim: 0.5},
23			},
24			Separators: []ports.Separator{{Rect: ports.Rect{X: 40, W: 1, H: 20}, Window: 2}},
25		}
26		contents := map[ports.WindowID]ports.SurfaceContent{
27			2: {ID: 2, Width: 1, Height: 1, LogicalW: 18, LogicalH: 18, Solid: &ports.SolidColor{R: 1, G: 0.5, B: 0, A: 1}},
28		}
29		// The renderer reuses its draw buffer: each walk's result is kept.
30		return slices.Clone(r.draws(s, contents, newDamage(&target{}, s, image.Rect(0, 0, 80, 60))))
31	}
32	r := &Renderer{width: 80, height: 60}
33	opaque := walk(r, 0)
34	// Tile fill, float body fill, float content, float line, float dim.
35	if len(opaque) != 5 {
36		t.Fatalf("draws = %d, want 5: %+v", len(opaque), opaque)
37	}
38	half := walk(r, 0.5)
39	if len(half) != 5 {
40		t.Fatalf("faded draws = %d, want 5", len(half))
41	}
42	if half[0].pc.color != opaque[0].pc.color {
43		t.Fatalf("the tile's fill changed with the float's fade: %v", half[0].pc.color)
44	}
45	for i := 1; i < 5; i++ {
46		for ch := range 4 {
47			if !near(half[i].pc.color[ch], opaque[i].pc.color[ch]*0.5) {
48				t.Fatalf("draw %d channel %d: %v, want half of %v", i, ch, half[i].pc.color, opaque[i].pc.color)
49			}
50		}
51		if half[i].pc.rect != opaque[i].pc.rect {
52			t.Fatalf("draw %d moved with the fade: %v vs %v", i, half[i].pc.rect, opaque[i].pc.rect)
53		}
54	}
55	if got := half[2].pc.misc[0]; got != modeSolid {
56		t.Fatalf("content draw mode %d", got)
57	}
58	// Faded out: nothing of the float, the tile unchanged.
59	gone := walk(r, 1)
60	if len(gone) != 1 || gone[0].pc.color != opaque[0].pc.color {
61		t.Fatalf("faded-out draws = %+v, want the tile only", gone)
62	}
63	// A second window after the faded one draws at full alpha.
64	s := ports.Scene{Background: "#ffffff", Windows: []ports.SceneWindow{
65		{ID: 1, Rect: ports.Rect{W: 40, H: 60}, Fade: 0.25},
66		{ID: 2, Rect: ports.Rect{X: 40, W: 40, H: 60}},
67	}}
68	ds := r.draws(s, nil, newDamage(&target{}, s, image.Rect(0, 0, 80, 60)))
69	if len(ds) != 2 || !near(ds[0].pc.color[3], 0.75) || ds[1].pc.color[3] != 1 {
70		t.Fatalf("alpha leaked past the faded window: %+v", ds)
71	}
72}
73
74// On an HDR output a solid's rgb is re-encoded so the shader's decode
75// yields the faded linear premultiplied color.
76func TestSceneWindowFadeHDR(t *testing.T) {
77	r := &Renderer{width: 80, height: 60, hdrNits: 203}
78	s := ports.Scene{Background: "#ffffff", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 40, H: 60}, Fade: 0.5}}}
79	ds := r.draws(s, nil, newDamage(&target{}, s, image.Rect(0, 0, 80, 60)))
80	if len(ds) != 1 {
81		t.Fatalf("draws = %d", len(ds))
82	}
83	want := float32(linearToSRGB(0.5))
84	c := ds[0].pc.color
85	if math.Abs(float64(c[0]-want)) > 1e-6 || c[0] != c[1] || c[1] != c[2] || c[3] != 0.5 {
86		t.Fatalf("hdr faded white = %v, want rgb %v alpha 0.5", c, want)
87	}
88}
89
90// An appearing or leaving float carries a Zoom, not a Preview: it still
91// opens the floats, so the tile lines and the veil are painted under it,
92// exactly as for the same float unzoomed; only its content draw shrinks.
93func TestSceneZoomedFloatOpensFloats(t *testing.T) {
94	r := &Renderer{width: 80, height: 60}
95	scene := func(zoom float64) ports.Scene {
96		return ports.Scene{
97			Dim: 0.4, Background: "#000000", Border: ports.Border{Width: 1, Active: "#ffffff", Inactive: "#808080"},
98			Windows: []ports.SceneWindow{
99				{ID: 1, Rect: ports.Rect{W: 80, H: 60}},
100				{ID: 2, Floating: true, Rect: ports.Rect{X: 20, Y: 10, W: 40, H: 40}, Inset: ports.SideAll, Fade: 0.5, Zoom: zoom},
101			},
102			Separators: []ports.Separator{
103				{Rect: ports.Rect{X: 40, W: 1, H: 60}},
104				{Rect: ports.Rect{X: 20, Y: 10, W: 1, H: 40}, Window: 2},
105			},
106		}
107	}
108	contents := map[ports.WindowID]ports.SurfaceContent{
109		2: {ID: 2, Width: 1, Height: 1, LogicalW: 38, LogicalH: 38, Solid: &ports.SolidColor{R: 1, G: 1, B: 1, A: 1}},
110	}
111	walk := func(s ports.Scene) []draw {
112		return slices.Clone(r.draws(s, contents, newDamage(&target{}, s, image.Rect(0, 0, 80, 60))))
113	}
114	plain, zoomed := walk(scene(0)), walk(scene(0.9))
115	// tile fill, tile line, veil, float fill, float content, float line
116	if len(plain) != 6 || len(zoomed) != 6 {
117		t.Fatalf("draws: plain %d zoomed %d, want 6", len(plain), len(zoomed))
118	}
119	for i := range plain {
120		if i == 4 {
121			continue
122		}
123		if plain[i].pc != zoomed[i].pc {
124			t.Fatalf("draw %d differs with the zoom: %+v vs %+v", i, plain[i].pc, zoomed[i].pc)
125		}
126	}
127	if veil := zoomed[2].pc; veil.rect != [4]int32{0, 0, 80, 60} || veil.color != [4]float32{0, 0, 0, 0.4} {
128		t.Fatalf("veil = %+v, want the full-output 0.4 dim under the float", veil)
129	}
130	if zoomed[1].pc.rect != [4]int32{40, 0, 41, 60} {
131		t.Fatalf("tile line = %+v, want it painted before the float", zoomed[1].pc.rect)
132	}
133	pr, zr := plain[4].pc.rect, zoomed[4].pc.rect
134	if !(zr[2]-zr[0] < pr[2]-pr[0]) || zr[0] != pr[0] || zr[1] != pr[1] {
135		t.Fatalf("zoomed content %v, want smaller than %v from the same origin", zr, pr)
136	}
137	// A card (Preview) keeps not opening the floats: no tile lines, no veil.
138	card := scene(0)
139	card.Windows[1].Preview = 0.9
140	if ds := walk(card); len(ds) != 5 || ds[1].pc.color[3] != 0.5 {
141		t.Fatalf("card draws = %d, want 5 (lines after, no veil): %+v", len(ds), ds)
142	}
143}

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 26

Tags 10

↑↓ moveenter openesc close