detached e2fd4a0

fix(layer): on-demand focus ends on any focus change, window popups stay under top layers

commit
e2fd4a06f00cf0e10dfe49f02727eaf63ed78377
author
brice · · 2026-09-26 05:59 UTC
committer
brice · · 2026-09-26 06:05 UTC
parent
2273fad
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
5 files · +127 -23
M internal/adapters/vulkan/renderer.go +11 -8
@@ -548,16 +548,19 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
548548 }
549549 }
550550 }
551- addLayers(true)
552- // Popups go over everything, layers included: a bar's menu shows over
553- // the bar and the windows.
554- for _, w := range s.Windows {
555- if w.Popup && !w.Hidden && w.Rect.W > 0 && w.Rect.H > 0 {
556- // Menus draw only what the client drew, shadows clipped.
557- content := contents[w.ID]
558- place(&content, w.Rect.X, w.Rect.Y, w.Rect.W, w.Rect.H)
551+ // Popups draw only what the client drew, shadows clipped. Window popups
552+ // stay with the windows; a layer's go over every layer.
553+ addPopups := func(overLayers bool) {
554+ for _, w := range s.Windows {
555+ if w.Popup && w.OverLayers == overLayers && !w.Hidden && w.Rect.W > 0 && w.Rect.H > 0 {
556+ content := contents[w.ID]
557+ place(&content, w.Rect.X, w.Rect.Y, w.Rect.W, w.Rect.H)
558+ }
559559 }
560560 }
561+ addPopups(false)
562+ addLayers(true)
563+ addPopups(true)
561564 if err := r.ensureStaging(used); err != nil {
562565 return err
563566 }
@@ -548,16 +548,19 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
548 }548 }
549 }549 }
550 }550 }
551- addLayers(true)551+ // Popups draw only what the client drew, shadows clipped. Window popups
552- // Popups go over everything, layers included: a bar's menu shows over552+ // stay with the windows; a layer's go over every layer.
553- // the bar and the windows.553+ addPopups := func(overLayers bool) {
554- for _, w := range s.Windows {554+ for _, w := range s.Windows {
555- if w.Popup && !w.Hidden && w.Rect.W > 0 && w.Rect.H > 0 {555+ if w.Popup && w.OverLayers == overLayers && !w.Hidden && w.Rect.W > 0 && w.Rect.H > 0 {
556- // Menus draw only what the client drew, shadows clipped.556+ content := contents[w.ID]
557- content := contents[w.ID]557+ place(&content, w.Rect.X, w.Rect.Y, w.Rect.W, w.Rect.H)
558- place(&content, w.Rect.X, w.Rect.Y, w.Rect.W, w.Rect.H)558+ }
559 }559 }
560 }560 }
561+ addPopups(false)
562+ addLayers(true)
563+ addPopups(true)
561 if err := r.ensureStaging(used); err != nil {564 if err := r.ensureStaging(used); err != nil {
562 return err565 return err
563 }566 }
M internal/core/core.go +23 -10
@@ -82,8 +82,11 @@ type Core struct {
8282 popups map[WindowID]*popupState
8383 popupOrder []WindowID
8484 // layerFocus is the on-demand layer surface the user clicked; it keeps
85- // the keyboard until a window is clicked or the layer unmaps.
85+ // the keyboard while it stays mapped on-demand and the window focus
86+ // (layerOver) does not change: any click elsewhere, bind, activation or
87+ // new window takes the keyboard back.
8688 layerFocus WindowID
89+ layerOver WindowID
8790 }
8891
8992 func keyName(s string) string {
@@ -319,10 +322,13 @@ func (c *Core) keyboardFocus() WindowID {
319322 if g := c.grabFocus(); g != 0 {
320323 return g
321324 }
322- if c.layerFocus != 0 && c.onDemand(c.layerFocus) {
325+ id, _ := c.cur().mon.Focused()
326+ if c.layerFocus != 0 && (!c.onDemand(c.layerFocus) || id != c.layerOver) {
327+ c.layerFocus = 0
328+ }
329+ if c.layerFocus != 0 {
323330 return c.layerFocus
324331 }
325- id, _ := c.cur().mon.Focused()
326332 return id
327333 }
328334
@@ -515,13 +521,17 @@ func (c *Core) hit(x, y float64) (WindowID, float64, float64) {
515521 }
516522 lx, ly := x-float64(o.X), y-float64(o.Y)
517523 sc := c.screens[c.screenIndex(o.Info.Name)]
518- // Popups are above everything but fullscreen covers nothing of them.
519- if id, px, py := c.popupAt(sc, lx, ly); id != 0 {
524+ // Layer popups are over everything; window popups are over the windows
525+ // only, under the top and overlay layers (as drawn).
526+ if id, px, py := c.popupAt(sc, lx, ly, true); id != 0 {
520527 return id, px, py
521528 }
522529 if id, px, py := layerAt(sc, lx, ly, true); id != 0 {
523530 return id, px, py
524531 }
532+ if id, px, py := c.popupAt(sc, lx, ly, false); id != 0 {
533+ return id, px, py
534+ }
525535 var id WindowID
526536 var sx, sy float64
527537 full := false
@@ -737,16 +747,19 @@ func (c *Core) Run(ctx context.Context) error {
737747 // A click on an on-demand layer gives it the keyboard.
738748 if v.Pressed && c.onDemand(id) && c.layerFocus != id {
739749 c.layerFocus = id
750+ c.layerOver, _ = c.cur().mon.Focused()
740751 if err := c.publish(ctx); err != nil {
741752 return nil
742753 }
743- }
744- // A click focuses the window and its output, taking the
745- // keyboard back from a layer.
746- s, w := c.screenOf(id)
747- if v.Pressed && s != nil {
754+ } else if v.Pressed && c.layerFocus != 0 && id != c.layerFocus && c.popupRoot(id) != c.layerFocus {
755+ // A click anywhere else takes the keyboard back.
748756 c.layerFocus = 0
757+ if err := c.publish(ctx); err != nil {
758+ return nil
759+ }
749760 }
761+ // A click focuses the window and its output.
762+ s, w := c.screenOf(id)
750763 if v.Pressed && s != nil && w == s.mon.Current() && (c.focus != id || s != c.cur()) {
751764 w.FocusID(id)
752765 c.focusScreen = c.screenIndex(s.name())
@@ -82,8 +82,11 @@ type Core struct {
82 popups map[WindowID]*popupState82 popups map[WindowID]*popupState
83 popupOrder []WindowID83 popupOrder []WindowID
84 // layerFocus is the on-demand layer surface the user clicked; it keeps84 // layerFocus is the on-demand layer surface the user clicked; it keeps
85- // the keyboard until a window is clicked or the layer unmaps.85+ // the keyboard while it stays mapped on-demand and the window focus
86+ // (layerOver) does not change: any click elsewhere, bind, activation or
87+ // new window takes the keyboard back.
86 layerFocus WindowID88 layerFocus WindowID
89+ layerOver WindowID
87 }90 }
88 91
89 func keyName(s string) string {92 func keyName(s string) string {
@@ -319,10 +322,13 @@ func (c *Core) keyboardFocus() WindowID {
319 if g := c.grabFocus(); g != 0 {322 if g := c.grabFocus(); g != 0 {
320 return g323 return g
321 }324 }
322- if c.layerFocus != 0 && c.onDemand(c.layerFocus) {325+ id, _ := c.cur().mon.Focused()
326+ if c.layerFocus != 0 && (!c.onDemand(c.layerFocus) || id != c.layerOver) {
327+ c.layerFocus = 0
328+ }
329+ if c.layerFocus != 0 {
323 return c.layerFocus330 return c.layerFocus
324 }331 }
325- id, _ := c.cur().mon.Focused()
326 return id332 return id
327 }333 }
328 334
@@ -515,13 +521,17 @@ func (c *Core) hit(x, y float64) (WindowID, float64, float64) {
515 }521 }
516 lx, ly := x-float64(o.X), y-float64(o.Y)522 lx, ly := x-float64(o.X), y-float64(o.Y)
517 sc := c.screens[c.screenIndex(o.Info.Name)]523 sc := c.screens[c.screenIndex(o.Info.Name)]
518- // Popups are above everything but fullscreen covers nothing of them.524+ // Layer popups are over everything; window popups are over the windows
519- if id, px, py := c.popupAt(sc, lx, ly); id != 0 {525+ // only, under the top and overlay layers (as drawn).
526+ if id, px, py := c.popupAt(sc, lx, ly, true); id != 0 {
520 return id, px, py527 return id, px, py
521 }528 }
522 if id, px, py := layerAt(sc, lx, ly, true); id != 0 {529 if id, px, py := layerAt(sc, lx, ly, true); id != 0 {
523 return id, px, py530 return id, px, py
524 }531 }
532+ if id, px, py := c.popupAt(sc, lx, ly, false); id != 0 {
533+ return id, px, py
534+ }
525 var id WindowID535 var id WindowID
526 var sx, sy float64536 var sx, sy float64
527 full := false537 full := false
@@ -737,16 +747,19 @@ func (c *Core) Run(ctx context.Context) error {
737 // A click on an on-demand layer gives it the keyboard.747 // A click on an on-demand layer gives it the keyboard.
738 if v.Pressed && c.onDemand(id) && c.layerFocus != id {748 if v.Pressed && c.onDemand(id) && c.layerFocus != id {
739 c.layerFocus = id749 c.layerFocus = id
750+ c.layerOver, _ = c.cur().mon.Focused()
740 if err := c.publish(ctx); err != nil {751 if err := c.publish(ctx); err != nil {
741 return nil752 return nil
742 }753 }
743- }754+ } else if v.Pressed && c.layerFocus != 0 && id != c.layerFocus && c.popupRoot(id) != c.layerFocus {
744- // A click focuses the window and its output, taking the755+ // A click anywhere else takes the keyboard back.
745- // keyboard back from a layer.
746- s, w := c.screenOf(id)
747- if v.Pressed && s != nil {
748 c.layerFocus = 0756 c.layerFocus = 0
757+ if err := c.publish(ctx); err != nil {
758+ return nil
759+ }
749 }760 }
761+ // A click focuses the window and its output.
762+ s, w := c.screenOf(id)
750 if v.Pressed && s != nil && w == s.mon.Current() && (c.focus != id || s != c.cur()) {763 if v.Pressed && s != nil && w == s.mon.Current() && (c.focus != id || s != c.cur()) {
751 w.FocusID(id)764 w.FocusID(id)
752 c.focusScreen = c.screenIndex(s.name())765 c.focusScreen = c.screenIndex(s.name())
M internal/core/layer_input_test.go +62 -0
@@ -112,6 +112,68 @@ func TestLayerPointerPopupAndFocus(t *testing.T) {
112112 if f := next(t, commands, anyOf[ports.FocusWindow]); f.ID != 6 {
113113 t.Fatalf("focus %d, want the bar back", f.ID)
114114 }
115+
116+ // Unmapped then mapped again, the bar does not take the keyboard back.
117+ client <- ports.LayerChanged{}
118+ next(t, commands, func(v ports.FocusWindow) bool { return v.ID == 1 })
119+ client <- ports.LayerChanged{Layers: []ports.LayerSurface{bar}}
120+ scene(t, scenes)
121+ input <- ports.PointerMotion{X: 20, Y: 50} // publishes
122+ next(t, commands, func(v ports.PointerFocus) bool { return v.ID == 1 })
123+ select {
124+ case v := <-commands:
125+ if f, ok := v.(ports.FocusWindow); ok {
126+ t.Fatalf("focus moved to %d", f.ID)
127+ }
128+ default:
129+ }
130+
131+ // A window focus change by keyboard or a new window takes it back too.
132+ input <- ports.PointerMotion{X: 50, Y: 5}
133+ next(t, commands, func(v ports.PointerFocus) bool { return v.ID == 6 })
134+ input <- ports.PointerButton{Button: 0x110, Pressed: true}
135+ next(t, commands, func(v ports.FocusWindow) bool { return v.ID == 6 })
136+ input <- ports.PointerButton{Button: 0x110}
137+ client <- ports.WindowMapped{ID: 2}
138+ if f := next(t, commands, anyOf[ports.FocusWindow]); f.ID != 2 {
139+ t.Fatalf("focus %d, want the new window", f.ID)
140+ }
141+}
142+
143+// A window's menu stays with the windows: an overlay layer covers it.
144+func TestWindowPopupUnderOverlay(t *testing.T) {
145+ cfg := config.Defaults()
146+ cfg.Border.Width = 0
147+ client := make(chan ports.ClientEvent, 8)
148+ input := make(chan ports.InputEvent, 8)
149+ output := make(chan ports.OutputEvent, 8)
150+ commands := make(chan ports.ClientCommand, 256)
151+ scenes := make(chan []ports.Scene, 1)
152+ c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes})
153+ if err != nil {
154+ t.Fatal(err)
155+ }
156+ ctx, cancel := context.WithCancel(context.Background())
157+ defer cancel()
158+ go c.Run(ctx)
159+ output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "OUT-1", Width: 100, Height: 80}}
160+ scene(t, scenes)
161+ client <- ports.WindowMapped{ID: 1}
162+ scene(t, scenes)
163+ client <- ports.PopupRequest{ID: 9, Parent: 1, Positioner: ports.Positioner{Width: 30, Height: 20, AnchorRect: ports.Rect{X: 0, Y: 0, W: 10, H: 10}}}
164+ next(t, commands, func(v ports.ConfigurePopup) bool { return v.ID == 9 })
165+ client <- ports.PopupMapped{ID: 9}
166+ sc := sceneMatch(t, scenes, func(sc ports.Scene) bool { return len(sc.Windows) == 2 })
167+ if w := sc.Windows[1]; !w.Popup || w.OverLayers {
168+ t.Fatalf("window popup %+v", w)
169+ }
170+ osd := ports.LayerSurface{ID: 7, Layer: ports.LayerOverlay, Anchor: 15}
171+ client <- ports.LayerChanged{Layers: []ports.LayerSurface{osd}}
172+ scene(t, scenes)
173+ input <- ports.PointerMotion{X: 5, Y: 5}
174+ if v := next(t, commands, anyOf[ports.PointerMotionTo]); v.ID != 7 {
175+ t.Fatalf("hit %d, want the overlay", v.ID)
176+ }
115177 }
116178
117179 // A fullscreen window hides top and bottom layers from the pointer, not
@@ -112,6 +112,68 @@ func TestLayerPointerPopupAndFocus(t *testing.T) {
112 if f := next(t, commands, anyOf[ports.FocusWindow]); f.ID != 6 {112 if f := next(t, commands, anyOf[ports.FocusWindow]); f.ID != 6 {
113 t.Fatalf("focus %d, want the bar back", f.ID)113 t.Fatalf("focus %d, want the bar back", f.ID)
114 }114 }
115+
116+ // Unmapped then mapped again, the bar does not take the keyboard back.
117+ client <- ports.LayerChanged{}
118+ next(t, commands, func(v ports.FocusWindow) bool { return v.ID == 1 })
119+ client <- ports.LayerChanged{Layers: []ports.LayerSurface{bar}}
120+ scene(t, scenes)
121+ input <- ports.PointerMotion{X: 20, Y: 50} // publishes
122+ next(t, commands, func(v ports.PointerFocus) bool { return v.ID == 1 })
123+ select {
124+ case v := <-commands:
125+ if f, ok := v.(ports.FocusWindow); ok {
126+ t.Fatalf("focus moved to %d", f.ID)
127+ }
128+ default:
129+ }
130+
131+ // A window focus change by keyboard or a new window takes it back too.
132+ input <- ports.PointerMotion{X: 50, Y: 5}
133+ next(t, commands, func(v ports.PointerFocus) bool { return v.ID == 6 })
134+ input <- ports.PointerButton{Button: 0x110, Pressed: true}
135+ next(t, commands, func(v ports.FocusWindow) bool { return v.ID == 6 })
136+ input <- ports.PointerButton{Button: 0x110}
137+ client <- ports.WindowMapped{ID: 2}
138+ if f := next(t, commands, anyOf[ports.FocusWindow]); f.ID != 2 {
139+ t.Fatalf("focus %d, want the new window", f.ID)
140+ }
141+}
142+
143+// A window's menu stays with the windows: an overlay layer covers it.
144+func TestWindowPopupUnderOverlay(t *testing.T) {
145+ cfg := config.Defaults()
146+ cfg.Border.Width = 0
147+ client := make(chan ports.ClientEvent, 8)
148+ input := make(chan ports.InputEvent, 8)
149+ output := make(chan ports.OutputEvent, 8)
150+ commands := make(chan ports.ClientCommand, 256)
151+ scenes := make(chan []ports.Scene, 1)
152+ c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes})
153+ if err != nil {
154+ t.Fatal(err)
155+ }
156+ ctx, cancel := context.WithCancel(context.Background())
157+ defer cancel()
158+ go c.Run(ctx)
159+ output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "OUT-1", Width: 100, Height: 80}}
160+ scene(t, scenes)
161+ client <- ports.WindowMapped{ID: 1}
162+ scene(t, scenes)
163+ client <- ports.PopupRequest{ID: 9, Parent: 1, Positioner: ports.Positioner{Width: 30, Height: 20, AnchorRect: ports.Rect{X: 0, Y: 0, W: 10, H: 10}}}
164+ next(t, commands, func(v ports.ConfigurePopup) bool { return v.ID == 9 })
165+ client <- ports.PopupMapped{ID: 9}
166+ sc := sceneMatch(t, scenes, func(sc ports.Scene) bool { return len(sc.Windows) == 2 })
167+ if w := sc.Windows[1]; !w.Popup || w.OverLayers {
168+ t.Fatalf("window popup %+v", w)
169+ }
170+ osd := ports.LayerSurface{ID: 7, Layer: ports.LayerOverlay, Anchor: 15}
171+ client <- ports.LayerChanged{Layers: []ports.LayerSurface{osd}}
172+ scene(t, scenes)
173+ input <- ports.PointerMotion{X: 5, Y: 5}
174+ if v := next(t, commands, anyOf[ports.PointerMotionTo]); v.ID != 7 {
175+ t.Fatalf("hit %d, want the overlay", v.ID)
176+ }
115 }177 }
116 178
117 // A fullscreen window hides top and bottom layers from the pointer, not179 // A fullscreen window hides top and bottom layers from the pointer, not
M internal/core/popup.go +26 -4
@@ -278,11 +278,12 @@ func (c *Core) grabFocus() WindowID {
278278 return 0
279279 }
280280
281-// popupAt returns the topmost mapped popup under the output-local point.
282-func (c *Core) popupAt(sc *screen, lx, ly float64) (WindowID, float64, float64) {
281+// popupAt returns the topmost mapped popup under the output-local point,
282+// among layer popups (overLayers) or window popups.
283+func (c *Core) popupAt(sc *screen, lx, ly float64, overLayers bool) (WindowID, float64, float64) {
283284 for i := len(c.popupOrder) - 1; i >= 0; i-- {
284285 p := c.popups[c.popupOrder[i]]
285- if p == nil || !p.mapped {
286+ if p == nil || !p.mapped || c.onLayer(c.popupRoot(p.id)) != overLayers {
286287 continue
287288 }
288289 s, r, ok := c.windowRect(p.id)
@@ -296,6 +297,27 @@ func (c *Core) popupAt(sc *screen, lx, ly float64) (WindowID, float64, float64)
296297 return 0, 0, 0
297298 }
298299
300+// popupRoot is the window or layer a popup chain hangs from; id itself
301+// when it is not a popup.
302+func (c *Core) popupRoot(id WindowID) WindowID {
303+ for p := c.popups[id]; p != nil; p = c.popups[id] {
304+ id = p.parent
305+ }
306+ return id
307+}
308+
309+// onLayer reports whether id is a mapped layer surface.
310+func (c *Core) onLayer(id WindowID) bool {
311+ for _, sc := range c.screens {
312+ for _, l := range sc.layers {
313+ if l.ID == id {
314+ return true
315+ }
316+ }
317+ }
318+ return false
319+}
320+
299321 // scenePopups are the mapped popups on a screen, bottom to top.
300322 func (c *Core) scenePopups(sc *screen) []ports.SceneWindow {
301323 var out []ports.SceneWindow
@@ -305,7 +327,7 @@ func (c *Core) scenePopups(sc *screen) []ports.SceneWindow {
305327 continue
306328 }
307329 if s, r, ok := c.windowRect(id); ok && s == sc {
308- out = append(out, ports.SceneWindow{ID: id, Rect: r, Popup: true})
330+ out = append(out, ports.SceneWindow{ID: id, Rect: r, Popup: true, OverLayers: c.onLayer(c.popupRoot(id))})
309331 }
310332 }
311333 return out
@@ -278,11 +278,12 @@ func (c *Core) grabFocus() WindowID {
278 return 0278 return 0
279 }279 }
280 280
281-// popupAt returns the topmost mapped popup under the output-local point.281+// popupAt returns the topmost mapped popup under the output-local point,
282-func (c *Core) popupAt(sc *screen, lx, ly float64) (WindowID, float64, float64) {282+// among layer popups (overLayers) or window popups.
283+func (c *Core) popupAt(sc *screen, lx, ly float64, overLayers bool) (WindowID, float64, float64) {
283 for i := len(c.popupOrder) - 1; i >= 0; i-- {284 for i := len(c.popupOrder) - 1; i >= 0; i-- {
284 p := c.popups[c.popupOrder[i]]285 p := c.popups[c.popupOrder[i]]
285- if p == nil || !p.mapped {286+ if p == nil || !p.mapped || c.onLayer(c.popupRoot(p.id)) != overLayers {
286 continue287 continue
287 }288 }
288 s, r, ok := c.windowRect(p.id)289 s, r, ok := c.windowRect(p.id)
@@ -296,6 +297,27 @@ func (c *Core) popupAt(sc *screen, lx, ly float64) (WindowID, float64, float64)
296 return 0, 0, 0297 return 0, 0, 0
297 }298 }
298 299
300+// popupRoot is the window or layer a popup chain hangs from; id itself
301+// when it is not a popup.
302+func (c *Core) popupRoot(id WindowID) WindowID {
303+ for p := c.popups[id]; p != nil; p = c.popups[id] {
304+ id = p.parent
305+ }
306+ return id
307+}
308+
309+// onLayer reports whether id is a mapped layer surface.
310+func (c *Core) onLayer(id WindowID) bool {
311+ for _, sc := range c.screens {
312+ for _, l := range sc.layers {
313+ if l.ID == id {
314+ return true
315+ }
316+ }
317+ }
318+ return false
319+}
320+
299 // scenePopups are the mapped popups on a screen, bottom to top.321 // scenePopups are the mapped popups on a screen, bottom to top.
300 func (c *Core) scenePopups(sc *screen) []ports.SceneWindow {322 func (c *Core) scenePopups(sc *screen) []ports.SceneWindow {
301 var out []ports.SceneWindow323 var out []ports.SceneWindow
@@ -305,7 +327,7 @@ func (c *Core) scenePopups(sc *screen) []ports.SceneWindow {
305 continue327 continue
306 }328 }
307 if s, r, ok := c.windowRect(id); ok && s == sc {329 if s, r, ok := c.windowRect(id); ok && s == sc {
308- out = append(out, ports.SceneWindow{ID: id, Rect: r, Popup: true})330+ out = append(out, ports.SceneWindow{ID: id, Rect: r, Popup: true, OverLayers: c.onLayer(c.popupRoot(id))})
309 }331 }
310 }332 }
311 return out333 return out
M internal/ports/messages.go +5 -1
@@ -532,7 +532,8 @@ type Scene struct {
532532 Windows []SceneWindow
533533 // Layers are drawn in slice order: background and bottom before windows,
534534 // top and overlay after. A fullscreen window covers bottom and top.
535- // Popups (SceneWindow.Popup) are drawn last, over every layer.
535+ // Window popups are drawn after the windows, layer popups
536+ // (SceneWindow.OverLayers) last, over every layer.
536537 Layers []SceneLayer
537538 }
538539
@@ -658,6 +659,9 @@ type SceneWindow struct {
658659 Borderless bool
659660 // Popups are drawn from their content only: no border, no background.
660661 Popup bool
662+ // OverLayers popups hang from a layer surface: drawn over the top and
663+ // overlay layers. Window popups stay with the windows, under them.
664+ OverLayers bool
661665 }
662666
663667 // State carries core → state publisher a snapshot of what is on screen, for
@@ -532,7 +532,8 @@ type Scene struct {
532 Windows []SceneWindow532 Windows []SceneWindow
533 // Layers are drawn in slice order: background and bottom before windows,533 // Layers are drawn in slice order: background and bottom before windows,
534 // top and overlay after. A fullscreen window covers bottom and top.534 // top and overlay after. A fullscreen window covers bottom and top.
535- // Popups (SceneWindow.Popup) are drawn last, over every layer.535+ // Window popups are drawn after the windows, layer popups
536+ // (SceneWindow.OverLayers) last, over every layer.
536 Layers []SceneLayer537 Layers []SceneLayer
537 }538 }
538 539
@@ -658,6 +659,9 @@ type SceneWindow struct {
658 Borderless bool659 Borderless bool
659 // Popups are drawn from their content only: no border, no background.660 // Popups are drawn from their content only: no border, no background.
660 Popup bool661 Popup bool
662+ // OverLayers popups hang from a layer surface: drawn over the top and
663+ // overlay layers. Window popups stay with the windows, under them.
664+ OverLayers bool
661 }665 }
662 666
663 // State carries core → state publisher a snapshot of what is on screen, for667 // State carries core → state publisher a snapshot of what is on screen, for

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