Merge branch 'feat/x7-xdgoutput' into feat/x7
4 files · +226 -1
M internal/adapters/wayland/outputs.go +83 -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,51 @@ func (o *output) sendXDG(x *xdgoutput.ZxdgOutputV1, withName bool) { | |||
| 89 | 91 | } | |
| 90 | 92 | } | |
| 91 | 93 | ||
| 94 | + | // xOrigin is the layout point that is X (0,0): the top-left of the layout | |
| 95 | + | // extents, so every X coordinate is non-negative. A plain copy of the | |
| 96 | + | // xwm.Space rule at scale 1 until that type merges. | |
| 97 | + | func (s *Server) xOrigin() (ox, oy int) { | |
| 98 | + | for i, p := range s.outputPlaces { | |
| 99 | + | if i == 0 || p.X < ox { | |
| 100 | + | ox = p.X | |
| 101 | + | } | |
| 102 | + | if i == 0 || p.Y < oy { | |
| 103 | + | oy = p.Y | |
| 104 | + | } | |
| 105 | + | } | |
| 106 | + | return ox, oy | |
| 107 | + | } | |
| 108 | + | ||
| 109 | + | // sendXDGX sends the private xdg-output state: the layout position moved to | |
| 110 | + | // X space and the logical size. | |
| 111 | + | func (o *output) sendXDGX(s *Server, x *xdgoutput.ZxdgOutputV1, withName bool) { | |
| 112 | + | ox, oy := s.xOrigin() | |
| 113 | + | x.SendLogicalPosition(int32(o.place.X-ox), int32(o.place.Y-oy)) | |
| 114 | + | x.SendLogicalSize(int32(o.place.Width), int32(o.place.Height)) | |
| 115 | + | if withName && x.Version() >= 2 { | |
| 116 | + | x.SendName(o.name()) | |
| 117 | + | x.SendDescription(o.description()) | |
| 118 | + | } | |
| 119 | + | } | |
| 120 | + | ||
| 121 | + | // republishXDGX resends every private xdg-output after a layout change, so | |
| 122 | + | // Xwayland resizes the root and its RandR outputs. | |
| 123 | + | func (s *Server) republishXDGX() { | |
| 124 | + | for _, o := range s.outputs { | |
| 125 | + | for _, x := range o.xdgOutputsX { | |
| 126 | + | o.sendXDGX(s, x, false) | |
| 127 | + | if x.Version() < 3 { | |
| 128 | + | x.SendDone() | |
| 129 | + | } | |
| 130 | + | } | |
| 131 | + | for _, r := range o.resources { | |
| 132 | + | if r.Client() == s.xwayland.client && r.Version() >= 2 { | |
| 133 | + | r.SendDone() | |
| 134 | + | } | |
| 135 | + | } | |
| 136 | + | } | |
| 137 | + | } | |
| 138 | + | ||
| 92 | 139 | // outputOf returns the output a wl_output resource belongs to, or nil for a | |
| 93 | 140 | // removed one. | |
| 94 | 141 | func (s *Server) outputOf(r *wayland.Output) *output { | |
| @@ -244,6 +291,9 @@ func (s *Server) setOutputs(c ports.SetOutputs) { | |||
| 244 | 291 | for _, surf := range s.surfaces { | |
| 245 | 292 | surf.sendScale() | |
| 246 | 293 | } | |
| 294 | + | if changed { | |
| 295 | + | s.republishXDGX() | |
| 296 | + | } | |
| 247 | 297 | s.notifyColorFeedback() | |
| 248 | 298 | // Layer surfaces sized from the output follow its new logical size. | |
| 249 | 299 | adopted := false | |
| @@ -279,10 +329,42 @@ type outputHandler struct{} | |||
| 279 | 329 | ||
| 280 | 330 | func (outputHandler) Release(*wayland.Output) {} | |
| 281 | 331 | ||
| 332 | + | // registerXDGOutput registers the public zxdg_output_manager_v1 and a second, | |
| 333 | + | // private one for Xwayland whose outputs are in X space (see globalVisible). | |
| 282 | 334 | func registerXDGOutput(d *server.Display, s *Server) error { | |
| 283 | - | return xdgoutput.NewZxdgOutputManagerV1Global(d, 3, func(c server.Client, v, id uint32) { | |
| 335 | + | var err error | |
| 336 | + | s.xdgPublic, err = d.AddGlobal(xdgoutput.ZxdgOutputManagerV1Interface, 3, func(c server.Client, v, id uint32) { | |
| 284 | 337 | _, _ = xdgoutput.NewZxdgOutputManagerV1(c, int32(v), id, xdgOutputManager{s}) | |
| 285 | 338 | }) | |
| 339 | + | if err != nil { | |
| 340 | + | return err | |
| 341 | + | } | |
| 342 | + | s.xdgPrivate, err = d.AddGlobal(xdgoutput.ZxdgOutputManagerV1Interface, 3, func(c server.Client, v, id uint32) { | |
| 343 | + | _, _ = xdgoutput.NewZxdgOutputManagerV1(c, int32(v), id, xdgOutputManagerX{s}) | |
| 344 | + | }) | |
| 345 | + | return err | |
| 346 | + | } | |
| 347 | + | ||
| 348 | + | type xdgOutputManagerX struct{ server *Server } | |
| 349 | + | ||
| 350 | + | func (xdgOutputManagerX) Destroy(*xdgoutput.ZxdgOutputManagerV1) {} | |
| 351 | + | func (m xdgOutputManagerX) GetXdgOutput(r *xdgoutput.ZxdgOutputManagerV1, id uint32, wl *wayland.Output) { | |
| 352 | + | x, err := xdgoutput.NewZxdgOutputV1(r.Client(), r.Version(), id, xdgOutput{}) | |
| 353 | + | if err != nil { | |
| 354 | + | return | |
| 355 | + | } | |
| 356 | + | o := m.server.outputOf(wl) | |
| 357 | + | if o == nil { | |
| 358 | + | return | |
| 359 | + | } | |
| 360 | + | o.xdgOutputsX = append(o.xdgOutputsX, x) | |
| 361 | + | x.OnDestroy = func() { o.xdgOutputsX = removeItem(o.xdgOutputsX, x) } | |
| 362 | + | o.sendXDGX(m.server, x, true) | |
| 363 | + | if r.Version() < 3 || wl.Version() < 2 { | |
| 364 | + | x.SendDone() | |
| 365 | + | } else { | |
| 366 | + | wl.SendDone() | |
| 367 | + | } | |
| 286 | 368 | } | |
| 287 | 369 | ||
| 288 | 370 | 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,51 @@ func (o *output) sendXDG(x *xdgoutput.ZxdgOutputV1, withName bool) { | |||||
| 89 | } | 91 | } | ||
| 90 | } | 92 | } | ||
| 91 | 93 | ||||
| 94 | + | // xOrigin is the layout point that is X (0,0): the top-left of the layout | |||
| 95 | + | // extents, so every X coordinate is non-negative. A plain copy of the | |||
| 96 | + | // xwm.Space rule at scale 1 until that type merges. | |||
| 97 | + | func (s *Server) xOrigin() (ox, oy int) { | |||
| 98 | + | for i, p := range s.outputPlaces { | |||
| 99 | + | if i == 0 || p.X < ox { | |||
| 100 | + | ox = p.X | |||
| 101 | + | } | |||
| 102 | + | if i == 0 || p.Y < oy { | |||
| 103 | + | oy = p.Y | |||
| 104 | + | } | |||
| 105 | + | } | |||
| 106 | + | return ox, oy | |||
| 107 | + | } | |||
| 108 | + | ||||
| 109 | + | // sendXDGX sends the private xdg-output state: the layout position moved to | |||
| 110 | + | // X space and the logical size. | |||
| 111 | + | func (o *output) sendXDGX(s *Server, x *xdgoutput.ZxdgOutputV1, withName bool) { | |||
| 112 | + | ox, oy := s.xOrigin() | |||
| 113 | + | x.SendLogicalPosition(int32(o.place.X-ox), int32(o.place.Y-oy)) | |||
| 114 | + | x.SendLogicalSize(int32(o.place.Width), int32(o.place.Height)) | |||
| 115 | + | if withName && x.Version() >= 2 { | |||
| 116 | + | x.SendName(o.name()) | |||
| 117 | + | x.SendDescription(o.description()) | |||
| 118 | + | } | |||
| 119 | + | } | |||
| 120 | + | ||||
| 121 | + | // republishXDGX resends every private xdg-output after a layout change, so | |||
| 122 | + | // Xwayland resizes the root and its RandR outputs. | |||
| 123 | + | func (s *Server) republishXDGX() { | |||
| 124 | + | for _, o := range s.outputs { | |||
| 125 | + | for _, x := range o.xdgOutputsX { | |||
| 126 | + | o.sendXDGX(s, x, false) | |||
| 127 | + | if x.Version() < 3 { | |||
| 128 | + | x.SendDone() | |||
| 129 | + | } | |||
| 130 | + | } | |||
| 131 | + | for _, r := range o.resources { | |||
| 132 | + | if r.Client() == s.xwayland.client && r.Version() >= 2 { | |||
| 133 | + | r.SendDone() | |||
| 134 | + | } | |||
| 135 | + | } | |||
| 136 | + | } | |||
| 137 | + | } | |||
| 138 | + | ||||
| 92 | // outputOf returns the output a wl_output resource belongs to, or nil for a | 139 | // outputOf returns the output a wl_output resource belongs to, or nil for a | ||
| 93 | // removed one. | 140 | // removed one. | ||
| 94 | func (s *Server) outputOf(r *wayland.Output) *output { | 141 | func (s *Server) outputOf(r *wayland.Output) *output { | ||
| @@ -244,6 +291,9 @@ func (s *Server) setOutputs(c ports.SetOutputs) { | |||||
| 244 | for _, surf := range s.surfaces { | 291 | for _, surf := range s.surfaces { | ||
| 245 | surf.sendScale() | 292 | surf.sendScale() | ||
| 246 | } | 293 | } | ||
| 294 | + | if changed { | |||
| 295 | + | s.republishXDGX() | |||
| 296 | + | } | |||
| 247 | s.notifyColorFeedback() | 297 | s.notifyColorFeedback() | ||
| 248 | // Layer surfaces sized from the output follow its new logical size. | 298 | // Layer surfaces sized from the output follow its new logical size. | ||
| 249 | adopted := false | 299 | adopted := false | ||
| @@ -279,10 +329,42 @@ type outputHandler struct{} | |||||
| 279 | 329 | ||||
| 280 | func (outputHandler) Release(*wayland.Output) {} | 330 | func (outputHandler) Release(*wayland.Output) {} | ||
| 281 | 331 | ||||
| 332 | + | // registerXDGOutput registers the public zxdg_output_manager_v1 and a second, | |||
| 333 | + | // private one for Xwayland whose outputs are in X space (see globalVisible). | |||
| 282 | func registerXDGOutput(d *server.Display, s *Server) error { | 334 | func registerXDGOutput(d *server.Display, s *Server) error { | ||
| 283 | - | return xdgoutput.NewZxdgOutputManagerV1Global(d, 3, func(c server.Client, v, id uint32) { | 335 | + | var err error |
| 336 | + | s.xdgPublic, err = d.AddGlobal(xdgoutput.ZxdgOutputManagerV1Interface, 3, func(c server.Client, v, id uint32) { | |||
| 284 | _, _ = xdgoutput.NewZxdgOutputManagerV1(c, int32(v), id, xdgOutputManager{s}) | 337 | _, _ = xdgoutput.NewZxdgOutputManagerV1(c, int32(v), id, xdgOutputManager{s}) | ||
| 285 | }) | 338 | }) | ||
| 339 | + | if err != nil { | |||
| 340 | + | return err | |||
| 341 | + | } | |||
| 342 | + | s.xdgPrivate, err = d.AddGlobal(xdgoutput.ZxdgOutputManagerV1Interface, 3, func(c server.Client, v, id uint32) { | |||
| 343 | + | _, _ = xdgoutput.NewZxdgOutputManagerV1(c, int32(v), id, xdgOutputManagerX{s}) | |||
| 344 | + | }) | |||
| 345 | + | return err | |||
| 346 | + | } | |||
| 347 | + | ||||
| 348 | + | type xdgOutputManagerX struct{ server *Server } | |||
| 349 | + | ||||
| 350 | + | func (xdgOutputManagerX) Destroy(*xdgoutput.ZxdgOutputManagerV1) {} | |||
| 351 | + | func (m xdgOutputManagerX) GetXdgOutput(r *xdgoutput.ZxdgOutputManagerV1, id uint32, wl *wayland.Output) { | |||
| 352 | + | x, err := xdgoutput.NewZxdgOutputV1(r.Client(), r.Version(), id, xdgOutput{}) | |||
| 353 | + | if err != nil { | |||
| 354 | + | return | |||
| 355 | + | } | |||
| 356 | + | o := m.server.outputOf(wl) | |||
| 357 | + | if o == nil { | |||
| 358 | + | return | |||
| 359 | + | } | |||
| 360 | + | o.xdgOutputsX = append(o.xdgOutputsX, x) | |||
| 361 | + | x.OnDestroy = func() { o.xdgOutputsX = removeItem(o.xdgOutputsX, x) } | |||
| 362 | + | o.sendXDGX(m.server, x, true) | |||
| 363 | + | if r.Version() < 3 || wl.Version() < 2 { | |||
| 364 | + | x.SendDone() | |||
| 365 | + | } else { | |||
| 366 | + | wl.SendDone() | |||
| 367 | + | } | |||
| 286 | } | 368 | } | ||
| 287 | 369 | ||||
| 288 | type xdgOutputManager struct{ server *Server } | 370 | type xdgOutputManager struct{ server *Server } | ||
M internal/adapters/wayland/server.go +3 -0
| @@ -162,6 +162,9 @@ type Server struct { | |||
| 162 | 162 | securityWG sync.WaitGroup | |
| 163 | 163 | securityLive atomic.Int32 | |
| 164 | 164 | xwayland xwaylandClient // display owner | |
| 165 | + | // xdgPublic and xdgPrivate are the two zxdg_output_manager_v1 globals: | |
| 166 | + | // layout space for clients, X space for Xwayland. | |
| 167 | + | xdgPublic, xdgPrivate *server.Global | |
| 165 | 168 | // xwm manages the X11 windows of the Xwayland client; x11Slots maps its | |
| 166 | 169 | // slots to their windows. Display goroutine only. | |
| 167 | 170 | xwm *xwm.XWM | |
| @@ -162,6 +162,9 @@ type Server struct { | |||||
| 162 | securityWG sync.WaitGroup | 162 | securityWG sync.WaitGroup | ||
| 163 | securityLive atomic.Int32 | 163 | securityLive atomic.Int32 | ||
| 164 | xwayland xwaylandClient // display owner | 164 | xwayland xwaylandClient // display owner | ||
| 165 | + | // xdgPublic and xdgPrivate are the two zxdg_output_manager_v1 globals: | |||
| 166 | + | // layout space for clients, X space for Xwayland. | |||
| 167 | + | xdgPublic, xdgPrivate *server.Global | |||
| 165 | // xwm manages the X11 windows of the Xwayland client; x11Slots maps its | 168 | // xwm manages the X11 windows of the Xwayland client; x11Slots maps its | ||
| 166 | // slots to their windows. Display goroutine only. | 169 | // slots to their windows. Display goroutine only. | ||
| 167 | xwm *xwm.XWM | 170 | xwm *xwm.XWM | ||
M internal/adapters/wayland/x11window.go +6 -0
| @@ -28,6 +28,12 @@ func xwaylandRestricted(iface string) bool { | |||
| 28 | 28 | // globalVisible is the display's global filter: the Xwayland-only globals | |
| 29 | 29 | // are visible to, and bindable by, the Xwayland client alone. | |
| 30 | 30 | func (s *Server) globalVisible(c server.Client, g *server.Global) bool { | |
| 31 | + | if g == s.xdgPrivate { | |
| 32 | + | return s.xwayland.client != (server.Client{}) && c == s.xwayland.client | |
| 33 | + | } | |
| 34 | + | if g == s.xdgPublic { | |
| 35 | + | return c != s.xwayland.client | |
| 36 | + | } | |
| 31 | 37 | if xwaylandRestricted(g.Interface()) { | |
| 32 | 38 | return s.xwayland.client != (server.Client{}) && c == s.xwayland.client | |
| 33 | 39 | } | |
| @@ -28,6 +28,12 @@ func xwaylandRestricted(iface string) bool { | |||||
| 28 | // globalVisible is the display's global filter: the Xwayland-only globals | 28 | // globalVisible is the display's global filter: the Xwayland-only globals | ||
| 29 | // are visible to, and bindable by, the Xwayland client alone. | 29 | // are visible to, and bindable by, the Xwayland client alone. | ||
| 30 | func (s *Server) globalVisible(c server.Client, g *server.Global) bool { | 30 | func (s *Server) globalVisible(c server.Client, g *server.Global) bool { | ||
| 31 | + | if g == s.xdgPrivate { | |||
| 32 | + | return s.xwayland.client != (server.Client{}) && c == s.xwayland.client | |||
| 33 | + | } | |||
| 34 | + | if g == s.xdgPublic { | |||
| 35 | + | return c != s.xwayland.client | |||
| 36 | + | } | |||
| 31 | if xwaylandRestricted(g.Interface()) { | 37 | if xwaylandRestricted(g.Interface()) { | ||
| 32 | return s.xwayland.client != (server.Client{}) && c == s.xwayland.client | 38 | return s.xwayland.client != (server.Client{}) && c == s.xwayland.client | ||
| 33 | } | 39 | } | ||
A internal/adapters/wayland/xdgoutputx_test.go +134 -0
| @@ -0,0 +1,134 @@ | |||
| 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 | + | } | |
| @@ -0,0 +1,134 @@ | |||||
| 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 | + | } | |||