feat(core): hide the shown stash on a four-finger swipe up
- commit
- 9ff34338ebf488140d6b0d05c1c5756837a75d56
- author
- brice · · 2026-10-05 03:55 UTC
- committer
- brice · · 2026-10-05 03:55 UTC
- parent
- 03ba80b
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
3 files · +97 -7
M internal/core/gesture.go +8 -7
| @@ -36,8 +36,9 @@ 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); down | |
| 40 | - | // with the overview closed shows or hides the stash. | |
| 39 | + | // overview, down closes it on the selection (niri's gesture). With | |
| 40 | + | // the overview closed, down shows or hides the stash, and up hides | |
| 41 | + | // it when it is shown, else opens the overview. | |
| 41 | 42 | swipeOverview | |
| 42 | 43 | // swipeDropped ignores the rest of a swipe whose workspace changed. | |
| 43 | 44 | swipeDropped | |
| @@ -255,7 +256,7 @@ func (c *Core) swipeEnd(e ports.SwipeEnd) (shown bool) { | |||
| 255 | 256 | if step == 0 { | |
| 256 | 257 | return false | |
| 257 | 258 | } | |
| 258 | - | if step > 0 && !m.ov.open { | |
| 259 | + | if !m.ov.open && (step > 0 || m.Current().stashShown()) { | |
| 259 | 260 | return c.swipeStash(g, now) | |
| 260 | 261 | } | |
| 261 | 262 | if (step < 0) == m.ov.open { | |
| @@ -301,10 +302,10 @@ func (c *Core) swipeEnd(e ports.SwipeEnd) (shown bool) { | |||
| 301 | 302 | return shown | |
| 302 | 303 | } | |
| 303 | 304 | ||
| 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. | |
| 305 | + | // swipeStash runs toggle-stash-visible for a four-finger swipe down, or up | |
| 306 | + | // while the stash is shown, with the overview closed, on the swipe's screen | |
| 307 | + | // unless the pointer took the focus to another output meanwhile. It reports | |
| 308 | + | // whether the stash showed or hid: an empty stash stays as it is. | |
| 308 | 309 | func (c *Core) swipeStash(g *swipeGesture, now time.Time) bool { | |
| 309 | 310 | if c.cur() != g.screen { | |
| 310 | 311 | return false | |
| @@ -36,8 +36,9 @@ 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); down | 39 | + | // overview, down closes it on the selection (niri's gesture). With |
| 40 | - | // with the overview closed shows or hides the stash. | 40 | + | // the overview closed, down shows or hides the stash, and up hides |
| 41 | + | // it when it is shown, else opens the overview. | |||
| 41 | swipeOverview | 42 | swipeOverview | ||
| 42 | // swipeDropped ignores the rest of a swipe whose workspace changed. | 43 | // swipeDropped ignores the rest of a swipe whose workspace changed. | ||
| 43 | swipeDropped | 44 | swipeDropped | ||
| @@ -255,7 +256,7 @@ func (c *Core) swipeEnd(e ports.SwipeEnd) (shown bool) { | |||||
| 255 | if step == 0 { | 256 | if step == 0 { | ||
| 256 | return false | 257 | return false | ||
| 257 | } | 258 | } | ||
| 258 | - | if step > 0 && !m.ov.open { | 259 | + | if !m.ov.open && (step > 0 || m.Current().stashShown()) { |
| 259 | return c.swipeStash(g, now) | 260 | return c.swipeStash(g, now) | ||
| 260 | } | 261 | } | ||
| 261 | if (step < 0) == m.ov.open { | 262 | if (step < 0) == m.ov.open { | ||
| @@ -301,10 +302,10 @@ func (c *Core) swipeEnd(e ports.SwipeEnd) (shown bool) { | |||||
| 301 | return shown | 302 | return shown | ||
| 302 | } | 303 | } | ||
| 303 | 304 | ||||
| 304 | - | // swipeStash runs toggle-stash-visible for a four-finger swipe down with the | 305 | + | // swipeStash runs toggle-stash-visible for a four-finger swipe down, or up |
| 305 | - | // overview closed, on the swipe's screen unless the pointer took the focus | 306 | + | // while the stash is shown, with the overview closed, on the swipe's screen |
| 306 | - | // to another output meanwhile. It reports whether the stash showed or | 307 | + | // unless the pointer took the focus to another output meanwhile. It reports |
| 307 | - | // hid: an empty stash stays as it is. | 308 | + | // whether the stash showed or hid: an empty stash stays as it is. |
| 308 | func (c *Core) swipeStash(g *swipeGesture, now time.Time) bool { | 309 | func (c *Core) swipeStash(g *swipeGesture, now time.Time) bool { | ||
| 309 | if c.cur() != g.screen { | 310 | if c.cur() != g.screen { | ||
| 310 | return false | 311 | return false | ||
M internal/core/stash.go +6 -0
| @@ -191,6 +191,12 @@ func (w *Workspace) stashOverCover() bool { | |||
| 191 | 191 | return w.stashOver != 0 && w.stashOver == w.cover() && !w.stashHidden && len(w.Stash) > 0 | |
| 192 | 192 | } | |
| 193 | 193 | ||
| 194 | + | // stashShown reports whether the stash is on screen: it holds windows, is | |
| 195 | + | // not hidden, and no covering fullscreen window hides it. | |
| 196 | + | func (w *Workspace) stashShown() bool { | |
| 197 | + | return len(w.Stash) > 0 && !w.stashHidden && (w.cover() == 0 || w.stashOverCover()) | |
| 198 | + | } | |
| 199 | + | ||
| 194 | 200 | // dropStashOver ends the stash override when the focus leaves it: the | |
| 195 | 201 | // stash hides again under the covering window, as it was. | |
| 196 | 202 | func (w *Workspace) dropStashOver() { | |
| @@ -191,6 +191,12 @@ func (w *Workspace) stashOverCover() bool { | |||||
| 191 | return w.stashOver != 0 && w.stashOver == w.cover() && !w.stashHidden && len(w.Stash) > 0 | 191 | return w.stashOver != 0 && w.stashOver == w.cover() && !w.stashHidden && len(w.Stash) > 0 | ||
| 192 | } | 192 | } | ||
| 193 | 193 | ||||
| 194 | + | // stashShown reports whether the stash is on screen: it holds windows, is | |||
| 195 | + | // not hidden, and no covering fullscreen window hides it. | |||
| 196 | + | func (w *Workspace) stashShown() bool { | |||
| 197 | + | return len(w.Stash) > 0 && !w.stashHidden && (w.cover() == 0 || w.stashOverCover()) | |||
| 198 | + | } | |||
| 199 | + | ||||
| 194 | // dropStashOver ends the stash override when the focus leaves it: the | 200 | // dropStashOver ends the stash override when the focus leaves it: the | ||
| 195 | // stash hides again under the covering window, as it was. | 201 | // stash hides again under the covering window, as it was. | ||
| 196 | func (w *Workspace) dropStashOver() { | 202 | func (w *Workspace) dropStashOver() { | ||
M internal/core/swipe_test.go +83 -0
| @@ -723,6 +723,89 @@ func TestFourFingerSwipeDownTogglesStash(t *testing.T) { | |||
| 723 | 723 | } | |
| 724 | 724 | } | |
| 725 | 725 | ||
| 726 | + | // A four-finger swipe up hides the shown stash, with the same fade as a | |
| 727 | + | // swipe down, instead of opening the overview. | |
| 728 | + | func TestFourFingerSwipeUpHidesShownStash(t *testing.T) { | |
| 729 | + | r := startSwipe(t, nil) | |
| 730 | + | stashedThird(t, r) | |
| 731 | + | s := r.four(t, 0, -40, 10) | |
| 732 | + | for _, w := range s.Windows { | |
| 733 | + | if w.Preview > 0 { | |
| 734 | + | t.Fatal("swipe up opened the overview with the stash shown") | |
| 735 | + | } | |
| 736 | + | } | |
| 737 | + | if w := windowOf(t, r.frame(t, 15*time.Millisecond), 3); w.Hidden || !(w.Fade > 0 && w.Fade < 1) { | |
| 738 | + | t.Fatalf("hiding: window 3 hidden %t, fade %v, want it leaving", w.Hidden, w.Fade) | |
| 739 | + | } | |
| 740 | + | s = r.settle(t) | |
| 741 | + | if w := windowOf(t, s, 3); !w.Hidden { | |
| 742 | + | t.Fatalf("swipe up left the stash shown: %+v", w) | |
| 743 | + | } | |
| 744 | + | for _, w := range s.Windows { | |
| 745 | + | if w.Preview > 0 { | |
| 746 | + | t.Fatal("overview open after the stash hid") | |
| 747 | + | } | |
| 748 | + | } | |
| 749 | + | } | |
| 750 | + | ||
| 751 | + | // With the stash hidden or empty a swipe up opens the overview. | |
| 752 | + | func TestFourFingerSwipeUpOpensOverviewWithoutShownStash(t *testing.T) { | |
| 753 | + | inOverview := func(s ports.Scene) bool { | |
| 754 | + | for _, w := range s.Windows { | |
| 755 | + | if w.Preview > 0 { | |
| 756 | + | return true | |
| 757 | + | } | |
| 758 | + | } | |
| 759 | + | return false | |
| 760 | + | } | |
| 761 | + | for _, name := range []string{"hidden", "empty"} { | |
| 762 | + | r := startSwipe(t, nil) | |
| 763 | + | if name == "hidden" { | |
| 764 | + | stashedThird(t, r) | |
| 765 | + | r.key(t, "s", ports.ModAlt) | |
| 766 | + | r.settle(t) | |
| 767 | + | } else { | |
| 768 | + | threeColumns(t, r) | |
| 769 | + | } | |
| 770 | + | r.four(t, 0, -40, 10) | |
| 771 | + | if !inOverview(r.settle(t)) { | |
| 772 | + | t.Fatalf("%s stash: swipe up did not open the overview", name) | |
| 773 | + | } | |
| 774 | + | } | |
| 775 | + | } | |
| 776 | + | ||
| 777 | + | // The stash shown over a covering fullscreen window is on screen: a swipe | |
| 778 | + | // up hides it and leaves the window fullscreen. | |
| 779 | + | func TestFourFingerSwipeUpHidesStashOverCover(t *testing.T) { | |
| 780 | + | r := startSwipe(t, nil) | |
| 781 | + | stashedThird(t, r) | |
| 782 | + | r.key(t, "s", ports.ModAlt) // hide | |
| 783 | + | r.settle(t) | |
| 784 | + | r.key(t, "f", ports.ModAlt|ports.ModShift) | |
| 785 | + | r.settleAll(t, r.outs...) | |
| 786 | + | shown := r.key(t, "s", ports.ModAlt)[0] // show over the cover | |
| 787 | + | sc, _ := r.settleAll(t, r.outs...) | |
| 788 | + | if len(sc) == 0 { | |
| 789 | + | sc = []ports.Scene{shown} | |
| 790 | + | } | |
| 791 | + | if w := windowOf(t, sc[0], 3); w.Hidden { | |
| 792 | + | t.Fatalf("setup: the stash is hidden over the cover: %+v", w) | |
| 793 | + | } | |
| 794 | + | s := r.four(t, 0, -40, 10) | |
| 795 | + | for _, w := range s.Windows { | |
| 796 | + | if w.Preview > 0 { | |
| 797 | + | t.Fatal("swipe up opened the overview with the stash over the cover") | |
| 798 | + | } | |
| 799 | + | } | |
| 800 | + | // Over a cover the stash hides without a fade: the scene is final. | |
| 801 | + | if w := windowOf(t, s, 3); !w.Hidden { | |
| 802 | + | t.Fatalf("swipe up left the stash over the cover: %+v", w) | |
| 803 | + | } | |
| 804 | + | if w := windowOf(t, s, 2); !w.Fullscreen { | |
| 805 | + | t.Fatalf("the covering window left fullscreen: %+v", w) | |
| 806 | + | } | |
| 807 | + | } | |
| 808 | + | ||
| 726 | 809 | // With nothing in the stash a swipe down changes nothing: the scene is the | |
| 727 | 810 | // same one, with no new Seq and no animation. | |
| 728 | 811 | func TestFourFingerSwipeDownEmptyStash(t *testing.T) { | |
| @@ -723,6 +723,89 @@ func TestFourFingerSwipeDownTogglesStash(t *testing.T) { | |||||
| 723 | } | 723 | } | ||
| 724 | } | 724 | } | ||
| 725 | 725 | ||||
| 726 | + | // A four-finger swipe up hides the shown stash, with the same fade as a | |||
| 727 | + | // swipe down, instead of opening the overview. | |||
| 728 | + | func TestFourFingerSwipeUpHidesShownStash(t *testing.T) { | |||
| 729 | + | r := startSwipe(t, nil) | |||
| 730 | + | stashedThird(t, r) | |||
| 731 | + | s := r.four(t, 0, -40, 10) | |||
| 732 | + | for _, w := range s.Windows { | |||
| 733 | + | if w.Preview > 0 { | |||
| 734 | + | t.Fatal("swipe up opened the overview with the stash shown") | |||
| 735 | + | } | |||
| 736 | + | } | |||
| 737 | + | if w := windowOf(t, r.frame(t, 15*time.Millisecond), 3); w.Hidden || !(w.Fade > 0 && w.Fade < 1) { | |||
| 738 | + | t.Fatalf("hiding: window 3 hidden %t, fade %v, want it leaving", w.Hidden, w.Fade) | |||
| 739 | + | } | |||
| 740 | + | s = r.settle(t) | |||
| 741 | + | if w := windowOf(t, s, 3); !w.Hidden { | |||
| 742 | + | t.Fatalf("swipe up left the stash shown: %+v", w) | |||
| 743 | + | } | |||
| 744 | + | for _, w := range s.Windows { | |||
| 745 | + | if w.Preview > 0 { | |||
| 746 | + | t.Fatal("overview open after the stash hid") | |||
| 747 | + | } | |||
| 748 | + | } | |||
| 749 | + | } | |||
| 750 | + | ||||
| 751 | + | // With the stash hidden or empty a swipe up opens the overview. | |||
| 752 | + | func TestFourFingerSwipeUpOpensOverviewWithoutShownStash(t *testing.T) { | |||
| 753 | + | inOverview := func(s ports.Scene) bool { | |||
| 754 | + | for _, w := range s.Windows { | |||
| 755 | + | if w.Preview > 0 { | |||
| 756 | + | return true | |||
| 757 | + | } | |||
| 758 | + | } | |||
| 759 | + | return false | |||
| 760 | + | } | |||
| 761 | + | for _, name := range []string{"hidden", "empty"} { | |||
| 762 | + | r := startSwipe(t, nil) | |||
| 763 | + | if name == "hidden" { | |||
| 764 | + | stashedThird(t, r) | |||
| 765 | + | r.key(t, "s", ports.ModAlt) | |||
| 766 | + | r.settle(t) | |||
| 767 | + | } else { | |||
| 768 | + | threeColumns(t, r) | |||
| 769 | + | } | |||
| 770 | + | r.four(t, 0, -40, 10) | |||
| 771 | + | if !inOverview(r.settle(t)) { | |||
| 772 | + | t.Fatalf("%s stash: swipe up did not open the overview", name) | |||
| 773 | + | } | |||
| 774 | + | } | |||
| 775 | + | } | |||
| 776 | + | ||||
| 777 | + | // The stash shown over a covering fullscreen window is on screen: a swipe | |||
| 778 | + | // up hides it and leaves the window fullscreen. | |||
| 779 | + | func TestFourFingerSwipeUpHidesStashOverCover(t *testing.T) { | |||
| 780 | + | r := startSwipe(t, nil) | |||
| 781 | + | stashedThird(t, r) | |||
| 782 | + | r.key(t, "s", ports.ModAlt) // hide | |||
| 783 | + | r.settle(t) | |||
| 784 | + | r.key(t, "f", ports.ModAlt|ports.ModShift) | |||
| 785 | + | r.settleAll(t, r.outs...) | |||
| 786 | + | shown := r.key(t, "s", ports.ModAlt)[0] // show over the cover | |||
| 787 | + | sc, _ := r.settleAll(t, r.outs...) | |||
| 788 | + | if len(sc) == 0 { | |||
| 789 | + | sc = []ports.Scene{shown} | |||
| 790 | + | } | |||
| 791 | + | if w := windowOf(t, sc[0], 3); w.Hidden { | |||
| 792 | + | t.Fatalf("setup: the stash is hidden over the cover: %+v", w) | |||
| 793 | + | } | |||
| 794 | + | s := r.four(t, 0, -40, 10) | |||
| 795 | + | for _, w := range s.Windows { | |||
| 796 | + | if w.Preview > 0 { | |||
| 797 | + | t.Fatal("swipe up opened the overview with the stash over the cover") | |||
| 798 | + | } | |||
| 799 | + | } | |||
| 800 | + | // Over a cover the stash hides without a fade: the scene is final. | |||
| 801 | + | if w := windowOf(t, s, 3); !w.Hidden { | |||
| 802 | + | t.Fatalf("swipe up left the stash over the cover: %+v", w) | |||
| 803 | + | } | |||
| 804 | + | if w := windowOf(t, s, 2); !w.Fullscreen { | |||
| 805 | + | t.Fatalf("the covering window left fullscreen: %+v", w) | |||
| 806 | + | } | |||
| 807 | + | } | |||
| 808 | + | ||||
| 726 | // With nothing in the stash a swipe down changes nothing: the scene is the | 809 | // With nothing in the stash a swipe down changes nothing: the scene is the | ||
| 727 | // same one, with no new Seq and no animation. | 810 | // same one, with no new Seq and no animation. | ||
| 728 | func TestFourFingerSwipeDownEmptyStash(t *testing.T) { | 811 | func TestFourFingerSwipeDownEmptyStash(t *testing.T) { | ||