1package wayland
2
3import (
4 "context"
5 "encoding/binary"
6
7 ext "github.com/bnema/go-wayland-bindings/server/extworkspace"
8 "github.com/bnema/go-wayland-bindings/server/wayland"
9 "github.com/bnema/neferwl/internal/adapters/wayland/imagecapture"
10 "github.com/bnema/neferwl/internal/ports"
11 "github.com/bnema/purego-libwayland/server"
12)
13
14type workspaceManager struct {
15 s *Server
16 res *ext.ExtWorkspaceManagerV1
17 groups map[string]*workspaceGroup
18 handles map[uint64]*workspaceHandle
19 pending []uint64
20}
21type workspaceGroup struct {
22 res *ext.ExtWorkspaceGroupHandleV1
23 outputs map[*server.Resource]bool // output_enter sent for each live wl_output
24}
25type workspaceHandle struct {
26 res *ext.ExtWorkspaceHandleV1
27 info ports.WorkspaceInfo
28 group string
29}
30
31func registerWorkspaces(d *server.Display, s *Server) error {
32 return ext.NewExtWorkspaceManagerV1Global(d, 1, func(c server.Client, v, id uint32) {
33 m := &workspaceManager{s: s, groups: map[string]*workspaceGroup{}, handles: map[uint64]*workspaceHandle{}}
34 r, err := ext.NewExtWorkspaceManagerV1(c, int32(v), id, m)
35 if err != nil {
36 return
37 }
38 m.res = r
39 s.workspaceManagers = append(s.workspaceManagers, m)
40 r.OnDestroy = func() { s.workspaceManagers = removeItem(s.workspaceManagers, m) }
41 m.update(s.workspaceSnapshot)
42 })
43}
44func (m *workspaceManager) Commit(*ext.ExtWorkspaceManagerV1) {
45 if m.s.protected() {
46 m.pending = nil
47 return
48 }
49 ids := make([]uint64, 0, len(m.pending))
50 for _, id := range m.pending {
51 if h := m.handles[id]; h != nil && h.res.Alive() {
52 ids = append(ids, id)
53 }
54 }
55 m.pending = nil
56 if len(ids) > 0 {
57 m.s.emit(ports.WorkspaceActivate{IDs: ids})
58 }
59}
60func (m *workspaceManager) Stop(*ext.ExtWorkspaceManagerV1) {
61 m.res.SendFinished()
62 m.res.Destroy()
63}
64func (*workspaceManager) CreateWorkspace(*ext.ExtWorkspaceGroupHandleV1, string) {}
65func (*workspaceManager) Destroy(*ext.ExtWorkspaceGroupHandleV1) {}
66func (m *workspaceManager) Activate(r *ext.ExtWorkspaceHandleV1) {
67 if m.s.protected() {
68 return
69 }
70 for id, h := range m.handles {
71 if h.res.Resource == r.Resource {
72 m.pending = append(m.pending, id)
73 return
74 }
75 }
76}
77
78// workspaceRequests adapts the workspace destroy request separately from group destroy.
79type workspaceRequests struct{ m *workspaceManager }
80
81func (*workspaceRequests) Destroy(*ext.ExtWorkspaceHandleV1) {}
82func (w *workspaceRequests) Activate(r *ext.ExtWorkspaceHandleV1) { w.m.Activate(r) }
83func (*workspaceRequests) Deactivate(*ext.ExtWorkspaceHandleV1) {}
84func (*workspaceRequests) Assign(*ext.ExtWorkspaceHandleV1, *ext.ExtWorkspaceGroupHandleV1) {}
85func (*workspaceRequests) Remove(*ext.ExtWorkspaceHandleV1) {}
86
87// outputBound announces every wl_output resource bound by this client for an
88// assigned output. A client may bind the same global more than once.
89func (m *workspaceManager) outputBound(name string) bool {
90 if m.s.protected() {
91 return false
92 }
93 g := m.groups[name]
94 o := m.s.outputByNameExact(name)
95 if g == nil || !g.res.Alive() || o == nil {
96 return false
97 }
98 if g.outputs == nil {
99 g.outputs = make(map[*server.Resource]bool)
100 }
101 changed := false
102 for _, r := range o.resources {
103 if r.Client() != m.res.Client() || !r.Alive() || g.outputs[r.Resource] {
104 continue
105 }
106 g.outputs[r.Resource] = true
107 g.res.SendOutputEnter(r)
108 changed = true
109 }
110 return changed
111}
112
113func (m *workspaceManager) leaveOutputs(g *workspaceGroup) {
114 if !g.res.Alive() {
115 return
116 }
117 for res := range g.outputs {
118 if res.Alive() {
119 g.res.SendOutputLeave(wayland.WrapOutput(res))
120 }
121 }
122}
123func (s *Server) forwardWorkspaces(ctx context.Context) {
124 for {
125 select {
126 case <-ctx.Done():
127 return
128 case <-s.display.Stopped():
129 return
130 case snapshot, ok := <-s.channels.Workspaces:
131 if !ok {
132 return
133 }
134 if !s.display.Do(func() { s.updateWorkspaces(snapshot) }) {
135 return
136 }
137 }
138 }
139}
140func (s *Server) updateWorkspaces(snapshot ports.Workspaces) {
141 s.workspaceSnapshot = snapshot
142 s.updateWorkspaceManagers(snapshot)
143}
144
145// updateWorkspaceManagers also refreshes inventory on protection transitions.
146// Keep the last sent state while protected: old client knowledge cannot be
147// revoked, but an unlock refresh must compare against it, not suppressed updates.
148func (s *Server) updateWorkspaceManagers(snapshot ports.Workspaces) {
149 for _, m := range s.workspaceManagers {
150 m.update(snapshot)
151 }
152 s.updateWorkspaceFrames()
153 s.refreshSessions()
154}
155
156// workspaceFrame is a neferwl_workspace_frame_v1: the frame of one workspace,
157// output-local logical, sent when it is created and when it changes. Nothing
158// is sent for a workspace that is gone, nor while the session is protected.
159type workspaceFrame struct {
160 res *imagecapture.NeferwlWorkspaceFrameV1
161 handle *server.Resource
162 id uint64
163 sent bool
164 last ports.Rect
165}
166
167func (*workspaceFrame) Destroy(*imagecapture.NeferwlWorkspaceFrameV1) {}
168
169func (s *Server) newWorkspaceFrame(c server.Client, id uint32, ws *ext.ExtWorkspaceHandleV1) {
170 f := &workspaceFrame{}
171 known := false
172 if ws != nil {
173 f.handle = ws.Resource
174 f.id, known = s.workspaceIDOf(ws.Resource)
175 }
176 r, err := imagecapture.NewNeferwlWorkspaceFrameV1(c, 1, id, f)
177 if err != nil {
178 return
179 }
180 f.res = r
181 if !known {
182 return
183 }
184 s.workspaceFrames = append(s.workspaceFrames, f)
185 r.OnDestroy = func() { s.workspaceFrames = removeItem(s.workspaceFrames, f) }
186 s.updateWorkspaceFrames()
187}
188
189// updateWorkspaceFrames sends the frame of each workspace whose last sent one
190// differs. A removed workspace gets nothing and its entry is dropped.
191func (s *Server) updateWorkspaceFrames() {
192 if s.protected() {
193 return
194 }
195 kept := s.workspaceFrames[:0]
196 for _, f := range s.workspaceFrames {
197 // The handle must still exist: a removed or renamed handle never
198 // returns, so its frame object is dropped.
199 if id, ok := s.workspaceIDOf(f.handle); !ok || id != f.id {
200 continue
201 }
202 kept = append(kept, f)
203 if !f.res.Alive() {
204 continue
205 }
206 _, w, ok := s.workspaceInfo(f.id)
207 if !ok || (f.sent && f.last == w.Frame) {
208 continue
209 }
210 f.sent, f.last = true, w.Frame
211 f.res.SendFrame(int32(w.Frame.X), int32(w.Frame.Y), int32(w.Frame.W), int32(w.Frame.H))
212 }
213 clear(s.workspaceFrames[len(kept):])
214 s.workspaceFrames = kept
215}
216
217func workspaceState(w ports.WorkspaceInfo) uint32 {
218 var state uint32
219 if w.Active {
220 state |= uint32(ext.ExtWorkspaceHandleV1StateActive)
221 }
222 if w.Hidden {
223 state |= uint32(ext.ExtWorkspaceHandleV1StateHidden)
224 }
225 return state
226}
227func (m *workspaceManager) sendCoordinates(r *ext.ExtWorkspaceHandleV1, index int) {
228 var b [4]byte
229 binary.NativeEndian.PutUint32(b[:], uint32(index))
230 r.SendCoordinates(b[:])
231}
232func (m *workspaceManager) update(snapshot ports.Workspaces) {
233 if m.s.protected() {
234 m.pending = nil
235 return
236 }
237 if !m.res.Alive() {
238 return
239 }
240 wanted := map[string]bool{}
241 infos := map[uint64]ports.WorkspaceInfo{}
242 for _, out := range snapshot.Outputs {
243 wanted[out.Name] = true
244 for _, w := range out.Workspaces {
245 infos[w.ID] = w
246 }
247 }
248 // Move existing workspaces before retiring their old output group.
249 for id, h := range m.handles {
250 // A handle's id is fixed: a workspace that became configured or
251 // unconfigured (a rename in the config) is a removed handle and a
252 // new one.
253 if info, exists := infos[id]; !exists || info.Configured != h.info.Configured {
254 if g := m.groups[h.group]; g != nil && g.res.Alive() && h.res.Alive() {
255 g.res.SendWorkspaceLeave(h.res)
256 }
257 if h.res.Alive() {
258 h.res.SendRemoved()
259 }
260 delete(m.handles, id)
261 }
262 }
263 for _, out := range snapshot.Outputs {
264 g := m.groups[out.Name]
265 if g == nil {
266 r, err := ext.NewExtWorkspaceGroupHandleV1(m.res.Client(), 1, 0, m)
267 if err != nil {
268 continue
269 }
270 g = &workspaceGroup{res: r, outputs: map[*server.Resource]bool{}}
271 m.groups[out.Name] = g
272 m.res.SendWorkspaceGroup(r)
273 r.SendCapabilities(0)
274 }
275 m.outputBound(out.Name)
276 for _, w := range out.Workspaces {
277 if !g.res.Alive() {
278 continue
279 }
280 h := m.handles[w.ID]
281 if h == nil {
282 r, err := ext.NewExtWorkspaceHandleV1(m.res.Client(), 1, 0, &workspaceRequests{m})
283 if err != nil {
284 continue
285 }
286 h = &workspaceHandle{res: r}
287 m.handles[w.ID] = h
288 m.res.SendWorkspace(r)
289 r.SendId(m.s.workspaceIDs.ID(w.ID, w.Configured))
290 r.SendName(w.Name)
291 m.sendCoordinates(r, w.Index)
292 r.SendState(workspaceState(w))
293 r.SendCapabilities(uint32(ext.ExtWorkspaceHandleV1WorkspaceCapabilitiesActivate))
294 h.info = w
295 }
296 if !h.res.Alive() {
297 continue
298 }
299 if h.info.ID != w.ID {
300 continue
301 }
302 if h.info != w {
303 if h.info.Name != w.Name {
304 h.res.SendName(w.Name)
305 }
306 if h.info.Index != w.Index {
307 m.sendCoordinates(h.res, w.Index)
308 }
309 if h.info.Active != w.Active || h.info.Hidden != w.Hidden {
310 h.res.SendState(workspaceState(w))
311 }
312 h.info = w
313 }
314 if h.group != out.Name {
315 if old := m.groups[h.group]; old != nil && old.res.Alive() && h.res.Alive() {
316 old.res.SendWorkspaceLeave(h.res)
317 }
318 g.res.SendWorkspaceEnter(h.res)
319 h.group = out.Name
320 }
321 }
322 }
323 for name, g := range m.groups {
324 if !wanted[name] {
325 m.leaveOutputs(g)
326 if g.res.Alive() {
327 g.res.SendRemoved()
328 }
329 delete(m.groups, name)
330 }
331 }
332 m.res.SendDone()
333}