1package selection
2
3import (
4 "image"
5 "math"
6 "testing"
7
8 "github.com/stretchr/testify/assert"
9 "github.com/stretchr/testify/require"
10
11 "github.com/bnema/nefercap/internal/core"
12 "github.com/bnema/nefercap/internal/ports"
13)
14
15// modeModel is a one-output overlay whose output mode is mw x mh and whose
16// surface is w x h logical at scale.
17func modeModel(t *testing.T, mw, mh, w, h int, scale float64) *model {
18 t.Helper()
19 sess, _ := testSession()
20 outs := []ports.Output{{ID: 7, Name: "DP-1", Width: mw, Height: mh, Scale: 1}}
21 m := newModel(ports.Screenshot, outs, 0, sess, true)
22 m.resize(w, h, scale)
23 require.Equal(t, core.PickActive, m.picker.Status())
24 return m
25}
26
27// renderedGrid returns the scene's lines as the renderer places them.
28func renderedGrid(m *model) (g core.GridLines) {
29 for i, r := range m.scene().grid {
30 g[i] = physicalEdges(r, m.scale)
31 }
32 return g
33}
34
35func TestGridFollowsTheOutputModeNotTheBuffer(t *testing.T) {
36 // The logical surface is round(mode/scale); its buffer is ceil(logical*scale)
37 // and can be one pixel larger than the mode, which the compositor crops.
38 // The guides must sit on the visible mode's thirds.
39 want, err := core.MonitorGridPhysical(901, 601)
40 require.NoError(t, err)
41 for _, scale := range []float64{1.2, 1.25, 1.5, 2} {
42 w, h := int(math.Round(901/scale)), int(math.Round(601/scale))
43 m := modeModel(t, 901, 601, w, h, scale)
44 got := renderedGrid(m)
45 assert.Equal(t, want, got, "scale %g (%dx%d logical)", scale, w, h)
46 for i, x := range []int{300, 450, 600} {
47 assert.Equal(t, image.Rect(x, 0, x+1, 601), got[i], "scale %g vertical %d", scale, i)
48 }
49 for i, y := range []int{200, 300, 400} {
50 assert.Equal(t, image.Rect(0, y, 901, y+1), got[3+i], "scale %g horizontal %d", scale, i)
51 }
52 bw, _ := core.PhysicalExtent(w, scale)
53 bh, _ := core.PhysicalExtent(h, scale)
54 for i, r := range got {
55 assert.True(t, r.In(image.Rect(0, 0, bw, bh)), "scale %g line %d stays inside the %dx%d buffer", scale, i, bw, bh)
56 }
57 }
58}
59
60func TestGridFallsBackToTheBufferWithoutAMatchingMode(t *testing.T) {
61 buffer, err := core.MonitorGrid(751, 501, 1.2)
62 require.NoError(t, err)
63 for name, c := range map[string]struct{ mw, mh int }{
64 "unknown mode": {0, 0},
65 "negative mode": {-901, -601},
66 "width mismatches": {1920, 601},
67 "height mismatches": {901, 1080},
68 "stale config": {1920, 1080},
69 "one pixel off": {903, 601}, // 903/1.2 rounds to 753
70 } {
71 m := modeModel(t, c.mw, c.mh, 751, 501, 1.2)
72 assert.Equal(t, buffer, renderedGrid(m), name)
73 }
74}
75
76func TestGridModeIsCheckedAgainstEveryResize(t *testing.T) {
77 m := modeModel(t, 901, 601, 751, 501, 1.2)
78 mode, err := core.MonitorGridPhysical(901, 601)
79 require.NoError(t, err)
80 assert.Equal(t, mode, renderedGrid(m))
81
82 m.resize(751, 501, 1.25) // scale only: 901/1.25 is no longer 751
83 buffer, err := core.MonitorGrid(751, 501, 1.25)
84 require.NoError(t, err)
85 assert.Equal(t, buffer, renderedGrid(m), "the mode no longer explains the surface")
86
87 m.resize(721, 481, 1.25) // round(901/1.25) x round(601/1.25)
88 mode, err = core.MonitorGridPhysical(901, 601)
89 require.NoError(t, err)
90 assert.Equal(t, mode, renderedGrid(m), "and matches again when it does")
91}
92
93func TestGridUsesRotatedModeWhenItMatchesSwapped(t *testing.T) {
94 // A mode reported as 601x901 with a surface of 751x501 logical.
95 m := modeModel(t, 601, 901, 751, 501, 1.2)
96 want, err := core.MonitorGridPhysical(901, 601)
97 require.NoError(t, err)
98 assert.Equal(t, want, renderedGrid(m))
99
100 // An unrelated swapped mode is ignored.
101 m = modeModel(t, 1080, 1920, 751, 501, 1.2)
102 buffer, err := core.MonitorGrid(751, 501, 1.2)
103 require.NoError(t, err)
104 assert.Equal(t, buffer, renderedGrid(m))
105}
106
107func TestGridBufferMoreThanOnePixelPastTheModeFallsBack(t *testing.T) {
108 // At scale 3 the surface is round(901/3) = 300 wide, a 900 buffer: the
109 // mode is larger than the buffer, so the compositor resamples and the
110 // mode is not a safe position source.
111 m := modeModel(t, 901, 601, 300, 200, 3)
112 buffer, err := core.MonitorGrid(300, 200, 3)
113 require.NoError(t, err)
114 assert.Equal(t, buffer, renderedGrid(m))
115
116 // A surface size the mode does not round to: 1000/3 is 333, not 334.
117 m = modeModel(t, 1000, 1000, 334, 334, 3)
118 buffer, err = core.MonitorGrid(334, 334, 3)
119 require.NoError(t, err)
120 assert.Equal(t, buffer, renderedGrid(m))
121}
122
123func TestGridModePerOutputIsIndependent(t *testing.T) {
124 sess, _ := testSession()
125 outs := []ports.Output{
126 {ID: 7, Name: "DP-1", Width: 901, Height: 601, Scale: 1},
127 {ID: 9, Name: "HDMI-A-1", Width: 1280, Height: 720, Scale: 1},
128 }
129 a := newModel(ports.Screenshot, outs, 0, sess, true)
130 b := newModel(ports.Screenshot, outs, 1, sess, true)
131 a.resize(751, 501, 1.2)
132 b.resize(1280, 720, 1)
133 wantA, err := core.MonitorGridPhysical(901, 601)
134 require.NoError(t, err)
135 wantB, err := core.MonitorGridPhysical(1280, 720)
136 require.NoError(t, err)
137 assert.Equal(t, wantA, renderedGrid(a))
138 assert.Equal(t, wantB, renderedGrid(b))
139}
140
141func TestGridModeAllocations(t *testing.T) {
142 if raceEnabled {
143 t.Skip("the race detector adds allocations")
144 }
145 m := modeModel(t, 901, 601, 751, 501, 1.2)
146 n := testing.AllocsPerRun(200, func() {
147 m.resize(751, 501, 1.2)
148 m.resize(751, 501, 1.25)
149 _ = m.scene()
150 })
151 assert.Zero(t, n)
152}