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