fix(overview): let three-finger swipes select cards behind a maximized cascade column
- commit
- 9376c1c480308894c7484b74ebec02189aa4aed8
- author
- brice · · 2026-10-07 15:23 UTC
- parent
- 4a1ef6d
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
3 files · +49 -3
M docs/config.md +1 -1
| @@ -198,7 +198,7 @@ Three-finger vertical swipes scroll bands; horizontal swipes switch workspaces. | |||
| 198 | 198 | ||
| 199 | 199 | In the [overview](#overview), the selected band is centred at the usual overview zoom, whatever the band count. The other bands of the workspace sit above and below it, dimmed, and may run off screen. Neighbor workspaces sit left and right, fully dimmed, with their selected row level with the centred band. The stash pile stays left of the band, inside the workspace's slot. A maximized column shows the columns it hides as dimmed cards behind it, peeking out on their side of the band: the nearest one on each side, then the next ones stacked at the band's edge. Selecting a card (left/right, Return or a click on a visible card) moves the maximization to it, and Escape gives it back. | |
| 200 | 200 | ||
| 201 | - | In the overview, up/down move to the previous or next band and stop at the first and last; with a stash card selected they browse the pile. Left/right move along the band; past its first column they enter the stash, or go to the previous workspace when there is none; past its last column they go to the next workspace (with the overview closed, focus crosses into the next band instead). Covering floats fan left and right of the band: left/right traverse them before entering the stash or changing workspace, and up/down do nothing on a float. `h`/`j`/`k`/`l` and the focus binds follow the arrows; `focus-workspace-prev/next` always change workspace, except over a selected stash card, where they browse the pile. Three-finger swipes follow the fingers: vertical moves one band (or one stash card) and horizontal one workspace per gesture, even over a stash card. A vertical wheel or continuous scroll changes workspace, except over a selected stash card, where it browses the pile; a horizontal one moves along the band. | |
| 201 | + | In the overview, up/down move to the previous or next band and stop at the first and last; with a stash card selected they browse the pile. Left/right move along the band; past its first column they enter the stash, or go to the previous workspace when there is none; past its last column they go to the next workspace (with the overview closed, focus crosses into the next band instead). Covering floats fan left and right of the band: left/right traverse them before entering the stash or changing workspace, and up/down do nothing on a float. `h`/`j`/`k`/`l` and the focus binds follow the arrows; `focus-workspace-prev/next` always change workspace, except over a selected stash card, where they browse the pile. Three-finger swipes follow the fingers: vertical moves one band (or one stash card) and horizontal one workspace per gesture, even over a stash card. Over a maximized column that hides cards, horizontal moves one step per gesture like left/right: the next card, then past the band's edge the stash or a workspace. A vertical wheel or continuous scroll changes workspace, except over a selected stash card, where it browses the pile; a horizontal one moves along the band. | |
| 202 | 202 | ||
| 203 | 203 | Cascade supports dropping onto tiles, but not gap insertion or edge scrolling during a drag. | |
| 204 | 204 | ||
| @@ -198,7 +198,7 @@ Three-finger vertical swipes scroll bands; horizontal swipes switch workspaces. | |||||
| 198 | 198 | ||||
| 199 | In the [overview](#overview), the selected band is centred at the usual overview zoom, whatever the band count. The other bands of the workspace sit above and below it, dimmed, and may run off screen. Neighbor workspaces sit left and right, fully dimmed, with their selected row level with the centred band. The stash pile stays left of the band, inside the workspace's slot. A maximized column shows the columns it hides as dimmed cards behind it, peeking out on their side of the band: the nearest one on each side, then the next ones stacked at the band's edge. Selecting a card (left/right, Return or a click on a visible card) moves the maximization to it, and Escape gives it back. | 199 | In the [overview](#overview), the selected band is centred at the usual overview zoom, whatever the band count. The other bands of the workspace sit above and below it, dimmed, and may run off screen. Neighbor workspaces sit left and right, fully dimmed, with their selected row level with the centred band. The stash pile stays left of the band, inside the workspace's slot. A maximized column shows the columns it hides as dimmed cards behind it, peeking out on their side of the band: the nearest one on each side, then the next ones stacked at the band's edge. Selecting a card (left/right, Return or a click on a visible card) moves the maximization to it, and Escape gives it back. | ||
| 200 | 200 | ||||
| 201 | - | In the overview, up/down move to the previous or next band and stop at the first and last; with a stash card selected they browse the pile. Left/right move along the band; past its first column they enter the stash, or go to the previous workspace when there is none; past its last column they go to the next workspace (with the overview closed, focus crosses into the next band instead). Covering floats fan left and right of the band: left/right traverse them before entering the stash or changing workspace, and up/down do nothing on a float. `h`/`j`/`k`/`l` and the focus binds follow the arrows; `focus-workspace-prev/next` always change workspace, except over a selected stash card, where they browse the pile. Three-finger swipes follow the fingers: vertical moves one band (or one stash card) and horizontal one workspace per gesture, even over a stash card. A vertical wheel or continuous scroll changes workspace, except over a selected stash card, where it browses the pile; a horizontal one moves along the band. | 201 | + | In the overview, up/down move to the previous or next band and stop at the first and last; with a stash card selected they browse the pile. Left/right move along the band; past its first column they enter the stash, or go to the previous workspace when there is none; past its last column they go to the next workspace (with the overview closed, focus crosses into the next band instead). Covering floats fan left and right of the band: left/right traverse them before entering the stash or changing workspace, and up/down do nothing on a float. `h`/`j`/`k`/`l` and the focus binds follow the arrows; `focus-workspace-prev/next` always change workspace, except over a selected stash card, where they browse the pile. Three-finger swipes follow the fingers: vertical moves one band (or one stash card) and horizontal one workspace per gesture, even over a stash card. Over a maximized column that hides cards, horizontal moves one step per gesture like left/right: the next card, then past the band's edge the stash or a workspace. A vertical wheel or continuous scroll changes workspace, except over a selected stash card, where it browses the pile; a horizontal one moves along the band. |
| 202 | 202 | ||||
| 203 | Cascade supports dropping onto tiles, but not gap insertion or edge scrolling during a drag. | 203 | Cascade supports dropping onto tiles, but not gap insertion or edge scrolling during a drag. | ||
| 204 | 204 | ||||
M internal/core/overview.go +18 -2
| @@ -1346,8 +1346,14 @@ func (m *Monitor) overviewScroll(a ports.PointerAxis) (changed bool) { | |||
| 1346 | 1346 | case math.Abs(m.ov.scrollX) >= stepX: | |
| 1347 | 1347 | d := sign(m.ov.scrollX) | |
| 1348 | 1348 | if cascade && finger { | |
| 1349 | - | // One workspace per gesture, even over a stash card. | |
| 1350 | - | m.overviewStep(d) | |
| 1349 | + | // One step per gesture: a card behind a maximized | |
| 1350 | + | // column first, like the arrows, else one workspace, | |
| 1351 | + | // even over a stash card. | |
| 1352 | + | if m.bandCardsAt(m.Current()) { | |
| 1353 | + | m.OverviewMove(d, 0) | |
| 1354 | + | } else { | |
| 1355 | + | m.overviewStep(d) | |
| 1356 | + | } | |
| 1351 | 1357 | m.ov.scrollX, m.ov.scrollY, m.ov.scrolled = 0, 0, true | |
| 1352 | 1358 | return true | |
| 1353 | 1359 | } | |
| @@ -1367,6 +1373,16 @@ func (m *Monitor) overviewScroll(a ports.PointerAxis) (changed bool) { | |||
| 1367 | 1373 | } | |
| 1368 | 1374 | } | |
| 1369 | 1375 | ||
| 1376 | + | // bandCardsAt reports whether the selection of w is a maximized cascade | |
| 1377 | + | // column hiding cards of its band, not a stash card. | |
| 1378 | + | func (m *Monitor) bandCardsAt(w *Workspace) bool { | |
| 1379 | + | if m.cardAt(w) >= 0 || len(w.Columns) == 0 || !w.Columns[w.Focus].FullWidth { | |
| 1380 | + | return false | |
| 1381 | + | } | |
| 1382 | + | first, last := w.laneBounds(w.laneOf(w.Focus)) | |
| 1383 | + | return first < last | |
| 1384 | + | } | |
| 1385 | + | ||
| 1370 | 1386 | // overviewTurned reports whether a scroll step turned the overview from the | |
| 1371 | 1387 | // axis before. A finger scroll then waits for the fingers to lift, so it | |
| 1372 | 1388 | // cannot bounce back. | |
| @@ -1346,8 +1346,14 @@ func (m *Monitor) overviewScroll(a ports.PointerAxis) (changed bool) { | |||||
| 1346 | case math.Abs(m.ov.scrollX) >= stepX: | 1346 | case math.Abs(m.ov.scrollX) >= stepX: | ||
| 1347 | d := sign(m.ov.scrollX) | 1347 | d := sign(m.ov.scrollX) | ||
| 1348 | if cascade && finger { | 1348 | if cascade && finger { | ||
| 1349 | - | // One workspace per gesture, even over a stash card. | 1349 | + | // One step per gesture: a card behind a maximized |
| 1350 | - | m.overviewStep(d) | 1350 | + | // column first, like the arrows, else one workspace, |
| 1351 | + | // even over a stash card. | |||
| 1352 | + | if m.bandCardsAt(m.Current()) { | |||
| 1353 | + | m.OverviewMove(d, 0) | |||
| 1354 | + | } else { | |||
| 1355 | + | m.overviewStep(d) | |||
| 1356 | + | } | |||
| 1351 | m.ov.scrollX, m.ov.scrollY, m.ov.scrolled = 0, 0, true | 1357 | m.ov.scrollX, m.ov.scrollY, m.ov.scrolled = 0, 0, true | ||
| 1352 | return true | 1358 | return true | ||
| 1353 | } | 1359 | } | ||
| @@ -1367,6 +1373,16 @@ func (m *Monitor) overviewScroll(a ports.PointerAxis) (changed bool) { | |||||
| 1367 | } | 1373 | } | ||
| 1368 | } | 1374 | } | ||
| 1369 | 1375 | ||||
| 1376 | + | // bandCardsAt reports whether the selection of w is a maximized cascade | |||
| 1377 | + | // column hiding cards of its band, not a stash card. | |||
| 1378 | + | func (m *Monitor) bandCardsAt(w *Workspace) bool { | |||
| 1379 | + | if m.cardAt(w) >= 0 || len(w.Columns) == 0 || !w.Columns[w.Focus].FullWidth { | |||
| 1380 | + | return false | |||
| 1381 | + | } | |||
| 1382 | + | first, last := w.laneBounds(w.laneOf(w.Focus)) | |||
| 1383 | + | return first < last | |||
| 1384 | + | } | |||
| 1385 | + | ||||
| 1370 | // overviewTurned reports whether a scroll step turned the overview from the | 1386 | // overviewTurned reports whether a scroll step turned the overview from the | ||
| 1371 | // axis before. A finger scroll then waits for the fingers to lift, so it | 1387 | // axis before. A finger scroll then waits for the fingers to lift, so it | ||
| 1372 | // cannot bounce back. | 1388 | // cannot bounce back. | ||
M internal/core/overview_cascade_test.go +30 -0
| @@ -635,6 +635,36 @@ func TestOverviewCascadeThreeFinger(t *testing.T) { | |||
| 635 | 635 | } | |
| 636 | 636 | } | |
| 637 | 637 | ||
| 638 | + | // Three fingers sideways walk the cards behind a maximized column one per | |
| 639 | + | // gesture, like the arrows, then change workspace past the band's edge. | |
| 640 | + | func TestOverviewCascadeThreeFingerMaximizedCards(t *testing.T) { | |
| 641 | + | c, m := cascadeCore(t) | |
| 642 | + | w := m.Current() | |
| 643 | + | w.ToggleFullWidth() // Column 2 hides 3, the other column of its band. | |
| 644 | + | m.ToggleOverview() | |
| 645 | + | swipe := func(dx float64) { | |
| 646 | + | t.Helper() | |
| 647 | + | c.swipeBegin(ports.SwipeBegin{Fingers: 3}) | |
| 648 | + | for range 4 { | |
| 649 | + | c.swipeUpdate(ports.SwipeUpdate{DX: dx}) | |
| 650 | + | } | |
| 651 | + | c.swipeEnd(ports.SwipeEnd{}) | |
| 652 | + | } | |
| 653 | + | swipe(40) | |
| 654 | + | if m.Current() != w || m.overviewTarget() != 3 || !w.Columns[w.Focus].FullWidth { | |
| 655 | + | t.Fatalf("swipe right: workspace %d, target %d, want maximized 3", indexOf(m.Workspaces, m.Current()), m.overviewTarget()) | |
| 656 | + | } | |
| 657 | + | swipe(-40) | |
| 658 | + | if m.Current() != w || m.overviewTarget() != 2 || !w.Columns[w.Focus].FullWidth { | |
| 659 | + | t.Fatalf("swipe left: workspace %d, target %d, want maximized 2", indexOf(m.Workspaces, m.Current()), m.overviewTarget()) | |
| 660 | + | } | |
| 661 | + | swipe(40) | |
| 662 | + | swipe(40) | |
| 663 | + | if m.Current() != m.Workspaces[2] { | |
| 664 | + | t.Fatalf("swipe right past the band: workspace %d", indexOf(m.Workspaces, m.Current())) | |
| 665 | + | } | |
| 666 | + | } | |
| 667 | + | ||
| 638 | 668 | // T8: a named boundary of a cascade overview is a vertical rule strictly | |
| 639 | 669 | // between the two slots. | |
| 640 | 670 | func TestOverviewCascadeNamedDivider(t *testing.T) { | |
| @@ -635,6 +635,36 @@ func TestOverviewCascadeThreeFinger(t *testing.T) { | |||||
| 635 | } | 635 | } | ||
| 636 | } | 636 | } | ||
| 637 | 637 | ||||
| 638 | + | // Three fingers sideways walk the cards behind a maximized column one per | |||
| 639 | + | // gesture, like the arrows, then change workspace past the band's edge. | |||
| 640 | + | func TestOverviewCascadeThreeFingerMaximizedCards(t *testing.T) { | |||
| 641 | + | c, m := cascadeCore(t) | |||
| 642 | + | w := m.Current() | |||
| 643 | + | w.ToggleFullWidth() // Column 2 hides 3, the other column of its band. | |||
| 644 | + | m.ToggleOverview() | |||
| 645 | + | swipe := func(dx float64) { | |||
| 646 | + | t.Helper() | |||
| 647 | + | c.swipeBegin(ports.SwipeBegin{Fingers: 3}) | |||
| 648 | + | for range 4 { | |||
| 649 | + | c.swipeUpdate(ports.SwipeUpdate{DX: dx}) | |||
| 650 | + | } | |||
| 651 | + | c.swipeEnd(ports.SwipeEnd{}) | |||
| 652 | + | } | |||
| 653 | + | swipe(40) | |||
| 654 | + | if m.Current() != w || m.overviewTarget() != 3 || !w.Columns[w.Focus].FullWidth { | |||
| 655 | + | t.Fatalf("swipe right: workspace %d, target %d, want maximized 3", indexOf(m.Workspaces, m.Current()), m.overviewTarget()) | |||
| 656 | + | } | |||
| 657 | + | swipe(-40) | |||
| 658 | + | if m.Current() != w || m.overviewTarget() != 2 || !w.Columns[w.Focus].FullWidth { | |||
| 659 | + | t.Fatalf("swipe left: workspace %d, target %d, want maximized 2", indexOf(m.Workspaces, m.Current()), m.overviewTarget()) | |||
| 660 | + | } | |||
| 661 | + | swipe(40) | |||
| 662 | + | swipe(40) | |||
| 663 | + | if m.Current() != m.Workspaces[2] { | |||
| 664 | + | t.Fatalf("swipe right past the band: workspace %d", indexOf(m.Workspaces, m.Current())) | |||
| 665 | + | } | |||
| 666 | + | } | |||
| 667 | + | ||||
| 638 | // T8: a named boundary of a cascade overview is a vertical rule strictly | 668 | // T8: a named boundary of a cascade overview is a vertical rule strictly | ||
| 639 | // between the two slots. | 669 | // between the two slots. | ||
| 640 | func TestOverviewCascadeNamedDivider(t *testing.T) { | 670 | func TestOverviewCascadeNamedDivider(t *testing.T) { | ||