detached 2273fad

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 VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · 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
515515 }
516516 x, y := w.Rect.X, w.Rect.Y
517517 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)
521518 continue
522519 }
523520 // 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
552549 }
553550 }
554551 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+ }
555561 if err := r.ensureStaging(used); err != nil {
556562 return err
557563 }
@@ -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.Y516 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 continue518 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 err562 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,
100100 }
101101 func (pointer) Release(r *wayland.Pointer) { r.Destroy() }
102102
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.
104105 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
107108 }
108109
109110 // 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() == c107+ return ok && owner == c
107 }108 }
108 109
109 // cursorImage copies the committed shm buffer of a cursor surface. No110 // 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) {
361361 s.pointers[r.Client()] = list
362362 }
363363 }
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+ }
369370 }
370371 }
371372 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()] = list361 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
106106 l.pending.keyboard = k
107107 }
108108
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+}
111134 func (l *layerSurface) AckConfigure(r *wlrlayershell.ZwlrLayerSurfaceV1, serial uint32) {
112135 for i, v := range l.serials {
113136 if v == serial {
@@ -136,6 +159,9 @@ func (l *layerSurface) unmap() {
136159 if s := l.shell.server; s.focused == l.id {
137160 s.changeFocus(0)
138161 }
162+ if s := l.shell.server; s.pointerFocus == l.id {
163+ s.changePointerFocus(0, 0, 0)
164+ }
139165 l.mapped = false
140166 l.configured = false
141167 l.acked = false
@@ -106,8 +106,31 @@ func (l *layerSurface) SetKeyboardInteractivity(r *wlrlayershell.ZwlrLayerSurfac
106 l.pending.keyboard = k106 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 = false165 l.mapped = false
140 l.configured = false166 l.configured = false
141 l.acked = false167 l.acked = false
M internal/adapters/wayland/layer_test.go +115 -0
@@ -5,6 +5,7 @@ import (
55 "github.com/bnema/nefertty/internal/ports"
66 "github.com/bnema/purego-libwayland/protocol/wayland"
77 "github.com/bnema/purego-libwayland/protocol/wlrlayershell"
8+ "github.com/bnema/purego-libwayland/protocol/xdgshell"
89 "github.com/bnema/wlturbo"
910 "golang.org/x/sys/unix"
1011 "os"
@@ -231,3 +232,117 @@ func TestXDGOutputDone(t *testing.T) {
231232 })
232233 }
233234 }
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 (
44 "github.com/bnema/nefertty/internal/ports"
55 "github.com/bnema/purego-libwayland/protocol/wayland"
66 "github.com/bnema/purego-libwayland/protocol/xdgshell"
7+ "github.com/bnema/purego-libwayland/server"
78 )
89
910 // popup is the xdg_popup role of a window. Core places it (PopupRequest →
@@ -13,8 +14,11 @@ type popup struct {
1314 w *window
1415 resource *xdgshell.Popup
1516 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
1822 // sent is set once core got the PopupRequest; done once dismissed.
1923 sent, done bool
2024 }
@@ -42,11 +46,6 @@ func (s *Server) newPopup(x *xdgSurface, r *xdgshell.Surface, id uint32, parentX
4246 s.nextWindow++
4347 s.windows[w.id] = w
4448 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- }
5049 x.surface.role = func(buffer bool) {
5150 if x.surface.destroyed {
5251 w.unmap()
@@ -54,14 +53,21 @@ func (s *Server) newPopup(x *xdgSurface, r *xdgshell.Surface, id uint32, parentX
5453 }
5554 switch {
5655 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+ }
5763 if !pp.sent {
5864 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})
6066 }
6167 case buffer && !w.mapped && x.acked && !pp.done:
6268 w.mapped = true
6369 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")
6571 case !buffer && w.mapped:
6672 // Unmapped, it may map again after a new initial commit.
6773 w.unmap()
@@ -88,6 +94,17 @@ func (s *Server) newPopup(x *xdgSurface, r *xdgshell.Surface, id uint32, parentX
8894
8995 func (*popup) Destroy(*xdgshell.Popup) {}
9096
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+
91108 // Grab is only valid before the first commit; core then gives the popup the
92109 // keyboard and closes it on a click outside its popup chain.
93110 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) {
99116 // and a popup over a popup grabs only if its parent does.
100117 s := p.w.xdg.server
101118 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) {
103120 p.dismiss()
104121 return
105122 }
@@ -114,7 +131,7 @@ func (p *popup) Reposition(r *xdgshell.Popup, pos *xdgshell.Positioner, token ui
114131 }
115132 p.pos = next
116133 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})
118135 }
119136 }
120137
@@ -138,10 +155,10 @@ func (p *popup) configure(c ports.ConfigurePopup) {
138155 }
139156 }
140157
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 {
145162 var top *window
146163 for _, w := range s.windows {
147164 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 {
149166 }
150167 }
151168 if top == nil {
152- return parent.popup == nil
169+ return parent == nil || parent.popup == nil
153170 }
154171 return top == parent
155172 }
@@ -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 *window14 w *window
14 resource *xdgshell.Popup15 resource *xdgshell.Popup
15 parent *window16 parent *window
16- pos ports.Positioner17+ // layer is the parent set through zwlr_layer_surface_v1.get_popup,
17- grab bool18+ // 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 bool23 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] = w47 s.windows[w.id] = w
44 x.window = w48 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 = true64 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 = true68 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 the108 // 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.server117 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 return121 return
105 }122 }
@@ -114,7 +131,7 @@ func (p *popup) Reposition(r *xdgshell.Popup, pos *xdgshell.Positioner, token ui
114 }131 }
115 p.pos = next132 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 is158+// 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 *window162 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 == nil169+ return parent == nil || parent.popup == nil
153 }170 }
154 return top == parent171 return top == parent
155 }172 }
M internal/adapters/wayland/server.go +67 -25
@@ -363,6 +363,14 @@ func (s *Server) apply(cmd ports.ClientCommand) {
363363 case ports.PointerFocus:
364364 s.changePointerFocus(c.ID, c.X, c.Y)
365365 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+ }
366374 if w := s.windows[c.ID]; w != nil && c.ID == s.pointerFocus {
367375 if s.relativeMotion(w, c) && s.locked(w) {
368376 for _, p := range s.windowPointers(w) {
@@ -386,14 +394,14 @@ func (s *Server) apply(cmd ports.ClientCommand) {
386394 }
387395 case ports.PointerButtonTo:
388396 // 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 {
390398 s.serial++
391399 state := uint32(0)
392400 if c.Pressed {
393401 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()
395403 }
396- for _, p := range s.windowPointers(w) {
404+ for _, p := range pointers {
397405 p.SendButton(s.serial, c.TimeMsec, c.Button, state)
398406 pointerFrame(p)
399407 }
@@ -404,7 +412,8 @@ func (s *Server) apply(cmd ports.ClientCommand) {
404412 return
405413 }
406414 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 {
408417 sendAxis(p, c.Axis, steps, values)
409418 }
410419 case ports.SetKeymap:
@@ -506,15 +515,42 @@ func (s *Server) focusTarget(id ports.WindowID) (*wayland.Surface, []*wayland.Ke
506515 }
507516 }
508517 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())
513520 }
514521 }
515522 return nil, nil
516523 }
517524
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+
518554 // keymapFile writes a sealed memfd holding the NUL-terminated keymap.
519555 func keymapFile(keymap string) (int, uint32, error) {
520556 if uint64(len(keymap))+1 > uint64(^uint32(0)) {
@@ -698,8 +734,19 @@ func (s *Server) windowPointers(w *window) []*wayland.Pointer {
698734 if w == nil || !w.mapped || !w.xdg.resource.Resource.Alive() {
699735 return nil
700736 }
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 {
701748 var result []*wayland.Pointer
702- for _, p := range s.pointers[w.xdg.resource.Client()] {
749+ for _, p := range s.pointers[c] {
703750 if p.Resource.Alive() {
704751 result = append(result, p)
705752 }
@@ -714,25 +761,20 @@ func (s *Server) changePointerFocus(id ports.WindowID, x, y float64) {
714761 // The new client sets its own cursor on enter; until then, the arrow.
715762 s.cursorSurface = nil
716763 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)
724769 }
725770 }
726771 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)
736778 }
737779 }
738780 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 = 1401 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 return412 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, nil522 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 nil735 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.Pointer748 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 = nil762 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 = 0771 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, y774+ 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 {
8181 // popups are the placed xdg_popups; popupOrder stacks them, oldest first.
8282 popups map[WindowID]*popupState
8383 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
8487 }
8588
8689 func keyName(s string) string {
@@ -289,22 +292,17 @@ func (c *Core) clientRect(p Placement) Rect {
289292 return Rect{X: r.X + b, Y: r.Y + b, W: r.W - 2*b, H: r.H - 2*b}
290293 }
291294
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.
293297 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
302300 }
303301
304302 // keyboardFocus is the mapped top/overlay layer with exclusive keyboard
305303 // 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.
308306 func (c *Core) keyboardFocus() WindowID {
309307 var layer WindowID
310308 for _, sc := range c.screens {
@@ -321,6 +319,9 @@ func (c *Core) keyboardFocus() WindowID {
321319 if g := c.grabFocus(); g != 0 {
322320 return g
323321 }
322+ if c.layerFocus != 0 && c.onDemand(c.layerFocus) {
323+ return c.layerFocus
324+ }
324325 id, _ := c.cur().mon.Focused()
325326 return id
326327 }
@@ -503,9 +504,10 @@ func (c *Core) allLayers() []ports.LayerSurface {
503504 return all
504505 }
505506
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.
509511 func (c *Core) hit(x, y float64) (WindowID, float64, float64) {
510512 o, ok := c.layout().At(x, y)
511513 if !ok {
@@ -517,6 +519,9 @@ func (c *Core) hit(x, y float64) (WindowID, float64, float64) {
517519 if id, px, py := c.popupAt(sc, lx, ly); id != 0 {
518520 return id, px, py
519521 }
522+ if id, px, py := layerAt(sc, lx, ly, true); id != 0 {
523+ return id, px, py
524+ }
520525 var id WindowID
521526 var sx, sy float64
522527 full := false
@@ -534,6 +539,9 @@ func (c *Core) hit(x, y float64) (WindowID, float64, float64) {
534539 full = full || p.Fullscreen
535540 }
536541 }
542+ if id == 0 {
543+ return layerAt(sc, lx, ly, false)
544+ }
537545 return id, sx, sy
538546 }
539547
@@ -726,8 +734,20 @@ func (c *Core) Run(ctx context.Context) error {
726734 if err := c.command(ctx, ports.PointerButtonTo{ID: id, Button: v.Button, Pressed: v.Pressed, TimeMsec: v.TimeMsec}); err != nil {
727735 return nil
728736 }
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()) {
731751 w.FocusID(id)
732752 c.focusScreen = c.screenIndex(s.name())
733753 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]*popupState82 popups map[WindowID]*popupState
83 popupOrder []WindowID83 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 keyboard302 // keyboardFocus is the mapped top/overlay layer with exclusive keyboard
305 // interactivity and the highest ID on any output, else the focused window of303 // interactivity and the highest ID on any output, else the focused window of
306-// the focused output. On-demand layers never take focus automatically. When304+// 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 WindowID307 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 g320 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 id326 return id
326 }327 }
@@ -503,9 +504,10 @@ func (c *Core) allLayers() []ports.LayerSurface {
503 return all504 return all
504 }505 }
505 506
506-// hit returns the window under the global logical point and the point in507+// hit returns the surface under the global logical point and the point in
507-// its surface coordinates. Fullscreen wins; otherwise the last visible508+// 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, py520 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 WindowID525 var id WindowID
521 var sx, sy float64526 var sx, sy float64
522 full := false527 full := false
@@ -534,6 +539,9 @@ func (c *Core) hit(x, y float64) (WindowID, float64, float64) {
534 full = full || p.Fullscreen539 full = full || p.Fullscreen
535 }540 }
536 }541 }
542+ if id == 0 {
543+ return layerAt(sc, lx, ly, false)
544+ }
537 return id, sx, sy545 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 nil735 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.
7373 sort.SliceStable(placed, func(i, j int) bool { return placed[i].Layer < placed[j].Layer })
7474 return
7575 }
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 return74 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 {
125125 return e
126126 }
127127
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
129129 // area, output-local and logical. ok is false when it is not on screen.
130130 func (c *Core) windowRect(id WindowID) (*screen, Rect, bool) {
131131 if p := c.popups[id]; p != nil {
@@ -135,6 +135,12 @@ func (c *Core) windowRect(id WindowID) (*screen, Rect, bool) {
135135 }
136136 return sc, Rect{X: pr.X + p.rect.X, Y: pr.Y + p.rect.Y, W: p.rect.W, H: p.rect.H}, true
137137 }
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+ }
138144 for _, sc := range c.screens {
139145 for _, pl := range sc.mon.Layout() {
140146 if pl.ID == id {
@@ -125,7 +125,7 @@ func flipY(e uint32) uint32 {
125 return e125 return e
126 }126 }
127 127
128-// windowRect is where a window or popup is on its screen: its client128+// 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}, true136 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 {
532532 Windows []SceneWindow
533533 // Layers are drawn in slice order: background and bottom before windows,
534534 // top and overlay after. A fullscreen window covers bottom and top.
535+ // Popups (SceneWindow.Popup) are drawn last, over every layer.
535536 Layers []SceneLayer
536537 }
537538
@@ -532,6 +532,7 @@ type Scene struct {
532 Windows []SceneWindow532 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 []SceneLayer536 Layers []SceneLayer
536 }537 }
537 538

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 26

Tags 10

↑↓ moveenter openesc close