main f2d166b

popup.go RawHistoryFolder

1package core
2
3import (
4	"context"
5	"slices"
6
7	"github.com/bnema/neferwl/internal/ports"
8)
9
10// popupState is a placed xdg_popup. Rect is relative to the parent's
11// window geometry, as sent to the client.
12type popupState struct {
13	id, parent WindowID
14	rect       Rect
15	grab       bool
16	mapped     bool
17}
18
19// place computes a popup rectangle relative to its parent, kept inside
20// bounds (also parent-relative) as the positioner's adjustments allow.
21func place(p ports.Positioner, bounds Rect) Rect {
22	r := initial(p, p.Anchor, p.Gravity)
23	if bounds.W <= 0 || bounds.H <= 0 {
24		return r
25	}
26	outX := func(r Rect) bool { return r.X < bounds.X || r.X+r.W > bounds.X+bounds.W }
27	outY := func(r Rect) bool { return r.Y < bounds.Y || r.Y+r.H > bounds.Y+bounds.H }
28	if outX(r) && p.Adjust&ports.AdjustFlipX != 0 {
29		if f := initial(p, flipX(p.Anchor), flipX(p.Gravity)); !outX(f) {
30			r.X = f.X
31		}
32	}
33	if outY(r) && p.Adjust&ports.AdjustFlipY != 0 {
34		if f := initial(p, flipY(p.Anchor), flipY(p.Gravity)); !outY(f) {
35			r.Y = f.Y
36		}
37	}
38	if outX(r) && p.Adjust&ports.AdjustSlideX != 0 {
39		r.X = max(min(r.X, bounds.X+bounds.W-r.W), bounds.X)
40	}
41	if outY(r) && p.Adjust&ports.AdjustSlideY != 0 {
42		r.Y = max(min(r.Y, bounds.Y+bounds.H-r.H), bounds.Y)
43	}
44	if outX(r) && p.Adjust&ports.AdjustResizeX != 0 {
45		x0, x1 := max(r.X, bounds.X), min(r.X+r.W, bounds.X+bounds.W)
46		if x1 > x0 {
47			r.X, r.W = x0, x1-x0
48		}
49	}
50	if outY(r) && p.Adjust&ports.AdjustResizeY != 0 {
51		y0, y1 := max(r.Y, bounds.Y), min(r.Y+r.H, bounds.Y+bounds.H)
52		if y1 > y0 {
53			r.Y, r.H = y0, y1-y0
54		}
55	}
56	return r
57}
58
59// initial is the unconstrained placement for an anchor and gravity.
60func initial(p ports.Positioner, anchor, gravity uint32) Rect {
61	a := p.AnchorRect
62	x, y := a.X+a.W/2, a.Y+a.H/2
63	switch anchor {
64	case ports.EdgeLeft, ports.EdgeTopLeft, ports.EdgeBottomLeft:
65		x = a.X
66	case ports.EdgeRight, ports.EdgeTopRight, ports.EdgeBottomRight:
67		x = a.X + a.W
68	}
69	switch anchor {
70	case ports.EdgeTop, ports.EdgeTopLeft, ports.EdgeTopRight:
71		y = a.Y
72	case ports.EdgeBottom, ports.EdgeBottomLeft, ports.EdgeBottomRight:
73		y = a.Y + a.H
74	}
75	x, y = x+p.OffsetX, y+p.OffsetY
76	r := Rect{X: x - p.Width/2, Y: y - p.Height/2, W: p.Width, H: p.Height}
77	switch gravity {
78	case ports.EdgeLeft, ports.EdgeTopLeft, ports.EdgeBottomLeft:
79		r.X = x - p.Width
80	case ports.EdgeRight, ports.EdgeTopRight, ports.EdgeBottomRight:
81		r.X = x
82	}
83	switch gravity {
84	case ports.EdgeTop, ports.EdgeTopLeft, ports.EdgeTopRight:
85		r.Y = y - p.Height
86	case ports.EdgeBottom, ports.EdgeBottomLeft, ports.EdgeBottomRight:
87		r.Y = y
88	}
89	return r
90}
91
92func flipX(e uint32) uint32 {
93	switch e {
94	case ports.EdgeLeft:
95		return ports.EdgeRight
96	case ports.EdgeRight:
97		return ports.EdgeLeft
98	case ports.EdgeTopLeft:
99		return ports.EdgeTopRight
100	case ports.EdgeTopRight:
101		return ports.EdgeTopLeft
102	case ports.EdgeBottomLeft:
103		return ports.EdgeBottomRight
104	case ports.EdgeBottomRight:
105		return ports.EdgeBottomLeft
106	}
107	return e
108}
109
110func flipY(e uint32) uint32 {
111	switch e {
112	case ports.EdgeTop:
113		return ports.EdgeBottom
114	case ports.EdgeBottom:
115		return ports.EdgeTop
116	case ports.EdgeTopLeft:
117		return ports.EdgeBottomLeft
118	case ports.EdgeBottomLeft:
119		return ports.EdgeTopLeft
120	case ports.EdgeTopRight:
121		return ports.EdgeBottomRight
122	case ports.EdgeBottomRight:
123		return ports.EdgeTopRight
124	}
125	return e
126}
127
128// windowRect is where a window, layer surface or popup is on its screen: its client
129// area, output-local and logical. ok is false when it is not on screen.
130func (c *Core) windowRect(id WindowID) (*screen, Rect, bool) {
131	if p := c.popups[id]; p != nil {
132		sc, pr, ok := c.windowRect(p.parent)
133		// A peeking stashed window shows no popups: they would cover the
134		// selected one.
135		if !ok || c.peeking(p.parent) {
136			return nil, Rect{}, false
137		}
138		return sc, Rect{X: pr.X + p.rect.X, Y: pr.Y + p.rect.Y, W: p.rect.W, H: p.rect.H}, true
139	}
140	if sc, l, ok := c.layerOf(id); ok {
141		if !shown(sc, l) {
142			return nil, Rect{}, false
143		}
144		return sc, l.Rect, true
145	}
146	for _, sc := range c.screens {
147		for _, pl := range sc.shownLayout() {
148			if pl.ID == id {
149				// Hidden, scrolled off or under a panel: not on screen,
150				// like its popups. A preview takes no input and shows no
151				// popups.
152				if !sc.mon.viewport().shows(pl) || pl.Preview > 0 {
153					return nil, Rect{}, false
154				}
155				return sc, c.clientRect(pl), true
156			}
157		}
158	}
159	return nil, Rect{}, false
160}
161
162// peeking reports whether id is a stashed window peeking in on screen.
163func (c *Core) peeking(id WindowID) bool {
164	for _, sc := range c.screens {
165		for _, pl := range sc.shownLayout() {
166			if pl.ID == id {
167				return pl.Peek
168			}
169		}
170	}
171	return false
172}
173
174// placePopup places a new or repositioned popup and configures it; a popup
175// whose parent is not on screen is dismissed.
176func (c *Core) placePopup(ctx context.Context, v ports.PopupRequest) error {
177	sc, pr, physical, ok := c.popupRect(v.Parent)
178	if !ok {
179		if c.popups[v.ID] != nil {
180			return c.closePopup(ctx, v.ID)
181		}
182		return c.command(ctx, ports.ClosePopup{ID: v.ID})
183	}
184	// Window popups stay in the workspace viewport, layer popups in the output.
185	o := sc.mon.Output()
186	if !c.onLayer(c.popupRoot(v.Parent)) {
187		o = sc.mon.Frame()
188		if !physical {
189			o = c.configures.cw.frame // the captured workspace's viewport
190		}
191	}
192	bounds := Rect{X: o.X - pr.X, Y: o.Y - pr.Y, W: o.W, H: o.H}
193	p := c.popups[v.ID]
194	if p == nil {
195		p = &popupState{id: v.ID, parent: v.Parent}
196		c.popups[v.ID] = p
197		c.popupOrder = append(c.popupOrder, v.ID)
198	}
199	p.grab = v.Grab
200	p.rect = place(v.Positioner, bounds)
201	return c.command(ctx, ports.ConfigurePopup{ID: v.ID, Rect: p.rect, Reposition: v.Reposition, Token: v.Token})
202}
203
204// closePopup dismisses a popup after its children, topmost first as
205// xdg-shell requires, and forgets them.
206func (c *Core) closePopup(ctx context.Context, id WindowID) error {
207	if err := c.closePopupsOf(ctx, id); err != nil {
208		return err
209	}
210	if err := c.command(ctx, ports.ClosePopup{ID: id}); err != nil {
211		return err
212	}
213	c.forgetPopup(id)
214	return nil
215}
216
217// closePopupsOf dismisses the popups hanging from a window or popup,
218// newest first.
219func (c *Core) closePopupsOf(ctx context.Context, id WindowID) error {
220	for _, child := range slices.Backward(slices.Clone(c.popupOrder)) {
221		if p := c.popups[child]; p != nil && p.parent == id {
222			if err := c.closePopup(ctx, child); err != nil {
223				return err
224			}
225		}
226	}
227	return nil
228}
229
230// dropPopup forgets a popup the client unmapped and dismisses its children.
231func (c *Core) dropPopup(ctx context.Context, id WindowID) error {
232	if c.popups[id] == nil {
233		return nil
234	}
235	if err := c.closePopupsOf(ctx, id); err != nil {
236		return err
237	}
238	c.forgetPopup(id)
239	return nil
240}
241
242func (c *Core) forgetPopup(id WindowID) {
243	delete(c.popups, id)
244	c.popupOrder = slices.DeleteFunc(c.popupOrder, func(v WindowID) bool { return v == id })
245	if c.pointer == id {
246		c.pointer = 0
247	}
248}
249
250// closeHiddenPopups dismisses popups whose window left the screen.
251func (c *Core) closeHiddenPopups(ctx context.Context) error {
252	for _, id := range slices.Backward(slices.Clone(c.popupOrder)) {
253		if c.popups[id] == nil {
254			continue
255		}
256		// A popup of a window a capture session renders off screen stays.
257		if _, _, _, ok := c.popupRect(id); !ok {
258			if err := c.closePopup(ctx, id); err != nil {
259				return err
260			}
261		}
262	}
263	return nil
264}
265
266// dismissGrabs closes the grabbing popups unless the click hit one of
267// them: a click outside a menu closes it.
268func (c *Core) dismissGrabs(ctx context.Context, hit WindowID) error {
269	for _, id := range slices.Backward(slices.Clone(c.popupOrder)) {
270		p := c.popups[id]
271		if p == nil || !p.grab {
272			continue
273		}
274		// A click on a popup of the same chain keeps it; a click on
275		// anything else, its parent window included, closes it.
276		if c.popups[hit] != nil && (hit == id || c.isAncestor(id, hit) || c.isAncestor(hit, id)) {
277			continue
278		}
279		if err := c.closePopup(ctx, id); err != nil {
280			return err
281		}
282	}
283	return nil
284}
285
286// isAncestor reports whether a is a popup ancestor of b.
287func (c *Core) isAncestor(a, b WindowID) bool {
288	for p := c.popups[b]; p != nil; p = c.popups[p.parent] {
289		if p.parent == a {
290			return true
291		}
292	}
293	return false
294}
295
296// grabFocus is the topmost mapped grabbing popup, which takes the keyboard.
297func (c *Core) grabFocus() WindowID {
298	for i := len(c.popupOrder) - 1; i >= 0; i-- {
299		// A popup hidden with its parent (under a fullscreen window) has
300		// no keyboard.
301		if p := c.popups[c.popupOrder[i]]; p != nil && p.grab && p.mapped && c.visible(p.id) {
302			return p.id
303		}
304	}
305	return 0
306}
307
308// popupAt returns the topmost mapped popup under the output-local point,
309// among layer popups (overLayers) or window popups.
310func (c *Core) popupAt(sc *screen, lx, ly float64, overLayers bool) (WindowID, float64, float64) {
311	for i := len(c.popupOrder) - 1; i >= 0; i-- {
312		p := c.popups[c.popupOrder[i]]
313		if p == nil || !p.mapped || c.onLayer(c.popupRoot(p.id)) != overLayers {
314			continue
315		}
316		s, r, ok := c.windowRect(p.id)
317		if !ok || s != sc {
318			continue
319		}
320		if !overLayers && !sc.mon.frameHas(lx, ly) {
321			continue
322		}
323		if lx >= float64(r.X) && lx < float64(r.X+r.W) && ly >= float64(r.Y) && ly < float64(r.Y+r.H) && c.acceptsInput(p.id, lx-float64(r.X), ly-float64(r.Y)) {
324			return p.id, lx - float64(r.X), ly - float64(r.Y)
325		}
326	}
327	return 0, 0, 0
328}
329
330// popupRoot is the window or layer a popup chain hangs from; id itself
331// when it is not a popup.
332func (c *Core) popupRoot(id WindowID) WindowID {
333	for p := c.popups[id]; p != nil; p = c.popups[id] {
334		id = p.parent
335	}
336	return id
337}
338
339// onLayer reports whether id is a mapped layer surface.
340func (c *Core) onLayer(id WindowID) bool {
341	for _, sc := range c.screens {
342		for _, l := range sc.layers {
343			if l.ID == id {
344				return true
345			}
346		}
347	}
348	return false
349}
350
351// scenePopups are the mapped popups on a screen, bottom to top.
352func (c *Core) scenePopups(sc *screen) []ports.SceneWindow {
353	var out []ports.SceneWindow
354	for _, id := range c.popupOrder {
355		p := c.popups[id]
356		if p == nil || !p.mapped {
357			continue
358		}
359		if s, r, ok := c.windowRect(id); ok && s == sc {
360			out = append(out, ports.SceneWindow{ID: id, Rect: r, Popup: true, OverLayers: c.onLayer(c.popupRoot(id))})
361		}
362	}
363	return out
364}

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 25

Tags 10

↑↓ moveenter openesc close