1package core
2
3import (
4 "math"
5 "slices"
6 "testing"
7
8 "github.com/bnema/neferwl/internal/ports"
9)
10
11func TestOverviewVerticalBoundariesAndInputs(t *testing.T) {
12 m := stackMonitor()
13 w := m.Current()
14 m.ToggleOverview()
15 if m.stackFront(w) != (stackItem{stackFloat, 9}) {
16 t.Fatal("screen front")
17 }
18 m.overviewFocus(ActionFocusWorkspacePrev)
19 if m.stackFront(w).kind != stackColumns || m.Active != 0 {
20 t.Fatal("swipe did not visit card")
21 }
22 m.overviewFocus(ActionFocusWorkspacePrev)
23 if m.Active != 0 {
24 t.Fatal("up at top crossed workspace boundary")
25 }
26 m.overviewFocus(ActionFocusWorkspaceNext)
27 if m.stackFront(w).kind != stackFloat || m.Active != 0 {
28 t.Fatal("down did not return to front")
29 }
30 m.OverviewMove(0, 1)
31 if m.Active != 1 {
32 t.Fatal("down did not reach workspace below")
33 }
34 m.OverviewMove(0, -1)
35 if m.Active != 0 || m.stackFront(w).kind != stackFloat {
36 t.Fatal("row change kept stack cursor")
37 }
38 if !m.overviewScroll(ports.PointerAxis{Source: ports.AxisWheel, Vertical: ports.ScrollAxis{Set: true, V120: -120}}) || m.stackFront(w).kind != stackColumns {
39 t.Fatal("wheel did not visit card")
40 }
41 m.CancelOverview()
42}
43
44func TestOverviewPeekEnvelopeAndSingleCardHorizontal(t *testing.T) {
45 m := stackMonitor()
46 w := m.Current()
47 w.AddFloating(8, 300, 200)
48 w.AddFloating(7, 300, 200)
49 m.ToggleOverview()
50 front := previewOf(t, m.Layout(), 7)
51 back := previewOf(t, m.Layout(), 8)
52 step := float64(front.Rect.Y - back.Rect.Y)
53 if step < float64(w.Usable.H)*w.overviewZoom(verticalAxis)*0.06 {
54 t.Fatalf("peek step %.1f", step)
55 }
56 h := w.rowHeight(verticalAxis)
57 y := w.Usable.Y + (w.Usable.H-h)/2
58 if back.Rect.Y < y || front.Rect.Y+front.Rect.H > y+h {
59 t.Fatalf("outside row front %+v back %+v row %d..%d", front, back, y, y+h)
60 }
61 m.OverviewMove(1, 0)
62 if m.stackFront(w) != (stackItem{stackFloat, 7}) {
63 t.Fatal("right moved single card")
64 }
65 m.OverviewMove(-1, 0)
66 if m.stackFront(w) != (stackItem{stackFloat, 8}) {
67 t.Fatal("left did not send single card behind")
68 }
69 m.CancelOverview()
70}
71
72// The stash front is centred on the same row band as the stack's front,
73// whether that row has peeking cards or just one column-group card.
74func TestOverviewPileAlignedWithRowFront(t *testing.T) {
75 for _, stacked := range []bool{false, true} {
76 m := pileMonitor()
77 w := m.Current()
78 if stacked {
79 m.SetBorder(2)
80 w.AddFloating(9, 300, 200)
81 }
82 m.ToggleOverview()
83 ps := m.Layout()
84 pile := previewOf(t, ps, 7)
85 frontID := WindowID(1)
86 if stacked {
87 frontID = 9
88 }
89 front := previewOf(t, ps, frontID)
90 pileCentre := float64(pile.Rect.Y) + float64(pile.Rect.H)/2
91 frontBandCentre := float64(front.Rect.Y) + float64(w.Usable.H)*front.Preview/2
92 if math.Abs(pileCentre-frontBandCentre) > 1.5 {
93 t.Fatalf("stacked %v: pile centre %.1f, front band centre %.1f: pile %+v front %+v", stacked, pileCentre, frontBandCentre, pile, front)
94 }
95 }
96}
97
98func TestOverviewSingleCardLeftFallsBackToStash(t *testing.T) {
99 m := stackMonitor()
100 w := m.Current()
101 w.FocusID(1)
102 w.ToggleWindowStash()
103 w.RemoveWindow(2)
104 w.RemoveWindow(3)
105 // A covering float is the only stack item; stash stays independently reachable.
106 m.ToggleOverview()
107 m.OverviewMove(-1, 0)
108 if m.cardAt(w) < 0 {
109 t.Fatal("left with no card behind did not enter stash")
110 }
111 m.CancelOverview()
112}
113
114func TestOverviewStackMRUAndSize(t *testing.T) {
115 m := maximizedOverview()
116 w := m.Current()
117 m.ToggleOverview()
118 m.OverviewMove(0, -1)
119 m.ToggleOverview()
120 m.ToggleOverview()
121 if got := w.stack(); !slices.Equal(got, []stackItem{{stackColumn, 1}, {kind: stackColumns}}) {
122 t.Fatalf("stack %v", got)
123 }
124 if m.hiddenColumn(w) != 1 {
125 t.Fatalf("MRU preselection %d, want Firefox column", m.hiddenColumn(w))
126 }
127 if p := previewOf(t, m.Layout(), 2); math.Abs(float64(p.Rect.W)-float64(w.columnRectsFor(true)[1].W)*p.Preview) > 1 {
128 t.Fatalf("prior maximized buffer not fitted to cell: %+v", p)
129 }
130 m.CancelOverview()
131}
132
133// browsedMonitor has window 1 on workspace 1 and columns 2..5 on workspace
134// 2, focused on 5 with one column per screen, and shows workspace 1.
135func browsedMonitor(ov Overflow) (*Monitor, *Workspace) {
136 m := newMonitor("", "")
137 m.SetOutput(300, 200)
138 m.SetMaxColumns(1)
139 m.AddWindow(1)
140 m.Focus(1)
141 for id := WindowID(2); id <= 5; id++ {
142 m.AddWindow(id)
143 }
144 ws2 := m.Workspaces[1]
145 ws2.Overflow = ov
146 ws2.FocusID(5)
147 ws2.scroll()
148 m.Focus(0)
149 return m, ws2
150}
151
152// Escape gives every row the overview showed its focus and scroll back,
153// as first shown, even after leaving and revisiting it.
154func TestOverviewBrowsedRowsRestored(t *testing.T) {
155 paths := map[string][][2]int{
156 "browse": {{0, 1}, {-1, 0}, {-1, 0}},
157 "revisit": {{0, 1}, {-1, 0}, {0, -1}, {0, 1}, {-1, 0}},
158 }
159 for _, ov := range []Overflow{OverflowScroll, OverflowFixed} {
160 for name, path := range paths {
161 m, ws2 := browsedMonitor(ov)
162 view := ws2.View
163 m.ToggleOverview()
164 for _, d := range path {
165 m.OverviewMove(d[0], d[1])
166 }
167 m.CancelOverview()
168 if id, _ := ws2.Focused(); id != 5 || ws2.View != view || m.Current() != m.Workspaces[0] {
169 t.Fatalf("%v %s: focus %d view %v (want %v) current %v", ov, name, id, ws2.View, view, m.Active)
170 }
171 }
172 }
173}
174
175func TestOverviewReturnRestoresBrowsedRows(t *testing.T) {
176 m, ws2 := browsedMonitor(OverflowScroll)
177 m.Focus(2)
178 m.AddWindow(6)
179 m.Focus(0)
180 m.ToggleOverview()
181 m.OverviewMove(0, 1)
182 m.OverviewMove(-1, 0)
183 m.OverviewMove(-1, 0)
184 m.OverviewMove(0, 1)
185 if m.Current() != m.Workspaces[2] {
186 t.Fatal("not on workspace 3")
187 }
188 m.ToggleOverview()
189 if id, _ := ws2.Focused(); id != 5 || m.Current() != m.Workspaces[2] {
190 t.Fatalf("focus %d current %v", id, m.Active)
191 }
192}