feat(wayland): serve zwp_xwayland_keyboard_grab as a fullscreen shortcuts inhibitor
- commit
- 65eceab46dc3076bfabf0b57119e1756f81791fa
- author
- brice · · 2026-10-22 15:33 UTC
- committer
- brice · · 2026-10-22 17:54 UTC
- parent
- c5b7312
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
9 files · +175 -1
M Makefile +1 -1
| @@ -70,7 +70,7 @@ adapter-imports: | |||
| 70 | 70 | [ -z "$$bad" ] || { echo "$$bad"; echo 'adapter imports another adapter: use ports or a shared library (Makefile ADAPTER_LIBS)' >&2; exit 1; } | |
| 71 | 71 | # Fast, mandatory guards for tiled content publications and SHM copies. | |
| 72 | 72 | perf-check: | |
| 73 | - | CGO_ENABLED=0 go test ./internal/core ./internal/app ./internal/adapters/drm ./internal/adapters/headless ./internal/adapters/vulkan ./internal/adapters/wayland ./internal/adapters/outputkit/capture ./internal/adapters/screencast ./internal/adapters/wayland/xwm -run '^(TestAnimateAllocations|TestPublishAllocations|TestPublishSubscribersAllocations|TestPublishStashSlideAllocations|TestPublishOverviewAllocations|TestPublishLeavingAllocations|TestOutputRoutingAllocations|TestReportSeenAllocations|TestFrameLifecycleTransitionsAllocations|TestFullscreenShownAllocations|TestFrameDecisionAllocations|TestCommitFrameAllocations|TestColorPipelineAllocations|TestShownBySnapshotAllocations|TestFlipShowsAllocations|TestMRUNoteFocusDoesNotAllocateWhenSettled|TestDueFramesAllocations|TestRenderSteadyStateAllocations|TestRenderSteadyStateDMABufAllocations|TestSceneOpaqueRegionAllocations|TestSceneDeltaAllocations|TestAccountFlipAllocations|TestFlipDoneAllocations|TestSceneWalkUnchangedTiledSHM|TestTiledCommitPublishAllocations|TestCapturedCommitApplyAllocations|TestCapturedViewportApplyAllocations|TestEffectiveInputEmptyTreeAllocations|TestHeadlessTiledSHMCallbackAndCopyBudget|TestReleaseHeldAllocations|TestCommitDamageAllocations|TestEmitContentAllocations|TestPresentationPushAllocations|TestAnimatingFrameSendAllocations|TestContentBatchCycleAllocations|TestWriteMappedAllocations|TestCapturedForwardAllocations|TestCommandForwardAllocations|TestOutboxQueuedDeliveryAllocations|TestCastFrameAllocations|TestCastProducerAllocations|TestCaptureCycleAllocations|TestLogScenesAllocations|TestOverviewNamedRowsAllocations|TestBufferContentAllocations|TestDueFramesFireScratch|TestRetireSemaphoreAllocatesNothing|TestTableLookupAllocations|TestTableInsertRemoveAllocations|TestPropertyUnchangedAllocations|TestDispatchAllocations|TestConfigureFlushAllocations|TestFocusAllocations|TestSpaceAllocations|TestUnmanagedGeometryAllocations|TestFocusInUnmanagedAllocations|TestUnmanagedPublishAllocations|TestUnmanagedHitAllocations|TestOverrideMovedUnchangedAllocations)$$' -count=1 | |
| 73 | + | CGO_ENABLED=0 go test ./internal/core ./internal/app ./internal/adapters/drm ./internal/adapters/headless ./internal/adapters/vulkan ./internal/adapters/wayland ./internal/adapters/outputkit/capture ./internal/adapters/screencast ./internal/adapters/wayland/xwm -run '^(TestAnimateAllocations|TestPublishAllocations|TestPublishSubscribersAllocations|TestPublishStashSlideAllocations|TestPublishOverviewAllocations|TestPublishLeavingAllocations|TestOutputRoutingAllocations|TestReportSeenAllocations|TestFrameLifecycleTransitionsAllocations|TestFullscreenShownAllocations|TestFrameDecisionAllocations|TestCommitFrameAllocations|TestColorPipelineAllocations|TestShownBySnapshotAllocations|TestFlipShowsAllocations|TestMRUNoteFocusDoesNotAllocateWhenSettled|TestDueFramesAllocations|TestRenderSteadyStateAllocations|TestRenderSteadyStateDMABufAllocations|TestSceneOpaqueRegionAllocations|TestSceneDeltaAllocations|TestAccountFlipAllocations|TestFlipDoneAllocations|TestSceneWalkUnchangedTiledSHM|TestTiledCommitPublishAllocations|TestCapturedCommitApplyAllocations|TestCapturedViewportApplyAllocations|TestEffectiveInputEmptyTreeAllocations|TestHeadlessTiledSHMCallbackAndCopyBudget|TestReleaseHeldAllocations|TestCommitDamageAllocations|TestEmitContentAllocations|TestPresentationPushAllocations|TestAnimatingFrameSendAllocations|TestContentBatchCycleAllocations|TestWriteMappedAllocations|TestCapturedForwardAllocations|TestCommandForwardAllocations|TestOutboxQueuedDeliveryAllocations|TestCastFrameAllocations|TestCastProducerAllocations|TestCaptureCycleAllocations|TestLogScenesAllocations|TestOverviewNamedRowsAllocations|TestBufferContentAllocations|TestDueFramesFireScratch|TestRetireSemaphoreAllocatesNothing|TestTableLookupAllocations|TestTableInsertRemoveAllocations|TestPropertyUnchangedAllocations|TestDispatchAllocations|TestConfigureFlushAllocations|TestFocusAllocations|TestSpaceAllocations|TestUnmanagedGeometryAllocations|TestFocusInUnmanagedAllocations|TestUnmanagedPublishAllocations|TestUnmanagedHitAllocations|TestOverrideMovedUnchangedAllocations|TestKeyboardGrabSyncAllocations)$$' -count=1 | |
| 74 | 74 | CGO_ENABLED=0 go test -tags xturbo_poison ./internal/adapters/wayland/xwm -count=1 | |
| 75 | 75 | check: vet test arch fakes-check log-check perf-check | |
| 76 | 76 | ||
| @@ -70,7 +70,7 @@ adapter-imports: | |||||
| 70 | [ -z "$$bad" ] || { echo "$$bad"; echo 'adapter imports another adapter: use ports or a shared library (Makefile ADAPTER_LIBS)' >&2; exit 1; } | 70 | [ -z "$$bad" ] || { echo "$$bad"; echo 'adapter imports another adapter: use ports or a shared library (Makefile ADAPTER_LIBS)' >&2; exit 1; } | ||
| 71 | # Fast, mandatory guards for tiled content publications and SHM copies. | 71 | # Fast, mandatory guards for tiled content publications and SHM copies. | ||
| 72 | perf-check: | 72 | perf-check: | ||
| 73 | - | CGO_ENABLED=0 go test ./internal/core ./internal/app ./internal/adapters/drm ./internal/adapters/headless ./internal/adapters/vulkan ./internal/adapters/wayland ./internal/adapters/outputkit/capture ./internal/adapters/screencast ./internal/adapters/wayland/xwm -run '^(TestAnimateAllocations|TestPublishAllocations|TestPublishSubscribersAllocations|TestPublishStashSlideAllocations|TestPublishOverviewAllocations|TestPublishLeavingAllocations|TestOutputRoutingAllocations|TestReportSeenAllocations|TestFrameLifecycleTransitionsAllocations|TestFullscreenShownAllocations|TestFrameDecisionAllocations|TestCommitFrameAllocations|TestColorPipelineAllocations|TestShownBySnapshotAllocations|TestFlipShowsAllocations|TestMRUNoteFocusDoesNotAllocateWhenSettled|TestDueFramesAllocations|TestRenderSteadyStateAllocations|TestRenderSteadyStateDMABufAllocations|TestSceneOpaqueRegionAllocations|TestSceneDeltaAllocations|TestAccountFlipAllocations|TestFlipDoneAllocations|TestSceneWalkUnchangedTiledSHM|TestTiledCommitPublishAllocations|TestCapturedCommitApplyAllocations|TestCapturedViewportApplyAllocations|TestEffectiveInputEmptyTreeAllocations|TestHeadlessTiledSHMCallbackAndCopyBudget|TestReleaseHeldAllocations|TestCommitDamageAllocations|TestEmitContentAllocations|TestPresentationPushAllocations|TestAnimatingFrameSendAllocations|TestContentBatchCycleAllocations|TestWriteMappedAllocations|TestCapturedForwardAllocations|TestCommandForwardAllocations|TestOutboxQueuedDeliveryAllocations|TestCastFrameAllocations|TestCastProducerAllocations|TestCaptureCycleAllocations|TestLogScenesAllocations|TestOverviewNamedRowsAllocations|TestBufferContentAllocations|TestDueFramesFireScratch|TestRetireSemaphoreAllocatesNothing|TestTableLookupAllocations|TestTableInsertRemoveAllocations|TestPropertyUnchangedAllocations|TestDispatchAllocations|TestConfigureFlushAllocations|TestFocusAllocations|TestSpaceAllocations|TestUnmanagedGeometryAllocations|TestFocusInUnmanagedAllocations|TestUnmanagedPublishAllocations|TestUnmanagedHitAllocations|TestOverrideMovedUnchangedAllocations)$$' -count=1 | 73 | + | CGO_ENABLED=0 go test ./internal/core ./internal/app ./internal/adapters/drm ./internal/adapters/headless ./internal/adapters/vulkan ./internal/adapters/wayland ./internal/adapters/outputkit/capture ./internal/adapters/screencast ./internal/adapters/wayland/xwm -run '^(TestAnimateAllocations|TestPublishAllocations|TestPublishSubscribersAllocations|TestPublishStashSlideAllocations|TestPublishOverviewAllocations|TestPublishLeavingAllocations|TestOutputRoutingAllocations|TestReportSeenAllocations|TestFrameLifecycleTransitionsAllocations|TestFullscreenShownAllocations|TestFrameDecisionAllocations|TestCommitFrameAllocations|TestColorPipelineAllocations|TestShownBySnapshotAllocations|TestFlipShowsAllocations|TestMRUNoteFocusDoesNotAllocateWhenSettled|TestDueFramesAllocations|TestRenderSteadyStateAllocations|TestRenderSteadyStateDMABufAllocations|TestSceneOpaqueRegionAllocations|TestSceneDeltaAllocations|TestAccountFlipAllocations|TestFlipDoneAllocations|TestSceneWalkUnchangedTiledSHM|TestTiledCommitPublishAllocations|TestCapturedCommitApplyAllocations|TestCapturedViewportApplyAllocations|TestEffectiveInputEmptyTreeAllocations|TestHeadlessTiledSHMCallbackAndCopyBudget|TestReleaseHeldAllocations|TestCommitDamageAllocations|TestEmitContentAllocations|TestPresentationPushAllocations|TestAnimatingFrameSendAllocations|TestContentBatchCycleAllocations|TestWriteMappedAllocations|TestCapturedForwardAllocations|TestCommandForwardAllocations|TestOutboxQueuedDeliveryAllocations|TestCastFrameAllocations|TestCastProducerAllocations|TestCaptureCycleAllocations|TestLogScenesAllocations|TestOverviewNamedRowsAllocations|TestBufferContentAllocations|TestDueFramesFireScratch|TestRetireSemaphoreAllocatesNothing|TestTableLookupAllocations|TestTableInsertRemoveAllocations|TestPropertyUnchangedAllocations|TestDispatchAllocations|TestConfigureFlushAllocations|TestFocusAllocations|TestSpaceAllocations|TestUnmanagedGeometryAllocations|TestFocusInUnmanagedAllocations|TestUnmanagedPublishAllocations|TestUnmanagedHitAllocations|TestOverrideMovedUnchangedAllocations|TestKeyboardGrabSyncAllocations)$$' -count=1 |
| 74 | CGO_ENABLED=0 go test -tags xturbo_poison ./internal/adapters/wayland/xwm -count=1 | 74 | CGO_ENABLED=0 go test -tags xturbo_poison ./internal/adapters/wayland/xwm -count=1 | ||
| 75 | check: vet test arch fakes-check log-check perf-check | 75 | check: vet test arch fakes-check log-check perf-check | ||
| 76 | 76 | ||||
M internal/adapters/wayland/alloc_hotpath_test.go +9 -0
| @@ -246,3 +246,12 @@ func TestCommandForwardAllocations(t *testing.T) { | |||
| 246 | 246 | t.Errorf("command push + merge + drain + stats: %.1f allocs, want 0", allocs) | |
| 247 | 247 | } | |
| 248 | 248 | } | |
| 249 | + | ||
| 250 | + | // A settled grab (none, or unchanged) is re-checked at every focus and | |
| 251 | + | // configure without allocating. | |
| 252 | + | func TestKeyboardGrabSyncAllocations(t *testing.T) { | |
| 253 | + | s := &Server{windows: map[ports.WindowID]*window{}} | |
| 254 | + | if n := testing.AllocsPerRun(100, s.syncKeyboardGrab); n != 0 { | |
| 255 | + | t.Fatalf("%v allocs", n) | |
| 256 | + | } | |
| 257 | + | } | |
| @@ -246,3 +246,12 @@ func TestCommandForwardAllocations(t *testing.T) { | |||||
| 246 | t.Errorf("command push + merge + drain + stats: %.1f allocs, want 0", allocs) | 246 | t.Errorf("command push + merge + drain + stats: %.1f allocs, want 0", allocs) | ||
| 247 | } | 247 | } | ||
| 248 | } | 248 | } | ||
| 249 | + | ||||
| 250 | + | // A settled grab (none, or unchanged) is re-checked at every focus and | |||
| 251 | + | // configure without allocating. | |||
| 252 | + | func TestKeyboardGrabSyncAllocations(t *testing.T) { | |||
| 253 | + | s := &Server{windows: map[ports.WindowID]*window{}} | |||
| 254 | + | if n := testing.AllocsPerRun(100, s.syncKeyboardGrab); n != 0 { | |||
| 255 | + | t.Fatalf("%v allocs", n) | |||
| 256 | + | } | |||
| 257 | + | } | |||
M internal/adapters/wayland/globals.go +1 -0
| @@ -14,6 +14,7 @@ func registerGlobals(d *server.Display, o Options, s *Server) error { | |||
| 14 | 14 | for _, register := range []func() error{ | |
| 15 | 15 | func() error { return registerXDG(d, s) }, | |
| 16 | 16 | func() error { return registerXwaylandShell(d, s) }, | |
| 17 | + | func() error { return registerXwaylandGrab(d, s) }, | |
| 17 | 18 | func() error { return registerLayer(d, s) }, | |
| 18 | 19 | func() error { | |
| 19 | 20 | // Advertise only with the production owner gate and authoritative | |
| @@ -14,6 +14,7 @@ func registerGlobals(d *server.Display, o Options, s *Server) error { | |||||
| 14 | for _, register := range []func() error{ | 14 | for _, register := range []func() error{ | ||
| 15 | func() error { return registerXDG(d, s) }, | 15 | func() error { return registerXDG(d, s) }, | ||
| 16 | func() error { return registerXwaylandShell(d, s) }, | 16 | func() error { return registerXwaylandShell(d, s) }, | ||
| 17 | + | func() error { return registerXwaylandGrab(d, s) }, | |||
| 17 | func() error { return registerLayer(d, s) }, | 18 | func() error { return registerLayer(d, s) }, | ||
| 18 | func() error { | 19 | func() error { | ||
| 19 | // Advertise only with the production owner gate and authoritative | 20 | // Advertise only with the production owner gate and authoritative | ||
M internal/adapters/wayland/inhibit.go +4 -0
| @@ -121,6 +121,10 @@ func (s *Server) syncInhibitors() { | |||
| 121 | 121 | idle[in.window] = true | |
| 122 | 122 | } | |
| 123 | 123 | } | |
| 124 | + | if id := s.xgrabWindow(); id != 0 { | |
| 125 | + | shortcuts[id] = true | |
| 126 | + | } | |
| 127 | + | s.xgrabbed = s.xgrabWindow() | |
| 124 | 128 | for id, on := range diffSets(s.shortcutWindows, shortcuts) { | |
| 125 | 129 | s.emit(ports.ShortcutsInhibit{Window: id, Active: on}) | |
| 126 | 130 | } | |
| @@ -121,6 +121,10 @@ func (s *Server) syncInhibitors() { | |||||
| 121 | idle[in.window] = true | 121 | idle[in.window] = true | ||
| 122 | } | 122 | } | ||
| 123 | } | 123 | } | ||
| 124 | + | if id := s.xgrabWindow(); id != 0 { | |||
| 125 | + | shortcuts[id] = true | |||
| 126 | + | } | |||
| 127 | + | s.xgrabbed = s.xgrabWindow() | |||
| 124 | for id, on := range diffSets(s.shortcutWindows, shortcuts) { | 128 | for id, on := range diffSets(s.shortcutWindows, shortcuts) { | ||
| 125 | s.emit(ports.ShortcutsInhibit{Window: id, Active: on}) | 129 | s.emit(ports.ShortcutsInhibit{Window: id, Active: on}) | ||
| 126 | } | 130 | } | ||
M internal/adapters/wayland/seat.go +1 -0
| @@ -456,6 +456,7 @@ func (s *Server) changeFocus(id ports.WindowID) { | |||
| 456 | 456 | } | |
| 457 | 457 | s.textInputFocus() | |
| 458 | 458 | s.x11Focus() | |
| 459 | + | s.syncKeyboardGrab() | |
| 459 | 460 | s.log.Debug().Uint64("id", uint64(s.seat.focused)).Msg("keyboard focus") | |
| 460 | 461 | s.updateConstraint() | |
| 461 | 462 | } | |
| @@ -456,6 +456,7 @@ func (s *Server) changeFocus(id ports.WindowID) { | |||||
| 456 | } | 456 | } | ||
| 457 | s.textInputFocus() | 457 | s.textInputFocus() | ||
| 458 | s.x11Focus() | 458 | s.x11Focus() | ||
| 459 | + | s.syncKeyboardGrab() | |||
| 459 | s.log.Debug().Uint64("id", uint64(s.seat.focused)).Msg("keyboard focus") | 460 | s.log.Debug().Uint64("id", uint64(s.seat.focused)).Msg("keyboard focus") | ||
| 460 | s.updateConstraint() | 461 | s.updateConstraint() | ||
| 461 | } | 462 | } | ||
M internal/adapters/wayland/server.go +3 -0
| @@ -274,6 +274,8 @@ type Server struct { | |||
| 274 | 274 | // inhibitors are the live inhibitor objects (inhibit.go); | |
| 275 | 275 | // shortcutWindows and idleWindows what core was told. | |
| 276 | 276 | inhibitors []*inhibitor | |
| 277 | + | xgrab *xwaylandGrab // Xwayland keyboard grab (xwaylandgrab.go) | |
| 278 | + | xgrabbed ports.WindowID // window the grab inhibits for | |
| 277 | 279 | shortcutWindows, idleWindows map[ports.WindowID]bool | |
| 278 | 280 | // busInhibited is the last IdleInhibited value (idle.go). | |
| 279 | 281 | busInhibited bool | |
| @@ -816,6 +818,7 @@ func (s *Server) apply(cmd ports.ClientCommand) { | |||
| 816 | 818 | s.wakePacer() // reconsider FIFO barriers at the new period | |
| 817 | 819 | } | |
| 818 | 820 | w.sendConfigure() | |
| 821 | + | s.syncKeyboardGrab() | |
| 819 | 822 | s.toplevelChanged(w) | |
| 820 | 823 | if scanoutChanged && w.surf() != nil { | |
| 821 | 824 | s.dmabuf.resendSurface(w.surf()) | |
| @@ -274,6 +274,8 @@ type Server struct { | |||||
| 274 | // inhibitors are the live inhibitor objects (inhibit.go); | 274 | // inhibitors are the live inhibitor objects (inhibit.go); | ||
| 275 | // shortcutWindows and idleWindows what core was told. | 275 | // shortcutWindows and idleWindows what core was told. | ||
| 276 | inhibitors []*inhibitor | 276 | inhibitors []*inhibitor | ||
| 277 | + | xgrab *xwaylandGrab // Xwayland keyboard grab (xwaylandgrab.go) | |||
| 278 | + | xgrabbed ports.WindowID // window the grab inhibits for | |||
| 277 | shortcutWindows, idleWindows map[ports.WindowID]bool | 279 | shortcutWindows, idleWindows map[ports.WindowID]bool | ||
| 278 | // busInhibited is the last IdleInhibited value (idle.go). | 280 | // busInhibited is the last IdleInhibited value (idle.go). | ||
| 279 | busInhibited bool | 281 | busInhibited bool | ||
| @@ -816,6 +818,7 @@ func (s *Server) apply(cmd ports.ClientCommand) { | |||||
| 816 | s.wakePacer() // reconsider FIFO barriers at the new period | 818 | s.wakePacer() // reconsider FIFO barriers at the new period | ||
| 817 | } | 819 | } | ||
| 818 | w.sendConfigure() | 820 | w.sendConfigure() | ||
| 821 | + | s.syncKeyboardGrab() | |||
| 819 | s.toplevelChanged(w) | 822 | s.toplevelChanged(w) | ||
| 820 | if scanoutChanged && w.surf() != nil { | 823 | if scanoutChanged && w.surf() != nil { | ||
| 821 | s.dmabuf.resendSurface(w.surf()) | 824 | s.dmabuf.resendSurface(w.surf()) | ||
M internal/adapters/wayland/wire_client_test.go +3 -0
| @@ -55,6 +55,7 @@ import ( | |||
| 55 | 55 | wire_xdgforeign "github.com/bnema/go-wayland-bindings/server/xdgforeign" | |
| 56 | 56 | wire_xdgoutput "github.com/bnema/go-wayland-bindings/server/xdgoutput" | |
| 57 | 57 | wire_xdgshell "github.com/bnema/go-wayland-bindings/server/xdgshell" | |
| 58 | + | wire_xwaylandkeyboardgrab "github.com/bnema/go-wayland-bindings/server/xwaylandkeyboardgrab" | |
| 58 | 59 | wire_xwaylandshell "github.com/bnema/go-wayland-bindings/server/xwaylandshell" | |
| 59 | 60 | wire_imagecapture "github.com/bnema/neferwl/internal/adapters/wayland/imagecapture" | |
| 60 | 61 | "github.com/bnema/purego-libwayland/server" | |
| @@ -254,6 +255,8 @@ var wireSchemas = map[string]wireSchema{ | |||
| 254 | 255 | "zwp_text_input_v3": {wire_textinput.ZwpTextInputV3Interface, &client_textinput.TextInputV3{}, map[uint16]bool{0: true}, nil}, | |
| 255 | 256 | "zwp_virtual_keyboard_manager_v1": {wire_virtualkeyboard.ZwpVirtualKeyboardManagerV1Interface, &client_virtualkeyboard.VirtualKeyboardManager{}, nil, nil}, | |
| 256 | 257 | "zwp_virtual_keyboard_v1": {wire_virtualkeyboard.ZwpVirtualKeyboardV1Interface, &client_virtualkeyboard.VirtualKeyboard{}, map[uint16]bool{3: true}, nil}, | |
| 258 | + | "zwp_xwayland_keyboard_grab_manager_v1": {wire_xwaylandkeyboardgrab.ZwpXwaylandKeyboardGrabManagerV1Interface, nil, map[uint16]bool{0: true}, nil}, | |
| 259 | + | "zwp_xwayland_keyboard_grab_v1": {wire_xwaylandkeyboardgrab.ZwpXwaylandKeyboardGrabV1Interface, nil, map[uint16]bool{0: true}, nil}, | |
| 257 | 260 | "xwayland_shell_v1": {wire_xwaylandshell.XwaylandShellV1Interface, nil, map[uint16]bool{0: true}, nil}, | |
| 258 | 261 | "xwayland_surface_v1": {wire_xwaylandshell.XwaylandSurfaceV1Interface, nil, map[uint16]bool{1: true}, nil}, | |
| 259 | 262 | "zxdg_output_manager_v1": {wire_xdgoutput.ZxdgOutputManagerV1Interface, &client_xdgoutput.ZxdgOutputManager{}, map[uint16]bool{0: true}, nil}, | |
| @@ -55,6 +55,7 @@ import ( | |||||
| 55 | wire_xdgforeign "github.com/bnema/go-wayland-bindings/server/xdgforeign" | 55 | wire_xdgforeign "github.com/bnema/go-wayland-bindings/server/xdgforeign" | ||
| 56 | wire_xdgoutput "github.com/bnema/go-wayland-bindings/server/xdgoutput" | 56 | wire_xdgoutput "github.com/bnema/go-wayland-bindings/server/xdgoutput" | ||
| 57 | wire_xdgshell "github.com/bnema/go-wayland-bindings/server/xdgshell" | 57 | wire_xdgshell "github.com/bnema/go-wayland-bindings/server/xdgshell" | ||
| 58 | + | wire_xwaylandkeyboardgrab "github.com/bnema/go-wayland-bindings/server/xwaylandkeyboardgrab" | |||
| 58 | wire_xwaylandshell "github.com/bnema/go-wayland-bindings/server/xwaylandshell" | 59 | wire_xwaylandshell "github.com/bnema/go-wayland-bindings/server/xwaylandshell" | ||
| 59 | wire_imagecapture "github.com/bnema/neferwl/internal/adapters/wayland/imagecapture" | 60 | wire_imagecapture "github.com/bnema/neferwl/internal/adapters/wayland/imagecapture" | ||
| 60 | "github.com/bnema/purego-libwayland/server" | 61 | "github.com/bnema/purego-libwayland/server" | ||
| @@ -254,6 +255,8 @@ var wireSchemas = map[string]wireSchema{ | |||||
| 254 | "zwp_text_input_v3": {wire_textinput.ZwpTextInputV3Interface, &client_textinput.TextInputV3{}, map[uint16]bool{0: true}, nil}, | 255 | "zwp_text_input_v3": {wire_textinput.ZwpTextInputV3Interface, &client_textinput.TextInputV3{}, map[uint16]bool{0: true}, nil}, | ||
| 255 | "zwp_virtual_keyboard_manager_v1": {wire_virtualkeyboard.ZwpVirtualKeyboardManagerV1Interface, &client_virtualkeyboard.VirtualKeyboardManager{}, nil, nil}, | 256 | "zwp_virtual_keyboard_manager_v1": {wire_virtualkeyboard.ZwpVirtualKeyboardManagerV1Interface, &client_virtualkeyboard.VirtualKeyboardManager{}, nil, nil}, | ||
| 256 | "zwp_virtual_keyboard_v1": {wire_virtualkeyboard.ZwpVirtualKeyboardV1Interface, &client_virtualkeyboard.VirtualKeyboard{}, map[uint16]bool{3: true}, nil}, | 257 | "zwp_virtual_keyboard_v1": {wire_virtualkeyboard.ZwpVirtualKeyboardV1Interface, &client_virtualkeyboard.VirtualKeyboard{}, map[uint16]bool{3: true}, nil}, | ||
| 258 | + | "zwp_xwayland_keyboard_grab_manager_v1": {wire_xwaylandkeyboardgrab.ZwpXwaylandKeyboardGrabManagerV1Interface, nil, map[uint16]bool{0: true}, nil}, | |||
| 259 | + | "zwp_xwayland_keyboard_grab_v1": {wire_xwaylandkeyboardgrab.ZwpXwaylandKeyboardGrabV1Interface, nil, map[uint16]bool{0: true}, nil}, | |||
| 257 | "xwayland_shell_v1": {wire_xwaylandshell.XwaylandShellV1Interface, nil, map[uint16]bool{0: true}, nil}, | 260 | "xwayland_shell_v1": {wire_xwaylandshell.XwaylandShellV1Interface, nil, map[uint16]bool{0: true}, nil}, | ||
| 258 | "xwayland_surface_v1": {wire_xwaylandshell.XwaylandSurfaceV1Interface, nil, map[uint16]bool{1: true}, nil}, | 261 | "xwayland_surface_v1": {wire_xwaylandshell.XwaylandSurfaceV1Interface, nil, map[uint16]bool{1: true}, nil}, | ||
| 259 | "zxdg_output_manager_v1": {wire_xdgoutput.ZxdgOutputManagerV1Interface, &client_xdgoutput.ZxdgOutputManager{}, map[uint16]bool{0: true}, nil}, | 262 | "zxdg_output_manager_v1": {wire_xdgoutput.ZxdgOutputManagerV1Interface, &client_xdgoutput.ZxdgOutputManager{}, map[uint16]bool{0: true}, nil}, | ||
M internal/adapters/wayland/x11window_test.go +63 -0
| @@ -11,6 +11,7 @@ import ( | |||
| 11 | 11 | "github.com/bnema/go-wayland-bindings/server/pointerconstraints" | |
| 12 | 12 | "github.com/bnema/go-wayland-bindings/server/wayland" | |
| 13 | 13 | "github.com/bnema/go-wayland-bindings/server/xdgshell" | |
| 14 | + | "github.com/bnema/go-wayland-bindings/server/xwaylandkeyboardgrab" | |
| 14 | 15 | "github.com/bnema/go-wayland-bindings/server/xwaylandshell" | |
| 15 | 16 | "github.com/bnema/neferwl/internal/ports" | |
| 16 | 17 | "github.com/bnema/wlturbo" | |
| @@ -351,3 +352,65 @@ func TestX11WindowPointerLock(t *testing.T) { | |||
| 351 | 352 | t.Fatalf("constrained %+v", got) | |
| 352 | 353 | } | |
| 353 | 354 | } | |
| 355 | + | ||
| 356 | + | // grabRig is an X11 window with an Xwayland keyboard grab on its surface. | |
| 357 | + | func grabRig(t *testing.T) (r *x11Rig, w ports.WindowID, surf uint32) { | |
| 358 | + | t.Helper() | |
| 359 | + | r = newX11Rig(t) | |
| 360 | + | r.x11Window(x11Win) | |
| 361 | + | surf = r.pair(x11Win, x11Serial) | |
| 362 | + | w = awaitEvent[ports.WindowMapped](t, r.events).ID | |
| 363 | + | seat := bindProtocol(t, r.xc, "wl_seat") | |
| 364 | + | registerProtocol(t, r.xc, seat) | |
| 365 | + | mgr := bindProtocol(t, r.xc, "zwp_xwayland_keyboard_grab_manager_v1") | |
| 366 | + | grab := r.xc.AllocateID() | |
| 367 | + | requestProtocol(t, r.xc, mgr, xwaylandkeyboardgrab.ZwpXwaylandKeyboardGrabManagerV1RequestGrabKeyboard, grab, surf, seat) | |
| 368 | + | registerProtocol(t, r.xc, grab) | |
| 369 | + | if err := r.xc.Roundtrip(); err != nil { | |
| 370 | + | t.Fatal(err) | |
| 371 | + | } | |
| 372 | + | return r, w, grab | |
| 373 | + | } | |
| 374 | + | ||
| 375 | + | // noInhibit fails when a ShortcutsInhibit event arrives shortly. | |
| 376 | + | func noInhibit(t *testing.T, r *x11Rig) { | |
| 377 | + | t.Helper() | |
| 378 | + | if err := r.xc.Roundtrip(); err != nil { | |
| 379 | + | t.Fatal(err) | |
| 380 | + | } | |
| 381 | + | for deadline := time.After(150 * time.Millisecond); ; { | |
| 382 | + | select { | |
| 383 | + | case ev := <-r.events: | |
| 384 | + | if v, ok := ev.(ports.ShortcutsInhibit); ok { | |
| 385 | + | t.Fatalf("unexpected %+v", v) | |
| 386 | + | } | |
| 387 | + | case <-deadline: | |
| 388 | + | return | |
| 389 | + | } | |
| 390 | + | } | |
| 391 | + | } | |
| 392 | + | ||
| 393 | + | func TestXwaylandKeyboardGrabThreeStates(t *testing.T) { | |
| 394 | + | r, w, grab := grabRig(t) | |
| 395 | + | // Windowed and focused: accepted, no effect. | |
| 396 | + | r.commands <- ports.ConfigureWindow{ID: w, Width: 100, Height: 100, Visible: true} | |
| 397 | + | r.commands <- ports.FocusWindow{ID: w} | |
| 398 | + | noInhibit(t, r) | |
| 399 | + | // Focused and fullscreen: inhibits. | |
| 400 | + | r.commands <- ports.ConfigureWindow{ID: w, Width: 100, Height: 100, Visible: true, Fullscreen: true} | |
| 401 | + | if v := awaitEvent[ports.ShortcutsInhibit](t, r.events); v != (ports.ShortcutsInhibit{Window: w, Active: true}) { | |
| 402 | + | t.Fatal(v) | |
| 403 | + | } | |
| 404 | + | // Focus lost: released. | |
| 405 | + | r.commands <- ports.FocusWindow{} | |
| 406 | + | if v := awaitEvent[ports.ShortcutsInhibit](t, r.events); v != (ports.ShortcutsInhibit{Window: w}) { | |
| 407 | + | t.Fatal(v) | |
| 408 | + | } | |
| 409 | + | // Back, then the grab object goes away. | |
| 410 | + | r.commands <- ports.FocusWindow{ID: w} | |
| 411 | + | awaitEvent[ports.ShortcutsInhibit](t, r.events) | |
| 412 | + | requestProtocol(t, r.xc, grab, xwaylandkeyboardgrab.ZwpXwaylandKeyboardGrabV1RequestDestroy) | |
| 413 | + | if v := awaitEvent[ports.ShortcutsInhibit](t, r.events); v != (ports.ShortcutsInhibit{Window: w}) { | |
| 414 | + | t.Fatal(v) | |
| 415 | + | } | |
| 416 | + | } | |
| @@ -11,6 +11,7 @@ import ( | |||||
| 11 | "github.com/bnema/go-wayland-bindings/server/pointerconstraints" | 11 | "github.com/bnema/go-wayland-bindings/server/pointerconstraints" | ||
| 12 | "github.com/bnema/go-wayland-bindings/server/wayland" | 12 | "github.com/bnema/go-wayland-bindings/server/wayland" | ||
| 13 | "github.com/bnema/go-wayland-bindings/server/xdgshell" | 13 | "github.com/bnema/go-wayland-bindings/server/xdgshell" | ||
| 14 | + | "github.com/bnema/go-wayland-bindings/server/xwaylandkeyboardgrab" | |||
| 14 | "github.com/bnema/go-wayland-bindings/server/xwaylandshell" | 15 | "github.com/bnema/go-wayland-bindings/server/xwaylandshell" | ||
| 15 | "github.com/bnema/neferwl/internal/ports" | 16 | "github.com/bnema/neferwl/internal/ports" | ||
| 16 | "github.com/bnema/wlturbo" | 17 | "github.com/bnema/wlturbo" | ||
| @@ -351,3 +352,65 @@ func TestX11WindowPointerLock(t *testing.T) { | |||||
| 351 | t.Fatalf("constrained %+v", got) | 352 | t.Fatalf("constrained %+v", got) | ||
| 352 | } | 353 | } | ||
| 353 | } | 354 | } | ||
| 355 | + | ||||
| 356 | + | // grabRig is an X11 window with an Xwayland keyboard grab on its surface. | |||
| 357 | + | func grabRig(t *testing.T) (r *x11Rig, w ports.WindowID, surf uint32) { | |||
| 358 | + | t.Helper() | |||
| 359 | + | r = newX11Rig(t) | |||
| 360 | + | r.x11Window(x11Win) | |||
| 361 | + | surf = r.pair(x11Win, x11Serial) | |||
| 362 | + | w = awaitEvent[ports.WindowMapped](t, r.events).ID | |||
| 363 | + | seat := bindProtocol(t, r.xc, "wl_seat") | |||
| 364 | + | registerProtocol(t, r.xc, seat) | |||
| 365 | + | mgr := bindProtocol(t, r.xc, "zwp_xwayland_keyboard_grab_manager_v1") | |||
| 366 | + | grab := r.xc.AllocateID() | |||
| 367 | + | requestProtocol(t, r.xc, mgr, xwaylandkeyboardgrab.ZwpXwaylandKeyboardGrabManagerV1RequestGrabKeyboard, grab, surf, seat) | |||
| 368 | + | registerProtocol(t, r.xc, grab) | |||
| 369 | + | if err := r.xc.Roundtrip(); err != nil { | |||
| 370 | + | t.Fatal(err) | |||
| 371 | + | } | |||
| 372 | + | return r, w, grab | |||
| 373 | + | } | |||
| 374 | + | ||||
| 375 | + | // noInhibit fails when a ShortcutsInhibit event arrives shortly. | |||
| 376 | + | func noInhibit(t *testing.T, r *x11Rig) { | |||
| 377 | + | t.Helper() | |||
| 378 | + | if err := r.xc.Roundtrip(); err != nil { | |||
| 379 | + | t.Fatal(err) | |||
| 380 | + | } | |||
| 381 | + | for deadline := time.After(150 * time.Millisecond); ; { | |||
| 382 | + | select { | |||
| 383 | + | case ev := <-r.events: | |||
| 384 | + | if v, ok := ev.(ports.ShortcutsInhibit); ok { | |||
| 385 | + | t.Fatalf("unexpected %+v", v) | |||
| 386 | + | } | |||
| 387 | + | case <-deadline: | |||
| 388 | + | return | |||
| 389 | + | } | |||
| 390 | + | } | |||
| 391 | + | } | |||
| 392 | + | ||||
| 393 | + | func TestXwaylandKeyboardGrabThreeStates(t *testing.T) { | |||
| 394 | + | r, w, grab := grabRig(t) | |||
| 395 | + | // Windowed and focused: accepted, no effect. | |||
| 396 | + | r.commands <- ports.ConfigureWindow{ID: w, Width: 100, Height: 100, Visible: true} | |||
| 397 | + | r.commands <- ports.FocusWindow{ID: w} | |||
| 398 | + | noInhibit(t, r) | |||
| 399 | + | // Focused and fullscreen: inhibits. | |||
| 400 | + | r.commands <- ports.ConfigureWindow{ID: w, Width: 100, Height: 100, Visible: true, Fullscreen: true} | |||
| 401 | + | if v := awaitEvent[ports.ShortcutsInhibit](t, r.events); v != (ports.ShortcutsInhibit{Window: w, Active: true}) { | |||
| 402 | + | t.Fatal(v) | |||
| 403 | + | } | |||
| 404 | + | // Focus lost: released. | |||
| 405 | + | r.commands <- ports.FocusWindow{} | |||
| 406 | + | if v := awaitEvent[ports.ShortcutsInhibit](t, r.events); v != (ports.ShortcutsInhibit{Window: w}) { | |||
| 407 | + | t.Fatal(v) | |||
| 408 | + | } | |||
| 409 | + | // Back, then the grab object goes away. | |||
| 410 | + | r.commands <- ports.FocusWindow{ID: w} | |||
| 411 | + | awaitEvent[ports.ShortcutsInhibit](t, r.events) | |||
| 412 | + | requestProtocol(t, r.xc, grab, xwaylandkeyboardgrab.ZwpXwaylandKeyboardGrabV1RequestDestroy) | |||
| 413 | + | if v := awaitEvent[ports.ShortcutsInhibit](t, r.events); v != (ports.ShortcutsInhibit{Window: w}) { | |||
| 414 | + | t.Fatal(v) | |||
| 415 | + | } | |||
| 416 | + | } | |||
A internal/adapters/wayland/xwaylandgrab.go +90 -0
| @@ -0,0 +1,90 @@ | |||
| 1 | + | package wayland | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "github.com/bnema/go-wayland-bindings/server/wayland" | |
| 5 | + | "github.com/bnema/go-wayland-bindings/server/xwaylandkeyboardgrab" | |
| 6 | + | "github.com/bnema/neferwl/internal/ports" | |
| 7 | + | "github.com/bnema/purego-libwayland/server" | |
| 8 | + | ) | |
| 9 | + | ||
| 10 | + | // zwp_xwayland_keyboard_grab_v1. Xwayland asks for it when an X client | |
| 11 | + | // takes an active keyboard grab. It is a shortcuts inhibitor, but only | |
| 12 | + | // while the grabbing surface's window has the keyboard focus and is | |
| 13 | + | // fullscreen: a windowed app must not lock the compositor binds out. | |
| 14 | + | // Otherwise the request is accepted and has no effect. Never honoured | |
| 15 | + | // while the session is protected. The global is visible to Xwayland only | |
| 16 | + | // (xwaylandRestricted). | |
| 17 | + | ||
| 18 | + | // xwaylandGrab is the seat's single grab. | |
| 19 | + | type xwaylandGrab struct { | |
| 20 | + | res *xwaylandkeyboardgrab.ZwpXwaylandKeyboardGrabV1 | |
| 21 | + | surf *surface | |
| 22 | + | } | |
| 23 | + | ||
| 24 | + | func registerXwaylandGrab(d *server.Display, s *Server) error { | |
| 25 | + | return xwaylandkeyboardgrab.NewZwpXwaylandKeyboardGrabManagerV1Global(d, 1, func(c server.Client, v, id uint32) { | |
| 26 | + | _, _ = xwaylandkeyboardgrab.NewZwpXwaylandKeyboardGrabManagerV1(c, int32(v), id, xwaylandGrabManager{s}) | |
| 27 | + | }) | |
| 28 | + | } | |
| 29 | + | ||
| 30 | + | type xwaylandGrabManager struct{ s *Server } | |
| 31 | + | ||
| 32 | + | func (xwaylandGrabManager) Destroy(*xwaylandkeyboardgrab.ZwpXwaylandKeyboardGrabManagerV1) {} | |
| 33 | + | ||
| 34 | + | // GrabKeyboard replaces the seat's grab with a new one. | |
| 35 | + | func (m xwaylandGrabManager) GrabKeyboard(r *xwaylandkeyboardgrab.ZwpXwaylandKeyboardGrabManagerV1, id uint32, surf *wayland.Surface, _ *wayland.Seat) { | |
| 36 | + | state := m.s.surfaceOf(surf) | |
| 37 | + | if state == nil { | |
| 38 | + | return | |
| 39 | + | } | |
| 40 | + | g := &xwaylandGrab{surf: state} | |
| 41 | + | res, err := xwaylandkeyboardgrab.NewZwpXwaylandKeyboardGrabV1(r.Client(), r.Version(), id, xwaylandGrabHandler{}) | |
| 42 | + | if err != nil { | |
| 43 | + | return | |
| 44 | + | } | |
| 45 | + | g.res = res | |
| 46 | + | old := m.s.xgrab | |
| 47 | + | m.s.xgrab = g | |
| 48 | + | res.OnDestroy = func() { | |
| 49 | + | if m.s.xgrab == g { | |
| 50 | + | m.s.xgrab = nil | |
| 51 | + | m.s.syncKeyboardGrab() | |
| 52 | + | } | |
| 53 | + | } | |
| 54 | + | if old != nil && old.res != nil && old.res.Resource.Alive() { | |
| 55 | + | old.res.Resource.Destroy() | |
| 56 | + | } | |
| 57 | + | m.s.syncKeyboardGrab() | |
| 58 | + | } | |
| 59 | + | ||
| 60 | + | type xwaylandGrabHandler struct{} | |
| 61 | + | ||
| 62 | + | func (xwaylandGrabHandler) Destroy(*xwaylandkeyboardgrab.ZwpXwaylandKeyboardGrabV1) {} | |
| 63 | + | ||
| 64 | + | // xgrabWindow is the window whose grab is in effect (0: none). | |
| 65 | + | func (s *Server) xgrabWindow() ports.WindowID { | |
| 66 | + | g := s.xgrab | |
| 67 | + | if g == nil || g.surf == nil || g.surf.destroyed || !g.res.Resource.Alive() { | |
| 68 | + | return 0 | |
| 69 | + | } | |
| 70 | + | if s.security != nil && s.security.Snapshot().Protected { | |
| 71 | + | return 0 | |
| 72 | + | } | |
| 73 | + | id := g.surf.root().windowID() | |
| 74 | + | if id == 0 || id != s.seat.focused { | |
| 75 | + | return 0 | |
| 76 | + | } | |
| 77 | + | if w := s.windows[id]; w == nil || !w.hasLast || !w.last.Fullscreen { | |
| 78 | + | return 0 | |
| 79 | + | } | |
| 80 | + | return id | |
| 81 | + | } | |
| 82 | + | ||
| 83 | + | // syncKeyboardGrab re-evaluates the grab after a focus or fullscreen | |
| 84 | + | // change, and reports to core only when its effect changed. | |
| 85 | + | func (s *Server) syncKeyboardGrab() { | |
| 86 | + | if id := s.xgrabWindow(); id != s.xgrabbed { | |
| 87 | + | s.xgrabbed = id | |
| 88 | + | s.syncInhibitors() | |
| 89 | + | } | |
| 90 | + | } | |
| @@ -0,0 +1,90 @@ | |||||
| 1 | + | package wayland | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "github.com/bnema/go-wayland-bindings/server/wayland" | |||
| 5 | + | "github.com/bnema/go-wayland-bindings/server/xwaylandkeyboardgrab" | |||
| 6 | + | "github.com/bnema/neferwl/internal/ports" | |||
| 7 | + | "github.com/bnema/purego-libwayland/server" | |||
| 8 | + | ) | |||
| 9 | + | ||||
| 10 | + | // zwp_xwayland_keyboard_grab_v1. Xwayland asks for it when an X client | |||
| 11 | + | // takes an active keyboard grab. It is a shortcuts inhibitor, but only | |||
| 12 | + | // while the grabbing surface's window has the keyboard focus and is | |||
| 13 | + | // fullscreen: a windowed app must not lock the compositor binds out. | |||
| 14 | + | // Otherwise the request is accepted and has no effect. Never honoured | |||
| 15 | + | // while the session is protected. The global is visible to Xwayland only | |||
| 16 | + | // (xwaylandRestricted). | |||
| 17 | + | ||||
| 18 | + | // xwaylandGrab is the seat's single grab. | |||
| 19 | + | type xwaylandGrab struct { | |||
| 20 | + | res *xwaylandkeyboardgrab.ZwpXwaylandKeyboardGrabV1 | |||
| 21 | + | surf *surface | |||
| 22 | + | } | |||
| 23 | + | ||||
| 24 | + | func registerXwaylandGrab(d *server.Display, s *Server) error { | |||
| 25 | + | return xwaylandkeyboardgrab.NewZwpXwaylandKeyboardGrabManagerV1Global(d, 1, func(c server.Client, v, id uint32) { | |||
| 26 | + | _, _ = xwaylandkeyboardgrab.NewZwpXwaylandKeyboardGrabManagerV1(c, int32(v), id, xwaylandGrabManager{s}) | |||
| 27 | + | }) | |||
| 28 | + | } | |||
| 29 | + | ||||
| 30 | + | type xwaylandGrabManager struct{ s *Server } | |||
| 31 | + | ||||
| 32 | + | func (xwaylandGrabManager) Destroy(*xwaylandkeyboardgrab.ZwpXwaylandKeyboardGrabManagerV1) {} | |||
| 33 | + | ||||
| 34 | + | // GrabKeyboard replaces the seat's grab with a new one. | |||
| 35 | + | func (m xwaylandGrabManager) GrabKeyboard(r *xwaylandkeyboardgrab.ZwpXwaylandKeyboardGrabManagerV1, id uint32, surf *wayland.Surface, _ *wayland.Seat) { | |||
| 36 | + | state := m.s.surfaceOf(surf) | |||
| 37 | + | if state == nil { | |||
| 38 | + | return | |||
| 39 | + | } | |||
| 40 | + | g := &xwaylandGrab{surf: state} | |||
| 41 | + | res, err := xwaylandkeyboardgrab.NewZwpXwaylandKeyboardGrabV1(r.Client(), r.Version(), id, xwaylandGrabHandler{}) | |||
| 42 | + | if err != nil { | |||
| 43 | + | return | |||
| 44 | + | } | |||
| 45 | + | g.res = res | |||
| 46 | + | old := m.s.xgrab | |||
| 47 | + | m.s.xgrab = g | |||
| 48 | + | res.OnDestroy = func() { | |||
| 49 | + | if m.s.xgrab == g { | |||
| 50 | + | m.s.xgrab = nil | |||
| 51 | + | m.s.syncKeyboardGrab() | |||
| 52 | + | } | |||
| 53 | + | } | |||
| 54 | + | if old != nil && old.res != nil && old.res.Resource.Alive() { | |||
| 55 | + | old.res.Resource.Destroy() | |||
| 56 | + | } | |||
| 57 | + | m.s.syncKeyboardGrab() | |||
| 58 | + | } | |||
| 59 | + | ||||
| 60 | + | type xwaylandGrabHandler struct{} | |||
| 61 | + | ||||
| 62 | + | func (xwaylandGrabHandler) Destroy(*xwaylandkeyboardgrab.ZwpXwaylandKeyboardGrabV1) {} | |||
| 63 | + | ||||
| 64 | + | // xgrabWindow is the window whose grab is in effect (0: none). | |||
| 65 | + | func (s *Server) xgrabWindow() ports.WindowID { | |||
| 66 | + | g := s.xgrab | |||
| 67 | + | if g == nil || g.surf == nil || g.surf.destroyed || !g.res.Resource.Alive() { | |||
| 68 | + | return 0 | |||
| 69 | + | } | |||
| 70 | + | if s.security != nil && s.security.Snapshot().Protected { | |||
| 71 | + | return 0 | |||
| 72 | + | } | |||
| 73 | + | id := g.surf.root().windowID() | |||
| 74 | + | if id == 0 || id != s.seat.focused { | |||
| 75 | + | return 0 | |||
| 76 | + | } | |||
| 77 | + | if w := s.windows[id]; w == nil || !w.hasLast || !w.last.Fullscreen { | |||
| 78 | + | return 0 | |||
| 79 | + | } | |||
| 80 | + | return id | |||
| 81 | + | } | |||
| 82 | + | ||||
| 83 | + | // syncKeyboardGrab re-evaluates the grab after a focus or fullscreen | |||
| 84 | + | // change, and reports to core only when its effect changed. | |||
| 85 | + | func (s *Server) syncKeyboardGrab() { | |||
| 86 | + | if id := s.xgrabWindow(); id != s.xgrabbed { | |||
| 87 | + | s.xgrabbed = id | |||
| 88 | + | s.syncInhibitors() | |||
| 89 | + | } | |||
| 90 | + | } | |||