detached 5fe516e

fix(shell): close popups topmost first and check popup grab serials

commit
5fe516ef79f422923dcaeaaa8abb26a8338583c9
author
brice · · 2026-09-25 14:28 UTC
parent
6bc6748
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
7 files · +148 -58
M internal/adapters/wayland/popup.go +12 -2
@@ -63,12 +63,14 @@ func (s *Server) newPopup(x *xdgSurface, r *xdgshell.Surface, id uint32, parentX
6363 s.emit(ports.PopupMapped{ID: w.id})
6464 s.log.Debug().Uint64("id", uint64(w.id)).Uint64("parent", uint64(parent.id)).Msg("popup mapped")
6565 case !buffer && w.mapped:
66+ // Unmapped, it may map again after a new initial commit.
6667 w.unmap()
68+ pp.sent, pp.grab = false, false
6769 }
6870 }
6971 res.OnDestroy = func() {
7072 // Core forgets the popup, placed or mapped.
71- if !w.mapped && pp.sent {
73+ if !w.mapped && pp.sent && !pp.done {
7274 s.emit(ports.WindowUnmapped{ID: w.id})
7375 }
7476 w.unmap()
@@ -87,11 +89,19 @@ func (*popup) Destroy(*xdgshell.Popup) {}
8789
8890 // Grab is only valid before the first commit; core then gives the popup the
8991 // keyboard and closes it on a click outside its popup chain.
90-func (p *popup) Grab(r *xdgshell.Popup, _ *wayland.Seat, _ uint32) {
92+func (p *popup) Grab(r *xdgshell.Popup, _ *wayland.Seat, serial uint32) {
9193 if p.sent || p.w.mapped {
9294 r.PostError(uint32(xdgshell.PopupErrorInvalidGrab), "grab after the popup was committed")
9395 return
9496 }
97+ // Only a grab answering the client's last press may take the keyboard,
98+ // and a popup over a popup grabs only if its parent does.
99+ s := p.w.xdg.server
100+ fresh := serial == s.press && s.pressClient != nil && *s.pressClient == r.Client()
101+ if !fresh || p.parent == nil || p.parent.popup != nil && !p.parent.popup.grab {
102+ p.dismiss()
103+ return
104+ }
95105 p.grab = true
96106 }
97107
@@ -63,12 +63,14 @@ func (s *Server) newPopup(x *xdgSurface, r *xdgshell.Surface, id uint32, parentX
63 s.emit(ports.PopupMapped{ID: w.id})63 s.emit(ports.PopupMapped{ID: w.id})
64 s.log.Debug().Uint64("id", uint64(w.id)).Uint64("parent", uint64(parent.id)).Msg("popup mapped")64 s.log.Debug().Uint64("id", uint64(w.id)).Uint64("parent", uint64(parent.id)).Msg("popup mapped")
65 case !buffer && w.mapped:65 case !buffer && w.mapped:
66+ // Unmapped, it may map again after a new initial commit.
66 w.unmap()67 w.unmap()
68+ pp.sent, pp.grab = false, false
67 }69 }
68 }70 }
69 res.OnDestroy = func() {71 res.OnDestroy = func() {
70 // Core forgets the popup, placed or mapped.72 // Core forgets the popup, placed or mapped.
71- if !w.mapped && pp.sent {73+ if !w.mapped && pp.sent && !pp.done {
72 s.emit(ports.WindowUnmapped{ID: w.id})74 s.emit(ports.WindowUnmapped{ID: w.id})
73 }75 }
74 w.unmap()76 w.unmap()
@@ -87,11 +89,19 @@ func (*popup) Destroy(*xdgshell.Popup) {}
87 89
88 // Grab is only valid before the first commit; core then gives the popup the90 // Grab is only valid before the first commit; core then gives the popup the
89 // keyboard and closes it on a click outside its popup chain.91 // keyboard and closes it on a click outside its popup chain.
90-func (p *popup) Grab(r *xdgshell.Popup, _ *wayland.Seat, _ uint32) {92+func (p *popup) Grab(r *xdgshell.Popup, _ *wayland.Seat, serial uint32) {
91 if p.sent || p.w.mapped {93 if p.sent || p.w.mapped {
92 r.PostError(uint32(xdgshell.PopupErrorInvalidGrab), "grab after the popup was committed")94 r.PostError(uint32(xdgshell.PopupErrorInvalidGrab), "grab after the popup was committed")
93 return95 return
94 }96 }
97+ // Only a grab answering the client's last press may take the keyboard,
98+ // and a popup over a popup grabs only if its parent does.
99+ s := p.w.xdg.server
100+ fresh := serial == s.press && s.pressClient != nil && *s.pressClient == r.Client()
101+ if !fresh || p.parent == nil || p.parent.popup != nil && !p.parent.popup.grab {
102+ p.dismiss()
103+ return
104+ }
95 p.grab = true105 p.grab = true
96 }106 }
97 107
M internal/adapters/wayland/server.go +28 -14
@@ -37,20 +37,24 @@ type Channels struct {
3737 Cursors chan<- ports.CursorChange
3838 }
3939 type Server struct {
40- display *server.Display
41- env procEnv
42- slotsPending bool // core waits for a slot window
43- name string
44- cleanup func()
45- log zerowrap.Logger
46- channels Channels
47- awaiting []*wayland.Callback
48- frames uint64
49- started time.Time
50- surfaces map[*server.Resource]*surface
51- buffers map[*server.Resource]clientBuffer
52- dmabuf *dmabufGlobal
53- serial uint32
40+ display *server.Display
41+ env procEnv
42+ slotsPending bool // core waits for a slot window
43+ name string
44+ cleanup func()
45+ log zerowrap.Logger
46+ channels Channels
47+ awaiting []*wayland.Callback
48+ frames uint64
49+ started time.Time
50+ surfaces map[*server.Resource]*surface
51+ buffers map[*server.Resource]clientBuffer
52+ dmabuf *dmabufGlobal
53+ serial uint32
54+ // press is the serial of the last button or key press, sent to
55+ // pressClient: popup grabs must come from it.
56+ press uint32
57+ pressClient *server.Client
5458 ctx context.Context
5559 windows map[ports.WindowID]*window
5660 layers map[ports.WindowID]*layerSurface
@@ -397,6 +401,10 @@ func (s *Server) apply(cmd ports.ClientCommand) {
397401 if c.Pressed {
398402 state = 1
399403 }
404+ if c.Pressed {
405+ client := w.xdg.resource.Client()
406+ s.press, s.pressClient = s.serial, &client
407+ }
400408 p.SendButton(s.serial, c.TimeMsec, c.Button, state)
401409 pointerFrame(p)
402410 }
@@ -426,6 +434,12 @@ func (s *Server) apply(cmd ports.ClientCommand) {
426434 if c.Key.Pressed {
427435 state = 1
428436 }
437+ if c.Key.Pressed {
438+ if surf, _ := s.focusTarget(c.ID); surf != nil {
439+ client := surf.Client()
440+ s.press, s.pressClient = s.serial, &client
441+ }
442+ }
429443 for _, k := range keyboards {
430444 k.SendKey(s.serial, c.Key.TimeMsec, c.Key.Keycode, state)
431445 }
@@ -37,20 +37,24 @@ type Channels struct {
37 Cursors chan<- ports.CursorChange37 Cursors chan<- ports.CursorChange
38 }38 }
39 type Server struct {39 type Server struct {
40- display *server.Display40+ display *server.Display
41- env procEnv41+ env procEnv
42- slotsPending bool // core waits for a slot window42+ slotsPending bool // core waits for a slot window
43- name string43+ name string
44- cleanup func()44+ cleanup func()
45- log zerowrap.Logger45+ log zerowrap.Logger
46- channels Channels46+ channels Channels
47- awaiting []*wayland.Callback47+ awaiting []*wayland.Callback
48- frames uint6448+ frames uint64
49- started time.Time49+ started time.Time
50- surfaces map[*server.Resource]*surface50+ surfaces map[*server.Resource]*surface
51- buffers map[*server.Resource]clientBuffer51+ buffers map[*server.Resource]clientBuffer
52- dmabuf *dmabufGlobal52+ dmabuf *dmabufGlobal
53- serial uint3253+ serial uint32
54+ // press is the serial of the last button or key press, sent to
55+ // pressClient: popup grabs must come from it.
56+ press uint32
57+ pressClient *server.Client
54 ctx context.Context58 ctx context.Context
55 windows map[ports.WindowID]*window59 windows map[ports.WindowID]*window
56 layers map[ports.WindowID]*layerSurface60 layers map[ports.WindowID]*layerSurface
@@ -397,6 +401,10 @@ func (s *Server) apply(cmd ports.ClientCommand) {
397 if c.Pressed {401 if c.Pressed {
398 state = 1402 state = 1
399 }403 }
404+ if c.Pressed {
405+ client := w.xdg.resource.Client()
406+ s.press, s.pressClient = s.serial, &client
407+ }
400 p.SendButton(s.serial, c.TimeMsec, c.Button, state)408 p.SendButton(s.serial, c.TimeMsec, c.Button, state)
401 pointerFrame(p)409 pointerFrame(p)
402 }410 }
@@ -426,6 +434,12 @@ func (s *Server) apply(cmd ports.ClientCommand) {
426 if c.Key.Pressed {434 if c.Key.Pressed {
427 state = 1435 state = 1
428 }436 }
437+ if c.Key.Pressed {
438+ if surf, _ := s.focusTarget(c.ID); surf != nil {
439+ client := surf.Client()
440+ s.press, s.pressClient = s.serial, &client
441+ }
442+ }
429 for _, k := range keyboards {443 for _, k := range keyboards {
430 k.SendKey(s.serial, c.Key.TimeMsec, c.Key.Keycode, state)444 k.SendKey(s.serial, c.Key.TimeMsec, c.Key.Keycode, state)
431 }445 }
M internal/core/core.go +2 -2
@@ -565,7 +565,7 @@ func (c *Core) Run(ctx context.Context) error {
565565 c.clients[v.ID] = v
566566 if v.Floating {
567567 if s, _ := c.screenOf(v.ID); s == nil {
568- c.cur().mon.Current().AddFloating(v.ID, v.Width, v.Height)
568+ c.cur().mon.AddFloating(v.ID, v.Width, v.Height)
569569 }
570570 } else if v.Slot == "" || (!c.placeSlotWindow(v.ID, v.Slot) && !c.placeTerminal(v.ID, v.Slot)) {
571571 if s, _ := c.screenOf(v.ID); s == nil {
@@ -595,7 +595,7 @@ func (c *Core) Run(ctx context.Context) error {
595595 return nil
596596 }
597597 }
598- if err := c.dropPopupsOf(ctx, v.ID); err != nil {
598+ if err := c.closePopupsOf(ctx, v.ID); err != nil {
599599 return nil
600600 }
601601 delete(c.clients, v.ID)
@@ -565,7 +565,7 @@ func (c *Core) Run(ctx context.Context) error {
565 c.clients[v.ID] = v565 c.clients[v.ID] = v
566 if v.Floating {566 if v.Floating {
567 if s, _ := c.screenOf(v.ID); s == nil {567 if s, _ := c.screenOf(v.ID); s == nil {
568- c.cur().mon.Current().AddFloating(v.ID, v.Width, v.Height)568+ c.cur().mon.AddFloating(v.ID, v.Width, v.Height)
569 }569 }
570 } else if v.Slot == "" || (!c.placeSlotWindow(v.ID, v.Slot) && !c.placeTerminal(v.ID, v.Slot)) {570 } else if v.Slot == "" || (!c.placeSlotWindow(v.ID, v.Slot) && !c.placeTerminal(v.ID, v.Slot)) {
571 if s, _ := c.screenOf(v.ID); s == nil {571 if s, _ := c.screenOf(v.ID); s == nil {
@@ -595,7 +595,7 @@ func (c *Core) Run(ctx context.Context) error {
595 return nil595 return nil
596 }596 }
597 }597 }
598- if err := c.dropPopupsOf(ctx, v.ID); err != nil {598+ if err := c.closePopupsOf(ctx, v.ID); err != nil {
599 return nil599 return nil
600 }600 }
601 delete(c.clients, v.ID)601 delete(c.clients, v.ID)
M internal/core/core_test.go +48 -0
@@ -641,3 +641,51 @@ func TestPointerConstraint(t *testing.T) {
641641 t.Fatalf("release %+v", got)
642642 }
643643 }
644+
645+// A click outside a grabbing menu chain closes it, the topmost popup first.
646+func TestPopupChainDismissOrder(t *testing.T) {
647+ cfg := config.Defaults()
648+ cfg.Border.Width = 0
649+ client := make(chan ports.ClientEvent, 8)
650+ input := make(chan ports.InputEvent, 8)
651+ output := make(chan ports.OutputEvent, 8)
652+ commands := make(chan ports.ClientCommand, 64)
653+ scenes := make(chan []ports.Scene, 1)
654+ c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes})
655+ if err != nil {
656+ t.Fatal(err)
657+ }
658+ ctx, cancel := context.WithCancel(context.Background())
659+ defer cancel()
660+ go c.Run(ctx)
661+ output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "OUT-1", Width: 100, Height: 80}}
662+ scene(t, scenes)
663+ client <- ports.WindowMapped{ID: 1}
664+ scene(t, scenes)
665+ pos := ports.Positioner{Width: 10, Height: 10, AnchorRect: ports.Rect{W: 1, H: 1}, Anchor: ports.EdgeBottomRight, Gravity: ports.EdgeBottomRight}
666+ for _, v := range []struct{ id, parent ports.WindowID }{{2, 1}, {3, 2}} {
667+ client <- ports.PopupRequest{ID: v.id, Parent: v.parent, Positioner: pos, Grab: true}
668+ for {
669+ if cp, ok := command(t, commands).(ports.ConfigurePopup); ok && cp.ID == v.id {
670+ break
671+ }
672+ }
673+ client <- ports.PopupMapped{ID: v.id}
674+ scene(t, scenes)
675+ }
676+ for len(commands) > 0 {
677+ <-commands
678+ }
679+ // The window's far corner is outside both popups.
680+ input <- ports.PointerMotion{X: 90, Y: 70}
681+ input <- ports.PointerButton{Button: 0x110, Pressed: true}
682+ var closed []ports.WindowID
683+ for len(closed) < 2 {
684+ if v, ok := command(t, commands).(ports.ClosePopup); ok {
685+ closed = append(closed, v.ID)
686+ }
687+ }
688+ if closed[0] != 3 || closed[1] != 2 {
689+ t.Fatalf("close order %v, want [3 2]", closed)
690+ }
691+}
@@ -641,3 +641,51 @@ func TestPointerConstraint(t *testing.T) {
641 t.Fatalf("release %+v", got)641 t.Fatalf("release %+v", got)
642 }642 }
643 }643 }
644+
645+// A click outside a grabbing menu chain closes it, the topmost popup first.
646+func TestPopupChainDismissOrder(t *testing.T) {
647+ cfg := config.Defaults()
648+ cfg.Border.Width = 0
649+ client := make(chan ports.ClientEvent, 8)
650+ input := make(chan ports.InputEvent, 8)
651+ output := make(chan ports.OutputEvent, 8)
652+ commands := make(chan ports.ClientCommand, 64)
653+ scenes := make(chan []ports.Scene, 1)
654+ c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes})
655+ if err != nil {
656+ t.Fatal(err)
657+ }
658+ ctx, cancel := context.WithCancel(context.Background())
659+ defer cancel()
660+ go c.Run(ctx)
661+ output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "OUT-1", Width: 100, Height: 80}}
662+ scene(t, scenes)
663+ client <- ports.WindowMapped{ID: 1}
664+ scene(t, scenes)
665+ pos := ports.Positioner{Width: 10, Height: 10, AnchorRect: ports.Rect{W: 1, H: 1}, Anchor: ports.EdgeBottomRight, Gravity: ports.EdgeBottomRight}
666+ for _, v := range []struct{ id, parent ports.WindowID }{{2, 1}, {3, 2}} {
667+ client <- ports.PopupRequest{ID: v.id, Parent: v.parent, Positioner: pos, Grab: true}
668+ for {
669+ if cp, ok := command(t, commands).(ports.ConfigurePopup); ok && cp.ID == v.id {
670+ break
671+ }
672+ }
673+ client <- ports.PopupMapped{ID: v.id}
674+ scene(t, scenes)
675+ }
676+ for len(commands) > 0 {
677+ <-commands
678+ }
679+ // The window's far corner is outside both popups.
680+ input <- ports.PointerMotion{X: 90, Y: 70}
681+ input <- ports.PointerButton{Button: 0x110, Pressed: true}
682+ var closed []ports.WindowID
683+ for len(closed) < 2 {
684+ if v, ok := command(t, commands).(ports.ClosePopup); ok {
685+ closed = append(closed, v.ID)
686+ }
687+ }
688+ if closed[0] != 3 || closed[1] != 2 {
689+ t.Fatalf("close order %v, want [3 2]", closed)
690+ }
691+}
M internal/core/layout.go +9 -4
@@ -195,7 +195,10 @@ func (w *Workspace) has(id WindowID) bool {
195195
196196 func (w *Workspace) Focused() (WindowID, bool) {
197197 if w.floatFocus && len(w.Floats) > 0 {
198- return w.Floats[len(w.Floats)-1].ID, true
198+ // A fullscreen column window hides the floats: it has the focus.
199+ if top := w.Floats[len(w.Floats)-1].ID; w.fullscreen == 0 || w.fullscreen == top {
200+ return top, true
201+ }
199202 }
200203 if w.Focus < 0 || w.Focus >= len(w.Columns) {
201204 return 0, false
@@ -593,6 +596,8 @@ func (w *Workspace) Layout() []Placement {
593596 var result []Placement
594597 gap := w.gap()
595598 cols := w.columnRects()
599+ focusedID, _ := w.Focused()
600+ floatFocused := w.floatIndex(focusedID) >= 0
596601 for i, c := range w.Columns {
597602 col := cols[i]
598603 n := len(c.Windows)
@@ -629,14 +634,14 @@ func (w *Workspace) Layout() []Placement {
629634 if hidden {
630635 r = Rect{}
631636 }
632- focused := !w.floatFocus && i == w.Focus && j == c.Focus
637+ focused := !floatFocused && i == w.Focus && j == c.Focus
633638 result = append(result, Placement{ID: id, Rect: r, Fullscreen: full, Focused: focused, Hidden: hidden, Borderless: col.W >= w.Usable.W-2*gap && (w.Overflow != OverflowFixed || len(w.Columns) == 1)})
634639 y += h + gap
635640 }
636641 }
637642 // Floating windows go last: they are drawn and hit on top.
638- for i, f := range w.Floats {
639- p := Placement{ID: f.ID, Rect: w.floatRect(f), Floating: true, Focused: w.floatFocus && i == len(w.Floats)-1}
643+ for _, f := range w.Floats {
644+ p := Placement{ID: f.ID, Rect: w.floatRect(f), Floating: true, Focused: floatFocused && f.ID == focusedID}
640645 if w.fullscreen == f.ID {
641646 p.Rect, p.Fullscreen = Rect{W: w.Output.W, H: w.Output.H}, true
642647 } else if w.fullscreen != 0 {
@@ -195,7 +195,10 @@ func (w *Workspace) has(id WindowID) bool {
195 195
196 func (w *Workspace) Focused() (WindowID, bool) {196 func (w *Workspace) Focused() (WindowID, bool) {
197 if w.floatFocus && len(w.Floats) > 0 {197 if w.floatFocus && len(w.Floats) > 0 {
198- return w.Floats[len(w.Floats)-1].ID, true198+ // A fullscreen column window hides the floats: it has the focus.
199+ if top := w.Floats[len(w.Floats)-1].ID; w.fullscreen == 0 || w.fullscreen == top {
200+ return top, true
201+ }
199 }202 }
200 if w.Focus < 0 || w.Focus >= len(w.Columns) {203 if w.Focus < 0 || w.Focus >= len(w.Columns) {
201 return 0, false204 return 0, false
@@ -593,6 +596,8 @@ func (w *Workspace) Layout() []Placement {
593 var result []Placement596 var result []Placement
594 gap := w.gap()597 gap := w.gap()
595 cols := w.columnRects()598 cols := w.columnRects()
599+ focusedID, _ := w.Focused()
600+ floatFocused := w.floatIndex(focusedID) >= 0
596 for i, c := range w.Columns {601 for i, c := range w.Columns {
597 col := cols[i]602 col := cols[i]
598 n := len(c.Windows)603 n := len(c.Windows)
@@ -629,14 +634,14 @@ func (w *Workspace) Layout() []Placement {
629 if hidden {634 if hidden {
630 r = Rect{}635 r = Rect{}
631 }636 }
632- focused := !w.floatFocus && i == w.Focus && j == c.Focus637+ focused := !floatFocused && i == w.Focus && j == c.Focus
633 result = append(result, Placement{ID: id, Rect: r, Fullscreen: full, Focused: focused, Hidden: hidden, Borderless: col.W >= w.Usable.W-2*gap && (w.Overflow != OverflowFixed || len(w.Columns) == 1)})638 result = append(result, Placement{ID: id, Rect: r, Fullscreen: full, Focused: focused, Hidden: hidden, Borderless: col.W >= w.Usable.W-2*gap && (w.Overflow != OverflowFixed || len(w.Columns) == 1)})
634 y += h + gap639 y += h + gap
635 }640 }
636 }641 }
637 // Floating windows go last: they are drawn and hit on top.642 // Floating windows go last: they are drawn and hit on top.
638- for i, f := range w.Floats {643+ for _, f := range w.Floats {
639- p := Placement{ID: f.ID, Rect: w.floatRect(f), Floating: true, Focused: w.floatFocus && i == len(w.Floats)-1}644+ p := Placement{ID: f.ID, Rect: w.floatRect(f), Floating: true, Focused: floatFocused && f.ID == focusedID}
640 if w.fullscreen == f.ID {645 if w.fullscreen == f.ID {
641 p.Rect, p.Fullscreen = Rect{W: w.Output.W, H: w.Output.H}, true646 p.Rect, p.Fullscreen = Rect{W: w.Output.W, H: w.Output.H}, true
642 } else if w.fullscreen != 0 {647 } else if w.fullscreen != 0 {
M internal/core/monitor.go +9 -0
@@ -231,6 +231,15 @@ func (m *Monitor) AddWindow(id WindowID) {
231231 m.normalize()
232232 }
233233
234+// AddFloating adds a floating window to the active workspace.
235+func (m *Monitor) AddFloating(id WindowID, width, height int) {
236+ if w, _ := m.find(id); w != nil {
237+ return
238+ }
239+ m.Current().AddFloating(id, width, height)
240+ m.normalize()
241+}
242+
234243 // RemoveWindow drops the window wherever it is; focus stays on the active workspace.
235244 func (m *Monitor) RemoveWindow(id WindowID) {
236245 if w, _ := m.find(id); w != nil {
@@ -231,6 +231,15 @@ func (m *Monitor) AddWindow(id WindowID) {
231 m.normalize()231 m.normalize()
232 }232 }
233 233
234+// AddFloating adds a floating window to the active workspace.
235+func (m *Monitor) AddFloating(id WindowID, width, height int) {
236+ if w, _ := m.find(id); w != nil {
237+ return
238+ }
239+ m.Current().AddFloating(id, width, height)
240+ m.normalize()
241+}
242+
234 // RemoveWindow drops the window wherever it is; focus stays on the active workspace.243 // RemoveWindow drops the window wherever it is; focus stays on the active workspace.
235 func (m *Monitor) RemoveWindow(id WindowID) {244 func (m *Monitor) RemoveWindow(id WindowID) {
236 if w, _ := m.find(id); w != nil {245 if w, _ := m.find(id); w != nil {
M internal/core/popup.go +40 -36
@@ -153,7 +153,9 @@ func (c *Core) windowRect(id WindowID) (*screen, Rect, bool) {
153153 func (c *Core) placePopup(ctx context.Context, v ports.PopupRequest) error {
154154 sc, pr, ok := c.windowRect(v.Parent)
155155 if !ok {
156- delete(c.popups, v.ID)
156+ if c.popups[v.ID] != nil {
157+ return c.closePopup(ctx, v.ID)
158+ }
157159 return c.command(ctx, ports.ClosePopup{ID: v.ID})
158160 }
159161 o := sc.mon.Output()
@@ -169,37 +171,25 @@ func (c *Core) placePopup(ctx context.Context, v ports.PopupRequest) error {
169171 return c.command(ctx, ports.ConfigurePopup{ID: v.ID, Rect: p.rect, Reposition: v.Reposition, Token: v.Token})
170172 }
171173
172-// dropPopup forgets a popup and dismisses its children.
173-func (c *Core) dropPopup(ctx context.Context, id WindowID) error {
174- if c.popups[id] == nil {
175- return nil
176- }
177- delete(c.popups, id)
178- c.popupOrder = slices.DeleteFunc(c.popupOrder, func(v WindowID) bool { return v == id })
179- for _, child := range slices.Clone(c.popupOrder) {
180- if p := c.popups[child]; p != nil && p.parent == id {
181- if err := c.command(ctx, ports.ClosePopup{ID: child}); err != nil {
182- return err
183- }
184- if err := c.dropPopup(ctx, child); err != nil {
185- return err
186- }
187- }
174+// closePopup dismisses a popup after its children, topmost first as
175+// xdg-shell requires, and forgets them.
176+func (c *Core) closePopup(ctx context.Context, id WindowID) error {
177+ if err := c.closePopupsOf(ctx, id); err != nil {
178+ return err
188179 }
189- if c.pointer == id {
190- c.pointer = 0
180+ if err := c.command(ctx, ports.ClosePopup{ID: id}); err != nil {
181+ return err
191182 }
183+ c.forgetPopup(id)
192184 return nil
193185 }
194186
195-// dropPopupsOf dismisses the popups hanging from a window.
196-func (c *Core) dropPopupsOf(ctx context.Context, id WindowID) error {
197- for _, child := range slices.Clone(c.popupOrder) {
187+// closePopupsOf dismisses the popups hanging from a window or popup,
188+// newest first.
189+func (c *Core) closePopupsOf(ctx context.Context, id WindowID) error {
190+ for _, child := range slices.Backward(slices.Clone(c.popupOrder)) {
198191 if p := c.popups[child]; p != nil && p.parent == id {
199- if err := c.command(ctx, ports.ClosePopup{ID: child}); err != nil {
200- return err
201- }
202- if err := c.dropPopup(ctx, child); err != nil {
192+ if err := c.closePopup(ctx, child); err != nil {
203193 return err
204194 }
205195 }
@@ -207,17 +197,34 @@ func (c *Core) dropPopupsOf(ctx context.Context, id WindowID) error {
207197 return nil
208198 }
209199
200+// dropPopup forgets a popup the client unmapped and dismisses its children.
201+func (c *Core) dropPopup(ctx context.Context, id WindowID) error {
202+ if c.popups[id] == nil {
203+ return nil
204+ }
205+ if err := c.closePopupsOf(ctx, id); err != nil {
206+ return err
207+ }
208+ c.forgetPopup(id)
209+ return nil
210+}
211+
212+func (c *Core) forgetPopup(id WindowID) {
213+ delete(c.popups, id)
214+ c.popupOrder = slices.DeleteFunc(c.popupOrder, func(v WindowID) bool { return v == id })
215+ if c.pointer == id {
216+ c.pointer = 0
217+ }
218+}
219+
210220 // closeHiddenPopups dismisses popups whose window left the screen.
211221 func (c *Core) closeHiddenPopups(ctx context.Context) error {
212- for _, id := range slices.Clone(c.popupOrder) {
222+ for _, id := range slices.Backward(slices.Clone(c.popupOrder)) {
213223 if c.popups[id] == nil {
214224 continue
215225 }
216226 if _, _, ok := c.windowRect(id); !ok {
217- if err := c.command(ctx, ports.ClosePopup{ID: id}); err != nil {
218- return err
219- }
220- if err := c.dropPopup(ctx, id); err != nil {
227+ if err := c.closePopup(ctx, id); err != nil {
221228 return err
222229 }
223230 }
@@ -228,7 +235,7 @@ func (c *Core) closeHiddenPopups(ctx context.Context) error {
228235 // dismissGrabs closes the grabbing popups unless the click hit one of
229236 // them: a click outside a menu closes it.
230237 func (c *Core) dismissGrabs(ctx context.Context, hit WindowID) error {
231- for _, id := range slices.Clone(c.popupOrder) {
238+ for _, id := range slices.Backward(slices.Clone(c.popupOrder)) {
232239 p := c.popups[id]
233240 if p == nil || !p.grab {
234241 continue
@@ -238,10 +245,7 @@ func (c *Core) dismissGrabs(ctx context.Context, hit WindowID) error {
238245 if c.popups[hit] != nil && (hit == id || c.isAncestor(id, hit) || c.isAncestor(hit, id)) {
239246 continue
240247 }
241- if err := c.command(ctx, ports.ClosePopup{ID: id}); err != nil {
242- return err
243- }
244- if err := c.dropPopup(ctx, id); err != nil {
248+ if err := c.closePopup(ctx, id); err != nil {
245249 return err
246250 }
247251 }
@@ -153,7 +153,9 @@ func (c *Core) windowRect(id WindowID) (*screen, Rect, bool) {
153 func (c *Core) placePopup(ctx context.Context, v ports.PopupRequest) error {153 func (c *Core) placePopup(ctx context.Context, v ports.PopupRequest) error {
154 sc, pr, ok := c.windowRect(v.Parent)154 sc, pr, ok := c.windowRect(v.Parent)
155 if !ok {155 if !ok {
156- delete(c.popups, v.ID)156+ if c.popups[v.ID] != nil {
157+ return c.closePopup(ctx, v.ID)
158+ }
157 return c.command(ctx, ports.ClosePopup{ID: v.ID})159 return c.command(ctx, ports.ClosePopup{ID: v.ID})
158 }160 }
159 o := sc.mon.Output()161 o := sc.mon.Output()
@@ -169,37 +171,25 @@ func (c *Core) placePopup(ctx context.Context, v ports.PopupRequest) error {
169 return c.command(ctx, ports.ConfigurePopup{ID: v.ID, Rect: p.rect, Reposition: v.Reposition, Token: v.Token})171 return c.command(ctx, ports.ConfigurePopup{ID: v.ID, Rect: p.rect, Reposition: v.Reposition, Token: v.Token})
170 }172 }
171 173
172-// dropPopup forgets a popup and dismisses its children.174+// closePopup dismisses a popup after its children, topmost first as
173-func (c *Core) dropPopup(ctx context.Context, id WindowID) error {175+// xdg-shell requires, and forgets them.
174- if c.popups[id] == nil {176+func (c *Core) closePopup(ctx context.Context, id WindowID) error {
175- return nil177+ if err := c.closePopupsOf(ctx, id); err != nil {
176- }178+ return err
177- delete(c.popups, id)
178- c.popupOrder = slices.DeleteFunc(c.popupOrder, func(v WindowID) bool { return v == id })
179- for _, child := range slices.Clone(c.popupOrder) {
180- if p := c.popups[child]; p != nil && p.parent == id {
181- if err := c.command(ctx, ports.ClosePopup{ID: child}); err != nil {
182- return err
183- }
184- if err := c.dropPopup(ctx, child); err != nil {
185- return err
186- }
187- }
188 }179 }
189- if c.pointer == id {180+ if err := c.command(ctx, ports.ClosePopup{ID: id}); err != nil {
190- c.pointer = 0181+ return err
191 }182 }
183+ c.forgetPopup(id)
192 return nil184 return nil
193 }185 }
194 186
195-// dropPopupsOf dismisses the popups hanging from a window.187+// closePopupsOf dismisses the popups hanging from a window or popup,
196-func (c *Core) dropPopupsOf(ctx context.Context, id WindowID) error {188+// newest first.
197- for _, child := range slices.Clone(c.popupOrder) {189+func (c *Core) closePopupsOf(ctx context.Context, id WindowID) error {
190+ for _, child := range slices.Backward(slices.Clone(c.popupOrder)) {
198 if p := c.popups[child]; p != nil && p.parent == id {191 if p := c.popups[child]; p != nil && p.parent == id {
199- if err := c.command(ctx, ports.ClosePopup{ID: child}); err != nil {192+ if err := c.closePopup(ctx, child); err != nil {
200- return err
201- }
202- if err := c.dropPopup(ctx, child); err != nil {
203 return err193 return err
204 }194 }
205 }195 }
@@ -207,17 +197,34 @@ func (c *Core) dropPopupsOf(ctx context.Context, id WindowID) error {
207 return nil197 return nil
208 }198 }
209 199
200+// dropPopup forgets a popup the client unmapped and dismisses its children.
201+func (c *Core) dropPopup(ctx context.Context, id WindowID) error {
202+ if c.popups[id] == nil {
203+ return nil
204+ }
205+ if err := c.closePopupsOf(ctx, id); err != nil {
206+ return err
207+ }
208+ c.forgetPopup(id)
209+ return nil
210+}
211+
212+func (c *Core) forgetPopup(id WindowID) {
213+ delete(c.popups, id)
214+ c.popupOrder = slices.DeleteFunc(c.popupOrder, func(v WindowID) bool { return v == id })
215+ if c.pointer == id {
216+ c.pointer = 0
217+ }
218+}
219+
210 // closeHiddenPopups dismisses popups whose window left the screen.220 // closeHiddenPopups dismisses popups whose window left the screen.
211 func (c *Core) closeHiddenPopups(ctx context.Context) error {221 func (c *Core) closeHiddenPopups(ctx context.Context) error {
212- for _, id := range slices.Clone(c.popupOrder) {222+ for _, id := range slices.Backward(slices.Clone(c.popupOrder)) {
213 if c.popups[id] == nil {223 if c.popups[id] == nil {
214 continue224 continue
215 }225 }
216 if _, _, ok := c.windowRect(id); !ok {226 if _, _, ok := c.windowRect(id); !ok {
217- if err := c.command(ctx, ports.ClosePopup{ID: id}); err != nil {227+ if err := c.closePopup(ctx, id); err != nil {
218- return err
219- }
220- if err := c.dropPopup(ctx, id); err != nil {
221 return err228 return err
222 }229 }
223 }230 }
@@ -228,7 +235,7 @@ func (c *Core) closeHiddenPopups(ctx context.Context) error {
228 // dismissGrabs closes the grabbing popups unless the click hit one of235 // dismissGrabs closes the grabbing popups unless the click hit one of
229 // them: a click outside a menu closes it.236 // them: a click outside a menu closes it.
230 func (c *Core) dismissGrabs(ctx context.Context, hit WindowID) error {237 func (c *Core) dismissGrabs(ctx context.Context, hit WindowID) error {
231- for _, id := range slices.Clone(c.popupOrder) {238+ for _, id := range slices.Backward(slices.Clone(c.popupOrder)) {
232 p := c.popups[id]239 p := c.popups[id]
233 if p == nil || !p.grab {240 if p == nil || !p.grab {
234 continue241 continue
@@ -238,10 +245,7 @@ func (c *Core) dismissGrabs(ctx context.Context, hit WindowID) error {
238 if c.popups[hit] != nil && (hit == id || c.isAncestor(id, hit) || c.isAncestor(hit, id)) {245 if c.popups[hit] != nil && (hit == id || c.isAncestor(id, hit) || c.isAncestor(hit, id)) {
239 continue246 continue
240 }247 }
241- if err := c.command(ctx, ports.ClosePopup{ID: id}); err != nil {248+ if err := c.closePopup(ctx, id); err != nil {
242- return err
243- }
244- if err := c.dropPopup(ctx, id); err != nil {
245 return err249 return err
246 }250 }
247 }251 }

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 26

Tags 10

↑↓ moveenter openesc close