feat(layer): pointer input, popups and on-demand focus for layer surfaces
Hit-test popups, then overlay/top layers (a fullscreen window hides top), then windows, then bottom/background layers. The wayland adapter sends enter, leave, motion, button and axis to layer surfaces, and cursor-shape and set_cursor accept them. zwlr_layer_surface_v1.get_popup parents an xdg_popup on the layer; core places it relative to the layer rect, clamped to the output, and the renderer draws popups above all layers. Grabs accept a press on the layer. Clicking an on-demand layer gives it the keyboard until a window is clicked; exclusive layers still win.
- commit
- 2273fad6aa997c68e5fede199bb12b3b916055c0
- author
- brice · · 2026-09-26 05:50 UTC
- committer
- brice · · 2026-09-26 06:05 UTC
- parent
- 7c67dfb
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
12 files · +524 -71
M internal/adapters/vulkan/renderer.go +9 -3
| @@ -515,9 +515,6 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa | |||
| 515 | 515 | } | |
| 516 | 516 | x, y := w.Rect.X, w.Rect.Y | |
| 517 | 517 | if w.Popup { | |
| 518 | - | // Menus draw only what the client drew, shadows clipped. | |
| 519 | - | content := contents[w.ID] | |
| 520 | - | place(&content, x, y, w.Rect.W, w.Rect.H) | |
| 521 | 518 | continue | |
| 522 | 519 | } | |
| 523 | 520 | // Content sits inside the border; core sized the client to match. | |
| @@ -552,6 +549,15 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa | |||
| 552 | 549 | } | |
| 553 | 550 | } | |
| 554 | 551 | addLayers(true) | |
| 552 | + | // Popups go over everything, layers included: a bar's menu shows over | |
| 553 | + | // the bar and the windows. | |
| 554 | + | for _, w := range s.Windows { | |
| 555 | + | if w.Popup && !w.Hidden && w.Rect.W > 0 && w.Rect.H > 0 { | |
| 556 | + | // Menus draw only what the client drew, shadows clipped. | |
| 557 | + | content := contents[w.ID] | |
| 558 | + | place(&content, w.Rect.X, w.Rect.Y, w.Rect.W, w.Rect.H) | |
| 559 | + | } | |
| 560 | + | } | |
| 555 | 561 | if err := r.ensureStaging(used); err != nil { | |
| 556 | 562 | return err | |
| 557 | 563 | } | |
| @@ -515,9 +515,6 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa | |||||
| 515 | } | 515 | } | ||
| 516 | x, y := w.Rect.X, w.Rect.Y | 516 | x, y := w.Rect.X, w.Rect.Y | ||
| 517 | if w.Popup { | 517 | if w.Popup { | ||
| 518 | - | // Menus draw only what the client drew, shadows clipped. | |||
| 519 | - | content := contents[w.ID] | |||
| 520 | - | place(&content, x, y, w.Rect.W, w.Rect.H) | |||
| 521 | continue | 518 | continue | ||
| 522 | } | 519 | } | ||
| 523 | // Content sits inside the border; core sized the client to match. | 520 | // Content sits inside the border; core sized the client to match. | ||
| @@ -552,6 +549,15 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa | |||||
| 552 | } | 549 | } | ||
| 553 | } | 550 | } | ||
| 554 | addLayers(true) | 551 | addLayers(true) | ||
| 552 | + | // Popups go over everything, layers included: a bar's menu shows over | |||
| 553 | + | // the bar and the windows. | |||
| 554 | + | for _, w := range s.Windows { | |||
| 555 | + | if w.Popup && !w.Hidden && w.Rect.W > 0 && w.Rect.H > 0 { | |||
| 556 | + | // Menus draw only what the client drew, shadows clipped. | |||
| 557 | + | content := contents[w.ID] | |||
| 558 | + | place(&content, w.Rect.X, w.Rect.Y, w.Rect.W, w.Rect.H) | |||
| 559 | + | } | |||
| 560 | + | } | |||
| 555 | if err := r.ensureStaging(used); err != nil { | 561 | if err := r.ensureStaging(used); err != nil { | ||
| 556 | return err | 562 | return err | ||
| 557 | } | 563 | } | ||
M internal/adapters/wayland/cursor.go +4 -3
| @@ -100,10 +100,11 @@ func (p pointer) SetCursor(r *wayland.Pointer, _ uint32, surf *wayland.Surface, | |||
| 100 | 100 | } | |
| 101 | 101 | func (pointer) Release(r *wayland.Pointer) { r.Destroy() } | |
| 102 | 102 | ||
| 103 | - | // hasPointerFocus reports whether the client owns the window under the pointer. | |
| 103 | + | // hasPointerFocus reports whether the client owns the window or layer | |
| 104 | + | // under the pointer. | |
| 104 | 105 | func (s *Server) hasPointerFocus(c server.Client) bool { | |
| 105 | - | w := s.windows[s.pointerFocus] | |
| 106 | - | return w != nil && w.mapped && w.xdg.resource.Client() == c | |
| 106 | + | owner, _, ok := s.pointerTarget(s.pointerFocus) | |
| 107 | + | return ok && owner == c | |
| 107 | 108 | } | |
| 108 | 109 | ||
| 109 | 110 | // cursorImage copies the committed shm buffer of a cursor surface. No | |
| @@ -100,10 +100,11 @@ func (p pointer) SetCursor(r *wayland.Pointer, _ uint32, surf *wayland.Surface, | |||||
| 100 | } | 100 | } | ||
| 101 | func (pointer) Release(r *wayland.Pointer) { r.Destroy() } | 101 | func (pointer) Release(r *wayland.Pointer) { r.Destroy() } | ||
| 102 | 102 | ||||
| 103 | - | // hasPointerFocus reports whether the client owns the window under the pointer. | 103 | + | // hasPointerFocus reports whether the client owns the window or layer |
| 104 | + | // under the pointer. | |||
| 104 | func (s *Server) hasPointerFocus(c server.Client) bool { | 105 | func (s *Server) hasPointerFocus(c server.Client) bool { | ||
| 105 | - | w := s.windows[s.pointerFocus] | 106 | + | owner, _, ok := s.pointerTarget(s.pointerFocus) |
| 106 | - | return w != nil && w.mapped && w.xdg.resource.Client() == c | 107 | + | return ok && owner == c |
| 107 | } | 108 | } | ||
| 108 | 109 | ||||
| 109 | // cursorImage copies the committed shm buffer of a cursor surface. No | 110 | // cursorImage copies the committed shm buffer of a cursor surface. No | ||
M internal/adapters/wayland/globals.go +6 -5
| @@ -361,11 +361,12 @@ func (h seat) GetPointer(r *wayland.Seat, id uint32) { | |||
| 361 | 361 | s.pointers[r.Client()] = list | |
| 362 | 362 | } | |
| 363 | 363 | } | |
| 364 | - | if w := s.windows[s.pointerFocus]; w != nil && w.mapped && w.xdg.resource.Client() == r.Client() { | |
| 365 | - | s.serial++ | |
| 366 | - | x, y := w.surfacePoint(s.pointerX, s.pointerY) | |
| 367 | - | p.SendEnter(s.serial, w.xdg.surfaceResource(), server.FixedFromFloat(x), server.FixedFromFloat(y)) | |
| 368 | - | pointerFrame(p) | |
| 364 | + | if s.hasPointerFocus(r.Client()) { | |
| 365 | + | if surf, _, x, y := s.pointerSurface(s.pointerFocus, s.pointerX, s.pointerY); surf != nil { | |
| 366 | + | s.serial++ | |
| 367 | + | p.SendEnter(s.serial, surf, server.FixedFromFloat(x), server.FixedFromFloat(y)) | |
| 368 | + | pointerFrame(p) | |
| 369 | + | } | |
| 369 | 370 | } | |
| 370 | 371 | } | |
| 371 | 372 | func (h seat) GetKeyboard(r *wayland.Seat, id uint32) { | |
| @@ -361,11 +361,12 @@ func (h seat) GetPointer(r *wayland.Seat, id uint32) { | |||||
| 361 | s.pointers[r.Client()] = list | 361 | s.pointers[r.Client()] = list | ||
| 362 | } | 362 | } | ||
| 363 | } | 363 | } | ||
| 364 | - | if w := s.windows[s.pointerFocus]; w != nil && w.mapped && w.xdg.resource.Client() == r.Client() { | 364 | + | if s.hasPointerFocus(r.Client()) { |
| 365 | - | s.serial++ | 365 | + | if surf, _, x, y := s.pointerSurface(s.pointerFocus, s.pointerX, s.pointerY); surf != nil { |
| 366 | - | x, y := w.surfacePoint(s.pointerX, s.pointerY) | 366 | + | s.serial++ |
| 367 | - | p.SendEnter(s.serial, w.xdg.surfaceResource(), server.FixedFromFloat(x), server.FixedFromFloat(y)) | 367 | + | p.SendEnter(s.serial, surf, server.FixedFromFloat(x), server.FixedFromFloat(y)) |
| 368 | - | pointerFrame(p) | 368 | + | pointerFrame(p) |
| 369 | + | } | |||
| 369 | } | 370 | } | ||
| 370 | } | 371 | } | ||
| 371 | func (h seat) GetKeyboard(r *wayland.Seat, id uint32) { | 372 | func (h seat) GetKeyboard(r *wayland.Seat, id uint32) { | ||
M internal/adapters/wayland/layer.go +28 -2
| @@ -106,8 +106,31 @@ func (l *layerSurface) SetKeyboardInteractivity(r *wlrlayershell.ZwlrLayerSurfac | |||
| 106 | 106 | l.pending.keyboard = k | |
| 107 | 107 | } | |
| 108 | 108 | ||
| 109 | - | // Layer popups are deferred; layer-shell is capped at v4. | |
| 110 | - | func (*layerSurface) GetPopup(*wlrlayershell.ZwlrLayerSurfaceV1, *xdgshell.Popup) {} | |
| 109 | + | // GetPopup makes the layer surface the parent of an xdg_popup created | |
| 110 | + | // without one. Core places it relative to the layer. | |
| 111 | + | func (l *layerSurface) GetPopup(_ *wlrlayershell.ZwlrLayerSurfaceV1, r *xdgshell.Popup) { | |
| 112 | + | if r == nil { | |
| 113 | + | return | |
| 114 | + | } | |
| 115 | + | for _, w := range l.shell.server.windows { | |
| 116 | + | if p := w.popup; p != nil && p.resource.Resource == r.Resource { | |
| 117 | + | if p.parent == nil && !p.sent && !p.done { | |
| 118 | + | p.layer = l | |
| 119 | + | } | |
| 120 | + | return | |
| 121 | + | } | |
| 122 | + | } | |
| 123 | + | } | |
| 124 | + | ||
| 125 | + | // surfaceResource is the layer's live wl_surface, if any. | |
| 126 | + | func (l *layerSurface) surfaceResource() *wayland.Surface { | |
| 127 | + | for resource, state := range l.shell.server.surfaces { | |
| 128 | + | if state == l.surface && resource.Alive() { | |
| 129 | + | return wayland.WrapSurface(resource) | |
| 130 | + | } | |
| 131 | + | } | |
| 132 | + | return nil | |
| 133 | + | } | |
| 111 | 134 | func (l *layerSurface) AckConfigure(r *wlrlayershell.ZwlrLayerSurfaceV1, serial uint32) { | |
| 112 | 135 | for i, v := range l.serials { | |
| 113 | 136 | if v == serial { | |
| @@ -136,6 +159,9 @@ func (l *layerSurface) unmap() { | |||
| 136 | 159 | if s := l.shell.server; s.focused == l.id { | |
| 137 | 160 | s.changeFocus(0) | |
| 138 | 161 | } | |
| 162 | + | if s := l.shell.server; s.pointerFocus == l.id { | |
| 163 | + | s.changePointerFocus(0, 0, 0) | |
| 164 | + | } | |
| 139 | 165 | l.mapped = false | |
| 140 | 166 | l.configured = false | |
| 141 | 167 | l.acked = false | |
| @@ -106,8 +106,31 @@ func (l *layerSurface) SetKeyboardInteractivity(r *wlrlayershell.ZwlrLayerSurfac | |||||
| 106 | l.pending.keyboard = k | 106 | l.pending.keyboard = k | ||
| 107 | } | 107 | } | ||
| 108 | 108 | ||||
| 109 | - | // Layer popups are deferred; layer-shell is capped at v4. | 109 | + | // GetPopup makes the layer surface the parent of an xdg_popup created |
| 110 | - | func (*layerSurface) GetPopup(*wlrlayershell.ZwlrLayerSurfaceV1, *xdgshell.Popup) {} | 110 | + | // without one. Core places it relative to the layer. |
| 111 | + | func (l *layerSurface) GetPopup(_ *wlrlayershell.ZwlrLayerSurfaceV1, r *xdgshell.Popup) { | |||
| 112 | + | if r == nil { | |||
| 113 | + | return | |||
| 114 | + | } | |||
| 115 | + | for _, w := range l.shell.server.windows { | |||
| 116 | + | if p := w.popup; p != nil && p.resource.Resource == r.Resource { | |||
| 117 | + | if p.parent == nil && !p.sent && !p.done { | |||
| 118 | + | p.layer = l | |||
| 119 | + | } | |||
| 120 | + | return | |||
| 121 | + | } | |||
| 122 | + | } | |||
| 123 | + | } | |||
| 124 | + | ||||
| 125 | + | // surfaceResource is the layer's live wl_surface, if any. | |||
| 126 | + | func (l *layerSurface) surfaceResource() *wayland.Surface { | |||
| 127 | + | for resource, state := range l.shell.server.surfaces { | |||
| 128 | + | if state == l.surface && resource.Alive() { | |||
| 129 | + | return wayland.WrapSurface(resource) | |||
| 130 | + | } | |||
| 131 | + | } | |||
| 132 | + | return nil | |||
| 133 | + | } | |||
| 111 | func (l *layerSurface) AckConfigure(r *wlrlayershell.ZwlrLayerSurfaceV1, serial uint32) { | 134 | func (l *layerSurface) AckConfigure(r *wlrlayershell.ZwlrLayerSurfaceV1, serial uint32) { | ||
| 112 | for i, v := range l.serials { | 135 | for i, v := range l.serials { | ||
| 113 | if v == serial { | 136 | if v == serial { | ||
| @@ -136,6 +159,9 @@ func (l *layerSurface) unmap() { | |||||
| 136 | if s := l.shell.server; s.focused == l.id { | 159 | if s := l.shell.server; s.focused == l.id { | ||
| 137 | s.changeFocus(0) | 160 | s.changeFocus(0) | ||
| 138 | } | 161 | } | ||
| 162 | + | if s := l.shell.server; s.pointerFocus == l.id { | |||
| 163 | + | s.changePointerFocus(0, 0, 0) | |||
| 164 | + | } | |||
| 139 | l.mapped = false | 165 | l.mapped = false | ||
| 140 | l.configured = false | 166 | l.configured = false | ||
| 141 | l.acked = false | 167 | l.acked = false | ||
M internal/adapters/wayland/layer_test.go +115 -0
| @@ -5,6 +5,7 @@ import ( | |||
| 5 | 5 | "github.com/bnema/nefertty/internal/ports" | |
| 6 | 6 | "github.com/bnema/purego-libwayland/protocol/wayland" | |
| 7 | 7 | "github.com/bnema/purego-libwayland/protocol/wlrlayershell" | |
| 8 | + | "github.com/bnema/purego-libwayland/protocol/xdgshell" | |
| 8 | 9 | "github.com/bnema/wlturbo" | |
| 9 | 10 | "golang.org/x/sys/unix" | |
| 10 | 11 | "os" | |
| @@ -231,3 +232,117 @@ func TestXDGOutputDone(t *testing.T) { | |||
| 231 | 232 | }) | |
| 232 | 233 | } | |
| 233 | 234 | } | |
| 235 | + | ||
| 236 | + | type layerPointerProxy struct { | |
| 237 | + | wlturbo.BaseProxy | |
| 238 | + | enters chan [2]float64 | |
| 239 | + | buttons chan uint32 // serials | |
| 240 | + | } | |
| 241 | + | ||
| 242 | + | func (p *layerPointerProxy) Dispatch(e *wlturbo.Event) { | |
| 243 | + | switch e.Opcode { | |
| 244 | + | case uint16(wayland.PointerEventEnter): | |
| 245 | + | _, _ = e.Uint32(), e.Uint32() | |
| 246 | + | p.enters <- [2]float64{e.Fixed().Float64(), e.Fixed().Float64()} | |
| 247 | + | case uint16(wayland.PointerEventButton): | |
| 248 | + | p.buttons <- e.Uint32() | |
| 249 | + | } | |
| 250 | + | } | |
| 251 | + | ||
| 252 | + | // A bar gets the pointer, and a menu it opens on a click through | |
| 253 | + | // get_popup reaches core with the bar as its parent and a grab. | |
| 254 | + | func TestLayerPointerAndPopup(t *testing.T) { | |
| 255 | + | s, events, commands, dir := lifecycleServer(t) | |
| 256 | + | c := protocolClient(t, s, dir) | |
| 257 | + | seat := bindVersion(t, c, "wl_seat", 8) | |
| 258 | + | registerProtocol(t, c, seat) | |
| 259 | + | pointer := c.AllocateID() | |
| 260 | + | pp := &layerPointerProxy{enters: make(chan [2]float64, 1), buttons: make(chan uint32, 1)} | |
| 261 | + | pp.SetID(pointer) | |
| 262 | + | c.Context().Register(pp) | |
| 263 | + | requestProtocol(t, c, seat, wayland.SeatRequestGetPointer, pointer) | |
| 264 | + | comp := bindProtocol(t, c, "wl_compositor") | |
| 265 | + | wm := bindProtocol(t, c, "xdg_wm_base") | |
| 266 | + | shm := bindProtocol(t, c, "wl_shm") | |
| 267 | + | registerProtocol(t, c, shm) | |
| 268 | + | fd, err := unix.MemfdCreate("layer-popup-buffer", 0) | |
| 269 | + | if err != nil { | |
| 270 | + | t.Fatal(err) | |
| 271 | + | } | |
| 272 | + | defer unix.Close(fd) | |
| 273 | + | if err = unix.Ftruncate(fd, 4); err != nil { | |
| 274 | + | t.Fatal(err) | |
| 275 | + | } | |
| 276 | + | pool, buf := c.AllocateID(), c.AllocateID() | |
| 277 | + | registerProtocol(t, c, pool) | |
| 278 | + | registerProtocol(t, c, buf) | |
| 279 | + | if err = c.SendRequestWithFDs(shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, int32(4)); err != nil { | |
| 280 | + | t.Fatal(err) | |
| 281 | + | } | |
| 282 | + | requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, buf, int32(0), int32(1), int32(1), int32(4), uint32(0)) | |
| 283 | + | ||
| 284 | + | surf, layer := layerProtocol(t, c) | |
| 285 | + | lp := &layerProxy{configured: make(chan [3]uint32, 2)} | |
| 286 | + | lp.SetID(layer) | |
| 287 | + | c.Context().Register(lp) | |
| 288 | + | requestProtocol(t, c, layer, wlrlayershell.ZwlrLayerSurfaceV1RequestSetSize, uint32(1), uint32(1)) | |
| 289 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |
| 290 | + | if err = c.Roundtrip(); err != nil { | |
| 291 | + | t.Fatal(err) | |
| 292 | + | } | |
| 293 | + | var config [3]uint32 | |
| 294 | + | select { | |
| 295 | + | case config = <-lp.configured: | |
| 296 | + | case <-time.After(2 * time.Second): | |
| 297 | + | t.Fatal("configure timeout") | |
| 298 | + | } | |
| 299 | + | requestProtocol(t, c, layer, wlrlayershell.ZwlrLayerSurfaceV1RequestAckConfigure, config[0]) | |
| 300 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, buf, int32(0), int32(0)) | |
| 301 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |
| 302 | + | if err = c.Roundtrip(); err != nil { | |
| 303 | + | t.Fatal(err) | |
| 304 | + | } | |
| 305 | + | lc, ok := waitEvent(t, events, 2*time.Second).(ports.LayerChanged) | |
| 306 | + | if !ok || len(lc.Layers) != 1 { | |
| 307 | + | t.Fatalf("layer mapped %+v", lc) | |
| 308 | + | } | |
| 309 | + | id := lc.Layers[0].ID | |
| 310 | + | ||
| 311 | + | commands <- ports.PointerFocus{ID: id, X: 0.5, Y: 0.25} | |
| 312 | + | commands <- ports.PointerButtonTo{ID: id, Button: 0x110, Pressed: true} | |
| 313 | + | var press uint32 | |
| 314 | + | for deadline := time.Now().Add(2 * time.Second); len(pp.buttons) == 0; time.Sleep(5 * time.Millisecond) { | |
| 315 | + | if time.Now().After(deadline) { | |
| 316 | + | t.Fatal("pointer events timeout") | |
| 317 | + | } | |
| 318 | + | if err = c.Roundtrip(); err != nil { | |
| 319 | + | t.Fatal(err) | |
| 320 | + | } | |
| 321 | + | } | |
| 322 | + | if at := <-pp.enters; at != [2]float64{0.5, 0.25} { | |
| 323 | + | t.Fatalf("enter at %v", at) | |
| 324 | + | } | |
| 325 | + | press = <-pp.buttons | |
| 326 | + | ||
| 327 | + | positioner := c.AllocateID() | |
| 328 | + | requestProtocol(t, c, wm, xdgshell.WmBaseRequestCreatePositioner, positioner) | |
| 329 | + | requestProtocol(t, c, positioner, xdgshell.PositionerRequestSetSize, int32(1), int32(1)) | |
| 330 | + | requestProtocol(t, c, positioner, xdgshell.PositionerRequestSetAnchorRect, int32(0), int32(0), int32(1), int32(1)) | |
| 331 | + | done := make(chan struct{}, 1) | |
| 332 | + | xdgWindow(t, c, comp, wm, buf, func(pxdg uint32) { | |
| 333 | + | popup := c.AllocateID() | |
| 334 | + | proxy := &popupDoneProxy{done: done} | |
| 335 | + | proxy.SetID(popup) | |
| 336 | + | c.Context().Register(proxy) | |
| 337 | + | requestProtocol(t, c, pxdg, xdgshell.SurfaceRequestGetPopup, popup, uint32(0), positioner) | |
| 338 | + | requestProtocol(t, c, layer, wlrlayershell.ZwlrLayerSurfaceV1RequestGetPopup, popup) | |
| 339 | + | requestProtocol(t, c, popup, xdgshell.PopupRequestGrab, seat, press) | |
| 340 | + | }) | |
| 341 | + | req, ok := waitEvent(t, events, 2*time.Second).(ports.PopupRequest) | |
| 342 | + | if !ok || req.Parent != id || !req.Grab { | |
| 343 | + | t.Fatalf("popup request %+v, want parent %d with a grab", req, id) | |
| 344 | + | } | |
| 345 | + | if len(done) != 0 { | |
| 346 | + | t.Fatal("layer popup dismissed") | |
| 347 | + | } | |
| 348 | + | } | |
| @@ -5,6 +5,7 @@ import ( | |||||
| 5 | "github.com/bnema/nefertty/internal/ports" | 5 | "github.com/bnema/nefertty/internal/ports" | ||
| 6 | "github.com/bnema/purego-libwayland/protocol/wayland" | 6 | "github.com/bnema/purego-libwayland/protocol/wayland" | ||
| 7 | "github.com/bnema/purego-libwayland/protocol/wlrlayershell" | 7 | "github.com/bnema/purego-libwayland/protocol/wlrlayershell" | ||
| 8 | + | "github.com/bnema/purego-libwayland/protocol/xdgshell" | |||
| 8 | "github.com/bnema/wlturbo" | 9 | "github.com/bnema/wlturbo" | ||
| 9 | "golang.org/x/sys/unix" | 10 | "golang.org/x/sys/unix" | ||
| 10 | "os" | 11 | "os" | ||
| @@ -231,3 +232,117 @@ func TestXDGOutputDone(t *testing.T) { | |||||
| 231 | }) | 232 | }) | ||
| 232 | } | 233 | } | ||
| 233 | } | 234 | } | ||
| 235 | + | ||||
| 236 | + | type layerPointerProxy struct { | |||
| 237 | + | wlturbo.BaseProxy | |||
| 238 | + | enters chan [2]float64 | |||
| 239 | + | buttons chan uint32 // serials | |||
| 240 | + | } | |||
| 241 | + | ||||
| 242 | + | func (p *layerPointerProxy) Dispatch(e *wlturbo.Event) { | |||
| 243 | + | switch e.Opcode { | |||
| 244 | + | case uint16(wayland.PointerEventEnter): | |||
| 245 | + | _, _ = e.Uint32(), e.Uint32() | |||
| 246 | + | p.enters <- [2]float64{e.Fixed().Float64(), e.Fixed().Float64()} | |||
| 247 | + | case uint16(wayland.PointerEventButton): | |||
| 248 | + | p.buttons <- e.Uint32() | |||
| 249 | + | } | |||
| 250 | + | } | |||
| 251 | + | ||||
| 252 | + | // A bar gets the pointer, and a menu it opens on a click through | |||
| 253 | + | // get_popup reaches core with the bar as its parent and a grab. | |||
| 254 | + | func TestLayerPointerAndPopup(t *testing.T) { | |||
| 255 | + | s, events, commands, dir := lifecycleServer(t) | |||
| 256 | + | c := protocolClient(t, s, dir) | |||
| 257 | + | seat := bindVersion(t, c, "wl_seat", 8) | |||
| 258 | + | registerProtocol(t, c, seat) | |||
| 259 | + | pointer := c.AllocateID() | |||
| 260 | + | pp := &layerPointerProxy{enters: make(chan [2]float64, 1), buttons: make(chan uint32, 1)} | |||
| 261 | + | pp.SetID(pointer) | |||
| 262 | + | c.Context().Register(pp) | |||
| 263 | + | requestProtocol(t, c, seat, wayland.SeatRequestGetPointer, pointer) | |||
| 264 | + | comp := bindProtocol(t, c, "wl_compositor") | |||
| 265 | + | wm := bindProtocol(t, c, "xdg_wm_base") | |||
| 266 | + | shm := bindProtocol(t, c, "wl_shm") | |||
| 267 | + | registerProtocol(t, c, shm) | |||
| 268 | + | fd, err := unix.MemfdCreate("layer-popup-buffer", 0) | |||
| 269 | + | if err != nil { | |||
| 270 | + | t.Fatal(err) | |||
| 271 | + | } | |||
| 272 | + | defer unix.Close(fd) | |||
| 273 | + | if err = unix.Ftruncate(fd, 4); err != nil { | |||
| 274 | + | t.Fatal(err) | |||
| 275 | + | } | |||
| 276 | + | pool, buf := c.AllocateID(), c.AllocateID() | |||
| 277 | + | registerProtocol(t, c, pool) | |||
| 278 | + | registerProtocol(t, c, buf) | |||
| 279 | + | if err = c.SendRequestWithFDs(shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, int32(4)); err != nil { | |||
| 280 | + | t.Fatal(err) | |||
| 281 | + | } | |||
| 282 | + | requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, buf, int32(0), int32(1), int32(1), int32(4), uint32(0)) | |||
| 283 | + | ||||
| 284 | + | surf, layer := layerProtocol(t, c) | |||
| 285 | + | lp := &layerProxy{configured: make(chan [3]uint32, 2)} | |||
| 286 | + | lp.SetID(layer) | |||
| 287 | + | c.Context().Register(lp) | |||
| 288 | + | requestProtocol(t, c, layer, wlrlayershell.ZwlrLayerSurfaceV1RequestSetSize, uint32(1), uint32(1)) | |||
| 289 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |||
| 290 | + | if err = c.Roundtrip(); err != nil { | |||
| 291 | + | t.Fatal(err) | |||
| 292 | + | } | |||
| 293 | + | var config [3]uint32 | |||
| 294 | + | select { | |||
| 295 | + | case config = <-lp.configured: | |||
| 296 | + | case <-time.After(2 * time.Second): | |||
| 297 | + | t.Fatal("configure timeout") | |||
| 298 | + | } | |||
| 299 | + | requestProtocol(t, c, layer, wlrlayershell.ZwlrLayerSurfaceV1RequestAckConfigure, config[0]) | |||
| 300 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, buf, int32(0), int32(0)) | |||
| 301 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |||
| 302 | + | if err = c.Roundtrip(); err != nil { | |||
| 303 | + | t.Fatal(err) | |||
| 304 | + | } | |||
| 305 | + | lc, ok := waitEvent(t, events, 2*time.Second).(ports.LayerChanged) | |||
| 306 | + | if !ok || len(lc.Layers) != 1 { | |||
| 307 | + | t.Fatalf("layer mapped %+v", lc) | |||
| 308 | + | } | |||
| 309 | + | id := lc.Layers[0].ID | |||
| 310 | + | ||||
| 311 | + | commands <- ports.PointerFocus{ID: id, X: 0.5, Y: 0.25} | |||
| 312 | + | commands <- ports.PointerButtonTo{ID: id, Button: 0x110, Pressed: true} | |||
| 313 | + | var press uint32 | |||
| 314 | + | for deadline := time.Now().Add(2 * time.Second); len(pp.buttons) == 0; time.Sleep(5 * time.Millisecond) { | |||
| 315 | + | if time.Now().After(deadline) { | |||
| 316 | + | t.Fatal("pointer events timeout") | |||
| 317 | + | } | |||
| 318 | + | if err = c.Roundtrip(); err != nil { | |||
| 319 | + | t.Fatal(err) | |||
| 320 | + | } | |||
| 321 | + | } | |||
| 322 | + | if at := <-pp.enters; at != [2]float64{0.5, 0.25} { | |||
| 323 | + | t.Fatalf("enter at %v", at) | |||
| 324 | + | } | |||
| 325 | + | press = <-pp.buttons | |||
| 326 | + | ||||
| 327 | + | positioner := c.AllocateID() | |||
| 328 | + | requestProtocol(t, c, wm, xdgshell.WmBaseRequestCreatePositioner, positioner) | |||
| 329 | + | requestProtocol(t, c, positioner, xdgshell.PositionerRequestSetSize, int32(1), int32(1)) | |||
| 330 | + | requestProtocol(t, c, positioner, xdgshell.PositionerRequestSetAnchorRect, int32(0), int32(0), int32(1), int32(1)) | |||
| 331 | + | done := make(chan struct{}, 1) | |||
| 332 | + | xdgWindow(t, c, comp, wm, buf, func(pxdg uint32) { | |||
| 333 | + | popup := c.AllocateID() | |||
| 334 | + | proxy := &popupDoneProxy{done: done} | |||
| 335 | + | proxy.SetID(popup) | |||
| 336 | + | c.Context().Register(proxy) | |||
| 337 | + | requestProtocol(t, c, pxdg, xdgshell.SurfaceRequestGetPopup, popup, uint32(0), positioner) | |||
| 338 | + | requestProtocol(t, c, layer, wlrlayershell.ZwlrLayerSurfaceV1RequestGetPopup, popup) | |||
| 339 | + | requestProtocol(t, c, popup, xdgshell.PopupRequestGrab, seat, press) | |||
| 340 | + | }) | |||
| 341 | + | req, ok := waitEvent(t, events, 2*time.Second).(ports.PopupRequest) | |||
| 342 | + | if !ok || req.Parent != id || !req.Grab { | |||
| 343 | + | t.Fatalf("popup request %+v, want parent %d with a grab", req, id) | |||
| 344 | + | } | |||
| 345 | + | if len(done) != 0 { | |||
| 346 | + | t.Fatal("layer popup dismissed") | |||
| 347 | + | } | |||
| 348 | + | } | |||
M internal/adapters/wayland/popup.go +33 -16
| @@ -4,6 +4,7 @@ import ( | |||
| 4 | 4 | "github.com/bnema/nefertty/internal/ports" | |
| 5 | 5 | "github.com/bnema/purego-libwayland/protocol/wayland" | |
| 6 | 6 | "github.com/bnema/purego-libwayland/protocol/xdgshell" | |
| 7 | + | "github.com/bnema/purego-libwayland/server" | |
| 7 | 8 | ) | |
| 8 | 9 | ||
| 9 | 10 | // popup is the xdg_popup role of a window. Core places it (PopupRequest → | |
| @@ -13,8 +14,11 @@ type popup struct { | |||
| 13 | 14 | w *window | |
| 14 | 15 | resource *xdgshell.Popup | |
| 15 | 16 | parent *window | |
| 16 | - | pos ports.Positioner | |
| 17 | - | grab bool | |
| 17 | + | // layer is the parent set through zwlr_layer_surface_v1.get_popup, | |
| 18 | + | // for a popup created without an xdg parent. | |
| 19 | + | layer *layerSurface | |
| 20 | + | pos ports.Positioner | |
| 21 | + | grab bool | |
| 18 | 22 | // sent is set once core got the PopupRequest; done once dismissed. | |
| 19 | 23 | sent, done bool | |
| 20 | 24 | } | |
| @@ -42,11 +46,6 @@ func (s *Server) newPopup(x *xdgSurface, r *xdgshell.Surface, id uint32, parentX | |||
| 42 | 46 | s.nextWindow++ | |
| 43 | 47 | s.windows[w.id] = w | |
| 44 | 48 | x.window = w | |
| 45 | - | // Without a parent (xdg-shell lets clients set it through another | |
| 46 | - | // protocol, such as layer-shell) the popup has nowhere to go. | |
| 47 | - | if parent == nil { | |
| 48 | - | pp.dismiss() | |
| 49 | - | } | |
| 50 | 49 | x.surface.role = func(buffer bool) { | |
| 51 | 50 | if x.surface.destroyed { | |
| 52 | 51 | w.unmap() | |
| @@ -54,14 +53,21 @@ func (s *Server) newPopup(x *xdgSurface, r *xdgshell.Surface, id uint32, parentX | |||
| 54 | 53 | } | |
| 55 | 54 | switch { | |
| 56 | 55 | case !buffer && !w.mapped && !x.configured && !pp.done: | |
| 56 | + | // Without a parent (xdg-shell lets clients set it through | |
| 57 | + | // layer-shell before the first commit) the popup has nowhere | |
| 58 | + | // to go. | |
| 59 | + | if pp.parentID() == 0 { | |
| 60 | + | pp.dismiss() | |
| 61 | + | return | |
| 62 | + | } | |
| 57 | 63 | if !pp.sent { | |
| 58 | 64 | pp.sent = true | |
| 59 | - | s.emit(ports.PopupRequest{ID: w.id, Parent: parent.id, Positioner: pp.pos, Grab: pp.grab}) | |
| 65 | + | s.emit(ports.PopupRequest{ID: w.id, Parent: pp.parentID(), Positioner: pp.pos, Grab: pp.grab}) | |
| 60 | 66 | } | |
| 61 | 67 | case buffer && !w.mapped && x.acked && !pp.done: | |
| 62 | 68 | w.mapped = true | |
| 63 | 69 | s.emit(ports.PopupMapped{ID: w.id}) | |
| 64 | - | s.log.Debug().Uint64("id", uint64(w.id)).Uint64("parent", uint64(parent.id)).Msg("popup mapped") | |
| 70 | + | s.log.Debug().Uint64("id", uint64(w.id)).Uint64("parent", uint64(pp.parentID())).Msg("popup mapped") | |
| 65 | 71 | case !buffer && w.mapped: | |
| 66 | 72 | // Unmapped, it may map again after a new initial commit. | |
| 67 | 73 | w.unmap() | |
| @@ -88,6 +94,17 @@ func (s *Server) newPopup(x *xdgSurface, r *xdgshell.Surface, id uint32, parentX | |||
| 88 | 94 | ||
| 89 | 95 | func (*popup) Destroy(*xdgshell.Popup) {} | |
| 90 | 96 | ||
| 97 | + | // parentID is the window or layer surface the popup hangs from, 0 if none. | |
| 98 | + | func (p *popup) parentID() ports.WindowID { | |
| 99 | + | switch { | |
| 100 | + | case p.parent != nil: | |
| 101 | + | return p.parent.id | |
| 102 | + | case p.layer != nil: | |
| 103 | + | return p.layer.id | |
| 104 | + | } | |
| 105 | + | return 0 | |
| 106 | + | } | |
| 107 | + | ||
| 91 | 108 | // Grab is only valid before the first commit; core then gives the popup the | |
| 92 | 109 | // keyboard and closes it on a click outside its popup chain. | |
| 93 | 110 | func (p *popup) Grab(r *xdgshell.Popup, _ *wayland.Seat, serial uint32) { | |
| @@ -99,7 +116,7 @@ func (p *popup) Grab(r *xdgshell.Popup, _ *wayland.Seat, serial uint32) { | |||
| 99 | 116 | // and a popup over a popup grabs only if its parent does. | |
| 100 | 117 | s := p.w.xdg.server | |
| 101 | 118 | fresh := serial == s.press && s.pressClient == r.Client() | |
| 102 | - | if !fresh || p.parent == nil || !s.topGrab(p.parent) { | |
| 119 | + | if !fresh || p.parentID() == 0 || !s.topGrab(r.Client(), p.parent) { | |
| 103 | 120 | p.dismiss() | |
| 104 | 121 | return | |
| 105 | 122 | } | |
| @@ -114,7 +131,7 @@ func (p *popup) Reposition(r *xdgshell.Popup, pos *xdgshell.Positioner, token ui | |||
| 114 | 131 | } | |
| 115 | 132 | p.pos = next | |
| 116 | 133 | if p.sent { | |
| 117 | - | s.emit(ports.PopupRequest{ID: p.w.id, Parent: p.parent.id, Positioner: next, Grab: p.grab, Reposition: true, Token: token}) | |
| 134 | + | s.emit(ports.PopupRequest{ID: p.w.id, Parent: p.parentID(), Positioner: next, Grab: p.grab, Reposition: true, Token: token}) | |
| 118 | 135 | } | |
| 119 | 136 | } | |
| 120 | 137 | ||
| @@ -138,10 +155,10 @@ func (p *popup) configure(c ports.ConfigurePopup) { | |||
| 138 | 155 | } | |
| 139 | 156 | } | |
| 140 | 157 | ||
| 141 | - | // topGrab reports whether a grabbing popup may go on parent: parent is | |
| 142 | - | // the client's newest live grabbing popup, or a toplevel when it has none. | |
| 143 | - | func (s *Server) topGrab(parent *window) bool { | |
| 144 | - | client := parent.xdg.resource.Client() | |
| 158 | + | // topGrab reports whether a grabbing popup of client may go on parent: | |
| 159 | + | // parent is the client's newest live grabbing popup, or a toplevel (nil | |
| 160 | + | // for a layer surface) when it has none. | |
| 161 | + | func (s *Server) topGrab(client server.Client, parent *window) bool { | |
| 145 | 162 | var top *window | |
| 146 | 163 | for _, w := range s.windows { | |
| 147 | 164 | if w.popup != nil && w.popup.grab && !w.popup.done && w.xdg.resource.Client() == client && (top == nil || w.id > top.id) { | |
| @@ -149,7 +166,7 @@ func (s *Server) topGrab(parent *window) bool { | |||
| 149 | 166 | } | |
| 150 | 167 | } | |
| 151 | 168 | if top == nil { | |
| 152 | - | return parent.popup == nil | |
| 169 | + | return parent == nil || parent.popup == nil | |
| 153 | 170 | } | |
| 154 | 171 | return top == parent | |
| 155 | 172 | } | |
| @@ -4,6 +4,7 @@ import ( | |||||
| 4 | "github.com/bnema/nefertty/internal/ports" | 4 | "github.com/bnema/nefertty/internal/ports" | ||
| 5 | "github.com/bnema/purego-libwayland/protocol/wayland" | 5 | "github.com/bnema/purego-libwayland/protocol/wayland" | ||
| 6 | "github.com/bnema/purego-libwayland/protocol/xdgshell" | 6 | "github.com/bnema/purego-libwayland/protocol/xdgshell" | ||
| 7 | + | "github.com/bnema/purego-libwayland/server" | |||
| 7 | ) | 8 | ) | ||
| 8 | 9 | ||||
| 9 | // popup is the xdg_popup role of a window. Core places it (PopupRequest → | 10 | // popup is the xdg_popup role of a window. Core places it (PopupRequest → | ||
| @@ -13,8 +14,11 @@ type popup struct { | |||||
| 13 | w *window | 14 | w *window | ||
| 14 | resource *xdgshell.Popup | 15 | resource *xdgshell.Popup | ||
| 15 | parent *window | 16 | parent *window | ||
| 16 | - | pos ports.Positioner | 17 | + | // layer is the parent set through zwlr_layer_surface_v1.get_popup, |
| 17 | - | grab bool | 18 | + | // for a popup created without an xdg parent. |
| 19 | + | layer *layerSurface | |||
| 20 | + | pos ports.Positioner | |||
| 21 | + | grab bool | |||
| 18 | // sent is set once core got the PopupRequest; done once dismissed. | 22 | // sent is set once core got the PopupRequest; done once dismissed. | ||
| 19 | sent, done bool | 23 | sent, done bool | ||
| 20 | } | 24 | } | ||
| @@ -42,11 +46,6 @@ func (s *Server) newPopup(x *xdgSurface, r *xdgshell.Surface, id uint32, parentX | |||||
| 42 | s.nextWindow++ | 46 | s.nextWindow++ | ||
| 43 | s.windows[w.id] = w | 47 | s.windows[w.id] = w | ||
| 44 | x.window = w | 48 | x.window = w | ||
| 45 | - | // Without a parent (xdg-shell lets clients set it through another | |||
| 46 | - | // protocol, such as layer-shell) the popup has nowhere to go. | |||
| 47 | - | if parent == nil { | |||
| 48 | - | pp.dismiss() | |||
| 49 | - | } | |||
| 50 | x.surface.role = func(buffer bool) { | 49 | x.surface.role = func(buffer bool) { | ||
| 51 | if x.surface.destroyed { | 50 | if x.surface.destroyed { | ||
| 52 | w.unmap() | 51 | w.unmap() | ||
| @@ -54,14 +53,21 @@ func (s *Server) newPopup(x *xdgSurface, r *xdgshell.Surface, id uint32, parentX | |||||
| 54 | } | 53 | } | ||
| 55 | switch { | 54 | switch { | ||
| 56 | case !buffer && !w.mapped && !x.configured && !pp.done: | 55 | case !buffer && !w.mapped && !x.configured && !pp.done: | ||
| 56 | + | // Without a parent (xdg-shell lets clients set it through | |||
| 57 | + | // layer-shell before the first commit) the popup has nowhere | |||
| 58 | + | // to go. | |||
| 59 | + | if pp.parentID() == 0 { | |||
| 60 | + | pp.dismiss() | |||
| 61 | + | return | |||
| 62 | + | } | |||
| 57 | if !pp.sent { | 63 | if !pp.sent { | ||
| 58 | pp.sent = true | 64 | pp.sent = true | ||
| 59 | - | s.emit(ports.PopupRequest{ID: w.id, Parent: parent.id, Positioner: pp.pos, Grab: pp.grab}) | 65 | + | s.emit(ports.PopupRequest{ID: w.id, Parent: pp.parentID(), Positioner: pp.pos, Grab: pp.grab}) |
| 60 | } | 66 | } | ||
| 61 | case buffer && !w.mapped && x.acked && !pp.done: | 67 | case buffer && !w.mapped && x.acked && !pp.done: | ||
| 62 | w.mapped = true | 68 | w.mapped = true | ||
| 63 | s.emit(ports.PopupMapped{ID: w.id}) | 69 | s.emit(ports.PopupMapped{ID: w.id}) | ||
| 64 | - | s.log.Debug().Uint64("id", uint64(w.id)).Uint64("parent", uint64(parent.id)).Msg("popup mapped") | 70 | + | s.log.Debug().Uint64("id", uint64(w.id)).Uint64("parent", uint64(pp.parentID())).Msg("popup mapped") |
| 65 | case !buffer && w.mapped: | 71 | case !buffer && w.mapped: | ||
| 66 | // Unmapped, it may map again after a new initial commit. | 72 | // Unmapped, it may map again after a new initial commit. | ||
| 67 | w.unmap() | 73 | w.unmap() | ||
| @@ -88,6 +94,17 @@ func (s *Server) newPopup(x *xdgSurface, r *xdgshell.Surface, id uint32, parentX | |||||
| 88 | 94 | ||||
| 89 | func (*popup) Destroy(*xdgshell.Popup) {} | 95 | func (*popup) Destroy(*xdgshell.Popup) {} | ||
| 90 | 96 | ||||
| 97 | + | // parentID is the window or layer surface the popup hangs from, 0 if none. | |||
| 98 | + | func (p *popup) parentID() ports.WindowID { | |||
| 99 | + | switch { | |||
| 100 | + | case p.parent != nil: | |||
| 101 | + | return p.parent.id | |||
| 102 | + | case p.layer != nil: | |||
| 103 | + | return p.layer.id | |||
| 104 | + | } | |||
| 105 | + | return 0 | |||
| 106 | + | } | |||
| 107 | + | ||||
| 91 | // Grab is only valid before the first commit; core then gives the popup the | 108 | // Grab is only valid before the first commit; core then gives the popup the | ||
| 92 | // keyboard and closes it on a click outside its popup chain. | 109 | // keyboard and closes it on a click outside its popup chain. | ||
| 93 | func (p *popup) Grab(r *xdgshell.Popup, _ *wayland.Seat, serial uint32) { | 110 | func (p *popup) Grab(r *xdgshell.Popup, _ *wayland.Seat, serial uint32) { | ||
| @@ -99,7 +116,7 @@ func (p *popup) Grab(r *xdgshell.Popup, _ *wayland.Seat, serial uint32) { | |||||
| 99 | // and a popup over a popup grabs only if its parent does. | 116 | // and a popup over a popup grabs only if its parent does. | ||
| 100 | s := p.w.xdg.server | 117 | s := p.w.xdg.server | ||
| 101 | fresh := serial == s.press && s.pressClient == r.Client() | 118 | fresh := serial == s.press && s.pressClient == r.Client() | ||
| 102 | - | if !fresh || p.parent == nil || !s.topGrab(p.parent) { | 119 | + | if !fresh || p.parentID() == 0 || !s.topGrab(r.Client(), p.parent) { |
| 103 | p.dismiss() | 120 | p.dismiss() | ||
| 104 | return | 121 | return | ||
| 105 | } | 122 | } | ||
| @@ -114,7 +131,7 @@ func (p *popup) Reposition(r *xdgshell.Popup, pos *xdgshell.Positioner, token ui | |||||
| 114 | } | 131 | } | ||
| 115 | p.pos = next | 132 | p.pos = next | ||
| 116 | if p.sent { | 133 | if p.sent { | ||
| 117 | - | s.emit(ports.PopupRequest{ID: p.w.id, Parent: p.parent.id, Positioner: next, Grab: p.grab, Reposition: true, Token: token}) | 134 | + | s.emit(ports.PopupRequest{ID: p.w.id, Parent: p.parentID(), Positioner: next, Grab: p.grab, Reposition: true, Token: token}) |
| 118 | } | 135 | } | ||
| 119 | } | 136 | } | ||
| 120 | 137 | ||||
| @@ -138,10 +155,10 @@ func (p *popup) configure(c ports.ConfigurePopup) { | |||||
| 138 | } | 155 | } | ||
| 139 | } | 156 | } | ||
| 140 | 157 | ||||
| 141 | - | // topGrab reports whether a grabbing popup may go on parent: parent is | 158 | + | // topGrab reports whether a grabbing popup of client may go on parent: |
| 142 | - | // the client's newest live grabbing popup, or a toplevel when it has none. | 159 | + | // parent is the client's newest live grabbing popup, or a toplevel (nil |
| 143 | - | func (s *Server) topGrab(parent *window) bool { | 160 | + | // for a layer surface) when it has none. |
| 144 | - | client := parent.xdg.resource.Client() | 161 | + | func (s *Server) topGrab(client server.Client, parent *window) bool { |
| 145 | var top *window | 162 | var top *window | ||
| 146 | for _, w := range s.windows { | 163 | for _, w := range s.windows { | ||
| 147 | if w.popup != nil && w.popup.grab && !w.popup.done && w.xdg.resource.Client() == client && (top == nil || w.id > top.id) { | 164 | if w.popup != nil && w.popup.grab && !w.popup.done && w.xdg.resource.Client() == client && (top == nil || w.id > top.id) { | ||
| @@ -149,7 +166,7 @@ func (s *Server) topGrab(parent *window) bool { | |||||
| 149 | } | 166 | } | ||
| 150 | } | 167 | } | ||
| 151 | if top == nil { | 168 | if top == nil { | ||
| 152 | - | return parent.popup == nil | 169 | + | return parent == nil || parent.popup == nil |
| 153 | } | 170 | } | ||
| 154 | return top == parent | 171 | return top == parent | ||
| 155 | } | 172 | } | ||
M internal/adapters/wayland/server.go +67 -25
| @@ -363,6 +363,14 @@ func (s *Server) apply(cmd ports.ClientCommand) { | |||
| 363 | 363 | case ports.PointerFocus: | |
| 364 | 364 | s.changePointerFocus(c.ID, c.X, c.Y) | |
| 365 | 365 | case ports.PointerMotionTo: | |
| 366 | + | if l := s.layers[c.ID]; l != nil && c.ID == s.pointerFocus { | |
| 367 | + | s.pointerX, s.pointerY = c.X, c.Y | |
| 368 | + | for _, p := range s.layerPointers(l) { | |
| 369 | + | p.SendMotion(c.TimeMsec, server.FixedFromFloat(c.X), server.FixedFromFloat(c.Y)) | |
| 370 | + | pointerFrame(p) | |
| 371 | + | } | |
| 372 | + | return | |
| 373 | + | } | |
| 366 | 374 | if w := s.windows[c.ID]; w != nil && c.ID == s.pointerFocus { | |
| 367 | 375 | if s.relativeMotion(w, c) && s.locked(w) { | |
| 368 | 376 | for _, p := range s.windowPointers(w) { | |
| @@ -386,14 +394,14 @@ func (s *Server) apply(cmd ports.ClientCommand) { | |||
| 386 | 394 | } | |
| 387 | 395 | case ports.PointerButtonTo: | |
| 388 | 396 | // Releases may target the implicit-grab window after focus has moved. | |
| 389 | - | if w := s.windows[c.ID]; w != nil { | |
| 397 | + | if client, pointers, ok := s.pointerTarget(c.ID); ok { | |
| 390 | 398 | s.serial++ | |
| 391 | 399 | state := uint32(0) | |
| 392 | 400 | if c.Pressed { | |
| 393 | 401 | state = 1 | |
| 394 | - | s.press, s.pressClient, s.pressAt = s.serial, w.xdg.resource.Client(), time.Now() | |
| 402 | + | s.press, s.pressClient, s.pressAt = s.serial, client, time.Now() | |
| 395 | 403 | } | |
| 396 | - | for _, p := range s.windowPointers(w) { | |
| 404 | + | for _, p := range pointers { | |
| 397 | 405 | p.SendButton(s.serial, c.TimeMsec, c.Button, state) | |
| 398 | 406 | pointerFrame(p) | |
| 399 | 407 | } | |
| @@ -404,7 +412,8 @@ func (s *Server) apply(cmd ports.ClientCommand) { | |||
| 404 | 412 | return | |
| 405 | 413 | } | |
| 406 | 414 | steps, values := s.wheelSteps(c.Axis) | |
| 407 | - | for _, p := range s.windowPointers(s.windows[c.ID]) { | |
| 415 | + | _, pointers, _ := s.pointerTarget(c.ID) | |
| 416 | + | for _, p := range pointers { | |
| 408 | 417 | sendAxis(p, c.Axis, steps, values) | |
| 409 | 418 | } | |
| 410 | 419 | case ports.SetKeymap: | |
| @@ -506,15 +515,42 @@ func (s *Server) focusTarget(id ports.WindowID) (*wayland.Surface, []*wayland.Ke | |||
| 506 | 515 | } | |
| 507 | 516 | } | |
| 508 | 517 | if l := s.layers[id]; l != nil && l.mapped && l.resource.Resource.Alive() { | |
| 509 | - | for resource, state := range s.surfaces { | |
| 510 | - | if state == l.surface && resource.Alive() { | |
| 511 | - | return wayland.WrapSurface(resource), s.clientKeyboards(l.resource.Client()) | |
| 512 | - | } | |
| 518 | + | if surf := l.surfaceResource(); surf != nil { | |
| 519 | + | return surf, s.clientKeyboards(l.resource.Client()) | |
| 513 | 520 | } | |
| 514 | 521 | } | |
| 515 | 522 | return nil, nil | |
| 516 | 523 | } | |
| 517 | 524 | ||
| 525 | + | // pointerTarget resolves a mapped window or layer ID to its client and | |
| 526 | + | // live pointers. | |
| 527 | + | func (s *Server) pointerTarget(id ports.WindowID) (server.Client, []*wayland.Pointer, bool) { | |
| 528 | + | if w := s.windows[id]; w != nil && w.mapped && w.xdg.resource.Resource.Alive() { | |
| 529 | + | return w.xdg.resource.Client(), s.windowPointers(w), true | |
| 530 | + | } | |
| 531 | + | if l := s.layers[id]; l != nil && l.mapped && l.resource.Resource.Alive() { | |
| 532 | + | return l.resource.Client(), s.layerPointers(l), true | |
| 533 | + | } | |
| 534 | + | return server.Client{}, nil, false | |
| 535 | + | } | |
| 536 | + | ||
| 537 | + | // pointerSurface is the wl_surface of a mapped window or layer and the | |
| 538 | + | // point in its surface coordinates. | |
| 539 | + | func (s *Server) pointerSurface(id ports.WindowID, x, y float64) (*wayland.Surface, []*wayland.Pointer, float64, float64) { | |
| 540 | + | if w := s.windows[id]; w != nil { | |
| 541 | + | if surf := w.xdg.surfaceResource(); surf != nil && surf.Resource.Alive() { | |
| 542 | + | x, y := w.surfacePoint(x, y) | |
| 543 | + | return surf, s.windowPointers(w), x, y | |
| 544 | + | } | |
| 545 | + | } | |
| 546 | + | if l := s.layers[id]; l != nil && l.mapped { | |
| 547 | + | if surf := l.surfaceResource(); surf != nil { | |
| 548 | + | return surf, s.layerPointers(l), x, y | |
| 549 | + | } | |
| 550 | + | } | |
| 551 | + | return nil, nil, 0, 0 | |
| 552 | + | } | |
| 553 | + | ||
| 518 | 554 | // keymapFile writes a sealed memfd holding the NUL-terminated keymap. | |
| 519 | 555 | func keymapFile(keymap string) (int, uint32, error) { | |
| 520 | 556 | if uint64(len(keymap))+1 > uint64(^uint32(0)) { | |
| @@ -698,8 +734,19 @@ func (s *Server) windowPointers(w *window) []*wayland.Pointer { | |||
| 698 | 734 | if w == nil || !w.mapped || !w.xdg.resource.Resource.Alive() { | |
| 699 | 735 | return nil | |
| 700 | 736 | } | |
| 737 | + | return s.clientPointers(w.xdg.resource.Client()) | |
| 738 | + | } | |
| 739 | + | ||
| 740 | + | func (s *Server) layerPointers(l *layerSurface) []*wayland.Pointer { | |
| 741 | + | if l == nil || !l.mapped || !l.resource.Resource.Alive() { | |
| 742 | + | return nil | |
| 743 | + | } | |
| 744 | + | return s.clientPointers(l.resource.Client()) | |
| 745 | + | } | |
| 746 | + | ||
| 747 | + | func (s *Server) clientPointers(c server.Client) []*wayland.Pointer { | |
| 701 | 748 | var result []*wayland.Pointer | |
| 702 | - | for _, p := range s.pointers[w.xdg.resource.Client()] { | |
| 749 | + | for _, p := range s.pointers[c] { | |
| 703 | 750 | if p.Resource.Alive() { | |
| 704 | 751 | result = append(result, p) | |
| 705 | 752 | } | |
| @@ -714,25 +761,20 @@ func (s *Server) changePointerFocus(id ports.WindowID, x, y float64) { | |||
| 714 | 761 | // The new client sets its own cursor on enter; until then, the arrow. | |
| 715 | 762 | s.cursorSurface = nil | |
| 716 | 763 | s.setCursor(ports.CursorChange{}) | |
| 717 | - | if old := s.windows[s.pointerFocus]; old != nil { | |
| 718 | - | if surface := old.xdg.surfaceResource(); surface != nil && surface.Resource.Alive() { | |
| 719 | - | for _, p := range s.windowPointers(old) { | |
| 720 | - | s.serial++ | |
| 721 | - | p.SendLeave(s.serial, surface) | |
| 722 | - | pointerFrame(p) | |
| 723 | - | } | |
| 764 | + | if surface, pointers, _, _ := s.pointerSurface(s.pointerFocus, 0, 0); surface != nil { | |
| 765 | + | for _, p := range pointers { | |
| 766 | + | s.serial++ | |
| 767 | + | p.SendLeave(s.serial, surface) | |
| 768 | + | pointerFrame(p) | |
| 724 | 769 | } | |
| 725 | 770 | } | |
| 726 | 771 | s.pointerFocus = 0 | |
| 727 | - | if w := s.windows[id]; w != nil { | |
| 728 | - | if surface := w.xdg.surfaceResource(); surface != nil && surface.Resource.Alive() { | |
| 729 | - | s.pointerFocus, s.pointerX, s.pointerY = id, x, y | |
| 730 | - | x, y := w.surfacePoint(x, y) | |
| 731 | - | for _, p := range s.windowPointers(w) { | |
| 732 | - | s.serial++ | |
| 733 | - | p.SendEnter(s.serial, surface, server.FixedFromFloat(x), server.FixedFromFloat(y)) | |
| 734 | - | pointerFrame(p) | |
| 735 | - | } | |
| 772 | + | if surface, pointers, sx, sy := s.pointerSurface(id, x, y); surface != nil { | |
| 773 | + | s.pointerFocus, s.pointerX, s.pointerY = id, x, y | |
| 774 | + | for _, p := range pointers { | |
| 775 | + | s.serial++ | |
| 776 | + | p.SendEnter(s.serial, surface, server.FixedFromFloat(sx), server.FixedFromFloat(sy)) | |
| 777 | + | pointerFrame(p) | |
| 736 | 778 | } | |
| 737 | 779 | } | |
| 738 | 780 | s.updateConstraint() | |
| @@ -363,6 +363,14 @@ func (s *Server) apply(cmd ports.ClientCommand) { | |||||
| 363 | case ports.PointerFocus: | 363 | case ports.PointerFocus: | ||
| 364 | s.changePointerFocus(c.ID, c.X, c.Y) | 364 | s.changePointerFocus(c.ID, c.X, c.Y) | ||
| 365 | case ports.PointerMotionTo: | 365 | case ports.PointerMotionTo: | ||
| 366 | + | if l := s.layers[c.ID]; l != nil && c.ID == s.pointerFocus { | |||
| 367 | + | s.pointerX, s.pointerY = c.X, c.Y | |||
| 368 | + | for _, p := range s.layerPointers(l) { | |||
| 369 | + | p.SendMotion(c.TimeMsec, server.FixedFromFloat(c.X), server.FixedFromFloat(c.Y)) | |||
| 370 | + | pointerFrame(p) | |||
| 371 | + | } | |||
| 372 | + | return | |||
| 373 | + | } | |||
| 366 | if w := s.windows[c.ID]; w != nil && c.ID == s.pointerFocus { | 374 | if w := s.windows[c.ID]; w != nil && c.ID == s.pointerFocus { | ||
| 367 | if s.relativeMotion(w, c) && s.locked(w) { | 375 | if s.relativeMotion(w, c) && s.locked(w) { | ||
| 368 | for _, p := range s.windowPointers(w) { | 376 | for _, p := range s.windowPointers(w) { | ||
| @@ -386,14 +394,14 @@ func (s *Server) apply(cmd ports.ClientCommand) { | |||||
| 386 | } | 394 | } | ||
| 387 | case ports.PointerButtonTo: | 395 | case ports.PointerButtonTo: | ||
| 388 | // Releases may target the implicit-grab window after focus has moved. | 396 | // Releases may target the implicit-grab window after focus has moved. | ||
| 389 | - | if w := s.windows[c.ID]; w != nil { | 397 | + | if client, pointers, ok := s.pointerTarget(c.ID); ok { |
| 390 | s.serial++ | 398 | s.serial++ | ||
| 391 | state := uint32(0) | 399 | state := uint32(0) | ||
| 392 | if c.Pressed { | 400 | if c.Pressed { | ||
| 393 | state = 1 | 401 | state = 1 | ||
| 394 | - | s.press, s.pressClient, s.pressAt = s.serial, w.xdg.resource.Client(), time.Now() | 402 | + | s.press, s.pressClient, s.pressAt = s.serial, client, time.Now() |
| 395 | } | 403 | } | ||
| 396 | - | for _, p := range s.windowPointers(w) { | 404 | + | for _, p := range pointers { |
| 397 | p.SendButton(s.serial, c.TimeMsec, c.Button, state) | 405 | p.SendButton(s.serial, c.TimeMsec, c.Button, state) | ||
| 398 | pointerFrame(p) | 406 | pointerFrame(p) | ||
| 399 | } | 407 | } | ||
| @@ -404,7 +412,8 @@ func (s *Server) apply(cmd ports.ClientCommand) { | |||||
| 404 | return | 412 | return | ||
| 405 | } | 413 | } | ||
| 406 | steps, values := s.wheelSteps(c.Axis) | 414 | steps, values := s.wheelSteps(c.Axis) | ||
| 407 | - | for _, p := range s.windowPointers(s.windows[c.ID]) { | 415 | + | _, pointers, _ := s.pointerTarget(c.ID) |
| 416 | + | for _, p := range pointers { | |||
| 408 | sendAxis(p, c.Axis, steps, values) | 417 | sendAxis(p, c.Axis, steps, values) | ||
| 409 | } | 418 | } | ||
| 410 | case ports.SetKeymap: | 419 | case ports.SetKeymap: | ||
| @@ -506,15 +515,42 @@ func (s *Server) focusTarget(id ports.WindowID) (*wayland.Surface, []*wayland.Ke | |||||
| 506 | } | 515 | } | ||
| 507 | } | 516 | } | ||
| 508 | if l := s.layers[id]; l != nil && l.mapped && l.resource.Resource.Alive() { | 517 | if l := s.layers[id]; l != nil && l.mapped && l.resource.Resource.Alive() { | ||
| 509 | - | for resource, state := range s.surfaces { | 518 | + | if surf := l.surfaceResource(); surf != nil { |
| 510 | - | if state == l.surface && resource.Alive() { | 519 | + | return surf, s.clientKeyboards(l.resource.Client()) |
| 511 | - | return wayland.WrapSurface(resource), s.clientKeyboards(l.resource.Client()) | |||
| 512 | - | } | |||
| 513 | } | 520 | } | ||
| 514 | } | 521 | } | ||
| 515 | return nil, nil | 522 | return nil, nil | ||
| 516 | } | 523 | } | ||
| 517 | 524 | ||||
| 525 | + | // pointerTarget resolves a mapped window or layer ID to its client and | |||
| 526 | + | // live pointers. | |||
| 527 | + | func (s *Server) pointerTarget(id ports.WindowID) (server.Client, []*wayland.Pointer, bool) { | |||
| 528 | + | if w := s.windows[id]; w != nil && w.mapped && w.xdg.resource.Resource.Alive() { | |||
| 529 | + | return w.xdg.resource.Client(), s.windowPointers(w), true | |||
| 530 | + | } | |||
| 531 | + | if l := s.layers[id]; l != nil && l.mapped && l.resource.Resource.Alive() { | |||
| 532 | + | return l.resource.Client(), s.layerPointers(l), true | |||
| 533 | + | } | |||
| 534 | + | return server.Client{}, nil, false | |||
| 535 | + | } | |||
| 536 | + | ||||
| 537 | + | // pointerSurface is the wl_surface of a mapped window or layer and the | |||
| 538 | + | // point in its surface coordinates. | |||
| 539 | + | func (s *Server) pointerSurface(id ports.WindowID, x, y float64) (*wayland.Surface, []*wayland.Pointer, float64, float64) { | |||
| 540 | + | if w := s.windows[id]; w != nil { | |||
| 541 | + | if surf := w.xdg.surfaceResource(); surf != nil && surf.Resource.Alive() { | |||
| 542 | + | x, y := w.surfacePoint(x, y) | |||
| 543 | + | return surf, s.windowPointers(w), x, y | |||
| 544 | + | } | |||
| 545 | + | } | |||
| 546 | + | if l := s.layers[id]; l != nil && l.mapped { | |||
| 547 | + | if surf := l.surfaceResource(); surf != nil { | |||
| 548 | + | return surf, s.layerPointers(l), x, y | |||
| 549 | + | } | |||
| 550 | + | } | |||
| 551 | + | return nil, nil, 0, 0 | |||
| 552 | + | } | |||
| 553 | + | ||||
| 518 | // keymapFile writes a sealed memfd holding the NUL-terminated keymap. | 554 | // keymapFile writes a sealed memfd holding the NUL-terminated keymap. | ||
| 519 | func keymapFile(keymap string) (int, uint32, error) { | 555 | func keymapFile(keymap string) (int, uint32, error) { | ||
| 520 | if uint64(len(keymap))+1 > uint64(^uint32(0)) { | 556 | if uint64(len(keymap))+1 > uint64(^uint32(0)) { | ||
| @@ -698,8 +734,19 @@ func (s *Server) windowPointers(w *window) []*wayland.Pointer { | |||||
| 698 | if w == nil || !w.mapped || !w.xdg.resource.Resource.Alive() { | 734 | if w == nil || !w.mapped || !w.xdg.resource.Resource.Alive() { | ||
| 699 | return nil | 735 | return nil | ||
| 700 | } | 736 | } | ||
| 737 | + | return s.clientPointers(w.xdg.resource.Client()) | |||
| 738 | + | } | |||
| 739 | + | ||||
| 740 | + | func (s *Server) layerPointers(l *layerSurface) []*wayland.Pointer { | |||
| 741 | + | if l == nil || !l.mapped || !l.resource.Resource.Alive() { | |||
| 742 | + | return nil | |||
| 743 | + | } | |||
| 744 | + | return s.clientPointers(l.resource.Client()) | |||
| 745 | + | } | |||
| 746 | + | ||||
| 747 | + | func (s *Server) clientPointers(c server.Client) []*wayland.Pointer { | |||
| 701 | var result []*wayland.Pointer | 748 | var result []*wayland.Pointer | ||
| 702 | - | for _, p := range s.pointers[w.xdg.resource.Client()] { | 749 | + | for _, p := range s.pointers[c] { |
| 703 | if p.Resource.Alive() { | 750 | if p.Resource.Alive() { | ||
| 704 | result = append(result, p) | 751 | result = append(result, p) | ||
| 705 | } | 752 | } | ||
| @@ -714,25 +761,20 @@ func (s *Server) changePointerFocus(id ports.WindowID, x, y float64) { | |||||
| 714 | // The new client sets its own cursor on enter; until then, the arrow. | 761 | // The new client sets its own cursor on enter; until then, the arrow. | ||
| 715 | s.cursorSurface = nil | 762 | s.cursorSurface = nil | ||
| 716 | s.setCursor(ports.CursorChange{}) | 763 | s.setCursor(ports.CursorChange{}) | ||
| 717 | - | if old := s.windows[s.pointerFocus]; old != nil { | 764 | + | if surface, pointers, _, _ := s.pointerSurface(s.pointerFocus, 0, 0); surface != nil { |
| 718 | - | if surface := old.xdg.surfaceResource(); surface != nil && surface.Resource.Alive() { | 765 | + | for _, p := range pointers { |
| 719 | - | for _, p := range s.windowPointers(old) { | 766 | + | s.serial++ |
| 720 | - | s.serial++ | 767 | + | p.SendLeave(s.serial, surface) |
| 721 | - | p.SendLeave(s.serial, surface) | 768 | + | pointerFrame(p) |
| 722 | - | pointerFrame(p) | |||
| 723 | - | } | |||
| 724 | } | 769 | } | ||
| 725 | } | 770 | } | ||
| 726 | s.pointerFocus = 0 | 771 | s.pointerFocus = 0 | ||
| 727 | - | if w := s.windows[id]; w != nil { | 772 | + | if surface, pointers, sx, sy := s.pointerSurface(id, x, y); surface != nil { |
| 728 | - | if surface := w.xdg.surfaceResource(); surface != nil && surface.Resource.Alive() { | 773 | + | s.pointerFocus, s.pointerX, s.pointerY = id, x, y |
| 729 | - | s.pointerFocus, s.pointerX, s.pointerY = id, x, y | 774 | + | for _, p := range pointers { |
| 730 | - | x, y := w.surfacePoint(x, y) | 775 | + | s.serial++ |
| 731 | - | for _, p := range s.windowPointers(w) { | 776 | + | p.SendEnter(s.serial, surface, server.FixedFromFloat(sx), server.FixedFromFloat(sy)) |
| 732 | - | s.serial++ | 777 | + | pointerFrame(p) |
| 733 | - | p.SendEnter(s.serial, surface, server.FixedFromFloat(x), server.FixedFromFloat(y)) | |||
| 734 | - | pointerFrame(p) | |||
| 735 | - | } | |||
| 736 | } | 778 | } | ||
| 737 | } | 779 | } | ||
| 738 | s.updateConstraint() | 780 | s.updateConstraint() | ||
M internal/core/core.go +36 -16
| @@ -81,6 +81,9 @@ type Core struct { | |||
| 81 | 81 | // popups are the placed xdg_popups; popupOrder stacks them, oldest first. | |
| 82 | 82 | popups map[WindowID]*popupState | |
| 83 | 83 | popupOrder []WindowID | |
| 84 | + | // layerFocus is the on-demand layer surface the user clicked; it keeps | |
| 85 | + | // the keyboard until a window is clicked or the layer unmaps. | |
| 86 | + | layerFocus WindowID | |
| 84 | 87 | } | |
| 85 | 88 | ||
| 86 | 89 | func keyName(s string) string { | |
| @@ -289,22 +292,17 @@ func (c *Core) clientRect(p Placement) Rect { | |||
| 289 | 292 | return Rect{X: r.X + b, Y: r.Y + b, W: r.W - 2*b, H: r.H - 2*b} | |
| 290 | 293 | } | |
| 291 | 294 | ||
| 292 | - | // visible reports whether the window is on screen on any output. | |
| 295 | + | // visible reports whether the window, layer surface or popup is on screen | |
| 296 | + | // on any output. | |
| 293 | 297 | func (c *Core) visible(id WindowID) bool { | |
| 294 | - | for _, sc := range c.screens { | |
| 295 | - | for _, p := range sc.mon.Layout() { | |
| 296 | - | if p.ID == id { | |
| 297 | - | return !p.Hidden | |
| 298 | - | } | |
| 299 | - | } | |
| 300 | - | } | |
| 301 | - | return false | |
| 298 | + | _, _, ok := c.windowRect(id) | |
| 299 | + | return ok | |
| 302 | 300 | } | |
| 303 | 301 | ||
| 304 | 302 | // keyboardFocus is the mapped top/overlay layer with exclusive keyboard | |
| 305 | 303 | // interactivity and the highest ID on any output, else the focused window of | |
| 306 | - | // the focused output. On-demand layers never take focus automatically. When | |
| 307 | - | // the layer unmaps, focus returns to the window. | |
| 304 | + | // the focused output. An on-demand layer takes focus only when clicked, until | |
| 305 | + | // a window is clicked. When the layer unmaps, focus returns to the window. | |
| 308 | 306 | func (c *Core) keyboardFocus() WindowID { | |
| 309 | 307 | var layer WindowID | |
| 310 | 308 | for _, sc := range c.screens { | |
| @@ -321,6 +319,9 @@ func (c *Core) keyboardFocus() WindowID { | |||
| 321 | 319 | if g := c.grabFocus(); g != 0 { | |
| 322 | 320 | return g | |
| 323 | 321 | } | |
| 322 | + | if c.layerFocus != 0 && c.onDemand(c.layerFocus) { | |
| 323 | + | return c.layerFocus | |
| 324 | + | } | |
| 324 | 325 | id, _ := c.cur().mon.Focused() | |
| 325 | 326 | return id | |
| 326 | 327 | } | |
| @@ -503,9 +504,10 @@ func (c *Core) allLayers() []ports.LayerSurface { | |||
| 503 | 504 | return all | |
| 504 | 505 | } | |
| 505 | 506 | ||
| 506 | - | // hit returns the window under the global logical point and the point in | |
| 507 | - | // its surface coordinates. Fullscreen wins; otherwise the last visible | |
| 508 | - | // placement is topmost. | |
| 507 | + | // hit returns the surface under the global logical point and the point in | |
| 508 | + | // its surface coordinates: popups, then overlay and top layers, then | |
| 509 | + | // windows (fullscreen wins; otherwise the last visible placement is | |
| 510 | + | // topmost), then bottom and background layers. | |
| 509 | 511 | func (c *Core) hit(x, y float64) (WindowID, float64, float64) { | |
| 510 | 512 | o, ok := c.layout().At(x, y) | |
| 511 | 513 | if !ok { | |
| @@ -517,6 +519,9 @@ func (c *Core) hit(x, y float64) (WindowID, float64, float64) { | |||
| 517 | 519 | if id, px, py := c.popupAt(sc, lx, ly); id != 0 { | |
| 518 | 520 | return id, px, py | |
| 519 | 521 | } | |
| 522 | + | if id, px, py := layerAt(sc, lx, ly, true); id != 0 { | |
| 523 | + | return id, px, py | |
| 524 | + | } | |
| 520 | 525 | var id WindowID | |
| 521 | 526 | var sx, sy float64 | |
| 522 | 527 | full := false | |
| @@ -534,6 +539,9 @@ func (c *Core) hit(x, y float64) (WindowID, float64, float64) { | |||
| 534 | 539 | full = full || p.Fullscreen | |
| 535 | 540 | } | |
| 536 | 541 | } | |
| 542 | + | if id == 0 { | |
| 543 | + | return layerAt(sc, lx, ly, false) | |
| 544 | + | } | |
| 537 | 545 | return id, sx, sy | |
| 538 | 546 | } | |
| 539 | 547 | ||
| @@ -726,8 +734,20 @@ func (c *Core) Run(ctx context.Context) error { | |||
| 726 | 734 | if err := c.command(ctx, ports.PointerButtonTo{ID: id, Button: v.Button, Pressed: v.Pressed, TimeMsec: v.TimeMsec}); err != nil { | |
| 727 | 735 | return nil | |
| 728 | 736 | } | |
| 729 | - | // A click focuses the window and its output. | |
| 730 | - | if s, w := c.screenOf(id); v.Pressed && s != nil && w == s.mon.Current() && (c.focus != id || s != c.cur()) { | |
| 737 | + | // A click on an on-demand layer gives it the keyboard. | |
| 738 | + | if v.Pressed && c.onDemand(id) && c.layerFocus != id { | |
| 739 | + | c.layerFocus = id | |
| 740 | + | if err := c.publish(ctx); err != nil { | |
| 741 | + | return nil | |
| 742 | + | } | |
| 743 | + | } | |
| 744 | + | // A click focuses the window and its output, taking the | |
| 745 | + | // keyboard back from a layer. | |
| 746 | + | s, w := c.screenOf(id) | |
| 747 | + | if v.Pressed && s != nil { | |
| 748 | + | c.layerFocus = 0 | |
| 749 | + | } | |
| 750 | + | if v.Pressed && s != nil && w == s.mon.Current() && (c.focus != id || s != c.cur()) { | |
| 731 | 751 | w.FocusID(id) | |
| 732 | 752 | c.focusScreen = c.screenIndex(s.name()) | |
| 733 | 753 | if err := c.publish(ctx); err != nil { | |
| @@ -81,6 +81,9 @@ type Core struct { | |||||
| 81 | // popups are the placed xdg_popups; popupOrder stacks them, oldest first. | 81 | // popups are the placed xdg_popups; popupOrder stacks them, oldest first. | ||
| 82 | popups map[WindowID]*popupState | 82 | popups map[WindowID]*popupState | ||
| 83 | popupOrder []WindowID | 83 | popupOrder []WindowID | ||
| 84 | + | // layerFocus is the on-demand layer surface the user clicked; it keeps | |||
| 85 | + | // the keyboard until a window is clicked or the layer unmaps. | |||
| 86 | + | layerFocus WindowID | |||
| 84 | } | 87 | } | ||
| 85 | 88 | ||||
| 86 | func keyName(s string) string { | 89 | func keyName(s string) string { | ||
| @@ -289,22 +292,17 @@ func (c *Core) clientRect(p Placement) Rect { | |||||
| 289 | return Rect{X: r.X + b, Y: r.Y + b, W: r.W - 2*b, H: r.H - 2*b} | 292 | return Rect{X: r.X + b, Y: r.Y + b, W: r.W - 2*b, H: r.H - 2*b} | ||
| 290 | } | 293 | } | ||
| 291 | 294 | ||||
| 292 | - | // visible reports whether the window is on screen on any output. | 295 | + | // visible reports whether the window, layer surface or popup is on screen |
| 296 | + | // on any output. | |||
| 293 | func (c *Core) visible(id WindowID) bool { | 297 | func (c *Core) visible(id WindowID) bool { | ||
| 294 | - | for _, sc := range c.screens { | 298 | + | _, _, ok := c.windowRect(id) |
| 295 | - | for _, p := range sc.mon.Layout() { | 299 | + | return ok |
| 296 | - | if p.ID == id { | |||
| 297 | - | return !p.Hidden | |||
| 298 | - | } | |||
| 299 | - | } | |||
| 300 | - | } | |||
| 301 | - | return false | |||
| 302 | } | 300 | } | ||
| 303 | 301 | ||||
| 304 | // keyboardFocus is the mapped top/overlay layer with exclusive keyboard | 302 | // keyboardFocus is the mapped top/overlay layer with exclusive keyboard | ||
| 305 | // interactivity and the highest ID on any output, else the focused window of | 303 | // interactivity and the highest ID on any output, else the focused window of | ||
| 306 | - | // the focused output. On-demand layers never take focus automatically. When | 304 | + | // the focused output. An on-demand layer takes focus only when clicked, until |
| 307 | - | // the layer unmaps, focus returns to the window. | 305 | + | // a window is clicked. When the layer unmaps, focus returns to the window. |
| 308 | func (c *Core) keyboardFocus() WindowID { | 306 | func (c *Core) keyboardFocus() WindowID { | ||
| 309 | var layer WindowID | 307 | var layer WindowID | ||
| 310 | for _, sc := range c.screens { | 308 | for _, sc := range c.screens { | ||
| @@ -321,6 +319,9 @@ func (c *Core) keyboardFocus() WindowID { | |||||
| 321 | if g := c.grabFocus(); g != 0 { | 319 | if g := c.grabFocus(); g != 0 { | ||
| 322 | return g | 320 | return g | ||
| 323 | } | 321 | } | ||
| 322 | + | if c.layerFocus != 0 && c.onDemand(c.layerFocus) { | |||
| 323 | + | return c.layerFocus | |||
| 324 | + | } | |||
| 324 | id, _ := c.cur().mon.Focused() | 325 | id, _ := c.cur().mon.Focused() | ||
| 325 | return id | 326 | return id | ||
| 326 | } | 327 | } | ||
| @@ -503,9 +504,10 @@ func (c *Core) allLayers() []ports.LayerSurface { | |||||
| 503 | return all | 504 | return all | ||
| 504 | } | 505 | } | ||
| 505 | 506 | ||||
| 506 | - | // hit returns the window under the global logical point and the point in | 507 | + | // hit returns the surface under the global logical point and the point in |
| 507 | - | // its surface coordinates. Fullscreen wins; otherwise the last visible | 508 | + | // its surface coordinates: popups, then overlay and top layers, then |
| 508 | - | // placement is topmost. | 509 | + | // windows (fullscreen wins; otherwise the last visible placement is |
| 510 | + | // topmost), then bottom and background layers. | |||
| 509 | func (c *Core) hit(x, y float64) (WindowID, float64, float64) { | 511 | func (c *Core) hit(x, y float64) (WindowID, float64, float64) { | ||
| 510 | o, ok := c.layout().At(x, y) | 512 | o, ok := c.layout().At(x, y) | ||
| 511 | if !ok { | 513 | if !ok { | ||
| @@ -517,6 +519,9 @@ func (c *Core) hit(x, y float64) (WindowID, float64, float64) { | |||||
| 517 | if id, px, py := c.popupAt(sc, lx, ly); id != 0 { | 519 | if id, px, py := c.popupAt(sc, lx, ly); id != 0 { | ||
| 518 | return id, px, py | 520 | return id, px, py | ||
| 519 | } | 521 | } | ||
| 522 | + | if id, px, py := layerAt(sc, lx, ly, true); id != 0 { | |||
| 523 | + | return id, px, py | |||
| 524 | + | } | |||
| 520 | var id WindowID | 525 | var id WindowID | ||
| 521 | var sx, sy float64 | 526 | var sx, sy float64 | ||
| 522 | full := false | 527 | full := false | ||
| @@ -534,6 +539,9 @@ func (c *Core) hit(x, y float64) (WindowID, float64, float64) { | |||||
| 534 | full = full || p.Fullscreen | 539 | full = full || p.Fullscreen | ||
| 535 | } | 540 | } | ||
| 536 | } | 541 | } | ||
| 542 | + | if id == 0 { | |||
| 543 | + | return layerAt(sc, lx, ly, false) | |||
| 544 | + | } | |||
| 537 | return id, sx, sy | 545 | return id, sx, sy | ||
| 538 | } | 546 | } | ||
| 539 | 547 | ||||
| @@ -726,8 +734,20 @@ func (c *Core) Run(ctx context.Context) error { | |||||
| 726 | if err := c.command(ctx, ports.PointerButtonTo{ID: id, Button: v.Button, Pressed: v.Pressed, TimeMsec: v.TimeMsec}); err != nil { | 734 | if err := c.command(ctx, ports.PointerButtonTo{ID: id, Button: v.Button, Pressed: v.Pressed, TimeMsec: v.TimeMsec}); err != nil { | ||
| 727 | return nil | 735 | return nil | ||
| 728 | } | 736 | } | ||
| 729 | - | // A click focuses the window and its output. | 737 | + | // A click on an on-demand layer gives it the keyboard. |
| 730 | - | if s, w := c.screenOf(id); v.Pressed && s != nil && w == s.mon.Current() && (c.focus != id || s != c.cur()) { | 738 | + | if v.Pressed && c.onDemand(id) && c.layerFocus != id { |
| 739 | + | c.layerFocus = id | |||
| 740 | + | if err := c.publish(ctx); err != nil { | |||
| 741 | + | return nil | |||
| 742 | + | } | |||
| 743 | + | } | |||
| 744 | + | // A click focuses the window and its output, taking the | |||
| 745 | + | // keyboard back from a layer. | |||
| 746 | + | s, w := c.screenOf(id) | |||
| 747 | + | if v.Pressed && s != nil { | |||
| 748 | + | c.layerFocus = 0 | |||
| 749 | + | } | |||
| 750 | + | if v.Pressed && s != nil && w == s.mon.Current() && (c.focus != id || s != c.cur()) { | |||
| 731 | w.FocusID(id) | 751 | w.FocusID(id) | ||
| 732 | c.focusScreen = c.screenIndex(s.name()) | 752 | c.focusScreen = c.screenIndex(s.name()) | ||
| 733 | if err := c.publish(ctx); err != nil { | 753 | if err := c.publish(ctx); err != nil { | ||
A internal/core/layer_input_test.go +160 -0
| @@ -0,0 +1,160 @@ | |||
| 1 | + | package core_test | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "testing" | |
| 6 | + | ||
| 7 | + | "github.com/bnema/nefertty/internal/adapters/config" | |
| 8 | + | "github.com/bnema/nefertty/internal/core" | |
| 9 | + | "github.com/bnema/nefertty/internal/ports" | |
| 10 | + | ) | |
| 11 | + | ||
| 12 | + | // next returns the next command of type T matching ok, skipping others. | |
| 13 | + | func 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 | + | ||
| 22 | + | func anyOf[T any](T) bool { return true } | |
| 23 | + | ||
| 24 | + | // Bars and launchers get the pointer, popups and, on click, the keyboard. | |
| 25 | + | func 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 | + | ||
| 117 | + | // A fullscreen window hides top and bottom layers from the pointer, not | |
| 118 | + | // overlay; background and bottom layers get it only where no window is. | |
| 119 | + | func TestLayerHitOrder(t *testing.T) { | |
| 120 | + | cfg := config.Defaults() | |
| 121 | + | cfg.Border.Width = 0 | |
| 122 | + | client := make(chan ports.ClientEvent, 8) | |
| 123 | + | input := make(chan ports.InputEvent, 8) | |
| 124 | + | output := make(chan ports.OutputEvent, 8) | |
| 125 | + | commands := make(chan ports.ClientCommand, 256) | |
| 126 | + | scenes := make(chan []ports.Scene, 1) | |
| 127 | + | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes}) | |
| 128 | + | if err != nil { | |
| 129 | + | t.Fatal(err) | |
| 130 | + | } | |
| 131 | + | ctx, cancel := context.WithCancel(context.Background()) | |
| 132 | + | defer cancel() | |
| 133 | + | go c.Run(ctx) | |
| 134 | + | output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "OUT-1", Width: 100, Height: 80}} | |
| 135 | + | scene(t, scenes) | |
| 136 | + | wallpaper := ports.LayerSurface{ID: 5, Layer: ports.LayerBackground, Anchor: 15} | |
| 137 | + | bar := ports.LayerSurface{ID: 6, Layer: ports.LayerTop, Anchor: ports.AnchorTop | ports.AnchorLeft | ports.AnchorRight, Height: 10} | |
| 138 | + | osd := ports.LayerSurface{ID: 7, Layer: ports.LayerOverlay, Anchor: ports.AnchorBottom, Width: 20, Height: 10} | |
| 139 | + | client <- ports.LayerChanged{Layers: []ports.LayerSurface{wallpaper, bar, osd}} | |
| 140 | + | scene(t, scenes) | |
| 141 | + | hit := func(x, y float64, want ports.WindowID) { | |
| 142 | + | t.Helper() | |
| 143 | + | input <- ports.PointerMotion{X: x, Y: y} | |
| 144 | + | // A focus change is sent only when the target changes; motion always. | |
| 145 | + | if v := next(t, commands, anyOf[ports.PointerMotionTo]); v.ID != want { | |
| 146 | + | t.Fatalf("(%v,%v) hit %d, want %d", x, y, v.ID, want) | |
| 147 | + | } | |
| 148 | + | } | |
| 149 | + | hit(50, 40, 5) | |
| 150 | + | hit(50, 5, 6) | |
| 151 | + | client <- ports.WindowMapped{ID: 1} | |
| 152 | + | scene(t, scenes) | |
| 153 | + | hit(50, 40, 1) | |
| 154 | + | hit(50, 5, 6) | |
| 155 | + | hit(50, 75, 7) | |
| 156 | + | client <- ports.WindowFullscreenRequest{ID: 1, Fullscreen: true} | |
| 157 | + | scene(t, scenes) | |
| 158 | + | hit(50, 5, 1) | |
| 159 | + | hit(50, 75, 7) | |
| 160 | + | } | |
| @@ -0,0 +1,160 @@ | |||||
| 1 | + | package core_test | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "testing" | |||
| 6 | + | ||||
| 7 | + | "github.com/bnema/nefertty/internal/adapters/config" | |||
| 8 | + | "github.com/bnema/nefertty/internal/core" | |||
| 9 | + | "github.com/bnema/nefertty/internal/ports" | |||
| 10 | + | ) | |||
| 11 | + | ||||
| 12 | + | // next returns the next command of type T matching ok, skipping others. | |||
| 13 | + | func 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 | + | ||||
| 22 | + | func anyOf[T any](T) bool { return true } | |||
| 23 | + | ||||
| 24 | + | // Bars and launchers get the pointer, popups and, on click, the keyboard. | |||
| 25 | + | func 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 | + | ||||
| 117 | + | // A fullscreen window hides top and bottom layers from the pointer, not | |||
| 118 | + | // overlay; background and bottom layers get it only where no window is. | |||
| 119 | + | func TestLayerHitOrder(t *testing.T) { | |||
| 120 | + | cfg := config.Defaults() | |||
| 121 | + | cfg.Border.Width = 0 | |||
| 122 | + | client := make(chan ports.ClientEvent, 8) | |||
| 123 | + | input := make(chan ports.InputEvent, 8) | |||
| 124 | + | output := make(chan ports.OutputEvent, 8) | |||
| 125 | + | commands := make(chan ports.ClientCommand, 256) | |||
| 126 | + | scenes := make(chan []ports.Scene, 1) | |||
| 127 | + | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes}) | |||
| 128 | + | if err != nil { | |||
| 129 | + | t.Fatal(err) | |||
| 130 | + | } | |||
| 131 | + | ctx, cancel := context.WithCancel(context.Background()) | |||
| 132 | + | defer cancel() | |||
| 133 | + | go c.Run(ctx) | |||
| 134 | + | output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "OUT-1", Width: 100, Height: 80}} | |||
| 135 | + | scene(t, scenes) | |||
| 136 | + | wallpaper := ports.LayerSurface{ID: 5, Layer: ports.LayerBackground, Anchor: 15} | |||
| 137 | + | bar := ports.LayerSurface{ID: 6, Layer: ports.LayerTop, Anchor: ports.AnchorTop | ports.AnchorLeft | ports.AnchorRight, Height: 10} | |||
| 138 | + | osd := ports.LayerSurface{ID: 7, Layer: ports.LayerOverlay, Anchor: ports.AnchorBottom, Width: 20, Height: 10} | |||
| 139 | + | client <- ports.LayerChanged{Layers: []ports.LayerSurface{wallpaper, bar, osd}} | |||
| 140 | + | scene(t, scenes) | |||
| 141 | + | hit := func(x, y float64, want ports.WindowID) { | |||
| 142 | + | t.Helper() | |||
| 143 | + | input <- ports.PointerMotion{X: x, Y: y} | |||
| 144 | + | // A focus change is sent only when the target changes; motion always. | |||
| 145 | + | if v := next(t, commands, anyOf[ports.PointerMotionTo]); v.ID != want { | |||
| 146 | + | t.Fatalf("(%v,%v) hit %d, want %d", x, y, v.ID, want) | |||
| 147 | + | } | |||
| 148 | + | } | |||
| 149 | + | hit(50, 40, 5) | |||
| 150 | + | hit(50, 5, 6) | |||
| 151 | + | client <- ports.WindowMapped{ID: 1} | |||
| 152 | + | scene(t, scenes) | |||
| 153 | + | hit(50, 40, 1) | |||
| 154 | + | hit(50, 5, 6) | |||
| 155 | + | hit(50, 75, 7) | |||
| 156 | + | client <- ports.WindowFullscreenRequest{ID: 1, Fullscreen: true} | |||
| 157 | + | scene(t, scenes) | |||
| 158 | + | hit(50, 5, 1) | |||
| 159 | + | hit(50, 75, 7) | |||
| 160 | + | } | |||
M internal/core/layers.go +58 -0
| @@ -73,3 +73,61 @@ func arrangeLayers(outW, outH int, layers []ports.LayerSurface) (placed []ports. | |||
| 73 | 73 | sort.SliceStable(placed, func(i, j int) bool { return placed[i].Layer < placed[j].Layer }) | |
| 74 | 74 | return | |
| 75 | 75 | } | |
| 76 | + | ||
| 77 | + | // layerOf finds a placed layer surface by ID. | |
| 78 | + | func (c *Core) layerOf(id WindowID) (*screen, ports.SceneLayer, bool) { | |
| 79 | + | for _, sc := range c.screens { | |
| 80 | + | for _, l := range sc.placed { | |
| 81 | + | if l.ID == id { | |
| 82 | + | return sc, l, true | |
| 83 | + | } | |
| 84 | + | } | |
| 85 | + | } | |
| 86 | + | return nil, ports.SceneLayer{}, false | |
| 87 | + | } | |
| 88 | + | ||
| 89 | + | // hasFullscreen reports whether a visible fullscreen window covers the screen. | |
| 90 | + | func hasFullscreen(sc *screen) bool { | |
| 91 | + | for _, p := range sc.mon.Layout() { | |
| 92 | + | if p.Fullscreen && !p.Hidden { | |
| 93 | + | return true | |
| 94 | + | } | |
| 95 | + | } | |
| 96 | + | return false | |
| 97 | + | } | |
| 98 | + | ||
| 99 | + | // shown reports whether a layer is drawn: a fullscreen window hides the | |
| 100 | + | // bottom and top layers, as the renderer does. | |
| 101 | + | func shown(sc *screen, layer ports.Layer) bool { | |
| 102 | + | return !(hasFullscreen(sc) && (layer == ports.LayerBottom || layer == ports.LayerTop)) | |
| 103 | + | } | |
| 104 | + | ||
| 105 | + | // layerAt returns the topmost shown layer surface under the output-local | |
| 106 | + | // point among those above (or below) the windows. | |
| 107 | + | func layerAt(sc *screen, lx, ly float64, above bool) (WindowID, float64, float64) { | |
| 108 | + | full := hasFullscreen(sc) | |
| 109 | + | for i := len(sc.placed) - 1; i >= 0; i-- { | |
| 110 | + | l := sc.placed[i] | |
| 111 | + | r := l.Rect | |
| 112 | + | if (l.Layer >= ports.LayerTop) != above || (full && (l.Layer == ports.LayerBottom || l.Layer == ports.LayerTop)) || r.W <= 0 || r.H <= 0 { | |
| 113 | + | continue | |
| 114 | + | } | |
| 115 | + | if lx >= float64(r.X) && lx < float64(r.X+r.W) && ly >= float64(r.Y) && ly < float64(r.Y+r.H) { | |
| 116 | + | return l.ID, lx - float64(r.X), ly - float64(r.Y) | |
| 117 | + | } | |
| 118 | + | } | |
| 119 | + | return 0, 0, 0 | |
| 120 | + | } | |
| 121 | + | ||
| 122 | + | // onDemand reports whether id is a mapped layer surface with on-demand | |
| 123 | + | // keyboard interactivity. | |
| 124 | + | func (c *Core) onDemand(id WindowID) bool { | |
| 125 | + | for _, sc := range c.screens { | |
| 126 | + | for _, l := range sc.layers { | |
| 127 | + | if l.ID == id { | |
| 128 | + | return l.Keyboard == 2 | |
| 129 | + | } | |
| 130 | + | } | |
| 131 | + | } | |
| 132 | + | return false | |
| 133 | + | } | |
| @@ -73,3 +73,61 @@ func arrangeLayers(outW, outH int, layers []ports.LayerSurface) (placed []ports. | |||||
| 73 | sort.SliceStable(placed, func(i, j int) bool { return placed[i].Layer < placed[j].Layer }) | 73 | sort.SliceStable(placed, func(i, j int) bool { return placed[i].Layer < placed[j].Layer }) | ||
| 74 | return | 74 | return | ||
| 75 | } | 75 | } | ||
| 76 | + | ||||
| 77 | + | // layerOf finds a placed layer surface by ID. | |||
| 78 | + | func (c *Core) layerOf(id WindowID) (*screen, ports.SceneLayer, bool) { | |||
| 79 | + | for _, sc := range c.screens { | |||
| 80 | + | for _, l := range sc.placed { | |||
| 81 | + | if l.ID == id { | |||
| 82 | + | return sc, l, true | |||
| 83 | + | } | |||
| 84 | + | } | |||
| 85 | + | } | |||
| 86 | + | return nil, ports.SceneLayer{}, false | |||
| 87 | + | } | |||
| 88 | + | ||||
| 89 | + | // hasFullscreen reports whether a visible fullscreen window covers the screen. | |||
| 90 | + | func hasFullscreen(sc *screen) bool { | |||
| 91 | + | for _, p := range sc.mon.Layout() { | |||
| 92 | + | if p.Fullscreen && !p.Hidden { | |||
| 93 | + | return true | |||
| 94 | + | } | |||
| 95 | + | } | |||
| 96 | + | return false | |||
| 97 | + | } | |||
| 98 | + | ||||
| 99 | + | // shown reports whether a layer is drawn: a fullscreen window hides the | |||
| 100 | + | // bottom and top layers, as the renderer does. | |||
| 101 | + | func shown(sc *screen, layer ports.Layer) bool { | |||
| 102 | + | return !(hasFullscreen(sc) && (layer == ports.LayerBottom || layer == ports.LayerTop)) | |||
| 103 | + | } | |||
| 104 | + | ||||
| 105 | + | // layerAt returns the topmost shown layer surface under the output-local | |||
| 106 | + | // point among those above (or below) the windows. | |||
| 107 | + | func layerAt(sc *screen, lx, ly float64, above bool) (WindowID, float64, float64) { | |||
| 108 | + | full := hasFullscreen(sc) | |||
| 109 | + | for i := len(sc.placed) - 1; i >= 0; i-- { | |||
| 110 | + | l := sc.placed[i] | |||
| 111 | + | r := l.Rect | |||
| 112 | + | if (l.Layer >= ports.LayerTop) != above || (full && (l.Layer == ports.LayerBottom || l.Layer == ports.LayerTop)) || r.W <= 0 || r.H <= 0 { | |||
| 113 | + | continue | |||
| 114 | + | } | |||
| 115 | + | if lx >= float64(r.X) && lx < float64(r.X+r.W) && ly >= float64(r.Y) && ly < float64(r.Y+r.H) { | |||
| 116 | + | return l.ID, lx - float64(r.X), ly - float64(r.Y) | |||
| 117 | + | } | |||
| 118 | + | } | |||
| 119 | + | return 0, 0, 0 | |||
| 120 | + | } | |||
| 121 | + | ||||
| 122 | + | // onDemand reports whether id is a mapped layer surface with on-demand | |||
| 123 | + | // keyboard interactivity. | |||
| 124 | + | func (c *Core) onDemand(id WindowID) bool { | |||
| 125 | + | for _, sc := range c.screens { | |||
| 126 | + | for _, l := range sc.layers { | |||
| 127 | + | if l.ID == id { | |||
| 128 | + | return l.Keyboard == 2 | |||
| 129 | + | } | |||
| 130 | + | } | |||
| 131 | + | } | |||
| 132 | + | return false | |||
| 133 | + | } | |||
M internal/core/popup.go +7 -1
| @@ -125,7 +125,7 @@ func flipY(e uint32) uint32 { | |||
| 125 | 125 | return e | |
| 126 | 126 | } | |
| 127 | 127 | ||
| 128 | - | // windowRect is where a window or popup is on its screen: its client | |
| 128 | + | // windowRect is where a window, layer surface or popup is on its screen: its client | |
| 129 | 129 | // area, output-local and logical. ok is false when it is not on screen. | |
| 130 | 130 | func (c *Core) windowRect(id WindowID) (*screen, Rect, bool) { | |
| 131 | 131 | if p := c.popups[id]; p != nil { | |
| @@ -135,6 +135,12 @@ func (c *Core) windowRect(id WindowID) (*screen, Rect, bool) { | |||
| 135 | 135 | } | |
| 136 | 136 | return sc, Rect{X: pr.X + p.rect.X, Y: pr.Y + p.rect.Y, W: p.rect.W, H: p.rect.H}, true | |
| 137 | 137 | } | |
| 138 | + | if sc, l, ok := c.layerOf(id); ok { | |
| 139 | + | if !shown(sc, l.Layer) { | |
| 140 | + | return nil, Rect{}, false | |
| 141 | + | } | |
| 142 | + | return sc, l.Rect, true | |
| 143 | + | } | |
| 138 | 144 | for _, sc := range c.screens { | |
| 139 | 145 | for _, pl := range sc.mon.Layout() { | |
| 140 | 146 | if pl.ID == id { | |
| @@ -125,7 +125,7 @@ func flipY(e uint32) uint32 { | |||||
| 125 | return e | 125 | return e | ||
| 126 | } | 126 | } | ||
| 127 | 127 | ||||
| 128 | - | // windowRect is where a window or popup is on its screen: its client | 128 | + | // windowRect is where a window, layer surface or popup is on its screen: its client |
| 129 | // area, output-local and logical. ok is false when it is not on screen. | 129 | // area, output-local and logical. ok is false when it is not on screen. | ||
| 130 | func (c *Core) windowRect(id WindowID) (*screen, Rect, bool) { | 130 | func (c *Core) windowRect(id WindowID) (*screen, Rect, bool) { | ||
| 131 | if p := c.popups[id]; p != nil { | 131 | if p := c.popups[id]; p != nil { | ||
| @@ -135,6 +135,12 @@ func (c *Core) windowRect(id WindowID) (*screen, Rect, bool) { | |||||
| 135 | } | 135 | } | ||
| 136 | return sc, Rect{X: pr.X + p.rect.X, Y: pr.Y + p.rect.Y, W: p.rect.W, H: p.rect.H}, true | 136 | return sc, Rect{X: pr.X + p.rect.X, Y: pr.Y + p.rect.Y, W: p.rect.W, H: p.rect.H}, true | ||
| 137 | } | 137 | } | ||
| 138 | + | if sc, l, ok := c.layerOf(id); ok { | |||
| 139 | + | if !shown(sc, l.Layer) { | |||
| 140 | + | return nil, Rect{}, false | |||
| 141 | + | } | |||
| 142 | + | return sc, l.Rect, true | |||
| 143 | + | } | |||
| 138 | for _, sc := range c.screens { | 144 | for _, sc := range c.screens { | ||
| 139 | for _, pl := range sc.mon.Layout() { | 145 | for _, pl := range sc.mon.Layout() { | ||
| 140 | if pl.ID == id { | 146 | if pl.ID == id { | ||
M internal/ports/messages.go +1 -0
| @@ -532,6 +532,7 @@ type Scene struct { | |||
| 532 | 532 | Windows []SceneWindow | |
| 533 | 533 | // Layers are drawn in slice order: background and bottom before windows, | |
| 534 | 534 | // top and overlay after. A fullscreen window covers bottom and top. | |
| 535 | + | // Popups (SceneWindow.Popup) are drawn last, over every layer. | |
| 535 | 536 | Layers []SceneLayer | |
| 536 | 537 | } | |
| 537 | 538 | ||
| @@ -532,6 +532,7 @@ type Scene struct { | |||||
| 532 | Windows []SceneWindow | 532 | Windows []SceneWindow | ||
| 533 | // Layers are drawn in slice order: background and bottom before windows, | 533 | // Layers are drawn in slice order: background and bottom before windows, | ||
| 534 | // top and overlay after. A fullscreen window covers bottom and top. | 534 | // top and overlay after. A fullscreen window covers bottom and top. | ||
| 535 | + | // Popups (SceneWindow.Popup) are drawn last, over every layer. | |||
| 535 | Layers []SceneLayer | 536 | Layers []SceneLayer | ||
| 536 | } | 537 | } | ||
| 537 | 538 | ||||