1package core_test
2
3import (
4 "context"
5 "testing"
6
7 "github.com/bnema/neferwl/internal/adapters/config"
8 "github.com/bnema/neferwl/internal/core"
9 "github.com/bnema/neferwl/internal/ports"
10)
11
12// next returns the next command of type T matching ok, skipping others.
13func next[T ports.ClientCommand](t *testing.T, ch <-chan ports.ClientCommand, ok func(T) bool) T {
14 t.Helper()
15 for {
16 if v, is := command(t, ch).(T); is && ok(v) {
17 return v
18 }
19 }
20}
21
22func anyOf[T any](T) bool { return true }
23
24// Bars and launchers get the pointer, popups and, on click, the keyboard.
25func TestLayerPointerPopupAndFocus(t *testing.T) {
26 cfg := config.Defaults()
27 cfg.Border.Width = 0
28 client := make(chan ports.ClientEvent, 8)
29 input := make(chan ports.InputEvent, 8)
30 output := make(chan ports.OutputEvent, 8)
31 commands := make(chan ports.ClientCommand, 256)
32 scenes := make(chan []ports.Scene, 1)
33 c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes})
34 if err != nil {
35 t.Fatal(err)
36 }
37 ctx, cancel := context.WithCancel(context.Background())
38 defer cancel()
39 go c.Run(ctx)
40 output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "OUT-1", Width: 100, Height: 80}}
41 scene(t, scenes)
42 client <- ports.WindowMapped{ID: 1}
43 scene(t, scenes)
44 if f := next(t, commands, anyOf[ports.FocusWindow]); f.ID != 1 {
45 t.Fatalf("focus %d", f.ID)
46 }
47 bar := ports.LayerSurface{ID: 6, Layer: ports.LayerTop, Anchor: ports.AnchorTop | ports.AnchorLeft | ports.AnchorRight, Width: 100, Height: 10, ExclusiveZone: 10, Keyboard: 2}
48 client <- ports.LayerChanged{Layers: []ports.LayerSurface{bar}}
49 scene(t, scenes)
50
51 // The bar gets the pointer, in its own coordinates.
52 input <- ports.PointerMotion{X: 50, Y: 5}
53 if v := next(t, commands, anyOf[ports.PointerFocus]); v.ID != 6 || v.X != 50 || v.Y != 5 {
54 t.Fatalf("pointer focus %+v, want the bar", v)
55 }
56 // A click gives the on-demand bar the keyboard.
57 input <- ports.PointerButton{Button: 0x110, Pressed: true}
58 if v := next(t, commands, anyOf[ports.PointerButtonTo]); v.ID != 6 {
59 t.Fatalf("button to %d", v.ID)
60 }
61 if f := next(t, commands, anyOf[ports.FocusWindow]); f.ID != 6 {
62 t.Fatalf("focus %d, want the bar", f.ID)
63 }
64 input <- ports.PointerButton{Button: 0x110}
65
66 // A menu from the bar is placed under it and slides inside the output.
67 pos := ports.Positioner{Width: 30, Height: 20, AnchorRect: ports.Rect{X: 90, Y: 0, W: 10, H: 10}, Anchor: ports.EdgeBottomLeft, Gravity: ports.EdgeBottomRight, Adjust: ports.AdjustSlideX}
68 client <- ports.PopupRequest{ID: 9, Parent: 6, Positioner: pos, Grab: true}
69 cp := next(t, commands, func(v ports.ConfigurePopup) bool { return v.ID == 9 })
70 if cp.Rect != (ports.Rect{X: 70, Y: 10, W: 30, H: 20}) {
71 t.Fatalf("popup rect %+v", cp.Rect)
72 }
73 client <- ports.PopupMapped{ID: 9}
74 sceneMatch(t, scenes, func(sc ports.Scene) bool {
75 w := sc.Windows[len(sc.Windows)-1]
76 return w.Popup && w.ID == 9 && w.Rect == (ports.Rect{X: 70, Y: 10, W: 30, H: 20})
77 })
78 if f := next(t, commands, anyOf[ports.FocusWindow]); f.ID != 9 {
79 t.Fatalf("grab focus %d", f.ID)
80 }
81 input <- ports.PointerMotion{X: 80, Y: 15}
82 if v := next(t, commands, anyOf[ports.PointerFocus]); v.ID != 9 || v.X != 10 || v.Y != 5 {
83 t.Fatalf("pointer focus %+v, want the menu", v)
84 }
85
86 // A click on the window closes the menu and takes the keyboard back.
87 input <- ports.PointerMotion{X: 20, Y: 50}
88 if v := next(t, commands, anyOf[ports.PointerFocus]); v.ID != 1 {
89 t.Fatalf("pointer focus %+v, want the window", v)
90 }
91 input <- ports.PointerButton{Button: 0x110, Pressed: true}
92 if v := next(t, commands, anyOf[ports.ClosePopup]); v.ID != 9 {
93 t.Fatalf("closed %d", v.ID)
94 }
95 if f := next(t, commands, anyOf[ports.FocusWindow]); f.ID != 1 {
96 t.Fatalf("focus %d, want the window", f.ID)
97 }
98 input <- ports.PointerButton{Button: 0x110}
99
100 // An exclusive launcher wins over a clicked on-demand bar.
101 input <- ports.PointerMotion{X: 50, Y: 5}
102 next(t, commands, func(v ports.PointerFocus) bool { return v.ID == 6 })
103 input <- ports.PointerButton{Button: 0x110, Pressed: true}
104 next(t, commands, func(v ports.FocusWindow) bool { return v.ID == 6 })
105 input <- ports.PointerButton{Button: 0x110}
106 launcher := ports.LayerSurface{ID: 7, Layer: ports.LayerOverlay, Width: 20, Height: 20, Keyboard: 1}
107 client <- ports.LayerChanged{Layers: []ports.LayerSurface{bar, launcher}}
108 if f := next(t, commands, anyOf[ports.FocusWindow]); f.ID != 7 {
109 t.Fatalf("focus %d, want the launcher", f.ID)
110 }
111 client <- ports.LayerChanged{Layers: []ports.LayerSurface{bar}}
112 if f := next(t, commands, anyOf[ports.FocusWindow]); f.ID != 6 {
113 t.Fatalf("focus %d, want the bar back", f.ID)
114 }
115
116 // Unmapped then mapped again, the bar does not take the keyboard back.
117 client <- ports.LayerChanged{}
118 next(t, commands, func(v ports.FocusWindow) bool { return v.ID == 1 })
119 client <- ports.LayerChanged{Layers: []ports.LayerSurface{bar}}
120 scene(t, scenes)
121 input <- ports.PointerMotion{X: 20, Y: 50} // publishes
122 next(t, commands, func(v ports.PointerFocus) bool { return v.ID == 1 })
123 select {
124 case v := <-commands:
125 if f, ok := v.(ports.FocusWindow); ok {
126 t.Fatalf("focus moved to %d", f.ID)
127 }
128 default:
129 }
130
131 // A window focus change by keyboard or a new window takes it back too.
132 input <- ports.PointerMotion{X: 50, Y: 5}
133 next(t, commands, func(v ports.PointerFocus) bool { return v.ID == 6 })
134 input <- ports.PointerButton{Button: 0x110, Pressed: true}
135 next(t, commands, func(v ports.FocusWindow) bool { return v.ID == 6 })
136 input <- ports.PointerButton{Button: 0x110}
137 client <- ports.WindowMapped{ID: 2}
138 if f := next(t, commands, anyOf[ports.FocusWindow]); f.ID != 2 {
139 t.Fatalf("focus %d, want the new window", f.ID)
140 }
141}
142
143// A window's menu stays with the windows: an overlay layer covers it.
144func TestWindowPopupUnderOverlay(t *testing.T) {
145 cfg := config.Defaults()
146 cfg.Border.Width = 0
147 client := make(chan ports.ClientEvent, 8)
148 input := make(chan ports.InputEvent, 8)
149 output := make(chan ports.OutputEvent, 8)
150 commands := make(chan ports.ClientCommand, 256)
151 scenes := make(chan []ports.Scene, 1)
152 c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes})
153 if err != nil {
154 t.Fatal(err)
155 }
156 ctx, cancel := context.WithCancel(context.Background())
157 defer cancel()
158 go c.Run(ctx)
159 output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "OUT-1", Width: 100, Height: 80}}
160 scene(t, scenes)
161 client <- ports.WindowMapped{ID: 1}
162 scene(t, scenes)
163 client <- ports.PopupRequest{ID: 9, Parent: 1, Positioner: ports.Positioner{Width: 30, Height: 20, AnchorRect: ports.Rect{X: 0, Y: 0, W: 10, H: 10}}}
164 next(t, commands, func(v ports.ConfigurePopup) bool { return v.ID == 9 })
165 client <- ports.PopupMapped{ID: 9}
166 sc := sceneMatch(t, scenes, func(sc ports.Scene) bool { return len(sc.Windows) == 2 })
167 if w := sc.Windows[1]; !w.Popup || w.OverLayers {
168 t.Fatalf("window popup %+v", w)
169 }
170 osd := ports.LayerSurface{ID: 7, Layer: ports.LayerOverlay, Anchor: 15}
171 client <- ports.LayerChanged{Layers: []ports.LayerSurface{osd}}
172 scene(t, scenes)
173 input <- ports.PointerMotion{X: 5, Y: 5}
174 if v := next(t, commands, anyOf[ports.PointerMotionTo]); v.ID != 7 {
175 t.Fatalf("hit %d, want the overlay", v.ID)
176 }
177}
178
179// A fullscreen window hides top and bottom layers from the pointer, not
180// overlay; background and bottom layers get it only where no window is.
181func TestLayerHitOrder(t *testing.T) {
182 cfg := config.Defaults()
183 cfg.Border.Width = 0
184 client := make(chan ports.ClientEvent, 8)
185 input := make(chan ports.InputEvent, 8)
186 output := make(chan ports.OutputEvent, 8)
187 commands := make(chan ports.ClientCommand, 256)
188 scenes := make(chan []ports.Scene, 1)
189 c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes})
190 if err != nil {
191 t.Fatal(err)
192 }
193 ctx, cancel := context.WithCancel(context.Background())
194 defer cancel()
195 go c.Run(ctx)
196 output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "OUT-1", Width: 100, Height: 80}}
197 scene(t, scenes)
198 wallpaper := ports.LayerSurface{ID: 5, Layer: ports.LayerBackground, Anchor: 15}
199 bar := ports.LayerSurface{ID: 6, Layer: ports.LayerTop, Anchor: ports.AnchorTop | ports.AnchorLeft | ports.AnchorRight, Height: 10}
200 osd := ports.LayerSurface{ID: 7, Layer: ports.LayerOverlay, Anchor: ports.AnchorBottom, Width: 20, Height: 10}
201 client <- ports.LayerChanged{Layers: []ports.LayerSurface{wallpaper, bar, osd}}
202 scene(t, scenes)
203 hit := func(x, y float64, want ports.WindowID) {
204 t.Helper()
205 input <- ports.PointerMotion{X: x, Y: y}
206 // A focus change is sent only when the target changes; motion always.
207 if v := next(t, commands, anyOf[ports.PointerMotionTo]); v.ID != want {
208 t.Fatalf("(%v,%v) hit %d, want %d", x, y, v.ID, want)
209 }
210 }
211 hit(50, 40, 5)
212 hit(50, 5, 6)
213 client <- ports.WindowMapped{ID: 1}
214 scene(t, scenes)
215 hit(50, 40, 1)
216 hit(50, 5, 6)
217 hit(50, 75, 7)
218 client <- ports.WindowFullscreenRequest{ID: 1, Fullscreen: true}
219 scene(t, scenes)
220 hit(50, 5, 1)
221 hit(50, 75, 7)
222}
223
224// The pointer moving to another output does not take the keyboard from a
225// clicked on-demand layer: keys still reach it.
226func TestLayerFocusAcrossOutputs(t *testing.T) {
227 cfg := config.Defaults()
228 cfg.Border.Width = 0
229 client := make(chan ports.ClientEvent, 8)
230 input := make(chan ports.InputEvent, 8)
231 output := make(chan ports.OutputEvent, 8)
232 commands := make(chan ports.ClientCommand, 256)
233 scenes := make(chan []ports.Scene, 1)
234 c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes})
235 if err != nil {
236 t.Fatal(err)
237 }
238 ctx, cancel := context.WithCancel(context.Background())
239 defer cancel()
240 go c.Run(ctx)
241 output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "OUT-1", Width: 100, Height: 80}}
242 scene(t, scenes)
243 output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "OUT-2", Width: 100, Height: 80}}
244 scene(t, scenes)
245 client <- ports.WindowMapped{ID: 1}
246 scene(t, scenes)
247 bar := ports.LayerSurface{ID: 6, Layer: ports.LayerTop, Anchor: ports.AnchorTop | ports.AnchorLeft | ports.AnchorRight, Height: 10, Keyboard: 2, Output: "OUT-1"}
248 client <- ports.LayerChanged{Layers: []ports.LayerSurface{bar}}
249 scene(t, scenes)
250 input <- ports.PointerMotion{X: 50, Y: 5}
251 next(t, commands, func(v ports.PointerFocus) bool { return v.ID == 6 })
252 input <- ports.PointerButton{Button: 0x110, Pressed: true}
253 next(t, commands, func(v ports.FocusWindow) bool { return v.ID == 6 })
254 input <- ports.PointerButton{Button: 0x110}
255 input <- ports.PointerMotion{X: 100, Y: 40} // onto OUT-2, now focused
256 next(t, commands, func(v ports.PointerFocus) bool { return v.ID == 0 })
257 input <- ports.KeyEvent{Keysym: "a", Pressed: true}
258 if v := next(t, commands, anyOf[ports.ForwardKey]); v.ID != 6 {
259 t.Fatalf("key to %d, want the bar", v.ID)
260 }
261 input <- ports.KeyEvent{Keysym: "a"}
262 next(t, commands, anyOf[ports.ForwardKey])
263
264 // A window mapped on OUT-2 (focused there) takes the keyboard back.
265 client <- ports.WindowMapped{ID: 2}
266 next(t, commands, func(v ports.FocusWindow) bool { return v.ID == 2 })
267 input <- ports.KeyEvent{Keysym: "b", Pressed: true}
268 if v := next(t, commands, anyOf[ports.ForwardKey]); v.ID != 2 {
269 t.Fatalf("key to %d, want the new window", v.ID)
270 }
271}