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