1package wayland
2
3import (
4 "slices"
5 "strconv"
6
7 ext "github.com/bnema/go-wayland-bindings/server/extforeigntoplevellist"
8 "github.com/bnema/neferwl/internal/ports"
9 "github.com/bnema/purego-libwayland/server"
10)
11
12// ext-foreign-toplevel-list lists mapped toplevels, read-only: title, app_id
13// and a stable identifier. xdg-desktop-portal-wlr lists the windows it can
14// share with it, and ext_foreign_toplevel_image_capture_source_manager_v1
15// takes its handles (window capture, imagecopy.go). It follows the same
16// window state and session-protection rules as wlr-foreign-toplevel.
17
18type extToplevelList struct {
19 s *Server
20 res *ext.ExtForeignToplevelListV1
21 stopped bool
22 handles map[ports.WindowID]*extToplevelHandle
23}
24
25// extToplevelHandle is one window seen by one list, with what it was last
26// sent so that only changes go out.
27type extToplevelHandle struct {
28 res *ext.ExtForeignToplevelHandleV1
29 window ports.WindowID
30 gen uint32 // the window's mapping it was made for
31 closed bool
32 title, appID string
33}
34
35func (extToplevelHandle) Destroy(*ext.ExtForeignToplevelHandleV1) {}
36
37// toplevelIdentifier is the ext-foreign-toplevel identifier of a window's
38// mapping: window IDs are never reused and a remap counts a new mapping, so
39// neither is the identifier.
40func toplevelIdentifier(id ports.WindowID, gen uint32) string {
41 return "neferwl-" + strconv.FormatUint(uint64(id), 10) + "-" + strconv.FormatUint(uint64(gen), 10)
42}
43
44func registerExtForeignToplevel(d *server.Display, s *Server) error {
45 return ext.NewExtForeignToplevelListV1Global(d, 1, func(c server.Client, v, id uint32) {
46 l := &extToplevelList{s: s, handles: map[ports.WindowID]*extToplevelHandle{}}
47 r, err := ext.NewExtForeignToplevelListV1(c, int32(v), id, l)
48 if err != nil {
49 return
50 }
51 l.res = r
52 s.extToplevelLists = append(s.extToplevelLists, l)
53 r.OnDestroy = func() { s.extToplevelLists = removeItem(s.extToplevelLists, l) }
54 for _, id := range s.mappedToplevels() {
55 l.refresh(s.windows[id])
56 }
57 })
58}
59
60// Stop ends the list: finished, and no more toplevel events.
61func (l *extToplevelList) Stop(*ext.ExtForeignToplevelListV1) {
62 if l.stopped {
63 return
64 }
65 l.stopped = true
66 l.res.SendFinished()
67}
68
69func (*extToplevelList) Destroy(*ext.ExtForeignToplevelListV1) {}
70
71// mappedToplevels lists the mapped toplevels, by ID.
72func (s *Server) mappedToplevels() []ports.WindowID {
73 ids := make([]ports.WindowID, 0, len(s.windows))
74 for id, w := range s.windows {
75 if w.toplevel != nil && w.mapped {
76 ids = append(ids, id)
77 }
78 }
79 slices.Sort(ids)
80 return ids
81}
82
83// refresh announces w or sends what changed since the last done. After
84// stop no new toplevel is announced; the handles already sent stay live.
85func (l *extToplevelList) refresh(w *window) {
86 if l.s.protected() || !l.res.Alive() {
87 return
88 }
89 h := l.handles[w.id]
90 if h != nil && (h.closed || h.gen != w.gen) {
91 l.closed(w.id)
92 h = nil
93 }
94 fresh := h == nil
95 if fresh && l.stopped {
96 return
97 }
98 if fresh {
99 h = &extToplevelHandle{window: w.id, gen: w.gen}
100 r, err := ext.NewExtForeignToplevelHandleV1(l.res.Client(), l.res.Version(), 0, h)
101 if err != nil {
102 return
103 }
104 h.res = r
105 l.handles[w.id] = h
106 l.res.SendToplevel(r)
107 r.SendIdentifier(toplevelIdentifier(w.id, w.gen))
108 }
109 if !h.res.Alive() {
110 return
111 }
112 changed := fresh
113 if fresh || h.title != w.title {
114 h.title = w.title
115 h.res.SendTitle(w.title)
116 changed = true
117 }
118 if fresh || h.appID != w.appID {
119 h.appID = w.appID
120 h.res.SendAppId(w.appID)
121 changed = true
122 }
123 if changed {
124 h.res.SendDone()
125 }
126}
127
128// closed sends closed for a window; like wlr-foreign-toplevel, a close
129// during session protection is sent at the unlock.
130func (l *extToplevelList) closed(id ports.WindowID) {
131 h := l.handles[id]
132 if h == nil {
133 return
134 }
135 h.closed = true
136 if l.s.protected() {
137 return
138 }
139 delete(l.handles, id)
140 if h.res.Alive() {
141 h.res.SendClosed()
142 }
143}
144
145// extHandleWindow is the window mapping behind an
146// ext_foreign_toplevel_handle_v1 of any list, while it is open.
147func (s *Server) extHandleWindow(res *server.Resource) (ports.WindowID, uint32, bool) {
148 for _, l := range s.extToplevelLists {
149 for id, h := range l.handles {
150 if h.res.Resource == res && !h.closed {
151 return id, h.gen, true
152 }
153 }
154 }
155 return 0, 0, false
156}