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