#21 WIP: feat(xwayland): add built-in Xwayland launcher
- state
- draft
- branch
- feat/xwayland-builtin → main
- opened
- bnema ·
- updated
M .mockery.yml +30 -0
M Makefile +9 -2
M README.md +1 -1
M docs/config.md +9 -3
M docs/desktop.md +12 -1
M docs/dev/headless.md +25 -0
M go.mod +4 -3
M go.sum +8 -6
M internal/adapters/config/config.go +18 -4
M internal/adapters/config/config_test.go +36 -4
M internal/adapters/drm/colorop_match.go +11 -0
A internal/adapters/drm/sdrplane_test.go +30 -0
M internal/adapters/vulkan/capture_test.go +3 -0
A internal/adapters/vulkan/norace_test.go +5 -0
A internal/adapters/vulkan/race_test.go +8 -0
M internal/adapters/vulkan/scene_perf_test.go +6 -0
M internal/adapters/wayland/activation.go +3 -3
M internal/adapters/wayland/alloc_hotpath_test.go +9 -0
M internal/adapters/wayland/clipboard.go +5 -0
A internal/adapters/wayland/clipboard_xwm.go +93 -0
A internal/adapters/wayland/clipboard_xwm_test.go +147 -0
M internal/adapters/wayland/constraints.go +14 -13
M internal/adapters/wayland/decoration.go +1 -1
M internal/adapters/wayland/dialog.go +1 -1
M internal/adapters/wayland/dmabuf.go +3 -2
M internal/adapters/wayland/extforeigntoplevel.go +1 -1
| @@ -72,7 +72,7 @@ func (*extToplevelList) Destroy(*ext.ExtForeignToplevelListV1) {} | |||
| 72 | 72 | func (s *Server) mappedToplevels() []ports.WindowID { | |
| 73 | 73 | ids := make([]ports.WindowID, 0, len(s.windows)) | |
| 74 | 74 | for id, w := range s.windows { | |
| 75 | - | if w.toplevel != nil && w.mapped { | |
| 75 | + | if w.isToplevel() && w.mapped { | |
| 76 | 76 | ids = append(ids, id) | |
| 77 | 77 | } | |
| 78 | 78 | } | |
| @@ -72,7 +72,7 @@ func (*extToplevelList) Destroy(*ext.ExtForeignToplevelListV1) {} | |||||
| 72 | func (s *Server) mappedToplevels() []ports.WindowID { | 72 | func (s *Server) mappedToplevels() []ports.WindowID { | ||
| 73 | ids := make([]ports.WindowID, 0, len(s.windows)) | 73 | ids := make([]ports.WindowID, 0, len(s.windows)) | ||
| 74 | for id, w := range s.windows { | 74 | for id, w := range s.windows { | ||
| 75 | - | if w.toplevel != nil && w.mapped { | 75 | + | if w.isToplevel() && w.mapped { |
| 76 | ids = append(ids, id) | 76 | ids = append(ids, id) | ||
| 77 | } | 77 | } | ||
| 78 | } | 78 | } | ||
M internal/adapters/wayland/foreigntoplevel.go +6 -6
| @@ -44,7 +44,7 @@ func registerForeignToplevel(d *server.Display, s *Server) error { | |||
| 44 | 44 | r.OnDestroy = func() { s.toplevelManagers = removeItem(s.toplevelManagers, m) } | |
| 45 | 45 | ids := make([]ports.WindowID, 0, len(s.windows)) | |
| 46 | 46 | for id, w := range s.windows { | |
| 47 | - | if w.toplevel != nil && w.mapped { | |
| 47 | + | if w.isToplevel() && w.mapped { | |
| 48 | 48 | ids = append(ids, id) | |
| 49 | 49 | } | |
| 50 | 50 | } | |
| @@ -186,7 +186,7 @@ func toplevelState(w *window, version int32) []byte { | |||
| 186 | 186 | ||
| 187 | 187 | // toplevelChanged updates every manager after w mapped or changed. | |
| 188 | 188 | func (s *Server) toplevelChanged(w *window) { | |
| 189 | - | if w.toplevel == nil || !w.mapped { | |
| 189 | + | if !w.isToplevel() || !w.mapped { | |
| 190 | 190 | return | |
| 191 | 191 | } | |
| 192 | 192 | for _, m := range s.toplevelManagers { | |
| @@ -220,7 +220,7 @@ func (s *Server) refreshToplevels() { | |||
| 220 | 220 | for _, l := range s.extToplevelLists { | |
| 221 | 221 | for id, h := range l.handles { | |
| 222 | 222 | w := s.windows[id] | |
| 223 | - | if h.closed || w == nil || w.toplevel == nil || !w.mapped { | |
| 223 | + | if h.closed || w == nil || !w.isToplevel() || !w.mapped { | |
| 224 | 224 | l.closed(id) | |
| 225 | 225 | } | |
| 226 | 226 | } | |
| @@ -231,7 +231,7 @@ func (s *Server) refreshToplevels() { | |||
| 231 | 231 | for _, m := range s.toplevelManagers { | |
| 232 | 232 | for id, h := range m.handles { | |
| 233 | 233 | w := s.windows[id] | |
| 234 | - | if h.req.closed || w == nil || w.toplevel == nil || !w.mapped { | |
| 234 | + | if h.req.closed || w == nil || !w.isToplevel() || !w.mapped { | |
| 235 | 235 | m.closed(id) | |
| 236 | 236 | } | |
| 237 | 237 | } | |
| @@ -253,7 +253,7 @@ func (t *toplevelRequests) window() *window { | |||
| 253 | 253 | if t.s.protected() || t.closed { | |
| 254 | 254 | return nil | |
| 255 | 255 | } | |
| 256 | - | if w := t.s.windows[t.id]; w != nil && w.toplevel != nil && w.mapped && w.toplevel.Resource.Alive() { | |
| 256 | + | if w := t.s.windows[t.id]; w != nil && w.toplevelAlive() && w.mapped { | |
| 257 | 257 | return w | |
| 258 | 258 | } | |
| 259 | 259 | return nil | |
| @@ -274,7 +274,7 @@ func (t *toplevelRequests) Activate(*wlr.ZwlrForeignToplevelHandleV1, *wayland.S | |||
| 274 | 274 | } | |
| 275 | 275 | func (t *toplevelRequests) Close(*wlr.ZwlrForeignToplevelHandleV1) { | |
| 276 | 276 | if w := t.window(); w != nil { | |
| 277 | - | w.toplevel.SendClose() | |
| 277 | + | w.sendClose() | |
| 278 | 278 | } | |
| 279 | 279 | } | |
| 280 | 280 | func (t *toplevelRequests) SetRectangle(r *wlr.ZwlrForeignToplevelHandleV1, _ *wayland.Surface, _, _, w, h int32) { | |
| @@ -44,7 +44,7 @@ func registerForeignToplevel(d *server.Display, s *Server) error { | |||||
| 44 | r.OnDestroy = func() { s.toplevelManagers = removeItem(s.toplevelManagers, m) } | 44 | r.OnDestroy = func() { s.toplevelManagers = removeItem(s.toplevelManagers, m) } | ||
| 45 | ids := make([]ports.WindowID, 0, len(s.windows)) | 45 | ids := make([]ports.WindowID, 0, len(s.windows)) | ||
| 46 | for id, w := range s.windows { | 46 | for id, w := range s.windows { | ||
| 47 | - | if w.toplevel != nil && w.mapped { | 47 | + | if w.isToplevel() && w.mapped { |
| 48 | ids = append(ids, id) | 48 | ids = append(ids, id) | ||
| 49 | } | 49 | } | ||
| 50 | } | 50 | } | ||
| @@ -186,7 +186,7 @@ func toplevelState(w *window, version int32) []byte { | |||||
| 186 | 186 | ||||
| 187 | // toplevelChanged updates every manager after w mapped or changed. | 187 | // toplevelChanged updates every manager after w mapped or changed. | ||
| 188 | func (s *Server) toplevelChanged(w *window) { | 188 | func (s *Server) toplevelChanged(w *window) { | ||
| 189 | - | if w.toplevel == nil || !w.mapped { | 189 | + | if !w.isToplevel() || !w.mapped { |
| 190 | return | 190 | return | ||
| 191 | } | 191 | } | ||
| 192 | for _, m := range s.toplevelManagers { | 192 | for _, m := range s.toplevelManagers { | ||
| @@ -220,7 +220,7 @@ func (s *Server) refreshToplevels() { | |||||
| 220 | for _, l := range s.extToplevelLists { | 220 | for _, l := range s.extToplevelLists { | ||
| 221 | for id, h := range l.handles { | 221 | for id, h := range l.handles { | ||
| 222 | w := s.windows[id] | 222 | w := s.windows[id] | ||
| 223 | - | if h.closed || w == nil || w.toplevel == nil || !w.mapped { | 223 | + | if h.closed || w == nil || !w.isToplevel() || !w.mapped { |
| 224 | l.closed(id) | 224 | l.closed(id) | ||
| 225 | } | 225 | } | ||
| 226 | } | 226 | } | ||
| @@ -231,7 +231,7 @@ func (s *Server) refreshToplevels() { | |||||
| 231 | for _, m := range s.toplevelManagers { | 231 | for _, m := range s.toplevelManagers { | ||
| 232 | for id, h := range m.handles { | 232 | for id, h := range m.handles { | ||
| 233 | w := s.windows[id] | 233 | w := s.windows[id] | ||
| 234 | - | if h.req.closed || w == nil || w.toplevel == nil || !w.mapped { | 234 | + | if h.req.closed || w == nil || !w.isToplevel() || !w.mapped { |
| 235 | m.closed(id) | 235 | m.closed(id) | ||
| 236 | } | 236 | } | ||
| 237 | } | 237 | } | ||
| @@ -253,7 +253,7 @@ func (t *toplevelRequests) window() *window { | |||||
| 253 | if t.s.protected() || t.closed { | 253 | if t.s.protected() || t.closed { | ||
| 254 | return nil | 254 | return nil | ||
| 255 | } | 255 | } | ||
| 256 | - | if w := t.s.windows[t.id]; w != nil && w.toplevel != nil && w.mapped && w.toplevel.Resource.Alive() { | 256 | + | if w := t.s.windows[t.id]; w != nil && w.toplevelAlive() && w.mapped { |
| 257 | return w | 257 | return w | ||
| 258 | } | 258 | } | ||
| 259 | return nil | 259 | return nil | ||
| @@ -274,7 +274,7 @@ func (t *toplevelRequests) Activate(*wlr.ZwlrForeignToplevelHandleV1, *wayland.S | |||||
| 274 | } | 274 | } | ||
| 275 | func (t *toplevelRequests) Close(*wlr.ZwlrForeignToplevelHandleV1) { | 275 | func (t *toplevelRequests) Close(*wlr.ZwlrForeignToplevelHandleV1) { | ||
| 276 | if w := t.window(); w != nil { | 276 | if w := t.window(); w != nil { | ||
| 277 | - | w.toplevel.SendClose() | 277 | + | w.sendClose() |
| 278 | } | 278 | } | ||
| 279 | } | 279 | } | ||
| 280 | func (t *toplevelRequests) SetRectangle(r *wlr.ZwlrForeignToplevelHandleV1, _ *wayland.Surface, _, _, w, h int32) { | 280 | func (t *toplevelRequests) SetRectangle(r *wlr.ZwlrForeignToplevelHandleV1, _ *wayland.Surface, _, _, w, h int32) { | ||
M internal/adapters/wayland/frames.go +10 -6
| @@ -183,7 +183,7 @@ func (s *Server) pacePass(flipped map[string]bool, reports []ports.OutputPresent | |||
| 183 | 183 | func toplevelRoot(surf *surface) *surface { | |
| 184 | 184 | surf = surf.root() | |
| 185 | 185 | for surf.xdg != nil && surf.xdg.window != nil && surf.xdg.window.popup != nil && surf.xdg.window.popup.parent != nil { | |
| 186 | - | surf = surf.xdg.window.popup.parent.xdg.surface.root() | |
| 186 | + | surf = surf.xdg.window.popup.parent.surf().root() | |
| 187 | 187 | } | |
| 188 | 188 | return surf | |
| 189 | 189 | } | |
| @@ -193,7 +193,8 @@ func toplevelRoot(surf *surface) *surface { | |||
| 193 | 193 | // for a capture session is invisible. | |
| 194 | 194 | func (s *Server) invisible(surf *surface) bool { | |
| 195 | 195 | root := toplevelRoot(surf) | |
| 196 | - | return root.xdg != nil && root.xdg.window != nil && root.xdg.window.hasLast && !root.xdg.window.last.Visible | |
| 196 | + | w := root.toplevelWindow() | |
| 197 | + | return w != nil && w.hasLast && !w.last.Visible | |
| 197 | 198 | } | |
| 198 | 199 | ||
| 199 | 200 | // suspended reports that nothing draws the toplevel tree, not even a capture | |
| @@ -201,10 +202,11 @@ func (s *Server) invisible(surf *surface) bool { | |||
| 201 | 202 | // frame callbacks of its output so the capture keeps moving. | |
| 202 | 203 | func (s *Server) suspended(surf *surface) bool { | |
| 203 | 204 | root := toplevelRoot(surf) | |
| 204 | - | if root.xdg == nil || root.xdg.window == nil || !root.xdg.window.hasLast { | |
| 205 | + | w := root.toplevelWindow() | |
| 206 | + | if w == nil || !w.hasLast { | |
| 205 | 207 | return false | |
| 206 | 208 | } | |
| 207 | - | return !root.xdg.window.last.Visible && !root.xdg.window.last.Captured | |
| 209 | + | return !w.last.Visible && !w.last.Captured | |
| 208 | 210 | } | |
| 209 | 211 | ||
| 210 | 212 | // frameOutput is the pacing class for a surface; "" is the 60 Hz | |
| @@ -212,11 +214,13 @@ func (s *Server) suspended(surf *surface) bool { | |||
| 212 | 214 | func (s *Server) frameOutput(surf *surface) string { | |
| 213 | 215 | surf = toplevelRoot(surf) | |
| 214 | 216 | switch { | |
| 215 | - | case surf.xdg != nil && surf.xdg.window != nil: | |
| 217 | + | case surf.x11win != nil && surf.x11win.isOverride(): | |
| 218 | + | return s.overrideOutput(surf.x11win) | |
| 219 | + | case surf.toplevelWindow() != nil: | |
| 216 | 220 | if s.suspended(surf) { | |
| 217 | 221 | return suspendedFrameQueue | |
| 218 | 222 | } | |
| 219 | - | return surf.xdg.window.last.Output | |
| 223 | + | return surf.toplevelWindow().last.Output | |
| 220 | 224 | case surf.layer != nil && surf.layer.output != nil: | |
| 221 | 225 | return surf.layer.output.name() | |
| 222 | 226 | case surf.lock != nil && surf.lock.output != nil && !surf.lock.closed: | |
| @@ -183,7 +183,7 @@ func (s *Server) pacePass(flipped map[string]bool, reports []ports.OutputPresent | |||||
| 183 | func toplevelRoot(surf *surface) *surface { | 183 | func toplevelRoot(surf *surface) *surface { | ||
| 184 | surf = surf.root() | 184 | surf = surf.root() | ||
| 185 | for surf.xdg != nil && surf.xdg.window != nil && surf.xdg.window.popup != nil && surf.xdg.window.popup.parent != nil { | 185 | for surf.xdg != nil && surf.xdg.window != nil && surf.xdg.window.popup != nil && surf.xdg.window.popup.parent != nil { | ||
| 186 | - | surf = surf.xdg.window.popup.parent.xdg.surface.root() | 186 | + | surf = surf.xdg.window.popup.parent.surf().root() |
| 187 | } | 187 | } | ||
| 188 | return surf | 188 | return surf | ||
| 189 | } | 189 | } | ||
| @@ -193,7 +193,8 @@ func toplevelRoot(surf *surface) *surface { | |||||
| 193 | // for a capture session is invisible. | 193 | // for a capture session is invisible. | ||
| 194 | func (s *Server) invisible(surf *surface) bool { | 194 | func (s *Server) invisible(surf *surface) bool { | ||
| 195 | root := toplevelRoot(surf) | 195 | root := toplevelRoot(surf) | ||
| 196 | - | return root.xdg != nil && root.xdg.window != nil && root.xdg.window.hasLast && !root.xdg.window.last.Visible | 196 | + | w := root.toplevelWindow() |
| 197 | + | return w != nil && w.hasLast && !w.last.Visible | |||
| 197 | } | 198 | } | ||
| 198 | 199 | ||||
| 199 | // suspended reports that nothing draws the toplevel tree, not even a capture | 200 | // suspended reports that nothing draws the toplevel tree, not even a capture | ||
| @@ -201,10 +202,11 @@ func (s *Server) invisible(surf *surface) bool { | |||||
| 201 | // frame callbacks of its output so the capture keeps moving. | 202 | // frame callbacks of its output so the capture keeps moving. | ||
| 202 | func (s *Server) suspended(surf *surface) bool { | 203 | func (s *Server) suspended(surf *surface) bool { | ||
| 203 | root := toplevelRoot(surf) | 204 | root := toplevelRoot(surf) | ||
| 204 | - | if root.xdg == nil || root.xdg.window == nil || !root.xdg.window.hasLast { | 205 | + | w := root.toplevelWindow() |
| 206 | + | if w == nil || !w.hasLast { | |||
| 205 | return false | 207 | return false | ||
| 206 | } | 208 | } | ||
| 207 | - | return !root.xdg.window.last.Visible && !root.xdg.window.last.Captured | 209 | + | return !w.last.Visible && !w.last.Captured |
| 208 | } | 210 | } | ||
| 209 | 211 | ||||
| 210 | // frameOutput is the pacing class for a surface; "" is the 60 Hz | 212 | // frameOutput is the pacing class for a surface; "" is the 60 Hz | ||
| @@ -212,11 +214,13 @@ func (s *Server) suspended(surf *surface) bool { | |||||
| 212 | func (s *Server) frameOutput(surf *surface) string { | 214 | func (s *Server) frameOutput(surf *surface) string { | ||
| 213 | surf = toplevelRoot(surf) | 215 | surf = toplevelRoot(surf) | ||
| 214 | switch { | 216 | switch { | ||
| 215 | - | case surf.xdg != nil && surf.xdg.window != nil: | 217 | + | case surf.x11win != nil && surf.x11win.isOverride(): |
| 218 | + | return s.overrideOutput(surf.x11win) | |||
| 219 | + | case surf.toplevelWindow() != nil: | |||
| 216 | if s.suspended(surf) { | 220 | if s.suspended(surf) { | ||
| 217 | return suspendedFrameQueue | 221 | return suspendedFrameQueue | ||
| 218 | } | 222 | } | ||
| 219 | - | return surf.xdg.window.last.Output | 223 | + | return surf.toplevelWindow().last.Output |
| 220 | case surf.layer != nil && surf.layer.output != nil: | 224 | case surf.layer != nil && surf.layer.output != nil: | ||
| 221 | return surf.layer.output.name() | 225 | return surf.layer.output.name() | ||
| 222 | case surf.lock != nil && surf.lock.output != nil && !surf.lock.closed: | 226 | case surf.lock != nil && surf.lock.output != nil && !surf.lock.closed: | ||
M internal/adapters/wayland/globals.go +11 -1
| @@ -13,6 +13,8 @@ import ( | |||
| 13 | 13 | 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 | + | func() error { return registerXwaylandShell(d, s) }, | |
| 17 | + | func() error { return registerXwaylandGrab(d, s) }, | |
| 16 | 18 | func() error { return registerLayer(d, s) }, | |
| 17 | 19 | func() error { | |
| 18 | 20 | // Advertise only with the production owner gate and authoritative | |
| @@ -131,8 +133,16 @@ func (c compositor) CreateSurface(r *wayland.Compositor, id uint32) { | |||
| 131 | 133 | state.identity = c.server.nextSurface | |
| 132 | 134 | state.wl = w | |
| 133 | 135 | c.server.surfaces[w.Resource] = state | |
| 136 | + | c.server.surfaceByIdentity[state.identity] = state | |
| 134 | 137 | state.sendScale() | |
| 135 | - | w.OnDestroy = func() { delete(c.server.surfaces, w.Resource); state.Destroy(w) } | |
| 138 | + | w.OnDestroy = func() { | |
| 139 | + | delete(c.server.surfaces, w.Resource) | |
| 140 | + | delete(c.server.surfaceByIdentity, state.identity) | |
| 141 | + | state.Destroy(w) | |
| 142 | + | if state.kind == roleX11 && c.server.xwm != nil { | |
| 143 | + | c.server.xwm.SurfaceGone(state.identity) | |
| 144 | + | } | |
| 145 | + | } | |
| 136 | 146 | } | |
| 137 | 147 | } | |
| 138 | 148 | func (c compositor) CreateRegion(r *wayland.Compositor, id uint32) { | |
| @@ -13,6 +13,8 @@ import ( | |||||
| 13 | func registerGlobals(d *server.Display, o Options, s *Server) error { | 13 | 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) }, | |||
| 17 | + | func() error { return registerXwaylandGrab(d, s) }, | |||
| 16 | func() error { return registerLayer(d, s) }, | 18 | func() error { return registerLayer(d, s) }, | ||
| 17 | func() error { | 19 | func() error { | ||
| 18 | // Advertise only with the production owner gate and authoritative | 20 | // Advertise only with the production owner gate and authoritative | ||
| @@ -131,8 +133,16 @@ func (c compositor) CreateSurface(r *wayland.Compositor, id uint32) { | |||||
| 131 | state.identity = c.server.nextSurface | 133 | state.identity = c.server.nextSurface | ||
| 132 | state.wl = w | 134 | state.wl = w | ||
| 133 | c.server.surfaces[w.Resource] = state | 135 | c.server.surfaces[w.Resource] = state | ||
| 136 | + | c.server.surfaceByIdentity[state.identity] = state | |||
| 134 | state.sendScale() | 137 | state.sendScale() | ||
| 135 | - | w.OnDestroy = func() { delete(c.server.surfaces, w.Resource); state.Destroy(w) } | 138 | + | w.OnDestroy = func() { |
| 139 | + | delete(c.server.surfaces, w.Resource) | |||
| 140 | + | delete(c.server.surfaceByIdentity, state.identity) | |||
| 141 | + | state.Destroy(w) | |||
| 142 | + | if state.kind == roleX11 && c.server.xwm != nil { | |||
| 143 | + | c.server.xwm.SurfaceGone(state.identity) | |||
| 144 | + | } | |||
| 145 | + | } | |||
| 136 | } | 146 | } | ||
| 137 | } | 147 | } | ||
| 138 | func (c compositor) CreateRegion(r *wayland.Compositor, id uint32) { | 148 | func (c compositor) CreateRegion(r *wayland.Compositor, id uint32) { | ||
M internal/adapters/wayland/imagecopy.go +1 -1
| @@ -351,7 +351,7 @@ func (c *captureTarget) geometry() (o *output, rect image.Rectangle, hidden, ok | |||
| 351 | 351 | case srcWindow: | |
| 352 | 352 | // Core's state gives the size; before it, the last configure. | |
| 353 | 353 | w := c.s.windows[src.window] | |
| 354 | - | if w == nil || !w.mapped || w.toplevel == nil || w.gen != src.gen { | |
| 354 | + | if w == nil || !w.mapped || !w.isToplevel() || w.gen != src.gen { | |
| 355 | 355 | return nil, image.Rectangle{}, false, false | |
| 356 | 356 | } | |
| 357 | 357 | name, size := w.last.Output, ports.Rect{W: w.last.Width, H: w.last.Height} | |
| @@ -351,7 +351,7 @@ func (c *captureTarget) geometry() (o *output, rect image.Rectangle, hidden, ok | |||||
| 351 | case srcWindow: | 351 | case srcWindow: | ||
| 352 | // Core's state gives the size; before it, the last configure. | 352 | // Core's state gives the size; before it, the last configure. | ||
| 353 | w := c.s.windows[src.window] | 353 | w := c.s.windows[src.window] | ||
| 354 | - | if w == nil || !w.mapped || w.toplevel == nil || w.gen != src.gen { | 354 | + | if w == nil || !w.mapped || !w.isToplevel() || w.gen != src.gen { |
| 355 | return nil, image.Rectangle{}, false, false | 355 | return nil, image.Rectangle{}, false, false | ||
| 356 | } | 356 | } | ||
| 357 | name, size := w.last.Output, ports.Rect{W: w.last.Width, H: w.last.Height} | 357 | name, size := w.last.Output, ports.Rect{W: w.last.Width, H: w.last.Height} | ||
M internal/adapters/wayland/inhibit.go +5 -0
| @@ -121,6 +121,11 @@ func (s *Server) syncInhibitors() { | |||
| 121 | 121 | idle[in.window] = true | |
| 122 | 122 | } | |
| 123 | 123 | } | |
| 124 | + | // syncKeyboardGrab compares against xgrabbed: it is only written here. | |
| 125 | + | s.xgrabbed = s.xgrabWindow() | |
| 126 | + | if s.xgrabbed != 0 { | |
| 127 | + | shortcuts[s.xgrabbed] = true | |
| 128 | + | } | |
| 124 | 129 | for id, on := range diffSets(s.shortcutWindows, shortcuts) { | |
| 125 | 130 | s.emit(ports.ShortcutsInhibit{Window: id, Active: on}) | |
| 126 | 131 | } | |
| @@ -121,6 +121,11 @@ func (s *Server) syncInhibitors() { | |||||
| 121 | idle[in.window] = true | 121 | idle[in.window] = true | ||
| 122 | } | 122 | } | ||
| 123 | } | 123 | } | ||
| 124 | + | // syncKeyboardGrab compares against xgrabbed: it is only written here. | |||
| 125 | + | s.xgrabbed = s.xgrabWindow() | |||
| 126 | + | if s.xgrabbed != 0 { | |||
| 127 | + | shortcuts[s.xgrabbed] = true | |||
| 128 | + | } | |||
| 124 | for id, on := range diffSets(s.shortcutWindows, shortcuts) { | 129 | for id, on := range diffSets(s.shortcutWindows, shortcuts) { | ||
| 125 | s.emit(ports.ShortcutsInhibit{Window: id, Active: on}) | 130 | s.emit(ports.ShortcutsInhibit{Window: id, Active: on}) | ||
| 126 | } | 131 | } | ||
M internal/adapters/wayland/outputs.go +72 -1
| @@ -42,6 +42,8 @@ type output struct { | |||
| 42 | 42 | global *server.Global | |
| 43 | 43 | resources []*wayland.Output | |
| 44 | 44 | xdgOutputs []*xdgoutput.ZxdgOutputV1 | |
| 45 | + | // xdgOutputsX are the private xdg-outputs of the Xwayland client (X space). | |
| 46 | + | xdgOutputsX []*xdgoutput.ZxdgOutputV1 | |
| 45 | 47 | } | |
| 46 | 48 | ||
| 47 | 49 | func (o *output) name() string { return o.place.Info.Name } | |
| @@ -89,6 +91,36 @@ func (o *output) sendXDG(x *xdgoutput.ZxdgOutputV1, withName bool) { | |||
| 89 | 91 | } | |
| 90 | 92 | } | |
| 91 | 93 | ||
| 94 | + | // sendXDGX sends the private xdg-output state: the layout position moved to | |
| 95 | + | // X space and the logical size. | |
| 96 | + | func (o *output) sendXDGX(s *Server, x *xdgoutput.ZxdgOutputV1, withName bool) { | |
| 97 | + | ox, oy := s.xspace.Origin() | |
| 98 | + | x.SendLogicalPosition(int32(o.place.X-ox), int32(o.place.Y-oy)) | |
| 99 | + | x.SendLogicalSize(int32(o.place.Width), int32(o.place.Height)) | |
| 100 | + | if withName && x.Version() >= 2 { | |
| 101 | + | x.SendName(o.name()) | |
| 102 | + | x.SendDescription(o.description()) | |
| 103 | + | } | |
| 104 | + | } | |
| 105 | + | ||
| 106 | + | // republishXDGX resends every private xdg-output after a layout change, so | |
| 107 | + | // Xwayland resizes the root and its RandR outputs. | |
| 108 | + | func (s *Server) republishXDGX() { | |
| 109 | + | for _, o := range s.outputs { | |
| 110 | + | for _, x := range o.xdgOutputsX { | |
| 111 | + | o.sendXDGX(s, x, false) | |
| 112 | + | if x.Version() < 3 { | |
| 113 | + | x.SendDone() | |
| 114 | + | } | |
| 115 | + | } | |
| 116 | + | for _, r := range o.resources { | |
| 117 | + | if r.Client() == s.xwayland.client && r.Version() >= 2 { | |
| 118 | + | r.SendDone() | |
| 119 | + | } | |
| 120 | + | } | |
| 121 | + | } | |
| 122 | + | } | |
| 123 | + | ||
| 92 | 124 | // outputOf returns the output a wl_output resource belongs to, or nil for a | |
| 93 | 125 | // removed one. | |
| 94 | 126 | func (s *Server) outputOf(r *wayland.Output) *output { | |
| @@ -162,6 +194,10 @@ func (s *Server) setOutputs(c ports.SetOutputs) { | |||
| 162 | 194 | changed := !slices.Equal(s.outputPlaces, c.Outputs) | |
| 163 | 195 | focusChanged := s.focusedOutput != c.Focused | |
| 164 | 196 | s.outputPlaces = append(ports.Layout(nil), c.Outputs...) | |
| 197 | + | xChanged := s.xspace.Set(s.outputPlaces) | |
| 198 | + | if s.xwm != nil { | |
| 199 | + | s.xwm.LayoutChanged(xChanged) | |
| 200 | + | } | |
| 165 | 201 | defer func() { | |
| 166 | 202 | if changed { | |
| 167 | 203 | s.refreshOutputManagers() | |
| @@ -244,6 +280,9 @@ func (s *Server) setOutputs(c ports.SetOutputs) { | |||
| 244 | 280 | for _, surf := range s.surfaces { | |
| 245 | 281 | surf.sendScale() | |
| 246 | 282 | } | |
| 283 | + | if changed { | |
| 284 | + | s.republishXDGX() | |
| 285 | + | } | |
| 247 | 286 | s.notifyColorFeedback() | |
| 248 | 287 | // Layer surfaces sized from the output follow its new logical size. | |
| 249 | 288 | adopted := false | |
| @@ -279,10 +318,42 @@ type outputHandler struct{} | |||
| 279 | 318 | ||
| 280 | 319 | func (outputHandler) Release(*wayland.Output) {} | |
| 281 | 320 | ||
| 321 | + | // registerXDGOutput registers the public zxdg_output_manager_v1 and a second, | |
| 322 | + | // private one for Xwayland whose outputs are in X space (see globalVisible). | |
| 282 | 323 | func registerXDGOutput(d *server.Display, s *Server) error { | |
| 283 | - | return xdgoutput.NewZxdgOutputManagerV1Global(d, 3, func(c server.Client, v, id uint32) { | |
| 324 | + | var err error | |
| 325 | + | s.xdgPublic, err = d.AddGlobal(xdgoutput.ZxdgOutputManagerV1Interface, 3, func(c server.Client, v, id uint32) { | |
| 284 | 326 | _, _ = xdgoutput.NewZxdgOutputManagerV1(c, int32(v), id, xdgOutputManager{s}) | |
| 285 | 327 | }) | |
| 328 | + | if err != nil { | |
| 329 | + | return err | |
| 330 | + | } | |
| 331 | + | s.xdgPrivate, err = d.AddGlobal(xdgoutput.ZxdgOutputManagerV1Interface, 3, func(c server.Client, v, id uint32) { | |
| 332 | + | _, _ = xdgoutput.NewZxdgOutputManagerV1(c, int32(v), id, xdgOutputManagerX{s}) | |
| 333 | + | }) | |
| 334 | + | return err | |
| 335 | + | } | |
| 336 | + | ||
| 337 | + | type xdgOutputManagerX struct{ server *Server } | |
| 338 | + | ||
| 339 | + | func (xdgOutputManagerX) Destroy(*xdgoutput.ZxdgOutputManagerV1) {} | |
| 340 | + | func (m xdgOutputManagerX) GetXdgOutput(r *xdgoutput.ZxdgOutputManagerV1, id uint32, wl *wayland.Output) { | |
| 341 | + | x, err := xdgoutput.NewZxdgOutputV1(r.Client(), r.Version(), id, xdgOutput{}) | |
| 342 | + | if err != nil { | |
| 343 | + | return | |
| 344 | + | } | |
| 345 | + | o := m.server.outputOf(wl) | |
| 346 | + | if o == nil { | |
| 347 | + | return | |
| 348 | + | } | |
| 349 | + | o.xdgOutputsX = append(o.xdgOutputsX, x) | |
| 350 | + | x.OnDestroy = func() { o.xdgOutputsX = removeItem(o.xdgOutputsX, x) } | |
| 351 | + | o.sendXDGX(m.server, x, true) | |
| 352 | + | if r.Version() < 3 || wl.Version() < 2 { | |
| 353 | + | x.SendDone() | |
| 354 | + | } else { | |
| 355 | + | wl.SendDone() | |
| 356 | + | } | |
| 286 | 357 | } | |
| 287 | 358 | ||
| 288 | 359 | type xdgOutputManager struct{ server *Server } | |
| @@ -42,6 +42,8 @@ type output struct { | |||||
| 42 | global *server.Global | 42 | global *server.Global | ||
| 43 | resources []*wayland.Output | 43 | resources []*wayland.Output | ||
| 44 | xdgOutputs []*xdgoutput.ZxdgOutputV1 | 44 | xdgOutputs []*xdgoutput.ZxdgOutputV1 | ||
| 45 | + | // xdgOutputsX are the private xdg-outputs of the Xwayland client (X space). | |||
| 46 | + | xdgOutputsX []*xdgoutput.ZxdgOutputV1 | |||
| 45 | } | 47 | } | ||
| 46 | 48 | ||||
| 47 | func (o *output) name() string { return o.place.Info.Name } | 49 | func (o *output) name() string { return o.place.Info.Name } | ||
| @@ -89,6 +91,36 @@ func (o *output) sendXDG(x *xdgoutput.ZxdgOutputV1, withName bool) { | |||||
| 89 | } | 91 | } | ||
| 90 | } | 92 | } | ||
| 91 | 93 | ||||
| 94 | + | // sendXDGX sends the private xdg-output state: the layout position moved to | |||
| 95 | + | // X space and the logical size. | |||
| 96 | + | func (o *output) sendXDGX(s *Server, x *xdgoutput.ZxdgOutputV1, withName bool) { | |||
| 97 | + | ox, oy := s.xspace.Origin() | |||
| 98 | + | x.SendLogicalPosition(int32(o.place.X-ox), int32(o.place.Y-oy)) | |||
| 99 | + | x.SendLogicalSize(int32(o.place.Width), int32(o.place.Height)) | |||
| 100 | + | if withName && x.Version() >= 2 { | |||
| 101 | + | x.SendName(o.name()) | |||
| 102 | + | x.SendDescription(o.description()) | |||
| 103 | + | } | |||
| 104 | + | } | |||
| 105 | + | ||||
| 106 | + | // republishXDGX resends every private xdg-output after a layout change, so | |||
| 107 | + | // Xwayland resizes the root and its RandR outputs. | |||
| 108 | + | func (s *Server) republishXDGX() { | |||
| 109 | + | for _, o := range s.outputs { | |||
| 110 | + | for _, x := range o.xdgOutputsX { | |||
| 111 | + | o.sendXDGX(s, x, false) | |||
| 112 | + | if x.Version() < 3 { | |||
| 113 | + | x.SendDone() | |||
| 114 | + | } | |||
| 115 | + | } | |||
| 116 | + | for _, r := range o.resources { | |||
| 117 | + | if r.Client() == s.xwayland.client && r.Version() >= 2 { | |||
| 118 | + | r.SendDone() | |||
| 119 | + | } | |||
| 120 | + | } | |||
| 121 | + | } | |||
| 122 | + | } | |||
| 123 | + | ||||
| 92 | // outputOf returns the output a wl_output resource belongs to, or nil for a | 124 | // outputOf returns the output a wl_output resource belongs to, or nil for a | ||
| 93 | // removed one. | 125 | // removed one. | ||
| 94 | func (s *Server) outputOf(r *wayland.Output) *output { | 126 | func (s *Server) outputOf(r *wayland.Output) *output { | ||
| @@ -162,6 +194,10 @@ func (s *Server) setOutputs(c ports.SetOutputs) { | |||||
| 162 | changed := !slices.Equal(s.outputPlaces, c.Outputs) | 194 | changed := !slices.Equal(s.outputPlaces, c.Outputs) | ||
| 163 | focusChanged := s.focusedOutput != c.Focused | 195 | focusChanged := s.focusedOutput != c.Focused | ||
| 164 | s.outputPlaces = append(ports.Layout(nil), c.Outputs...) | 196 | s.outputPlaces = append(ports.Layout(nil), c.Outputs...) | ||
| 197 | + | xChanged := s.xspace.Set(s.outputPlaces) | |||
| 198 | + | if s.xwm != nil { | |||
| 199 | + | s.xwm.LayoutChanged(xChanged) | |||
| 200 | + | } | |||
| 165 | defer func() { | 201 | defer func() { | ||
| 166 | if changed { | 202 | if changed { | ||
| 167 | s.refreshOutputManagers() | 203 | s.refreshOutputManagers() | ||
| @@ -244,6 +280,9 @@ func (s *Server) setOutputs(c ports.SetOutputs) { | |||||
| 244 | for _, surf := range s.surfaces { | 280 | for _, surf := range s.surfaces { | ||
| 245 | surf.sendScale() | 281 | surf.sendScale() | ||
| 246 | } | 282 | } | ||
| 283 | + | if changed { | |||
| 284 | + | s.republishXDGX() | |||
| 285 | + | } | |||
| 247 | s.notifyColorFeedback() | 286 | s.notifyColorFeedback() | ||
| 248 | // Layer surfaces sized from the output follow its new logical size. | 287 | // Layer surfaces sized from the output follow its new logical size. | ||
| 249 | adopted := false | 288 | adopted := false | ||
| @@ -279,10 +318,42 @@ type outputHandler struct{} | |||||
| 279 | 318 | ||||
| 280 | func (outputHandler) Release(*wayland.Output) {} | 319 | func (outputHandler) Release(*wayland.Output) {} | ||
| 281 | 320 | ||||
| 321 | + | // registerXDGOutput registers the public zxdg_output_manager_v1 and a second, | |||
| 322 | + | // private one for Xwayland whose outputs are in X space (see globalVisible). | |||
| 282 | func registerXDGOutput(d *server.Display, s *Server) error { | 323 | func registerXDGOutput(d *server.Display, s *Server) error { | ||
| 283 | - | return xdgoutput.NewZxdgOutputManagerV1Global(d, 3, func(c server.Client, v, id uint32) { | 324 | + | var err error |
| 325 | + | s.xdgPublic, err = d.AddGlobal(xdgoutput.ZxdgOutputManagerV1Interface, 3, func(c server.Client, v, id uint32) { | |||
| 284 | _, _ = xdgoutput.NewZxdgOutputManagerV1(c, int32(v), id, xdgOutputManager{s}) | 326 | _, _ = xdgoutput.NewZxdgOutputManagerV1(c, int32(v), id, xdgOutputManager{s}) | ||
| 285 | }) | 327 | }) | ||
| 328 | + | if err != nil { | |||
| 329 | + | return err | |||
| 330 | + | } | |||
| 331 | + | s.xdgPrivate, err = d.AddGlobal(xdgoutput.ZxdgOutputManagerV1Interface, 3, func(c server.Client, v, id uint32) { | |||
| 332 | + | _, _ = xdgoutput.NewZxdgOutputManagerV1(c, int32(v), id, xdgOutputManagerX{s}) | |||
| 333 | + | }) | |||
| 334 | + | return err | |||
| 335 | + | } | |||
| 336 | + | ||||
| 337 | + | type xdgOutputManagerX struct{ server *Server } | |||
| 338 | + | ||||
| 339 | + | func (xdgOutputManagerX) Destroy(*xdgoutput.ZxdgOutputManagerV1) {} | |||
| 340 | + | func (m xdgOutputManagerX) GetXdgOutput(r *xdgoutput.ZxdgOutputManagerV1, id uint32, wl *wayland.Output) { | |||
| 341 | + | x, err := xdgoutput.NewZxdgOutputV1(r.Client(), r.Version(), id, xdgOutput{}) | |||
| 342 | + | if err != nil { | |||
| 343 | + | return | |||
| 344 | + | } | |||
| 345 | + | o := m.server.outputOf(wl) | |||
| 346 | + | if o == nil { | |||
| 347 | + | return | |||
| 348 | + | } | |||
| 349 | + | o.xdgOutputsX = append(o.xdgOutputsX, x) | |||
| 350 | + | x.OnDestroy = func() { o.xdgOutputsX = removeItem(o.xdgOutputsX, x) } | |||
| 351 | + | o.sendXDGX(m.server, x, true) | |||
| 352 | + | if r.Version() < 3 || wl.Version() < 2 { | |||
| 353 | + | x.SendDone() | |||
| 354 | + | } else { | |||
| 355 | + | wl.SendDone() | |||
| 356 | + | } | |||
| 286 | } | 357 | } | ||
| 287 | 358 | ||||
| 288 | type xdgOutputManager struct{ server *Server } | 359 | type xdgOutputManager struct{ server *Server } | ||
M internal/adapters/wayland/popup.go +2 -2
| @@ -31,7 +31,7 @@ func (s *Server) newPopup(x *xdgSurface, r *xdgshell.Surface, id uint32, parentX | |||
| 31 | 31 | var parent *window | |
| 32 | 32 | if parentXDG != nil { | |
| 33 | 33 | for _, w := range s.windows { | |
| 34 | - | if w.xdg.resource != nil && w.xdg.resource.Resource == parentXDG.Resource { | |
| 34 | + | if w.xdg != nil && w.xdg.resource != nil && w.xdg.resource.Resource == parentXDG.Resource { | |
| 35 | 35 | parent = w | |
| 36 | 36 | } | |
| 37 | 37 | } | |
| @@ -166,7 +166,7 @@ func (p *popup) configure(c ports.ConfigurePopup) { | |||
| 166 | 166 | func (s *Server) topGrab(client server.Client, parent *window) bool { | |
| 167 | 167 | var top *window | |
| 168 | 168 | for _, w := range s.windows { | |
| 169 | - | if w.popup != nil && w.popup.grab && !w.popup.done && w.xdg.resource.Client() == client && (top == nil || w.id > top.id) { | |
| 169 | + | if w.popup != nil && w.popup.grab && !w.popup.done && w.client() == client && (top == nil || w.id > top.id) { | |
| 170 | 170 | top = w | |
| 171 | 171 | } | |
| 172 | 172 | } | |
| @@ -31,7 +31,7 @@ func (s *Server) newPopup(x *xdgSurface, r *xdgshell.Surface, id uint32, parentX | |||||
| 31 | var parent *window | 31 | var parent *window | ||
| 32 | if parentXDG != nil { | 32 | if parentXDG != nil { | ||
| 33 | for _, w := range s.windows { | 33 | for _, w := range s.windows { | ||
| 34 | - | if w.xdg.resource != nil && w.xdg.resource.Resource == parentXDG.Resource { | 34 | + | if w.xdg != nil && w.xdg.resource != nil && w.xdg.resource.Resource == parentXDG.Resource { |
| 35 | parent = w | 35 | parent = w | ||
| 36 | } | 36 | } | ||
| 37 | } | 37 | } | ||
| @@ -166,7 +166,7 @@ func (p *popup) configure(c ports.ConfigurePopup) { | |||||
| 166 | func (s *Server) topGrab(client server.Client, parent *window) bool { | 166 | func (s *Server) topGrab(client server.Client, parent *window) bool { | ||
| 167 | var top *window | 167 | var top *window | ||
| 168 | for _, w := range s.windows { | 168 | for _, w := range s.windows { | ||
| 169 | - | if w.popup != nil && w.popup.grab && !w.popup.done && w.xdg.resource.Client() == client && (top == nil || w.id > top.id) { | 169 | + | if w.popup != nil && w.popup.grab && !w.popup.done && w.client() == client && (top == nil || w.id > top.id) { |
| 170 | top = w | 170 | top = w | ||
| 171 | } | 171 | } | ||
| 172 | } | 172 | } | ||
M internal/adapters/wayland/scale.go +5 -3
| @@ -21,12 +21,14 @@ func (s *Server) outputOfSurface(surf *surface) *output { | |||
| 21 | 21 | surf = surf.root() | |
| 22 | 22 | // Popups are on their parent's output. | |
| 23 | 23 | for surf.xdg != nil && surf.xdg.window != nil && surf.xdg.window.popup != nil && surf.xdg.window.popup.parent != nil { | |
| 24 | - | surf = surf.xdg.window.popup.parent.xdg.surface.root() | |
| 24 | + | surf = surf.xdg.window.popup.parent.surf().root() | |
| 25 | 25 | } | |
| 26 | 26 | name := "" | |
| 27 | 27 | switch { | |
| 28 | - | case surf.xdg != nil && surf.xdg.window != nil: | |
| 29 | - | name = surf.xdg.window.last.Output | |
| 28 | + | case surf.x11win != nil && surf.x11win.isOverride(): | |
| 29 | + | name = s.overrideOutput(surf.x11win) | |
| 30 | + | case surf.toplevelWindow() != nil: | |
| 31 | + | name = surf.toplevelWindow().last.Output | |
| 30 | 32 | case surf.layer != nil && surf.layer.output != nil: | |
| 31 | 33 | return surf.layer.output | |
| 32 | 34 | case surf.lock != nil: | |
| @@ -21,12 +21,14 @@ func (s *Server) outputOfSurface(surf *surface) *output { | |||||
| 21 | surf = surf.root() | 21 | surf = surf.root() | ||
| 22 | // Popups are on their parent's output. | 22 | // Popups are on their parent's output. | ||
| 23 | for surf.xdg != nil && surf.xdg.window != nil && surf.xdg.window.popup != nil && surf.xdg.window.popup.parent != nil { | 23 | for surf.xdg != nil && surf.xdg.window != nil && surf.xdg.window.popup != nil && surf.xdg.window.popup.parent != nil { | ||
| 24 | - | surf = surf.xdg.window.popup.parent.xdg.surface.root() | 24 | + | surf = surf.xdg.window.popup.parent.surf().root() |
| 25 | } | 25 | } | ||
| 26 | name := "" | 26 | name := "" | ||
| 27 | switch { | 27 | switch { | ||
| 28 | - | case surf.xdg != nil && surf.xdg.window != nil: | 28 | + | case surf.x11win != nil && surf.x11win.isOverride(): |
| 29 | - | name = surf.xdg.window.last.Output | 29 | + | name = s.overrideOutput(surf.x11win) |
| 30 | + | case surf.toplevelWindow() != nil: | |||
| 31 | + | name = surf.toplevelWindow().last.Output | |||
| 30 | case surf.layer != nil && surf.layer.output != nil: | 32 | case surf.layer != nil && surf.layer.output != nil: | ||
| 31 | return surf.layer.output | 33 | return surf.layer.output | ||
| 32 | case surf.lock != nil: | 34 | case surf.lock != nil: | ||
M internal/adapters/wayland/screencast.go +1 -1
| @@ -146,7 +146,7 @@ func (s *Server) openCast(cmd ports.CastCommand) { | |||
| 146 | 146 | gone := ports.CaptureReasonOutputGone | |
| 147 | 147 | if cmd.Window != 0 { | |
| 148 | 148 | gone = ports.CaptureReasonWindowGone | |
| 149 | - | if w := s.windows[cmd.Window]; w != nil && w.mapped && w.toplevel != nil { | |
| 149 | + | if w := s.windows[cmd.Window]; w != nil && w.mapped && w.isToplevel() { | |
| 150 | 150 | src = &captureSource{kind: srcWindow, window: cmd.Window, gen: w.gen} | |
| 151 | 151 | open.Window = cmd.Window | |
| 152 | 152 | } | |
| @@ -146,7 +146,7 @@ func (s *Server) openCast(cmd ports.CastCommand) { | |||||
| 146 | gone := ports.CaptureReasonOutputGone | 146 | gone := ports.CaptureReasonOutputGone | ||
| 147 | if cmd.Window != 0 { | 147 | if cmd.Window != 0 { | ||
| 148 | gone = ports.CaptureReasonWindowGone | 148 | gone = ports.CaptureReasonWindowGone | ||
| 149 | - | if w := s.windows[cmd.Window]; w != nil && w.mapped && w.toplevel != nil { | 149 | + | if w := s.windows[cmd.Window]; w != nil && w.mapped && w.isToplevel() { |
| 150 | src = &captureSource{kind: srcWindow, window: cmd.Window, gen: w.gen} | 150 | src = &captureSource{kind: srcWindow, window: cmd.Window, gen: w.gen} | ||
| 151 | open.Window = cmd.Window | 151 | open.Window = cmd.Window | ||
| 152 | } | 152 | } | ||
M internal/adapters/wayland/seat.go +11 -9
| @@ -276,10 +276,10 @@ func (s *Server) setModifiers(c ports.SetModifiers) { | |||
| 276 | 276 | } | |
| 277 | 277 | ||
| 278 | 278 | func (s *Server) windowKeyboards(w *window) []*wayland.Keyboard { | |
| 279 | - | if w == nil || !w.mapped || !w.xdg.resource.Resource.Alive() { | |
| 279 | + | if w == nil || !w.mapped || !w.alive() { | |
| 280 | 280 | return nil | |
| 281 | 281 | } | |
| 282 | - | return s.clientKeyboards(w.xdg.resource.Client()) | |
| 282 | + | return s.clientKeyboards(w.client()) | |
| 283 | 283 | } | |
| 284 | 284 | ||
| 285 | 285 | // clientKeyboards returns c's wl_keyboards without allocating. The list holds | |
| @@ -300,8 +300,8 @@ func (s *Server) focusTarget(id ports.WindowID) (*wayland.Surface, []*wayland.Ke | |||
| 300 | 300 | // already dead. Admission is enforced before entering or delivering. | |
| 301 | 301 | return l.surface.wl, s.clientKeyboards(l.surface.wl.Client()) | |
| 302 | 302 | } | |
| 303 | - | if w := s.windows[id]; w != nil && w.mapped { | |
| 304 | - | if surf := w.xdg.surfaceResource(); surf != nil { | |
| 303 | + | if w := s.windows[id]; w != nil && w.mapped && !w.isOverride() { | |
| 304 | + | if surf := w.surfaceResource(); surf != nil { | |
| 305 | 305 | return surf, s.windowKeyboards(w) | |
| 306 | 306 | } | |
| 307 | 307 | } | |
| @@ -319,8 +319,8 @@ func (s *Server) pointerTarget(id ports.WindowID) (server.Client, []*wayland.Poi | |||
| 319 | 319 | if l := s.lockSurfaces[id]; l != nil && s.lockInputTarget(id) { | |
| 320 | 320 | return l.resource.Client(), s.clientPointers(l.resource.Client()), true | |
| 321 | 321 | } | |
| 322 | - | if w := s.windows[id]; w != nil && w.mapped && w.xdg.resource.Resource.Alive() { | |
| 323 | - | return w.xdg.resource.Client(), s.windowPointers(w), true | |
| 322 | + | if w := s.windows[id]; w != nil && w.mapped && w.alive() { | |
| 323 | + | return w.client(), s.windowPointers(w), true | |
| 324 | 324 | } | |
| 325 | 325 | if l := s.layers[id]; l != nil && l.mapped && l.resource.Resource.Alive() { | |
| 326 | 326 | return l.resource.Client(), s.layerPointers(l), true | |
| @@ -335,7 +335,7 @@ func (s *Server) pointerSurface(id ports.WindowID, x, y float64) (*wayland.Surfa | |||
| 335 | 335 | return l.surface.wl, s.clientPointers(l.surface.wl.Client()), x, y | |
| 336 | 336 | } | |
| 337 | 337 | if w := s.windows[id]; w != nil { | |
| 338 | - | if surf := w.xdg.surfaceResource(); surf != nil && surf.Resource.Alive() { | |
| 338 | + | if surf := w.surfaceResource(); surf != nil && surf.Resource.Alive() { | |
| 339 | 339 | x, y := w.surfacePoint(x, y) | |
| 340 | 340 | return surf, s.windowPointers(w), x, y | |
| 341 | 341 | } | |
| @@ -455,6 +455,8 @@ func (s *Server) changeFocus(id ports.WindowID) { | |||
| 455 | 455 | } | |
| 456 | 456 | } | |
| 457 | 457 | s.textInputFocus() | |
| 458 | + | s.x11Focus() | |
| 459 | + | s.syncKeyboardGrab() | |
| 458 | 460 | s.log.Debug().Uint64("id", uint64(s.seat.focused)).Msg("keyboard focus") | |
| 459 | 461 | s.updateConstraint() | |
| 460 | 462 | } | |
| @@ -543,10 +545,10 @@ func pointerFrame(p *wayland.Pointer) { | |||
| 543 | 545 | } | |
| 544 | 546 | } | |
| 545 | 547 | func (s *Server) windowPointers(w *window) []*wayland.Pointer { | |
| 546 | - | if w == nil || !w.mapped || !w.xdg.resource.Resource.Alive() { | |
| 548 | + | if w == nil || !w.mapped || !w.alive() { | |
| 547 | 549 | return nil | |
| 548 | 550 | } | |
| 549 | - | return s.clientPointers(w.xdg.resource.Client()) | |
| 551 | + | return s.clientPointers(w.client()) | |
| 550 | 552 | } | |
| 551 | 553 | ||
| 552 | 554 | func (s *Server) layerPointers(l *layerSurface) []*wayland.Pointer { | |
| @@ -276,10 +276,10 @@ func (s *Server) setModifiers(c ports.SetModifiers) { | |||||
| 276 | } | 276 | } | ||
| 277 | 277 | ||||
| 278 | func (s *Server) windowKeyboards(w *window) []*wayland.Keyboard { | 278 | func (s *Server) windowKeyboards(w *window) []*wayland.Keyboard { | ||
| 279 | - | if w == nil || !w.mapped || !w.xdg.resource.Resource.Alive() { | 279 | + | if w == nil || !w.mapped || !w.alive() { |
| 280 | return nil | 280 | return nil | ||
| 281 | } | 281 | } | ||
| 282 | - | return s.clientKeyboards(w.xdg.resource.Client()) | 282 | + | return s.clientKeyboards(w.client()) |
| 283 | } | 283 | } | ||
| 284 | 284 | ||||
| 285 | // clientKeyboards returns c's wl_keyboards without allocating. The list holds | 285 | // clientKeyboards returns c's wl_keyboards without allocating. The list holds | ||
| @@ -300,8 +300,8 @@ func (s *Server) focusTarget(id ports.WindowID) (*wayland.Surface, []*wayland.Ke | |||||
| 300 | // already dead. Admission is enforced before entering or delivering. | 300 | // already dead. Admission is enforced before entering or delivering. | ||
| 301 | return l.surface.wl, s.clientKeyboards(l.surface.wl.Client()) | 301 | return l.surface.wl, s.clientKeyboards(l.surface.wl.Client()) | ||
| 302 | } | 302 | } | ||
| 303 | - | if w := s.windows[id]; w != nil && w.mapped { | 303 | + | if w := s.windows[id]; w != nil && w.mapped && !w.isOverride() { |
| 304 | - | if surf := w.xdg.surfaceResource(); surf != nil { | 304 | + | if surf := w.surfaceResource(); surf != nil { |
| 305 | return surf, s.windowKeyboards(w) | 305 | return surf, s.windowKeyboards(w) | ||
| 306 | } | 306 | } | ||
| 307 | } | 307 | } | ||
| @@ -319,8 +319,8 @@ func (s *Server) pointerTarget(id ports.WindowID) (server.Client, []*wayland.Poi | |||||
| 319 | if l := s.lockSurfaces[id]; l != nil && s.lockInputTarget(id) { | 319 | if l := s.lockSurfaces[id]; l != nil && s.lockInputTarget(id) { | ||
| 320 | return l.resource.Client(), s.clientPointers(l.resource.Client()), true | 320 | return l.resource.Client(), s.clientPointers(l.resource.Client()), true | ||
| 321 | } | 321 | } | ||
| 322 | - | if w := s.windows[id]; w != nil && w.mapped && w.xdg.resource.Resource.Alive() { | 322 | + | if w := s.windows[id]; w != nil && w.mapped && w.alive() { |
| 323 | - | return w.xdg.resource.Client(), s.windowPointers(w), true | 323 | + | return w.client(), s.windowPointers(w), true |
| 324 | } | 324 | } | ||
| 325 | if l := s.layers[id]; l != nil && l.mapped && l.resource.Resource.Alive() { | 325 | if l := s.layers[id]; l != nil && l.mapped && l.resource.Resource.Alive() { | ||
| 326 | return l.resource.Client(), s.layerPointers(l), true | 326 | return l.resource.Client(), s.layerPointers(l), true | ||
| @@ -335,7 +335,7 @@ func (s *Server) pointerSurface(id ports.WindowID, x, y float64) (*wayland.Surfa | |||||
| 335 | return l.surface.wl, s.clientPointers(l.surface.wl.Client()), x, y | 335 | return l.surface.wl, s.clientPointers(l.surface.wl.Client()), x, y | ||
| 336 | } | 336 | } | ||
| 337 | if w := s.windows[id]; w != nil { | 337 | if w := s.windows[id]; w != nil { | ||
| 338 | - | if surf := w.xdg.surfaceResource(); surf != nil && surf.Resource.Alive() { | 338 | + | if surf := w.surfaceResource(); surf != nil && surf.Resource.Alive() { |
| 339 | x, y := w.surfacePoint(x, y) | 339 | x, y := w.surfacePoint(x, y) | ||
| 340 | return surf, s.windowPointers(w), x, y | 340 | return surf, s.windowPointers(w), x, y | ||
| 341 | } | 341 | } | ||
| @@ -455,6 +455,8 @@ func (s *Server) changeFocus(id ports.WindowID) { | |||||
| 455 | } | 455 | } | ||
| 456 | } | 456 | } | ||
| 457 | s.textInputFocus() | 457 | s.textInputFocus() | ||
| 458 | + | s.x11Focus() | |||
| 459 | + | s.syncKeyboardGrab() | |||
| 458 | 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") | ||
| 459 | s.updateConstraint() | 461 | s.updateConstraint() | ||
| 460 | } | 462 | } | ||
| @@ -543,10 +545,10 @@ func pointerFrame(p *wayland.Pointer) { | |||||
| 543 | } | 545 | } | ||
| 544 | } | 546 | } | ||
| 545 | func (s *Server) windowPointers(w *window) []*wayland.Pointer { | 547 | func (s *Server) windowPointers(w *window) []*wayland.Pointer { | ||
| 546 | - | if w == nil || !w.mapped || !w.xdg.resource.Resource.Alive() { | 548 | + | if w == nil || !w.mapped || !w.alive() { |
| 547 | return nil | 549 | return nil | ||
| 548 | } | 550 | } | ||
| 549 | - | return s.clientPointers(w.xdg.resource.Client()) | 551 | + | return s.clientPointers(w.client()) |
| 550 | } | 552 | } | ||
| 551 | 553 | ||||
| 552 | func (s *Server) layerPointers(l *layerSurface) []*wayland.Pointer { | 554 | func (s *Server) layerPointers(l *layerSurface) []*wayland.Pointer { | ||
M internal/adapters/wayland/server.go +51 -16
| @@ -16,6 +16,7 @@ import ( | |||
| 16 | 16 | "github.com/bnema/neferwl/internal/adapters/captureallow" | |
| 17 | 17 | "github.com/bnema/neferwl/internal/adapters/clock" | |
| 18 | 18 | "github.com/bnema/neferwl/internal/adapters/wayland/sessionlock" | |
| 19 | + | "github.com/bnema/neferwl/internal/adapters/wayland/xwm" | |
| 19 | 20 | "github.com/bnema/neferwl/internal/adapters/workspaceid" | |
| 20 | 21 | "github.com/bnema/neferwl/internal/ports" | |
| 21 | 22 | "github.com/bnema/purego-libwayland/server" | |
| @@ -51,6 +52,9 @@ type Options struct { | |||
| 51 | 52 | // Casts receives the screencast events and inventory (screencast.go); | |
| 52 | 53 | // nil serves no casts (Channels.CastCommands is not read). | |
| 53 | 54 | Casts ports.CastSink | |
| 55 | + | // Xwayland is what the built-in XWM publishes to X11 clients | |
| 56 | + | // (XSETTINGS, root cursor); the zero value publishes nothing. | |
| 57 | + | Xwayland ports.XwaylandSettings | |
| 54 | 58 | // LogDamage logs the damage of every fresh client commit at debug level | |
| 55 | 59 | // (--debug=wayland-damage). | |
| 56 | 60 | LogDamage bool | |
| @@ -160,13 +164,21 @@ type Server struct { | |||
| 160 | 164 | securityStop int | |
| 161 | 165 | securityWG sync.WaitGroup | |
| 162 | 166 | securityLive atomic.Int32 | |
| 163 | - | display *server.Display | |
| 164 | - | env procEnv | |
| 165 | - | name string | |
| 166 | - | cleanup func() | |
| 167 | - | log zerowrap.Logger | |
| 168 | - | logDamage bool | |
| 169 | - | channels Channels | |
| 167 | + | xwayland xwaylandClient // display owner | |
| 168 | + | // xdgPublic and xdgPrivate are the two zxdg_output_manager_v1 globals: | |
| 169 | + | // layout space for clients, X space for Xwayland. | |
| 170 | + | xdgPublic, xdgPrivate *server.Global | |
| 171 | + | // xwm manages the X11 windows of the Xwayland client; x11Slots maps its | |
| 172 | + | // slots to their windows. Display goroutine only. | |
| 173 | + | xwm *xwm.XWM | |
| 174 | + | x11Slots map[xwm.Slot]*window | |
| 175 | + | display *server.Display | |
| 176 | + | env procEnv | |
| 177 | + | name string | |
| 178 | + | cleanup func() | |
| 179 | + | log zerowrap.Logger | |
| 180 | + | logDamage bool | |
| 181 | + | channels Channels | |
| 170 | 182 | // awaiting holds frame callbacks by output name, due at its next | |
| 171 | 183 | // frame (frameDue, or its page flip). | |
| 172 | 184 | awaiting map[string][]*wayland.Callback | |
| @@ -214,8 +226,11 @@ type Server struct { | |||
| 214 | 226 | // foreignExports are the live xdg-foreign handles (foreign.go). | |
| 215 | 227 | foreignExports map[string]*foreignExport | |
| 216 | 228 | surfaces map[*server.Resource]*surface | |
| 217 | - | buffers map[*server.Resource]clientBuffer | |
| 218 | - | dmabuf *dmabufGlobal | |
| 229 | + | // surfaceByIdentity finds a surface by its identity (xwayland_shell_v1 | |
| 230 | + | // pairing); kept with surfaces. | |
| 231 | + | surfaceByIdentity map[uint64]*surface | |
| 232 | + | buffers map[*server.Resource]clientBuffer | |
| 233 | + | dmabuf *dmabufGlobal | |
| 219 | 234 | // hdrOutputs records confirmed DRM modesets; absent outputs are SDR. | |
| 220 | 235 | hdrOutputs map[string]ports.OutputHDR | |
| 221 | 236 | colorID uint64 | |
| @@ -262,6 +277,8 @@ type Server struct { | |||
| 262 | 277 | // inhibitors are the live inhibitor objects (inhibit.go); | |
| 263 | 278 | // shortcutWindows and idleWindows what core was told. | |
| 264 | 279 | inhibitors []*inhibitor | |
| 280 | + | xgrab *xwaylandGrab // Xwayland keyboard grab (xwaylandgrab.go) | |
| 281 | + | xgrabbed ports.WindowID // window the grab inhibits for | |
| 265 | 282 | shortcutWindows, idleWindows map[ports.WindowID]bool | |
| 266 | 283 | // busInhibited is the last IdleInhibited value (idle.go). | |
| 267 | 284 | busInhibited bool | |
| @@ -281,6 +298,8 @@ type Server struct { | |||
| 281 | 298 | workspaceIDs *workspaceid.IDs | |
| 282 | 299 | outputHeads ports.OutputHeads | |
| 283 | 300 | outputPlaces ports.Layout | |
| 301 | + | xsettings ports.XwaylandSettings | |
| 302 | + | xspace xwm.Space // the one layout <-> X mapping, shared with the XWM | |
| 284 | 303 | managementSerial uint32 | |
| 285 | 304 | nextOutputApply uint64 | |
| 286 | 305 | outputReplies map[uint64]*outputConfiguration | |
| @@ -298,6 +317,8 @@ type Server struct { | |||
| 298 | 317 | cursorReady chan struct{} | |
| 299 | 318 | // Clipboard and primary selection (clipboard.go). | |
| 300 | 319 | selections [2]*clipSource | |
| 320 | + | xsel [2]*clipSource // the current X-owned source per kind, if any | |
| 321 | + | xwmSel xwmSelection // the XWM's clipboard side; nil without one | |
| 301 | 322 | // Drag and drop (dnd.go): the drag in progress, drops waiting for | |
| 302 | 323 | // their finish, and each client's data devices. | |
| 303 | 324 | dnd *dndState | |
| @@ -364,10 +385,13 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) { | |||
| 364 | 385 | cleanup: cleanup, | |
| 365 | 386 | log: log, | |
| 366 | 387 | logDamage: opts.LogDamage, | |
| 388 | + | xsettings: opts.Xwayland, | |
| 367 | 389 | channels: ch, | |
| 368 | 390 | surfaces: make(map[*server.Resource]*surface), | |
| 391 | + | surfaceByIdentity: make(map[uint64]*surface), | |
| 369 | 392 | buffers: make(map[*server.Resource]clientBuffer), | |
| 370 | 393 | windows: make(map[ports.WindowID]*window), | |
| 394 | + | x11Slots: make(map[xwm.Slot]*window), | |
| 371 | 395 | layers: make(map[ports.WindowID]*layerSurface), | |
| 372 | 396 | nextWindow: 1, | |
| 373 | 397 | eventReady: make(chan struct{}, 1), | |
| @@ -498,6 +522,7 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) { | |||
| 498 | 522 | if err = s.openPaceTimer(); err != nil { | |
| 499 | 523 | return abort(err) | |
| 500 | 524 | } | |
| 525 | + | d.SetGlobalFilter(s.globalVisible) | |
| 501 | 526 | if err = registerGlobals(d, opts, s); err != nil { | |
| 502 | 527 | return abort(err) | |
| 503 | 528 | } | |
| @@ -585,6 +610,13 @@ func (s *Server) Run(ctx context.Context) error { | |||
| 585 | 610 | } | |
| 586 | 611 | s.castInventoryChanged() | |
| 587 | 612 | err := s.display.Run(ctx) | |
| 613 | + | // The display is gone: only the X connection is left to close. | |
| 614 | + | if s.xwm != nil { | |
| 615 | + | s.xwm.CloseConn() | |
| 616 | + | s.xwm = nil | |
| 617 | + | s.xwmSel = nil | |
| 618 | + | } | |
| 619 | + | s.forgetXwayland() | |
| 588 | 620 | s.stopSecurityContexts() | |
| 589 | 621 | wg.Wait() | |
| 590 | 622 | return err | |
| @@ -774,9 +806,11 @@ func (s *Server) apply(cmd ports.ClientCommand) { | |||
| 774 | 806 | if w := s.windows[c.ID]; w != nil && w.popup != nil { | |
| 775 | 807 | w.popup.dismiss() | |
| 776 | 808 | } | |
| 809 | + | case ports.PlaceWindow: | |
| 810 | + | s.x11Place(c) | |
| 777 | 811 | case ports.ConfigureWindow: | |
| 778 | 812 | w := s.windows[c.ID] | |
| 779 | - | if w == nil || w.toplevel == nil || !w.toplevel.Resource.Alive() || !w.xdg.resource.Resource.Alive() { | |
| 813 | + | if w == nil || !w.toplevelAlive() || !w.alive() { | |
| 780 | 814 | s.log.Debug().Uint64("id", uint64(c.ID)).Msg("configure missing window") | |
| 781 | 815 | return | |
| 782 | 816 | } | |
| @@ -787,16 +821,17 @@ func (s *Server) apply(cmd ports.ClientCommand) { | |||
| 787 | 821 | scanoutChanged := !w.hasLast || w.last.Fullscreen != c.Fullscreen || w.last.Output != c.Output || w.last.Visible != c.Visible | |
| 788 | 822 | w.last, w.hasLast = c, true | |
| 789 | 823 | if visibilityChanged { | |
| 790 | - | s.relocateCallbacks(w.xdg.surface) | |
| 824 | + | s.relocateCallbacks(w.surf()) | |
| 791 | 825 | s.dropFeedbacks(time.Now()) | |
| 792 | 826 | s.wakePacer() // reconsider FIFO barriers at the new period | |
| 793 | 827 | } | |
| 794 | 828 | w.sendConfigure() | |
| 829 | + | s.syncKeyboardGrab() | |
| 795 | 830 | s.toplevelChanged(w) | |
| 796 | - | if scanoutChanged && w.xdg.surface != nil { | |
| 797 | - | s.dmabuf.resendSurface(w.xdg.surface) | |
| 831 | + | if scanoutChanged && w.surf() != nil { | |
| 832 | + | s.dmabuf.resendSurface(w.surf()) | |
| 798 | 833 | } | |
| 799 | - | if surf := w.xdg.surface; surf != nil && c.Output != "" { | |
| 834 | + | if surf := w.surf(); surf != nil && c.Output != "" { | |
| 800 | 835 | surf.sendTreeScale() | |
| 801 | 836 | } | |
| 802 | 837 | if scanoutChanged { | |
| @@ -817,10 +852,10 @@ func (s *Server) apply(cmd ports.ClientCommand) { | |||
| 817 | 852 | s.setShortcutsInhibit(c) | |
| 818 | 853 | case ports.CloseWindow: | |
| 819 | 854 | w := s.windows[c.ID] | |
| 820 | - | if w == nil || w.toplevel == nil || !w.toplevel.Resource.Alive() { | |
| 855 | + | if w == nil || !w.toplevelAlive() { | |
| 821 | 856 | s.log.Debug().Uint64("id", uint64(c.ID)).Msg("close missing window") | |
| 822 | 857 | return | |
| 823 | 858 | } | |
| 824 | - | w.toplevel.SendClose() | |
| 859 | + | w.sendClose() | |
| 825 | 860 | } | |
| 826 | 861 | } | |
| @@ -16,6 +16,7 @@ import ( | |||||
| 16 | "github.com/bnema/neferwl/internal/adapters/captureallow" | 16 | "github.com/bnema/neferwl/internal/adapters/captureallow" | ||
| 17 | "github.com/bnema/neferwl/internal/adapters/clock" | 17 | "github.com/bnema/neferwl/internal/adapters/clock" | ||
| 18 | "github.com/bnema/neferwl/internal/adapters/wayland/sessionlock" | 18 | "github.com/bnema/neferwl/internal/adapters/wayland/sessionlock" | ||
| 19 | + | "github.com/bnema/neferwl/internal/adapters/wayland/xwm" | |||
| 19 | "github.com/bnema/neferwl/internal/adapters/workspaceid" | 20 | "github.com/bnema/neferwl/internal/adapters/workspaceid" | ||
| 20 | "github.com/bnema/neferwl/internal/ports" | 21 | "github.com/bnema/neferwl/internal/ports" | ||
| 21 | "github.com/bnema/purego-libwayland/server" | 22 | "github.com/bnema/purego-libwayland/server" | ||
| @@ -51,6 +52,9 @@ type Options struct { | |||||
| 51 | // Casts receives the screencast events and inventory (screencast.go); | 52 | // Casts receives the screencast events and inventory (screencast.go); | ||
| 52 | // nil serves no casts (Channels.CastCommands is not read). | 53 | // nil serves no casts (Channels.CastCommands is not read). | ||
| 53 | Casts ports.CastSink | 54 | Casts ports.CastSink | ||
| 55 | + | // Xwayland is what the built-in XWM publishes to X11 clients | |||
| 56 | + | // (XSETTINGS, root cursor); the zero value publishes nothing. | |||
| 57 | + | Xwayland ports.XwaylandSettings | |||
| 54 | // LogDamage logs the damage of every fresh client commit at debug level | 58 | // LogDamage logs the damage of every fresh client commit at debug level | ||
| 55 | // (--debug=wayland-damage). | 59 | // (--debug=wayland-damage). | ||
| 56 | LogDamage bool | 60 | LogDamage bool | ||
| @@ -160,13 +164,21 @@ type Server struct { | |||||
| 160 | securityStop int | 164 | securityStop int | ||
| 161 | securityWG sync.WaitGroup | 165 | securityWG sync.WaitGroup | ||
| 162 | securityLive atomic.Int32 | 166 | securityLive atomic.Int32 | ||
| 163 | - | display *server.Display | 167 | + | xwayland xwaylandClient // display owner |
| 164 | - | env procEnv | 168 | + | // xdgPublic and xdgPrivate are the two zxdg_output_manager_v1 globals: |
| 165 | - | name string | 169 | + | // layout space for clients, X space for Xwayland. |
| 166 | - | cleanup func() | 170 | + | xdgPublic, xdgPrivate *server.Global |
| 167 | - | log zerowrap.Logger | 171 | + | // xwm manages the X11 windows of the Xwayland client; x11Slots maps its |
| 168 | - | logDamage bool | 172 | + | // slots to their windows. Display goroutine only. |
| 169 | - | channels Channels | 173 | + | xwm *xwm.XWM |
| 174 | + | x11Slots map[xwm.Slot]*window | |||
| 175 | + | display *server.Display | |||
| 176 | + | env procEnv | |||
| 177 | + | name string | |||
| 178 | + | cleanup func() | |||
| 179 | + | log zerowrap.Logger | |||
| 180 | + | logDamage bool | |||
| 181 | + | channels Channels | |||
| 170 | // awaiting holds frame callbacks by output name, due at its next | 182 | // awaiting holds frame callbacks by output name, due at its next | ||
| 171 | // frame (frameDue, or its page flip). | 183 | // frame (frameDue, or its page flip). | ||
| 172 | awaiting map[string][]*wayland.Callback | 184 | awaiting map[string][]*wayland.Callback | ||
| @@ -214,8 +226,11 @@ type Server struct { | |||||
| 214 | // foreignExports are the live xdg-foreign handles (foreign.go). | 226 | // foreignExports are the live xdg-foreign handles (foreign.go). | ||
| 215 | foreignExports map[string]*foreignExport | 227 | foreignExports map[string]*foreignExport | ||
| 216 | surfaces map[*server.Resource]*surface | 228 | surfaces map[*server.Resource]*surface | ||
| 217 | - | buffers map[*server.Resource]clientBuffer | 229 | + | // surfaceByIdentity finds a surface by its identity (xwayland_shell_v1 |
| 218 | - | dmabuf *dmabufGlobal | 230 | + | // pairing); kept with surfaces. |
| 231 | + | surfaceByIdentity map[uint64]*surface | |||
| 232 | + | buffers map[*server.Resource]clientBuffer | |||
| 233 | + | dmabuf *dmabufGlobal | |||
| 219 | // hdrOutputs records confirmed DRM modesets; absent outputs are SDR. | 234 | // hdrOutputs records confirmed DRM modesets; absent outputs are SDR. | ||
| 220 | hdrOutputs map[string]ports.OutputHDR | 235 | hdrOutputs map[string]ports.OutputHDR | ||
| 221 | colorID uint64 | 236 | colorID uint64 | ||
| @@ -262,6 +277,8 @@ type Server struct { | |||||
| 262 | // inhibitors are the live inhibitor objects (inhibit.go); | 277 | // inhibitors are the live inhibitor objects (inhibit.go); | ||
| 263 | // shortcutWindows and idleWindows what core was told. | 278 | // shortcutWindows and idleWindows what core was told. | ||
| 264 | inhibitors []*inhibitor | 279 | inhibitors []*inhibitor | ||
| 280 | + | xgrab *xwaylandGrab // Xwayland keyboard grab (xwaylandgrab.go) | |||
| 281 | + | xgrabbed ports.WindowID // window the grab inhibits for | |||
| 265 | shortcutWindows, idleWindows map[ports.WindowID]bool | 282 | shortcutWindows, idleWindows map[ports.WindowID]bool | ||
| 266 | // busInhibited is the last IdleInhibited value (idle.go). | 283 | // busInhibited is the last IdleInhibited value (idle.go). | ||
| 267 | busInhibited bool | 284 | busInhibited bool | ||
| @@ -281,6 +298,8 @@ type Server struct { | |||||
| 281 | workspaceIDs *workspaceid.IDs | 298 | workspaceIDs *workspaceid.IDs | ||
| 282 | outputHeads ports.OutputHeads | 299 | outputHeads ports.OutputHeads | ||
| 283 | outputPlaces ports.Layout | 300 | outputPlaces ports.Layout | ||
| 301 | + | xsettings ports.XwaylandSettings | |||
| 302 | + | xspace xwm.Space // the one layout <-> X mapping, shared with the XWM | |||
| 284 | managementSerial uint32 | 303 | managementSerial uint32 | ||
| 285 | nextOutputApply uint64 | 304 | nextOutputApply uint64 | ||
| 286 | outputReplies map[uint64]*outputConfiguration | 305 | outputReplies map[uint64]*outputConfiguration | ||
| @@ -298,6 +317,8 @@ type Server struct { | |||||
| 298 | cursorReady chan struct{} | 317 | cursorReady chan struct{} | ||
| 299 | // Clipboard and primary selection (clipboard.go). | 318 | // Clipboard and primary selection (clipboard.go). | ||
| 300 | selections [2]*clipSource | 319 | selections [2]*clipSource | ||
| 320 | + | xsel [2]*clipSource // the current X-owned source per kind, if any | |||
| 321 | + | xwmSel xwmSelection // the XWM's clipboard side; nil without one | |||
| 301 | // Drag and drop (dnd.go): the drag in progress, drops waiting for | 322 | // Drag and drop (dnd.go): the drag in progress, drops waiting for | ||
| 302 | // their finish, and each client's data devices. | 323 | // their finish, and each client's data devices. | ||
| 303 | dnd *dndState | 324 | dnd *dndState | ||
| @@ -364,10 +385,13 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) { | |||||
| 364 | cleanup: cleanup, | 385 | cleanup: cleanup, | ||
| 365 | log: log, | 386 | log: log, | ||
| 366 | logDamage: opts.LogDamage, | 387 | logDamage: opts.LogDamage, | ||
| 388 | + | xsettings: opts.Xwayland, | |||
| 367 | channels: ch, | 389 | channels: ch, | ||
| 368 | surfaces: make(map[*server.Resource]*surface), | 390 | surfaces: make(map[*server.Resource]*surface), | ||
| 391 | + | surfaceByIdentity: make(map[uint64]*surface), | |||
| 369 | buffers: make(map[*server.Resource]clientBuffer), | 392 | buffers: make(map[*server.Resource]clientBuffer), | ||
| 370 | windows: make(map[ports.WindowID]*window), | 393 | windows: make(map[ports.WindowID]*window), | ||
| 394 | + | x11Slots: make(map[xwm.Slot]*window), | |||
| 371 | layers: make(map[ports.WindowID]*layerSurface), | 395 | layers: make(map[ports.WindowID]*layerSurface), | ||
| 372 | nextWindow: 1, | 396 | nextWindow: 1, | ||
| 373 | eventReady: make(chan struct{}, 1), | 397 | eventReady: make(chan struct{}, 1), | ||
| @@ -498,6 +522,7 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) { | |||||
| 498 | if err = s.openPaceTimer(); err != nil { | 522 | if err = s.openPaceTimer(); err != nil { | ||
| 499 | return abort(err) | 523 | return abort(err) | ||
| 500 | } | 524 | } | ||
| 525 | + | d.SetGlobalFilter(s.globalVisible) | |||
| 501 | if err = registerGlobals(d, opts, s); err != nil { | 526 | if err = registerGlobals(d, opts, s); err != nil { | ||
| 502 | return abort(err) | 527 | return abort(err) | ||
| 503 | } | 528 | } | ||
| @@ -585,6 +610,13 @@ func (s *Server) Run(ctx context.Context) error { | |||||
| 585 | } | 610 | } | ||
| 586 | s.castInventoryChanged() | 611 | s.castInventoryChanged() | ||
| 587 | err := s.display.Run(ctx) | 612 | err := s.display.Run(ctx) | ||
| 613 | + | // The display is gone: only the X connection is left to close. | |||
| 614 | + | if s.xwm != nil { | |||
| 615 | + | s.xwm.CloseConn() | |||
| 616 | + | s.xwm = nil | |||
| 617 | + | s.xwmSel = nil | |||
| 618 | + | } | |||
| 619 | + | s.forgetXwayland() | |||
| 588 | s.stopSecurityContexts() | 620 | s.stopSecurityContexts() | ||
| 589 | wg.Wait() | 621 | wg.Wait() | ||
| 590 | return err | 622 | return err | ||
| @@ -774,9 +806,11 @@ func (s *Server) apply(cmd ports.ClientCommand) { | |||||
| 774 | if w := s.windows[c.ID]; w != nil && w.popup != nil { | 806 | if w := s.windows[c.ID]; w != nil && w.popup != nil { | ||
| 775 | w.popup.dismiss() | 807 | w.popup.dismiss() | ||
| 776 | } | 808 | } | ||
| 809 | + | case ports.PlaceWindow: | |||
| 810 | + | s.x11Place(c) | |||
| 777 | case ports.ConfigureWindow: | 811 | case ports.ConfigureWindow: | ||
| 778 | w := s.windows[c.ID] | 812 | w := s.windows[c.ID] | ||
| 779 | - | if w == nil || w.toplevel == nil || !w.toplevel.Resource.Alive() || !w.xdg.resource.Resource.Alive() { | 813 | + | if w == nil || !w.toplevelAlive() || !w.alive() { |
| 780 | s.log.Debug().Uint64("id", uint64(c.ID)).Msg("configure missing window") | 814 | s.log.Debug().Uint64("id", uint64(c.ID)).Msg("configure missing window") | ||
| 781 | return | 815 | return | ||
| 782 | } | 816 | } | ||
| @@ -787,16 +821,17 @@ func (s *Server) apply(cmd ports.ClientCommand) { | |||||
| 787 | scanoutChanged := !w.hasLast || w.last.Fullscreen != c.Fullscreen || w.last.Output != c.Output || w.last.Visible != c.Visible | 821 | scanoutChanged := !w.hasLast || w.last.Fullscreen != c.Fullscreen || w.last.Output != c.Output || w.last.Visible != c.Visible | ||
| 788 | w.last, w.hasLast = c, true | 822 | w.last, w.hasLast = c, true | ||
| 789 | if visibilityChanged { | 823 | if visibilityChanged { | ||
| 790 | - | s.relocateCallbacks(w.xdg.surface) | 824 | + | s.relocateCallbacks(w.surf()) |
| 791 | s.dropFeedbacks(time.Now()) | 825 | s.dropFeedbacks(time.Now()) | ||
| 792 | s.wakePacer() // reconsider FIFO barriers at the new period | 826 | s.wakePacer() // reconsider FIFO barriers at the new period | ||
| 793 | } | 827 | } | ||
| 794 | w.sendConfigure() | 828 | w.sendConfigure() | ||
| 829 | + | s.syncKeyboardGrab() | |||
| 795 | s.toplevelChanged(w) | 830 | s.toplevelChanged(w) | ||
| 796 | - | if scanoutChanged && w.xdg.surface != nil { | 831 | + | if scanoutChanged && w.surf() != nil { |
| 797 | - | s.dmabuf.resendSurface(w.xdg.surface) | 832 | + | s.dmabuf.resendSurface(w.surf()) |
| 798 | } | 833 | } | ||
| 799 | - | if surf := w.xdg.surface; surf != nil && c.Output != "" { | 834 | + | if surf := w.surf(); surf != nil && c.Output != "" { |
| 800 | surf.sendTreeScale() | 835 | surf.sendTreeScale() | ||
| 801 | } | 836 | } | ||
| 802 | if scanoutChanged { | 837 | if scanoutChanged { | ||
| @@ -817,10 +852,10 @@ func (s *Server) apply(cmd ports.ClientCommand) { | |||||
| 817 | s.setShortcutsInhibit(c) | 852 | s.setShortcutsInhibit(c) | ||
| 818 | case ports.CloseWindow: | 853 | case ports.CloseWindow: | ||
| 819 | w := s.windows[c.ID] | 854 | w := s.windows[c.ID] | ||
| 820 | - | if w == nil || w.toplevel == nil || !w.toplevel.Resource.Alive() { | 855 | + | if w == nil || !w.toplevelAlive() { |
| 821 | s.log.Debug().Uint64("id", uint64(c.ID)).Msg("close missing window") | 856 | s.log.Debug().Uint64("id", uint64(c.ID)).Msg("close missing window") | ||
| 822 | return | 857 | return | ||
| 823 | } | 858 | } | ||
| 824 | - | w.toplevel.SendClose() | 859 | + | w.sendClose() |
| 825 | } | 860 | } | ||
| 826 | } | 861 | } | ||
M internal/adapters/wayland/surface.go +15 -2
| @@ -28,6 +28,9 @@ const ( | |||
| 28 | 28 | roleSessionLock | |
| 29 | 29 | // roleDragIcon is the icon of a data drag (dnd.go). | |
| 30 | 30 | roleDragIcon | |
| 31 | + | // roleX11 is the surface of an X11 window paired through | |
| 32 | + | // xwayland_shell_v1. | |
| 33 | + | roleX11 | |
| 31 | 34 | ) | |
| 32 | 35 | ||
| 33 | 36 | type surface struct { | |
| @@ -47,8 +50,12 @@ type surface struct { | |||
| 47 | 50 | formatOpaque bool | |
| 48 | 51 | kind roleKind | |
| 49 | 52 | xdg *xdgSurface | |
| 50 | - | layer *layerSurface | |
| 51 | - | lock *lockSurface | |
| 53 | + | // x11win is the X11 window the surface is associated with; x11Serial is | |
| 54 | + | // the serial of xwayland_surface_v1.set_serial, 0 until set. | |
| 55 | + | x11win *window | |
| 56 | + | x11Serial uint64 | |
| 57 | + | layer *layerSurface | |
| 58 | + | lock *lockSurface | |
| 52 | 59 | // bufferCommitted is permanent, including commits later discarded by FIFO. | |
| 53 | 60 | bufferCommitted bool | |
| 54 | 61 | server *Server | |
| @@ -192,6 +199,9 @@ func (s *surface) windowID() ports.WindowID { | |||
| 192 | 199 | if s.xdg != nil && s.xdg.window != nil && s.xdg.window.mapped { | |
| 193 | 200 | return s.xdg.window.id | |
| 194 | 201 | } | |
| 202 | + | if s.x11win != nil && s.x11win.mapped { | |
| 203 | + | return s.x11win.id | |
| 204 | + | } | |
| 195 | 205 | if s.layer != nil && s.layer.mapped { | |
| 196 | 206 | return s.layer.id | |
| 197 | 207 | } | |
| @@ -566,6 +576,9 @@ func (s *surface) applyCommit(u *update) { | |||
| 566 | 576 | if s.xdg != nil && s.xdg.window != nil { | |
| 567 | 577 | s.xdg.window.afterCommit() | |
| 568 | 578 | } | |
| 579 | + | if s.x11win != nil { | |
| 580 | + | s.x11win.afterCommit() | |
| 581 | + | } | |
| 569 | 582 | if s.lock != nil { | |
| 570 | 583 | // Role callbacks precede content construction; publish lock placement | |
| 571 | 584 | // only once validated current dimensions are available. | |
| @@ -28,6 +28,9 @@ const ( | |||||
| 28 | roleSessionLock | 28 | roleSessionLock | ||
| 29 | // roleDragIcon is the icon of a data drag (dnd.go). | 29 | // roleDragIcon is the icon of a data drag (dnd.go). | ||
| 30 | roleDragIcon | 30 | roleDragIcon | ||
| 31 | + | // roleX11 is the surface of an X11 window paired through | |||
| 32 | + | // xwayland_shell_v1. | |||
| 33 | + | roleX11 | |||
| 31 | ) | 34 | ) | ||
| 32 | 35 | ||||
| 33 | type surface struct { | 36 | type surface struct { | ||
| @@ -47,8 +50,12 @@ type surface struct { | |||||
| 47 | formatOpaque bool | 50 | formatOpaque bool | ||
| 48 | kind roleKind | 51 | kind roleKind | ||
| 49 | xdg *xdgSurface | 52 | xdg *xdgSurface | ||
| 50 | - | layer *layerSurface | 53 | + | // x11win is the X11 window the surface is associated with; x11Serial is |
| 51 | - | lock *lockSurface | 54 | + | // the serial of xwayland_surface_v1.set_serial, 0 until set. |
| 55 | + | x11win *window | |||
| 56 | + | x11Serial uint64 | |||
| 57 | + | layer *layerSurface | |||
| 58 | + | lock *lockSurface | |||
| 52 | // bufferCommitted is permanent, including commits later discarded by FIFO. | 59 | // bufferCommitted is permanent, including commits later discarded by FIFO. | ||
| 53 | bufferCommitted bool | 60 | bufferCommitted bool | ||
| 54 | server *Server | 61 | server *Server | ||
| @@ -192,6 +199,9 @@ func (s *surface) windowID() ports.WindowID { | |||||
| 192 | if s.xdg != nil && s.xdg.window != nil && s.xdg.window.mapped { | 199 | if s.xdg != nil && s.xdg.window != nil && s.xdg.window.mapped { | ||
| 193 | return s.xdg.window.id | 200 | return s.xdg.window.id | ||
| 194 | } | 201 | } | ||
| 202 | + | if s.x11win != nil && s.x11win.mapped { | |||
| 203 | + | return s.x11win.id | |||
| 204 | + | } | |||
| 195 | if s.layer != nil && s.layer.mapped { | 205 | if s.layer != nil && s.layer.mapped { | ||
| 196 | return s.layer.id | 206 | return s.layer.id | ||
| 197 | } | 207 | } | ||
| @@ -566,6 +576,9 @@ func (s *surface) applyCommit(u *update) { | |||||
| 566 | if s.xdg != nil && s.xdg.window != nil { | 576 | if s.xdg != nil && s.xdg.window != nil { | ||
| 567 | s.xdg.window.afterCommit() | 577 | s.xdg.window.afterCommit() | ||
| 568 | } | 578 | } | ||
| 579 | + | if s.x11win != nil { | |||
| 580 | + | s.x11win.afterCommit() | |||
| 581 | + | } | |||
| 569 | if s.lock != nil { | 582 | if s.lock != nil { | ||
| 570 | // Role callbacks precede content construction; publish lock placement | 583 | // Role callbacks precede content construction; publish lock placement | ||
| 571 | // only once validated current dimensions are available. | 584 | // only once validated current dimensions are available. | ||
A internal/adapters/wayland/window.go +112 -0
| @@ -0,0 +1,112 @@ | |||
| 1 | + | package wayland | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "github.com/bnema/go-wayland-bindings/server/wayland" | |
| 5 | + | "github.com/bnema/neferwl/internal/ports" | |
| 6 | + | "github.com/bnema/purego-libwayland/server" | |
| 7 | + | ) | |
| 8 | + | ||
| 9 | + | // Role-independent accessors of a window. Code outside xdg.go goes through | |
| 10 | + | // them instead of dereferencing w.xdg, which is nil for windows of another | |
| 11 | + | // role. | |
| 12 | + | ||
| 13 | + | // isToplevel reports whether w is a toplevel (xdg or X11), not a popup. | |
| 14 | + | func (w *window) isToplevel() bool { return w.toplevel != nil || (w.x11 != nil && !w.x11.override) } | |
| 15 | + | ||
| 16 | + | // srv is the server the window belongs to. | |
| 17 | + | func (w *window) srv() *Server { | |
| 18 | + | if w.xdg != nil { | |
| 19 | + | return w.xdg.server | |
| 20 | + | } | |
| 21 | + | return w.x11.server | |
| 22 | + | } | |
| 23 | + | ||
| 24 | + | // surf is the window's surface, nil when it has none. | |
| 25 | + | func (w *window) surf() *surface { | |
| 26 | + | switch { | |
| 27 | + | case w.xdg != nil: | |
| 28 | + | return w.xdg.surface | |
| 29 | + | case w.x11 != nil: | |
| 30 | + | return w.x11.surface | |
| 31 | + | } | |
| 32 | + | return nil | |
| 33 | + | } | |
| 34 | + | ||
| 35 | + | // alive reports whether the window's role object is alive. | |
| 36 | + | func (w *window) alive() bool { | |
| 37 | + | switch { | |
| 38 | + | case w.xdg != nil: | |
| 39 | + | return w.xdg.resource.Resource.Alive() | |
| 40 | + | case w.x11 != nil: | |
| 41 | + | return w.x11.surface != nil && w.x11.surface.wl.Resource.Alive() | |
| 42 | + | } | |
| 43 | + | return false | |
| 44 | + | } | |
| 45 | + | ||
| 46 | + | // client is the window's client; the zero Client when it has no role object. | |
| 47 | + | func (w *window) client() server.Client { | |
| 48 | + | switch { | |
| 49 | + | case w.xdg != nil: | |
| 50 | + | return w.xdg.resource.Client() | |
| 51 | + | case w.x11 != nil && w.x11.surface != nil: | |
| 52 | + | return w.x11.surface.wl.Client() | |
| 53 | + | } | |
| 54 | + | return server.Client{} | |
| 55 | + | } | |
| 56 | + | ||
| 57 | + | // geometry is the committed window geometry; empty means the whole surface. | |
| 58 | + | func (w *window) geometry() ports.Rect { | |
| 59 | + | if w.xdg != nil { | |
| 60 | + | return w.xdg.geometry | |
| 61 | + | } | |
| 62 | + | return ports.Rect{} | |
| 63 | + | } | |
| 64 | + | ||
| 65 | + | // sendConfigure sends w.last to the client. | |
| 66 | + | func (w *window) sendConfigure() { | |
| 67 | + | switch { | |
| 68 | + | case w.xdg != nil: | |
| 69 | + | w.sendXDGConfigure() | |
| 70 | + | case w.x11 != nil: | |
| 71 | + | w.x11.server.x11Configure(w) | |
| 72 | + | } | |
| 73 | + | } | |
| 74 | + | ||
| 75 | + | // sendClose asks the client to close the window. | |
| 76 | + | func (w *window) sendClose() { | |
| 77 | + | switch { | |
| 78 | + | case w.toplevel != nil: | |
| 79 | + | w.toplevel.SendClose() | |
| 80 | + | case w.x11 != nil: | |
| 81 | + | w.x11.server.x11Close(w) | |
| 82 | + | } | |
| 83 | + | } | |
| 84 | + | ||
| 85 | + | // toplevelAlive reports whether the window is a toplevel whose protocol | |
| 86 | + | // object is alive. | |
| 87 | + | func (w *window) toplevelAlive() bool { | |
| 88 | + | if w.x11 != nil { | |
| 89 | + | return w.alive() | |
| 90 | + | } | |
| 91 | + | return w.toplevel != nil && w.toplevel.Resource.Alive() | |
| 92 | + | } | |
| 93 | + | ||
| 94 | + | // surfaceResource is the window's wl_surface, nil when it has none or it died. | |
| 95 | + | func (w *window) surfaceResource() *wayland.Surface { | |
| 96 | + | switch { | |
| 97 | + | case w.xdg != nil: | |
| 98 | + | return w.xdg.surfaceResource() | |
| 99 | + | case w.x11 != nil && w.alive(): | |
| 100 | + | return w.x11.surface.wl | |
| 101 | + | } | |
| 102 | + | return nil | |
| 103 | + | } | |
| 104 | + | ||
| 105 | + | // toplevelWindow is the toplevel window (xdg or X11) the surface is the role | |
| 106 | + | // surface of, nil for other roles and popups' parents are not followed. | |
| 107 | + | func (s *surface) toplevelWindow() *window { | |
| 108 | + | if s.xdg != nil && s.xdg.window != nil { | |
| 109 | + | return s.xdg.window | |
| 110 | + | } | |
| 111 | + | return s.x11win | |
| 112 | + | } | |
| @@ -0,0 +1,112 @@ | |||||
| 1 | + | package wayland | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "github.com/bnema/go-wayland-bindings/server/wayland" | |||
| 5 | + | "github.com/bnema/neferwl/internal/ports" | |||
| 6 | + | "github.com/bnema/purego-libwayland/server" | |||
| 7 | + | ) | |||
| 8 | + | ||||
| 9 | + | // Role-independent accessors of a window. Code outside xdg.go goes through | |||
| 10 | + | // them instead of dereferencing w.xdg, which is nil for windows of another | |||
| 11 | + | // role. | |||
| 12 | + | ||||
| 13 | + | // isToplevel reports whether w is a toplevel (xdg or X11), not a popup. | |||
| 14 | + | func (w *window) isToplevel() bool { return w.toplevel != nil || (w.x11 != nil && !w.x11.override) } | |||
| 15 | + | ||||
| 16 | + | // srv is the server the window belongs to. | |||
| 17 | + | func (w *window) srv() *Server { | |||
| 18 | + | if w.xdg != nil { | |||
| 19 | + | return w.xdg.server | |||
| 20 | + | } | |||
| 21 | + | return w.x11.server | |||
| 22 | + | } | |||
| 23 | + | ||||
| 24 | + | // surf is the window's surface, nil when it has none. | |||
| 25 | + | func (w *window) surf() *surface { | |||
| 26 | + | switch { | |||
| 27 | + | case w.xdg != nil: | |||
| 28 | + | return w.xdg.surface | |||
| 29 | + | case w.x11 != nil: | |||
| 30 | + | return w.x11.surface | |||
| 31 | + | } | |||
| 32 | + | return nil | |||
| 33 | + | } | |||
| 34 | + | ||||
| 35 | + | // alive reports whether the window's role object is alive. | |||
| 36 | + | func (w *window) alive() bool { | |||
| 37 | + | switch { | |||
| 38 | + | case w.xdg != nil: | |||
| 39 | + | return w.xdg.resource.Resource.Alive() | |||
| 40 | + | case w.x11 != nil: | |||
| 41 | + | return w.x11.surface != nil && w.x11.surface.wl.Resource.Alive() | |||
| 42 | + | } | |||
| 43 | + | return false | |||
| 44 | + | } | |||
| 45 | + | ||||
| 46 | + | // client is the window's client; the zero Client when it has no role object. | |||
| 47 | + | func (w *window) client() server.Client { | |||
| 48 | + | switch { | |||
| 49 | + | case w.xdg != nil: | |||
| 50 | + | return w.xdg.resource.Client() | |||
| 51 | + | case w.x11 != nil && w.x11.surface != nil: | |||
| 52 | + | return w.x11.surface.wl.Client() | |||
| 53 | + | } | |||
| 54 | + | return server.Client{} | |||
| 55 | + | } | |||
| 56 | + | ||||
| 57 | + | // geometry is the committed window geometry; empty means the whole surface. | |||
| 58 | + | func (w *window) geometry() ports.Rect { | |||
| 59 | + | if w.xdg != nil { | |||
| 60 | + | return w.xdg.geometry | |||
| 61 | + | } | |||
| 62 | + | return ports.Rect{} | |||
| 63 | + | } | |||
| 64 | + | ||||
| 65 | + | // sendConfigure sends w.last to the client. | |||
| 66 | + | func (w *window) sendConfigure() { | |||
| 67 | + | switch { | |||
| 68 | + | case w.xdg != nil: | |||
| 69 | + | w.sendXDGConfigure() | |||
| 70 | + | case w.x11 != nil: | |||
| 71 | + | w.x11.server.x11Configure(w) | |||
| 72 | + | } | |||
| 73 | + | } | |||
| 74 | + | ||||
| 75 | + | // sendClose asks the client to close the window. | |||
| 76 | + | func (w *window) sendClose() { | |||
| 77 | + | switch { | |||
| 78 | + | case w.toplevel != nil: | |||
| 79 | + | w.toplevel.SendClose() | |||
| 80 | + | case w.x11 != nil: | |||
| 81 | + | w.x11.server.x11Close(w) | |||
| 82 | + | } | |||
| 83 | + | } | |||
| 84 | + | ||||
| 85 | + | // toplevelAlive reports whether the window is a toplevel whose protocol | |||
| 86 | + | // object is alive. | |||
| 87 | + | func (w *window) toplevelAlive() bool { | |||
| 88 | + | if w.x11 != nil { | |||
| 89 | + | return w.alive() | |||
| 90 | + | } | |||
| 91 | + | return w.toplevel != nil && w.toplevel.Resource.Alive() | |||
| 92 | + | } | |||
| 93 | + | ||||
| 94 | + | // surfaceResource is the window's wl_surface, nil when it has none or it died. | |||
| 95 | + | func (w *window) surfaceResource() *wayland.Surface { | |||
| 96 | + | switch { | |||
| 97 | + | case w.xdg != nil: | |||
| 98 | + | return w.xdg.surfaceResource() | |||
| 99 | + | case w.x11 != nil && w.alive(): | |||
| 100 | + | return w.x11.surface.wl | |||
| 101 | + | } | |||
| 102 | + | return nil | |||
| 103 | + | } | |||
| 104 | + | ||||
| 105 | + | // toplevelWindow is the toplevel window (xdg or X11) the surface is the role | |||
| 106 | + | // surface of, nil for other roles and popups' parents are not followed. | |||
| 107 | + | func (s *surface) toplevelWindow() *window { | |||
| 108 | + | if s.xdg != nil && s.xdg.window != nil { | |||
| 109 | + | return s.xdg.window | |||
| 110 | + | } | |||
| 111 | + | return s.x11win | |||
| 112 | + | } | |||
M internal/adapters/wayland/wire_client_test.go +6 -0
| @@ -55,6 +55,8 @@ 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" | |
| 59 | + | wire_xwaylandshell "github.com/bnema/go-wayland-bindings/server/xwaylandshell" | |
| 58 | 60 | wire_imagecapture "github.com/bnema/neferwl/internal/adapters/wayland/imagecapture" | |
| 59 | 61 | "github.com/bnema/purego-libwayland/server" | |
| 60 | 62 | "github.com/bnema/wlturbo" | |
| @@ -253,6 +255,10 @@ var wireSchemas = map[string]wireSchema{ | |||
| 253 | 255 | "zwp_text_input_v3": {wire_textinput.ZwpTextInputV3Interface, &client_textinput.TextInputV3{}, map[uint16]bool{0: true}, nil}, | |
| 254 | 256 | "zwp_virtual_keyboard_manager_v1": {wire_virtualkeyboard.ZwpVirtualKeyboardManagerV1Interface, &client_virtualkeyboard.VirtualKeyboardManager{}, nil, nil}, | |
| 255 | 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}, | |
| 260 | + | "xwayland_shell_v1": {wire_xwaylandshell.XwaylandShellV1Interface, nil, map[uint16]bool{0: true}, nil}, | |
| 261 | + | "xwayland_surface_v1": {wire_xwaylandshell.XwaylandSurfaceV1Interface, nil, map[uint16]bool{1: true}, nil}, | |
| 256 | 262 | "zxdg_output_manager_v1": {wire_xdgoutput.ZxdgOutputManagerV1Interface, &client_xdgoutput.ZxdgOutputManager{}, map[uint16]bool{0: true}, nil}, | |
| 257 | 263 | "zxdg_output_v1": {wire_xdgoutput.ZxdgOutputV1Interface, &client_xdgoutput.ZxdgOutput{}, map[uint16]bool{0: true}, nil}, | |
| 258 | 264 | } | |
| @@ -55,6 +55,8 @@ 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" | |||
| 59 | + | wire_xwaylandshell "github.com/bnema/go-wayland-bindings/server/xwaylandshell" | |||
| 58 | wire_imagecapture "github.com/bnema/neferwl/internal/adapters/wayland/imagecapture" | 60 | wire_imagecapture "github.com/bnema/neferwl/internal/adapters/wayland/imagecapture" | ||
| 59 | "github.com/bnema/purego-libwayland/server" | 61 | "github.com/bnema/purego-libwayland/server" | ||
| 60 | "github.com/bnema/wlturbo" | 62 | "github.com/bnema/wlturbo" | ||
| @@ -253,6 +255,10 @@ var wireSchemas = map[string]wireSchema{ | |||||
| 253 | "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}, | ||
| 254 | "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}, | ||
| 255 | "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}, | |||
| 260 | + | "xwayland_shell_v1": {wire_xwaylandshell.XwaylandShellV1Interface, nil, map[uint16]bool{0: true}, nil}, | |||
| 261 | + | "xwayland_surface_v1": {wire_xwaylandshell.XwaylandSurfaceV1Interface, nil, map[uint16]bool{1: true}, nil}, | |||
| 256 | "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}, | ||
| 257 | "zxdg_output_v1": {wire_xdgoutput.ZxdgOutputV1Interface, &client_xdgoutput.ZxdgOutput{}, map[uint16]bool{0: true}, nil}, | 263 | "zxdg_output_v1": {wire_xdgoutput.ZxdgOutputV1Interface, &client_xdgoutput.ZxdgOutput{}, map[uint16]bool{0: true}, nil}, | ||
| 258 | } | 264 | } | ||
A internal/adapters/wayland/x11override_test.go +107 -0
| @@ -0,0 +1,107 @@ | |||
| 1 | + | package wayland | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "testing" | |
| 5 | + | ||
| 6 | + | "github.com/bnema/go-wayland-bindings/server/wayland" | |
| 7 | + | "github.com/bnema/neferwl/internal/adapters/wayland/xwm" | |
| 8 | + | "github.com/bnema/neferwl/internal/ports" | |
| 9 | + | ) | |
| 10 | + | ||
| 11 | + | const ovWin = 0x500002 | |
| 12 | + | ||
| 13 | + | // overrideWindow maps an override-redirect X window and pairs its surface. | |
| 14 | + | func (r *x11Rig) overrideWindow(win uint32, x, y int16, serial uint64) uint32 { | |
| 15 | + | r.t.Helper() | |
| 16 | + | r.peer.createOverride(win, x, y) | |
| 17 | + | r.peer.mapNotify(win) | |
| 18 | + | return r.pair(win, serial) | |
| 19 | + | } | |
| 20 | + | ||
| 21 | + | func (r *x11Rig) state(f func()) { | |
| 22 | + | r.t.Helper() | |
| 23 | + | r.s.display.Do(f) | |
| 24 | + | } | |
| 25 | + | ||
| 26 | + | func TestOverrideMapsOnFirstCommitAndMoves(t *testing.T) { | |
| 27 | + | r := newX11Rig(t) | |
| 28 | + | r.overrideWindow(ovWin, 30, 40, x11Serial) | |
| 29 | + | ||
| 30 | + | m := awaitEvent[ports.UnmanagedMapped](t, r.events) | |
| 31 | + | if m.X != 30 || m.Y != 40 || m.Width != 100 || m.Height != 50 || m.NoInput { | |
| 32 | + | t.Fatalf("mapped %+v", m) | |
| 33 | + | } | |
| 34 | + | r.state(func() { | |
| 35 | + | w := r.s.windows[m.ID] | |
| 36 | + | if w == nil || !w.isOverride() || w.isToplevel() || !w.mapped { | |
| 37 | + | t.Errorf("override window %+v", w) | |
| 38 | + | } | |
| 39 | + | if surf, _ := r.s.focusTarget(m.ID); surf != nil { | |
| 40 | + | t.Error("override window is a keyboard focus target") | |
| 41 | + | } | |
| 42 | + | }) | |
| 43 | + | ||
| 44 | + | r.peer.configureNotify(ovWin, 60, 70, 100, 50) | |
| 45 | + | if mv := awaitEvent[ports.UnmanagedMoved](t, r.events); mv.ID != m.ID || mv.X != 60 || mv.Y != 70 { | |
| 46 | + | t.Fatalf("moved %+v", mv) | |
| 47 | + | } | |
| 48 | + | ||
| 49 | + | r.peer.unmapNotify(ovWin) | |
| 50 | + | if u := awaitEvent[ports.UnmanagedUnmapped](t, r.events); u.ID != m.ID { | |
| 51 | + | t.Fatalf("unmapped %+v", u) | |
| 52 | + | } | |
| 53 | + | r.state(func() { | |
| 54 | + | if r.s.windows[m.ID] != nil { | |
| 55 | + | t.Error("override window kept after unmap") | |
| 56 | + | } | |
| 57 | + | }) | |
| 58 | + | } | |
| 59 | + | ||
| 60 | + | func TestOverrideSurfaceDestroyUnmaps(t *testing.T) { | |
| 61 | + | r := newX11Rig(t) | |
| 62 | + | surf := r.overrideWindow(ovWin, 0, 0, x11Serial) | |
| 63 | + | m := awaitEvent[ports.UnmanagedMapped](t, r.events) | |
| 64 | + | requestProtocol(t, r.xc, surf, wayland.SurfaceRequestDestroy) | |
| 65 | + | if err := r.xc.Roundtrip(); err != nil { | |
| 66 | + | t.Fatal(err) | |
| 67 | + | } | |
| 68 | + | if u := awaitEvent[ports.UnmanagedUnmapped](t, r.events); u.ID != m.ID { | |
| 69 | + | t.Fatalf("unmapped %+v", u) | |
| 70 | + | } | |
| 71 | + | } | |
| 72 | + | ||
| 73 | + | func TestOverrideTakesPointerAndOutput(t *testing.T) { | |
| 74 | + | r := newX11Rig(t) | |
| 75 | + | r.commands <- ports.SetOutputs{Outputs: ports.Layout{ | |
| 76 | + | {Info: ports.OutputInfo{Name: "A"}, X: 0, Y: 0, Width: 1920, Height: 1080, Scale: 1}, | |
| 77 | + | {Info: ports.OutputInfo{Name: "B"}, X: 1920, Y: 0, Width: 1920, Height: 1080, Scale: 1}, | |
| 78 | + | }, Focused: "A"} | |
| 79 | + | r.overrideWindow(ovWin, 100, 10, x11Serial) | |
| 80 | + | m := awaitEvent[ports.UnmanagedMapped](t, r.events) | |
| 81 | + | r.state(func() { | |
| 82 | + | w := r.s.windows[m.ID] | |
| 83 | + | if got := r.s.frameOutput(w.x11.surface); got != "A" { | |
| 84 | + | t.Errorf("frame output %q, want A", got) | |
| 85 | + | } | |
| 86 | + | if surf, _, _, _ := r.s.pointerSurface(m.ID, 1, 1); surf == nil { | |
| 87 | + | t.Error("pointer cannot enter the override surface") | |
| 88 | + | } | |
| 89 | + | }) | |
| 90 | + | r.peer.configureNotify(ovWin, 2000, 10, 100, 50) | |
| 91 | + | awaitEvent[ports.UnmanagedMoved](t, r.events) | |
| 92 | + | r.state(func() { | |
| 93 | + | if got := r.s.frameOutput(r.s.windows[m.ID].x11.surface); got != "B" { | |
| 94 | + | t.Errorf("frame output %q after move, want B", got) | |
| 95 | + | } | |
| 96 | + | }) | |
| 97 | + | } | |
| 98 | + | ||
| 99 | + | func TestOverrideMovedUnchangedAllocations(t *testing.T) { | |
| 100 | + | s := &Server{windows: map[ports.WindowID]*window{}, x11Slots: map[xwm.Slot]*window{}} | |
| 101 | + | w := &window{id: 1, x11: &x11Window{override: true, rect: ports.Rect{X: 1, Y: 2, W: 3, H: 4}}} | |
| 102 | + | s.x11Slots[1] = w | |
| 103 | + | h := hostImpl{s} | |
| 104 | + | if n := testing.AllocsPerRun(1000, func() { h.OverrideMoved(1, 1, 2, 3, 4) }); n != 0 { | |
| 105 | + | t.Fatalf("%v allocs", n) | |
| 106 | + | } | |
| 107 | + | } | |
| @@ -0,0 +1,107 @@ | |||||
| 1 | + | package wayland | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "testing" | |||
| 5 | + | ||||
| 6 | + | "github.com/bnema/go-wayland-bindings/server/wayland" | |||
| 7 | + | "github.com/bnema/neferwl/internal/adapters/wayland/xwm" | |||
| 8 | + | "github.com/bnema/neferwl/internal/ports" | |||
| 9 | + | ) | |||
| 10 | + | ||||
| 11 | + | const ovWin = 0x500002 | |||
| 12 | + | ||||
| 13 | + | // overrideWindow maps an override-redirect X window and pairs its surface. | |||
| 14 | + | func (r *x11Rig) overrideWindow(win uint32, x, y int16, serial uint64) uint32 { | |||
| 15 | + | r.t.Helper() | |||
| 16 | + | r.peer.createOverride(win, x, y) | |||
| 17 | + | r.peer.mapNotify(win) | |||
| 18 | + | return r.pair(win, serial) | |||
| 19 | + | } | |||
| 20 | + | ||||
| 21 | + | func (r *x11Rig) state(f func()) { | |||
| 22 | + | r.t.Helper() | |||
| 23 | + | r.s.display.Do(f) | |||
| 24 | + | } | |||
| 25 | + | ||||
| 26 | + | func TestOverrideMapsOnFirstCommitAndMoves(t *testing.T) { | |||
| 27 | + | r := newX11Rig(t) | |||
| 28 | + | r.overrideWindow(ovWin, 30, 40, x11Serial) | |||
| 29 | + | ||||
| 30 | + | m := awaitEvent[ports.UnmanagedMapped](t, r.events) | |||
| 31 | + | if m.X != 30 || m.Y != 40 || m.Width != 100 || m.Height != 50 || m.NoInput { | |||
| 32 | + | t.Fatalf("mapped %+v", m) | |||
| 33 | + | } | |||
| 34 | + | r.state(func() { | |||
| 35 | + | w := r.s.windows[m.ID] | |||
| 36 | + | if w == nil || !w.isOverride() || w.isToplevel() || !w.mapped { | |||
| 37 | + | t.Errorf("override window %+v", w) | |||
| 38 | + | } | |||
| 39 | + | if surf, _ := r.s.focusTarget(m.ID); surf != nil { | |||
| 40 | + | t.Error("override window is a keyboard focus target") | |||
| 41 | + | } | |||
| 42 | + | }) | |||
| 43 | + | ||||
| 44 | + | r.peer.configureNotify(ovWin, 60, 70, 100, 50) | |||
| 45 | + | if mv := awaitEvent[ports.UnmanagedMoved](t, r.events); mv.ID != m.ID || mv.X != 60 || mv.Y != 70 { | |||
| 46 | + | t.Fatalf("moved %+v", mv) | |||
| 47 | + | } | |||
| 48 | + | ||||
| 49 | + | r.peer.unmapNotify(ovWin) | |||
| 50 | + | if u := awaitEvent[ports.UnmanagedUnmapped](t, r.events); u.ID != m.ID { | |||
| 51 | + | t.Fatalf("unmapped %+v", u) | |||
| 52 | + | } | |||
| 53 | + | r.state(func() { | |||
| 54 | + | if r.s.windows[m.ID] != nil { | |||
| 55 | + | t.Error("override window kept after unmap") | |||
| 56 | + | } | |||
| 57 | + | }) | |||
| 58 | + | } | |||
| 59 | + | ||||
| 60 | + | func TestOverrideSurfaceDestroyUnmaps(t *testing.T) { | |||
| 61 | + | r := newX11Rig(t) | |||
| 62 | + | surf := r.overrideWindow(ovWin, 0, 0, x11Serial) | |||
| 63 | + | m := awaitEvent[ports.UnmanagedMapped](t, r.events) | |||
| 64 | + | requestProtocol(t, r.xc, surf, wayland.SurfaceRequestDestroy) | |||
| 65 | + | if err := r.xc.Roundtrip(); err != nil { | |||
| 66 | + | t.Fatal(err) | |||
| 67 | + | } | |||
| 68 | + | if u := awaitEvent[ports.UnmanagedUnmapped](t, r.events); u.ID != m.ID { | |||
| 69 | + | t.Fatalf("unmapped %+v", u) | |||
| 70 | + | } | |||
| 71 | + | } | |||
| 72 | + | ||||
| 73 | + | func TestOverrideTakesPointerAndOutput(t *testing.T) { | |||
| 74 | + | r := newX11Rig(t) | |||
| 75 | + | r.commands <- ports.SetOutputs{Outputs: ports.Layout{ | |||
| 76 | + | {Info: ports.OutputInfo{Name: "A"}, X: 0, Y: 0, Width: 1920, Height: 1080, Scale: 1}, | |||
| 77 | + | {Info: ports.OutputInfo{Name: "B"}, X: 1920, Y: 0, Width: 1920, Height: 1080, Scale: 1}, | |||
| 78 | + | }, Focused: "A"} | |||
| 79 | + | r.overrideWindow(ovWin, 100, 10, x11Serial) | |||
| 80 | + | m := awaitEvent[ports.UnmanagedMapped](t, r.events) | |||
| 81 | + | r.state(func() { | |||
| 82 | + | w := r.s.windows[m.ID] | |||
| 83 | + | if got := r.s.frameOutput(w.x11.surface); got != "A" { | |||
| 84 | + | t.Errorf("frame output %q, want A", got) | |||
| 85 | + | } | |||
| 86 | + | if surf, _, _, _ := r.s.pointerSurface(m.ID, 1, 1); surf == nil { | |||
| 87 | + | t.Error("pointer cannot enter the override surface") | |||
| 88 | + | } | |||
| 89 | + | }) | |||
| 90 | + | r.peer.configureNotify(ovWin, 2000, 10, 100, 50) | |||
| 91 | + | awaitEvent[ports.UnmanagedMoved](t, r.events) | |||
| 92 | + | r.state(func() { | |||
| 93 | + | if got := r.s.frameOutput(r.s.windows[m.ID].x11.surface); got != "B" { | |||
| 94 | + | t.Errorf("frame output %q after move, want B", got) | |||
| 95 | + | } | |||
| 96 | + | }) | |||
| 97 | + | } | |||
| 98 | + | ||||
| 99 | + | func TestOverrideMovedUnchangedAllocations(t *testing.T) { | |||
| 100 | + | s := &Server{windows: map[ports.WindowID]*window{}, x11Slots: map[xwm.Slot]*window{}} | |||
| 101 | + | w := &window{id: 1, x11: &x11Window{override: true, rect: ports.Rect{X: 1, Y: 2, W: 3, H: 4}}} | |||
| 102 | + | s.x11Slots[1] = w | |||
| 103 | + | h := hostImpl{s} | |||
| 104 | + | if n := testing.AllocsPerRun(1000, func() { h.OverrideMoved(1, 1, 2, 3, 4) }); n != 0 { | |||
| 105 | + | t.Fatalf("%v allocs", n) | |||
| 106 | + | } | |||
| 107 | + | } | |||
A internal/adapters/wayland/x11peer_test.go +332 -0
| @@ -0,0 +1,332 @@ | |||
| 1 | + | package wayland | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "encoding/binary" | |
| 5 | + | "io" | |
| 6 | + | "os" | |
| 7 | + | "sync" | |
| 8 | + | "testing" | |
| 9 | + | "time" | |
| 10 | + | ||
| 11 | + | "golang.org/x/sys/unix" | |
| 12 | + | ) | |
| 13 | + | ||
| 14 | + | // x11Peer is a scripted X server speaking the real wire protocol over a | |
| 15 | + | // socketpair: setup, extensions, atoms, properties, geometry. It records every | |
| 16 | + | // request. It is a protocol peer, not a double of a project interface. | |
| 17 | + | type x11Peer struct { | |
| 18 | + | t *testing.T | |
| 19 | + | f *os.File | |
| 20 | + | mu sync.Mutex | |
| 21 | + | ||
| 22 | + | seq uint16 | |
| 23 | + | reqs []x11Req | |
| 24 | + | atoms map[string]uint32 | |
| 25 | + | nextAtom uint32 | |
| 26 | + | props map[uint32]map[uint32][]byte // window -> atom -> typed data (type, format, data) | |
| 27 | + | propMeta map[uint32]map[uint32][2]uint32 | |
| 28 | + | done chan struct{} | |
| 29 | + | } | |
| 30 | + | ||
| 31 | + | type x11Req struct { | |
| 32 | + | major, minor uint8 | |
| 33 | + | raw []byte | |
| 34 | + | } | |
| 35 | + | ||
| 36 | + | func (r x11Req) u32(off int) uint32 { return binary.LittleEndian.Uint32(r.raw[off:]) } | |
| 37 | + | ||
| 38 | + | const ( | |
| 39 | + | x11PeerRoot = 0x100 | |
| 40 | + | x11Win = 0x500001 | |
| 41 | + | // Opcodes the tests look for. | |
| 42 | + | x11OpConfigureWindow = 12 | |
| 43 | + | x11OpSetInputFocus = 42 | |
| 44 | + | x11OpKillClient = 113 | |
| 45 | + | ) | |
| 46 | + | ||
| 47 | + | func startX11Peer(t *testing.T) (int, *x11Peer) { | |
| 48 | + | t.Helper() | |
| 49 | + | fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0) | |
| 50 | + | if err != nil { | |
| 51 | + | t.Fatal(err) | |
| 52 | + | } | |
| 53 | + | p := &x11Peer{ | |
| 54 | + | t: t, f: os.NewFile(uintptr(fds[1]), "x11peer"), atoms: map[string]uint32{}, nextAtom: 1000, | |
| 55 | + | props: map[uint32]map[uint32][]byte{}, propMeta: map[uint32]map[uint32][2]uint32{}, done: make(chan struct{}), | |
| 56 | + | } | |
| 57 | + | t.Cleanup(func() { p.f.Close(); <-p.done }) | |
| 58 | + | go p.serve() | |
| 59 | + | return fds[0], p | |
| 60 | + | } | |
| 61 | + | ||
| 62 | + | func (p *x11Peer) setupReply() []byte { | |
| 63 | + | vendor := []byte("x11pr\x00\x00\x00") | |
| 64 | + | b := make([]byte, 40+len(vendor)+40) | |
| 65 | + | b[0], b[2] = 1, 11 | |
| 66 | + | binary.LittleEndian.PutUint16(b[6:], uint16((len(b)-8)/4)) | |
| 67 | + | binary.LittleEndian.PutUint32(b[12:], 0x200000) | |
| 68 | + | binary.LittleEndian.PutUint32(b[16:], 0x1fffff) | |
| 69 | + | binary.LittleEndian.PutUint16(b[24:], 5) | |
| 70 | + | b[28] = 1 | |
| 71 | + | copy(b[40:], vendor) | |
| 72 | + | scr := b[40+len(vendor):] | |
| 73 | + | binary.LittleEndian.PutUint32(scr, x11PeerRoot) | |
| 74 | + | binary.LittleEndian.PutUint32(scr[32:], 0x22) | |
| 75 | + | scr[38] = 24 | |
| 76 | + | return b | |
| 77 | + | } | |
| 78 | + | ||
| 79 | + | func (p *x11Peer) serve() { | |
| 80 | + | defer close(p.done) | |
| 81 | + | var hdr [12]byte | |
| 82 | + | if _, err := io.ReadFull(p.f, hdr[:]); err != nil { | |
| 83 | + | return | |
| 84 | + | } | |
| 85 | + | if _, err := p.f.Write(p.setupReply()); err != nil { | |
| 86 | + | return | |
| 87 | + | } | |
| 88 | + | for { | |
| 89 | + | var h [4]byte | |
| 90 | + | if _, err := io.ReadFull(p.f, h[:]); err != nil { | |
| 91 | + | return | |
| 92 | + | } | |
| 93 | + | raw := h[:] | |
| 94 | + | n := int(binary.LittleEndian.Uint16(h[2:])) * 4 | |
| 95 | + | if n == 0 { | |
| 96 | + | var l [4]byte | |
| 97 | + | if _, err := io.ReadFull(p.f, l[:]); err != nil { | |
| 98 | + | return | |
| 99 | + | } | |
| 100 | + | raw = append(raw, l[:]...) | |
| 101 | + | n = int(binary.LittleEndian.Uint32(l[:])) * 4 | |
| 102 | + | } | |
| 103 | + | rest := make([]byte, n-len(raw)) | |
| 104 | + | if _, err := io.ReadFull(p.f, rest); err != nil { | |
| 105 | + | return | |
| 106 | + | } | |
| 107 | + | raw = append(append([]byte(nil), raw...), rest...) | |
| 108 | + | p.mu.Lock() | |
| 109 | + | p.seq++ | |
| 110 | + | r := x11Req{h[0], h[1], raw} | |
| 111 | + | p.reqs = append(p.reqs, r) | |
| 112 | + | p.answer(r, raw[len(raw)-len(rest):]) | |
| 113 | + | p.mu.Unlock() | |
| 114 | + | } | |
| 115 | + | } | |
| 116 | + | ||
| 117 | + | func (p *x11Peer) reply(data uint8, body []byte) { | |
| 118 | + | words := (max(len(body), 24) - 24 + 3) / 4 | |
| 119 | + | b := make([]byte, 32+4*words) | |
| 120 | + | b[0], b[1] = 1, data | |
| 121 | + | binary.LittleEndian.PutUint16(b[2:], p.seq) | |
| 122 | + | binary.LittleEndian.PutUint32(b[4:], uint32(words)) | |
| 123 | + | copy(b[8:], body) | |
| 124 | + | _, _ = p.f.Write(b) | |
| 125 | + | } | |
| 126 | + | ||
| 127 | + | func x11le32(v ...uint32) []byte { | |
| 128 | + | b := make([]byte, 0, 4*len(v)) | |
| 129 | + | for _, x := range v { | |
| 130 | + | b = binary.LittleEndian.AppendUint32(b, x) | |
| 131 | + | } | |
| 132 | + | return b | |
| 133 | + | } | |
| 134 | + | ||
| 135 | + | var x11Exts = map[string]uint8{"BIG-REQUESTS": 133, "XC-MISC": 136, "Composite": 142} | |
| 136 | + | ||
| 137 | + | func (p *x11Peer) answer(r x11Req, body []byte) { | |
| 138 | + | switch { | |
| 139 | + | case r.major == 98: // QueryExtension | |
| 140 | + | major, ok := x11Exts[string(body[4:4+binary.LittleEndian.Uint16(body)])] | |
| 141 | + | present := uint8(0) | |
| 142 | + | if ok { | |
| 143 | + | present = 1 | |
| 144 | + | } | |
| 145 | + | p.reply(0, []byte{present, major, 0, 0}) | |
| 146 | + | case r.major == 16: // InternAtom | |
| 147 | + | name := string(body[4 : 4+binary.LittleEndian.Uint16(body)]) | |
| 148 | + | a, ok := p.atoms[name] | |
| 149 | + | if !ok { | |
| 150 | + | p.nextAtom++ | |
| 151 | + | a = p.nextAtom | |
| 152 | + | p.atoms[name] = a | |
| 153 | + | } | |
| 154 | + | p.reply(0, x11le32(a)) | |
| 155 | + | case r.major == 43: // GetInputFocus (Sync) | |
| 156 | + | p.reply(1, x11le32(0)) | |
| 157 | + | case r.major == 15: // QueryTree | |
| 158 | + | b := make([]byte, 24) | |
| 159 | + | binary.LittleEndian.PutUint32(b, x11PeerRoot) | |
| 160 | + | p.reply(0, b) | |
| 161 | + | case r.major == 14: // GetGeometry | |
| 162 | + | b := make([]byte, 24) | |
| 163 | + | binary.LittleEndian.PutUint32(b, x11PeerRoot) | |
| 164 | + | binary.LittleEndian.PutUint16(b[8:], 300) | |
| 165 | + | binary.LittleEndian.PutUint16(b[10:], 200) | |
| 166 | + | p.reply(24, b) | |
| 167 | + | case r.major == 18: // ChangeProperty | |
| 168 | + | win, prop := binary.LittleEndian.Uint32(body), binary.LittleEndian.Uint32(body[4:]) | |
| 169 | + | format, n := body[12], int(binary.LittleEndian.Uint32(body[16:])) | |
| 170 | + | p.store(win, prop, binary.LittleEndian.Uint32(body[8:]), uint32(format), body[20:20+n*int(format)/8]) | |
| 171 | + | case r.major == 20: // GetProperty | |
| 172 | + | win, prop := binary.LittleEndian.Uint32(body), binary.LittleEndian.Uint32(body[4:]) | |
| 173 | + | data, ok := p.props[win][prop] | |
| 174 | + | b := make([]byte, 24+(len(data)+3)&^3) | |
| 175 | + | var format uint8 | |
| 176 | + | if ok { | |
| 177 | + | meta := p.propMeta[win][prop] | |
| 178 | + | format = uint8(meta[1]) | |
| 179 | + | binary.LittleEndian.PutUint32(b[0:], meta[0]) | |
| 180 | + | binary.LittleEndian.PutUint32(b[8:], uint32(len(data)/(int(format)/8))) | |
| 181 | + | copy(b[24:], data) | |
| 182 | + | } | |
| 183 | + | p.reply(format, b) | |
| 184 | + | case r.major >= 128: | |
| 185 | + | switch r.major { | |
| 186 | + | case 133: | |
| 187 | + | p.reply(0, x11le32(0x100000)) | |
| 188 | + | case 136: | |
| 189 | + | if r.minor == 1 { | |
| 190 | + | p.reply(0, x11le32(0, 1)) | |
| 191 | + | } | |
| 192 | + | case 142: | |
| 193 | + | if r.minor == 0 { | |
| 194 | + | p.reply(0, x11le32(0, 4)) | |
| 195 | + | } | |
| 196 | + | } | |
| 197 | + | } | |
| 198 | + | } | |
| 199 | + | ||
| 200 | + | func (p *x11Peer) store(win, prop, typ, format uint32, data []byte) { | |
| 201 | + | if p.props[win] == nil { | |
| 202 | + | p.props[win], p.propMeta[win] = map[uint32][]byte{}, map[uint32][2]uint32{} | |
| 203 | + | } | |
| 204 | + | p.props[win][prop] = append([]byte(nil), data...) | |
| 205 | + | p.propMeta[win][prop] = [2]uint32{typ, format} | |
| 206 | + | } | |
| 207 | + | ||
| 208 | + | // atom interns name the way the XWM would have. | |
| 209 | + | func (p *x11Peer) atom(name string) uint32 { | |
| 210 | + | p.mu.Lock() | |
| 211 | + | defer p.mu.Unlock() | |
| 212 | + | a, ok := p.atoms[name] | |
| 213 | + | if !ok { | |
| 214 | + | p.nextAtom++ | |
| 215 | + | a = p.nextAtom | |
| 216 | + | p.atoms[name] = a | |
| 217 | + | } | |
| 218 | + | return a | |
| 219 | + | } | |
| 220 | + | ||
| 221 | + | // setProp scripts a window property (typ is an atom number, format 8 or 32). | |
| 222 | + | func (p *x11Peer) setProp(win, prop, typ, format uint32, data []byte) { | |
| 223 | + | p.mu.Lock() | |
| 224 | + | defer p.mu.Unlock() | |
| 225 | + | p.store(win, prop, typ, format, data) | |
| 226 | + | } | |
| 227 | + | ||
| 228 | + | func (p *x11Peer) event(code uint8, fill func(b *[32]byte)) { | |
| 229 | + | p.mu.Lock() | |
| 230 | + | defer p.mu.Unlock() | |
| 231 | + | var b [32]byte | |
| 232 | + | b[0] = code | |
| 233 | + | binary.LittleEndian.PutUint16(b[2:], p.seq) | |
| 234 | + | fill(&b) | |
| 235 | + | _, _ = p.f.Write(b[:]) | |
| 236 | + | } | |
| 237 | + | ||
| 238 | + | func (p *x11Peer) createNotify(win uint32) { | |
| 239 | + | p.event(16, func(b *[32]byte) { | |
| 240 | + | binary.LittleEndian.PutUint32(b[4:], x11PeerRoot) | |
| 241 | + | binary.LittleEndian.PutUint32(b[8:], win) | |
| 242 | + | binary.LittleEndian.PutUint16(b[16:], 100) | |
| 243 | + | binary.LittleEndian.PutUint16(b[18:], 50) | |
| 244 | + | }) | |
| 245 | + | } | |
| 246 | + | ||
| 247 | + | // createOverride announces an override-redirect window of 100x50 at x,y. | |
| 248 | + | func (p *x11Peer) createOverride(win uint32, x, y int16) { | |
| 249 | + | p.event(16, func(b *[32]byte) { | |
| 250 | + | binary.LittleEndian.PutUint32(b[4:], x11PeerRoot) | |
| 251 | + | binary.LittleEndian.PutUint32(b[8:], win) | |
| 252 | + | binary.LittleEndian.PutUint16(b[12:], uint16(x)) | |
| 253 | + | binary.LittleEndian.PutUint16(b[14:], uint16(y)) | |
| 254 | + | binary.LittleEndian.PutUint16(b[16:], 100) | |
| 255 | + | binary.LittleEndian.PutUint16(b[18:], 50) | |
| 256 | + | b[22] = 1 | |
| 257 | + | }) | |
| 258 | + | } | |
| 259 | + | ||
| 260 | + | func (p *x11Peer) mapNotify(win uint32) { | |
| 261 | + | p.event(19, func(b *[32]byte) { | |
| 262 | + | binary.LittleEndian.PutUint32(b[4:], x11PeerRoot) | |
| 263 | + | binary.LittleEndian.PutUint32(b[8:], win) | |
| 264 | + | b[12] = 1 | |
| 265 | + | }) | |
| 266 | + | } | |
| 267 | + | ||
| 268 | + | // configureNotify moves and resizes a window (root SubstructureNotify form). | |
| 269 | + | func (p *x11Peer) configureNotify(win uint32, x, y int16, w, h uint16) { | |
| 270 | + | p.event(22, func(b *[32]byte) { | |
| 271 | + | binary.LittleEndian.PutUint32(b[4:], x11PeerRoot) | |
| 272 | + | binary.LittleEndian.PutUint32(b[8:], win) | |
| 273 | + | binary.LittleEndian.PutUint16(b[16:], uint16(x)) | |
| 274 | + | binary.LittleEndian.PutUint16(b[18:], uint16(y)) | |
| 275 | + | binary.LittleEndian.PutUint16(b[20:], w) | |
| 276 | + | binary.LittleEndian.PutUint16(b[22:], h) | |
| 277 | + | b[24] = 1 | |
| 278 | + | }) | |
| 279 | + | } | |
| 280 | + | ||
| 281 | + | func (p *x11Peer) windowEvent(code uint8, win uint32) { | |
| 282 | + | p.event(code, func(b *[32]byte) { | |
| 283 | + | binary.LittleEndian.PutUint32(b[4:], x11PeerRoot) | |
| 284 | + | binary.LittleEndian.PutUint32(b[8:], win) | |
| 285 | + | }) | |
| 286 | + | } | |
| 287 | + | ||
| 288 | + | func (p *x11Peer) mapRequest(win uint32) { p.windowEvent(20, win) } | |
| 289 | + | func (p *x11Peer) unmapNotify(win uint32) { p.windowEvent(18, win) } | |
| 290 | + | func (p *x11Peer) destroyNotify(win uint32) { p.windowEvent(17, win) } | |
| 291 | + | ||
| 292 | + | func (p *x11Peer) propertyNotify(win, atom uint32) { | |
| 293 | + | p.event(28, func(b *[32]byte) { | |
| 294 | + | binary.LittleEndian.PutUint32(b[4:], win) | |
| 295 | + | binary.LittleEndian.PutUint32(b[8:], atom) | |
| 296 | + | }) | |
| 297 | + | } | |
| 298 | + | ||
| 299 | + | func (p *x11Peer) clientMessage(win uint32, typ string, data ...uint32) { | |
| 300 | + | t := p.atom(typ) | |
| 301 | + | p.event(33, func(b *[32]byte) { | |
| 302 | + | b[1] = 32 | |
| 303 | + | binary.LittleEndian.PutUint32(b[4:], win) | |
| 304 | + | binary.LittleEndian.PutUint32(b[8:], t) | |
| 305 | + | for i, d := range data { | |
| 306 | + | binary.LittleEndian.PutUint32(b[12+4*i:], d) | |
| 307 | + | } | |
| 308 | + | }) | |
| 309 | + | } | |
| 310 | + | ||
| 311 | + | func (p *x11Peer) requests() []x11Req { | |
| 312 | + | p.mu.Lock() | |
| 313 | + | defer p.mu.Unlock() | |
| 314 | + | return append([]x11Req(nil), p.reqs...) | |
| 315 | + | } | |
| 316 | + | ||
| 317 | + | // waitReq waits for a request after the first skip ones that satisfies match. | |
| 318 | + | func (p *x11Peer) waitReq(skip int, match func(x11Req) bool) (x11Req, int) { | |
| 319 | + | p.t.Helper() | |
| 320 | + | deadline := time.Now().Add(5 * time.Second) | |
| 321 | + | for time.Now().Before(deadline) { | |
| 322 | + | reqs := p.requests() | |
| 323 | + | for i := skip; i < len(reqs); i++ { | |
| 324 | + | if match(reqs[i]) { | |
| 325 | + | return reqs[i], i + 1 | |
| 326 | + | } | |
| 327 | + | } | |
| 328 | + | time.Sleep(time.Millisecond) | |
| 329 | + | } | |
| 330 | + | p.t.Fatal("x11 peer: expected request never arrived") | |
| 331 | + | return x11Req{}, 0 | |
| 332 | + | } | |
| @@ -0,0 +1,332 @@ | |||||
| 1 | + | package wayland | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "encoding/binary" | |||
| 5 | + | "io" | |||
| 6 | + | "os" | |||
| 7 | + | "sync" | |||
| 8 | + | "testing" | |||
| 9 | + | "time" | |||
| 10 | + | ||||
| 11 | + | "golang.org/x/sys/unix" | |||
| 12 | + | ) | |||
| 13 | + | ||||
| 14 | + | // x11Peer is a scripted X server speaking the real wire protocol over a | |||
| 15 | + | // socketpair: setup, extensions, atoms, properties, geometry. It records every | |||
| 16 | + | // request. It is a protocol peer, not a double of a project interface. | |||
| 17 | + | type x11Peer struct { | |||
| 18 | + | t *testing.T | |||
| 19 | + | f *os.File | |||
| 20 | + | mu sync.Mutex | |||
| 21 | + | ||||
| 22 | + | seq uint16 | |||
| 23 | + | reqs []x11Req | |||
| 24 | + | atoms map[string]uint32 | |||
| 25 | + | nextAtom uint32 | |||
| 26 | + | props map[uint32]map[uint32][]byte // window -> atom -> typed data (type, format, data) | |||
| 27 | + | propMeta map[uint32]map[uint32][2]uint32 | |||
| 28 | + | done chan struct{} | |||
| 29 | + | } | |||
| 30 | + | ||||
| 31 | + | type x11Req struct { | |||
| 32 | + | major, minor uint8 | |||
| 33 | + | raw []byte | |||
| 34 | + | } | |||
| 35 | + | ||||
| 36 | + | func (r x11Req) u32(off int) uint32 { return binary.LittleEndian.Uint32(r.raw[off:]) } | |||
| 37 | + | ||||
| 38 | + | const ( | |||
| 39 | + | x11PeerRoot = 0x100 | |||
| 40 | + | x11Win = 0x500001 | |||
| 41 | + | // Opcodes the tests look for. | |||
| 42 | + | x11OpConfigureWindow = 12 | |||
| 43 | + | x11OpSetInputFocus = 42 | |||
| 44 | + | x11OpKillClient = 113 | |||
| 45 | + | ) | |||
| 46 | + | ||||
| 47 | + | func startX11Peer(t *testing.T) (int, *x11Peer) { | |||
| 48 | + | t.Helper() | |||
| 49 | + | fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0) | |||
| 50 | + | if err != nil { | |||
| 51 | + | t.Fatal(err) | |||
| 52 | + | } | |||
| 53 | + | p := &x11Peer{ | |||
| 54 | + | t: t, f: os.NewFile(uintptr(fds[1]), "x11peer"), atoms: map[string]uint32{}, nextAtom: 1000, | |||
| 55 | + | props: map[uint32]map[uint32][]byte{}, propMeta: map[uint32]map[uint32][2]uint32{}, done: make(chan struct{}), | |||
| 56 | + | } | |||
| 57 | + | t.Cleanup(func() { p.f.Close(); <-p.done }) | |||
| 58 | + | go p.serve() | |||
| 59 | + | return fds[0], p | |||
| 60 | + | } | |||
| 61 | + | ||||
| 62 | + | func (p *x11Peer) setupReply() []byte { | |||
| 63 | + | vendor := []byte("x11pr\x00\x00\x00") | |||
| 64 | + | b := make([]byte, 40+len(vendor)+40) | |||
| 65 | + | b[0], b[2] = 1, 11 | |||
| 66 | + | binary.LittleEndian.PutUint16(b[6:], uint16((len(b)-8)/4)) | |||
| 67 | + | binary.LittleEndian.PutUint32(b[12:], 0x200000) | |||
| 68 | + | binary.LittleEndian.PutUint32(b[16:], 0x1fffff) | |||
| 69 | + | binary.LittleEndian.PutUint16(b[24:], 5) | |||
| 70 | + | b[28] = 1 | |||
| 71 | + | copy(b[40:], vendor) | |||
| 72 | + | scr := b[40+len(vendor):] | |||
| 73 | + | binary.LittleEndian.PutUint32(scr, x11PeerRoot) | |||
| 74 | + | binary.LittleEndian.PutUint32(scr[32:], 0x22) | |||
| 75 | + | scr[38] = 24 | |||
| 76 | + | return b | |||
| 77 | + | } | |||
| 78 | + | ||||
| 79 | + | func (p *x11Peer) serve() { | |||
| 80 | + | defer close(p.done) | |||
| 81 | + | var hdr [12]byte | |||
| 82 | + | if _, err := io.ReadFull(p.f, hdr[:]); err != nil { | |||
| 83 | + | return | |||
| 84 | + | } | |||
| 85 | + | if _, err := p.f.Write(p.setupReply()); err != nil { | |||
| 86 | + | return | |||
| 87 | + | } | |||
| 88 | + | for { | |||
| 89 | + | var h [4]byte | |||
| 90 | + | if _, err := io.ReadFull(p.f, h[:]); err != nil { | |||
| 91 | + | return | |||
| 92 | + | } | |||
| 93 | + | raw := h[:] | |||
| 94 | + | n := int(binary.LittleEndian.Uint16(h[2:])) * 4 | |||
| 95 | + | if n == 0 { | |||
| 96 | + | var l [4]byte | |||
| 97 | + | if _, err := io.ReadFull(p.f, l[:]); err != nil { | |||
| 98 | + | return | |||
| 99 | + | } | |||
| 100 | + | raw = append(raw, l[:]...) | |||
| 101 | + | n = int(binary.LittleEndian.Uint32(l[:])) * 4 | |||
| 102 | + | } | |||
| 103 | + | rest := make([]byte, n-len(raw)) | |||
| 104 | + | if _, err := io.ReadFull(p.f, rest); err != nil { | |||
| 105 | + | return | |||
| 106 | + | } | |||
| 107 | + | raw = append(append([]byte(nil), raw...), rest...) | |||
| 108 | + | p.mu.Lock() | |||
| 109 | + | p.seq++ | |||
| 110 | + | r := x11Req{h[0], h[1], raw} | |||
| 111 | + | p.reqs = append(p.reqs, r) | |||
| 112 | + | p.answer(r, raw[len(raw)-len(rest):]) | |||
| 113 | + | p.mu.Unlock() | |||
| 114 | + | } | |||
| 115 | + | } | |||
| 116 | + | ||||
| 117 | + | func (p *x11Peer) reply(data uint8, body []byte) { | |||
| 118 | + | words := (max(len(body), 24) - 24 + 3) / 4 | |||
| 119 | + | b := make([]byte, 32+4*words) | |||
| 120 | + | b[0], b[1] = 1, data | |||
| 121 | + | binary.LittleEndian.PutUint16(b[2:], p.seq) | |||
| 122 | + | binary.LittleEndian.PutUint32(b[4:], uint32(words)) | |||
| 123 | + | copy(b[8:], body) | |||
| 124 | + | _, _ = p.f.Write(b) | |||
| 125 | + | } | |||
| 126 | + | ||||
| 127 | + | func x11le32(v ...uint32) []byte { | |||
| 128 | + | b := make([]byte, 0, 4*len(v)) | |||
| 129 | + | for _, x := range v { | |||
| 130 | + | b = binary.LittleEndian.AppendUint32(b, x) | |||
| 131 | + | } | |||
| 132 | + | return b | |||
| 133 | + | } | |||
| 134 | + | ||||
| 135 | + | var x11Exts = map[string]uint8{"BIG-REQUESTS": 133, "XC-MISC": 136, "Composite": 142} | |||
| 136 | + | ||||
| 137 | + | func (p *x11Peer) answer(r x11Req, body []byte) { | |||
| 138 | + | switch { | |||
| 139 | + | case r.major == 98: // QueryExtension | |||
| 140 | + | major, ok := x11Exts[string(body[4:4+binary.LittleEndian.Uint16(body)])] | |||
| 141 | + | present := uint8(0) | |||
| 142 | + | if ok { | |||
| 143 | + | present = 1 | |||
| 144 | + | } | |||
| 145 | + | p.reply(0, []byte{present, major, 0, 0}) | |||
| 146 | + | case r.major == 16: // InternAtom | |||
| 147 | + | name := string(body[4 : 4+binary.LittleEndian.Uint16(body)]) | |||
| 148 | + | a, ok := p.atoms[name] | |||
| 149 | + | if !ok { | |||
| 150 | + | p.nextAtom++ | |||
| 151 | + | a = p.nextAtom | |||
| 152 | + | p.atoms[name] = a | |||
| 153 | + | } | |||
| 154 | + | p.reply(0, x11le32(a)) | |||
| 155 | + | case r.major == 43: // GetInputFocus (Sync) | |||
| 156 | + | p.reply(1, x11le32(0)) | |||
| 157 | + | case r.major == 15: // QueryTree | |||
| 158 | + | b := make([]byte, 24) | |||
| 159 | + | binary.LittleEndian.PutUint32(b, x11PeerRoot) | |||
| 160 | + | p.reply(0, b) | |||
| 161 | + | case r.major == 14: // GetGeometry | |||
| 162 | + | b := make([]byte, 24) | |||
| 163 | + | binary.LittleEndian.PutUint32(b, x11PeerRoot) | |||
| 164 | + | binary.LittleEndian.PutUint16(b[8:], 300) | |||
| 165 | + | binary.LittleEndian.PutUint16(b[10:], 200) | |||
| 166 | + | p.reply(24, b) | |||
| 167 | + | case r.major == 18: // ChangeProperty | |||
| 168 | + | win, prop := binary.LittleEndian.Uint32(body), binary.LittleEndian.Uint32(body[4:]) | |||
| 169 | + | format, n := body[12], int(binary.LittleEndian.Uint32(body[16:])) | |||
| 170 | + | p.store(win, prop, binary.LittleEndian.Uint32(body[8:]), uint32(format), body[20:20+n*int(format)/8]) | |||
| 171 | + | case r.major == 20: // GetProperty | |||
| 172 | + | win, prop := binary.LittleEndian.Uint32(body), binary.LittleEndian.Uint32(body[4:]) | |||
| 173 | + | data, ok := p.props[win][prop] | |||
| 174 | + | b := make([]byte, 24+(len(data)+3)&^3) | |||
| 175 | + | var format uint8 | |||
| 176 | + | if ok { | |||
| 177 | + | meta := p.propMeta[win][prop] | |||
| 178 | + | format = uint8(meta[1]) | |||
| 179 | + | binary.LittleEndian.PutUint32(b[0:], meta[0]) | |||
| 180 | + | binary.LittleEndian.PutUint32(b[8:], uint32(len(data)/(int(format)/8))) | |||
| 181 | + | copy(b[24:], data) | |||
| 182 | + | } | |||
| 183 | + | p.reply(format, b) | |||
| 184 | + | case r.major >= 128: | |||
| 185 | + | switch r.major { | |||
| 186 | + | case 133: | |||
| 187 | + | p.reply(0, x11le32(0x100000)) | |||
| 188 | + | case 136: | |||
| 189 | + | if r.minor == 1 { | |||
| 190 | + | p.reply(0, x11le32(0, 1)) | |||
| 191 | + | } | |||
| 192 | + | case 142: | |||
| 193 | + | if r.minor == 0 { | |||
| 194 | + | p.reply(0, x11le32(0, 4)) | |||
| 195 | + | } | |||
| 196 | + | } | |||
| 197 | + | } | |||
| 198 | + | } | |||
| 199 | + | ||||
| 200 | + | func (p *x11Peer) store(win, prop, typ, format uint32, data []byte) { | |||
| 201 | + | if p.props[win] == nil { | |||
| 202 | + | p.props[win], p.propMeta[win] = map[uint32][]byte{}, map[uint32][2]uint32{} | |||
| 203 | + | } | |||
| 204 | + | p.props[win][prop] = append([]byte(nil), data...) | |||
| 205 | + | p.propMeta[win][prop] = [2]uint32{typ, format} | |||
| 206 | + | } | |||
| 207 | + | ||||
| 208 | + | // atom interns name the way the XWM would have. | |||
| 209 | + | func (p *x11Peer) atom(name string) uint32 { | |||
| 210 | + | p.mu.Lock() | |||
| 211 | + | defer p.mu.Unlock() | |||
| 212 | + | a, ok := p.atoms[name] | |||
| 213 | + | if !ok { | |||
| 214 | + | p.nextAtom++ | |||
| 215 | + | a = p.nextAtom | |||
| 216 | + | p.atoms[name] = a | |||
| 217 | + | } | |||
| 218 | + | return a | |||
| 219 | + | } | |||
| 220 | + | ||||
| 221 | + | // setProp scripts a window property (typ is an atom number, format 8 or 32). | |||
| 222 | + | func (p *x11Peer) setProp(win, prop, typ, format uint32, data []byte) { | |||
| 223 | + | p.mu.Lock() | |||
| 224 | + | defer p.mu.Unlock() | |||
| 225 | + | p.store(win, prop, typ, format, data) | |||
| 226 | + | } | |||
| 227 | + | ||||
| 228 | + | func (p *x11Peer) event(code uint8, fill func(b *[32]byte)) { | |||
| 229 | + | p.mu.Lock() | |||
| 230 | + | defer p.mu.Unlock() | |||
| 231 | + | var b [32]byte | |||
| 232 | + | b[0] = code | |||
| 233 | + | binary.LittleEndian.PutUint16(b[2:], p.seq) | |||
| 234 | + | fill(&b) | |||
| 235 | + | _, _ = p.f.Write(b[:]) | |||
| 236 | + | } | |||
| 237 | + | ||||
| 238 | + | func (p *x11Peer) createNotify(win uint32) { | |||
| 239 | + | p.event(16, func(b *[32]byte) { | |||
| 240 | + | binary.LittleEndian.PutUint32(b[4:], x11PeerRoot) | |||
| 241 | + | binary.LittleEndian.PutUint32(b[8:], win) | |||
| 242 | + | binary.LittleEndian.PutUint16(b[16:], 100) | |||
| 243 | + | binary.LittleEndian.PutUint16(b[18:], 50) | |||
| 244 | + | }) | |||
| 245 | + | } | |||
| 246 | + | ||||
| 247 | + | // createOverride announces an override-redirect window of 100x50 at x,y. | |||
| 248 | + | func (p *x11Peer) createOverride(win uint32, x, y int16) { | |||
| 249 | + | p.event(16, func(b *[32]byte) { | |||
| 250 | + | binary.LittleEndian.PutUint32(b[4:], x11PeerRoot) | |||
| 251 | + | binary.LittleEndian.PutUint32(b[8:], win) | |||
| 252 | + | binary.LittleEndian.PutUint16(b[12:], uint16(x)) | |||
| 253 | + | binary.LittleEndian.PutUint16(b[14:], uint16(y)) | |||
| 254 | + | binary.LittleEndian.PutUint16(b[16:], 100) | |||
| 255 | + | binary.LittleEndian.PutUint16(b[18:], 50) | |||
| 256 | + | b[22] = 1 | |||
| 257 | + | }) | |||
| 258 | + | } | |||
| 259 | + | ||||
| 260 | + | func (p *x11Peer) mapNotify(win uint32) { | |||
| 261 | + | p.event(19, func(b *[32]byte) { | |||
| 262 | + | binary.LittleEndian.PutUint32(b[4:], x11PeerRoot) | |||
| 263 | + | binary.LittleEndian.PutUint32(b[8:], win) | |||
| 264 | + | b[12] = 1 | |||
| 265 | + | }) | |||
| 266 | + | } | |||
| 267 | + | ||||
| 268 | + | // configureNotify moves and resizes a window (root SubstructureNotify form). | |||
| 269 | + | func (p *x11Peer) configureNotify(win uint32, x, y int16, w, h uint16) { | |||
| 270 | + | p.event(22, func(b *[32]byte) { | |||
| 271 | + | binary.LittleEndian.PutUint32(b[4:], x11PeerRoot) | |||
| 272 | + | binary.LittleEndian.PutUint32(b[8:], win) | |||
| 273 | + | binary.LittleEndian.PutUint16(b[16:], uint16(x)) | |||
| 274 | + | binary.LittleEndian.PutUint16(b[18:], uint16(y)) | |||
| 275 | + | binary.LittleEndian.PutUint16(b[20:], w) | |||
| 276 | + | binary.LittleEndian.PutUint16(b[22:], h) | |||
| 277 | + | b[24] = 1 | |||
| 278 | + | }) | |||
| 279 | + | } | |||
| 280 | + | ||||
| 281 | + | func (p *x11Peer) windowEvent(code uint8, win uint32) { | |||
| 282 | + | p.event(code, func(b *[32]byte) { | |||
| 283 | + | binary.LittleEndian.PutUint32(b[4:], x11PeerRoot) | |||
| 284 | + | binary.LittleEndian.PutUint32(b[8:], win) | |||
| 285 | + | }) | |||
| 286 | + | } | |||
| 287 | + | ||||
| 288 | + | func (p *x11Peer) mapRequest(win uint32) { p.windowEvent(20, win) } | |||
| 289 | + | func (p *x11Peer) unmapNotify(win uint32) { p.windowEvent(18, win) } | |||
| 290 | + | func (p *x11Peer) destroyNotify(win uint32) { p.windowEvent(17, win) } | |||
| 291 | + | ||||
| 292 | + | func (p *x11Peer) propertyNotify(win, atom uint32) { | |||
| 293 | + | p.event(28, func(b *[32]byte) { | |||
| 294 | + | binary.LittleEndian.PutUint32(b[4:], win) | |||
| 295 | + | binary.LittleEndian.PutUint32(b[8:], atom) | |||
| 296 | + | }) | |||
| 297 | + | } | |||
| 298 | + | ||||
| 299 | + | func (p *x11Peer) clientMessage(win uint32, typ string, data ...uint32) { | |||
| 300 | + | t := p.atom(typ) | |||
| 301 | + | p.event(33, func(b *[32]byte) { | |||
| 302 | + | b[1] = 32 | |||
| 303 | + | binary.LittleEndian.PutUint32(b[4:], win) | |||
| 304 | + | binary.LittleEndian.PutUint32(b[8:], t) | |||
| 305 | + | for i, d := range data { | |||
| 306 | + | binary.LittleEndian.PutUint32(b[12+4*i:], d) | |||
| 307 | + | } | |||
| 308 | + | }) | |||
| 309 | + | } | |||
| 310 | + | ||||
| 311 | + | func (p *x11Peer) requests() []x11Req { | |||
| 312 | + | p.mu.Lock() | |||
| 313 | + | defer p.mu.Unlock() | |||
| 314 | + | return append([]x11Req(nil), p.reqs...) | |||
| 315 | + | } | |||
| 316 | + | ||||
| 317 | + | // waitReq waits for a request after the first skip ones that satisfies match. | |||
| 318 | + | func (p *x11Peer) waitReq(skip int, match func(x11Req) bool) (x11Req, int) { | |||
| 319 | + | p.t.Helper() | |||
| 320 | + | deadline := time.Now().Add(5 * time.Second) | |||
| 321 | + | for time.Now().Before(deadline) { | |||
| 322 | + | reqs := p.requests() | |||
| 323 | + | for i := skip; i < len(reqs); i++ { | |||
| 324 | + | if match(reqs[i]) { | |||
| 325 | + | return reqs[i], i + 1 | |||
| 326 | + | } | |||
| 327 | + | } | |||
| 328 | + | time.Sleep(time.Millisecond) | |||
| 329 | + | } | |||
| 330 | + | p.t.Fatal("x11 peer: expected request never arrived") | |||
| 331 | + | return x11Req{}, 0 | |||
| 332 | + | } | |||
A internal/adapters/wayland/x11window.go +458 -0
| @@ -0,0 +1,458 @@ | |||
| 1 | + | package wayland | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "time" | |
| 5 | + | ||
| 6 | + | "github.com/bnema/go-wayland-bindings/server/wayland" | |
| 7 | + | "github.com/bnema/go-wayland-bindings/server/xwaylandshell" | |
| 8 | + | "github.com/bnema/neferwl/internal/adapters/wayland/xwm" | |
| 9 | + | "github.com/bnema/neferwl/internal/ports" | |
| 10 | + | "github.com/bnema/purego-libwayland/server" | |
| 11 | + | ) | |
| 12 | + | ||
| 13 | + | // x11Window is the X11 role of a window: the XWM slot, the window's X id and | |
| 14 | + | // the wl_surface Xwayland paired with it. Display goroutine only. | |
| 15 | + | type x11Window struct { | |
| 16 | + | server *Server | |
| 17 | + | slot xwm.Slot | |
| 18 | + | xid uint32 | |
| 19 | + | surface *surface | |
| 20 | + | info xwm.WindowInfo | |
| 21 | + | // ready is set once the XWM read the window's properties (Ready). | |
| 22 | + | ready bool | |
| 23 | + | // override marks an override-redirect window: tracked and drawn, never a | |
| 24 | + | // toplevel nor keyboard focus. rect is its layout rect, noInput keeps the | |
| 25 | + | // pointer off it. | |
| 26 | + | override bool | |
| 27 | + | noInput bool | |
| 28 | + | rect ports.Rect | |
| 29 | + | } | |
| 30 | + | ||
| 31 | + | // xwaylandRestricted are the globals only the Xwayland client may see. | |
| 32 | + | func xwaylandRestricted(iface string) bool { | |
| 33 | + | return iface == "xwayland_shell_v1" || iface == "zwp_xwayland_keyboard_grab_manager_v1" | |
| 34 | + | } | |
| 35 | + | ||
| 36 | + | // globalVisible is the display's global filter: the Xwayland-only globals | |
| 37 | + | // are visible to, and bindable by, the Xwayland client alone. | |
| 38 | + | func (s *Server) globalVisible(c server.Client, g *server.Global) bool { | |
| 39 | + | if g == s.xdgPrivate { | |
| 40 | + | return s.xwayland.client != (server.Client{}) && c == s.xwayland.client | |
| 41 | + | } | |
| 42 | + | if g == s.xdgPublic { | |
| 43 | + | return c != s.xwayland.client | |
| 44 | + | } | |
| 45 | + | if xwaylandRestricted(g.Interface()) { | |
| 46 | + | return s.xwayland.client != (server.Client{}) && c == s.xwayland.client | |
| 47 | + | } | |
| 48 | + | return true | |
| 49 | + | } | |
| 50 | + | ||
| 51 | + | func registerXwaylandShell(d *server.Display, s *Server) error { | |
| 52 | + | return xwaylandshell.NewXwaylandShellV1Global(d, 1, func(c server.Client, v, id uint32) { | |
| 53 | + | _, _ = xwaylandshell.NewXwaylandShellV1(c, int32(v), id, xwaylandShell{s}) | |
| 54 | + | }) | |
| 55 | + | } | |
| 56 | + | ||
| 57 | + | type xwaylandShell struct{ s *Server } | |
| 58 | + | ||
| 59 | + | func (xwaylandShell) Destroy(*xwaylandshell.XwaylandShellV1) {} | |
| 60 | + | ||
| 61 | + | func (h xwaylandShell) GetXwaylandSurface(r *xwaylandshell.XwaylandShellV1, id uint32, w *wayland.Surface) { | |
| 62 | + | surf := h.s.surfaceOf(w) | |
| 63 | + | if surf == nil { | |
| 64 | + | return | |
| 65 | + | } | |
| 66 | + | if surf.kind != roleNone { | |
| 67 | + | r.PostError(uint32(xwaylandshell.XwaylandShellV1ErrorRole), "surface already has a role") | |
| 68 | + | return | |
| 69 | + | } | |
| 70 | + | if _, err := xwaylandshell.NewXwaylandSurfaceV1(r.Client(), r.Version(), id, xwaylandSurface{h.s, surf}); err != nil { | |
| 71 | + | return | |
| 72 | + | } | |
| 73 | + | surf.kind = roleX11 | |
| 74 | + | } | |
| 75 | + | ||
| 76 | + | type xwaylandSurface struct { | |
| 77 | + | s *Server | |
| 78 | + | surf *surface | |
| 79 | + | } | |
| 80 | + | ||
| 81 | + | func (x xwaylandSurface) SetSerial(r *xwaylandshell.XwaylandSurfaceV1, lo, hi uint32) { | |
| 82 | + | serial := uint64(hi)<<32 | uint64(lo) | |
| 83 | + | switch { | |
| 84 | + | case x.surf.x11Serial != 0: | |
| 85 | + | r.PostError(uint32(xwaylandshell.XwaylandSurfaceV1ErrorAlreadyAssociated), "serial already set") | |
| 86 | + | case serial == 0: | |
| 87 | + | r.PostError(uint32(xwaylandshell.XwaylandSurfaceV1ErrorInvalidSerial), "serial is zero") | |
| 88 | + | default: | |
| 89 | + | x.surf.x11Serial = serial | |
| 90 | + | if x.s.xwm != nil { | |
| 91 | + | x.s.xwm.SetSerial(x.surf.identity, serial) | |
| 92 | + | } | |
| 93 | + | } | |
| 94 | + | } | |
| 95 | + | ||
| 96 | + | func (xwaylandSurface) Destroy(*xwaylandshell.XwaylandSurfaceV1) {} | |
| 97 | + | ||
| 98 | + | // installXWM makes x the server's window manager. Display goroutine only. | |
| 99 | + | func (s *Server) installXWM(x *xwm.XWM) { | |
| 100 | + | s.teardownXWM() | |
| 101 | + | s.xwm = x | |
| 102 | + | s.xwmSel = x | |
| 103 | + | x.UseSpace(&s.xspace) | |
| 104 | + | if err := x.Attach(s.display, hostImpl{s}); err != nil { | |
| 105 | + | s.log.Warn().Err(err).Msg("X11 window management disabled") | |
| 106 | + | s.xwm, s.xwmSel = nil, nil | |
| 107 | + | x.CloseConn() | |
| 108 | + | return | |
| 109 | + | } | |
| 110 | + | // Serials sent before the XWM was attached pair now. | |
| 111 | + | for _, surf := range s.surfaces { | |
| 112 | + | if surf.kind == roleX11 && surf.x11Serial != 0 && surf.x11win == nil { | |
| 113 | + | x.SetSerial(surf.identity, surf.x11Serial) | |
| 114 | + | } | |
| 115 | + | } | |
| 116 | + | for kind, src := range s.selections { // what Wayland offered before the XWM attached | |
| 117 | + | if src != nil && src.alive() { | |
| 118 | + | s.exposeToX(kind, src) | |
| 119 | + | } | |
| 120 | + | } | |
| 121 | + | x.LayoutChanged(true) | |
| 122 | + | } | |
| 123 | + | ||
| 124 | + | // teardownXWM closes the window manager, dropping its windows. Idempotent. | |
| 125 | + | func (s *Server) teardownXWM() { | |
| 126 | + | if x := s.xwm; x != nil { | |
| 127 | + | s.xwm, s.xwmSel = nil, nil | |
| 128 | + | x.Close() | |
| 129 | + | } | |
| 130 | + | } | |
| 131 | + | ||
| 132 | + | // x11Configure forwards the latest core configure of an X11 window. | |
| 133 | + | func (s *Server) x11Configure(w *window) { | |
| 134 | + | if s.xwm != nil { | |
| 135 | + | s.xwm.Configure(w.x11.slot, w.last) | |
| 136 | + | } | |
| 137 | + | } | |
| 138 | + | ||
| 139 | + | // x11Close asks an X11 window to close. | |
| 140 | + | func (s *Server) x11Close(w *window) { | |
| 141 | + | if s.xwm != nil { | |
| 142 | + | s.xwm.CloseWindow(w.x11.slot) | |
| 143 | + | } | |
| 144 | + | } | |
| 145 | + | ||
| 146 | + | // x11Place forwards core's position of an X11 window's client rect. | |
| 147 | + | func (s *Server) x11Place(c ports.PlaceWindow) { | |
| 148 | + | if w := s.windows[c.ID]; w != nil && w.x11 != nil && s.xwm != nil { | |
| 149 | + | s.xwm.Place(w.x11.slot, c.X, c.Y) | |
| 150 | + | } | |
| 151 | + | } | |
| 152 | + | ||
| 153 | + | // x11Focus tells the XWM which X window holds the keyboard focus, none when a | |
| 154 | + | // native window does. | |
| 155 | + | func (s *Server) x11Focus() { | |
| 156 | + | if s.xwm == nil { | |
| 157 | + | return | |
| 158 | + | } | |
| 159 | + | var slot xwm.Slot | |
| 160 | + | if w := s.windows[s.seat.focused]; w != nil && w.x11 != nil { | |
| 161 | + | slot = w.x11.slot | |
| 162 | + | } | |
| 163 | + | s.xwm.Focus(slot) | |
| 164 | + | } | |
| 165 | + | ||
| 166 | + | // hostImpl is the server as the XWM sees it. | |
| 167 | + | type hostImpl struct{ s *Server } | |
| 168 | + | ||
| 169 | + | func (h hostImpl) window(slot xwm.Slot) *window { return h.s.x11Slots[slot] } | |
| 170 | + | ||
| 171 | + | func (h hostImpl) Created(slot xwm.Slot, xid uint32) ports.WindowID { | |
| 172 | + | s := h.s | |
| 173 | + | w := &window{id: s.nextWindow} | |
| 174 | + | w.x11 = &x11Window{server: s, slot: slot, xid: xid} | |
| 175 | + | s.nextWindow++ | |
| 176 | + | s.windows[w.id] = w | |
| 177 | + | s.x11Slots[slot] = w | |
| 178 | + | return w.id | |
| 179 | + | } | |
| 180 | + | ||
| 181 | + | func (h hostImpl) Ready(slot xwm.Slot, info xwm.WindowInfo) { | |
| 182 | + | w := h.window(slot) | |
| 183 | + | if w == nil { | |
| 184 | + | return | |
| 185 | + | } | |
| 186 | + | w.x11.info, w.x11.ready = info, true | |
| 187 | + | w.title, w.appID, w.modal = info.Title, info.AppID, info.Modal | |
| 188 | + | w.parent = h.window(info.Parent) | |
| 189 | + | // A surface associated and committed earlier maps now. | |
| 190 | + | w.x11.tryMap() | |
| 191 | + | } | |
| 192 | + | ||
| 193 | + | func (h hostImpl) Associate(slot xwm.Slot, surfaceID uint64) { | |
| 194 | + | w := h.window(slot) | |
| 195 | + | surf := h.s.surfaceByIdentity[surfaceID] | |
| 196 | + | if w == nil || surf == nil || surf.kind != roleX11 { | |
| 197 | + | return | |
| 198 | + | } | |
| 199 | + | x := w.x11 | |
| 200 | + | if x.surface != nil && x.surface != surf { | |
| 201 | + | // A new surface replaces the shown one: core sees a remap. | |
| 202 | + | w.unmap() | |
| 203 | + | } | |
| 204 | + | x.drop() | |
| 205 | + | x.surface, surf.x11win = surf, w | |
| 206 | + | surf.role = func(buffer bool) { | |
| 207 | + | switch { | |
| 208 | + | case surf.destroyed: | |
| 209 | + | w.unmap() | |
| 210 | + | if x.surface == surf { | |
| 211 | + | x.surface = nil | |
| 212 | + | } | |
| 213 | + | surf.x11win = nil | |
| 214 | + | case buffer: | |
| 215 | + | x.tryMap() | |
| 216 | + | case w.mapped: | |
| 217 | + | w.unmap() | |
| 218 | + | } | |
| 219 | + | } | |
| 220 | + | // The buffer may have been committed before the pairing. | |
| 221 | + | x.tryMap() | |
| 222 | + | } | |
| 223 | + | ||
| 224 | + | func (h hostImpl) Dissociate(slot xwm.Slot, destroyed bool) { | |
| 225 | + | w := h.window(slot) | |
| 226 | + | if w == nil { | |
| 227 | + | return | |
| 228 | + | } | |
| 229 | + | w.unmap() | |
| 230 | + | w.x11.drop() | |
| 231 | + | w.x11.ready = false | |
| 232 | + | if destroyed { | |
| 233 | + | for _, c := range h.s.windows { | |
| 234 | + | if c.parent == w { | |
| 235 | + | c.parent = nil | |
| 236 | + | } | |
| 237 | + | } | |
| 238 | + | delete(h.s.windows, w.id) | |
| 239 | + | delete(h.s.x11Slots, slot) | |
| 240 | + | h.s.foreignToplevelGone(w) | |
| 241 | + | } | |
| 242 | + | } | |
| 243 | + | ||
| 244 | + | func (h hostImpl) Title(slot xwm.Slot, title string) { | |
| 245 | + | if w := h.window(slot); w != nil && w.title != title { | |
| 246 | + | w.title = title | |
| 247 | + | h.s.toplevelChanged(w) | |
| 248 | + | if w.mapped { | |
| 249 | + | h.s.castInventoryChanged() | |
| 250 | + | } | |
| 251 | + | } | |
| 252 | + | } | |
| 253 | + | ||
| 254 | + | func (h hostImpl) AppID(slot xwm.Slot, appID string) { | |
| 255 | + | w := h.window(slot) | |
| 256 | + | if w == nil || w.appID == appID { | |
| 257 | + | return | |
| 258 | + | } | |
| 259 | + | w.appID = appID | |
| 260 | + | if w.mapped { | |
| 261 | + | h.s.emit(ports.WindowAppID{ID: w.id, AppID: appID}) | |
| 262 | + | h.s.toplevelChanged(w) | |
| 263 | + | h.s.castInventoryChanged() | |
| 264 | + | } | |
| 265 | + | } | |
| 266 | + | ||
| 267 | + | func (h hostImpl) Parent(slot xwm.Slot, parent xwm.Slot) { | |
| 268 | + | if w := h.window(slot); w != nil { | |
| 269 | + | w.setParent(h.window(parent)) | |
| 270 | + | } | |
| 271 | + | } | |
| 272 | + | ||
| 273 | + | func (h hostImpl) Fullscreen(slot xwm.Slot, on bool) { | |
| 274 | + | w := h.window(slot) | |
| 275 | + | if w == nil { | |
| 276 | + | return | |
| 277 | + | } | |
| 278 | + | w.x11.info.Fullscreen = on | |
| 279 | + | if w.mapped { | |
| 280 | + | h.s.emit(ports.WindowFullscreenRequest{ID: w.id, Fullscreen: on}) | |
| 281 | + | } | |
| 282 | + | } | |
| 283 | + | ||
| 284 | + | // drop unlinks the window from its surface. | |
| 285 | + | func (x *x11Window) drop() { | |
| 286 | + | if surf := x.surface; surf != nil { | |
| 287 | + | surf.x11win, surf.role = nil, nil | |
| 288 | + | x.surface = nil | |
| 289 | + | } | |
| 290 | + | } | |
| 291 | + | ||
| 292 | + | // tryMap maps the window once the XWM knows it and its surface shows a buffer. | |
| 293 | + | func (x *x11Window) tryMap() { | |
| 294 | + | surf := x.surface | |
| 295 | + | if surf == nil { | |
| 296 | + | return | |
| 297 | + | } | |
| 298 | + | w := surf.x11win | |
| 299 | + | if w == nil || w.mapped || !x.ready || surf.destroyed || surf.current == nil { | |
| 300 | + | return | |
| 301 | + | } | |
| 302 | + | s := x.server | |
| 303 | + | w.mapped = true | |
| 304 | + | w.gen++ | |
| 305 | + | w.floating = x.info.Floating | |
| 306 | + | w.floatW, w.floatH = 0, 0 | |
| 307 | + | if w.floating { | |
| 308 | + | w.floatW, w.floatH = x.info.Width, x.info.Height | |
| 309 | + | } | |
| 310 | + | var parent ports.WindowID | |
| 311 | + | if w.parent != nil && w.parent.mapped { | |
| 312 | + | parent = w.parent.id | |
| 313 | + | } | |
| 314 | + | s.emit(ports.WindowMapped{ID: w.id, AppID: w.appID, PID: x.info.PID, Floating: w.floating, Width: w.floatW, Height: w.floatH, Parent: parent, Positioned: true, Modal: w.modal, Remap: w.gen > 1, HasAt: x.info.HasAt, AtX: x.info.AtX, AtY: x.info.AtY, Fullscreen: x.info.Fullscreen}) | |
| 315 | + | s.syncInhibitors() | |
| 316 | + | s.toplevelChanged(w) | |
| 317 | + | s.castInventoryChanged() | |
| 318 | + | s.log.Info().Uint64("id", uint64(w.id)).Uint32("xid", x.xid).Str("app_id", w.appID).Bool("floating", w.floating).Msg("x11 window mapped") | |
| 319 | + | // Called from a commit, the content follows right after; called from the | |
| 320 | + | // pairing, the surface already shows a buffer and is published now. | |
| 321 | + | if surf.has { | |
| 322 | + | surf.resetInputEmission() | |
| 323 | + | surf.redraw() | |
| 324 | + | surf.emitInput() | |
| 325 | + | } | |
| 326 | + | } | |
| 327 | + | ||
| 328 | + | // OverrideMapped starts tracking an override-redirect window. It is announced | |
| 329 | + | // to core on the paired surface's first buffer commit, so it is never drawn | |
| 330 | + | // without content. | |
| 331 | + | func (h hostImpl) OverrideMapped(slot xwm.Slot, xid uint32, surfaceID uint64, x, y, w, h2 int, noInput bool) { | |
| 332 | + | s := h.s | |
| 333 | + | surf := s.surfaceByIdentity[surfaceID] | |
| 334 | + | if surf == nil || surf.kind != roleX11 || surf.x11win != nil { | |
| 335 | + | return | |
| 336 | + | } | |
| 337 | + | if old := s.x11Slots[slot]; old != nil { | |
| 338 | + | h.OverrideGone(slot) | |
| 339 | + | } | |
| 340 | + | win := &window{id: s.nextWindow} | |
| 341 | + | win.x11 = &x11Window{server: s, slot: slot, xid: xid, override: true, noInput: noInput, rect: ports.Rect{X: x, Y: y, W: w, H: h2}} | |
| 342 | + | s.nextWindow++ | |
| 343 | + | s.windows[win.id] = win | |
| 344 | + | s.x11Slots[slot] = win | |
| 345 | + | ox := win.x11 | |
| 346 | + | ox.surface, surf.x11win = surf, win | |
| 347 | + | surf.role = func(buffer bool) { | |
| 348 | + | switch { | |
| 349 | + | case surf.destroyed: | |
| 350 | + | ox.hide() | |
| 351 | + | ox.surface = nil | |
| 352 | + | surf.x11win = nil | |
| 353 | + | case buffer: | |
| 354 | + | ox.show() | |
| 355 | + | case win.mapped: | |
| 356 | + | ox.hide() | |
| 357 | + | } | |
| 358 | + | } | |
| 359 | + | ox.show() | |
| 360 | + | } | |
| 361 | + | ||
| 362 | + | // OverrideMoved records the new layout rect of an override window and, once | |
| 363 | + | // shown, tells core. An unchanged rect costs nothing. | |
| 364 | + | func (h hostImpl) OverrideMoved(slot xwm.Slot, x, y, w, h2 int) { | |
| 365 | + | win := h.s.x11Slots[slot] | |
| 366 | + | if win == nil || win.x11 == nil || !win.x11.override { | |
| 367 | + | return | |
| 368 | + | } | |
| 369 | + | r := ports.Rect{X: x, Y: y, W: w, H: h2} | |
| 370 | + | if r == win.x11.rect { | |
| 371 | + | return | |
| 372 | + | } | |
| 373 | + | win.x11.rect = r | |
| 374 | + | if win.mapped { | |
| 375 | + | h.s.emit(ports.UnmanagedMoved{ID: win.id, X: x, Y: y, Width: w, Height: h2}) | |
| 376 | + | win.x11.surface.sendTreeScale() | |
| 377 | + | } | |
| 378 | + | } | |
| 379 | + | ||
| 380 | + | // OverrideGone ends an override window: unmapped, or destroyed. | |
| 381 | + | func (h hostImpl) SelectionOwned(kind int, mimes []string) { | |
| 382 | + | h.s.log.Debug().Int("kind", kind).Int("mimes", len(mimes)).Msg("X selection owner changed") | |
| 383 | + | h.s.setXSelection(kind, mimes) | |
| 384 | + | } | |
| 385 | + | ||
| 386 | + | func (h hostImpl) SelectionRead(kind int, mime string) int { | |
| 387 | + | return h.s.xSelectionRead(kind, mime) | |
| 388 | + | } | |
| 389 | + | ||
| 390 | + | func (h hostImpl) After(d time.Duration, fn func()) func() { | |
| 391 | + | t := h.s.clock.AfterFunc(d, func() { h.s.display.Post(fn) }) | |
| 392 | + | return func() { t.Stop() } | |
| 393 | + | } | |
| 394 | + | ||
| 395 | + | func (h hostImpl) OverrideGone(slot xwm.Slot) { | |
| 396 | + | s := h.s | |
| 397 | + | win := s.x11Slots[slot] | |
| 398 | + | if win == nil || win.x11 == nil || !win.x11.override { | |
| 399 | + | return | |
| 400 | + | } | |
| 401 | + | win.x11.hide() | |
| 402 | + | win.x11.drop() | |
| 403 | + | delete(s.windows, win.id) | |
| 404 | + | delete(s.x11Slots, slot) | |
| 405 | + | } | |
| 406 | + | ||
| 407 | + | // show announces the window to core once its surface has content. | |
| 408 | + | func (x *x11Window) show() { | |
| 409 | + | surf := x.surface | |
| 410 | + | if surf == nil || surf.x11win == nil || surf.destroyed || surf.current == nil { | |
| 411 | + | return | |
| 412 | + | } | |
| 413 | + | w := surf.x11win | |
| 414 | + | if w.mapped { | |
| 415 | + | return | |
| 416 | + | } | |
| 417 | + | s := x.server | |
| 418 | + | w.mapped = true | |
| 419 | + | r := x.rect | |
| 420 | + | s.emit(ports.UnmanagedMapped{ID: w.id, X: r.X, Y: r.Y, Width: r.W, Height: r.H, NoInput: x.noInput}) | |
| 421 | + | surf.sendTreeScale() | |
| 422 | + | if surf.has { | |
| 423 | + | surf.resetInputEmission() | |
| 424 | + | surf.redraw() | |
| 425 | + | surf.emitInput() | |
| 426 | + | } | |
| 427 | + | } | |
| 428 | + | ||
| 429 | + | // hide withdraws a shown override window from core and the seat. | |
| 430 | + | func (x *x11Window) hide() { | |
| 431 | + | if x.surface == nil || x.surface.x11win == nil { | |
| 432 | + | return | |
| 433 | + | } | |
| 434 | + | w := x.surface.x11win | |
| 435 | + | s := x.server | |
| 436 | + | if s.seat.pointerFocus == w.id { | |
| 437 | + | s.changePointerFocus(0, 0, 0) | |
| 438 | + | } | |
| 439 | + | if !w.mapped { | |
| 440 | + | return | |
| 441 | + | } | |
| 442 | + | w.mapped = false | |
| 443 | + | x.surface.resetInputEmission() | |
| 444 | + | s.emit(ports.UnmanagedUnmapped{ID: w.id}) | |
| 445 | + | s.emitContent(ports.SurfaceContent{ID: w.id}, damage{full: true}) | |
| 446 | + | } | |
| 447 | + | ||
| 448 | + | // isOverride reports whether w is an override-redirect X11 window. | |
| 449 | + | func (w *window) isOverride() bool { return w.x11 != nil && w.x11.override } | |
| 450 | + | ||
| 451 | + | // overrideOutput is the name of the output under the origin of an override | |
| 452 | + | // window's rect, "" when none. | |
| 453 | + | func (s *Server) overrideOutput(w *window) string { | |
| 454 | + | if o, ok := s.outputPlaces.At(float64(w.x11.rect.X), float64(w.x11.rect.Y)); ok { | |
| 455 | + | return o.Info.Name | |
| 456 | + | } | |
| 457 | + | return "" | |
| 458 | + | } | |
| @@ -0,0 +1,458 @@ | |||||
| 1 | + | package wayland | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "time" | |||
| 5 | + | ||||
| 6 | + | "github.com/bnema/go-wayland-bindings/server/wayland" | |||
| 7 | + | "github.com/bnema/go-wayland-bindings/server/xwaylandshell" | |||
| 8 | + | "github.com/bnema/neferwl/internal/adapters/wayland/xwm" | |||
| 9 | + | "github.com/bnema/neferwl/internal/ports" | |||
| 10 | + | "github.com/bnema/purego-libwayland/server" | |||
| 11 | + | ) | |||
| 12 | + | ||||
| 13 | + | // x11Window is the X11 role of a window: the XWM slot, the window's X id and | |||
| 14 | + | // the wl_surface Xwayland paired with it. Display goroutine only. | |||
| 15 | + | type x11Window struct { | |||
| 16 | + | server *Server | |||
| 17 | + | slot xwm.Slot | |||
| 18 | + | xid uint32 | |||
| 19 | + | surface *surface | |||
| 20 | + | info xwm.WindowInfo | |||
| 21 | + | // ready is set once the XWM read the window's properties (Ready). | |||
| 22 | + | ready bool | |||
| 23 | + | // override marks an override-redirect window: tracked and drawn, never a | |||
| 24 | + | // toplevel nor keyboard focus. rect is its layout rect, noInput keeps the | |||
| 25 | + | // pointer off it. | |||
| 26 | + | override bool | |||
| 27 | + | noInput bool | |||
| 28 | + | rect ports.Rect | |||
| 29 | + | } | |||
| 30 | + | ||||
| 31 | + | // xwaylandRestricted are the globals only the Xwayland client may see. | |||
| 32 | + | func xwaylandRestricted(iface string) bool { | |||
| 33 | + | return iface == "xwayland_shell_v1" || iface == "zwp_xwayland_keyboard_grab_manager_v1" | |||
| 34 | + | } | |||
| 35 | + | ||||
| 36 | + | // globalVisible is the display's global filter: the Xwayland-only globals | |||
| 37 | + | // are visible to, and bindable by, the Xwayland client alone. | |||
| 38 | + | func (s *Server) globalVisible(c server.Client, g *server.Global) bool { | |||
| 39 | + | if g == s.xdgPrivate { | |||
| 40 | + | return s.xwayland.client != (server.Client{}) && c == s.xwayland.client | |||
| 41 | + | } | |||
| 42 | + | if g == s.xdgPublic { | |||
| 43 | + | return c != s.xwayland.client | |||
| 44 | + | } | |||
| 45 | + | if xwaylandRestricted(g.Interface()) { | |||
| 46 | + | return s.xwayland.client != (server.Client{}) && c == s.xwayland.client | |||
| 47 | + | } | |||
| 48 | + | return true | |||
| 49 | + | } | |||
| 50 | + | ||||
| 51 | + | func registerXwaylandShell(d *server.Display, s *Server) error { | |||
| 52 | + | return xwaylandshell.NewXwaylandShellV1Global(d, 1, func(c server.Client, v, id uint32) { | |||
| 53 | + | _, _ = xwaylandshell.NewXwaylandShellV1(c, int32(v), id, xwaylandShell{s}) | |||
| 54 | + | }) | |||
| 55 | + | } | |||
| 56 | + | ||||
| 57 | + | type xwaylandShell struct{ s *Server } | |||
| 58 | + | ||||
| 59 | + | func (xwaylandShell) Destroy(*xwaylandshell.XwaylandShellV1) {} | |||
| 60 | + | ||||
| 61 | + | func (h xwaylandShell) GetXwaylandSurface(r *xwaylandshell.XwaylandShellV1, id uint32, w *wayland.Surface) { | |||
| 62 | + | surf := h.s.surfaceOf(w) | |||
| 63 | + | if surf == nil { | |||
| 64 | + | return | |||
| 65 | + | } | |||
| 66 | + | if surf.kind != roleNone { | |||
| 67 | + | r.PostError(uint32(xwaylandshell.XwaylandShellV1ErrorRole), "surface already has a role") | |||
| 68 | + | return | |||
| 69 | + | } | |||
| 70 | + | if _, err := xwaylandshell.NewXwaylandSurfaceV1(r.Client(), r.Version(), id, xwaylandSurface{h.s, surf}); err != nil { | |||
| 71 | + | return | |||
| 72 | + | } | |||
| 73 | + | surf.kind = roleX11 | |||
| 74 | + | } | |||
| 75 | + | ||||
| 76 | + | type xwaylandSurface struct { | |||
| 77 | + | s *Server | |||
| 78 | + | surf *surface | |||
| 79 | + | } | |||
| 80 | + | ||||
| 81 | + | func (x xwaylandSurface) SetSerial(r *xwaylandshell.XwaylandSurfaceV1, lo, hi uint32) { | |||
| 82 | + | serial := uint64(hi)<<32 | uint64(lo) | |||
| 83 | + | switch { | |||
| 84 | + | case x.surf.x11Serial != 0: | |||
| 85 | + | r.PostError(uint32(xwaylandshell.XwaylandSurfaceV1ErrorAlreadyAssociated), "serial already set") | |||
| 86 | + | case serial == 0: | |||
| 87 | + | r.PostError(uint32(xwaylandshell.XwaylandSurfaceV1ErrorInvalidSerial), "serial is zero") | |||
| 88 | + | default: | |||
| 89 | + | x.surf.x11Serial = serial | |||
| 90 | + | if x.s.xwm != nil { | |||
| 91 | + | x.s.xwm.SetSerial(x.surf.identity, serial) | |||
| 92 | + | } | |||
| 93 | + | } | |||
| 94 | + | } | |||
| 95 | + | ||||
| 96 | + | func (xwaylandSurface) Destroy(*xwaylandshell.XwaylandSurfaceV1) {} | |||
| 97 | + | ||||
| 98 | + | // installXWM makes x the server's window manager. Display goroutine only. | |||
| 99 | + | func (s *Server) installXWM(x *xwm.XWM) { | |||
| 100 | + | s.teardownXWM() | |||
| 101 | + | s.xwm = x | |||
| 102 | + | s.xwmSel = x | |||
| 103 | + | x.UseSpace(&s.xspace) | |||
| 104 | + | if err := x.Attach(s.display, hostImpl{s}); err != nil { | |||
| 105 | + | s.log.Warn().Err(err).Msg("X11 window management disabled") | |||
| 106 | + | s.xwm, s.xwmSel = nil, nil | |||
| 107 | + | x.CloseConn() | |||
| 108 | + | return | |||
| 109 | + | } | |||
| 110 | + | // Serials sent before the XWM was attached pair now. | |||
| 111 | + | for _, surf := range s.surfaces { | |||
| 112 | + | if surf.kind == roleX11 && surf.x11Serial != 0 && surf.x11win == nil { | |||
| 113 | + | x.SetSerial(surf.identity, surf.x11Serial) | |||
| 114 | + | } | |||
| 115 | + | } | |||
| 116 | + | for kind, src := range s.selections { // what Wayland offered before the XWM attached | |||
| 117 | + | if src != nil && src.alive() { | |||
| 118 | + | s.exposeToX(kind, src) | |||
| 119 | + | } | |||
| 120 | + | } | |||
| 121 | + | x.LayoutChanged(true) | |||
| 122 | + | } | |||
| 123 | + | ||||
| 124 | + | // teardownXWM closes the window manager, dropping its windows. Idempotent. | |||
| 125 | + | func (s *Server) teardownXWM() { | |||
| 126 | + | if x := s.xwm; x != nil { | |||
| 127 | + | s.xwm, s.xwmSel = nil, nil | |||
| 128 | + | x.Close() | |||
| 129 | + | } | |||
| 130 | + | } | |||
| 131 | + | ||||
| 132 | + | // x11Configure forwards the latest core configure of an X11 window. | |||
| 133 | + | func (s *Server) x11Configure(w *window) { | |||
| 134 | + | if s.xwm != nil { | |||
| 135 | + | s.xwm.Configure(w.x11.slot, w.last) | |||
| 136 | + | } | |||
| 137 | + | } | |||
| 138 | + | ||||
| 139 | + | // x11Close asks an X11 window to close. | |||
| 140 | + | func (s *Server) x11Close(w *window) { | |||
| 141 | + | if s.xwm != nil { | |||
| 142 | + | s.xwm.CloseWindow(w.x11.slot) | |||
| 143 | + | } | |||
| 144 | + | } | |||
| 145 | + | ||||
| 146 | + | // x11Place forwards core's position of an X11 window's client rect. | |||
| 147 | + | func (s *Server) x11Place(c ports.PlaceWindow) { | |||
| 148 | + | if w := s.windows[c.ID]; w != nil && w.x11 != nil && s.xwm != nil { | |||
| 149 | + | s.xwm.Place(w.x11.slot, c.X, c.Y) | |||
| 150 | + | } | |||
| 151 | + | } | |||
| 152 | + | ||||
| 153 | + | // x11Focus tells the XWM which X window holds the keyboard focus, none when a | |||
| 154 | + | // native window does. | |||
| 155 | + | func (s *Server) x11Focus() { | |||
| 156 | + | if s.xwm == nil { | |||
| 157 | + | return | |||
| 158 | + | } | |||
| 159 | + | var slot xwm.Slot | |||
| 160 | + | if w := s.windows[s.seat.focused]; w != nil && w.x11 != nil { | |||
| 161 | + | slot = w.x11.slot | |||
| 162 | + | } | |||
| 163 | + | s.xwm.Focus(slot) | |||
| 164 | + | } | |||
| 165 | + | ||||
| 166 | + | // hostImpl is the server as the XWM sees it. | |||
| 167 | + | type hostImpl struct{ s *Server } | |||
| 168 | + | ||||
| 169 | + | func (h hostImpl) window(slot xwm.Slot) *window { return h.s.x11Slots[slot] } | |||
| 170 | + | ||||
| 171 | + | func (h hostImpl) Created(slot xwm.Slot, xid uint32) ports.WindowID { | |||
| 172 | + | s := h.s | |||
| 173 | + | w := &window{id: s.nextWindow} | |||
| 174 | + | w.x11 = &x11Window{server: s, slot: slot, xid: xid} | |||
| 175 | + | s.nextWindow++ | |||
| 176 | + | s.windows[w.id] = w | |||
| 177 | + | s.x11Slots[slot] = w | |||
| 178 | + | return w.id | |||
| 179 | + | } | |||
| 180 | + | ||||
| 181 | + | func (h hostImpl) Ready(slot xwm.Slot, info xwm.WindowInfo) { | |||
| 182 | + | w := h.window(slot) | |||
| 183 | + | if w == nil { | |||
| 184 | + | return | |||
| 185 | + | } | |||
| 186 | + | w.x11.info, w.x11.ready = info, true | |||
| 187 | + | w.title, w.appID, w.modal = info.Title, info.AppID, info.Modal | |||
| 188 | + | w.parent = h.window(info.Parent) | |||
| 189 | + | // A surface associated and committed earlier maps now. | |||
| 190 | + | w.x11.tryMap() | |||
| 191 | + | } | |||
| 192 | + | ||||
| 193 | + | func (h hostImpl) Associate(slot xwm.Slot, surfaceID uint64) { | |||
| 194 | + | w := h.window(slot) | |||
| 195 | + | surf := h.s.surfaceByIdentity[surfaceID] | |||
| 196 | + | if w == nil || surf == nil || surf.kind != roleX11 { | |||
| 197 | + | return | |||
| 198 | + | } | |||
| 199 | + | x := w.x11 | |||
| 200 | + | if x.surface != nil && x.surface != surf { | |||
| 201 | + | // A new surface replaces the shown one: core sees a remap. | |||
| 202 | + | w.unmap() | |||
| 203 | + | } | |||
| 204 | + | x.drop() | |||
| 205 | + | x.surface, surf.x11win = surf, w | |||
| 206 | + | surf.role = func(buffer bool) { | |||
| 207 | + | switch { | |||
| 208 | + | case surf.destroyed: | |||
| 209 | + | w.unmap() | |||
| 210 | + | if x.surface == surf { | |||
| 211 | + | x.surface = nil | |||
| 212 | + | } | |||
| 213 | + | surf.x11win = nil | |||
| 214 | + | case buffer: | |||
| 215 | + | x.tryMap() | |||
| 216 | + | case w.mapped: | |||
| 217 | + | w.unmap() | |||
| 218 | + | } | |||
| 219 | + | } | |||
| 220 | + | // The buffer may have been committed before the pairing. | |||
| 221 | + | x.tryMap() | |||
| 222 | + | } | |||
| 223 | + | ||||
| 224 | + | func (h hostImpl) Dissociate(slot xwm.Slot, destroyed bool) { | |||
| 225 | + | w := h.window(slot) | |||
| 226 | + | if w == nil { | |||
| 227 | + | return | |||
| 228 | + | } | |||
| 229 | + | w.unmap() | |||
| 230 | + | w.x11.drop() | |||
| 231 | + | w.x11.ready = false | |||
| 232 | + | if destroyed { | |||
| 233 | + | for _, c := range h.s.windows { | |||
| 234 | + | if c.parent == w { | |||
| 235 | + | c.parent = nil | |||
| 236 | + | } | |||
| 237 | + | } | |||
| 238 | + | delete(h.s.windows, w.id) | |||
| 239 | + | delete(h.s.x11Slots, slot) | |||
| 240 | + | h.s.foreignToplevelGone(w) | |||
| 241 | + | } | |||
| 242 | + | } | |||
| 243 | + | ||||
| 244 | + | func (h hostImpl) Title(slot xwm.Slot, title string) { | |||
| 245 | + | if w := h.window(slot); w != nil && w.title != title { | |||
| 246 | + | w.title = title | |||
| 247 | + | h.s.toplevelChanged(w) | |||
| 248 | + | if w.mapped { | |||
| 249 | + | h.s.castInventoryChanged() | |||
| 250 | + | } | |||
| 251 | + | } | |||
| 252 | + | } | |||
| 253 | + | ||||
| 254 | + | func (h hostImpl) AppID(slot xwm.Slot, appID string) { | |||
| 255 | + | w := h.window(slot) | |||
| 256 | + | if w == nil || w.appID == appID { | |||
| 257 | + | return | |||
| 258 | + | } | |||
| 259 | + | w.appID = appID | |||
| 260 | + | if w.mapped { | |||
| 261 | + | h.s.emit(ports.WindowAppID{ID: w.id, AppID: appID}) | |||
| 262 | + | h.s.toplevelChanged(w) | |||
| 263 | + | h.s.castInventoryChanged() | |||
| 264 | + | } | |||
| 265 | + | } | |||
| 266 | + | ||||
| 267 | + | func (h hostImpl) Parent(slot xwm.Slot, parent xwm.Slot) { | |||
| 268 | + | if w := h.window(slot); w != nil { | |||
| 269 | + | w.setParent(h.window(parent)) | |||
| 270 | + | } | |||
| 271 | + | } | |||
| 272 | + | ||||
| 273 | + | func (h hostImpl) Fullscreen(slot xwm.Slot, on bool) { | |||
| 274 | + | w := h.window(slot) | |||
| 275 | + | if w == nil { | |||
| 276 | + | return | |||
| 277 | + | } | |||
| 278 | + | w.x11.info.Fullscreen = on | |||
| 279 | + | if w.mapped { | |||
| 280 | + | h.s.emit(ports.WindowFullscreenRequest{ID: w.id, Fullscreen: on}) | |||
| 281 | + | } | |||
| 282 | + | } | |||
| 283 | + | ||||
| 284 | + | // drop unlinks the window from its surface. | |||
| 285 | + | func (x *x11Window) drop() { | |||
| 286 | + | if surf := x.surface; surf != nil { | |||
| 287 | + | surf.x11win, surf.role = nil, nil | |||
| 288 | + | x.surface = nil | |||
| 289 | + | } | |||
| 290 | + | } | |||
| 291 | + | ||||
| 292 | + | // tryMap maps the window once the XWM knows it and its surface shows a buffer. | |||
| 293 | + | func (x *x11Window) tryMap() { | |||
| 294 | + | surf := x.surface | |||
| 295 | + | if surf == nil { | |||
| 296 | + | return | |||
| 297 | + | } | |||
| 298 | + | w := surf.x11win | |||
| 299 | + | if w == nil || w.mapped || !x.ready || surf.destroyed || surf.current == nil { | |||
| 300 | + | return | |||
| 301 | + | } | |||
| 302 | + | s := x.server | |||
| 303 | + | w.mapped = true | |||
| 304 | + | w.gen++ | |||
| 305 | + | w.floating = x.info.Floating | |||
| 306 | + | w.floatW, w.floatH = 0, 0 | |||
| 307 | + | if w.floating { | |||
| 308 | + | w.floatW, w.floatH = x.info.Width, x.info.Height | |||
| 309 | + | } | |||
| 310 | + | var parent ports.WindowID | |||
| 311 | + | if w.parent != nil && w.parent.mapped { | |||
| 312 | + | parent = w.parent.id | |||
| 313 | + | } | |||
| 314 | + | s.emit(ports.WindowMapped{ID: w.id, AppID: w.appID, PID: x.info.PID, Floating: w.floating, Width: w.floatW, Height: w.floatH, Parent: parent, Positioned: true, Modal: w.modal, Remap: w.gen > 1, HasAt: x.info.HasAt, AtX: x.info.AtX, AtY: x.info.AtY, Fullscreen: x.info.Fullscreen}) | |||
| 315 | + | s.syncInhibitors() | |||
| 316 | + | s.toplevelChanged(w) | |||
| 317 | + | s.castInventoryChanged() | |||
| 318 | + | s.log.Info().Uint64("id", uint64(w.id)).Uint32("xid", x.xid).Str("app_id", w.appID).Bool("floating", w.floating).Msg("x11 window mapped") | |||
| 319 | + | // Called from a commit, the content follows right after; called from the | |||
| 320 | + | // pairing, the surface already shows a buffer and is published now. | |||
| 321 | + | if surf.has { | |||
| 322 | + | surf.resetInputEmission() | |||
| 323 | + | surf.redraw() | |||
| 324 | + | surf.emitInput() | |||
| 325 | + | } | |||
| 326 | + | } | |||
| 327 | + | ||||
| 328 | + | // OverrideMapped starts tracking an override-redirect window. It is announced | |||
| 329 | + | // to core on the paired surface's first buffer commit, so it is never drawn | |||
| 330 | + | // without content. | |||
| 331 | + | func (h hostImpl) OverrideMapped(slot xwm.Slot, xid uint32, surfaceID uint64, x, y, w, h2 int, noInput bool) { | |||
| 332 | + | s := h.s | |||
| 333 | + | surf := s.surfaceByIdentity[surfaceID] | |||
| 334 | + | if surf == nil || surf.kind != roleX11 || surf.x11win != nil { | |||
| 335 | + | return | |||
| 336 | + | } | |||
| 337 | + | if old := s.x11Slots[slot]; old != nil { | |||
| 338 | + | h.OverrideGone(slot) | |||
| 339 | + | } | |||
| 340 | + | win := &window{id: s.nextWindow} | |||
| 341 | + | win.x11 = &x11Window{server: s, slot: slot, xid: xid, override: true, noInput: noInput, rect: ports.Rect{X: x, Y: y, W: w, H: h2}} | |||
| 342 | + | s.nextWindow++ | |||
| 343 | + | s.windows[win.id] = win | |||
| 344 | + | s.x11Slots[slot] = win | |||
| 345 | + | ox := win.x11 | |||
| 346 | + | ox.surface, surf.x11win = surf, win | |||
| 347 | + | surf.role = func(buffer bool) { | |||
| 348 | + | switch { | |||
| 349 | + | case surf.destroyed: | |||
| 350 | + | ox.hide() | |||
| 351 | + | ox.surface = nil | |||
| 352 | + | surf.x11win = nil | |||
| 353 | + | case buffer: | |||
| 354 | + | ox.show() | |||
| 355 | + | case win.mapped: | |||
| 356 | + | ox.hide() | |||
| 357 | + | } | |||
| 358 | + | } | |||
| 359 | + | ox.show() | |||
| 360 | + | } | |||
| 361 | + | ||||
| 362 | + | // OverrideMoved records the new layout rect of an override window and, once | |||
| 363 | + | // shown, tells core. An unchanged rect costs nothing. | |||
| 364 | + | func (h hostImpl) OverrideMoved(slot xwm.Slot, x, y, w, h2 int) { | |||
| 365 | + | win := h.s.x11Slots[slot] | |||
| 366 | + | if win == nil || win.x11 == nil || !win.x11.override { | |||
| 367 | + | return | |||
| 368 | + | } | |||
| 369 | + | r := ports.Rect{X: x, Y: y, W: w, H: h2} | |||
| 370 | + | if r == win.x11.rect { | |||
| 371 | + | return | |||
| 372 | + | } | |||
| 373 | + | win.x11.rect = r | |||
| 374 | + | if win.mapped { | |||
| 375 | + | h.s.emit(ports.UnmanagedMoved{ID: win.id, X: x, Y: y, Width: w, Height: h2}) | |||
| 376 | + | win.x11.surface.sendTreeScale() | |||
| 377 | + | } | |||
| 378 | + | } | |||
| 379 | + | ||||
| 380 | + | // OverrideGone ends an override window: unmapped, or destroyed. | |||
| 381 | + | func (h hostImpl) SelectionOwned(kind int, mimes []string) { | |||
| 382 | + | h.s.log.Debug().Int("kind", kind).Int("mimes", len(mimes)).Msg("X selection owner changed") | |||
| 383 | + | h.s.setXSelection(kind, mimes) | |||
| 384 | + | } | |||
| 385 | + | ||||
| 386 | + | func (h hostImpl) SelectionRead(kind int, mime string) int { | |||
| 387 | + | return h.s.xSelectionRead(kind, mime) | |||
| 388 | + | } | |||
| 389 | + | ||||
| 390 | + | func (h hostImpl) After(d time.Duration, fn func()) func() { | |||
| 391 | + | t := h.s.clock.AfterFunc(d, func() { h.s.display.Post(fn) }) | |||
| 392 | + | return func() { t.Stop() } | |||
| 393 | + | } | |||
| 394 | + | ||||
| 395 | + | func (h hostImpl) OverrideGone(slot xwm.Slot) { | |||
| 396 | + | s := h.s | |||
| 397 | + | win := s.x11Slots[slot] | |||
| 398 | + | if win == nil || win.x11 == nil || !win.x11.override { | |||
| 399 | + | return | |||
| 400 | + | } | |||
| 401 | + | win.x11.hide() | |||
| 402 | + | win.x11.drop() | |||
| 403 | + | delete(s.windows, win.id) | |||
| 404 | + | delete(s.x11Slots, slot) | |||
| 405 | + | } | |||
| 406 | + | ||||
| 407 | + | // show announces the window to core once its surface has content. | |||
| 408 | + | func (x *x11Window) show() { | |||
| 409 | + | surf := x.surface | |||
| 410 | + | if surf == nil || surf.x11win == nil || surf.destroyed || surf.current == nil { | |||
| 411 | + | return | |||
| 412 | + | } | |||
| 413 | + | w := surf.x11win | |||
| 414 | + | if w.mapped { | |||
| 415 | + | return | |||
| 416 | + | } | |||
| 417 | + | s := x.server | |||
| 418 | + | w.mapped = true | |||
| 419 | + | r := x.rect | |||
| 420 | + | s.emit(ports.UnmanagedMapped{ID: w.id, X: r.X, Y: r.Y, Width: r.W, Height: r.H, NoInput: x.noInput}) | |||
| 421 | + | surf.sendTreeScale() | |||
| 422 | + | if surf.has { | |||
| 423 | + | surf.resetInputEmission() | |||
| 424 | + | surf.redraw() | |||
| 425 | + | surf.emitInput() | |||
| 426 | + | } | |||
| 427 | + | } | |||
| 428 | + | ||||
| 429 | + | // hide withdraws a shown override window from core and the seat. | |||
| 430 | + | func (x *x11Window) hide() { | |||
| 431 | + | if x.surface == nil || x.surface.x11win == nil { | |||
| 432 | + | return | |||
| 433 | + | } | |||
| 434 | + | w := x.surface.x11win | |||
| 435 | + | s := x.server | |||
| 436 | + | if s.seat.pointerFocus == w.id { | |||
| 437 | + | s.changePointerFocus(0, 0, 0) | |||
| 438 | + | } | |||
| 439 | + | if !w.mapped { | |||
| 440 | + | return | |||
| 441 | + | } | |||
| 442 | + | w.mapped = false | |||
| 443 | + | x.surface.resetInputEmission() | |||
| 444 | + | s.emit(ports.UnmanagedUnmapped{ID: w.id}) | |||
| 445 | + | s.emitContent(ports.SurfaceContent{ID: w.id}, damage{full: true}) | |||
| 446 | + | } | |||
| 447 | + | ||||
| 448 | + | // isOverride reports whether w is an override-redirect X11 window. | |||
| 449 | + | func (w *window) isOverride() bool { return w.x11 != nil && w.x11.override } | |||
| 450 | + | ||||
| 451 | + | // overrideOutput is the name of the output under the origin of an override | |||
| 452 | + | // window's rect, "" when none. | |||
| 453 | + | func (s *Server) overrideOutput(w *window) string { | |||
| 454 | + | if o, ok := s.outputPlaces.At(float64(w.x11.rect.X), float64(w.x11.rect.Y)); ok { | |||
| 455 | + | return o.Info.Name | |||
| 456 | + | } | |||
| 457 | + | return "" | |||
| 458 | + | } | |||
A internal/adapters/wayland/x11window_test.go +627 -0
| @@ -0,0 +1,627 @@ | |||
| 1 | + | package wayland | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "encoding/binary" | |
| 6 | + | "net" | |
| 7 | + | "os" | |
| 8 | + | "testing" | |
| 9 | + | "time" | |
| 10 | + | ||
| 11 | + | "github.com/bnema/go-wayland-bindings/server/pointerconstraints" | |
| 12 | + | "github.com/bnema/go-wayland-bindings/server/relativepointer" | |
| 13 | + | "github.com/bnema/go-wayland-bindings/server/wayland" | |
| 14 | + | "github.com/bnema/go-wayland-bindings/server/xdgshell" | |
| 15 | + | "github.com/bnema/go-wayland-bindings/server/xwaylandkeyboardgrab" | |
| 16 | + | "github.com/bnema/go-wayland-bindings/server/xwaylandshell" | |
| 17 | + | "github.com/bnema/neferwl/internal/ports" | |
| 18 | + | "github.com/bnema/wlturbo" | |
| 19 | + | "golang.org/x/sys/unix" | |
| 20 | + | ) | |
| 21 | + | ||
| 22 | + | var x11Serial = uint64(0x1234_0000_0042) | |
| 23 | + | ||
| 24 | + | // x11Rig is a server with an Xwayland client (a real wlturbo client on the | |
| 25 | + | // adopted descriptor), a scripted X peer on the WM descriptor and the XWM | |
| 26 | + | // between them. | |
| 27 | + | type x11Rig struct { | |
| 28 | + | t *testing.T | |
| 29 | + | s *Server | |
| 30 | + | events chan ports.ClientEvent | |
| 31 | + | commands chan ports.ClientCommand | |
| 32 | + | dir string | |
| 33 | + | xc *wlturbo.Display | |
| 34 | + | peer *x11Peer | |
| 35 | + | stop func() | |
| 36 | + | shell uint32 | |
| 37 | + | comp uint32 | |
| 38 | + | // surfaceProxy, when set, observes the next paired wl_surface. | |
| 39 | + | surfaceProxy func(id uint32) wlturbo.Proxy | |
| 40 | + | } | |
| 41 | + | ||
| 42 | + | func newX11Rig(t *testing.T) *x11Rig { | |
| 43 | + | t.Helper() | |
| 44 | + | s, events, commands, dir := lifecycleServer(t) | |
| 45 | + | r := &x11Rig{t: t, s: s, events: events, commands: commands, dir: dir} | |
| 46 | + | wl := xwaylandPair(t) | |
| 47 | + | s.AttachXwayland(wl[0]) | |
| 48 | + | f := os.NewFile(uintptr(wl[1]), "xwayland client") | |
| 49 | + | wl[1] = -1 | |
| 50 | + | conn, err := net.FileConn(f) | |
| 51 | + | f.Close() | |
| 52 | + | if err != nil { | |
| 53 | + | t.Fatal(err) | |
| 54 | + | } | |
| 55 | + | if r.xc, err = wlturbo.ConnectFromConn(conn); err != nil { | |
| 56 | + | t.Fatal(err) | |
| 57 | + | } | |
| 58 | + | t.Cleanup(func() { _ = r.xc.Close(); forgetWireClient(r.xc) }) | |
| 59 | + | if err := r.xc.Roundtrip(); err != nil { | |
| 60 | + | t.Fatal(err) | |
| 61 | + | } | |
| 62 | + | wm, peer := startX11Peer(t) | |
| 63 | + | r.peer = peer | |
| 64 | + | r.stop = s.AttachXWM(context.Background(), wm) | |
| 65 | + | t.Cleanup(func() { r.stop() }) | |
| 66 | + | r.shell = bindProtocol(t, r.xc, "xwayland_shell_v1") | |
| 67 | + | r.comp = bindProtocol(t, r.xc, "wl_compositor") | |
| 68 | + | return r | |
| 69 | + | } | |
| 70 | + | ||
| 71 | + | // x11Window announces and maps X window win with the properties of an | |
| 72 | + | // ordinary client, and waits for the XWM to be ready for it. | |
| 73 | + | func (r *x11Rig) x11Window(win uint32) { | |
| 74 | + | r.t.Helper() | |
| 75 | + | p := r.peer | |
| 76 | + | str := uint32(31) // XA_STRING | |
| 77 | + | p.setProp(win, 67, str, 8, []byte("inst\x00Class\x00")) // WM_CLASS | |
| 78 | + | p.setProp(win, p.atom("_NET_WM_NAME"), p.atom("UTF8_STRING"), 8, []byte("net title")) | |
| 79 | + | p.setProp(win, p.atom("_NET_WM_PID"), 6, 32, x11le32(4242)) | |
| 80 | + | p.createNotify(win) | |
| 81 | + | skip := len(p.requests()) | |
| 82 | + | p.mapRequest(win) | |
| 83 | + | p.waitReq(skip, func(q x11Req) bool { return q.major == 8 && q.u32(4) == win }) // MapWindow | |
| 84 | + | } | |
| 85 | + | ||
| 86 | + | // pair sends the serial from both sides and commits a buffer on the surface. | |
| 87 | + | // It returns the wl_surface id of the Xwayland client. | |
| 88 | + | func (r *x11Rig) pair(win uint32, serial uint64) uint32 { | |
| 89 | + | r.t.Helper() | |
| 90 | + | r.peer.clientMessage(win, "WL_SURFACE_SERIAL", uint32(serial), uint32(serial>>32)) | |
| 91 | + | surf, xs := r.xc.AllocateID(), r.xc.AllocateID() | |
| 92 | + | requestProtocol(r.t, r.xc, r.comp, wayland.CompositorRequestCreateSurface, surf) | |
| 93 | + | if r.surfaceProxy != nil { | |
| 94 | + | registerWireProxy(r.xc, r.surfaceProxy(surf)) | |
| 95 | + | } else { | |
| 96 | + | registerProtocol(r.t, r.xc, surf) | |
| 97 | + | } | |
| 98 | + | requestProtocol(r.t, r.xc, r.shell, xwaylandshell.XwaylandShellV1RequestGetXwaylandSurface, xs, surf) | |
| 99 | + | registerProtocol(r.t, r.xc, xs) | |
| 100 | + | requestProtocol(r.t, r.xc, xs, xwaylandshell.XwaylandSurfaceV1RequestSetSerial, uint32(serial), uint32(serial>>32)) | |
| 101 | + | requestProtocol(r.t, r.xc, surf, wayland.SurfaceRequestAttach, shmBuffer(r.t, r.xc), int32(0), int32(0)) | |
| 102 | + | requestProtocol(r.t, r.xc, surf, wayland.SurfaceRequestCommit) | |
| 103 | + | if err := r.xc.Roundtrip(); err != nil { | |
| 104 | + | r.t.Fatal(err) | |
| 105 | + | } | |
| 106 | + | return surf | |
| 107 | + | } | |
| 108 | + | ||
| 109 | + | func awaitEvent[T ports.ClientEvent](t *testing.T, events <-chan ports.ClientEvent) T { | |
| 110 | + | t.Helper() | |
| 111 | + | for deadline := time.After(3 * time.Second); ; { | |
| 112 | + | select { | |
| 113 | + | case ev := <-events: | |
| 114 | + | if v, ok := ev.(T); ok { | |
| 115 | + | return v | |
| 116 | + | } | |
| 117 | + | case <-deadline: | |
| 118 | + | var zero T | |
| 119 | + | t.Fatalf("no %T event", zero) | |
| 120 | + | } | |
| 121 | + | } | |
| 122 | + | } | |
| 123 | + | ||
| 124 | + | func TestXwaylandGlobalFilter(t *testing.T) { | |
| 125 | + | s, _, _, dir := lifecycleServer(t) | |
| 126 | + | other := protocolClient(t, s, dir) | |
| 127 | + | for _, iface := range []string{"xwayland_shell_v1", "zwp_xwayland_keyboard_grab_manager_v1"} { | |
| 128 | + | if _, ok := other.Registry().FindGlobal(iface); ok { | |
| 129 | + | t.Errorf("ordinary client sees %s", iface) | |
| 130 | + | } | |
| 131 | + | } | |
| 132 | + | if _, ok := other.Registry().FindGlobal("xdg_wm_base"); !ok { | |
| 133 | + | t.Error("ordinary globals filtered out") | |
| 134 | + | } | |
| 135 | + | r := newX11Rig(t) | |
| 136 | + | if _, ok := r.xc.Registry().FindGlobal("xwayland_shell_v1"); !ok { | |
| 137 | + | t.Error("Xwayland client misses xwayland_shell_v1") | |
| 138 | + | } | |
| 139 | + | // The ordinary client of the same server still does not see it. | |
| 140 | + | late := protocolClient(t, r.s, r.dir) | |
| 141 | + | if _, ok := late.Registry().FindGlobal("xwayland_shell_v1"); ok { | |
| 142 | + | t.Error("ordinary client sees xwayland_shell_v1 while Xwayland is attached") | |
| 143 | + | } | |
| 144 | + | } | |
| 145 | + | ||
| 146 | + | func TestXwaylandShellRoleErrors(t *testing.T) { | |
| 147 | + | for _, scenario := range []string{"xdg role first", "serial twice", "zero serial"} { | |
| 148 | + | t.Run(scenario, func(t *testing.T) { | |
| 149 | + | r := newX11Rig(t) | |
| 150 | + | c := r.xc | |
| 151 | + | wmBase := bindProtocol(t, c, "xdg_wm_base") | |
| 152 | + | surf := c.AllocateID() | |
| 153 | + | requestProtocol(t, c, r.comp, wayland.CompositorRequestCreateSurface, surf) | |
| 154 | + | registerProtocol(t, c, surf) | |
| 155 | + | xs := c.AllocateID() | |
| 156 | + | switch scenario { | |
| 157 | + | case "xdg role first": | |
| 158 | + | xdg := xs | |
| 159 | + | requestProtocol(t, c, wmBase, xdgshell.WmBaseRequestGetXdgSurface, xdg, surf) | |
| 160 | + | requestProtocol(t, c, r.shell, xwaylandshell.XwaylandShellV1RequestGetXwaylandSurface, c.AllocateID(), surf) | |
| 161 | + | expectProtocolError(t, c, r.shell, uint32(xwaylandshell.XwaylandShellV1ErrorRole)) | |
| 162 | + | case "serial twice": | |
| 163 | + | requestProtocol(t, c, r.shell, xwaylandshell.XwaylandShellV1RequestGetXwaylandSurface, xs, surf) | |
| 164 | + | registerProtocol(t, c, xs) | |
| 165 | + | requestProtocol(t, c, xs, xwaylandshell.XwaylandSurfaceV1RequestSetSerial, uint32(1), uint32(0)) | |
| 166 | + | requestProtocol(t, c, xs, xwaylandshell.XwaylandSurfaceV1RequestSetSerial, uint32(2), uint32(0)) | |
| 167 | + | expectProtocolError(t, c, xs, uint32(xwaylandshell.XwaylandSurfaceV1ErrorAlreadyAssociated)) | |
| 168 | + | case "zero serial": | |
| 169 | + | requestProtocol(t, c, r.shell, xwaylandshell.XwaylandShellV1RequestGetXwaylandSurface, xs, surf) | |
| 170 | + | registerProtocol(t, c, xs) | |
| 171 | + | requestProtocol(t, c, xs, xwaylandshell.XwaylandSurfaceV1RequestSetSerial, uint32(0), uint32(0)) | |
| 172 | + | expectProtocolError(t, c, xs, uint32(xwaylandshell.XwaylandSurfaceV1ErrorInvalidSerial)) | |
| 173 | + | } | |
| 174 | + | }) | |
| 175 | + | } | |
| 176 | + | } | |
| 177 | + | ||
| 178 | + | // A window announced by the X server and a surface carrying the same serial | |
| 179 | + | // map as one neferwl window with the X11 info; core commands reach the X side. | |
| 180 | + | func TestX11WindowLifecycle(t *testing.T) { | |
| 181 | + | r := newX11Rig(t) | |
| 182 | + | win := uint32(x11Win) | |
| 183 | + | r.x11Window(win) | |
| 184 | + | surf := r.pair(win, x11Serial) | |
| 185 | + | ||
| 186 | + | m := awaitEvent[ports.WindowMapped](t, r.events) | |
| 187 | + | if m.AppID != "Class" || m.PID != 4242 || !m.Positioned || m.Floating || m.Slot != "" { | |
| 188 | + | t.Fatalf("mapped %+v", m) | |
| 189 | + | } | |
| 190 | + | ||
| 191 | + | // Title and app id changes. | |
| 192 | + | p := r.peer | |
| 193 | + | p.setProp(win, p.atom("_NET_WM_NAME"), p.atom("UTF8_STRING"), 8, []byte("renamed")) | |
| 194 | + | p.propertyNotify(win, p.atom("_NET_WM_NAME")) | |
| 195 | + | p.setProp(win, 67, 31, 8, []byte("inst\x00Other\x00")) | |
| 196 | + | p.propertyNotify(win, 67) | |
| 197 | + | if a := awaitEvent[ports.WindowAppID](t, r.events); a.ID != m.ID || a.AppID != "Other" { | |
| 198 | + | t.Fatalf("app id %+v", a) | |
| 199 | + | } | |
| 200 | + | var title string | |
| 201 | + | waitTrue(t, "title", func() bool { | |
| 202 | + | r.s.display.Do(func() { | |
| 203 | + | if w := r.s.windows[m.ID]; w != nil { | |
| 204 | + | title = w.title | |
| 205 | + | } | |
| 206 | + | }) | |
| 207 | + | return title == "renamed" | |
| 208 | + | }) | |
| 209 | + | ||
| 210 | + | // Fullscreen request from the client. | |
| 211 | + | p.clientMessage(win, "_NET_WM_STATE", 1, p.atom("_NET_WM_STATE_FULLSCREEN")) | |
| 212 | + | if f := awaitEvent[ports.WindowFullscreenRequest](t, r.events); f.ID != m.ID || !f.Fullscreen || f.External { | |
| 213 | + | t.Fatalf("fullscreen %+v", f) | |
| 214 | + | } | |
| 215 | + | ||
| 216 | + | // Configure and Place reach the X server as ConfigureWindow. | |
| 217 | + | skip := len(p.requests()) | |
| 218 | + | r.commands <- ports.ConfigureWindow{ID: m.ID, Width: 640, Height: 360, Visible: true} | |
| 219 | + | cfg, skip := p.waitReq(skip, func(q x11Req) bool { return q.major == x11OpConfigureWindow && q.u32(4) == win }) | |
| 220 | + | if w := binary.LittleEndian.Uint32(cfg.raw[20:]); w != 640 { | |
| 221 | + | t.Fatalf("configured width %d, want 640 (%x)", w, cfg.raw) | |
| 222 | + | } | |
| 223 | + | r.commands <- ports.PlaceWindow{ID: m.ID, X: 10, Y: 20} | |
| 224 | + | cfg, _ = p.waitReq(skip, func(q x11Req) bool { return q.major == x11OpConfigureWindow && q.u32(4) == win }) | |
| 225 | + | if x, y := int32(binary.LittleEndian.Uint32(cfg.raw[12:])), int32(binary.LittleEndian.Uint32(cfg.raw[16:])); x != 10 || y != 20 { | |
| 226 | + | t.Fatalf("placed at %d,%d, want 10,20", x, y) | |
| 227 | + | } | |
| 228 | + | ||
| 229 | + | // Focus. | |
| 230 | + | skip = len(p.requests()) | |
| 231 | + | r.commands <- ports.FocusWindow{ID: m.ID} | |
| 232 | + | if f, _ := p.waitReq(skip, func(q x11Req) bool { return q.major == x11OpSetInputFocus }); f.u32(4) != win { | |
| 233 | + | t.Fatalf("focus on %#x, want %#x", f.u32(4), win) | |
| 234 | + | } | |
| 235 | + | ||
| 236 | + | // Close: without WM_DELETE_WINDOW the client is killed. | |
| 237 | + | skip = len(p.requests()) | |
| 238 | + | r.commands <- ports.CloseWindow{ID: m.ID} | |
| 239 | + | if k, _ := p.waitReq(skip, func(q x11Req) bool { return q.major == x11OpKillClient }); k.u32(4) != win { | |
| 240 | + | t.Fatalf("killed %#x, want %#x", k.u32(4), win) | |
| 241 | + | } | |
| 242 | + | ||
| 243 | + | // Destroying the surface unmaps the window. | |
| 244 | + | requestProtocol(t, r.xc, surf, wayland.SurfaceRequestDestroy) | |
| 245 | + | if err := r.xc.Roundtrip(); err != nil { | |
| 246 | + | t.Fatal(err) | |
| 247 | + | } | |
| 248 | + | if u := awaitEvent[ports.WindowUnmapped](t, r.events); u.ID != m.ID { | |
| 249 | + | t.Fatalf("unmapped %+v", u) | |
| 250 | + | } | |
| 251 | + | } | |
| 252 | + | ||
| 253 | + | // The serial may arrive on the Wayland side before the X side knows it, and | |
| 254 | + | // an UnmapNotify unmaps; a remap pairs again. | |
| 255 | + | func TestX11WindowSurfaceFirstAndRemap(t *testing.T) { | |
| 256 | + | r := newX11Rig(t) | |
| 257 | + | win := uint32(x11Win) | |
| 258 | + | p := r.peer | |
| 259 | + | surf, xs := r.xc.AllocateID(), r.xc.AllocateID() | |
| 260 | + | requestProtocol(t, r.xc, r.comp, wayland.CompositorRequestCreateSurface, surf) | |
| 261 | + | registerProtocol(t, r.xc, surf) | |
| 262 | + | requestProtocol(t, r.xc, r.shell, xwaylandshell.XwaylandShellV1RequestGetXwaylandSurface, xs, surf) | |
| 263 | + | registerProtocol(t, r.xc, xs) | |
| 264 | + | requestProtocol(t, r.xc, xs, xwaylandshell.XwaylandSurfaceV1RequestSetSerial, uint32(x11Serial), uint32(x11Serial>>32)) | |
| 265 | + | requestProtocol(t, r.xc, surf, wayland.SurfaceRequestAttach, shmBuffer(t, r.xc), int32(0), int32(0)) | |
| 266 | + | requestProtocol(t, r.xc, surf, wayland.SurfaceRequestCommit) | |
| 267 | + | if err := r.xc.Roundtrip(); err != nil { | |
| 268 | + | t.Fatal(err) | |
| 269 | + | } | |
| 270 | + | r.x11Window(win) | |
| 271 | + | p.clientMessage(win, "WL_SURFACE_SERIAL", uint32(x11Serial), uint32(x11Serial>>32)) | |
| 272 | + | m := awaitEvent[ports.WindowMapped](t, r.events) | |
| 273 | + | if m.Remap { | |
| 274 | + | t.Fatalf("first map flagged as remap: %+v", m) | |
| 275 | + | } | |
| 276 | + | p.unmapNotify(win) | |
| 277 | + | if u := awaitEvent[ports.WindowUnmapped](t, r.events); u.ID != m.ID { | |
| 278 | + | t.Fatalf("unmapped %+v", u) | |
| 279 | + | } | |
| 280 | + | p.destroyNotify(win) | |
| 281 | + | waitTrue(t, "window forgotten", func() bool { | |
| 282 | + | gone := false | |
| 283 | + | r.s.display.Do(func() { gone = r.s.windows[m.ID] == nil }) | |
| 284 | + | return gone | |
| 285 | + | }) | |
| 286 | + | } | |
| 287 | + | ||
| 288 | + | // stop closes the XWM: its windows unmap, and the server keeps serving. | |
| 289 | + | func TestX11StopUnmapsWindows(t *testing.T) { | |
| 290 | + | r := newX11Rig(t) | |
| 291 | + | win := uint32(x11Win) | |
| 292 | + | r.x11Window(win) | |
| 293 | + | r.pair(win, x11Serial) | |
| 294 | + | m := awaitEvent[ports.WindowMapped](t, r.events) | |
| 295 | + | r.stop() | |
| 296 | + | if u := awaitEvent[ports.WindowUnmapped](t, r.events); u.ID != m.ID { | |
| 297 | + | t.Fatalf("unmapped %+v", u) | |
| 298 | + | } | |
| 299 | + | r.s.display.Do(func() { | |
| 300 | + | if r.s.xwm != nil || r.s.windows[m.ID] != nil || len(r.s.x11Slots) != 0 { | |
| 301 | + | t.Error("XWM state left behind") | |
| 302 | + | } | |
| 303 | + | }) | |
| 304 | + | r.stop() // idempotent | |
| 305 | + | } | |
| 306 | + | ||
| 307 | + | // Boot gives up as soon as its context ends, even against a silent server, | |
| 308 | + | // and leaves the server serving. | |
| 309 | + | func TestAttachXWMBootAbortsWithContext(t *testing.T) { | |
| 310 | + | s, _, _, dir := lifecycleServer(t) | |
| 311 | + | fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0) | |
| 312 | + | if err != nil { | |
| 313 | + | t.Fatal(err) | |
| 314 | + | } | |
| 315 | + | defer unix.Close(fds[1]) // the silent server end | |
| 316 | + | ctx, cancel := context.WithCancel(context.Background()) | |
| 317 | + | done := make(chan func(), 1) | |
| 318 | + | go func() { done <- s.AttachXWM(ctx, fds[0]) }() | |
| 319 | + | select { | |
| 320 | + | case <-done: | |
| 321 | + | t.Fatal("AttachXWM returned before the context ended") | |
| 322 | + | case <-time.After(150 * time.Millisecond): | |
| 323 | + | } | |
| 324 | + | cancel() | |
| 325 | + | select { | |
| 326 | + | case stop := <-done: | |
| 327 | + | stop() | |
| 328 | + | case <-time.After(2 * time.Second): | |
| 329 | + | t.Fatal("AttachXWM did not abort with its context") | |
| 330 | + | } | |
| 331 | + | assertFDClosed(t, fds[0]) | |
| 332 | + | protocolClient(t, s, dir) // still serving | |
| 333 | + | } | |
| 334 | + | ||
| 335 | + | // Xwayland locks the pointer for games: the constraint reaches core for the | |
| 336 | + | // X11 window like for an xdg toplevel. | |
| 337 | + | func TestX11WindowPointerLock(t *testing.T) { | |
| 338 | + | r := newX11Rig(t) | |
| 339 | + | win := uint32(x11Win) | |
| 340 | + | r.x11Window(win) | |
| 341 | + | surf := r.pair(win, x11Serial) | |
| 342 | + | m := awaitEvent[ports.WindowMapped](t, r.events) | |
| 343 | + | ||
| 344 | + | seat := bindProtocol(t, r.xc, "wl_seat") | |
| 345 | + | registerProtocol(t, r.xc, seat) | |
| 346 | + | pointer := r.xc.AllocateID() | |
| 347 | + | requestProtocol(t, r.xc, seat, wayland.SeatRequestGetPointer, pointer) | |
| 348 | + | registerProtocol(t, r.xc, pointer) | |
| 349 | + | constraints := bindProtocol(t, r.xc, "zwp_pointer_constraints_v1") | |
| 350 | + | lock := r.xc.AllocateID() | |
| 351 | + | requestProtocol(t, r.xc, constraints, pointerconstraints.ZwpPointerConstraintsV1RequestLockPointer, lock, surf, pointer, uint32(0), uint32(pointerconstraints.ZwpPointerConstraintsV1LifetimePersistent)) | |
| 352 | + | registerProtocol(t, r.xc, lock) | |
| 353 | + | if err := r.xc.Roundtrip(); err != nil { | |
| 354 | + | t.Fatal(err) | |
| 355 | + | } | |
| 356 | + | r.commands <- ports.PointerFocus{ID: m.ID} | |
| 357 | + | r.commands <- ports.FocusWindow{ID: m.ID} | |
| 358 | + | if got := constrained(t, r.events); got.ID != m.ID || got.Mode != ports.ConstraintLock { | |
| 359 | + | t.Fatalf("constrained %+v", got) | |
| 360 | + | } | |
| 361 | + | } | |
| 362 | + | ||
| 363 | + | // grabRig is an X11 window with an Xwayland keyboard grab on its surface. | |
| 364 | + | func grabRig(t *testing.T) (r *x11Rig, w ports.WindowID, surf uint32) { | |
| 365 | + | t.Helper() | |
| 366 | + | r = newX11Rig(t) | |
| 367 | + | r.x11Window(x11Win) | |
| 368 | + | surf = r.pair(x11Win, x11Serial) | |
| 369 | + | w = awaitEvent[ports.WindowMapped](t, r.events).ID | |
| 370 | + | seat := bindProtocol(t, r.xc, "wl_seat") | |
| 371 | + | registerProtocol(t, r.xc, seat) | |
| 372 | + | mgr := bindProtocol(t, r.xc, "zwp_xwayland_keyboard_grab_manager_v1") | |
| 373 | + | grab := r.xc.AllocateID() | |
| 374 | + | requestProtocol(t, r.xc, mgr, xwaylandkeyboardgrab.ZwpXwaylandKeyboardGrabManagerV1RequestGrabKeyboard, grab, surf, seat) | |
| 375 | + | registerProtocol(t, r.xc, grab) | |
| 376 | + | if err := r.xc.Roundtrip(); err != nil { | |
| 377 | + | t.Fatal(err) | |
| 378 | + | } | |
| 379 | + | return r, w, grab | |
| 380 | + | } | |
| 381 | + | ||
| 382 | + | // noInhibit fails when a ShortcutsInhibit event arrives shortly. | |
| 383 | + | func noInhibit(t *testing.T, r *x11Rig) { | |
| 384 | + | t.Helper() | |
| 385 | + | if err := r.xc.Roundtrip(); err != nil { | |
| 386 | + | t.Fatal(err) | |
| 387 | + | } | |
| 388 | + | for deadline := time.After(150 * time.Millisecond); ; { | |
| 389 | + | select { | |
| 390 | + | case ev := <-r.events: | |
| 391 | + | if v, ok := ev.(ports.ShortcutsInhibit); ok { | |
| 392 | + | t.Fatalf("unexpected %+v", v) | |
| 393 | + | } | |
| 394 | + | case <-deadline: | |
| 395 | + | return | |
| 396 | + | } | |
| 397 | + | } | |
| 398 | + | } | |
| 399 | + | ||
| 400 | + | func TestXwaylandKeyboardGrabThreeStates(t *testing.T) { | |
| 401 | + | r, w, grab := grabRig(t) | |
| 402 | + | // Windowed and focused: accepted, no effect. | |
| 403 | + | r.commands <- ports.ConfigureWindow{ID: w, Width: 100, Height: 100, Visible: true} | |
| 404 | + | r.commands <- ports.FocusWindow{ID: w} | |
| 405 | + | noInhibit(t, r) | |
| 406 | + | // Focused and fullscreen: inhibits. | |
| 407 | + | r.commands <- ports.ConfigureWindow{ID: w, Width: 100, Height: 100, Visible: true, Fullscreen: true} | |
| 408 | + | if v := awaitEvent[ports.ShortcutsInhibit](t, r.events); v != (ports.ShortcutsInhibit{Window: w, Active: true}) { | |
| 409 | + | t.Fatal(v) | |
| 410 | + | } | |
| 411 | + | // Focus lost: released. | |
| 412 | + | r.commands <- ports.FocusWindow{} | |
| 413 | + | if v := awaitEvent[ports.ShortcutsInhibit](t, r.events); v != (ports.ShortcutsInhibit{Window: w}) { | |
| 414 | + | t.Fatal(v) | |
| 415 | + | } | |
| 416 | + | // Back, then the grab object goes away. | |
| 417 | + | r.commands <- ports.FocusWindow{ID: w} | |
| 418 | + | awaitEvent[ports.ShortcutsInhibit](t, r.events) | |
| 419 | + | requestProtocol(t, r.xc, grab, xwaylandkeyboardgrab.ZwpXwaylandKeyboardGrabV1RequestDestroy) | |
| 420 | + | if v := awaitEvent[ports.ShortcutsInhibit](t, r.events); v != (ports.ShortcutsInhibit{Window: w}) { | |
| 421 | + | t.Fatal(v) | |
| 422 | + | } | |
| 423 | + | } | |
| 424 | + | ||
| 425 | + | // x11Pointer gives the Xwayland client a wl_pointer and a relative pointer, | |
| 426 | + | // both with event logs. | |
| 427 | + | func (r *x11Rig) x11Pointer() (pointer uint32, ptrEvents, relEvents chan []float64) { | |
| 428 | + | r.t.Helper() | |
| 429 | + | seat := bindProtocol(r.t, r.xc, "wl_seat") | |
| 430 | + | registerProtocol(r.t, r.xc, seat) | |
| 431 | + | pointer = r.xc.AllocateID() | |
| 432 | + | ptrEvents = logProxy(r.t, r.xc, pointer) | |
| 433 | + | requestProtocol(r.t, r.xc, seat, wayland.SeatRequestGetPointer, pointer) | |
| 434 | + | mgr := bindProtocol(r.t, r.xc, "zwp_relative_pointer_manager_v1") | |
| 435 | + | rel := r.xc.AllocateID() | |
| 436 | + | relEvents = logProxy(r.t, r.xc, rel) | |
| 437 | + | requestProtocol(r.t, r.xc, mgr, relativepointer.ZwpRelativePointerManagerV1RequestGetRelativePointer, rel, pointer) | |
| 438 | + | return pointer, ptrEvents, relEvents | |
| 439 | + | } | |
| 440 | + | ||
| 441 | + | // A lock on an X11 surface activates with pointer and keyboard focus, sends | |
| 442 | + | // relative motion to the Xwayland client and deactivates when a native | |
| 443 | + | // window takes the focus; it comes back with the X11 focus. | |
| 444 | + | func TestX11LockFollowsFocusAndRelativeMotion(t *testing.T) { | |
| 445 | + | r := newX11Rig(t) | |
| 446 | + | win := uint32(x11Win) | |
| 447 | + | r.x11Window(win) | |
| 448 | + | surf := r.pair(win, x11Serial) | |
| 449 | + | m := awaitEvent[ports.WindowMapped](t, r.events) | |
| 450 | + | pointer, _, relEvents := r.x11Pointer() | |
| 451 | + | constraints := bindProtocol(t, r.xc, "zwp_pointer_constraints_v1") | |
| 452 | + | lock := r.xc.AllocateID() | |
| 453 | + | lockEvents := logProxy(t, r.xc, lock) | |
| 454 | + | requestProtocol(t, r.xc, constraints, pointerconstraints.ZwpPointerConstraintsV1RequestLockPointer, lock, surf, pointer, uint32(0), uint32(pointerconstraints.ZwpPointerConstraintsV1LifetimePersistent)) | |
| 455 | + | if err := r.xc.Roundtrip(); err != nil { | |
| 456 | + | t.Fatal(err) | |
| 457 | + | } | |
| 458 | + | if len(lockEvents) > 0 { | |
| 459 | + | t.Fatal("locked without focus") | |
| 460 | + | } | |
| 461 | + | ||
| 462 | + | r.commands <- ports.PointerFocus{ID: m.ID} | |
| 463 | + | r.commands <- ports.FocusWindow{ID: m.ID} | |
| 464 | + | if ev := next(t, r.xc, lockEvents); ev[0] != float64(pointerconstraints.ZwpLockedPointerV1EventLocked) { | |
| 465 | + | t.Fatalf("lock event %v", ev) | |
| 466 | + | } | |
| 467 | + | if got := constrained(t, r.events); got.ID != m.ID || got.Mode != ports.ConstraintLock || got.Rect != (ports.Rect{}) { | |
| 468 | + | t.Fatalf("constrained %+v", got) | |
| 469 | + | } | |
| 470 | + | ||
| 471 | + | r.commands <- ports.PointerMotionTo{ID: m.ID, X: 5, Y: 5, DX: 2.5, DY: -1, UnaccelDX: 4, UnaccelDY: -2} | |
| 472 | + | if ev := next(t, r.xc, relEvents); len(ev) != 5 || ev[1] != 2.5 || ev[2] != -1 || ev[3] != 4 || ev[4] != -2 { | |
| 473 | + | t.Fatalf("relative motion %v", ev) | |
| 474 | + | } | |
| 475 | + | ||
| 476 | + | // A native window takes the focus: the X11 lock ends and the XWM drops | |
| 477 | + | // the X focus. | |
| 478 | + | nc := protocolClient(t, r.s, r.dir) | |
| 479 | + | nm, _, _ := surfaceMapper(t, nc, r.events)() | |
| 480 | + | skip := len(r.peer.requests()) | |
| 481 | + | r.commands <- ports.FocusWindow{ID: nm.ID} | |
| 482 | + | if ev := next(t, r.xc, lockEvents); ev[0] != float64(pointerconstraints.ZwpLockedPointerV1EventUnlocked) { | |
| 483 | + | t.Fatalf("unlock event %v", ev) | |
| 484 | + | } | |
| 485 | + | if got := constrained(t, r.events); got.ID != 0 { | |
| 486 | + | t.Fatalf("constrained %+v after native focus", got) | |
| 487 | + | } | |
| 488 | + | if f, _ := r.peer.waitReq(skip, func(q x11Req) bool { return q.major == x11OpSetInputFocus }); f.u32(4) == win { | |
| 489 | + | t.Fatalf("X focus still on %#x", win) | |
| 490 | + | } | |
| 491 | + | ||
| 492 | + | r.commands <- ports.FocusWindow{ID: m.ID} | |
| 493 | + | if ev := next(t, r.xc, lockEvents); ev[0] != float64(pointerconstraints.ZwpLockedPointerV1EventLocked) { | |
| 494 | + | t.Fatalf("relock event %v", ev) | |
| 495 | + | } | |
| 496 | + | } | |
| 497 | + | ||
| 498 | + | // A confine on an X11 surface reaches core with its region as given: an X11 | |
| 499 | + | // window has no geometry offset. | |
| 500 | + | func TestX11ConfineRegionHasNoOffset(t *testing.T) { | |
| 501 | + | r := newX11Rig(t) | |
| 502 | + | win := uint32(x11Win) | |
| 503 | + | r.x11Window(win) | |
| 504 | + | surf := r.pair(win, x11Serial) | |
| 505 | + | m := awaitEvent[ports.WindowMapped](t, r.events) | |
| 506 | + | pointer, _, _ := r.x11Pointer() | |
| 507 | + | region := r.xc.AllocateID() | |
| 508 | + | registerProtocol(t, r.xc, region) | |
| 509 | + | requestProtocol(t, r.xc, r.comp, wayland.CompositorRequestCreateRegion, region) | |
| 510 | + | requestProtocol(t, r.xc, region, wayland.RegionRequestAdd, int32(5), int32(6), int32(20), int32(30)) | |
| 511 | + | constraints := bindProtocol(t, r.xc, "zwp_pointer_constraints_v1") | |
| 512 | + | confine := r.xc.AllocateID() | |
| 513 | + | confineEvents := logProxy(t, r.xc, confine) | |
| 514 | + | requestProtocol(t, r.xc, constraints, pointerconstraints.ZwpPointerConstraintsV1RequestConfinePointer, confine, surf, pointer, region, uint32(pointerconstraints.ZwpPointerConstraintsV1LifetimePersistent)) | |
| 515 | + | if err := r.xc.Roundtrip(); err != nil { | |
| 516 | + | t.Fatal(err) | |
| 517 | + | } | |
| 518 | + | r.state(func() { | |
| 519 | + | if g := r.s.windows[m.ID].geometry(); g != (ports.Rect{}) { | |
| 520 | + | t.Errorf("X11 geometry %+v, want zero", g) | |
| 521 | + | } | |
| 522 | + | }) | |
| 523 | + | ||
| 524 | + | r.commands <- ports.PointerFocus{ID: m.ID, X: 10, Y: 10} | |
| 525 | + | r.commands <- ports.FocusWindow{ID: m.ID} | |
| 526 | + | r.commands <- ports.PointerMotionTo{ID: m.ID, X: 10, Y: 10} | |
| 527 | + | if ev := next(t, r.xc, confineEvents); ev[0] != float64(pointerconstraints.ZwpConfinedPointerV1EventConfined) { | |
| 528 | + | t.Fatalf("confine event %v", ev) | |
| 529 | + | } | |
| 530 | + | want := ports.PointerConstrained{ID: m.ID, PointerConstraint: ports.PointerConstraint{Mode: ports.ConstraintConfine, Rect: ports.Rect{X: 5, Y: 6, W: 20, H: 30}}} | |
| 531 | + | if got := constrained(t, r.events); got != want { | |
| 532 | + | t.Fatalf("constrained %+v, want %+v", got, want) | |
| 533 | + | } | |
| 534 | + | } | |
| 535 | + | ||
| 536 | + | // waitOn waits until window id's surface is on output name: commands reach | |
| 537 | + | // the display goroutine asynchronously. | |
| 538 | + | func (r *x11Rig) waitOn(id ports.WindowID, name string) { | |
| 539 | + | r.t.Helper() | |
| 540 | + | waitTrue(r.t, "surface on "+name, func() bool { | |
| 541 | + | ok := false | |
| 542 | + | r.state(func() { | |
| 543 | + | if w := r.s.windows[id]; w != nil { | |
| 544 | + | on := w.surf().on | |
| 545 | + | ok = on != nil && on.name() == name | |
| 546 | + | } | |
| 547 | + | }) | |
| 548 | + | return ok | |
| 549 | + | }) | |
| 550 | + | } | |
| 551 | + | ||
| 552 | + | // An X11 window is on one output only, even straddling two: a single | |
| 553 | + | // wl_surface.enter, and a leave only when its output changes. | |
| 554 | + | func TestX11WindowEntersOneOutput(t *testing.T) { | |
| 555 | + | r := newX11Rig(t) | |
| 556 | + | r.commands <- ports.SetOutputs{Outputs: ports.Layout{ | |
| 557 | + | {Info: ports.OutputInfo{Name: "A"}, X: 0, Y: 0, Width: 1920, Height: 1080, Scale: 1}, | |
| 558 | + | {Info: ports.OutputInfo{Name: "B"}, X: 1920, Y: 0, Width: 1920, Height: 1080, Scale: 1}, | |
| 559 | + | }, Focused: "A"} | |
| 560 | + | r.state(func() {}) // the layout is applied | |
| 561 | + | if err := r.xc.Roundtrip(); err != nil { | |
| 562 | + | t.Fatal(err) | |
| 563 | + | } | |
| 564 | + | outputs := 0 | |
| 565 | + | for _, g := range r.xc.Registry().GetGlobals() { | |
| 566 | + | if g.Interface != "wl_output" { | |
| 567 | + | continue | |
| 568 | + | } | |
| 569 | + | id, err := bindWireID(r.xc, g.Name, g.Interface, 1) | |
| 570 | + | if err != nil { | |
| 571 | + | t.Fatal(err) | |
| 572 | + | } | |
| 573 | + | registerProtocol(t, r.xc, id) | |
| 574 | + | outputs++ | |
| 575 | + | } | |
| 576 | + | if outputs != 2 { | |
| 577 | + | t.Fatalf("%d wl_output globals, want 2", outputs) | |
| 578 | + | } | |
| 579 | + | ||
| 580 | + | win := uint32(x11Win) | |
| 581 | + | r.x11Window(win) | |
| 582 | + | events := make(chan []float64, 64) | |
| 583 | + | r.surfaceProxy = func(id uint32) wlturbo.Proxy { | |
| 584 | + | p := &eventLog{events: events} | |
| 585 | + | p.SetID(id) | |
| 586 | + | return p | |
| 587 | + | } | |
| 588 | + | r.pair(win, x11Serial) | |
| 589 | + | m := awaitEvent[ports.WindowMapped](t, r.events) | |
| 590 | + | ||
| 591 | + | // Straddling both outputs, configured on A. | |
| 592 | + | r.commands <- ports.PlaceWindow{ID: m.ID, X: 1900, Y: 0} | |
| 593 | + | r.commands <- ports.ConfigureWindow{ID: m.ID, Width: 200, Height: 100, Visible: true, Output: "A"} | |
| 594 | + | r.waitOn(m.ID, "A") | |
| 595 | + | if err := r.xc.Roundtrip(); err != nil { | |
| 596 | + | t.Fatal(err) | |
| 597 | + | } | |
| 598 | + | count := func() (enters, leaves int) { | |
| 599 | + | for len(events) > 0 { | |
| 600 | + | switch ev := <-events; ev[0] { | |
| 601 | + | case float64(wayland.SurfaceEventEnter): | |
| 602 | + | enters++ | |
| 603 | + | case float64(wayland.SurfaceEventLeave): | |
| 604 | + | leaves++ | |
| 605 | + | } | |
| 606 | + | } | |
| 607 | + | return | |
| 608 | + | } | |
| 609 | + | if e, l := count(); e != 1 || l != 0 { | |
| 610 | + | t.Fatalf("straddling window: %d enters, %d leaves, want 1, 0", e, l) | |
| 611 | + | } | |
| 612 | + | r.state(func() { | |
| 613 | + | if on := r.s.windows[m.ID].surf().on; on == nil || on.name() != "A" { | |
| 614 | + | t.Errorf("surface on %v, want A", on) | |
| 615 | + | } | |
| 616 | + | }) | |
| 617 | + | ||
| 618 | + | // Moving to B leaves A and enters B once. | |
| 619 | + | r.commands <- ports.ConfigureWindow{ID: m.ID, Width: 200, Height: 100, Visible: true, Output: "B"} | |
| 620 | + | r.waitOn(m.ID, "B") | |
| 621 | + | if err := r.xc.Roundtrip(); err != nil { | |
| 622 | + | t.Fatal(err) | |
| 623 | + | } | |
| 624 | + | if e, l := count(); e != 1 || l != 1 { | |
| 625 | + | t.Fatalf("move to B: %d enters, %d leaves, want 1, 1", e, l) | |
| 626 | + | } | |
| 627 | + | } | |
| @@ -0,0 +1,627 @@ | |||||
| 1 | + | package wayland | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "encoding/binary" | |||
| 6 | + | "net" | |||
| 7 | + | "os" | |||
| 8 | + | "testing" | |||
| 9 | + | "time" | |||
| 10 | + | ||||
| 11 | + | "github.com/bnema/go-wayland-bindings/server/pointerconstraints" | |||
| 12 | + | "github.com/bnema/go-wayland-bindings/server/relativepointer" | |||
| 13 | + | "github.com/bnema/go-wayland-bindings/server/wayland" | |||
| 14 | + | "github.com/bnema/go-wayland-bindings/server/xdgshell" | |||
| 15 | + | "github.com/bnema/go-wayland-bindings/server/xwaylandkeyboardgrab" | |||
| 16 | + | "github.com/bnema/go-wayland-bindings/server/xwaylandshell" | |||
| 17 | + | "github.com/bnema/neferwl/internal/ports" | |||
| 18 | + | "github.com/bnema/wlturbo" | |||
| 19 | + | "golang.org/x/sys/unix" | |||
| 20 | + | ) | |||
| 21 | + | ||||
| 22 | + | var x11Serial = uint64(0x1234_0000_0042) | |||
| 23 | + | ||||
| 24 | + | // x11Rig is a server with an Xwayland client (a real wlturbo client on the | |||
| 25 | + | // adopted descriptor), a scripted X peer on the WM descriptor and the XWM | |||
| 26 | + | // between them. | |||
| 27 | + | type x11Rig struct { | |||
| 28 | + | t *testing.T | |||
| 29 | + | s *Server | |||
| 30 | + | events chan ports.ClientEvent | |||
| 31 | + | commands chan ports.ClientCommand | |||
| 32 | + | dir string | |||
| 33 | + | xc *wlturbo.Display | |||
| 34 | + | peer *x11Peer | |||
| 35 | + | stop func() | |||
| 36 | + | shell uint32 | |||
| 37 | + | comp uint32 | |||
| 38 | + | // surfaceProxy, when set, observes the next paired wl_surface. | |||
| 39 | + | surfaceProxy func(id uint32) wlturbo.Proxy | |||
| 40 | + | } | |||
| 41 | + | ||||
| 42 | + | func newX11Rig(t *testing.T) *x11Rig { | |||
| 43 | + | t.Helper() | |||
| 44 | + | s, events, commands, dir := lifecycleServer(t) | |||
| 45 | + | r := &x11Rig{t: t, s: s, events: events, commands: commands, dir: dir} | |||
| 46 | + | wl := xwaylandPair(t) | |||
| 47 | + | s.AttachXwayland(wl[0]) | |||
| 48 | + | f := os.NewFile(uintptr(wl[1]), "xwayland client") | |||
| 49 | + | wl[1] = -1 | |||
| 50 | + | conn, err := net.FileConn(f) | |||
| 51 | + | f.Close() | |||
| 52 | + | if err != nil { | |||
| 53 | + | t.Fatal(err) | |||
| 54 | + | } | |||
| 55 | + | if r.xc, err = wlturbo.ConnectFromConn(conn); err != nil { | |||
| 56 | + | t.Fatal(err) | |||
| 57 | + | } | |||
| 58 | + | t.Cleanup(func() { _ = r.xc.Close(); forgetWireClient(r.xc) }) | |||
| 59 | + | if err := r.xc.Roundtrip(); err != nil { | |||
| 60 | + | t.Fatal(err) | |||
| 61 | + | } | |||
| 62 | + | wm, peer := startX11Peer(t) | |||
| 63 | + | r.peer = peer | |||
| 64 | + | r.stop = s.AttachXWM(context.Background(), wm) | |||
| 65 | + | t.Cleanup(func() { r.stop() }) | |||
| 66 | + | r.shell = bindProtocol(t, r.xc, "xwayland_shell_v1") | |||
| 67 | + | r.comp = bindProtocol(t, r.xc, "wl_compositor") | |||
| 68 | + | return r | |||
| 69 | + | } | |||
| 70 | + | ||||
| 71 | + | // x11Window announces and maps X window win with the properties of an | |||
| 72 | + | // ordinary client, and waits for the XWM to be ready for it. | |||
| 73 | + | func (r *x11Rig) x11Window(win uint32) { | |||
| 74 | + | r.t.Helper() | |||
| 75 | + | p := r.peer | |||
| 76 | + | str := uint32(31) // XA_STRING | |||
| 77 | + | p.setProp(win, 67, str, 8, []byte("inst\x00Class\x00")) // WM_CLASS | |||
| 78 | + | p.setProp(win, p.atom("_NET_WM_NAME"), p.atom("UTF8_STRING"), 8, []byte("net title")) | |||
| 79 | + | p.setProp(win, p.atom("_NET_WM_PID"), 6, 32, x11le32(4242)) | |||
| 80 | + | p.createNotify(win) | |||
| 81 | + | skip := len(p.requests()) | |||
| 82 | + | p.mapRequest(win) | |||
| 83 | + | p.waitReq(skip, func(q x11Req) bool { return q.major == 8 && q.u32(4) == win }) // MapWindow | |||
| 84 | + | } | |||
| 85 | + | ||||
| 86 | + | // pair sends the serial from both sides and commits a buffer on the surface. | |||
| 87 | + | // It returns the wl_surface id of the Xwayland client. | |||
| 88 | + | func (r *x11Rig) pair(win uint32, serial uint64) uint32 { | |||
| 89 | + | r.t.Helper() | |||
| 90 | + | r.peer.clientMessage(win, "WL_SURFACE_SERIAL", uint32(serial), uint32(serial>>32)) | |||
| 91 | + | surf, xs := r.xc.AllocateID(), r.xc.AllocateID() | |||
| 92 | + | requestProtocol(r.t, r.xc, r.comp, wayland.CompositorRequestCreateSurface, surf) | |||
| 93 | + | if r.surfaceProxy != nil { | |||
| 94 | + | registerWireProxy(r.xc, r.surfaceProxy(surf)) | |||
| 95 | + | } else { | |||
| 96 | + | registerProtocol(r.t, r.xc, surf) | |||
| 97 | + | } | |||
| 98 | + | requestProtocol(r.t, r.xc, r.shell, xwaylandshell.XwaylandShellV1RequestGetXwaylandSurface, xs, surf) | |||
| 99 | + | registerProtocol(r.t, r.xc, xs) | |||
| 100 | + | requestProtocol(r.t, r.xc, xs, xwaylandshell.XwaylandSurfaceV1RequestSetSerial, uint32(serial), uint32(serial>>32)) | |||
| 101 | + | requestProtocol(r.t, r.xc, surf, wayland.SurfaceRequestAttach, shmBuffer(r.t, r.xc), int32(0), int32(0)) | |||
| 102 | + | requestProtocol(r.t, r.xc, surf, wayland.SurfaceRequestCommit) | |||
| 103 | + | if err := r.xc.Roundtrip(); err != nil { | |||
| 104 | + | r.t.Fatal(err) | |||
| 105 | + | } | |||
| 106 | + | return surf | |||
| 107 | + | } | |||
| 108 | + | ||||
| 109 | + | func awaitEvent[T ports.ClientEvent](t *testing.T, events <-chan ports.ClientEvent) T { | |||
| 110 | + | t.Helper() | |||
| 111 | + | for deadline := time.After(3 * time.Second); ; { | |||
| 112 | + | select { | |||
| 113 | + | case ev := <-events: | |||
| 114 | + | if v, ok := ev.(T); ok { | |||
| 115 | + | return v | |||
| 116 | + | } | |||
| 117 | + | case <-deadline: | |||
| 118 | + | var zero T | |||
| 119 | + | t.Fatalf("no %T event", zero) | |||
| 120 | + | } | |||
| 121 | + | } | |||
| 122 | + | } | |||
| 123 | + | ||||
| 124 | + | func TestXwaylandGlobalFilter(t *testing.T) { | |||
| 125 | + | s, _, _, dir := lifecycleServer(t) | |||
| 126 | + | other := protocolClient(t, s, dir) | |||
| 127 | + | for _, iface := range []string{"xwayland_shell_v1", "zwp_xwayland_keyboard_grab_manager_v1"} { | |||
| 128 | + | if _, ok := other.Registry().FindGlobal(iface); ok { | |||
| 129 | + | t.Errorf("ordinary client sees %s", iface) | |||
| 130 | + | } | |||
| 131 | + | } | |||
| 132 | + | if _, ok := other.Registry().FindGlobal("xdg_wm_base"); !ok { | |||
| 133 | + | t.Error("ordinary globals filtered out") | |||
| 134 | + | } | |||
| 135 | + | r := newX11Rig(t) | |||
| 136 | + | if _, ok := r.xc.Registry().FindGlobal("xwayland_shell_v1"); !ok { | |||
| 137 | + | t.Error("Xwayland client misses xwayland_shell_v1") | |||
| 138 | + | } | |||
| 139 | + | // The ordinary client of the same server still does not see it. | |||
| 140 | + | late := protocolClient(t, r.s, r.dir) | |||
| 141 | + | if _, ok := late.Registry().FindGlobal("xwayland_shell_v1"); ok { | |||
| 142 | + | t.Error("ordinary client sees xwayland_shell_v1 while Xwayland is attached") | |||
| 143 | + | } | |||
| 144 | + | } | |||
| 145 | + | ||||
| 146 | + | func TestXwaylandShellRoleErrors(t *testing.T) { | |||
| 147 | + | for _, scenario := range []string{"xdg role first", "serial twice", "zero serial"} { | |||
| 148 | + | t.Run(scenario, func(t *testing.T) { | |||
| 149 | + | r := newX11Rig(t) | |||
| 150 | + | c := r.xc | |||
| 151 | + | wmBase := bindProtocol(t, c, "xdg_wm_base") | |||
| 152 | + | surf := c.AllocateID() | |||
| 153 | + | requestProtocol(t, c, r.comp, wayland.CompositorRequestCreateSurface, surf) | |||
| 154 | + | registerProtocol(t, c, surf) | |||
| 155 | + | xs := c.AllocateID() | |||
| 156 | + | switch scenario { | |||
| 157 | + | case "xdg role first": | |||
| 158 | + | xdg := xs | |||
| 159 | + | requestProtocol(t, c, wmBase, xdgshell.WmBaseRequestGetXdgSurface, xdg, surf) | |||
| 160 | + | requestProtocol(t, c, r.shell, xwaylandshell.XwaylandShellV1RequestGetXwaylandSurface, c.AllocateID(), surf) | |||
| 161 | + | expectProtocolError(t, c, r.shell, uint32(xwaylandshell.XwaylandShellV1ErrorRole)) | |||
| 162 | + | case "serial twice": | |||
| 163 | + | requestProtocol(t, c, r.shell, xwaylandshell.XwaylandShellV1RequestGetXwaylandSurface, xs, surf) | |||
| 164 | + | registerProtocol(t, c, xs) | |||
| 165 | + | requestProtocol(t, c, xs, xwaylandshell.XwaylandSurfaceV1RequestSetSerial, uint32(1), uint32(0)) | |||
| 166 | + | requestProtocol(t, c, xs, xwaylandshell.XwaylandSurfaceV1RequestSetSerial, uint32(2), uint32(0)) | |||
| 167 | + | expectProtocolError(t, c, xs, uint32(xwaylandshell.XwaylandSurfaceV1ErrorAlreadyAssociated)) | |||
| 168 | + | case "zero serial": | |||
| 169 | + | requestProtocol(t, c, r.shell, xwaylandshell.XwaylandShellV1RequestGetXwaylandSurface, xs, surf) | |||
| 170 | + | registerProtocol(t, c, xs) | |||
| 171 | + | requestProtocol(t, c, xs, xwaylandshell.XwaylandSurfaceV1RequestSetSerial, uint32(0), uint32(0)) | |||
| 172 | + | expectProtocolError(t, c, xs, uint32(xwaylandshell.XwaylandSurfaceV1ErrorInvalidSerial)) | |||
| 173 | + | } | |||
| 174 | + | }) | |||
| 175 | + | } | |||
| 176 | + | } | |||
| 177 | + | ||||
| 178 | + | // A window announced by the X server and a surface carrying the same serial | |||
| 179 | + | // map as one neferwl window with the X11 info; core commands reach the X side. | |||
| 180 | + | func TestX11WindowLifecycle(t *testing.T) { | |||
| 181 | + | r := newX11Rig(t) | |||
| 182 | + | win := uint32(x11Win) | |||
| 183 | + | r.x11Window(win) | |||
| 184 | + | surf := r.pair(win, x11Serial) | |||
| 185 | + | ||||
| 186 | + | m := awaitEvent[ports.WindowMapped](t, r.events) | |||
| 187 | + | if m.AppID != "Class" || m.PID != 4242 || !m.Positioned || m.Floating || m.Slot != "" { | |||
| 188 | + | t.Fatalf("mapped %+v", m) | |||
| 189 | + | } | |||
| 190 | + | ||||
| 191 | + | // Title and app id changes. | |||
| 192 | + | p := r.peer | |||
| 193 | + | p.setProp(win, p.atom("_NET_WM_NAME"), p.atom("UTF8_STRING"), 8, []byte("renamed")) | |||
| 194 | + | p.propertyNotify(win, p.atom("_NET_WM_NAME")) | |||
| 195 | + | p.setProp(win, 67, 31, 8, []byte("inst\x00Other\x00")) | |||
| 196 | + | p.propertyNotify(win, 67) | |||
| 197 | + | if a := awaitEvent[ports.WindowAppID](t, r.events); a.ID != m.ID || a.AppID != "Other" { | |||
| 198 | + | t.Fatalf("app id %+v", a) | |||
| 199 | + | } | |||
| 200 | + | var title string | |||
| 201 | + | waitTrue(t, "title", func() bool { | |||
| 202 | + | r.s.display.Do(func() { | |||
| 203 | + | if w := r.s.windows[m.ID]; w != nil { | |||
| 204 | + | title = w.title | |||
| 205 | + | } | |||
| 206 | + | }) | |||
| 207 | + | return title == "renamed" | |||
| 208 | + | }) | |||
| 209 | + | ||||
| 210 | + | // Fullscreen request from the client. | |||
| 211 | + | p.clientMessage(win, "_NET_WM_STATE", 1, p.atom("_NET_WM_STATE_FULLSCREEN")) | |||
| 212 | + | if f := awaitEvent[ports.WindowFullscreenRequest](t, r.events); f.ID != m.ID || !f.Fullscreen || f.External { | |||
| 213 | + | t.Fatalf("fullscreen %+v", f) | |||
| 214 | + | } | |||
| 215 | + | ||||
| 216 | + | // Configure and Place reach the X server as ConfigureWindow. | |||
| 217 | + | skip := len(p.requests()) | |||
| 218 | + | r.commands <- ports.ConfigureWindow{ID: m.ID, Width: 640, Height: 360, Visible: true} | |||
| 219 | + | cfg, skip := p.waitReq(skip, func(q x11Req) bool { return q.major == x11OpConfigureWindow && q.u32(4) == win }) | |||
| 220 | + | if w := binary.LittleEndian.Uint32(cfg.raw[20:]); w != 640 { | |||
| 221 | + | t.Fatalf("configured width %d, want 640 (%x)", w, cfg.raw) | |||
| 222 | + | } | |||
| 223 | + | r.commands <- ports.PlaceWindow{ID: m.ID, X: 10, Y: 20} | |||
| 224 | + | cfg, _ = p.waitReq(skip, func(q x11Req) bool { return q.major == x11OpConfigureWindow && q.u32(4) == win }) | |||
| 225 | + | if x, y := int32(binary.LittleEndian.Uint32(cfg.raw[12:])), int32(binary.LittleEndian.Uint32(cfg.raw[16:])); x != 10 || y != 20 { | |||
| 226 | + | t.Fatalf("placed at %d,%d, want 10,20", x, y) | |||
| 227 | + | } | |||
| 228 | + | ||||
| 229 | + | // Focus. | |||
| 230 | + | skip = len(p.requests()) | |||
| 231 | + | r.commands <- ports.FocusWindow{ID: m.ID} | |||
| 232 | + | if f, _ := p.waitReq(skip, func(q x11Req) bool { return q.major == x11OpSetInputFocus }); f.u32(4) != win { | |||
| 233 | + | t.Fatalf("focus on %#x, want %#x", f.u32(4), win) | |||
| 234 | + | } | |||
| 235 | + | ||||
| 236 | + | // Close: without WM_DELETE_WINDOW the client is killed. | |||
| 237 | + | skip = len(p.requests()) | |||
| 238 | + | r.commands <- ports.CloseWindow{ID: m.ID} | |||
| 239 | + | if k, _ := p.waitReq(skip, func(q x11Req) bool { return q.major == x11OpKillClient }); k.u32(4) != win { | |||
| 240 | + | t.Fatalf("killed %#x, want %#x", k.u32(4), win) | |||
| 241 | + | } | |||
| 242 | + | ||||
| 243 | + | // Destroying the surface unmaps the window. | |||
| 244 | + | requestProtocol(t, r.xc, surf, wayland.SurfaceRequestDestroy) | |||
| 245 | + | if err := r.xc.Roundtrip(); err != nil { | |||
| 246 | + | t.Fatal(err) | |||
| 247 | + | } | |||
| 248 | + | if u := awaitEvent[ports.WindowUnmapped](t, r.events); u.ID != m.ID { | |||
| 249 | + | t.Fatalf("unmapped %+v", u) | |||
| 250 | + | } | |||
| 251 | + | } | |||
| 252 | + | ||||
| 253 | + | // The serial may arrive on the Wayland side before the X side knows it, and | |||
| 254 | + | // an UnmapNotify unmaps; a remap pairs again. | |||
| 255 | + | func TestX11WindowSurfaceFirstAndRemap(t *testing.T) { | |||
| 256 | + | r := newX11Rig(t) | |||
| 257 | + | win := uint32(x11Win) | |||
| 258 | + | p := r.peer | |||
| 259 | + | surf, xs := r.xc.AllocateID(), r.xc.AllocateID() | |||
| 260 | + | requestProtocol(t, r.xc, r.comp, wayland.CompositorRequestCreateSurface, surf) | |||
| 261 | + | registerProtocol(t, r.xc, surf) | |||
| 262 | + | requestProtocol(t, r.xc, r.shell, xwaylandshell.XwaylandShellV1RequestGetXwaylandSurface, xs, surf) | |||
| 263 | + | registerProtocol(t, r.xc, xs) | |||
| 264 | + | requestProtocol(t, r.xc, xs, xwaylandshell.XwaylandSurfaceV1RequestSetSerial, uint32(x11Serial), uint32(x11Serial>>32)) | |||
| 265 | + | requestProtocol(t, r.xc, surf, wayland.SurfaceRequestAttach, shmBuffer(t, r.xc), int32(0), int32(0)) | |||
| 266 | + | requestProtocol(t, r.xc, surf, wayland.SurfaceRequestCommit) | |||
| 267 | + | if err := r.xc.Roundtrip(); err != nil { | |||
| 268 | + | t.Fatal(err) | |||
| 269 | + | } | |||
| 270 | + | r.x11Window(win) | |||
| 271 | + | p.clientMessage(win, "WL_SURFACE_SERIAL", uint32(x11Serial), uint32(x11Serial>>32)) | |||
| 272 | + | m := awaitEvent[ports.WindowMapped](t, r.events) | |||
| 273 | + | if m.Remap { | |||
| 274 | + | t.Fatalf("first map flagged as remap: %+v", m) | |||
| 275 | + | } | |||
| 276 | + | p.unmapNotify(win) | |||
| 277 | + | if u := awaitEvent[ports.WindowUnmapped](t, r.events); u.ID != m.ID { | |||
| 278 | + | t.Fatalf("unmapped %+v", u) | |||
| 279 | + | } | |||
| 280 | + | p.destroyNotify(win) | |||
| 281 | + | waitTrue(t, "window forgotten", func() bool { | |||
| 282 | + | gone := false | |||
| 283 | + | r.s.display.Do(func() { gone = r.s.windows[m.ID] == nil }) | |||
| 284 | + | return gone | |||
| 285 | + | }) | |||
| 286 | + | } | |||
| 287 | + | ||||
| 288 | + | // stop closes the XWM: its windows unmap, and the server keeps serving. | |||
| 289 | + | func TestX11StopUnmapsWindows(t *testing.T) { | |||
| 290 | + | r := newX11Rig(t) | |||
| 291 | + | win := uint32(x11Win) | |||
| 292 | + | r.x11Window(win) | |||
| 293 | + | r.pair(win, x11Serial) | |||
| 294 | + | m := awaitEvent[ports.WindowMapped](t, r.events) | |||
| 295 | + | r.stop() | |||
| 296 | + | if u := awaitEvent[ports.WindowUnmapped](t, r.events); u.ID != m.ID { | |||
| 297 | + | t.Fatalf("unmapped %+v", u) | |||
| 298 | + | } | |||
| 299 | + | r.s.display.Do(func() { | |||
| 300 | + | if r.s.xwm != nil || r.s.windows[m.ID] != nil || len(r.s.x11Slots) != 0 { | |||
| 301 | + | t.Error("XWM state left behind") | |||
| 302 | + | } | |||
| 303 | + | }) | |||
| 304 | + | r.stop() // idempotent | |||
| 305 | + | } | |||
| 306 | + | ||||
| 307 | + | // Boot gives up as soon as its context ends, even against a silent server, | |||
| 308 | + | // and leaves the server serving. | |||
| 309 | + | func TestAttachXWMBootAbortsWithContext(t *testing.T) { | |||
| 310 | + | s, _, _, dir := lifecycleServer(t) | |||
| 311 | + | fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0) | |||
| 312 | + | if err != nil { | |||
| 313 | + | t.Fatal(err) | |||
| 314 | + | } | |||
| 315 | + | defer unix.Close(fds[1]) // the silent server end | |||
| 316 | + | ctx, cancel := context.WithCancel(context.Background()) | |||
| 317 | + | done := make(chan func(), 1) | |||
| 318 | + | go func() { done <- s.AttachXWM(ctx, fds[0]) }() | |||
| 319 | + | select { | |||
| 320 | + | case <-done: | |||
| 321 | + | t.Fatal("AttachXWM returned before the context ended") | |||
| 322 | + | case <-time.After(150 * time.Millisecond): | |||
| 323 | + | } | |||
| 324 | + | cancel() | |||
| 325 | + | select { | |||
| 326 | + | case stop := <-done: | |||
| 327 | + | stop() | |||
| 328 | + | case <-time.After(2 * time.Second): | |||
| 329 | + | t.Fatal("AttachXWM did not abort with its context") | |||
| 330 | + | } | |||
| 331 | + | assertFDClosed(t, fds[0]) | |||
| 332 | + | protocolClient(t, s, dir) // still serving | |||
| 333 | + | } | |||
| 334 | + | ||||
| 335 | + | // Xwayland locks the pointer for games: the constraint reaches core for the | |||
| 336 | + | // X11 window like for an xdg toplevel. | |||
| 337 | + | func TestX11WindowPointerLock(t *testing.T) { | |||
| 338 | + | r := newX11Rig(t) | |||
| 339 | + | win := uint32(x11Win) | |||
| 340 | + | r.x11Window(win) | |||
| 341 | + | surf := r.pair(win, x11Serial) | |||
| 342 | + | m := awaitEvent[ports.WindowMapped](t, r.events) | |||
| 343 | + | ||||
| 344 | + | seat := bindProtocol(t, r.xc, "wl_seat") | |||
| 345 | + | registerProtocol(t, r.xc, seat) | |||
| 346 | + | pointer := r.xc.AllocateID() | |||
| 347 | + | requestProtocol(t, r.xc, seat, wayland.SeatRequestGetPointer, pointer) | |||
| 348 | + | registerProtocol(t, r.xc, pointer) | |||
| 349 | + | constraints := bindProtocol(t, r.xc, "zwp_pointer_constraints_v1") | |||
| 350 | + | lock := r.xc.AllocateID() | |||
| 351 | + | requestProtocol(t, r.xc, constraints, pointerconstraints.ZwpPointerConstraintsV1RequestLockPointer, lock, surf, pointer, uint32(0), uint32(pointerconstraints.ZwpPointerConstraintsV1LifetimePersistent)) | |||
| 352 | + | registerProtocol(t, r.xc, lock) | |||
| 353 | + | if err := r.xc.Roundtrip(); err != nil { | |||
| 354 | + | t.Fatal(err) | |||
| 355 | + | } | |||
| 356 | + | r.commands <- ports.PointerFocus{ID: m.ID} | |||
| 357 | + | r.commands <- ports.FocusWindow{ID: m.ID} | |||
| 358 | + | if got := constrained(t, r.events); got.ID != m.ID || got.Mode != ports.ConstraintLock { | |||
| 359 | + | t.Fatalf("constrained %+v", got) | |||
| 360 | + | } | |||
| 361 | + | } | |||
| 362 | + | ||||
| 363 | + | // grabRig is an X11 window with an Xwayland keyboard grab on its surface. | |||
| 364 | + | func grabRig(t *testing.T) (r *x11Rig, w ports.WindowID, surf uint32) { | |||
| 365 | + | t.Helper() | |||
| 366 | + | r = newX11Rig(t) | |||
| 367 | + | r.x11Window(x11Win) | |||
| 368 | + | surf = r.pair(x11Win, x11Serial) | |||
| 369 | + | w = awaitEvent[ports.WindowMapped](t, r.events).ID | |||
| 370 | + | seat := bindProtocol(t, r.xc, "wl_seat") | |||
| 371 | + | registerProtocol(t, r.xc, seat) | |||
| 372 | + | mgr := bindProtocol(t, r.xc, "zwp_xwayland_keyboard_grab_manager_v1") | |||
| 373 | + | grab := r.xc.AllocateID() | |||
| 374 | + | requestProtocol(t, r.xc, mgr, xwaylandkeyboardgrab.ZwpXwaylandKeyboardGrabManagerV1RequestGrabKeyboard, grab, surf, seat) | |||
| 375 | + | registerProtocol(t, r.xc, grab) | |||
| 376 | + | if err := r.xc.Roundtrip(); err != nil { | |||
| 377 | + | t.Fatal(err) | |||
| 378 | + | } | |||
| 379 | + | return r, w, grab | |||
| 380 | + | } | |||
| 381 | + | ||||
| 382 | + | // noInhibit fails when a ShortcutsInhibit event arrives shortly. | |||
| 383 | + | func noInhibit(t *testing.T, r *x11Rig) { | |||
| 384 | + | t.Helper() | |||
| 385 | + | if err := r.xc.Roundtrip(); err != nil { | |||
| 386 | + | t.Fatal(err) | |||
| 387 | + | } | |||
| 388 | + | for deadline := time.After(150 * time.Millisecond); ; { | |||
| 389 | + | select { | |||
| 390 | + | case ev := <-r.events: | |||
| 391 | + | if v, ok := ev.(ports.ShortcutsInhibit); ok { | |||
| 392 | + | t.Fatalf("unexpected %+v", v) | |||
| 393 | + | } | |||
| 394 | + | case <-deadline: | |||
| 395 | + | return | |||
| 396 | + | } | |||
| 397 | + | } | |||
| 398 | + | } | |||
| 399 | + | ||||
| 400 | + | func TestXwaylandKeyboardGrabThreeStates(t *testing.T) { | |||
| 401 | + | r, w, grab := grabRig(t) | |||
| 402 | + | // Windowed and focused: accepted, no effect. | |||
| 403 | + | r.commands <- ports.ConfigureWindow{ID: w, Width: 100, Height: 100, Visible: true} | |||
| 404 | + | r.commands <- ports.FocusWindow{ID: w} | |||
| 405 | + | noInhibit(t, r) | |||
| 406 | + | // Focused and fullscreen: inhibits. | |||
| 407 | + | r.commands <- ports.ConfigureWindow{ID: w, Width: 100, Height: 100, Visible: true, Fullscreen: true} | |||
| 408 | + | if v := awaitEvent[ports.ShortcutsInhibit](t, r.events); v != (ports.ShortcutsInhibit{Window: w, Active: true}) { | |||
| 409 | + | t.Fatal(v) | |||
| 410 | + | } | |||
| 411 | + | // Focus lost: released. | |||
| 412 | + | r.commands <- ports.FocusWindow{} | |||
| 413 | + | if v := awaitEvent[ports.ShortcutsInhibit](t, r.events); v != (ports.ShortcutsInhibit{Window: w}) { | |||
| 414 | + | t.Fatal(v) | |||
| 415 | + | } | |||
| 416 | + | // Back, then the grab object goes away. | |||
| 417 | + | r.commands <- ports.FocusWindow{ID: w} | |||
| 418 | + | awaitEvent[ports.ShortcutsInhibit](t, r.events) | |||
| 419 | + | requestProtocol(t, r.xc, grab, xwaylandkeyboardgrab.ZwpXwaylandKeyboardGrabV1RequestDestroy) | |||
| 420 | + | if v := awaitEvent[ports.ShortcutsInhibit](t, r.events); v != (ports.ShortcutsInhibit{Window: w}) { | |||
| 421 | + | t.Fatal(v) | |||
| 422 | + | } | |||
| 423 | + | } | |||
| 424 | + | ||||
| 425 | + | // x11Pointer gives the Xwayland client a wl_pointer and a relative pointer, | |||
| 426 | + | // both with event logs. | |||
| 427 | + | func (r *x11Rig) x11Pointer() (pointer uint32, ptrEvents, relEvents chan []float64) { | |||
| 428 | + | r.t.Helper() | |||
| 429 | + | seat := bindProtocol(r.t, r.xc, "wl_seat") | |||
| 430 | + | registerProtocol(r.t, r.xc, seat) | |||
| 431 | + | pointer = r.xc.AllocateID() | |||
| 432 | + | ptrEvents = logProxy(r.t, r.xc, pointer) | |||
| 433 | + | requestProtocol(r.t, r.xc, seat, wayland.SeatRequestGetPointer, pointer) | |||
| 434 | + | mgr := bindProtocol(r.t, r.xc, "zwp_relative_pointer_manager_v1") | |||
| 435 | + | rel := r.xc.AllocateID() | |||
| 436 | + | relEvents = logProxy(r.t, r.xc, rel) | |||
| 437 | + | requestProtocol(r.t, r.xc, mgr, relativepointer.ZwpRelativePointerManagerV1RequestGetRelativePointer, rel, pointer) | |||
| 438 | + | return pointer, ptrEvents, relEvents | |||
| 439 | + | } | |||
| 440 | + | ||||
| 441 | + | // A lock on an X11 surface activates with pointer and keyboard focus, sends | |||
| 442 | + | // relative motion to the Xwayland client and deactivates when a native | |||
| 443 | + | // window takes the focus; it comes back with the X11 focus. | |||
| 444 | + | func TestX11LockFollowsFocusAndRelativeMotion(t *testing.T) { | |||
| 445 | + | r := newX11Rig(t) | |||
| 446 | + | win := uint32(x11Win) | |||
| 447 | + | r.x11Window(win) | |||
| 448 | + | surf := r.pair(win, x11Serial) | |||
| 449 | + | m := awaitEvent[ports.WindowMapped](t, r.events) | |||
| 450 | + | pointer, _, relEvents := r.x11Pointer() | |||
| 451 | + | constraints := bindProtocol(t, r.xc, "zwp_pointer_constraints_v1") | |||
| 452 | + | lock := r.xc.AllocateID() | |||
| 453 | + | lockEvents := logProxy(t, r.xc, lock) | |||
| 454 | + | requestProtocol(t, r.xc, constraints, pointerconstraints.ZwpPointerConstraintsV1RequestLockPointer, lock, surf, pointer, uint32(0), uint32(pointerconstraints.ZwpPointerConstraintsV1LifetimePersistent)) | |||
| 455 | + | if err := r.xc.Roundtrip(); err != nil { | |||
| 456 | + | t.Fatal(err) | |||
| 457 | + | } | |||
| 458 | + | if len(lockEvents) > 0 { | |||
| 459 | + | t.Fatal("locked without focus") | |||
| 460 | + | } | |||
| 461 | + | ||||
| 462 | + | r.commands <- ports.PointerFocus{ID: m.ID} | |||
| 463 | + | r.commands <- ports.FocusWindow{ID: m.ID} | |||
| 464 | + | if ev := next(t, r.xc, lockEvents); ev[0] != float64(pointerconstraints.ZwpLockedPointerV1EventLocked) { | |||
| 465 | + | t.Fatalf("lock event %v", ev) | |||
| 466 | + | } | |||
| 467 | + | if got := constrained(t, r.events); got.ID != m.ID || got.Mode != ports.ConstraintLock || got.Rect != (ports.Rect{}) { | |||
| 468 | + | t.Fatalf("constrained %+v", got) | |||
| 469 | + | } | |||
| 470 | + | ||||
| 471 | + | r.commands <- ports.PointerMotionTo{ID: m.ID, X: 5, Y: 5, DX: 2.5, DY: -1, UnaccelDX: 4, UnaccelDY: -2} | |||
| 472 | + | if ev := next(t, r.xc, relEvents); len(ev) != 5 || ev[1] != 2.5 || ev[2] != -1 || ev[3] != 4 || ev[4] != -2 { | |||
| 473 | + | t.Fatalf("relative motion %v", ev) | |||
| 474 | + | } | |||
| 475 | + | ||||
| 476 | + | // A native window takes the focus: the X11 lock ends and the XWM drops | |||
| 477 | + | // the X focus. | |||
| 478 | + | nc := protocolClient(t, r.s, r.dir) | |||
| 479 | + | nm, _, _ := surfaceMapper(t, nc, r.events)() | |||
| 480 | + | skip := len(r.peer.requests()) | |||
| 481 | + | r.commands <- ports.FocusWindow{ID: nm.ID} | |||
| 482 | + | if ev := next(t, r.xc, lockEvents); ev[0] != float64(pointerconstraints.ZwpLockedPointerV1EventUnlocked) { | |||
| 483 | + | t.Fatalf("unlock event %v", ev) | |||
| 484 | + | } | |||
| 485 | + | if got := constrained(t, r.events); got.ID != 0 { | |||
| 486 | + | t.Fatalf("constrained %+v after native focus", got) | |||
| 487 | + | } | |||
| 488 | + | if f, _ := r.peer.waitReq(skip, func(q x11Req) bool { return q.major == x11OpSetInputFocus }); f.u32(4) == win { | |||
| 489 | + | t.Fatalf("X focus still on %#x", win) | |||
| 490 | + | } | |||
| 491 | + | ||||
| 492 | + | r.commands <- ports.FocusWindow{ID: m.ID} | |||
| 493 | + | if ev := next(t, r.xc, lockEvents); ev[0] != float64(pointerconstraints.ZwpLockedPointerV1EventLocked) { | |||
| 494 | + | t.Fatalf("relock event %v", ev) | |||
| 495 | + | } | |||
| 496 | + | } | |||
| 497 | + | ||||
| 498 | + | // A confine on an X11 surface reaches core with its region as given: an X11 | |||
| 499 | + | // window has no geometry offset. | |||
| 500 | + | func TestX11ConfineRegionHasNoOffset(t *testing.T) { | |||
| 501 | + | r := newX11Rig(t) | |||
| 502 | + | win := uint32(x11Win) | |||
| 503 | + | r.x11Window(win) | |||
| 504 | + | surf := r.pair(win, x11Serial) | |||
| 505 | + | m := awaitEvent[ports.WindowMapped](t, r.events) | |||
| 506 | + | pointer, _, _ := r.x11Pointer() | |||
| 507 | + | region := r.xc.AllocateID() | |||
| 508 | + | registerProtocol(t, r.xc, region) | |||
| 509 | + | requestProtocol(t, r.xc, r.comp, wayland.CompositorRequestCreateRegion, region) | |||
| 510 | + | requestProtocol(t, r.xc, region, wayland.RegionRequestAdd, int32(5), int32(6), int32(20), int32(30)) | |||
| 511 | + | constraints := bindProtocol(t, r.xc, "zwp_pointer_constraints_v1") | |||
| 512 | + | confine := r.xc.AllocateID() | |||
| 513 | + | confineEvents := logProxy(t, r.xc, confine) | |||
| 514 | + | requestProtocol(t, r.xc, constraints, pointerconstraints.ZwpPointerConstraintsV1RequestConfinePointer, confine, surf, pointer, region, uint32(pointerconstraints.ZwpPointerConstraintsV1LifetimePersistent)) | |||
| 515 | + | if err := r.xc.Roundtrip(); err != nil { | |||
| 516 | + | t.Fatal(err) | |||
| 517 | + | } | |||
| 518 | + | r.state(func() { | |||
| 519 | + | if g := r.s.windows[m.ID].geometry(); g != (ports.Rect{}) { | |||
| 520 | + | t.Errorf("X11 geometry %+v, want zero", g) | |||
| 521 | + | } | |||
| 522 | + | }) | |||
| 523 | + | ||||
| 524 | + | r.commands <- ports.PointerFocus{ID: m.ID, X: 10, Y: 10} | |||
| 525 | + | r.commands <- ports.FocusWindow{ID: m.ID} | |||
| 526 | + | r.commands <- ports.PointerMotionTo{ID: m.ID, X: 10, Y: 10} | |||
| 527 | + | if ev := next(t, r.xc, confineEvents); ev[0] != float64(pointerconstraints.ZwpConfinedPointerV1EventConfined) { | |||
| 528 | + | t.Fatalf("confine event %v", ev) | |||
| 529 | + | } | |||
| 530 | + | want := ports.PointerConstrained{ID: m.ID, PointerConstraint: ports.PointerConstraint{Mode: ports.ConstraintConfine, Rect: ports.Rect{X: 5, Y: 6, W: 20, H: 30}}} | |||
| 531 | + | if got := constrained(t, r.events); got != want { | |||
| 532 | + | t.Fatalf("constrained %+v, want %+v", got, want) | |||
| 533 | + | } | |||
| 534 | + | } | |||
| 535 | + | ||||
| 536 | + | // waitOn waits until window id's surface is on output name: commands reach | |||
| 537 | + | // the display goroutine asynchronously. | |||
| 538 | + | func (r *x11Rig) waitOn(id ports.WindowID, name string) { | |||
| 539 | + | r.t.Helper() | |||
| 540 | + | waitTrue(r.t, "surface on "+name, func() bool { | |||
| 541 | + | ok := false | |||
| 542 | + | r.state(func() { | |||
| 543 | + | if w := r.s.windows[id]; w != nil { | |||
| 544 | + | on := w.surf().on | |||
| 545 | + | ok = on != nil && on.name() == name | |||
| 546 | + | } | |||
| 547 | + | }) | |||
| 548 | + | return ok | |||
| 549 | + | }) | |||
| 550 | + | } | |||
| 551 | + | ||||
| 552 | + | // An X11 window is on one output only, even straddling two: a single | |||
| 553 | + | // wl_surface.enter, and a leave only when its output changes. | |||
| 554 | + | func TestX11WindowEntersOneOutput(t *testing.T) { | |||
| 555 | + | r := newX11Rig(t) | |||
| 556 | + | r.commands <- ports.SetOutputs{Outputs: ports.Layout{ | |||
| 557 | + | {Info: ports.OutputInfo{Name: "A"}, X: 0, Y: 0, Width: 1920, Height: 1080, Scale: 1}, | |||
| 558 | + | {Info: ports.OutputInfo{Name: "B"}, X: 1920, Y: 0, Width: 1920, Height: 1080, Scale: 1}, | |||
| 559 | + | }, Focused: "A"} | |||
| 560 | + | r.state(func() {}) // the layout is applied | |||
| 561 | + | if err := r.xc.Roundtrip(); err != nil { | |||
| 562 | + | t.Fatal(err) | |||
| 563 | + | } | |||
| 564 | + | outputs := 0 | |||
| 565 | + | for _, g := range r.xc.Registry().GetGlobals() { | |||
| 566 | + | if g.Interface != "wl_output" { | |||
| 567 | + | continue | |||
| 568 | + | } | |||
| 569 | + | id, err := bindWireID(r.xc, g.Name, g.Interface, 1) | |||
| 570 | + | if err != nil { | |||
| 571 | + | t.Fatal(err) | |||
| 572 | + | } | |||
| 573 | + | registerProtocol(t, r.xc, id) | |||
| 574 | + | outputs++ | |||
| 575 | + | } | |||
| 576 | + | if outputs != 2 { | |||
| 577 | + | t.Fatalf("%d wl_output globals, want 2", outputs) | |||
| 578 | + | } | |||
| 579 | + | ||||
| 580 | + | win := uint32(x11Win) | |||
| 581 | + | r.x11Window(win) | |||
| 582 | + | events := make(chan []float64, 64) | |||
| 583 | + | r.surfaceProxy = func(id uint32) wlturbo.Proxy { | |||
| 584 | + | p := &eventLog{events: events} | |||
| 585 | + | p.SetID(id) | |||
| 586 | + | return p | |||
| 587 | + | } | |||
| 588 | + | r.pair(win, x11Serial) | |||
| 589 | + | m := awaitEvent[ports.WindowMapped](t, r.events) | |||
| 590 | + | ||||
| 591 | + | // Straddling both outputs, configured on A. | |||
| 592 | + | r.commands <- ports.PlaceWindow{ID: m.ID, X: 1900, Y: 0} | |||
| 593 | + | r.commands <- ports.ConfigureWindow{ID: m.ID, Width: 200, Height: 100, Visible: true, Output: "A"} | |||
| 594 | + | r.waitOn(m.ID, "A") | |||
| 595 | + | if err := r.xc.Roundtrip(); err != nil { | |||
| 596 | + | t.Fatal(err) | |||
| 597 | + | } | |||
| 598 | + | count := func() (enters, leaves int) { | |||
| 599 | + | for len(events) > 0 { | |||
| 600 | + | switch ev := <-events; ev[0] { | |||
| 601 | + | case float64(wayland.SurfaceEventEnter): | |||
| 602 | + | enters++ | |||
| 603 | + | case float64(wayland.SurfaceEventLeave): | |||
| 604 | + | leaves++ | |||
| 605 | + | } | |||
| 606 | + | } | |||
| 607 | + | return | |||
| 608 | + | } | |||
| 609 | + | if e, l := count(); e != 1 || l != 0 { | |||
| 610 | + | t.Fatalf("straddling window: %d enters, %d leaves, want 1, 0", e, l) | |||
| 611 | + | } | |||
| 612 | + | r.state(func() { | |||
| 613 | + | if on := r.s.windows[m.ID].surf().on; on == nil || on.name() != "A" { | |||
| 614 | + | t.Errorf("surface on %v, want A", on) | |||
| 615 | + | } | |||
| 616 | + | }) | |||
| 617 | + | ||||
| 618 | + | // Moving to B leaves A and enters B once. | |||
| 619 | + | r.commands <- ports.ConfigureWindow{ID: m.ID, Width: 200, Height: 100, Visible: true, Output: "B"} | |||
| 620 | + | r.waitOn(m.ID, "B") | |||
| 621 | + | if err := r.xc.Roundtrip(); err != nil { | |||
| 622 | + | t.Fatal(err) | |||
| 623 | + | } | |||
| 624 | + | if e, l := count(); e != 1 || l != 1 { | |||
| 625 | + | t.Fatalf("move to B: %d enters, %d leaves, want 1, 1", e, l) | |||
| 626 | + | } | |||
| 627 | + | } | |||
M internal/adapters/wayland/xdg.go +32 -21
| @@ -128,10 +128,12 @@ func (x *xdgSurface) Destroy(r *xdgshell.Surface) { | |||
| 128 | 128 | ||
| 129 | 129 | // window is an xdg_toplevel, or an xdg_popup when popup is set. | |
| 130 | 130 | type window struct { | |
| 131 | - | id ports.WindowID | |
| 132 | - | toplevel *xdgshell.Toplevel | |
| 133 | - | popup *popup | |
| 134 | - | xdg *xdgSurface | |
| 131 | + | id ports.WindowID | |
| 132 | + | toplevel *xdgshell.Toplevel | |
| 133 | + | popup *popup | |
| 134 | + | xdg *xdgSurface | |
| 135 | + | // x11 is set for the window of an X11 client (x11window.go). | |
| 136 | + | x11 *x11Window | |
| 135 | 137 | appID, title string | |
| 136 | 138 | mapped bool | |
| 137 | 139 | // gen counts the mappings: an unmap and a remap is a new toplevel to | |
| @@ -151,8 +153,8 @@ type window struct { | |||
| 151 | 153 | hasLast bool | |
| 152 | 154 | } | |
| 153 | 155 | ||
| 154 | - | // sendConfigure sends w.last as xdg_toplevel + xdg_surface configure. | |
| 155 | - | func (w *window) sendConfigure() { | |
| 156 | + | // sendXDGConfigure sends w.last as xdg_toplevel + xdg_surface configure. | |
| 157 | + | func (w *window) sendXDGConfigure() { | |
| 156 | 158 | c := w.last | |
| 157 | 159 | w.toplevel.SendConfigure(int32(c.Width), int32(c.Height), toplevelStates(c, w.toplevel.Version())) | |
| 158 | 160 | s := w.xdg.server | |
| @@ -192,24 +194,29 @@ func toplevelStates(c ports.ConfigureWindow, version int32) []byte { | |||
| 192 | 194 | } | |
| 193 | 195 | ||
| 194 | 196 | func (w *window) unmap() { | |
| 195 | - | if w.xdg.server.seat.pointerFocus == w.id { | |
| 196 | - | w.xdg.server.changePointerFocus(0, 0, 0) | |
| 197 | + | s := w.srv() | |
| 198 | + | if s.seat.pointerFocus == w.id { | |
| 199 | + | s.changePointerFocus(0, 0, 0) | |
| 197 | 200 | } | |
| 198 | - | if w.xdg.server.seat.focused == w.id { | |
| 199 | - | w.xdg.server.changeFocus(0) | |
| 201 | + | if s.seat.focused == w.id { | |
| 202 | + | s.changeFocus(0) | |
| 200 | 203 | } | |
| 201 | 204 | if !w.mapped { | |
| 202 | 205 | return | |
| 203 | 206 | } | |
| 204 | 207 | w.mapped = false | |
| 205 | - | w.xdg.server.toplevelClosed(w.id) | |
| 206 | - | w.xdg.surface.resetInputEmission() | |
| 207 | - | w.xdg.configured = false | |
| 208 | - | w.xdg.acked = false | |
| 209 | - | w.xdg.serials = nil | |
| 210 | - | w.xdg.server.emit(ports.WindowUnmapped{ID: w.id}) | |
| 211 | - | w.xdg.server.syncInhibitors() | |
| 212 | - | w.xdg.server.emitContent(ports.SurfaceContent{ID: w.id}, damage{full: true}) | |
| 208 | + | s.toplevelClosed(w.id) | |
| 209 | + | if surf := w.surf(); surf != nil { | |
| 210 | + | surf.resetInputEmission() | |
| 211 | + | } | |
| 212 | + | if x := w.xdg; x != nil { | |
| 213 | + | x.configured = false | |
| 214 | + | x.acked = false | |
| 215 | + | x.serials = nil | |
| 216 | + | } | |
| 217 | + | s.emit(ports.WindowUnmapped{ID: w.id}) | |
| 218 | + | s.syncInhibitors() | |
| 219 | + | s.emitContent(ports.SurfaceContent{ID: w.id}, damage{full: true}) | |
| 213 | 220 | } | |
| 214 | 221 | func (x *xdgSurface) GetToplevel(r *xdgshell.Surface, id uint32) { | |
| 215 | 222 | if x.window != nil { | |
| @@ -337,7 +344,7 @@ func (w *window) setParent(p *window) { | |||
| 337 | 344 | if p != nil && p.mapped { | |
| 338 | 345 | id = p.id | |
| 339 | 346 | } | |
| 340 | - | w.xdg.server.emit(ports.WindowParent{ID: w.id, Parent: id}) | |
| 347 | + | w.srv().emit(ports.WindowParent{ID: w.id, Parent: id}) | |
| 341 | 348 | } | |
| 342 | 349 | func (t top) SetTitle(_ *xdgshell.Toplevel, title string) { | |
| 343 | 350 | if title == t.w.title { | |
| @@ -417,7 +424,7 @@ func (w *window) afterCommit() { | |||
| 417 | 424 | } | |
| 418 | 425 | if fw, fh := w.size(); fw > 0 && fh > 0 && (fw != w.floatW || fh != w.floatH) { | |
| 419 | 426 | w.floatW, w.floatH = fw, fh | |
| 420 | - | w.xdg.server.emit(ports.WindowResized{ID: w.id, Width: fw, Height: fh}) | |
| 427 | + | w.srv().emit(ports.WindowResized{ID: w.id, Width: fw, Height: fh}) | |
| 421 | 428 | } | |
| 422 | 429 | } | |
| 423 | 430 | ||
| @@ -431,6 +438,10 @@ func (w *window) floats() bool { | |||
| 431 | 438 | ||
| 432 | 439 | // size is the window geometry size, else the surface size, in logical pixels. | |
| 433 | 440 | func (w *window) size() (int, int) { | |
| 441 | + | if w.xdg == nil { | |
| 442 | + | c := w.surf().content | |
| 443 | + | return c.LogicalW, c.LogicalH | |
| 444 | + | } | |
| 434 | 445 | if g := w.xdg.geometry; g.W > 0 && g.H > 0 { | |
| 435 | 446 | return g.W, g.H | |
| 436 | 447 | } | |
| @@ -441,7 +452,7 @@ func (w *window) size() (int, int) { | |||
| 441 | 452 | // surfacePoint turns window coordinates (core's, from the geometry origin) | |
| 442 | 453 | // into surface coordinates. | |
| 443 | 454 | func (w *window) surfacePoint(x, y float64) (float64, float64) { | |
| 444 | - | g := w.xdg.geometry | |
| 455 | + | g := w.geometry() | |
| 445 | 456 | return x + float64(g.X), y + float64(g.Y) | |
| 446 | 457 | } | |
| 447 | 458 | ||
| @@ -128,10 +128,12 @@ func (x *xdgSurface) Destroy(r *xdgshell.Surface) { | |||||
| 128 | 128 | ||||
| 129 | // window is an xdg_toplevel, or an xdg_popup when popup is set. | 129 | // window is an xdg_toplevel, or an xdg_popup when popup is set. | ||
| 130 | type window struct { | 130 | type window struct { | ||
| 131 | - | id ports.WindowID | 131 | + | id ports.WindowID |
| 132 | - | toplevel *xdgshell.Toplevel | 132 | + | toplevel *xdgshell.Toplevel |
| 133 | - | popup *popup | 133 | + | popup *popup |
| 134 | - | xdg *xdgSurface | 134 | + | xdg *xdgSurface |
| 135 | + | // x11 is set for the window of an X11 client (x11window.go). | |||
| 136 | + | x11 *x11Window | |||
| 135 | appID, title string | 137 | appID, title string | ||
| 136 | mapped bool | 138 | mapped bool | ||
| 137 | // gen counts the mappings: an unmap and a remap is a new toplevel to | 139 | // gen counts the mappings: an unmap and a remap is a new toplevel to | ||
| @@ -151,8 +153,8 @@ type window struct { | |||||
| 151 | hasLast bool | 153 | hasLast bool | ||
| 152 | } | 154 | } | ||
| 153 | 155 | ||||
| 154 | - | // sendConfigure sends w.last as xdg_toplevel + xdg_surface configure. | 156 | + | // sendXDGConfigure sends w.last as xdg_toplevel + xdg_surface configure. |
| 155 | - | func (w *window) sendConfigure() { | 157 | + | func (w *window) sendXDGConfigure() { |
| 156 | c := w.last | 158 | c := w.last | ||
| 157 | w.toplevel.SendConfigure(int32(c.Width), int32(c.Height), toplevelStates(c, w.toplevel.Version())) | 159 | w.toplevel.SendConfigure(int32(c.Width), int32(c.Height), toplevelStates(c, w.toplevel.Version())) | ||
| 158 | s := w.xdg.server | 160 | s := w.xdg.server | ||
| @@ -192,24 +194,29 @@ func toplevelStates(c ports.ConfigureWindow, version int32) []byte { | |||||
| 192 | } | 194 | } | ||
| 193 | 195 | ||||
| 194 | func (w *window) unmap() { | 196 | func (w *window) unmap() { | ||
| 195 | - | if w.xdg.server.seat.pointerFocus == w.id { | 197 | + | s := w.srv() |
| 196 | - | w.xdg.server.changePointerFocus(0, 0, 0) | 198 | + | if s.seat.pointerFocus == w.id { |
| 199 | + | s.changePointerFocus(0, 0, 0) | |||
| 197 | } | 200 | } | ||
| 198 | - | if w.xdg.server.seat.focused == w.id { | 201 | + | if s.seat.focused == w.id { |
| 199 | - | w.xdg.server.changeFocus(0) | 202 | + | s.changeFocus(0) |
| 200 | } | 203 | } | ||
| 201 | if !w.mapped { | 204 | if !w.mapped { | ||
| 202 | return | 205 | return | ||
| 203 | } | 206 | } | ||
| 204 | w.mapped = false | 207 | w.mapped = false | ||
| 205 | - | w.xdg.server.toplevelClosed(w.id) | 208 | + | s.toplevelClosed(w.id) |
| 206 | - | w.xdg.surface.resetInputEmission() | 209 | + | if surf := w.surf(); surf != nil { |
| 207 | - | w.xdg.configured = false | 210 | + | surf.resetInputEmission() |
| 208 | - | w.xdg.acked = false | 211 | + | } |
| 209 | - | w.xdg.serials = nil | 212 | + | if x := w.xdg; x != nil { |
| 210 | - | w.xdg.server.emit(ports.WindowUnmapped{ID: w.id}) | 213 | + | x.configured = false |
| 211 | - | w.xdg.server.syncInhibitors() | 214 | + | x.acked = false |
| 212 | - | w.xdg.server.emitContent(ports.SurfaceContent{ID: w.id}, damage{full: true}) | 215 | + | x.serials = nil |
| 216 | + | } | |||
| 217 | + | s.emit(ports.WindowUnmapped{ID: w.id}) | |||
| 218 | + | s.syncInhibitors() | |||
| 219 | + | s.emitContent(ports.SurfaceContent{ID: w.id}, damage{full: true}) | |||
| 213 | } | 220 | } | ||
| 214 | func (x *xdgSurface) GetToplevel(r *xdgshell.Surface, id uint32) { | 221 | func (x *xdgSurface) GetToplevel(r *xdgshell.Surface, id uint32) { | ||
| 215 | if x.window != nil { | 222 | if x.window != nil { | ||
| @@ -337,7 +344,7 @@ func (w *window) setParent(p *window) { | |||||
| 337 | if p != nil && p.mapped { | 344 | if p != nil && p.mapped { | ||
| 338 | id = p.id | 345 | id = p.id | ||
| 339 | } | 346 | } | ||
| 340 | - | w.xdg.server.emit(ports.WindowParent{ID: w.id, Parent: id}) | 347 | + | w.srv().emit(ports.WindowParent{ID: w.id, Parent: id}) |
| 341 | } | 348 | } | ||
| 342 | func (t top) SetTitle(_ *xdgshell.Toplevel, title string) { | 349 | func (t top) SetTitle(_ *xdgshell.Toplevel, title string) { | ||
| 343 | if title == t.w.title { | 350 | if title == t.w.title { | ||
| @@ -417,7 +424,7 @@ func (w *window) afterCommit() { | |||||
| 417 | } | 424 | } | ||
| 418 | if fw, fh := w.size(); fw > 0 && fh > 0 && (fw != w.floatW || fh != w.floatH) { | 425 | if fw, fh := w.size(); fw > 0 && fh > 0 && (fw != w.floatW || fh != w.floatH) { | ||
| 419 | w.floatW, w.floatH = fw, fh | 426 | w.floatW, w.floatH = fw, fh | ||
| 420 | - | w.xdg.server.emit(ports.WindowResized{ID: w.id, Width: fw, Height: fh}) | 427 | + | w.srv().emit(ports.WindowResized{ID: w.id, Width: fw, Height: fh}) |
| 421 | } | 428 | } | ||
| 422 | } | 429 | } | ||
| 423 | 430 | ||||
| @@ -431,6 +438,10 @@ func (w *window) floats() bool { | |||||
| 431 | 438 | ||||
| 432 | // size is the window geometry size, else the surface size, in logical pixels. | 439 | // size is the window geometry size, else the surface size, in logical pixels. | ||
| 433 | func (w *window) size() (int, int) { | 440 | func (w *window) size() (int, int) { | ||
| 441 | + | if w.xdg == nil { | |||
| 442 | + | c := w.surf().content | |||
| 443 | + | return c.LogicalW, c.LogicalH | |||
| 444 | + | } | |||
| 434 | if g := w.xdg.geometry; g.W > 0 && g.H > 0 { | 445 | if g := w.xdg.geometry; g.W > 0 && g.H > 0 { | ||
| 435 | return g.W, g.H | 446 | return g.W, g.H | ||
| 436 | } | 447 | } | ||
| @@ -441,7 +452,7 @@ func (w *window) size() (int, int) { | |||||
| 441 | // surfacePoint turns window coordinates (core's, from the geometry origin) | 452 | // surfacePoint turns window coordinates (core's, from the geometry origin) | ||
| 442 | // into surface coordinates. | 453 | // into surface coordinates. | ||
| 443 | func (w *window) surfacePoint(x, y float64) (float64, float64) { | 454 | func (w *window) surfacePoint(x, y float64) (float64, float64) { | ||
| 444 | - | g := w.xdg.geometry | 455 | + | g := w.geometry() |
| 445 | return x + float64(g.X), y + float64(g.Y) | 456 | return x + float64(g.X), y + float64(g.Y) | ||
| 446 | } | 457 | } | ||
| 447 | 458 | ||||
A internal/adapters/wayland/xdgoutputx_test.go +143 -0
| @@ -0,0 +1,143 @@ | |||
| 1 | + | package wayland | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "slices" | |
| 5 | + | "testing" | |
| 6 | + | ||
| 7 | + | "github.com/bnema/neferwl/internal/ports" | |
| 8 | + | "github.com/bnema/purego-libwayland/server" | |
| 9 | + | "github.com/bnema/wlturbo" | |
| 10 | + | ) | |
| 11 | + | ||
| 12 | + | // xdgPosProxy records the logical_position events of one zxdg_output_v1. | |
| 13 | + | type xdgPosProxy struct { | |
| 14 | + | wlturbo.BaseProxy | |
| 15 | + | pos [][2]int32 | |
| 16 | + | } | |
| 17 | + | ||
| 18 | + | func (p *xdgPosProxy) Dispatch(e *wlturbo.Event) { | |
| 19 | + | if e.Opcode == 0 { | |
| 20 | + | p.pos = append(p.pos, [2]int32{e.Int32(), e.Int32()}) | |
| 21 | + | } | |
| 22 | + | } | |
| 23 | + | ||
| 24 | + | // xdgOutputsOf binds the xdg-output manager and an xdg-output for every | |
| 25 | + | // wl_output of c, and returns what each one reported. | |
| 26 | + | func xdgOutputsOf(t *testing.T, c *wlturbo.Display) []*xdgPosProxy { | |
| 27 | + | t.Helper() | |
| 28 | + | if err := c.Roundtrip(); err != nil { | |
| 29 | + | t.Fatal(err) | |
| 30 | + | } | |
| 31 | + | var mgrs int | |
| 32 | + | var manager uint32 | |
| 33 | + | for _, g := range c.Registry().GetGlobals() { | |
| 34 | + | if g.Interface == "zxdg_output_manager_v1" { | |
| 35 | + | mgrs++ | |
| 36 | + | id, err := bindWireID(c, g.Name, g.Interface, 3) | |
| 37 | + | if err != nil { | |
| 38 | + | t.Fatal(err) | |
| 39 | + | } | |
| 40 | + | registerProtocol(t, c, id) | |
| 41 | + | manager = id | |
| 42 | + | } | |
| 43 | + | } | |
| 44 | + | if mgrs != 1 { | |
| 45 | + | t.Fatalf("client sees %d zxdg_output_manager_v1 globals, want 1", mgrs) | |
| 46 | + | } | |
| 47 | + | var out []*xdgPosProxy | |
| 48 | + | for _, g := range c.Registry().GetGlobals() { | |
| 49 | + | if g.Interface != "wl_output" { | |
| 50 | + | continue | |
| 51 | + | } | |
| 52 | + | wo, err := bindWireID(c, g.Name, g.Interface, 4) | |
| 53 | + | if err != nil { | |
| 54 | + | t.Fatal(err) | |
| 55 | + | } | |
| 56 | + | xp := &xdgPosProxy{} | |
| 57 | + | xp.SetID(c.AllocateID()) | |
| 58 | + | registerWireProxy(c, xp) | |
| 59 | + | requestProtocol(t, c, manager, 1, xp.ID(), wo) | |
| 60 | + | out = append(out, xp) | |
| 61 | + | } | |
| 62 | + | if err := c.Roundtrip(); err != nil { | |
| 63 | + | t.Fatal(err) | |
| 64 | + | } | |
| 65 | + | return out | |
| 66 | + | } | |
| 67 | + | ||
| 68 | + | func lastPositions(ps []*xdgPosProxy) [][2]int32 { | |
| 69 | + | var r [][2]int32 | |
| 70 | + | for _, p := range ps { | |
| 71 | + | r = append(r, p.pos[len(p.pos)-1]) | |
| 72 | + | } | |
| 73 | + | slices.SortFunc(r, func(a, b [2]int32) int { return int(a[0] - b[0]) }) | |
| 74 | + | return r | |
| 75 | + | } | |
| 76 | + | ||
| 77 | + | func twoOutputs(ax int) ports.SetOutputs { | |
| 78 | + | mk := func(name string, x int) ports.OutputPlacement { | |
| 79 | + | return ports.OutputPlacement{Info: ports.OutputInfo{Name: name, Width: 1920, Height: 1080}, X: x, Y: 0, Width: 1920, Height: 1080, Scale: 1} | |
| 80 | + | } | |
| 81 | + | return ports.SetOutputs{Outputs: ports.Layout{mk("A", ax), mk("B", 0)}} | |
| 82 | + | } | |
| 83 | + | ||
| 84 | + | // Xwayland gets X-space positions (layout top-left is 0,0) and re-receives | |
| 85 | + | // them on a layout change; every other client keeps the layout coordinates. | |
| 86 | + | func TestXwaylandPrivateXDGOutput(t *testing.T) { | |
| 87 | + | r := newX11Rig(t) | |
| 88 | + | applyOutputs(t, r.s, r.commands, twoOutputs(-1920)) | |
| 89 | + | other := protocolClient(t, r.s, r.dir) | |
| 90 | + | xo := xdgOutputsOf(t, r.xc) | |
| 91 | + | oo := xdgOutputsOf(t, other) | |
| 92 | + | if got, want := lastPositions(xo), [][2]int32{{0, 0}, {1920, 0}}; !slices.Equal(got, want) { | |
| 93 | + | t.Errorf("Xwayland positions %v, want %v", got, want) | |
| 94 | + | } | |
| 95 | + | if got, want := lastPositions(oo), [][2]int32{{-1920, 0}, {0, 0}}; !slices.Equal(got, want) { | |
| 96 | + | t.Errorf("ordinary positions %v, want %v", got, want) | |
| 97 | + | } | |
| 98 | + | applyOutputs(t, r.s, r.commands, twoOutputs(-3840)) | |
| 99 | + | if err := r.xc.Roundtrip(); err != nil { | |
| 100 | + | t.Fatal(err) | |
| 101 | + | } | |
| 102 | + | if err := other.Roundtrip(); err != nil { | |
| 103 | + | t.Fatal(err) | |
| 104 | + | } | |
| 105 | + | if got, want := lastPositions(xo), [][2]int32{{0, 0}, {3840, 0}}; !slices.Equal(got, want) { | |
| 106 | + | t.Errorf("Xwayland positions after change %v, want %v", got, want) | |
| 107 | + | } | |
| 108 | + | if got, want := lastPositions(oo), [][2]int32{{-3840, 0}, {0, 0}}; !slices.Equal(got, want) { | |
| 109 | + | t.Errorf("ordinary positions after change %v, want %v", got, want) | |
| 110 | + | } | |
| 111 | + | // Hotplug: a third output on the right grows the extents. | |
| 112 | + | third := twoOutputs(-3840) | |
| 113 | + | third.Outputs = append(third.Outputs, ports.OutputPlacement{Info: ports.OutputInfo{Name: "C", Width: 1920, Height: 1080}, X: 1920, Width: 1920, Height: 1080, Scale: 1}) | |
| 114 | + | applyOutputs(t, r.s, r.commands, third) | |
| 115 | + | if err := r.xc.Roundtrip(); err != nil { | |
| 116 | + | t.Fatal(err) | |
| 117 | + | } | |
| 118 | + | if got := lastPositions(xo); len(got) != 2 || got[1] != [2]int32{3840, 0} { | |
| 119 | + | t.Errorf("Xwayland positions after hotplug %v", got) | |
| 120 | + | } | |
| 121 | + | } | |
| 122 | + | ||
| 123 | + | func TestGlobalFilterAllocations(t *testing.T) { | |
| 124 | + | if raceEnabled { | |
| 125 | + | t.Skip("the race detector allocates in the background") | |
| 126 | + | } | |
| 127 | + | s := &Server{xdgPublic: &server.Global{}, xdgPrivate: &server.Global{}} | |
| 128 | + | if allocs := testing.AllocsPerRun(100, func() { | |
| 129 | + | s.globalVisible(server.Client{}, s.xdgPrivate) | |
| 130 | + | s.globalVisible(server.Client{}, s.xdgPublic) | |
| 131 | + | }); allocs != 0 { | |
| 132 | + | t.Errorf("globalVisible: %.1f allocs, want 0", allocs) | |
| 133 | + | } | |
| 134 | + | } | |
| 135 | + | ||
| 136 | + | // The X origin is derived by one Space, kept without a window manager. | |
| 137 | + | func TestXSpaceFollowsLayoutWithoutXWM(t *testing.T) { | |
| 138 | + | r := newX11Rig(t) | |
| 139 | + | applyOutputs(t, r.s, r.commands, twoOutputs(-1920)) | |
| 140 | + | if ox, oy := r.s.xspace.Origin(); ox != -1920 || oy != 0 { | |
| 141 | + | t.Fatalf("origin %d,%d, want -1920,0", ox, oy) | |
| 142 | + | } | |
| 143 | + | } | |
| @@ -0,0 +1,143 @@ | |||||
| 1 | + | package wayland | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "slices" | |||
| 5 | + | "testing" | |||
| 6 | + | ||||
| 7 | + | "github.com/bnema/neferwl/internal/ports" | |||
| 8 | + | "github.com/bnema/purego-libwayland/server" | |||
| 9 | + | "github.com/bnema/wlturbo" | |||
| 10 | + | ) | |||
| 11 | + | ||||
| 12 | + | // xdgPosProxy records the logical_position events of one zxdg_output_v1. | |||
| 13 | + | type xdgPosProxy struct { | |||
| 14 | + | wlturbo.BaseProxy | |||
| 15 | + | pos [][2]int32 | |||
| 16 | + | } | |||
| 17 | + | ||||
| 18 | + | func (p *xdgPosProxy) Dispatch(e *wlturbo.Event) { | |||
| 19 | + | if e.Opcode == 0 { | |||
| 20 | + | p.pos = append(p.pos, [2]int32{e.Int32(), e.Int32()}) | |||
| 21 | + | } | |||
| 22 | + | } | |||
| 23 | + | ||||
| 24 | + | // xdgOutputsOf binds the xdg-output manager and an xdg-output for every | |||
| 25 | + | // wl_output of c, and returns what each one reported. | |||
| 26 | + | func xdgOutputsOf(t *testing.T, c *wlturbo.Display) []*xdgPosProxy { | |||
| 27 | + | t.Helper() | |||
| 28 | + | if err := c.Roundtrip(); err != nil { | |||
| 29 | + | t.Fatal(err) | |||
| 30 | + | } | |||
| 31 | + | var mgrs int | |||
| 32 | + | var manager uint32 | |||
| 33 | + | for _, g := range c.Registry().GetGlobals() { | |||
| 34 | + | if g.Interface == "zxdg_output_manager_v1" { | |||
| 35 | + | mgrs++ | |||
| 36 | + | id, err := bindWireID(c, g.Name, g.Interface, 3) | |||
| 37 | + | if err != nil { | |||
| 38 | + | t.Fatal(err) | |||
| 39 | + | } | |||
| 40 | + | registerProtocol(t, c, id) | |||
| 41 | + | manager = id | |||
| 42 | + | } | |||
| 43 | + | } | |||
| 44 | + | if mgrs != 1 { | |||
| 45 | + | t.Fatalf("client sees %d zxdg_output_manager_v1 globals, want 1", mgrs) | |||
| 46 | + | } | |||
| 47 | + | var out []*xdgPosProxy | |||
| 48 | + | for _, g := range c.Registry().GetGlobals() { | |||
| 49 | + | if g.Interface != "wl_output" { | |||
| 50 | + | continue | |||
| 51 | + | } | |||
| 52 | + | wo, err := bindWireID(c, g.Name, g.Interface, 4) | |||
| 53 | + | if err != nil { | |||
| 54 | + | t.Fatal(err) | |||
| 55 | + | } | |||
| 56 | + | xp := &xdgPosProxy{} | |||
| 57 | + | xp.SetID(c.AllocateID()) | |||
| 58 | + | registerWireProxy(c, xp) | |||
| 59 | + | requestProtocol(t, c, manager, 1, xp.ID(), wo) | |||
| 60 | + | out = append(out, xp) | |||
| 61 | + | } | |||
| 62 | + | if err := c.Roundtrip(); err != nil { | |||
| 63 | + | t.Fatal(err) | |||
| 64 | + | } | |||
| 65 | + | return out | |||
| 66 | + | } | |||
| 67 | + | ||||
| 68 | + | func lastPositions(ps []*xdgPosProxy) [][2]int32 { | |||
| 69 | + | var r [][2]int32 | |||
| 70 | + | for _, p := range ps { | |||
| 71 | + | r = append(r, p.pos[len(p.pos)-1]) | |||
| 72 | + | } | |||
| 73 | + | slices.SortFunc(r, func(a, b [2]int32) int { return int(a[0] - b[0]) }) | |||
| 74 | + | return r | |||
| 75 | + | } | |||
| 76 | + | ||||
| 77 | + | func twoOutputs(ax int) ports.SetOutputs { | |||
| 78 | + | mk := func(name string, x int) ports.OutputPlacement { | |||
| 79 | + | return ports.OutputPlacement{Info: ports.OutputInfo{Name: name, Width: 1920, Height: 1080}, X: x, Y: 0, Width: 1920, Height: 1080, Scale: 1} | |||
| 80 | + | } | |||
| 81 | + | return ports.SetOutputs{Outputs: ports.Layout{mk("A", ax), mk("B", 0)}} | |||
| 82 | + | } | |||
| 83 | + | ||||
| 84 | + | // Xwayland gets X-space positions (layout top-left is 0,0) and re-receives | |||
| 85 | + | // them on a layout change; every other client keeps the layout coordinates. | |||
| 86 | + | func TestXwaylandPrivateXDGOutput(t *testing.T) { | |||
| 87 | + | r := newX11Rig(t) | |||
| 88 | + | applyOutputs(t, r.s, r.commands, twoOutputs(-1920)) | |||
| 89 | + | other := protocolClient(t, r.s, r.dir) | |||
| 90 | + | xo := xdgOutputsOf(t, r.xc) | |||
| 91 | + | oo := xdgOutputsOf(t, other) | |||
| 92 | + | if got, want := lastPositions(xo), [][2]int32{{0, 0}, {1920, 0}}; !slices.Equal(got, want) { | |||
| 93 | + | t.Errorf("Xwayland positions %v, want %v", got, want) | |||
| 94 | + | } | |||
| 95 | + | if got, want := lastPositions(oo), [][2]int32{{-1920, 0}, {0, 0}}; !slices.Equal(got, want) { | |||
| 96 | + | t.Errorf("ordinary positions %v, want %v", got, want) | |||
| 97 | + | } | |||
| 98 | + | applyOutputs(t, r.s, r.commands, twoOutputs(-3840)) | |||
| 99 | + | if err := r.xc.Roundtrip(); err != nil { | |||
| 100 | + | t.Fatal(err) | |||
| 101 | + | } | |||
| 102 | + | if err := other.Roundtrip(); err != nil { | |||
| 103 | + | t.Fatal(err) | |||
| 104 | + | } | |||
| 105 | + | if got, want := lastPositions(xo), [][2]int32{{0, 0}, {3840, 0}}; !slices.Equal(got, want) { | |||
| 106 | + | t.Errorf("Xwayland positions after change %v, want %v", got, want) | |||
| 107 | + | } | |||
| 108 | + | if got, want := lastPositions(oo), [][2]int32{{-3840, 0}, {0, 0}}; !slices.Equal(got, want) { | |||
| 109 | + | t.Errorf("ordinary positions after change %v, want %v", got, want) | |||
| 110 | + | } | |||
| 111 | + | // Hotplug: a third output on the right grows the extents. | |||
| 112 | + | third := twoOutputs(-3840) | |||
| 113 | + | third.Outputs = append(third.Outputs, ports.OutputPlacement{Info: ports.OutputInfo{Name: "C", Width: 1920, Height: 1080}, X: 1920, Width: 1920, Height: 1080, Scale: 1}) | |||
| 114 | + | applyOutputs(t, r.s, r.commands, third) | |||
| 115 | + | if err := r.xc.Roundtrip(); err != nil { | |||
| 116 | + | t.Fatal(err) | |||
| 117 | + | } | |||
| 118 | + | if got := lastPositions(xo); len(got) != 2 || got[1] != [2]int32{3840, 0} { | |||
| 119 | + | t.Errorf("Xwayland positions after hotplug %v", got) | |||
| 120 | + | } | |||
| 121 | + | } | |||
| 122 | + | ||||
| 123 | + | func TestGlobalFilterAllocations(t *testing.T) { | |||
| 124 | + | if raceEnabled { | |||
| 125 | + | t.Skip("the race detector allocates in the background") | |||
| 126 | + | } | |||
| 127 | + | s := &Server{xdgPublic: &server.Global{}, xdgPrivate: &server.Global{}} | |||
| 128 | + | if allocs := testing.AllocsPerRun(100, func() { | |||
| 129 | + | s.globalVisible(server.Client{}, s.xdgPrivate) | |||
| 130 | + | s.globalVisible(server.Client{}, s.xdgPublic) | |||
| 131 | + | }); allocs != 0 { | |||
| 132 | + | t.Errorf("globalVisible: %.1f allocs, want 0", allocs) | |||
| 133 | + | } | |||
| 134 | + | } | |||
| 135 | + | ||||
| 136 | + | // The X origin is derived by one Space, kept without a window manager. | |||
| 137 | + | func TestXSpaceFollowsLayoutWithoutXWM(t *testing.T) { | |||
| 138 | + | r := newX11Rig(t) | |||
| 139 | + | applyOutputs(t, r.s, r.commands, twoOutputs(-1920)) | |||
| 140 | + | if ox, oy := r.s.xspace.Origin(); ox != -1920 || oy != 0 { | |||
| 141 | + | t.Fatalf("origin %d,%d, want -1920,0", ox, oy) | |||
| 142 | + | } | |||
| 143 | + | } | |||
A internal/adapters/wayland/xwayland.go +75 -0
| @@ -0,0 +1,75 @@ | |||
| 1 | + | package wayland | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | ||
| 6 | + | "github.com/bnema/neferwl/internal/adapters/logging" | |
| 7 | + | "github.com/bnema/neferwl/internal/adapters/wayland/xwm" | |
| 8 | + | "github.com/bnema/purego-libwayland/server" | |
| 9 | + | "golang.org/x/sys/unix" | |
| 10 | + | ) | |
| 11 | + | ||
| 12 | + | // xwaylandClient belongs to the display goroutine. | |
| 13 | + | type xwaylandClient struct { | |
| 14 | + | client server.Client | |
| 15 | + | } | |
| 16 | + | ||
| 17 | + | // AttachXwayland transfers ownership of the Wayland descriptor to the display. | |
| 18 | + | // It may be called from any goroutine. | |
| 19 | + | func (s *Server) AttachXwayland(wlFD int) { | |
| 20 | + | ran := s.display.Do(func() { | |
| 21 | + | c, err := s.display.CreateClient(wlFD) | |
| 22 | + | if err != nil { | |
| 23 | + | s.log.Warn().Err(err).Msg("Xwayland connection not adopted") | |
| 24 | + | _ = unix.Close(wlFD) // CreateClient only adopts on success. | |
| 25 | + | return | |
| 26 | + | } | |
| 27 | + | s.forgetXwayland() | |
| 28 | + | s.xwayland = xwaylandClient{client: c} | |
| 29 | + | c.OnDestroy(func() { | |
| 30 | + | if s.xwayland.client == c { | |
| 31 | + | s.teardownXWM() | |
| 32 | + | s.forgetXwayland() | |
| 33 | + | } | |
| 34 | + | }) | |
| 35 | + | s.log.Info().Msg("Xwayland attached") | |
| 36 | + | }) | |
| 37 | + | if !ran { | |
| 38 | + | _ = unix.Close(wlFD) | |
| 39 | + | } | |
| 40 | + | } | |
| 41 | + | ||
| 42 | + | // forgetXwayland runs on the display owner, or after Run has joined it. | |
| 43 | + | func (s *Server) forgetXwayland() { | |
| 44 | + | if s.xwayland.client != (server.Client{}) { | |
| 45 | + | s.xwayland = xwaylandClient{} | |
| 46 | + | } | |
| 47 | + | } | |
| 48 | + | ||
| 49 | + | // AttachXWM bootstraps the X11 window manager on the window manager | |
| 50 | + | // descriptor, which it owns. It runs on the starter goroutine once Xwayland is | |
| 51 | + | // ready and blocks until the XWM is installed, ctx ends or Boot fails. stop | |
| 52 | + | // closes the XWM on the display goroutine and waits for it. | |
| 53 | + | func (s *Server) AttachXWM(ctx context.Context, wmFD int) (stop func()) { | |
| 54 | + | log := logging.For(ctx, "xwm") | |
| 55 | + | x, err := xwm.BootSettings(ctx, wmFD, log, s.xsettings) | |
| 56 | + | if err != nil { | |
| 57 | + | if ctx.Err() != nil { | |
| 58 | + | log.Info().Err(err).Msg("X11 window management not started: Xwayland is going away") | |
| 59 | + | } else { | |
| 60 | + | log.Warn().Err(err).Msg("X11 window management disabled") | |
| 61 | + | } | |
| 62 | + | return func() {} | |
| 63 | + | } | |
| 64 | + | if !s.display.Do(func() { s.installXWM(x) }) { | |
| 65 | + | x.CloseConn() | |
| 66 | + | return func() {} | |
| 67 | + | } | |
| 68 | + | return func() { | |
| 69 | + | s.display.Do(func() { | |
| 70 | + | if s.xwm == x { | |
| 71 | + | s.teardownXWM() | |
| 72 | + | } | |
| 73 | + | }) | |
| 74 | + | } | |
| 75 | + | } | |
| @@ -0,0 +1,75 @@ | |||||
| 1 | + | package wayland | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | ||||
| 6 | + | "github.com/bnema/neferwl/internal/adapters/logging" | |||
| 7 | + | "github.com/bnema/neferwl/internal/adapters/wayland/xwm" | |||
| 8 | + | "github.com/bnema/purego-libwayland/server" | |||
| 9 | + | "golang.org/x/sys/unix" | |||
| 10 | + | ) | |||
| 11 | + | ||||
| 12 | + | // xwaylandClient belongs to the display goroutine. | |||
| 13 | + | type xwaylandClient struct { | |||
| 14 | + | client server.Client | |||
| 15 | + | } | |||
| 16 | + | ||||
| 17 | + | // AttachXwayland transfers ownership of the Wayland descriptor to the display. | |||
| 18 | + | // It may be called from any goroutine. | |||
| 19 | + | func (s *Server) AttachXwayland(wlFD int) { | |||
| 20 | + | ran := s.display.Do(func() { | |||
| 21 | + | c, err := s.display.CreateClient(wlFD) | |||
| 22 | + | if err != nil { | |||
| 23 | + | s.log.Warn().Err(err).Msg("Xwayland connection not adopted") | |||
| 24 | + | _ = unix.Close(wlFD) // CreateClient only adopts on success. | |||
| 25 | + | return | |||
| 26 | + | } | |||
| 27 | + | s.forgetXwayland() | |||
| 28 | + | s.xwayland = xwaylandClient{client: c} | |||
| 29 | + | c.OnDestroy(func() { | |||
| 30 | + | if s.xwayland.client == c { | |||
| 31 | + | s.teardownXWM() | |||
| 32 | + | s.forgetXwayland() | |||
| 33 | + | } | |||
| 34 | + | }) | |||
| 35 | + | s.log.Info().Msg("Xwayland attached") | |||
| 36 | + | }) | |||
| 37 | + | if !ran { | |||
| 38 | + | _ = unix.Close(wlFD) | |||
| 39 | + | } | |||
| 40 | + | } | |||
| 41 | + | ||||
| 42 | + | // forgetXwayland runs on the display owner, or after Run has joined it. | |||
| 43 | + | func (s *Server) forgetXwayland() { | |||
| 44 | + | if s.xwayland.client != (server.Client{}) { | |||
| 45 | + | s.xwayland = xwaylandClient{} | |||
| 46 | + | } | |||
| 47 | + | } | |||
| 48 | + | ||||
| 49 | + | // AttachXWM bootstraps the X11 window manager on the window manager | |||
| 50 | + | // descriptor, which it owns. It runs on the starter goroutine once Xwayland is | |||
| 51 | + | // ready and blocks until the XWM is installed, ctx ends or Boot fails. stop | |||
| 52 | + | // closes the XWM on the display goroutine and waits for it. | |||
| 53 | + | func (s *Server) AttachXWM(ctx context.Context, wmFD int) (stop func()) { | |||
| 54 | + | log := logging.For(ctx, "xwm") | |||
| 55 | + | x, err := xwm.BootSettings(ctx, wmFD, log, s.xsettings) | |||
| 56 | + | if err != nil { | |||
| 57 | + | if ctx.Err() != nil { | |||
| 58 | + | log.Info().Err(err).Msg("X11 window management not started: Xwayland is going away") | |||
| 59 | + | } else { | |||
| 60 | + | log.Warn().Err(err).Msg("X11 window management disabled") | |||
| 61 | + | } | |||
| 62 | + | return func() {} | |||
| 63 | + | } | |||
| 64 | + | if !s.display.Do(func() { s.installXWM(x) }) { | |||
| 65 | + | x.CloseConn() | |||
| 66 | + | return func() {} | |||
| 67 | + | } | |||
| 68 | + | return func() { | |||
| 69 | + | s.display.Do(func() { | |||
| 70 | + | if s.xwm == x { | |||
| 71 | + | s.teardownXWM() | |||
| 72 | + | } | |||
| 73 | + | }) | |||
| 74 | + | } | |||
| 75 | + | } | |||
A internal/adapters/wayland/xwayland_test.go +84 -0
| @@ -0,0 +1,84 @@ | |||
| 1 | + | package wayland | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "errors" | |
| 5 | + | "testing" | |
| 6 | + | ||
| 7 | + | "github.com/bnema/purego-libwayland/server" | |
| 8 | + | "golang.org/x/sys/unix" | |
| 9 | + | ) | |
| 10 | + | ||
| 11 | + | func xwaylandPair(t *testing.T) *[2]int { | |
| 12 | + | t.Helper() | |
| 13 | + | p, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0) | |
| 14 | + | if err != nil { | |
| 15 | + | t.Fatal(err) | |
| 16 | + | } | |
| 17 | + | t.Cleanup(func() { | |
| 18 | + | if p[1] >= 0 { | |
| 19 | + | unix.Close(p[1]) | |
| 20 | + | } | |
| 21 | + | }) | |
| 22 | + | return &p | |
| 23 | + | } | |
| 24 | + | ||
| 25 | + | func assertFDClosed(t *testing.T, fd int) { | |
| 26 | + | t.Helper() | |
| 27 | + | if _, err := unix.FcntlInt(uintptr(fd), unix.F_GETFD, 0); !errors.Is(err, unix.EBADF) { | |
| 28 | + | t.Fatalf("fd %d still open: %v", fd, err) | |
| 29 | + | } | |
| 30 | + | } | |
| 31 | + | ||
| 32 | + | func TestAttachXwaylandLifecycle(t *testing.T) { | |
| 33 | + | h := newAdmissionHarness(t, 1) | |
| 34 | + | wl := xwaylandPair(t) | |
| 35 | + | h.s.AttachXwayland(wl[0]) | |
| 36 | + | var first server.Client | |
| 37 | + | h.s.display.Do(func() { first = h.s.xwayland.client }) | |
| 38 | + | if first == (server.Client{}) { | |
| 39 | + | t.Fatal("client not recorded") | |
| 40 | + | } | |
| 41 | + | wl2 := xwaylandPair(t) | |
| 42 | + | h.s.AttachXwayland(wl2[0]) | |
| 43 | + | unix.Close(wl[1]) | |
| 44 | + | wl[1] = -1 // Old client's destroy must not clear the new record. | |
| 45 | + | waitTrue(t, "old client destroyed", func() bool { | |
| 46 | + | _, err := unix.FcntlInt(uintptr(wl[0]), unix.F_GETFD, 0) | |
| 47 | + | return errors.Is(err, unix.EBADF) | |
| 48 | + | }) | |
| 49 | + | h.s.display.Do(func() { | |
| 50 | + | if h.s.xwayland.client == (server.Client{}) || h.s.xwayland.client == first { | |
| 51 | + | t.Error("replacement lost") | |
| 52 | + | } | |
| 53 | + | }) | |
| 54 | + | unix.Close(wl2[1]) | |
| 55 | + | wl2[1] = -1 | |
| 56 | + | waitTrue(t, "Xwayland disconnected", func() bool { | |
| 57 | + | clear := false | |
| 58 | + | h.s.display.Do(func() { clear = h.s.xwayland.client == (server.Client{}) }) | |
| 59 | + | return clear | |
| 60 | + | }) | |
| 61 | + | } | |
| 62 | + | ||
| 63 | + | func TestAttachXwaylandFailureClosesFDs(t *testing.T) { | |
| 64 | + | h := newAdmissionHarness(t, 1) | |
| 65 | + | var p [2]int | |
| 66 | + | if err := unix.Pipe2(p[:], unix.O_CLOEXEC); err != nil { | |
| 67 | + | t.Fatal(err) | |
| 68 | + | } | |
| 69 | + | defer unix.Close(p[1]) | |
| 70 | + | h.s.AttachXwayland(p[0]) | |
| 71 | + | assertFDClosed(t, p[0]) | |
| 72 | + | } | |
| 73 | + | ||
| 74 | + | func TestAttachXwaylandStopped(t *testing.T) { | |
| 75 | + | d, err := server.NewDisplay() | |
| 76 | + | if err != nil { | |
| 77 | + | t.Fatal(err) | |
| 78 | + | } | |
| 79 | + | d.Close() | |
| 80 | + | s := &Server{display: d} | |
| 81 | + | wl := xwaylandPair(t) | |
| 82 | + | s.AttachXwayland(wl[0]) | |
| 83 | + | assertFDClosed(t, wl[0]) | |
| 84 | + | } | |
| @@ -0,0 +1,84 @@ | |||||
| 1 | + | package wayland | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "errors" | |||
| 5 | + | "testing" | |||
| 6 | + | ||||
| 7 | + | "github.com/bnema/purego-libwayland/server" | |||
| 8 | + | "golang.org/x/sys/unix" | |||
| 9 | + | ) | |||
| 10 | + | ||||
| 11 | + | func xwaylandPair(t *testing.T) *[2]int { | |||
| 12 | + | t.Helper() | |||
| 13 | + | p, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0) | |||
| 14 | + | if err != nil { | |||
| 15 | + | t.Fatal(err) | |||
| 16 | + | } | |||
| 17 | + | t.Cleanup(func() { | |||
| 18 | + | if p[1] >= 0 { | |||
| 19 | + | unix.Close(p[1]) | |||
| 20 | + | } | |||
| 21 | + | }) | |||
| 22 | + | return &p | |||
| 23 | + | } | |||
| 24 | + | ||||
| 25 | + | func assertFDClosed(t *testing.T, fd int) { | |||
| 26 | + | t.Helper() | |||
| 27 | + | if _, err := unix.FcntlInt(uintptr(fd), unix.F_GETFD, 0); !errors.Is(err, unix.EBADF) { | |||
| 28 | + | t.Fatalf("fd %d still open: %v", fd, err) | |||
| 29 | + | } | |||
| 30 | + | } | |||
| 31 | + | ||||
| 32 | + | func TestAttachXwaylandLifecycle(t *testing.T) { | |||
| 33 | + | h := newAdmissionHarness(t, 1) | |||
| 34 | + | wl := xwaylandPair(t) | |||
| 35 | + | h.s.AttachXwayland(wl[0]) | |||
| 36 | + | var first server.Client | |||
| 37 | + | h.s.display.Do(func() { first = h.s.xwayland.client }) | |||
| 38 | + | if first == (server.Client{}) { | |||
| 39 | + | t.Fatal("client not recorded") | |||
| 40 | + | } | |||
| 41 | + | wl2 := xwaylandPair(t) | |||
| 42 | + | h.s.AttachXwayland(wl2[0]) | |||
| 43 | + | unix.Close(wl[1]) | |||
| 44 | + | wl[1] = -1 // Old client's destroy must not clear the new record. | |||
| 45 | + | waitTrue(t, "old client destroyed", func() bool { | |||
| 46 | + | _, err := unix.FcntlInt(uintptr(wl[0]), unix.F_GETFD, 0) | |||
| 47 | + | return errors.Is(err, unix.EBADF) | |||
| 48 | + | }) | |||
| 49 | + | h.s.display.Do(func() { | |||
| 50 | + | if h.s.xwayland.client == (server.Client{}) || h.s.xwayland.client == first { | |||
| 51 | + | t.Error("replacement lost") | |||
| 52 | + | } | |||
| 53 | + | }) | |||
| 54 | + | unix.Close(wl2[1]) | |||
| 55 | + | wl2[1] = -1 | |||
| 56 | + | waitTrue(t, "Xwayland disconnected", func() bool { | |||
| 57 | + | clear := false | |||
| 58 | + | h.s.display.Do(func() { clear = h.s.xwayland.client == (server.Client{}) }) | |||
| 59 | + | return clear | |||
| 60 | + | }) | |||
| 61 | + | } | |||
| 62 | + | ||||
| 63 | + | func TestAttachXwaylandFailureClosesFDs(t *testing.T) { | |||
| 64 | + | h := newAdmissionHarness(t, 1) | |||
| 65 | + | var p [2]int | |||
| 66 | + | if err := unix.Pipe2(p[:], unix.O_CLOEXEC); err != nil { | |||
| 67 | + | t.Fatal(err) | |||
| 68 | + | } | |||
| 69 | + | defer unix.Close(p[1]) | |||
| 70 | + | h.s.AttachXwayland(p[0]) | |||
| 71 | + | assertFDClosed(t, p[0]) | |||
| 72 | + | } | |||
| 73 | + | ||||
| 74 | + | func TestAttachXwaylandStopped(t *testing.T) { | |||
| 75 | + | d, err := server.NewDisplay() | |||
| 76 | + | if err != nil { | |||
| 77 | + | t.Fatal(err) | |||
| 78 | + | } | |||
| 79 | + | d.Close() | |||
| 80 | + | s := &Server{display: d} | |||
| 81 | + | wl := xwaylandPair(t) | |||
| 82 | + | s.AttachXwayland(wl[0]) | |||
| 83 | + | assertFDClosed(t, wl[0]) | |||
| 84 | + | } | |||
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 | + | } | |||
A internal/adapters/wayland/xwm/atoms.go +118 -0
| @@ -0,0 +1,118 @@ | |||
| 1 | + | package xwm | |
| 2 | + | ||
| 3 | + | // Atoms interned at boot in one round trip, in this fixed order. The table | |
| 4 | + | // includes the clipboard and selection atoms. | |
| 5 | + | const ( | |
| 6 | + | atomWMProtocols = iota | |
| 7 | + | atomWMDeleteWindow | |
| 8 | + | atomWMTakeFocus | |
| 9 | + | atomWMState | |
| 10 | + | atomWMChangeState | |
| 11 | + | atomWMS0 | |
| 12 | + | atomUTF8String | |
| 13 | + | atomNetWMCMS0 | |
| 14 | + | atomNetSupported | |
| 15 | + | atomNetSupportingWMCheck | |
| 16 | + | atomNetWMName | |
| 17 | + | atomNetWMPID | |
| 18 | + | atomNetWMWindowType | |
| 19 | + | atomNetWMWindowTypeNormal | |
| 20 | + | atomNetWMWindowTypeDialog | |
| 21 | + | atomNetWMWindowTypeUtility | |
| 22 | + | atomNetWMWindowTypeSplash | |
| 23 | + | atomNetWMWindowTypeToolbar | |
| 24 | + | atomNetWMWindowTypeMenu | |
| 25 | + | atomNetWMWindowTypeDropdownMenu | |
| 26 | + | atomNetWMWindowTypePopupMenu | |
| 27 | + | atomNetWMWindowTypeTooltip | |
| 28 | + | atomNetWMWindowTypeNotification | |
| 29 | + | atomNetWMWindowTypeCombo | |
| 30 | + | atomNetWMWindowTypeDND | |
| 31 | + | atomNetWMState | |
| 32 | + | atomNetWMStateModal | |
| 33 | + | atomNetWMStateFullscreen | |
| 34 | + | atomNetWMStateFocused | |
| 35 | + | atomNetWMStateMaximizedVert | |
| 36 | + | atomNetWMStateMaximizedHorz | |
| 37 | + | atomNetActiveWindow | |
| 38 | + | atomNetClientList | |
| 39 | + | atomNetClientListStacking | |
| 40 | + | atomNetWMMoveresize | |
| 41 | + | atomNetWMBypassCompositor | |
| 42 | + | atomWLSurfaceID | |
| 43 | + | atomWLSurfaceSerial | |
| 44 | + | atomNetWMIcon | |
| 45 | + | atomMotifWMHints | |
| 46 | + | atomClipboard | |
| 47 | + | atomPrimary | |
| 48 | + | atomTargets | |
| 49 | + | atomString | |
| 50 | + | atomText | |
| 51 | + | atomTextPlain | |
| 52 | + | atomTextPlainUTF8 | |
| 53 | + | atomIncr | |
| 54 | + | atomTimestamp | |
| 55 | + | atomNeferwlData | |
| 56 | + | atomNeferwlTargets | |
| 57 | + | atomManager | |
| 58 | + | atomXSettingsS0 | |
| 59 | + | atomXSettingsSettings | |
| 60 | + | atomCount | |
| 61 | + | ) | |
| 62 | + | ||
| 63 | + | var atomNames = [atomCount]string{ | |
| 64 | + | atomWMProtocols: "WM_PROTOCOLS", | |
| 65 | + | atomWMDeleteWindow: "WM_DELETE_WINDOW", | |
| 66 | + | atomWMTakeFocus: "WM_TAKE_FOCUS", | |
| 67 | + | atomWMState: "WM_STATE", | |
| 68 | + | atomWMChangeState: "WM_CHANGE_STATE", | |
| 69 | + | atomWMS0: "WM_S0", | |
| 70 | + | atomUTF8String: "UTF8_STRING", | |
| 71 | + | atomNetWMCMS0: "_NET_WM_CM_S0", | |
| 72 | + | atomNetSupported: "_NET_SUPPORTED", | |
| 73 | + | atomNetSupportingWMCheck: "_NET_SUPPORTING_WM_CHECK", | |
| 74 | + | atomNetWMName: "_NET_WM_NAME", | |
| 75 | + | atomNetWMPID: "_NET_WM_PID", | |
| 76 | + | atomNetWMWindowType: "_NET_WM_WINDOW_TYPE", | |
| 77 | + | atomNetWMWindowTypeNormal: "_NET_WM_WINDOW_TYPE_NORMAL", | |
| 78 | + | atomNetWMWindowTypeDialog: "_NET_WM_WINDOW_TYPE_DIALOG", | |
| 79 | + | atomNetWMWindowTypeUtility: "_NET_WM_WINDOW_TYPE_UTILITY", | |
| 80 | + | atomNetWMWindowTypeSplash: "_NET_WM_WINDOW_TYPE_SPLASH", | |
| 81 | + | atomNetWMWindowTypeToolbar: "_NET_WM_WINDOW_TYPE_TOOLBAR", | |
| 82 | + | atomNetWMWindowTypeMenu: "_NET_WM_WINDOW_TYPE_MENU", | |
| 83 | + | atomNetWMWindowTypeDropdownMenu: "_NET_WM_WINDOW_TYPE_DROPDOWN_MENU", | |
| 84 | + | atomNetWMWindowTypePopupMenu: "_NET_WM_WINDOW_TYPE_POPUP_MENU", | |
| 85 | + | atomNetWMWindowTypeTooltip: "_NET_WM_WINDOW_TYPE_TOOLTIP", | |
| 86 | + | atomNetWMWindowTypeNotification: "_NET_WM_WINDOW_TYPE_NOTIFICATION", | |
| 87 | + | atomNetWMWindowTypeCombo: "_NET_WM_WINDOW_TYPE_COMBO", | |
| 88 | + | atomNetWMWindowTypeDND: "_NET_WM_WINDOW_TYPE_DND", | |
| 89 | + | atomNetWMState: "_NET_WM_STATE", | |
| 90 | + | atomNetWMStateModal: "_NET_WM_STATE_MODAL", | |
| 91 | + | atomNetWMStateFullscreen: "_NET_WM_STATE_FULLSCREEN", | |
| 92 | + | atomNetWMStateFocused: "_NET_WM_STATE_FOCUSED", | |
| 93 | + | atomNetWMStateMaximizedVert: "_NET_WM_STATE_MAXIMIZED_VERT", | |
| 94 | + | atomNetWMStateMaximizedHorz: "_NET_WM_STATE_MAXIMIZED_HORZ", | |
| 95 | + | atomNetActiveWindow: "_NET_ACTIVE_WINDOW", | |
| 96 | + | atomNetClientList: "_NET_CLIENT_LIST", | |
| 97 | + | atomNetClientListStacking: "_NET_CLIENT_LIST_STACKING", | |
| 98 | + | atomNetWMMoveresize: "_NET_WM_MOVERESIZE", | |
| 99 | + | atomNetWMBypassCompositor: "_NET_WM_BYPASS_COMPOSITOR", | |
| 100 | + | atomWLSurfaceID: "WL_SURFACE_ID", | |
| 101 | + | atomWLSurfaceSerial: "WL_SURFACE_SERIAL", | |
| 102 | + | atomNetWMIcon: "_NET_WM_ICON", | |
| 103 | + | atomMotifWMHints: "_MOTIF_WM_HINTS", | |
| 104 | + | atomClipboard: "CLIPBOARD", | |
| 105 | + | atomPrimary: "PRIMARY", | |
| 106 | + | atomTargets: "TARGETS", | |
| 107 | + | atomString: "STRING", | |
| 108 | + | atomText: "TEXT", | |
| 109 | + | atomTextPlain: "text/plain", | |
| 110 | + | atomTextPlainUTF8: "text/plain;charset=utf-8", | |
| 111 | + | atomIncr: "INCR", | |
| 112 | + | atomTimestamp: "TIMESTAMP", | |
| 113 | + | atomNeferwlData: "_NEFERWL_DATA", | |
| 114 | + | atomNeferwlTargets: "_NEFERWL_TARGETS", | |
| 115 | + | atomManager: "MANAGER", | |
| 116 | + | atomXSettingsS0: "_XSETTINGS_S0", | |
| 117 | + | atomXSettingsSettings: "_XSETTINGS_SETTINGS", | |
| 118 | + | } | |
| @@ -0,0 +1,118 @@ | |||||
| 1 | + | package xwm | |||
| 2 | + | ||||
| 3 | + | // Atoms interned at boot in one round trip, in this fixed order. The table | |||
| 4 | + | // includes the clipboard and selection atoms. | |||
| 5 | + | const ( | |||
| 6 | + | atomWMProtocols = iota | |||
| 7 | + | atomWMDeleteWindow | |||
| 8 | + | atomWMTakeFocus | |||
| 9 | + | atomWMState | |||
| 10 | + | atomWMChangeState | |||
| 11 | + | atomWMS0 | |||
| 12 | + | atomUTF8String | |||
| 13 | + | atomNetWMCMS0 | |||
| 14 | + | atomNetSupported | |||
| 15 | + | atomNetSupportingWMCheck | |||
| 16 | + | atomNetWMName | |||
| 17 | + | atomNetWMPID | |||
| 18 | + | atomNetWMWindowType | |||
| 19 | + | atomNetWMWindowTypeNormal | |||
| 20 | + | atomNetWMWindowTypeDialog | |||
| 21 | + | atomNetWMWindowTypeUtility | |||
| 22 | + | atomNetWMWindowTypeSplash | |||
| 23 | + | atomNetWMWindowTypeToolbar | |||
| 24 | + | atomNetWMWindowTypeMenu | |||
| 25 | + | atomNetWMWindowTypeDropdownMenu | |||
| 26 | + | atomNetWMWindowTypePopupMenu | |||
| 27 | + | atomNetWMWindowTypeTooltip | |||
| 28 | + | atomNetWMWindowTypeNotification | |||
| 29 | + | atomNetWMWindowTypeCombo | |||
| 30 | + | atomNetWMWindowTypeDND | |||
| 31 | + | atomNetWMState | |||
| 32 | + | atomNetWMStateModal | |||
| 33 | + | atomNetWMStateFullscreen | |||
| 34 | + | atomNetWMStateFocused | |||
| 35 | + | atomNetWMStateMaximizedVert | |||
| 36 | + | atomNetWMStateMaximizedHorz | |||
| 37 | + | atomNetActiveWindow | |||
| 38 | + | atomNetClientList | |||
| 39 | + | atomNetClientListStacking | |||
| 40 | + | atomNetWMMoveresize | |||
| 41 | + | atomNetWMBypassCompositor | |||
| 42 | + | atomWLSurfaceID | |||
| 43 | + | atomWLSurfaceSerial | |||
| 44 | + | atomNetWMIcon | |||
| 45 | + | atomMotifWMHints | |||
| 46 | + | atomClipboard | |||
| 47 | + | atomPrimary | |||
| 48 | + | atomTargets | |||
| 49 | + | atomString | |||
| 50 | + | atomText | |||
| 51 | + | atomTextPlain | |||
| 52 | + | atomTextPlainUTF8 | |||
| 53 | + | atomIncr | |||
| 54 | + | atomTimestamp | |||
| 55 | + | atomNeferwlData | |||
| 56 | + | atomNeferwlTargets | |||
| 57 | + | atomManager | |||
| 58 | + | atomXSettingsS0 | |||
| 59 | + | atomXSettingsSettings | |||
| 60 | + | atomCount | |||
| 61 | + | ) | |||
| 62 | + | ||||
| 63 | + | var atomNames = [atomCount]string{ | |||
| 64 | + | atomWMProtocols: "WM_PROTOCOLS", | |||
| 65 | + | atomWMDeleteWindow: "WM_DELETE_WINDOW", | |||
| 66 | + | atomWMTakeFocus: "WM_TAKE_FOCUS", | |||
| 67 | + | atomWMState: "WM_STATE", | |||
| 68 | + | atomWMChangeState: "WM_CHANGE_STATE", | |||
| 69 | + | atomWMS0: "WM_S0", | |||
| 70 | + | atomUTF8String: "UTF8_STRING", | |||
| 71 | + | atomNetWMCMS0: "_NET_WM_CM_S0", | |||
| 72 | + | atomNetSupported: "_NET_SUPPORTED", | |||
| 73 | + | atomNetSupportingWMCheck: "_NET_SUPPORTING_WM_CHECK", | |||
| 74 | + | atomNetWMName: "_NET_WM_NAME", | |||
| 75 | + | atomNetWMPID: "_NET_WM_PID", | |||
| 76 | + | atomNetWMWindowType: "_NET_WM_WINDOW_TYPE", | |||
| 77 | + | atomNetWMWindowTypeNormal: "_NET_WM_WINDOW_TYPE_NORMAL", | |||
| 78 | + | atomNetWMWindowTypeDialog: "_NET_WM_WINDOW_TYPE_DIALOG", | |||
| 79 | + | atomNetWMWindowTypeUtility: "_NET_WM_WINDOW_TYPE_UTILITY", | |||
| 80 | + | atomNetWMWindowTypeSplash: "_NET_WM_WINDOW_TYPE_SPLASH", | |||
| 81 | + | atomNetWMWindowTypeToolbar: "_NET_WM_WINDOW_TYPE_TOOLBAR", | |||
| 82 | + | atomNetWMWindowTypeMenu: "_NET_WM_WINDOW_TYPE_MENU", | |||
| 83 | + | atomNetWMWindowTypeDropdownMenu: "_NET_WM_WINDOW_TYPE_DROPDOWN_MENU", | |||
| 84 | + | atomNetWMWindowTypePopupMenu: "_NET_WM_WINDOW_TYPE_POPUP_MENU", | |||
| 85 | + | atomNetWMWindowTypeTooltip: "_NET_WM_WINDOW_TYPE_TOOLTIP", | |||
| 86 | + | atomNetWMWindowTypeNotification: "_NET_WM_WINDOW_TYPE_NOTIFICATION", | |||
| 87 | + | atomNetWMWindowTypeCombo: "_NET_WM_WINDOW_TYPE_COMBO", | |||
| 88 | + | atomNetWMWindowTypeDND: "_NET_WM_WINDOW_TYPE_DND", | |||
| 89 | + | atomNetWMState: "_NET_WM_STATE", | |||
| 90 | + | atomNetWMStateModal: "_NET_WM_STATE_MODAL", | |||
| 91 | + | atomNetWMStateFullscreen: "_NET_WM_STATE_FULLSCREEN", | |||
| 92 | + | atomNetWMStateFocused: "_NET_WM_STATE_FOCUSED", | |||
| 93 | + | atomNetWMStateMaximizedVert: "_NET_WM_STATE_MAXIMIZED_VERT", | |||
| 94 | + | atomNetWMStateMaximizedHorz: "_NET_WM_STATE_MAXIMIZED_HORZ", | |||
| 95 | + | atomNetActiveWindow: "_NET_ACTIVE_WINDOW", | |||
| 96 | + | atomNetClientList: "_NET_CLIENT_LIST", | |||
| 97 | + | atomNetClientListStacking: "_NET_CLIENT_LIST_STACKING", | |||
| 98 | + | atomNetWMMoveresize: "_NET_WM_MOVERESIZE", | |||
| 99 | + | atomNetWMBypassCompositor: "_NET_WM_BYPASS_COMPOSITOR", | |||
| 100 | + | atomWLSurfaceID: "WL_SURFACE_ID", | |||
| 101 | + | atomWLSurfaceSerial: "WL_SURFACE_SERIAL", | |||
| 102 | + | atomNetWMIcon: "_NET_WM_ICON", | |||
| 103 | + | atomMotifWMHints: "_MOTIF_WM_HINTS", | |||
| 104 | + | atomClipboard: "CLIPBOARD", | |||
| 105 | + | atomPrimary: "PRIMARY", | |||
| 106 | + | atomTargets: "TARGETS", | |||
| 107 | + | atomString: "STRING", | |||
| 108 | + | atomText: "TEXT", | |||
| 109 | + | atomTextPlain: "text/plain", | |||
| 110 | + | atomTextPlainUTF8: "text/plain;charset=utf-8", | |||
| 111 | + | atomIncr: "INCR", | |||
| 112 | + | atomTimestamp: "TIMESTAMP", | |||
| 113 | + | atomNeferwlData: "_NEFERWL_DATA", | |||
| 114 | + | atomNeferwlTargets: "_NEFERWL_TARGETS", | |||
| 115 | + | atomManager: "MANAGER", | |||
| 116 | + | atomXSettingsS0: "_XSETTINGS_S0", | |||
| 117 | + | atomXSettingsSettings: "_XSETTINGS_SETTINGS", | |||
| 118 | + | } | |||
A internal/adapters/wayland/xwm/configure.go +317 -0
A internal/adapters/wayland/xwm/configure_test.go +591 -0
A internal/adapters/wayland/xwm/cursor.go +109 -0
A internal/adapters/wayland/xwm/cursor_test.go +86 -0
A internal/adapters/wayland/xwm/events.go +303 -0
A internal/adapters/wayland/xwm/fdsource_mock_test.go +129 -0
A internal/adapters/wayland/xwm/fixes_test.go +230 -0
A internal/adapters/wayland/xwm/focus.go +218 -0
A internal/adapters/wayland/xwm/host.go +82 -0
A internal/adapters/wayland/xwm/host_mock_test.go +775 -0
A internal/adapters/wayland/xwm/loop_mock_test.go +180 -0
A internal/adapters/wayland/xwm/mapflow.go +359 -0
A internal/adapters/wayland/xwm/mapflow_test.go +806 -0
A internal/adapters/wayland/xwm/props.go +376 -0
A internal/adapters/wayland/xwm/selection.go +1050 -0
A internal/adapters/wayland/xwm/selection_test.go +801 -0
A internal/adapters/wayland/xwm/selection_xsettings.go +198 -0
A internal/adapters/wayland/xwm/space.go +158 -0
A internal/adapters/wayland/xwm/space_test.go +110 -0
A internal/adapters/wayland/xwm/streams_test.go +197 -0
A internal/adapters/wayland/xwm/table.go +242 -0
A internal/adapters/wayland/xwm/table_test.go +138 -0
A internal/adapters/wayland/xwm/testdata/streams/property_unchanged.hex +950 -0
A internal/adapters/wayland/xwm/testdata/streams/steady.hex +2150 -0
A internal/adapters/wayland/xwm/unmanaged.go +109 -0
A internal/adapters/wayland/xwm/unmanaged_test.go +247 -0
A internal/adapters/wayland/xwm/xpeer_test.go +655 -0
A internal/adapters/wayland/xwm/xsettings_test.go +244 -0
A internal/adapters/wayland/xwm/xwm.go +509 -0
A internal/adapters/wayland/xwm/xwm_test.go +387 -0
A internal/adapters/wayland/xwm_selection_mock_test.go +143 -0
A internal/adapters/xwayland/builtin.go +216 -0
A internal/adapters/xwayland/starter_mock_test.go +110 -0
M internal/adapters/xwayland/xwayland.go +46 -18
M internal/adapters/xwayland/xwayland_test.go +207 -0
M internal/app/app.go +13 -6
M internal/app/cursor.go +18 -0
M internal/app/session.go +2 -1
A internal/app/testdata/x11probe/main.go +692 -0
A internal/app/xwayland_test.go +808 -0
M internal/core/client.go +8 -0
M internal/core/core.go +16 -5
M internal/core/core_test.go +52 -3
M internal/core/datadrag.go +1 -1
M internal/core/fullscreen_left_core_test.go +1 -6
M internal/core/fullscreen_left_test.go +29 -0
M internal/core/layout.go +7 -1
A internal/core/place_test.go +80 -0
M internal/core/rules_test.go +44 -0
M internal/core/scene.go +12 -1
M internal/core/spawn_placement.go +9 -1
M internal/core/transition.go +20 -0
A internal/core/unmanaged.go +117 -0
A internal/core/unmanaged_test.go +145 -0
M internal/ports/client_commands.go +10 -0
M internal/ports/client_events.go +37 -0
M internal/ports/config.go +35 -3
Comments0 open / 0
no comments yet.