1package wayland
2
3import (
4 "encoding/binary"
5
6 "github.com/bnema/go-wayland-bindings/server/wayland"
7 "github.com/bnema/go-wayland-bindings/server/xdgshell"
8 "github.com/bnema/neferwl/internal/ports"
9 "github.com/bnema/purego-libwayland/server"
10)
11
12func registerXDG(d *server.Display, s *Server) error {
13 return xdgshell.NewWmBaseGlobal(d, 6, func(c server.Client, v, id uint32) { xdgshell.NewWmBase(c, int32(v), id, wm{s}) })
14}
15
16type wm struct{ server *Server }
17
18func (wm) Destroy(*xdgshell.WmBase) {}
19func (wm) Pong(*xdgshell.WmBase, uint32) {}
20func (m wm) CreatePositioner(r *xdgshell.WmBase, id uint32) {
21 p := &positioner{}
22 if res, err := xdgshell.NewPositioner(r.Client(), r.Version(), id, p); err == nil {
23 m.server.positioners[res.Resource] = p
24 res.OnDestroy = func() { delete(m.server.positioners, res.Resource) }
25 }
26}
27func (m wm) GetXdgSurface(r *xdgshell.WmBase, id uint32, w *wayland.Surface) {
28 if w == nil {
29 return
30 }
31 state := m.server.surfaces[w.Resource]
32 if state == nil {
33 return
34 }
35 if state.kind != roleNone && state.kind != roleXDG || state.xdg != nil {
36 r.PostError(uint32(xdgshell.WmBaseErrorRole), "surface already has role")
37 return
38 }
39 x := &xdgSurface{wm: r, server: m.server, surface: state}
40 resource, err := xdgshell.NewSurface(r.Client(), r.Version(), id, x)
41 if err == nil {
42 x.resource = resource
43 state.kind = roleXDG
44 state.xdg = x
45 resource.OnDestroy = func() { state.xdg = nil; state.role = nil }
46 }
47
48}
49
50// positioner is an xdg_positioner: popups copy it when they use it.
51type positioner struct {
52 p ports.Positioner
53 sizeSet, anchorSet bool
54}
55
56func (*positioner) Destroy(*xdgshell.Positioner) {}
57func (p *positioner) SetSize(r *xdgshell.Positioner, w, h int32) {
58 if w <= 0 || h <= 0 {
59 r.PostError(uint32(xdgshell.PositionerErrorInvalidInput), "size must be positive")
60 return
61 }
62 p.p.Width, p.p.Height, p.sizeSet = int(w), int(h), true
63}
64func (p *positioner) SetAnchorRect(r *xdgshell.Positioner, x, y, w, h int32) {
65 if w < 0 || h < 0 {
66 r.PostError(uint32(xdgshell.PositionerErrorInvalidInput), "anchor rect size must not be negative")
67 return
68 }
69 p.p.AnchorRect, p.anchorSet = ports.Rect{X: int(x), Y: int(y), W: int(w), H: int(h)}, true
70}
71func (p *positioner) SetAnchor(r *xdgshell.Positioner, v uint32) {
72 if v > ports.EdgeBottomRight {
73 r.PostError(uint32(xdgshell.PositionerErrorInvalidInput), "invalid anchor")
74 return
75 }
76 p.p.Anchor = v
77}
78func (p *positioner) SetGravity(r *xdgshell.Positioner, v uint32) {
79 if v > ports.EdgeBottomRight {
80 r.PostError(uint32(xdgshell.PositionerErrorInvalidInput), "invalid gravity")
81 return
82 }
83 p.p.Gravity = v
84}
85func (p *positioner) SetConstraintAdjustment(_ *xdgshell.Positioner, v uint32) { p.p.Adjust = v }
86func (p *positioner) SetOffset(_ *xdgshell.Positioner, x, y int32) {
87 p.p.OffsetX, p.p.OffsetY = int(x), int(y)
88}
89
90// Reactive popups, parent size and parent configure hints are not used:
91// core places popups when they are created or repositioned.
92func (*positioner) SetReactive(*xdgshell.Positioner) {}
93func (*positioner) SetParentSize(*xdgshell.Positioner, int32, int32) {}
94func (*positioner) SetParentConfigure(*xdgshell.Positioner, uint32) {}
95
96// positionerOf returns a complete positioner, or posts invalid_positioner.
97func (s *Server) positionerOf(wm *xdgshell.WmBase, p *xdgshell.Positioner) (ports.Positioner, bool) {
98 if p != nil && p.Resource != nil {
99 if state := s.positioners[p.Resource]; state != nil && state.sizeSet && state.anchorSet {
100 return state.p, true
101 }
102 }
103 if wm != nil {
104 wm.PostError(uint32(xdgshell.WmBaseErrorInvalidPositioner), "incomplete positioner")
105 }
106 return ports.Positioner{}, false
107}
108
109type xdgSurface struct {
110 wm *xdgshell.WmBase
111 resource *xdgshell.Surface
112 server *Server
113 surface *surface
114 configured bool
115 acked bool
116 window *window
117 serials []uint32
118 // geometry is the committed window geometry: the visible window inside
119 // client shadows. Empty means the whole surface.
120 geometry, pendingGeometry ports.Rect
121}
122
123func (x *xdgSurface) Destroy(r *xdgshell.Surface) {
124 if x.window != nil {
125 r.PostError(uint32(xdgshell.SurfaceErrorDefunctRoleObject), "role object still exists")
126 }
127}
128
129// window is an xdg_toplevel, or an xdg_popup when popup is set.
130type window struct {
131 id ports.WindowID
132 toplevel *xdgshell.Toplevel
133 popup *popup
134 xdg *xdgSurface
135 appID, title string
136 mapped bool
137 // gen counts the mappings: an unmap and a remap is a new toplevel to
138 // ext-foreign-toplevel-list (identifier) and to its capture sources.
139 gen uint32
140 // parent is set_parent (dialogs); min and max are the size hints.
141 parent *window
142 minW, minH int32
143 maxW, maxH int32
144 floating bool
145 floatW, floatH int // size last sent to core
146 // last is the most recent core configure, resent when decoration mode changes.
147 last ports.ConfigureWindow
148 hasLast bool
149}
150
151// sendConfigure sends w.last as xdg_toplevel + xdg_surface configure.
152func (w *window) sendConfigure() {
153 c := w.last
154 w.toplevel.SendConfigure(int32(c.Width), int32(c.Height), toplevelStates(c, w.toplevel.Version()))
155 s := w.xdg.server
156 s.serial++
157 w.xdg.resource.SendConfigure(s.serial)
158 w.xdg.serials = append(w.xdg.serials, s.serial)
159}
160
161// toplevelStates encodes the xdg_toplevel states of configure c.
162func toplevelStates(c ports.ConfigureWindow, version int32) []byte {
163 var states []byte
164 add := func(st xdgshell.ToplevelState) {
165 states = binary.LittleEndian.AppendUint32(states, uint32(st))
166 }
167 if c.Fullscreen {
168 add(xdgshell.ToplevelStateFullscreen)
169 }
170 if c.Activated {
171 add(xdgshell.ToplevelStateActivated)
172 }
173 if !c.Visible && !c.Captured && version >= 6 {
174 add(xdgshell.ToplevelStateSuspended)
175 }
176 if c.Fullscreen || c.Floating {
177 return states
178 }
179 // Tiled windows must use exactly this size. Maximized is the strict
180 // size state every client honours: Wine ignores a size change that only
181 // carries tiled states on a window it considers monitor-sized.
182 add(xdgshell.ToplevelStateMaximized)
183 if version >= 2 {
184 for _, st := range []xdgshell.ToplevelState{xdgshell.ToplevelStateTiledLeft, xdgshell.ToplevelStateTiledRight, xdgshell.ToplevelStateTiledTop, xdgshell.ToplevelStateTiledBottom} {
185 add(st)
186 }
187 }
188 return states
189}
190
191func (w *window) unmap() {
192 if w.xdg.server.seat.pointerFocus == w.id {
193 w.xdg.server.changePointerFocus(0, 0, 0)
194 }
195 if w.xdg.server.seat.focused == w.id {
196 w.xdg.server.changeFocus(0)
197 }
198 if !w.mapped {
199 return
200 }
201 w.mapped = false
202 w.xdg.server.toplevelClosed(w.id)
203 w.xdg.surface.resetInputEmission()
204 w.xdg.configured = false
205 w.xdg.acked = false
206 w.xdg.serials = nil
207 w.xdg.server.emit(ports.WindowUnmapped{ID: w.id})
208 w.xdg.server.syncInhibitors()
209 w.xdg.server.emitContent(ports.SurfaceContent{ID: w.id}, damage{full: true})
210}
211func (x *xdgSurface) GetToplevel(r *xdgshell.Surface, id uint32) {
212 if x.window != nil {
213 r.PostError(uint32(xdgshell.SurfaceErrorAlreadyConstructed), "role object already constructed")
214 return
215 }
216 w := &window{id: x.server.nextWindow, xdg: x}
217 t, err := xdgshell.NewToplevel(r.Client(), r.Version(), id, &top{w})
218 if err != nil {
219 return
220 }
221 w.toplevel = t
222 x.window = w
223 x.server.nextWindow++
224 x.server.windows[w.id] = w
225 x.surface.role = func(buffer bool) {
226 if x.surface.destroyed {
227 w.unmap()
228 return
229 }
230 if !buffer && !w.mapped && !x.configured {
231 x.configured = true
232 x.server.serial++
233 t.SendConfigure(0, 0, nil)
234 r.SendConfigure(x.server.serial)
235 x.serials = append(x.serials, x.server.serial)
236 } else if buffer && !w.mapped && x.acked {
237 w.mapped = true
238 w.gen++
239 slot := x.server.slotToken(r.Client())
240 // Its size comes with the commit, in WindowResized (afterCommit).
241 w.floating = w.floats()
242 var parent ports.WindowID
243 if w.parent != nil && w.parent.mapped {
244 parent = w.parent.id
245 }
246 x.server.emit(ports.WindowMapped{ID: w.id, AppID: w.appID, Slot: slot, PID: r.Client().PID(), Floating: w.floating, Width: w.floatW, Height: w.floatH, Parent: parent})
247 x.server.syncInhibitors()
248 x.server.toplevelChanged(w)
249 x.server.log.Info().Uint64("id", uint64(w.id)).Str("app_id", w.appID).Str("slot", slot).Bool("floating", w.floating).Msg("window mapped")
250 } else if !buffer && w.mapped {
251 w.unmap()
252 }
253 }
254 t.OnDestroy = func() {
255 w.unmap()
256 x.server.foreignToplevelGone(w)
257 x.surface.dropQueue()
258 x.surface.current, x.surface.next.buffer = nil, nil
259 x.surface.next.attached = false
260 delete(x.server.windows, w.id)
261 x.window = nil
262 x.surface.role = nil
263 x.configured = false
264 x.acked = false
265 x.serials = nil
266 x.geometry, x.pendingGeometry = ports.Rect{}, ports.Rect{}
267 }
268}
269func (x *xdgSurface) GetPopup(r *xdgshell.Surface, id uint32, parent *xdgshell.Surface, pos *xdgshell.Positioner) {
270 if x.window != nil {
271 r.PostError(uint32(xdgshell.SurfaceErrorAlreadyConstructed), "role object already constructed")
272 return
273 }
274 x.server.newPopup(x, r, id, parent, pos)
275}
276
277// SetWindowGeometry sets the visible window inside the surface; it
278// applies on the next commit.
279func (x *xdgSurface) SetWindowGeometry(r *xdgshell.Surface, gx, gy, gw, gh int32) {
280 if gw <= 0 || gh <= 0 {
281 r.PostError(uint32(xdgshell.SurfaceErrorInvalidSize), "invalid window geometry")
282 return
283 }
284 x.pendingGeometry = ports.Rect{X: int(gx), Y: int(gy), W: int(gw), H: int(gh)}
285 id := uint64(0)
286 if x.window != nil {
287 id = uint64(x.window.id)
288 }
289 x.server.log.Debug().Uint64("id", id).Int32("x", gx).Int32("y", gy).Int32("w", gw).Int32("h", gh).Msg("window geometry")
290}
291func (x *xdgSurface) AckConfigure(r *xdgshell.Surface, serial uint32) {
292 for i, issued := range x.serials {
293 if issued == serial {
294 x.serials = x.serials[i+1:]
295 x.acked = true
296 return
297 }
298 }
299 r.PostError(uint32(xdgshell.SurfaceErrorInvalidSerial), "unknown configure serial")
300}
301
302type top struct{ w *window }
303
304func (top) Destroy(*xdgshell.Toplevel) {}
305func (t top) SetParent(_ *xdgshell.Toplevel, parent *xdgshell.Toplevel) {
306 var p *window
307 if parent != nil {
308 for _, w := range t.w.xdg.server.windows {
309 if w.toplevel != nil && w.toplevel.Resource == parent.Resource && w != t.w {
310 p = w
311 }
312 }
313 }
314 t.w.setParent(p)
315}
316
317// setParent changes the dialog parent; core follows it once mapped (the
318// map carries the parent of the first frame).
319func (w *window) setParent(p *window) {
320 if w.parent == p {
321 return
322 }
323 w.parent = p
324 if !w.mapped {
325 return
326 }
327 var id ports.WindowID
328 if p != nil && p.mapped {
329 id = p.id
330 }
331 w.xdg.server.emit(ports.WindowParent{ID: w.id, Parent: id})
332}
333func (t top) SetTitle(_ *xdgshell.Toplevel, title string) {
334 if title == t.w.title {
335 return
336 }
337 t.w.title = title
338 t.w.xdg.server.toplevelChanged(t.w)
339}
340func (t top) SetAppId(_ *xdgshell.Toplevel, appID string) {
341 if appID == t.w.appID {
342 return
343 }
344 t.w.appID = appID
345 if t.w.mapped {
346 t.w.xdg.server.emit(ports.WindowAppID{ID: t.w.id, AppID: appID})
347 t.w.xdg.server.toplevelChanged(t.w)
348 }
349}
350func (top) ShowWindowMenu(*xdgshell.Toplevel, *wayland.Seat, uint32, int32, int32) {}
351
352// Move and Resize start a pointer drag in core when they answer the
353// client's current press; stale serials are ignored.
354func (t top) Move(r *xdgshell.Toplevel, _ *wayland.Seat, serial uint32) {
355 t.moveRequest(r, serial, false, 0)
356}
357func (t top) Resize(r *xdgshell.Toplevel, _ *wayland.Seat, serial, edges uint32) {
358 if edges > uint32(xdgshell.ToplevelResizeEdgeBottomRight) || edges&3 == 3 || edges&12 == 12 {
359 r.PostError(uint32(xdgshell.ToplevelErrorInvalidResizeEdge), "invalid resize edge")
360 return
361 }
362 t.moveRequest(r, serial, true, ports.ResizeEdges(edges))
363}
364func (t top) moveRequest(r *xdgshell.Toplevel, serial uint32, resize bool, edges ports.ResizeEdges) {
365 s := t.w.xdg.server
366 if !t.w.mapped || serial != s.seat.press || s.seat.pressClient != r.Client() {
367 return
368 }
369 s.emit(ports.WindowMoveRequest{ID: t.w.id, Resize: resize, Edges: edges})
370}
371func (t top) SetMaxSize(r *xdgshell.Toplevel, w, h int32) {
372 if w < 0 || h < 0 {
373 r.PostError(uint32(xdgshell.ToplevelErrorInvalidSize), "negative max size")
374 return
375 }
376 t.w.maxW, t.w.maxH = w, h
377}
378func (t top) SetMinSize(r *xdgshell.Toplevel, w, h int32) {
379 if w < 0 || h < 0 {
380 r.PostError(uint32(xdgshell.ToplevelErrorInvalidSize), "negative min size")
381 return
382 }
383 t.w.minW, t.w.minH = w, h
384}
385func (top) SetMaximized(*xdgshell.Toplevel) {}
386func (top) UnsetMaximized(*xdgshell.Toplevel) {}
387func (t top) SetFullscreen(*xdgshell.Toplevel, *wayland.Output) {
388 if t.w.mapped {
389 t.w.xdg.server.emit(ports.WindowFullscreenRequest{ID: t.w.id, Fullscreen: true})
390 }
391}
392func (t top) UnsetFullscreen(*xdgshell.Toplevel) {
393 if t.w.mapped {
394 t.w.xdg.server.emit(ports.WindowFullscreenRequest{ID: t.w.id})
395 }
396}
397func (top) SetMinimized(*xdgshell.Toplevel) {}
398
399// afterCommit runs once the commit applied content and geometry: a
400// floating window keeps the size it draws.
401func (w *window) afterCommit() {
402 if !w.mapped || !w.floating {
403 return
404 }
405 if fw, fh := w.size(); fw > 0 && fh > 0 && (fw != w.floatW || fh != w.floatH) {
406 w.floatW, w.floatH = fw, fh
407 w.xdg.server.emit(ports.WindowResized{ID: w.id, Width: fw, Height: fh})
408 }
409}
410
411// floats reports whether the toplevel should float over the columns, as
412// on niri: dialogs (a parent) and fixed-size windows such as splash
413// screens.
414func (w *window) floats() bool {
415 fixed := w.minW > 0 && w.minH > 0 && w.minW == w.maxW && w.minH == w.maxH
416 return w.parent != nil || fixed
417}
418
419// size is the window geometry size, else the surface size, in logical pixels.
420func (w *window) size() (int, int) {
421 if g := w.xdg.geometry; g.W > 0 && g.H > 0 {
422 return g.W, g.H
423 }
424 c := w.xdg.surface.content
425 return c.LogicalW, c.LogicalH
426}
427
428// surfacePoint turns window coordinates (core's, from the geometry origin)
429// into surface coordinates.
430func (w *window) surfacePoint(x, y float64) (float64, float64) {
431 g := w.xdg.geometry
432 return x + float64(g.X), y + float64(g.Y)
433}
434
435func (x *xdgSurface) surfaceResource() *wayland.Surface {
436 for resource, state := range x.server.surfaces {
437 if state == x.surface && resource.Alive() {
438 return wayland.WrapSurface(resource)
439 }
440 }
441 return nil
442}