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.emit(ports.PopupMapped{ID: w.id})
70 s.log.Debug().Uint64("id", uint64(w.id)).Uint64("parent", uint64(pp.parentID())).Msg("popup mapped")
71 case !buffer && w.mapped:
72 // Unmapped, it may map again after a new initial commit.
73 w.unmap()
74 pp.sent, pp.grab = false, false
75 }
76 }
77 res.OnDestroy = func() {
78 // Core forgets the popup, placed or mapped.
79 if !w.mapped && pp.sent && !pp.done {
80 s.emit(ports.WindowUnmapped{ID: w.id})
81 }
82 w.unmap()
83 pp.done = true
84 x.surface.dropQueue()
85 x.surface.current, x.surface.next.buffer = nil, nil
86 x.surface.next.attached = false
87 delete(s.windows, w.id)
88 x.window = nil
89 x.surface.role = nil
90 x.configured, x.acked, x.serials = false, false, nil
91 x.geometry, x.pendingGeometry = ports.Rect{}, ports.Rect{}
92 }
93}
94
95func (*popup) Destroy(*xdgshell.Popup) {}
96
97// parentID is the window or layer surface the popup hangs from, 0 if none.
98func (p *popup) parentID() ports.WindowID {
99 switch {
100 case p.parent != nil:
101 return p.parent.id
102 case p.layer != nil:
103 return p.layer.id
104 }
105 return 0
106}
107
108// Grab is only valid before the first commit; core then gives the popup the
109// keyboard and closes it on a click outside its popup chain.
110func (p *popup) Grab(r *xdgshell.Popup, _ *wayland.Seat, serial uint32) {
111 if p.sent || p.w.mapped {
112 r.PostError(uint32(xdgshell.PopupErrorInvalidGrab), "grab after the popup was committed")
113 return
114 }
115 // Only a grab answering the client's last press may take the keyboard,
116 // and a popup over a popup grabs only if its parent does.
117 s := p.w.xdg.server
118 fresh := serial == s.seat.press && s.seat.pressClient == r.Client()
119 if !fresh || p.parentID() == 0 || !s.topGrab(r.Client(), p.parent) {
120 p.dismiss()
121 return
122 }
123 p.grab = true
124}
125
126func (p *popup) Reposition(r *xdgshell.Popup, pos *xdgshell.Positioner, token uint32) {
127 s := p.w.xdg.server
128 next, ok := s.positionerOf(p.w.xdg.wm, pos)
129 if !ok || p.done {
130 return
131 }
132 p.pos = next
133 if p.sent {
134 s.emit(ports.PopupRequest{ID: p.w.id, Parent: p.parentID(), Positioner: next, Grab: p.grab, Reposition: true, Token: token})
135 }
136}
137
138// configure sends core's placement: xdg_popup.configure then
139// xdg_surface.configure.
140func (p *popup) configure(c ports.ConfigurePopup) {
141 if p.done || !p.resource.Resource.Alive() || !p.w.xdg.resource.Resource.Alive() {
142 return
143 }
144 if c.Reposition && p.resource.Version() >= 3 {
145 p.resource.SendRepositioned(c.Token)
146 }
147 p.resource.SendConfigure(int32(c.Rect.X), int32(c.Rect.Y), int32(c.Rect.W), int32(c.Rect.H))
148 s := p.w.xdg.server
149 s.serial++
150 p.w.xdg.resource.SendConfigure(s.serial)
151 p.w.xdg.serials = append(p.w.xdg.serials, s.serial)
152 p.w.xdg.configured = true
153 if surf := p.w.xdg.surface; surf != nil {
154 surf.sendTreeScale()
155 }
156}
157
158// topGrab reports whether a grabbing popup of client may go on parent:
159// parent is the client's newest live grabbing popup, or a toplevel (nil
160// for a layer surface) when it has none.
161func (s *Server) topGrab(client server.Client, parent *window) bool {
162 var top *window
163 for _, w := range s.windows {
164 if w.popup != nil && w.popup.grab && !w.popup.done && w.xdg.resource.Client() == client && (top == nil || w.id > top.id) {
165 top = w
166 }
167 }
168 if top == nil {
169 return parent == nil || parent.popup == nil
170 }
171 return top == parent
172}
173
174// dismiss sends popup_done once; the client then destroys the popup.
175func (p *popup) dismiss() {
176 if p.done {
177 return
178 }
179 p.done = true
180 if p.resource.Resource.Alive() {
181 p.resource.SendPopupDone()
182 }
183 p.w.unmap()
184}