detached 45d51da

fix(core): reroute data drags on layout changes and keep swipes and switcher Escape out

commit
45d51dacf5e9febfed748eec432f16b28f5e3776
author
brice · · 2026-10-17 15:01 UTC
parent
a976009
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
5 files · +118 -12
M internal/core/core.go +6 -2
@@ -102,7 +102,7 @@ type Core struct {
102102 // held, cmdMod the one keyboard.cmd names. lastButton is the latest
103103 // press a client saw, the one xdg_toplevel.move's serial names.
104104 drag *dragState
105- // dataDrag is the client data drag in progress (datadrag.go);
105+ // dataDrag is the client data drag in progress (datadrag.go).
106106 dataDrag *dataDragState
107107 swallow map[uint32]bool
108108 lastButton uint32
@@ -762,7 +762,11 @@ func (c *Core) slidFrame(ctx context.Context, shown bool, only *screen) error {
762762 // rehit points the pointer at what now lies under a still cursor, as
763763 // windows slide under it. A held button keeps its grab.
764764 func (c *Core) rehit(ctx context.Context) error {
765- if c.grab != 0 || c.drag != nil || c.dataDrag != nil {
765+ if c.dataDrag != nil {
766+ // The drag target follows the layout too.
767+ return c.dataDragRetarget(ctx, c.motionTime)
768+ }
769+ if c.grab != 0 || c.drag != nil {
766770 return nil
767771 }
768772 id, x, y := c.hit(c.cursorX, c.cursorY)
@@ -102,7 +102,7 @@ type Core struct {
102 // held, cmdMod the one keyboard.cmd names. lastButton is the latest102 // held, cmdMod the one keyboard.cmd names. lastButton is the latest
103 // press a client saw, the one xdg_toplevel.move's serial names.103 // press a client saw, the one xdg_toplevel.move's serial names.
104 drag *dragState104 drag *dragState
105- // dataDrag is the client data drag in progress (datadrag.go);105+ // dataDrag is the client data drag in progress (datadrag.go).
106 dataDrag *dataDragState106 dataDrag *dataDragState
107 swallow map[uint32]bool107 swallow map[uint32]bool
108 lastButton uint32108 lastButton uint32
@@ -762,7 +762,11 @@ func (c *Core) slidFrame(ctx context.Context, shown bool, only *screen) error {
762 // rehit points the pointer at what now lies under a still cursor, as762 // rehit points the pointer at what now lies under a still cursor, as
763 // windows slide under it. A held button keeps its grab.763 // windows slide under it. A held button keeps its grab.
764 func (c *Core) rehit(ctx context.Context) error {764 func (c *Core) rehit(ctx context.Context) error {
765- if c.grab != 0 || c.drag != nil || c.dataDrag != nil {765+ if c.dataDrag != nil {
766+ // The drag target follows the layout too.
767+ return c.dataDragRetarget(ctx, c.motionTime)
768+ }
769+ if c.grab != 0 || c.drag != nil {
766 return nil770 return nil
767 }771 }
768 id, x, y := c.hit(c.cursorX, c.cursorY)772 id, x, y := c.hit(c.cursorX, c.cursorY)
M internal/core/datadrag.go +24 -3
@@ -17,13 +17,15 @@ import (
1717 // target, told to wayland as DataDragFocus and DataDragMotion;
1818 // - releasing the drag button drops (DataDragDrop), Escape cancels
1919 // (DataDragCancel); neither reaches a client;
20-// - scrolling and touchpad gestures go nowhere; the keyboard keeps its
20+// - scrolling and touchpad gestures go nowhere, compositor swipes
21+// included; binds still run, Escape cancels the drag before the
22+// switcher; the keyboard keeps its
2123 // focus, clients read the modifiers that choose the action from it;
2224 // - the drag icon, a client surface wayland measures (DataDragIcon), is
2325 // drawn at the cursor over everything.
2426 //
25-// Layout changes under a still cursor, like an unmapped target, are
26-// resolved at the next motion.
27+// Layout changes under a still cursor reroute the drag (rehit), and the
28+// drop re-hits.
2729
2830 // dataDragState is the data drag in progress.
2931 type dataDragState struct {
@@ -57,6 +59,11 @@ func (c *Core) startDataDrag(ctx context.Context, v ports.DataDragStarted) error
5759 }
5860 }
5961 c.grab = 0
62+ // A compositor swipe in progress lets go of what it moved.
63+ if c.swipe != nil {
64+ c.dropSwipe()
65+ c.swipe = nil
66+ }
6067 // The origin gets its pointer leave, as the drag takes the pointer.
6168 if c.pointer != 0 {
6269 c.pointer = 0
@@ -84,6 +91,15 @@ func (c *Core) dataDragRoute(ctx context.Context, at time.Duration) error {
8491 return nil
8592 }
8693
94+// dataDragRetarget re-routes a still cursor: only a changed target is
95+// told.
96+func (c *Core) dataDragRetarget(ctx context.Context, at time.Duration) error {
97+ if id, _, _ := c.hit(c.cursorX, c.cursorY); id == c.dataDrag.target {
98+ return nil
99+ }
100+ return c.dataDragRoute(ctx, at)
101+}
102+
87103 // dataDragMotion follows the cursor: the target gets the motion and the
88104 // icon moves with it.
89105 func (c *Core) dataDragMotion(ctx context.Context, at time.Duration) error {
@@ -105,6 +121,11 @@ func (c *Core) dataDragButton(ctx context.Context, v ports.PointerButton) error
105121 return nil
106122 }
107123 delete(c.buttons, v.Button)
124+ // A bind may have moved the layout under a still cursor: the
125+ // drop goes to what lies there now.
126+ if err := c.dataDragRetarget(ctx, v.Time); err != nil {
127+ return err
128+ }
108129 c.dataDrag = nil
109130 if err := c.command(ctx, ports.DataDragDrop{}); err != nil {
110131 return err
@@ -17,13 +17,15 @@ import (
17 // target, told to wayland as DataDragFocus and DataDragMotion;17 // target, told to wayland as DataDragFocus and DataDragMotion;
18 // - releasing the drag button drops (DataDragDrop), Escape cancels18 // - releasing the drag button drops (DataDragDrop), Escape cancels
19 // (DataDragCancel); neither reaches a client;19 // (DataDragCancel); neither reaches a client;
20-// - scrolling and touchpad gestures go nowhere; the keyboard keeps its20+// - scrolling and touchpad gestures go nowhere, compositor swipes
21+// included; binds still run, Escape cancels the drag before the
22+// switcher; the keyboard keeps its
21 // focus, clients read the modifiers that choose the action from it;23 // focus, clients read the modifiers that choose the action from it;
22 // - the drag icon, a client surface wayland measures (DataDragIcon), is24 // - the drag icon, a client surface wayland measures (DataDragIcon), is
23 // drawn at the cursor over everything.25 // drawn at the cursor over everything.
24 //26 //
25-// Layout changes under a still cursor, like an unmapped target, are27+// Layout changes under a still cursor reroute the drag (rehit), and the
26-// resolved at the next motion.28+// drop re-hits.
27 29
28 // dataDragState is the data drag in progress.30 // dataDragState is the data drag in progress.
29 type dataDragState struct {31 type dataDragState struct {
@@ -57,6 +59,11 @@ func (c *Core) startDataDrag(ctx context.Context, v ports.DataDragStarted) error
57 }59 }
58 }60 }
59 c.grab = 061 c.grab = 0
62+ // A compositor swipe in progress lets go of what it moved.
63+ if c.swipe != nil {
64+ c.dropSwipe()
65+ c.swipe = nil
66+ }
60 // The origin gets its pointer leave, as the drag takes the pointer.67 // The origin gets its pointer leave, as the drag takes the pointer.
61 if c.pointer != 0 {68 if c.pointer != 0 {
62 c.pointer = 069 c.pointer = 0
@@ -84,6 +91,15 @@ func (c *Core) dataDragRoute(ctx context.Context, at time.Duration) error {
84 return nil91 return nil
85 }92 }
86 93
94+// dataDragRetarget re-routes a still cursor: only a changed target is
95+// told.
96+func (c *Core) dataDragRetarget(ctx context.Context, at time.Duration) error {
97+ if id, _, _ := c.hit(c.cursorX, c.cursorY); id == c.dataDrag.target {
98+ return nil
99+ }
100+ return c.dataDragRoute(ctx, at)
101+}
102+
87 // dataDragMotion follows the cursor: the target gets the motion and the103 // dataDragMotion follows the cursor: the target gets the motion and the
88 // icon moves with it.104 // icon moves with it.
89 func (c *Core) dataDragMotion(ctx context.Context, at time.Duration) error {105 func (c *Core) dataDragMotion(ctx context.Context, at time.Duration) error {
@@ -105,6 +121,11 @@ func (c *Core) dataDragButton(ctx context.Context, v ports.PointerButton) error
105 return nil121 return nil
106 }122 }
107 delete(c.buttons, v.Button)123 delete(c.buttons, v.Button)
124+ // A bind may have moved the layout under a still cursor: the
125+ // drop goes to what lies there now.
126+ if err := c.dataDragRetarget(ctx, v.Time); err != nil {
127+ return err
128+ }
108 c.dataDrag = nil129 c.dataDrag = nil
109 if err := c.command(ctx, ports.DataDragDrop{}); err != nil {130 if err := c.command(ctx, ports.DataDragDrop{}); err != nil {
110 return err131 return err
M internal/core/datadrag_test.go +69 -0
@@ -463,3 +463,72 @@ func TestDataDragFocusScreenFollowsPointer(t *testing.T) {
463463 t.Fatalf("focus %+v", focus)
464464 }
465465 }
466+
467+func TestDataDragFollowsWorkspaceSwitchUnderStillCursor(t *testing.T) {
468+ d := startDnd(t)
469+ d.start(t, 0)
470+ d.moveTo(float64(d.two.X+5), float64(d.two.Y+6), 3)
471+ d.settle(t)
472+ d.drain()
473+ // cmd+2 shows an empty workspace under the still cursor: the target
474+ // leaves, and the release drops on nothing.
475+ d.in(ports.KeyEvent{Keysym: "2", Keycode: 3, Mods: ports.ModSuper, Pressed: true})
476+ d.in(ports.KeyEvent{Keysym: "2", Keycode: 3, Mods: ports.ModSuper})
477+ d.settle(t)
478+ got := dataDragCommands(d.drain())
479+ if !slices.Contains(got, ports.ClientCommand(ports.DataDragFocus{Time: 3 * time.Millisecond})) {
480+ t.Fatalf("no leave after the switch: %#v", got)
481+ }
482+ d.in(ports.PointerButton{Button: 0x110, Time: 4 * time.Millisecond})
483+ d.settle(t)
484+ got = dataDragCommands(d.drain())
485+ if len(got) == 0 || got[0] != (ports.DataDragDrop{}) {
486+ t.Fatalf("commands %#v", got)
487+ }
488+ for _, v := range got {
489+ if f, ok := v.(ports.DataDragFocus); ok && f.ID != 0 {
490+ t.Fatalf("hidden window targeted: %#v", got)
491+ }
492+ }
493+}
494+
495+func TestDataDragEscapeCancelsOverSwitcher(t *testing.T) {
496+ d := startDnd(t)
497+ d.start(t, 0)
498+ d.in(ports.KeyEvent{Keysym: "Super_L", Keycode: 125, Mods: ports.ModSuper, Pressed: true})
499+ d.in(ports.KeyEvent{Keysym: "Tab", Keycode: 15, Mods: ports.ModSuper, Pressed: true})
500+ d.in(ports.KeyEvent{Keysym: "Tab", Keycode: 15, Mods: ports.ModSuper})
501+ d.settle(t)
502+ d.drain()
503+ d.in(ports.KeyEvent{Keysym: "Escape", Keycode: 1, Mods: ports.ModSuper, Pressed: true})
504+ d.settle(t)
505+ got := dataDragCommands(d.drain())
506+ if len(got) == 0 || got[0] != (ports.DataDragCancel{}) {
507+ t.Fatalf("Escape did not cancel the drag: %#v", got)
508+ }
509+ d.in(ports.PointerButton{Button: 0x110})
510+ d.settle(t)
511+ if slices.Contains(dataDragCommands(d.drain()), ports.ClientCommand(ports.DataDragDrop{})) {
512+ t.Fatal("dropped after Escape")
513+ }
514+}
515+
516+func TestDataDragIgnoresCompositorSwipes(t *testing.T) {
517+ d := startDnd(t)
518+ d.start(t, 0)
519+ before := d.settle(t)
520+ // Flicks that page columns and workspaces, as navigation would.
521+ for _, fingers := range []int{3, 4} {
522+ at := time.Second
523+ d.in(ports.SwipeBegin{Fingers: fingers, Time: at})
524+ for range 10 {
525+ at += 8 * time.Millisecond
526+ d.in(ports.SwipeUpdate{DX: 60, AccelDX: 60, DY: 40, AccelDY: 40, Time: at})
527+ }
528+ d.in(ports.SwipeEnd{Time: at})
529+ }
530+ after := d.settle(t)
531+ if !slices.Equal(before[0].Windows, after[0].Windows) {
532+ t.Fatalf("a swipe moved windows during the drag:\n%#v\n%#v", before[0].Windows, after[0].Windows)
533+ }
534+}
@@ -463,3 +463,72 @@ func TestDataDragFocusScreenFollowsPointer(t *testing.T) {
463 t.Fatalf("focus %+v", focus)463 t.Fatalf("focus %+v", focus)
464 }464 }
465 }465 }
466+
467+func TestDataDragFollowsWorkspaceSwitchUnderStillCursor(t *testing.T) {
468+ d := startDnd(t)
469+ d.start(t, 0)
470+ d.moveTo(float64(d.two.X+5), float64(d.two.Y+6), 3)
471+ d.settle(t)
472+ d.drain()
473+ // cmd+2 shows an empty workspace under the still cursor: the target
474+ // leaves, and the release drops on nothing.
475+ d.in(ports.KeyEvent{Keysym: "2", Keycode: 3, Mods: ports.ModSuper, Pressed: true})
476+ d.in(ports.KeyEvent{Keysym: "2", Keycode: 3, Mods: ports.ModSuper})
477+ d.settle(t)
478+ got := dataDragCommands(d.drain())
479+ if !slices.Contains(got, ports.ClientCommand(ports.DataDragFocus{Time: 3 * time.Millisecond})) {
480+ t.Fatalf("no leave after the switch: %#v", got)
481+ }
482+ d.in(ports.PointerButton{Button: 0x110, Time: 4 * time.Millisecond})
483+ d.settle(t)
484+ got = dataDragCommands(d.drain())
485+ if len(got) == 0 || got[0] != (ports.DataDragDrop{}) {
486+ t.Fatalf("commands %#v", got)
487+ }
488+ for _, v := range got {
489+ if f, ok := v.(ports.DataDragFocus); ok && f.ID != 0 {
490+ t.Fatalf("hidden window targeted: %#v", got)
491+ }
492+ }
493+}
494+
495+func TestDataDragEscapeCancelsOverSwitcher(t *testing.T) {
496+ d := startDnd(t)
497+ d.start(t, 0)
498+ d.in(ports.KeyEvent{Keysym: "Super_L", Keycode: 125, Mods: ports.ModSuper, Pressed: true})
499+ d.in(ports.KeyEvent{Keysym: "Tab", Keycode: 15, Mods: ports.ModSuper, Pressed: true})
500+ d.in(ports.KeyEvent{Keysym: "Tab", Keycode: 15, Mods: ports.ModSuper})
501+ d.settle(t)
502+ d.drain()
503+ d.in(ports.KeyEvent{Keysym: "Escape", Keycode: 1, Mods: ports.ModSuper, Pressed: true})
504+ d.settle(t)
505+ got := dataDragCommands(d.drain())
506+ if len(got) == 0 || got[0] != (ports.DataDragCancel{}) {
507+ t.Fatalf("Escape did not cancel the drag: %#v", got)
508+ }
509+ d.in(ports.PointerButton{Button: 0x110})
510+ d.settle(t)
511+ if slices.Contains(dataDragCommands(d.drain()), ports.ClientCommand(ports.DataDragDrop{})) {
512+ t.Fatal("dropped after Escape")
513+ }
514+}
515+
516+func TestDataDragIgnoresCompositorSwipes(t *testing.T) {
517+ d := startDnd(t)
518+ d.start(t, 0)
519+ before := d.settle(t)
520+ // Flicks that page columns and workspaces, as navigation would.
521+ for _, fingers := range []int{3, 4} {
522+ at := time.Second
523+ d.in(ports.SwipeBegin{Fingers: fingers, Time: at})
524+ for range 10 {
525+ at += 8 * time.Millisecond
526+ d.in(ports.SwipeUpdate{DX: 60, AccelDX: 60, DY: 40, AccelDY: 40, Time: at})
527+ }
528+ d.in(ports.SwipeEnd{Time: at})
529+ }
530+ after := d.settle(t)
531+ if !slices.Equal(before[0].Windows, after[0].Windows) {
532+ t.Fatalf("a swipe moved windows during the drag:\n%#v\n%#v", before[0].Windows, after[0].Windows)
533+ }
534+}
M internal/core/input.go +18 -6
@@ -26,6 +26,10 @@ func (c *Core) handleInput(ctx context.Context, ev ports.InputEvent) error {
2626 case ports.PointerAxis:
2727 return c.pointerAxis(ctx, v)
2828 case ports.SwipeBegin:
29+ if c.dataDrag != nil && !forwardsSwipe(v.Fingers) {
30+ // The drag owns the pointer: no navigation under it.
31+ return nil
32+ }
2933 if forwardsSwipe(v.Fingers) {
3034 return c.gestureBegin(ctx, ports.GestureSwipe, v.Fingers, v.Time)
3135 }
@@ -252,16 +256,17 @@ func (c *Core) keyEvent(ctx context.Context, key ports.KeyEvent) error {
252256 return err
253257 }
254258 }
259+ if c.dataDrag != nil && key.Pressed && key.Keysym == "Escape" {
260+ // Escape cancels the data drag first, even over the switcher;
261+ // its release goes nowhere either.
262+ c.pressed[heldKey(key)] = true
263+ return c.cancelDataDrag(ctx)
264+ }
255265 if c.switching() && key.Pressed && key.Keysym == "Escape" {
256266 // Escape cancels the switcher; its release goes nowhere either.
257267 c.pressed[heldKey(key)] = true
258268 return c.abortSwitcher(ctx)
259269 }
260- if c.dataDrag != nil && key.Pressed && key.Keysym == "Escape" {
261- // Escape cancels the data drag; its release goes nowhere either.
262- c.pressed[heldKey(key)] = true
263- return c.cancelDataDrag(ctx)
264- }
265270 if c.drag != nil && key.Pressed && key.Keysym == "Escape" {
266271 // Escape cancels the drag; its release goes nowhere either.
267272 c.cancelDrag()
@@ -385,7 +390,14 @@ func (c *Core) runBind(ctx context.Context, action Action) error {
385390 return err
386391 }
387392 }
388- return c.publish(ctx)
393+ if err := c.publish(ctx); err != nil {
394+ return err
395+ }
396+ if c.dataDrag != nil {
397+ // A data drag follows the layout under the still cursor.
398+ return c.rehit(ctx)
399+ }
400+ return nil
389401 }
390402
391403 // runSpawn starts the programs of a spawn effect of an action applied to w:
@@ -26,6 +26,10 @@ func (c *Core) handleInput(ctx context.Context, ev ports.InputEvent) error {
26 case ports.PointerAxis:26 case ports.PointerAxis:
27 return c.pointerAxis(ctx, v)27 return c.pointerAxis(ctx, v)
28 case ports.SwipeBegin:28 case ports.SwipeBegin:
29+ if c.dataDrag != nil && !forwardsSwipe(v.Fingers) {
30+ // The drag owns the pointer: no navigation under it.
31+ return nil
32+ }
29 if forwardsSwipe(v.Fingers) {33 if forwardsSwipe(v.Fingers) {
30 return c.gestureBegin(ctx, ports.GestureSwipe, v.Fingers, v.Time)34 return c.gestureBegin(ctx, ports.GestureSwipe, v.Fingers, v.Time)
31 }35 }
@@ -252,16 +256,17 @@ func (c *Core) keyEvent(ctx context.Context, key ports.KeyEvent) error {
252 return err256 return err
253 }257 }
254 }258 }
259+ if c.dataDrag != nil && key.Pressed && key.Keysym == "Escape" {
260+ // Escape cancels the data drag first, even over the switcher;
261+ // its release goes nowhere either.
262+ c.pressed[heldKey(key)] = true
263+ return c.cancelDataDrag(ctx)
264+ }
255 if c.switching() && key.Pressed && key.Keysym == "Escape" {265 if c.switching() && key.Pressed && key.Keysym == "Escape" {
256 // Escape cancels the switcher; its release goes nowhere either.266 // Escape cancels the switcher; its release goes nowhere either.
257 c.pressed[heldKey(key)] = true267 c.pressed[heldKey(key)] = true
258 return c.abortSwitcher(ctx)268 return c.abortSwitcher(ctx)
259 }269 }
260- if c.dataDrag != nil && key.Pressed && key.Keysym == "Escape" {
261- // Escape cancels the data drag; its release goes nowhere either.
262- c.pressed[heldKey(key)] = true
263- return c.cancelDataDrag(ctx)
264- }
265 if c.drag != nil && key.Pressed && key.Keysym == "Escape" {270 if c.drag != nil && key.Pressed && key.Keysym == "Escape" {
266 // Escape cancels the drag; its release goes nowhere either.271 // Escape cancels the drag; its release goes nowhere either.
267 c.cancelDrag()272 c.cancelDrag()
@@ -385,7 +390,14 @@ func (c *Core) runBind(ctx context.Context, action Action) error {
385 return err390 return err
386 }391 }
387 }392 }
388- return c.publish(ctx)393+ if err := c.publish(ctx); err != nil {
394+ return err
395+ }
396+ if c.dataDrag != nil {
397+ // A data drag follows the layout under the still cursor.
398+ return c.rehit(ctx)
399+ }
400+ return nil
389 }401 }
390 402
391 // runSpawn starts the programs of a spawn effect of an action applied to w:403 // runSpawn starts the programs of a spawn effect of an action applied to w:
M internal/core/scene.go +1 -1
@@ -21,7 +21,7 @@ func (c *Core) sceneFor(ctx context.Context, i int, sc *screen, only *screen, ca
2121 // layout is what is drawn; settled (same indexes) is where the
2222 // windows are going, and alone sizes the configures.
2323 layout, settled := sc.shown, sc.settledLayout
24- scene = ports.Scene{Security: c.security, Output: sc.name(), OutputWidth: o.W, OutputHeight: o.H, WorkspaceClip: clip, Scale: sc.scale, Transform: sc.transform, Off: sc.off, Background: c.cfg.Background.Color, Border: ports.Border{Width: c.cfg.Border.Width, Active: c.cfg.Border.Active, Inactive: c.cfg.Border.Inactive}, Windows: make([]ports.SceneWindow, 0, len(layout)+len(c.popupOrder)), Layers: shownLayers(sc)}
24+ scene = ports.Scene{Security: c.security, Output: sc.name(), OutputWidth: o.W, OutputHeight: o.H, WorkspaceClip: clip, Scale: sc.scale, Transform: sc.transform, Off: sc.off, Background: c.cfg.Background.Color, Border: ports.Border{Width: c.cfg.Border.Width, Active: c.cfg.Border.Active, Inactive: c.cfg.Border.Inactive}, Windows: make([]ports.SceneWindow, 0, len(layout)+len(c.popupOrder)+1), Layers: shownLayers(sc)}
2525 if !sc.mon.previewing() {
2626 // Tiles stop at the panels; the overview lays out previews over
2727 // the whole output.
@@ -21,7 +21,7 @@ func (c *Core) sceneFor(ctx context.Context, i int, sc *screen, only *screen, ca
21 // layout is what is drawn; settled (same indexes) is where the21 // layout is what is drawn; settled (same indexes) is where the
22 // windows are going, and alone sizes the configures.22 // windows are going, and alone sizes the configures.
23 layout, settled := sc.shown, sc.settledLayout23 layout, settled := sc.shown, sc.settledLayout
24- scene = ports.Scene{Security: c.security, Output: sc.name(), OutputWidth: o.W, OutputHeight: o.H, WorkspaceClip: clip, Scale: sc.scale, Transform: sc.transform, Off: sc.off, Background: c.cfg.Background.Color, Border: ports.Border{Width: c.cfg.Border.Width, Active: c.cfg.Border.Active, Inactive: c.cfg.Border.Inactive}, Windows: make([]ports.SceneWindow, 0, len(layout)+len(c.popupOrder)), Layers: shownLayers(sc)}24+ scene = ports.Scene{Security: c.security, Output: sc.name(), OutputWidth: o.W, OutputHeight: o.H, WorkspaceClip: clip, Scale: sc.scale, Transform: sc.transform, Off: sc.off, Background: c.cfg.Background.Color, Border: ports.Border{Width: c.cfg.Border.Width, Active: c.cfg.Border.Active, Inactive: c.cfg.Border.Inactive}, Windows: make([]ports.SceneWindow, 0, len(layout)+len(c.popupOrder)+1), Layers: shownLayers(sc)}
25 if !sc.mon.previewing() {25 if !sc.mon.previewing() {
26 // Tiles stop at the panels; the overview lays out previews over26 // Tiles stop at the panels; the overview lays out previews over
27 // the whole output.27 // the whole output.

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