1package wayland
2
3import (
4 "github.com/bnema/go-wayland-bindings/server/wayland"
5 "github.com/bnema/go-wayland-bindings/server/xdgshell"
6 "github.com/bnema/neferwl/internal/ports"
7 "github.com/bnema/purego-libwayland/server"
8)
9
10// popup is the xdg_popup role of a window. Core places it (PopupRequest →
11// ConfigurePopup); it maps on the first buffer after its configure is
12// acked, and is drawn over its parent like a window without border.
13type popup struct {
14 w *window
15 resource *xdgshell.Popup
16 parent *window
17 // layer is the parent set through zwlr_layer_surface_v1.get_popup,
18 // for a popup created without an xdg parent.
19 layer *layerSurface
20 pos ports.Positioner
21 grab bool
22 // sent is set once core got the PopupRequest; done once dismissed.
23 sent, done bool
24}
25
26func (s *Server) newPopup(x *xdgSurface, r *xdgshell.Surface, id uint32, parentXDG *xdgshell.Surface, pos *xdgshell.Positioner) {
27 p, ok := s.positionerOf(x.wm, pos)
28 if !ok {
29 return
30 }
31 var parent *window
32 if parentXDG != nil {
33 for _, w := range s.windows {
34 if w.xdg.resource != nil && w.xdg.resource.Resource == parentXDG.Resource {
35 parent = w
36 }
37 }
38 }
39 w := &window{id: s.nextWindow, xdg: x}
40 pp := &popup{w: w, parent: parent, pos: p}
41 res, err := xdgshell.NewPopup(r.Client(), r.Version(), id, pp)
42 if err != nil {
43 return
44 }
45 pp.resource, w.popup = res, pp
46 s.nextWindow++
47 s.windows[w.id] = w
48 x.window = w
49 x.surface.role = func(buffer bool) {
50 if x.surface.destroyed {
51 w.unmap()
52 return
53 }
54 switch {
55 case !buffer && !w.mapped && !x.configured && !pp.done:
56 // Without a parent (xdg-shell lets clients set it through
57 // layer-shell before the first commit) the popup has nowhere
58 // to go.
59 if pp.parentID() == 0 {
60 pp.dismiss()
61 return
62 }
63 if !pp.sent {
64 pp.sent = true
65 s.emit(ports.PopupRequest{ID: w.id, Parent: pp.parentID(), Positioner: pp.pos, Grab: pp.grab})
66 }
67 case buffer && !w.mapped && x.acked && !pp.done:
68 w.mapped = true
69 s.castCommitted(w.id)
70 s.emit(ports.PopupMapped{ID: w.id})
71 s.log.Debug().Uint64("id", uint64(w.id)).Uint64("parent", uint64(pp.parentID())).Msg("popup mapped")
72 case !buffer && w.mapped:
73 // Unmapped, it may map again after a new initial commit.
74 w.unmap()
75 pp.sent, pp.grab = false, false
76 }
77 }
78 res.OnDestroy = func() {
79 // Core forgets the popup, placed or mapped.
80 if !w.mapped && pp.sent && !pp.done {
81 s.emit(ports.WindowUnmapped{ID: w.id})
82 }
83 w.unmap()
84 pp.done = true
85 x.surface.dropQueue()
86 x.surface.current, x.surface.next.buffer = nil, nil
87 x.surface.next.attached = false
88 delete(s.windows, w.id)
89 x.window = nil
90 x.surface.role = nil
91 x.configured, x.acked, x.serials = false, false, nil
92 x.geometry, x.pendingGeometry = ports.Rect{}, ports.Rect{}
93 }
94}
95
96func (*popup) Destroy(*xdgshell.Popup) {}
97
98// parentID is the window or layer surface the popup hangs from, 0 if none.
99func (p *popup) parentID() ports.WindowID {
100 switch {
101 case p.parent != nil:
102 return p.parent.id
103 case p.layer != nil:
104 return p.layer.id
105 }
106 return 0
107}
108
109// Grab is only valid before the first commit; core then gives the popup the
110// keyboard and closes it on a click outside its popup chain.
111func (p *popup) Grab(r *xdgshell.Popup, _ *wayland.Seat, serial uint32) {
112 if p.sent || p.w.mapped {
113 r.PostError(uint32(xdgshell.PopupErrorInvalidGrab), "grab after the popup was committed")
114 return
115 }
116 // Only a grab answering the client's last press may take the keyboard,
117 // and a popup over a popup grabs only if its parent does.
118 s := p.w.xdg.server
119 fresh := serial == s.seat.press && s.seat.pressClient == r.Client()
120 if !fresh || p.parentID() == 0 || !s.topGrab(r.Client(), p.parent) {
121 p.dismiss()
122 return
123 }
124 p.grab = true
125}
126
127func (p *popup) Reposition(r *xdgshell.Popup, pos *xdgshell.Positioner, token uint32) {
128 s := p.w.xdg.server
129 next, ok := s.positionerOf(p.w.xdg.wm, pos)
130 if !ok || p.done {
131 return
132 }
133 p.pos = next
134 if p.sent {
135 s.emit(ports.PopupRequest{ID: p.w.id, Parent: p.parentID(), Positioner: next, Grab: p.grab, Reposition: true, Token: token})
136 }
137}
138
139// configure sends core's placement: xdg_popup.configure then
140// xdg_surface.configure.
141func (p *popup) configure(c ports.ConfigurePopup) {
142 if p.done || !p.resource.Resource.Alive() || !p.w.xdg.resource.Resource.Alive() {
143 return
144 }
145 if c.Reposition && p.resource.Version() >= 3 {
146 p.resource.SendRepositioned(c.Token)
147 }
148 p.resource.SendConfigure(int32(c.Rect.X), int32(c.Rect.Y), int32(c.Rect.W), int32(c.Rect.H))
149 s := p.w.xdg.server
150 if p.w.mapped {
151 // A moved popup changes the toplevel's cast picture.
152 s.castCommitted(p.w.id)
153 }
154 s.serial++
155 p.w.xdg.resource.SendConfigure(s.serial)
156 p.w.xdg.serials = append(p.w.xdg.serials, s.serial)
157 p.w.xdg.configured = true
158 if surf := p.w.xdg.surface; surf != nil {
159 surf.sendTreeScale()
160 }
161}
162
163// topGrab reports whether a grabbing popup of client may go on parent:
164// parent is the client's newest live grabbing popup, or a toplevel (nil
165// for a layer surface) when it has none.
166func (s *Server) topGrab(client server.Client, parent *window) bool {
167 var top *window
168 for _, w := range s.windows {
169 if w.popup != nil && w.popup.grab && !w.popup.done && w.xdg.resource.Client() == client && (top == nil || w.id > top.id) {
170 top = w
171 }
172 }
173 if top == nil {
174 return parent == nil || parent.popup == nil
175 }
176 return top == parent
177}
178
179// dismiss sends popup_done once; the client then destroys the popup.
180func (p *popup) dismiss() {
181 if p.done {
182 return
183 }
184 p.done = true
185 if p.resource.Resource.Alive() {
186 p.resource.SendPopupDone()
187 }
188 p.w.unmap()
189}