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 // dialog is set while an xdg_dialog_v1 object exists for the
143 // toplevel; modal is its state.
144 dialog, modal bool
145 minW, minH int32
146 maxW, maxH int32
147 floating bool
148 floatW, floatH int // size last sent to core
149 // last is the most recent core configure, resent when decoration mode changes.
150 last ports.ConfigureWindow
151 hasLast bool
152}
153
154// sendConfigure sends w.last as xdg_toplevel + xdg_surface configure.
155func (w *window) sendConfigure() {
156 c := w.last
157 w.toplevel.SendConfigure(int32(c.Width), int32(c.Height), toplevelStates(c, w.toplevel.Version()))
158 s := w.xdg.server
159 s.serial++
160 w.xdg.resource.SendConfigure(s.serial)
161 w.xdg.serials = append(w.xdg.serials, s.serial)
162}
163
164// toplevelStates encodes the xdg_toplevel states of configure c.
165func toplevelStates(c ports.ConfigureWindow, version int32) []byte {
166 var states []byte
167 add := func(st xdgshell.ToplevelState) {
168 states = binary.LittleEndian.AppendUint32(states, uint32(st))
169 }
170 if c.Fullscreen {
171 add(xdgshell.ToplevelStateFullscreen)
172 }
173 if c.Activated {
174 add(xdgshell.ToplevelStateActivated)
175 }
176 if !c.Visible && !c.Captured && version >= 6 {
177 add(xdgshell.ToplevelStateSuspended)
178 }
179 if c.Fullscreen || c.Floating {
180 return states
181 }
182 // Tiled windows must use exactly this size. Maximized is the strict
183 // size state every client honours: Wine ignores a size change that only
184 // carries tiled states on a window it considers monitor-sized.
185 add(xdgshell.ToplevelStateMaximized)
186 if version >= 2 {
187 for _, st := range []xdgshell.ToplevelState{xdgshell.ToplevelStateTiledLeft, xdgshell.ToplevelStateTiledRight, xdgshell.ToplevelStateTiledTop, xdgshell.ToplevelStateTiledBottom} {
188 add(st)
189 }
190 }
191 return states
192}
193
194func (w *window) unmap() {
195 if w.xdg.server.seat.pointerFocus == w.id {
196 w.xdg.server.changePointerFocus(0, 0, 0)
197 }
198 if w.xdg.server.seat.focused == w.id {
199 w.xdg.server.changeFocus(0)
200 }
201 if !w.mapped {
202 return
203 }
204 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})
213}
214func (x *xdgSurface) GetToplevel(r *xdgshell.Surface, id uint32) {
215 if x.window != nil {
216 r.PostError(uint32(xdgshell.SurfaceErrorAlreadyConstructed), "role object already constructed")
217 return
218 }
219 w := &window{id: x.server.nextWindow, xdg: x}
220 t, err := xdgshell.NewToplevel(r.Client(), r.Version(), id, &top{w})
221 if err != nil {
222 return
223 }
224 w.toplevel = t
225 x.window = w
226 x.server.nextWindow++
227 x.server.windows[w.id] = w
228 x.surface.role = func(buffer bool) {
229 if x.surface.destroyed {
230 w.unmap()
231 return
232 }
233 if !buffer && !w.mapped && !x.configured {
234 x.configured = true
235 x.server.serial++
236 t.SendConfigure(0, 0, nil)
237 r.SendConfigure(x.server.serial)
238 x.serials = append(x.serials, x.server.serial)
239 } else if buffer && !w.mapped && x.acked {
240 w.mapped = true
241 w.gen++
242 slot := x.server.slotToken(r.Client())
243 // Its size comes with the commit, in WindowResized (afterCommit).
244 w.floating = w.floats()
245 var parent ports.WindowID
246 if w.parent != nil && w.parent.mapped {
247 parent = w.parent.id
248 }
249 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, Modal: w.modal, Remap: w.gen > 1})
250 x.server.syncInhibitors()
251 x.server.toplevelChanged(w)
252 x.server.log.Info().Uint64("id", uint64(w.id)).Str("app_id", w.appID).Str("slot", slot).Bool("floating", w.floating).Msg("window mapped")
253 } else if !buffer && w.mapped {
254 w.unmap()
255 }
256 }
257 t.OnDestroy = func() {
258 w.unmap()
259 x.server.foreignToplevelGone(w)
260 x.surface.dropQueue()
261 x.surface.current, x.surface.next.buffer = nil, nil
262 x.surface.next.attached = false
263 delete(x.server.windows, w.id)
264 x.window = nil
265 x.surface.role = nil
266 x.configured = false
267 x.acked = false
268 x.serials = nil
269 x.geometry, x.pendingGeometry = ports.Rect{}, ports.Rect{}
270 }
271}
272func (x *xdgSurface) GetPopup(r *xdgshell.Surface, id uint32, parent *xdgshell.Surface, pos *xdgshell.Positioner) {
273 if x.window != nil {
274 r.PostError(uint32(xdgshell.SurfaceErrorAlreadyConstructed), "role object already constructed")
275 return
276 }
277 x.server.newPopup(x, r, id, parent, pos)
278}
279
280// SetWindowGeometry sets the visible window inside the surface; it
281// applies on the next commit.
282func (x *xdgSurface) SetWindowGeometry(r *xdgshell.Surface, gx, gy, gw, gh int32) {
283 if gw <= 0 || gh <= 0 {
284 r.PostError(uint32(xdgshell.SurfaceErrorInvalidSize), "invalid window geometry")
285 return
286 }
287 x.pendingGeometry = ports.Rect{X: int(gx), Y: int(gy), W: int(gw), H: int(gh)}
288 id := uint64(0)
289 if x.window != nil {
290 id = uint64(x.window.id)
291 }
292 x.server.log.Debug().Uint64("id", id).Int32("x", gx).Int32("y", gy).Int32("w", gw).Int32("h", gh).Msg("window geometry")
293}
294func (x *xdgSurface) AckConfigure(r *xdgshell.Surface, serial uint32) {
295 for i, issued := range x.serials {
296 if issued == serial {
297 x.serials = x.serials[i+1:]
298 x.acked = true
299 return
300 }
301 }
302 r.PostError(uint32(xdgshell.SurfaceErrorInvalidSerial), "unknown configure serial")
303}
304
305type top struct{ w *window }
306
307func (top) Destroy(*xdgshell.Toplevel) {}
308func (t top) SetParent(_ *xdgshell.Toplevel, parent *xdgshell.Toplevel) {
309 var p *window
310 if parent != nil {
311 for _, w := range t.w.xdg.server.windows {
312 if w.toplevel != nil && w.toplevel.Resource == parent.Resource && w != t.w {
313 p = w
314 }
315 }
316 }
317 t.w.setParent(p)
318}
319
320// setParent changes the dialog parent; core follows it once mapped (the
321// map carries the parent of the first frame).
322func (w *window) setParent(p *window) {
323 if w.parent == p {
324 return
325 }
326 w.parent = p
327 if !w.mapped {
328 return
329 }
330 var id ports.WindowID
331 if p != nil && p.mapped {
332 id = p.id
333 }
334 w.xdg.server.emit(ports.WindowParent{ID: w.id, Parent: id})
335}
336func (t top) SetTitle(_ *xdgshell.Toplevel, title string) {
337 if title == t.w.title {
338 return
339 }
340 t.w.title = title
341 t.w.xdg.server.toplevelChanged(t.w)
342}
343func (t top) SetAppId(_ *xdgshell.Toplevel, appID string) {
344 if appID == t.w.appID {
345 return
346 }
347 t.w.appID = appID
348 if t.w.mapped {
349 t.w.xdg.server.emit(ports.WindowAppID{ID: t.w.id, AppID: appID})
350 t.w.xdg.server.toplevelChanged(t.w)
351 }
352}
353func (top) ShowWindowMenu(*xdgshell.Toplevel, *wayland.Seat, uint32, int32, int32) {}
354
355// Move and Resize start a pointer drag in core when they answer the
356// client's current press; stale serials are ignored.
357func (t top) Move(r *xdgshell.Toplevel, _ *wayland.Seat, serial uint32) {
358 t.moveRequest(r, serial, false, 0)
359}
360func (t top) Resize(r *xdgshell.Toplevel, _ *wayland.Seat, serial, edges uint32) {
361 if edges > uint32(xdgshell.ToplevelResizeEdgeBottomRight) || edges&3 == 3 || edges&12 == 12 {
362 r.PostError(uint32(xdgshell.ToplevelErrorInvalidResizeEdge), "invalid resize edge")
363 return
364 }
365 t.moveRequest(r, serial, true, ports.ResizeEdges(edges))
366}
367func (t top) moveRequest(r *xdgshell.Toplevel, serial uint32, resize bool, edges ports.ResizeEdges) {
368 s := t.w.xdg.server
369 if !t.w.mapped || serial != s.seat.press || s.seat.pressClient != r.Client() {
370 return
371 }
372 s.emit(ports.WindowMoveRequest{ID: t.w.id, Resize: resize, Edges: edges})
373}
374func (t top) SetMaxSize(r *xdgshell.Toplevel, w, h int32) {
375 if w < 0 || h < 0 {
376 r.PostError(uint32(xdgshell.ToplevelErrorInvalidSize), "negative max size")
377 return
378 }
379 t.w.maxW, t.w.maxH = w, h
380}
381func (t top) SetMinSize(r *xdgshell.Toplevel, w, h int32) {
382 if w < 0 || h < 0 {
383 r.PostError(uint32(xdgshell.ToplevelErrorInvalidSize), "negative min size")
384 return
385 }
386 t.w.minW, t.w.minH = w, h
387}
388func (top) SetMaximized(*xdgshell.Toplevel) {}
389func (top) UnsetMaximized(*xdgshell.Toplevel) {}
390func (t top) SetFullscreen(*xdgshell.Toplevel, *wayland.Output) {
391 if t.w.mapped {
392 t.w.xdg.server.emit(ports.WindowFullscreenRequest{ID: t.w.id, Fullscreen: true})
393 }
394}
395func (t top) UnsetFullscreen(*xdgshell.Toplevel) {
396 if t.w.mapped {
397 t.w.xdg.server.emit(ports.WindowFullscreenRequest{ID: t.w.id})
398 }
399}
400func (top) SetMinimized(*xdgshell.Toplevel) {}
401
402// afterCommit runs once the commit applied content and geometry: a
403// floating window keeps the size it draws.
404func (w *window) afterCommit() {
405 if !w.mapped || !w.floating {
406 return
407 }
408 if fw, fh := w.size(); fw > 0 && fh > 0 && (fw != w.floatW || fh != w.floatH) {
409 w.floatW, w.floatH = fw, fh
410 w.xdg.server.emit(ports.WindowResized{ID: w.id, Width: fw, Height: fh})
411 }
412}
413
414// floats reports whether the toplevel should float over the columns, as
415// on niri: dialogs (a parent) and fixed-size windows such as splash
416// screens.
417func (w *window) floats() bool {
418 fixed := w.minW > 0 && w.minH > 0 && w.minW == w.maxW && w.minH == w.maxH
419 return w.parent != nil || fixed
420}
421
422// size is the window geometry size, else the surface size, in logical pixels.
423func (w *window) size() (int, int) {
424 if g := w.xdg.geometry; g.W > 0 && g.H > 0 {
425 return g.W, g.H
426 }
427 c := w.xdg.surface.content
428 return c.LogicalW, c.LogicalH
429}
430
431// surfacePoint turns window coordinates (core's, from the geometry origin)
432// into surface coordinates.
433func (w *window) surfacePoint(x, y float64) (float64, float64) {
434 g := w.xdg.geometry
435 return x + float64(g.X), y + float64(g.Y)
436}
437
438func (x *xdgSurface) surfaceResource() *wayland.Surface {
439 for resource, state := range x.server.surfaces {
440 if state == x.surface && resource.Alive() {
441 return wayland.WrapSurface(resource)
442 }
443 }
444 return nil
445}