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}
144
145func TestRoundedWindowDraws(t *testing.T) {
146 r := &Renderer{width: 80, height: 60}
147 s := ports.Scene{Scale: 1, Background: "#ffffff", Border: ports.Border{Width: 3, Active: "#ff8800", Inactive: "#888888"},
148 Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{X: 8, Y: 8, W: 40, H: 40}, Inset: ports.SideAll, Radius: 10, Dim: 0.5}},
149 Separators: []ports.Separator{{Window: 1, Active: true}}}
150 ds := r.draws(s, nil, newDamage(&target{}, s, image.Rect(0, 0, 80, 60)))
151 if len(ds) != 3 {
152 t.Fatalf("draws %d", len(ds))
153 }
154 for i, d := range ds {
155 if d.pc.misc[1]&flagRounded == 0 {
156 t.Fatalf("draw %d not rounded", i)
157 }
158 }
159 if ds[0].pc.misc[1]&flagRing == 0 || math.Float32frombits(ds[0].radii[0]) != 10 || math.Float32frombits(ds[0].radii[1]) != 6 {
160 t.Fatal("ring radii")
161 }
162 if ds[1].round != roundRect(image.Rect(11, 11, 45, 45)) {
163 t.Fatal("body clip")
164 }
165 var b composeBatch
166 if !b.add(ds[0], 64, 64) || b.instances[0].round != ds[0].round || b.instances[0].resource[1] != ds[0].radii[0] {
167 t.Fatal("batch loses clip")
168 }
169}
170
171// The ring's hole runs 1px under the body when the window is opaque; faded,
172// that overlap would show twice, so the hole is the body itself.
173func TestRoundedRingHoleFadedVersusOpaque(t *testing.T) {
174 r := &Renderer{width: 80, height: 60}
175 ring := func(fade float64) draw {
176 s := ports.Scene{Scale: 1, Background: "#ffffff", Border: ports.Border{Width: 3, Active: "#ff8800", Inactive: "#888888"},
177 Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{X: 8, Y: 8, W: 40, H: 40}, Inset: ports.SideAll, Radius: 10, Fade: fade}},
178 Separators: []ports.Separator{{Window: 1, Active: true}}}
179 ds := r.draws(s, nil, newDamage(&target{}, s, image.Rect(0, 0, 80, 60)))
180 if len(ds) == 0 || ds[0].pc.misc[1]&flagRing == 0 {
181 t.Fatalf("fade %v: no ring: %+v", fade, ds)
182 }
183 return ds[0]
184 }
185 opaque, faded := ring(0), ring(0.5)
186 if opaque.roundInner != roundRect(image.Rect(12, 12, 44, 44)) || math.Float32frombits(opaque.radii[1]) != 6 {
187 t.Fatalf("opaque hole %v radius %v", opaque.roundInner, math.Float32frombits(opaque.radii[1]))
188 }
189 if faded.roundInner != roundRect(image.Rect(11, 11, 45, 45)) || math.Float32frombits(faded.radii[1]) != 7 {
190 t.Fatalf("faded hole %v radius %v", faded.roundInner, math.Float32frombits(faded.radii[1]))
191 }
192}