1package wayland
2
3import (
4 "bytes"
5 "encoding/binary"
6 "slices"
7
8 "github.com/bnema/go-wayland-bindings/server/wayland"
9 wlr "github.com/bnema/go-wayland-bindings/server/wlrforeigntoplevelmanagement"
10 "github.com/bnema/neferwl/internal/ports"
11 "github.com/bnema/purego-libwayland/server"
12)
13
14// wlr-foreign-toplevel-management lists mapped toplevels for taskbars and
15// notification daemons (Dunst reads the fullscreen state). Every client
16// may bind it, as on other compositors: it reveals less than screencopy.
17// Requests reuse the messages of xdg-shell and xdg-activation.
18
19type toplevelManager struct {
20 s *Server
21 res *wlr.ZwlrForeignToplevelManagerV1
22 handles map[ports.WindowID]*toplevelHandle
23}
24
25// toplevelHandle is one window seen by one manager, with what it was last
26// sent so that only changes go out.
27type toplevelHandle struct {
28 res *wlr.ZwlrForeignToplevelHandleV1
29 req *toplevelRequests
30 title, appID string
31 state []byte
32 outputs map[*server.Resource]bool // output_enter sent
33}
34
35func registerForeignToplevel(d *server.Display, s *Server) error {
36 return wlr.NewZwlrForeignToplevelManagerV1Global(d, 3, func(c server.Client, v, id uint32) {
37 m := &toplevelManager{s: s, handles: map[ports.WindowID]*toplevelHandle{}}
38 r, err := wlr.NewZwlrForeignToplevelManagerV1(c, int32(v), id, m)
39 if err != nil {
40 return
41 }
42 m.res = r
43 s.toplevelManagers = append(s.toplevelManagers, m)
44 r.OnDestroy = func() { s.toplevelManagers = removeItem(s.toplevelManagers, m) }
45 ids := make([]ports.WindowID, 0, len(s.windows))
46 for id, w := range s.windows {
47 if w.toplevel != nil && w.mapped {
48 ids = append(ids, id)
49 }
50 }
51 slices.Sort(ids)
52 for _, id := range ids {
53 m.refresh(s.windows[id])
54 }
55 })
56}
57
58func (m *toplevelManager) Stop(*wlr.ZwlrForeignToplevelManagerV1) {
59 m.res.SendFinished()
60 m.res.Destroy()
61}
62
63// refresh announces w or sends what changed since the last done.
64func (m *toplevelManager) refresh(w *window) {
65 if m.s.protected() || !m.res.Alive() {
66 return
67 }
68 h := m.handles[w.id]
69 if h != nil && h.req.closed {
70 m.closed(w.id)
71 h = nil
72 }
73 fresh := h == nil
74 if fresh {
75 req := &toplevelRequests{s: m.s, id: w.id}
76 r, err := wlr.NewZwlrForeignToplevelHandleV1(m.res.Client(), m.res.Version(), 0, req)
77 if err != nil {
78 return
79 }
80 h = &toplevelHandle{res: r, req: req, outputs: map[*server.Resource]bool{}}
81 m.handles[w.id] = h
82 m.res.SendToplevel(r)
83 }
84 if !h.res.Alive() {
85 return
86 }
87 changed := fresh
88 if fresh || h.title != w.title {
89 h.title = w.title
90 h.res.SendTitle(w.title)
91 changed = true
92 }
93 if fresh || h.appID != w.appID {
94 h.appID = w.appID
95 h.res.SendAppId(w.appID)
96 changed = true
97 }
98 if state := toplevelState(w, h.res.Version()); fresh || !bytes.Equal(h.state, state) {
99 h.state = state
100 h.res.SendState(state)
101 changed = true
102 }
103 if h.syncOutputs(m.s, m.res.Client(), w) {
104 changed = true
105 }
106 if changed {
107 h.res.SendDone()
108 }
109}
110
111// closed sends closed for w; the client then destroys the handle.
112func (m *toplevelManager) closed(id ports.WindowID) {
113 h := m.handles[id]
114 if h == nil {
115 return
116 }
117 // A remapped window keeps its ID but gets a new handle. Mark requests
118 // inert immediately, but defer the wire event to avoid leaking lock-time
119 // unmap/disconnect timing. Keep the handle for the unlock refresh.
120 h.req.closed = true
121 if m.s.protected() {
122 return
123 }
124 delete(m.handles, id)
125 if h.res.Alive() {
126 h.res.SendClosed()
127 }
128}
129
130// syncOutputs sends output_enter for every wl_output the client bound for
131// the window's output and output_leave for the others.
132func (h *toplevelHandle) syncOutputs(s *Server, c server.Client, w *window) bool {
133 want := map[*server.Resource]bool{}
134 if o := s.outputByNameExact(w.last.Output); o != nil && w.hasLast {
135 for _, r := range o.resources {
136 if r.Client() == c && r.Alive() {
137 want[r.Resource] = true
138 }
139 }
140 }
141 changed := false
142 for r := range h.outputs {
143 if !want[r] {
144 delete(h.outputs, r)
145 if r.Alive() {
146 h.res.SendOutputLeave(wayland.WrapOutput(r))
147 changed = true
148 }
149 }
150 }
151 for r := range want {
152 if !h.outputs[r] {
153 h.outputs[r] = true
154 h.res.SendOutputEnter(wayland.WrapOutput(r))
155 changed = true
156 }
157 }
158 return changed
159}
160
161// toplevelState mirrors the xdg_toplevel states of the last configure:
162// tiled windows are maximized. Fullscreen exists since version 2.
163func toplevelState(w *window, version int32) []byte {
164 if !w.hasLast {
165 return []byte{}
166 }
167 c := w.last
168 var states []byte
169 add := func(st wlr.ZwlrForeignToplevelHandleV1State) {
170 states = binary.NativeEndian.AppendUint32(states, uint32(st))
171 }
172 if !c.Fullscreen && !c.Floating {
173 add(wlr.ZwlrForeignToplevelHandleV1StateMaximized)
174 }
175 if c.Activated {
176 add(wlr.ZwlrForeignToplevelHandleV1StateActivated)
177 }
178 if c.Fullscreen && version >= 2 {
179 add(wlr.ZwlrForeignToplevelHandleV1StateFullscreen)
180 }
181 if states == nil {
182 return []byte{}
183 }
184 return states
185}
186
187// toplevelChanged updates every manager after w mapped or changed.
188func (s *Server) toplevelChanged(w *window) {
189 if w.toplevel == nil || !w.mapped {
190 return
191 }
192 for _, m := range s.toplevelManagers {
193 m.refresh(w)
194 }
195 for _, l := range s.extToplevelLists {
196 l.refresh(w)
197 }
198}
199
200// toplevelClosed tells every manager that w unmapped.
201func (s *Server) toplevelClosed(id ports.WindowID) {
202 for _, m := range s.toplevelManagers {
203 m.closed(id)
204 }
205 for _, l := range s.extToplevelLists {
206 l.closed(id)
207 }
208 s.windowCaptureGone(id)
209}
210
211// refreshToplevels reconciles inventory after output changes or unlock:
212// retire handles closed during protection, then announce current windows,
213// including those mapped (or managers bound) while inventory was suppressed.
214func (s *Server) refreshToplevels() {
215 if s.protected() {
216 return
217 }
218 ids := s.mappedToplevels()
219 for _, l := range s.extToplevelLists {
220 for id, h := range l.handles {
221 w := s.windows[id]
222 if h.closed || w == nil || w.toplevel == nil || !w.mapped {
223 l.closed(id)
224 }
225 }
226 for _, id := range ids {
227 l.refresh(s.windows[id])
228 }
229 }
230 for _, m := range s.toplevelManagers {
231 for id, h := range m.handles {
232 w := s.windows[id]
233 if h.req.closed || w == nil || w.toplevel == nil || !w.mapped {
234 m.closed(id)
235 }
236 }
237 for _, id := range ids {
238 m.refresh(s.windows[id])
239 }
240 }
241}
242
243// toplevelRequests handles requests on one handle. Requests on a closed
244// handle are ignored.
245type toplevelRequests struct {
246 s *Server
247 id ports.WindowID
248 closed bool
249}
250
251func (t *toplevelRequests) window() *window {
252 if t.s.protected() || t.closed {
253 return nil
254 }
255 if w := t.s.windows[t.id]; w != nil && w.toplevel != nil && w.mapped && w.toplevel.Resource.Alive() {
256 return w
257 }
258 return nil
259}
260
261// Tiled windows are always maximized and nothing minimizes: these are no-ops.
262func (*toplevelRequests) SetMaximized(*wlr.ZwlrForeignToplevelHandleV1) {}
263func (*toplevelRequests) UnsetMaximized(*wlr.ZwlrForeignToplevelHandleV1) {}
264func (*toplevelRequests) SetMinimized(*wlr.ZwlrForeignToplevelHandleV1) {}
265func (*toplevelRequests) UnsetMinimized(*wlr.ZwlrForeignToplevelHandleV1) {}
266func (*toplevelRequests) Destroy(*wlr.ZwlrForeignToplevelHandleV1) {}
267
268func (t *toplevelRequests) Activate(*wlr.ZwlrForeignToplevelHandleV1, *wayland.Seat) {
269 if w := t.window(); w != nil {
270 t.s.emit(ports.WindowActivate{ID: w.id})
271 }
272}
273func (t *toplevelRequests) Close(*wlr.ZwlrForeignToplevelHandleV1) {
274 if w := t.window(); w != nil {
275 w.toplevel.SendClose()
276 }
277}
278func (t *toplevelRequests) SetRectangle(r *wlr.ZwlrForeignToplevelHandleV1, _ *wayland.Surface, _, _, w, h int32) {
279 if !t.closed && (w < 0 || h < 0) {
280 r.PostError(uint32(wlr.ZwlrForeignToplevelHandleV1ErrorInvalidRectangle), "negative rectangle size")
281 }
282}
283func (t *toplevelRequests) SetFullscreen(*wlr.ZwlrForeignToplevelHandleV1, *wayland.Output) {
284 if w := t.window(); w != nil {
285 t.s.emit(ports.WindowFullscreenRequest{ID: w.id, Fullscreen: true, External: true})
286 }
287}
288func (t *toplevelRequests) UnsetFullscreen(*wlr.ZwlrForeignToplevelHandleV1) {
289 if w := t.window(); w != nil {
290 t.s.emit(ports.WindowFullscreenRequest{ID: w.id, External: true})
291 }
292}