feat(core): a four-finger swipe down shows or hides the stash
- commit
- 350422796fede67ae09495735bca59c9561a071a
- author
- brice · · 2026-10-05 00:13 UTC
- parent
- 6337cf2
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +116 -2
M internal/core/gesture.go +26 -2
| @@ -36,7 +36,8 @@ const ( | |||
| 36 | 36 | swipeWorkspaces | |
| 37 | 37 | swipeDiscrete | |
| 38 | 38 | // swipeOverview is a vertical four-finger swipe: up opens the | |
| 39 | - | // overview, down closes it on the selection (niri's gesture). | |
| 39 | + | // overview, down closes it on the selection (niri's gesture); down | |
| 40 | + | // with the overview closed shows or hides the stash. | |
| 40 | 41 | swipeOverview | |
| 41 | 42 | // swipeDropped ignores the rest of a swipe whose workspace changed. | |
| 42 | 43 | swipeDropped | |
| @@ -251,7 +252,13 @@ func (c *Core) swipeEnd(e ports.SwipeEnd) (shown bool) { | |||
| 251 | 252 | } | |
| 252 | 253 | case swipeOverview: | |
| 253 | 254 | step := g.snap.step(e.Cancelled, e.Time) | |
| 254 | - | if step == 0 || (step < 0) == m.ov.open { | |
| 255 | + | if step == 0 { | |
| 256 | + | return false | |
| 257 | + | } | |
| 258 | + | if step > 0 && !m.ov.open { | |
| 259 | + | return c.swipeStash(g, now) | |
| 260 | + | } | |
| 261 | + | if (step < 0) == m.ov.open { | |
| 255 | 262 | return false | |
| 256 | 263 | } | |
| 257 | 264 | before := m.Current() | |
| @@ -294,6 +301,23 @@ func (c *Core) swipeEnd(e ports.SwipeEnd) (shown bool) { | |||
| 294 | 301 | return shown | |
| 295 | 302 | } | |
| 296 | 303 | ||
| 304 | + | // swipeStash runs toggle-stash-visible for a four-finger swipe down with the | |
| 305 | + | // overview closed, on the swipe's screen unless the pointer took the focus | |
| 306 | + | // to another output meanwhile. It reports whether the stash showed or | |
| 307 | + | // hid: an empty stash stays as it is. | |
| 308 | + | func (c *Core) swipeStash(g *swipeGesture, now time.Time) bool { | |
| 309 | + | if c.cur() != g.screen { | |
| 310 | + | return false | |
| 311 | + | } | |
| 312 | + | w := g.screen.mon.Current() | |
| 313 | + | hidden, over := w.stashHidden, w.stashOver | |
| 314 | + | shots := c.snapshot(now) | |
| 315 | + | c.keyboard.takeBack() | |
| 316 | + | c.applyAction(ActionToggleStashVisible) | |
| 317 | + | c.transition(shots, now) | |
| 318 | + | return w.stashHidden != hidden || w.stashOver != over | |
| 319 | + | } | |
| 320 | + | ||
| 297 | 321 | // swipedWorkspace is the workspace on the swipe's screen, or nil without | |
| 298 | 322 | // a swipe. | |
| 299 | 323 | func (c *Core) swipedWorkspace() *Workspace { | |
| @@ -36,7 +36,8 @@ const ( | |||||
| 36 | swipeWorkspaces | 36 | swipeWorkspaces | ||
| 37 | swipeDiscrete | 37 | swipeDiscrete | ||
| 38 | // swipeOverview is a vertical four-finger swipe: up opens the | 38 | // swipeOverview is a vertical four-finger swipe: up opens the | ||
| 39 | - | // overview, down closes it on the selection (niri's gesture). | 39 | + | // overview, down closes it on the selection (niri's gesture); down |
| 40 | + | // with the overview closed shows or hides the stash. | |||
| 40 | swipeOverview | 41 | swipeOverview | ||
| 41 | // swipeDropped ignores the rest of a swipe whose workspace changed. | 42 | // swipeDropped ignores the rest of a swipe whose workspace changed. | ||
| 42 | swipeDropped | 43 | swipeDropped | ||
| @@ -251,7 +252,13 @@ func (c *Core) swipeEnd(e ports.SwipeEnd) (shown bool) { | |||||
| 251 | } | 252 | } | ||
| 252 | case swipeOverview: | 253 | case swipeOverview: | ||
| 253 | step := g.snap.step(e.Cancelled, e.Time) | 254 | step := g.snap.step(e.Cancelled, e.Time) | ||
| 254 | - | if step == 0 || (step < 0) == m.ov.open { | 255 | + | if step == 0 { |
| 256 | + | return false | |||
| 257 | + | } | |||
| 258 | + | if step > 0 && !m.ov.open { | |||
| 259 | + | return c.swipeStash(g, now) | |||
| 260 | + | } | |||
| 261 | + | if (step < 0) == m.ov.open { | |||
| 255 | return false | 262 | return false | ||
| 256 | } | 263 | } | ||
| 257 | before := m.Current() | 264 | before := m.Current() | ||
| @@ -294,6 +301,23 @@ func (c *Core) swipeEnd(e ports.SwipeEnd) (shown bool) { | |||||
| 294 | return shown | 301 | return shown | ||
| 295 | } | 302 | } | ||
| 296 | 303 | ||||
| 304 | + | // swipeStash runs toggle-stash-visible for a four-finger swipe down with the | |||
| 305 | + | // overview closed, on the swipe's screen unless the pointer took the focus | |||
| 306 | + | // to another output meanwhile. It reports whether the stash showed or | |||
| 307 | + | // hid: an empty stash stays as it is. | |||
| 308 | + | func (c *Core) swipeStash(g *swipeGesture, now time.Time) bool { | |||
| 309 | + | if c.cur() != g.screen { | |||
| 310 | + | return false | |||
| 311 | + | } | |||
| 312 | + | w := g.screen.mon.Current() | |||
| 313 | + | hidden, over := w.stashHidden, w.stashOver | |||
| 314 | + | shots := c.snapshot(now) | |||
| 315 | + | c.keyboard.takeBack() | |||
| 316 | + | c.applyAction(ActionToggleStashVisible) | |||
| 317 | + | c.transition(shots, now) | |||
| 318 | + | return w.stashHidden != hidden || w.stashOver != over | |||
| 319 | + | } | |||
| 320 | + | ||||
| 297 | // swipedWorkspace is the workspace on the swipe's screen, or nil without | 321 | // swipedWorkspace is the workspace on the swipe's screen, or nil without | ||
| 298 | // a swipe. | 322 | // a swipe. | ||
| 299 | func (c *Core) swipedWorkspace() *Workspace { | 323 | func (c *Core) swipedWorkspace() *Workspace { | ||
M internal/core/swipe_test.go +90 -0
| @@ -671,6 +671,96 @@ func TestFourFingerSwipeOverview(t *testing.T) { | |||
| 671 | 671 | } | |
| 672 | 672 | } | |
| 673 | 673 | ||
| 674 | + | // four lifts a four-finger swipe of n updates of (dx, dy) after the | |
| 675 | + | // fingers went down, and returns the scene right after the lift. | |
| 676 | + | func (r *swipeRig) four(t *testing.T, dx, dy float64, n int) ports.Scene { | |
| 677 | + | t.Helper() | |
| 678 | + | r.at += time.Second | |
| 679 | + | r.input <- ports.SwipeBegin{Fingers: 4, Time: r.at} | |
| 680 | + | for range n { | |
| 681 | + | r.at += 8 * time.Millisecond | |
| 682 | + | r.input <- ports.SwipeUpdate{DX: dx, DY: dy, Time: r.at} | |
| 683 | + | } | |
| 684 | + | return r.end(t, false) | |
| 685 | + | } | |
| 686 | + | ||
| 687 | + | // stashedThird maps three windows and stashes the third (shown, focused), | |
| 688 | + | // settled. | |
| 689 | + | func stashedThird(t *testing.T, r *swipeRig) ports.Scene { | |
| 690 | + | t.Helper() | |
| 691 | + | threeColumns(t, r) | |
| 692 | + | s := r.key(t, "s", ports.ModAlt|ports.ModShift)[0] | |
| 693 | + | if r.cfg.Animations.On { | |
| 694 | + | s = r.settle(t) | |
| 695 | + | } | |
| 696 | + | return s | |
| 697 | + | } | |
| 698 | + | ||
| 699 | + | // A four-finger swipe down with the overview closed hides the stash, and | |
| 700 | + | // the next one shows it again with its windows fading in; the stash's | |
| 701 | + | // windows stay where the swipe found them otherwise. | |
| 702 | + | func TestFourFingerSwipeDownTogglesStash(t *testing.T) { | |
| 703 | + | r := startSwipe(t, nil) | |
| 704 | + | if !r.cfg.Animations.On { | |
| 705 | + | t.Fatal("animations are off in the rig") | |
| 706 | + | } | |
| 707 | + | if w := windowOf(t, stashedThird(t, r), 3); w.Hidden { | |
| 708 | + | t.Fatalf("setup: the stash is hidden: %+v", w) | |
| 709 | + | } | |
| 710 | + | r.four(t, 0, 40, 10) | |
| 711 | + | if w := windowOf(t, r.frame(t, 15*time.Millisecond), 3); w.Hidden || !(w.Fade > 0 && w.Fade < 1) { | |
| 712 | + | t.Fatalf("hiding: window 3 hidden %t, fade %v, want it leaving", w.Hidden, w.Fade) | |
| 713 | + | } | |
| 714 | + | if w := windowOf(t, r.settle(t), 3); !w.Hidden { | |
| 715 | + | t.Fatalf("swipe down left the stash shown: %+v", w) | |
| 716 | + | } | |
| 717 | + | r.four(t, 0, 40, 10) | |
| 718 | + | if w := windowOf(t, r.frame(t, 15*time.Millisecond), 3); w.Hidden || !(w.Fade > 0 && w.Fade < 1) { | |
| 719 | + | t.Fatalf("showing: window 3 hidden %t, fade %v, want it appearing", w.Hidden, w.Fade) | |
| 720 | + | } | |
| 721 | + | if w := windowOf(t, r.settle(t), 3); w.Hidden || w.Fade != 0 { | |
| 722 | + | t.Fatalf("second swipe down did not show the stash: %+v", w) | |
| 723 | + | } | |
| 724 | + | } | |
| 725 | + | ||
| 726 | + | // With nothing in the stash a swipe down changes nothing: the scene is the | |
| 727 | + | // same one, with no new Seq and no animation. | |
| 728 | + | func TestFourFingerSwipeDownEmptyStash(t *testing.T) { | |
| 729 | + | r := startSwipe(t, nil) | |
| 730 | + | before := threeColumns(t, r) | |
| 731 | + | after := r.four(t, 0, 40, 10) | |
| 732 | + | if after.Seq != before.Seq || !after.SameAs(before) { | |
| 733 | + | t.Fatalf("empty stash: scene changed, seq %d then %d", before.Seq, after.Seq) | |
| 734 | + | } | |
| 735 | + | if _, ok := r.settleAll(t, r.outs...); ok { | |
| 736 | + | t.Fatal("empty stash: the swipe started an animation") | |
| 737 | + | } | |
| 738 | + | } | |
| 739 | + | ||
| 740 | + | // With the overview open a swipe down closes it and does not touch the | |
| 741 | + | // stash, shown or hidden. | |
| 742 | + | func TestFourFingerSwipeDownInOverviewKeepsStash(t *testing.T) { | |
| 743 | + | for _, hidden := range []bool{false, true} { | |
| 744 | + | r := startSwipe(t, nil) | |
| 745 | + | stashedThird(t, r) | |
| 746 | + | if hidden { | |
| 747 | + | r.key(t, "s", ports.ModAlt) | |
| 748 | + | r.settle(t) | |
| 749 | + | } | |
| 750 | + | r.four(t, 0, -40, 10) | |
| 751 | + | r.settle(t) | |
| 752 | + | s := r.four(t, 0, 40, 10) | |
| 753 | + | for _, w := range s.Windows { | |
| 754 | + | if w.Preview > 0 { | |
| 755 | + | t.Fatalf("hidden=%v: swipe down left the overview open", hidden) | |
| 756 | + | } | |
| 757 | + | } | |
| 758 | + | if w := windowOf(t, r.settle(t), 3); w.Hidden != hidden { | |
| 759 | + | t.Fatalf("hidden=%v: window 3 hidden %t after the overview closed", hidden, w.Hidden) | |
| 760 | + | } | |
| 761 | + | } | |
| 762 | + | } | |
| 763 | + | ||
| 674 | 764 | // Opening the overview during a column swipe drops the swipe: the rest | |
| 675 | 765 | // of it neither slides the columns under the overview nor lands them, | |
| 676 | 766 | // also once the overview closed again (escape before the rest). | |
| @@ -671,6 +671,96 @@ func TestFourFingerSwipeOverview(t *testing.T) { | |||||
| 671 | } | 671 | } | ||
| 672 | } | 672 | } | ||
| 673 | 673 | ||||
| 674 | + | // four lifts a four-finger swipe of n updates of (dx, dy) after the | |||
| 675 | + | // fingers went down, and returns the scene right after the lift. | |||
| 676 | + | func (r *swipeRig) four(t *testing.T, dx, dy float64, n int) ports.Scene { | |||
| 677 | + | t.Helper() | |||
| 678 | + | r.at += time.Second | |||
| 679 | + | r.input <- ports.SwipeBegin{Fingers: 4, Time: r.at} | |||
| 680 | + | for range n { | |||
| 681 | + | r.at += 8 * time.Millisecond | |||
| 682 | + | r.input <- ports.SwipeUpdate{DX: dx, DY: dy, Time: r.at} | |||
| 683 | + | } | |||
| 684 | + | return r.end(t, false) | |||
| 685 | + | } | |||
| 686 | + | ||||
| 687 | + | // stashedThird maps three windows and stashes the third (shown, focused), | |||
| 688 | + | // settled. | |||
| 689 | + | func stashedThird(t *testing.T, r *swipeRig) ports.Scene { | |||
| 690 | + | t.Helper() | |||
| 691 | + | threeColumns(t, r) | |||
| 692 | + | s := r.key(t, "s", ports.ModAlt|ports.ModShift)[0] | |||
| 693 | + | if r.cfg.Animations.On { | |||
| 694 | + | s = r.settle(t) | |||
| 695 | + | } | |||
| 696 | + | return s | |||
| 697 | + | } | |||
| 698 | + | ||||
| 699 | + | // A four-finger swipe down with the overview closed hides the stash, and | |||
| 700 | + | // the next one shows it again with its windows fading in; the stash's | |||
| 701 | + | // windows stay where the swipe found them otherwise. | |||
| 702 | + | func TestFourFingerSwipeDownTogglesStash(t *testing.T) { | |||
| 703 | + | r := startSwipe(t, nil) | |||
| 704 | + | if !r.cfg.Animations.On { | |||
| 705 | + | t.Fatal("animations are off in the rig") | |||
| 706 | + | } | |||
| 707 | + | if w := windowOf(t, stashedThird(t, r), 3); w.Hidden { | |||
| 708 | + | t.Fatalf("setup: the stash is hidden: %+v", w) | |||
| 709 | + | } | |||
| 710 | + | r.four(t, 0, 40, 10) | |||
| 711 | + | if w := windowOf(t, r.frame(t, 15*time.Millisecond), 3); w.Hidden || !(w.Fade > 0 && w.Fade < 1) { | |||
| 712 | + | t.Fatalf("hiding: window 3 hidden %t, fade %v, want it leaving", w.Hidden, w.Fade) | |||
| 713 | + | } | |||
| 714 | + | if w := windowOf(t, r.settle(t), 3); !w.Hidden { | |||
| 715 | + | t.Fatalf("swipe down left the stash shown: %+v", w) | |||
| 716 | + | } | |||
| 717 | + | r.four(t, 0, 40, 10) | |||
| 718 | + | if w := windowOf(t, r.frame(t, 15*time.Millisecond), 3); w.Hidden || !(w.Fade > 0 && w.Fade < 1) { | |||
| 719 | + | t.Fatalf("showing: window 3 hidden %t, fade %v, want it appearing", w.Hidden, w.Fade) | |||
| 720 | + | } | |||
| 721 | + | if w := windowOf(t, r.settle(t), 3); w.Hidden || w.Fade != 0 { | |||
| 722 | + | t.Fatalf("second swipe down did not show the stash: %+v", w) | |||
| 723 | + | } | |||
| 724 | + | } | |||
| 725 | + | ||||
| 726 | + | // With nothing in the stash a swipe down changes nothing: the scene is the | |||
| 727 | + | // same one, with no new Seq and no animation. | |||
| 728 | + | func TestFourFingerSwipeDownEmptyStash(t *testing.T) { | |||
| 729 | + | r := startSwipe(t, nil) | |||
| 730 | + | before := threeColumns(t, r) | |||
| 731 | + | after := r.four(t, 0, 40, 10) | |||
| 732 | + | if after.Seq != before.Seq || !after.SameAs(before) { | |||
| 733 | + | t.Fatalf("empty stash: scene changed, seq %d then %d", before.Seq, after.Seq) | |||
| 734 | + | } | |||
| 735 | + | if _, ok := r.settleAll(t, r.outs...); ok { | |||
| 736 | + | t.Fatal("empty stash: the swipe started an animation") | |||
| 737 | + | } | |||
| 738 | + | } | |||
| 739 | + | ||||
| 740 | + | // With the overview open a swipe down closes it and does not touch the | |||
| 741 | + | // stash, shown or hidden. | |||
| 742 | + | func TestFourFingerSwipeDownInOverviewKeepsStash(t *testing.T) { | |||
| 743 | + | for _, hidden := range []bool{false, true} { | |||
| 744 | + | r := startSwipe(t, nil) | |||
| 745 | + | stashedThird(t, r) | |||
| 746 | + | if hidden { | |||
| 747 | + | r.key(t, "s", ports.ModAlt) | |||
| 748 | + | r.settle(t) | |||
| 749 | + | } | |||
| 750 | + | r.four(t, 0, -40, 10) | |||
| 751 | + | r.settle(t) | |||
| 752 | + | s := r.four(t, 0, 40, 10) | |||
| 753 | + | for _, w := range s.Windows { | |||
| 754 | + | if w.Preview > 0 { | |||
| 755 | + | t.Fatalf("hidden=%v: swipe down left the overview open", hidden) | |||
| 756 | + | } | |||
| 757 | + | } | |||
| 758 | + | if w := windowOf(t, r.settle(t), 3); w.Hidden != hidden { | |||
| 759 | + | t.Fatalf("hidden=%v: window 3 hidden %t after the overview closed", hidden, w.Hidden) | |||
| 760 | + | } | |||
| 761 | + | } | |||
| 762 | + | } | |||
| 763 | + | ||||
| 674 | // Opening the overview during a column swipe drops the swipe: the rest | 764 | // Opening the overview during a column swipe drops the swipe: the rest | ||
| 675 | // of it neither slides the columns under the overview nor lands them, | 765 | // of it neither slides the columns under the overview nor lands them, | ||
| 676 | // also once the overview closed again (escape before the rest). | 766 | // also once the overview closed again (escape before the rest). | ||