detached 5f1ad00

popup.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 4

↑↓ moveenter openesc close