1package neferclient
2
3import (
4 "errors"
5 "fmt"
6 "math"
7
8 "github.com/bnema/go-wayland-bindings/client/xdgshell"
9)
10
11// PopupEdge names an edge or corner, used both as the anchor on the parent's
12// anchor rectangle and as the gravity of the popup. The values equal the
13// xdg_positioner anchor and gravity enums.
14type PopupEdge uint8
15
16const (
17 EdgeNone PopupEdge = iota
18 EdgeTop
19 EdgeBottom
20 EdgeLeft
21 EdgeRight
22 EdgeTopLeft
23 EdgeBottomLeft
24 EdgeTopRight
25 EdgeBottomRight
26)
27
28// PopupAdjust is a bit set telling the compositor how to move a popup that
29// would not fit on the output. The bits equal xdg_positioner
30// constraint_adjustment.
31type PopupAdjust uint8
32
33const (
34 AdjustSlideX PopupAdjust = 1 << iota
35 AdjustSlideY
36 AdjustFlipX
37 AdjustFlipY
38 AdjustResizeX
39 AdjustResizeY
40)
41
42const adjustAll = AdjustSlideX | AdjustSlideY | AdjustFlipX | AdjustFlipY | AdjustResizeX | AdjustResizeY
43
44// PopupPlacement tells the compositor where a popup goes relative to its
45// parent. Every length is in logical pixels.
46type PopupPlacement struct {
47 Anchor Rect // on the parent surface; Width and Height must be > 0
48 Edge, Gravity PopupEdge
49 Adjust PopupAdjust
50 Offset [2]int32
51 Width, Height int32 // popup size; must be > 0
52}
53
54// PopupConfig requests an xdg_popup.
55type PopupConfig struct {
56 // Parent is a layer surface, a toplevel or another popup. It must be open.
57 Parent *Surface
58 Place PopupPlacement
59 // Grab takes an explicit grab: the popup gets the keyboard and is
60 // dismissed by a click outside it. It needs a bound seat.
61 Grab bool
62 // Serial is the [PointerEvent.Serial] of the press that opened the popup.
63 // It is required (non-zero) with Grab.
64 Serial uint32
65}
66
67func validatePlacement(p *PopupPlacement) error {
68 switch {
69 case p.Anchor.Width <= 0 || p.Anchor.Height <= 0:
70 return fmt.Errorf("neferclient: popup anchor rectangle %dx%d must be positive", p.Anchor.Width, p.Anchor.Height)
71 case p.Width <= 0 || p.Height <= 0:
72 return fmt.Errorf("neferclient: invalid popup size %dx%d", p.Width, p.Height)
73 case p.Edge > EdgeBottomRight || p.Gravity > EdgeBottomRight:
74 return fmt.Errorf("neferclient: invalid popup edge %d or gravity %d", p.Edge, p.Gravity)
75 case p.Adjust&^adjustAll != 0:
76 return fmt.Errorf("neferclient: invalid popup constraint adjustment %#x", uint8(p.Adjust))
77 case int64(p.Anchor.X)+int64(p.Anchor.Width) > math.MaxInt32 || int64(p.Anchor.Y)+int64(p.Anchor.Height) > math.MaxInt32:
78 return errors.New("neferclient: popup anchor rectangle overflows")
79 }
80 return nil
81}
82
83// newPositioner builds an xdg_positioner from p. The caller destroys it.
84func (c *Conn) newPositioner(p *PopupPlacement) (*xdgshell.XdgPositioner, error) {
85 pos, err := c.g.xdg.CreatePositioner()
86 if err != nil {
87 return nil, err
88 }
89 for _, e := range []error{
90 pos.SetSize(p.Width, p.Height),
91 pos.SetAnchorRect(p.Anchor.X, p.Anchor.Y, p.Anchor.Width, p.Anchor.Height),
92 pos.SetAnchor(uint32(p.Edge)),
93 pos.SetGravity(uint32(p.Gravity)),
94 pos.SetConstraintAdjustment(uint32(p.Adjust)),
95 pos.SetOffset(p.Offset[0], p.Offset[1]),
96 } {
97 if e != nil {
98 return nil, errors.Join(e, pos.Destroy())
99 }
100 }
101 return pos, nil
102}
103
104// NewPopup creates an xdg_popup on a layer surface, a toplevel or another
105// popup. It becomes presentable after the first Handler.Configure, which
106// reports the size the compositor gave it. Handle [Handler.PopupDone]: the
107// compositor dismisses the popup on an outside click or when the grab is
108// denied, and the popup must then be closed.
109//
110// The parent must be mapped: configured by the compositor and, for the
111// compositor to place the popup, presented at least once. NewPopup returns an
112// error for a parent that has not been configured yet; a compositor dismisses
113// (PopupDone) a popup whose parent shows nothing.
114//
115// A grab needs a seat and the serial of the user action. Its parent must be a
116// toplevel or layer surface, or the grabbing popup that is the topmost of the
117// connection's grab chain; only one chain exists at a time. Other grabs return
118// an error before any request is sent (xdg-shell would raise a protocol error).
119//
120// Close popups before their parent: Close of a surface with open child popups
121// fails.
122func (c *Conn) NewPopup(cfg PopupConfig) (*Surface, error) {
123 if c.closed {
124 return nil, ErrClosed
125 }
126 parent := cfg.Parent
127 switch {
128 case parent == nil || parent.c != c:
129 return nil, errors.New("neferclient: popup needs a parent surface of this connection")
130 case parent.closed:
131 return nil, fmt.Errorf("neferclient: popup parent: %w", ErrClosed)
132 case parent.role != roleLayer && parent.role != roleToplevel && parent.role != rolePopup:
133 return nil, errors.New("neferclient: popup parent must be a layer surface, a toplevel or a popup")
134 case !parent.configured:
135 return nil, errors.New("neferclient: popup parent is not configured yet: wait for its first Handler.Configure")
136 }
137 if err := validatePlacement(&cfg.Place); err != nil {
138 return nil, err
139 }
140 if cfg.Grab {
141 if cfg.Serial == 0 {
142 return nil, errors.New("neferclient: popup grab needs the serial of the triggering input event")
143 }
144 c.ensureSeat()
145 if c.seat == nil || c.seat.proxy == nil {
146 cause := c.seatErr
147 if cause == nil {
148 cause = errors.New("popup grab needs a seat")
149 }
150 return nil, &CapabilityError{Name: "wl_seat", Cause: cause}
151 }
152 if parent.role == rolePopup && !parent.grabbed {
153 return nil, errors.New("neferclient: popup grab needs a grabbing parent popup")
154 }
155 if c.grabTop != nil && parent != c.grabTop {
156 return nil, errors.New("neferclient: popup grab needs the topmost grabbing popup as parent while a grab is open")
157 }
158 }
159 if err := c.ensureXdg(); err != nil {
160 return nil, err
161 }
162 c.pause()
163 defer c.resume()
164 s, err := c.newSurface(rolePopup, cfg.Place.Width, cfg.Place.Height)
165 if err != nil {
166 return nil, err
167 }
168 ok := false
169 defer func() {
170 if !ok {
171 s.destroy()
172 }
173 }()
174 if s.xdg, err = c.g.xdg.GetXdgSurface(s.surf); err != nil {
175 return nil, err
176 }
177 pos, err := c.newPositioner(&cfg.Place)
178 if err != nil {
179 return nil, err
180 }
181 var parentXdg *xdgshell.XdgSurface
182 if parent.role != roleLayer {
183 parentXdg = parent.xdg
184 }
185 s.popup, err = s.xdg.GetPopup(parentXdg, pos)
186 // The compositor copies the state at get_popup.
187 if err = errors.Join(err, pos.Destroy()); err != nil {
188 return nil, err
189 }
190 if parent.role == roleLayer {
191 if err = parent.layer.GetPopup(s.popup); err != nil {
192 return nil, err
193 }
194 }
195 q, sid := c.q, uint32(s.id)
196 s.popup.OnConfigure(func(_, _, w, h int32) {
197 e := event{kind: evPopupConfigure, id: sid, a: w, b: h}
198 if w < 0 || h < 0 || w > maxLayerDimension || h > maxLayerDimension {
199 e = event{kind: evBadEvent, id: sid, a: badConfigure}
200 }
201 q.post(&e)
202 })
203 s.popup.OnPopupDone(func() {
204 e := event{kind: evPopupDone, id: sid}
205 q.post(&e)
206 })
207 s.xdg.OnConfigure(func(serial uint32) {
208 e := event{kind: evXdgConfigure, id: sid, serial: serial}
209 q.post(&e)
210 })
211 if cfg.Grab {
212 if err = s.popup.Grab(c.seat.proxy, cfg.Serial); err != nil {
213 return nil, err
214 }
215 s.grabbed, c.grabTop = true, s
216 }
217 if err = s.finishSurface(); err != nil {
218 return nil, err
219 }
220 if err = s.surf.Commit(); err != nil {
221 return nil, err
222 }
223 s.parent = parent
224 parent.children++
225 ok = true
226 return s, nil
227}
228
229// Reposition asks the compositor to place the popup again. The following
230// Handler.Configure reports the resulting size. It needs xdg_wm_base version 3.
231func (s *Surface) Reposition(p PopupPlacement) error {
232 switch {
233 case s.closed || s.c.closed:
234 return ErrClosed
235 case s.role != rolePopup:
236 return errors.New("neferclient: Reposition needs a popup")
237 case s.c.g.xdgVersion < 3:
238 return &CapabilityError{Name: xdgshell.XdgWmBaseInterface, Cause: fmt.Errorf("reposition requires version 3, negotiated %d", s.c.g.xdgVersion)}
239 }
240 if err := validatePlacement(&p); err != nil {
241 return err
242 }
243 pos, err := s.c.newPositioner(&p)
244 if err != nil {
245 return err
246 }
247 s.repoToken++
248 return errors.Join(s.popup.Reposition(pos, s.repoToken), pos.Destroy())
249}
250
251// dropGrab ends the surface's grab: the topmost grabbing popup becomes its
252// parent, if that one still grabs.
253func (s *Surface) dropGrab() {
254 if !s.grabbed {
255 return
256 }
257 s.grabbed = false
258 if c := s.c; c.grabTop == s {
259 c.grabTop = nil
260 if p := s.parent; p != nil && p.grabbed {
261 c.grabTop = p
262 }
263 }
264}