fix(overview): keep cascade card navigation off floats and on the maximized column
- commit
- 7321c7bb5e20fe375b31134f895d4f8cea9a7aff
- author
- brice · · 2026-10-07 15:54 UTC
- parent
- 90ccc85
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +52 -19
M internal/core/overview.go +20 -19
| @@ -450,8 +450,12 @@ func (m *Monitor) overviewAcross(dx int) { | |||
| 450 | 450 | m.ov.card, m.ov.cardOf = 0, nil | |
| 451 | 451 | m.ov.selected = 0 | |
| 452 | 452 | if m.stackFront(w).kind == stackColumns && len(w.Columns) > 0 { | |
| 453 | - | first, _ := w.laneBounds(w.laneOf(w.Focus)) | |
| 454 | - | m.selectLaneColumn(w, first) | |
| 453 | + | // The front column: a maximized one keeps its cards behind. | |
| 454 | + | i := w.Focus | |
| 455 | + | if !w.hasBandCards() { | |
| 456 | + | i, _ = w.laneBounds(w.laneOf(w.Focus)) | |
| 457 | + | } | |
| 458 | + | m.selectLaneColumn(w, i) | |
| 455 | 459 | } | |
| 456 | 460 | return | |
| 457 | 461 | } | |
| @@ -972,7 +976,7 @@ func (w *Workspace) previewTilesInto(tiles []Placement) ([]Placement, int, Rect) | |||
| 972 | 976 | rects = w.colBuf | |
| 973 | 977 | span = w.Usable.W // Fixed overflow never scrolls, even with a stash pile. | |
| 974 | 978 | } | |
| 975 | - | if w.policy().wraps && len(w.Columns) > 0 && w.Columns[w.Focus].FullWidth { | |
| 979 | + | if w.hasBandCards() { | |
| 976 | 980 | // The columns the maximized one hides are cards behind it, | |
| 977 | 981 | // farthest first so the nearest paint over them. | |
| 978 | 982 | first, last := w.laneBounds(w.laneOf(w.Focus)) | |
| @@ -999,9 +1003,9 @@ func (w *Workspace) previewTilesInto(tiles []Placement) ([]Placement, int, Rect) | |||
| 999 | 1003 | r.W = w.cellWidth(i) | |
| 1000 | 1004 | if c.FullWidth { | |
| 1001 | 1005 | in := 0 | |
| 1002 | - | if i == w.Focus { | |
| 1006 | + | if i == w.Focus && w.hasBandCards() { | |
| 1003 | 1007 | // Only the focused column has cards behind it. | |
| 1004 | - | in = w.bandCardsInset(i) | |
| 1008 | + | in = 2 * w.bandCardStep() | |
| 1005 | 1009 | } | |
| 1006 | 1010 | r.X, r.W = g+in, max(w.Usable.W-2*g-2*in, 0) | |
| 1007 | 1011 | } | |
| @@ -1044,15 +1048,16 @@ func (w *Workspace) bandCardStep() int { | |||
| 1044 | 1048 | return max(1, int(math.Round(float64(w.Usable.H)*overviewPeekStep))) | |
| 1045 | 1049 | } | |
| 1046 | 1050 | ||
| 1047 | - | // bandCardsInset is how far the maximized column i is inset on each side | |
| 1048 | - | // of its band in the overview, leaving room for up to two cards peeking | |
| 1049 | - | // out on that side; 0 when it hides nothing. | |
| 1050 | - | func (w *Workspace) bandCardsInset(i int) int { | |
| 1051 | - | first, last := w.laneBounds(w.laneOf(i)) | |
| 1052 | - | if first >= last { | |
| 1053 | - | return 0 | |
| 1051 | + | // hasBandCards reports whether the focused column of a cascade is | |
| 1052 | + | // maximized and hides other columns of its band: the overview shows them | |
| 1053 | + | // as cards behind it, the maximized column inset by two card steps on | |
| 1054 | + | // each side. | |
| 1055 | + | func (w *Workspace) hasBandCards() bool { | |
| 1056 | + | if !w.policy().wraps || w.Focus < 0 || w.Focus >= len(w.Columns) || !w.Columns[w.Focus].FullWidth { | |
| 1057 | + | return false | |
| 1054 | 1058 | } | |
| 1055 | - | return 2 * w.bandCardStep() | |
| 1059 | + | first, last := w.laneBounds(w.laneOf(w.Focus)) | |
| 1060 | + | return first < last | |
| 1056 | 1061 | } | |
| 1057 | 1062 | ||
| 1058 | 1063 | // appendBandCard appends hidden column i of a cascade band as a dimmed card | |
| @@ -1374,13 +1379,9 @@ func (m *Monitor) overviewScroll(a ports.PointerAxis) (changed bool) { | |||
| 1374 | 1379 | } | |
| 1375 | 1380 | ||
| 1376 | 1381 | // bandCardsAt reports whether the selection of w is a maximized cascade | |
| 1377 | - | // column hiding cards of its band, not a stash card. | |
| 1382 | + | // column hiding cards of its band: not a stash card nor a covering float. | |
| 1378 | 1383 | 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 | + | return m.cardAt(w) < 0 && w.hasBandCards() && m.stackFront(w).kind == stackColumns | |
| 1384 | 1385 | } | |
| 1385 | 1386 | ||
| 1386 | 1387 | // overviewTurned reports whether a scroll step turned the overview from the | |
| @@ -450,8 +450,12 @@ func (m *Monitor) overviewAcross(dx int) { | |||||
| 450 | m.ov.card, m.ov.cardOf = 0, nil | 450 | m.ov.card, m.ov.cardOf = 0, nil | ||
| 451 | m.ov.selected = 0 | 451 | m.ov.selected = 0 | ||
| 452 | if m.stackFront(w).kind == stackColumns && len(w.Columns) > 0 { | 452 | if m.stackFront(w).kind == stackColumns && len(w.Columns) > 0 { | ||
| 453 | - | first, _ := w.laneBounds(w.laneOf(w.Focus)) | 453 | + | // The front column: a maximized one keeps its cards behind. |
| 454 | - | m.selectLaneColumn(w, first) | 454 | + | i := w.Focus |
| 455 | + | if !w.hasBandCards() { | |||
| 456 | + | i, _ = w.laneBounds(w.laneOf(w.Focus)) | |||
| 457 | + | } | |||
| 458 | + | m.selectLaneColumn(w, i) | |||
| 455 | } | 459 | } | ||
| 456 | return | 460 | return | ||
| 457 | } | 461 | } | ||
| @@ -972,7 +976,7 @@ func (w *Workspace) previewTilesInto(tiles []Placement) ([]Placement, int, Rect) | |||||
| 972 | rects = w.colBuf | 976 | rects = w.colBuf | ||
| 973 | span = w.Usable.W // Fixed overflow never scrolls, even with a stash pile. | 977 | span = w.Usable.W // Fixed overflow never scrolls, even with a stash pile. | ||
| 974 | } | 978 | } | ||
| 975 | - | if w.policy().wraps && len(w.Columns) > 0 && w.Columns[w.Focus].FullWidth { | 979 | + | if w.hasBandCards() { |
| 976 | // The columns the maximized one hides are cards behind it, | 980 | // The columns the maximized one hides are cards behind it, | ||
| 977 | // farthest first so the nearest paint over them. | 981 | // farthest first so the nearest paint over them. | ||
| 978 | first, last := w.laneBounds(w.laneOf(w.Focus)) | 982 | first, last := w.laneBounds(w.laneOf(w.Focus)) | ||
| @@ -999,9 +1003,9 @@ func (w *Workspace) previewTilesInto(tiles []Placement) ([]Placement, int, Rect) | |||||
| 999 | r.W = w.cellWidth(i) | 1003 | r.W = w.cellWidth(i) | ||
| 1000 | if c.FullWidth { | 1004 | if c.FullWidth { | ||
| 1001 | in := 0 | 1005 | in := 0 | ||
| 1002 | - | if i == w.Focus { | 1006 | + | if i == w.Focus && w.hasBandCards() { |
| 1003 | // Only the focused column has cards behind it. | 1007 | // Only the focused column has cards behind it. | ||
| 1004 | - | in = w.bandCardsInset(i) | 1008 | + | in = 2 * w.bandCardStep() |
| 1005 | } | 1009 | } | ||
| 1006 | r.X, r.W = g+in, max(w.Usable.W-2*g-2*in, 0) | 1010 | r.X, r.W = g+in, max(w.Usable.W-2*g-2*in, 0) | ||
| 1007 | } | 1011 | } | ||
| @@ -1044,15 +1048,16 @@ func (w *Workspace) bandCardStep() int { | |||||
| 1044 | return max(1, int(math.Round(float64(w.Usable.H)*overviewPeekStep))) | 1048 | return max(1, int(math.Round(float64(w.Usable.H)*overviewPeekStep))) | ||
| 1045 | } | 1049 | } | ||
| 1046 | 1050 | ||||
| 1047 | - | // bandCardsInset is how far the maximized column i is inset on each side | 1051 | + | // hasBandCards reports whether the focused column of a cascade is |
| 1048 | - | // of its band in the overview, leaving room for up to two cards peeking | 1052 | + | // maximized and hides other columns of its band: the overview shows them |
| 1049 | - | // out on that side; 0 when it hides nothing. | 1053 | + | // as cards behind it, the maximized column inset by two card steps on |
| 1050 | - | func (w *Workspace) bandCardsInset(i int) int { | 1054 | + | // each side. |
| 1051 | - | first, last := w.laneBounds(w.laneOf(i)) | 1055 | + | func (w *Workspace) hasBandCards() bool { |
| 1052 | - | if first >= last { | 1056 | + | if !w.policy().wraps || w.Focus < 0 || w.Focus >= len(w.Columns) || !w.Columns[w.Focus].FullWidth { |
| 1053 | - | return 0 | 1057 | + | return false |
| 1054 | } | 1058 | } | ||
| 1055 | - | return 2 * w.bandCardStep() | 1059 | + | first, last := w.laneBounds(w.laneOf(w.Focus)) |
| 1060 | + | return first < last | |||
| 1056 | } | 1061 | } | ||
| 1057 | 1062 | ||||
| 1058 | // appendBandCard appends hidden column i of a cascade band as a dimmed card | 1063 | // appendBandCard appends hidden column i of a cascade band as a dimmed card | ||
| @@ -1374,13 +1379,9 @@ func (m *Monitor) overviewScroll(a ports.PointerAxis) (changed bool) { | |||||
| 1374 | } | 1379 | } | ||
| 1375 | 1380 | ||||
| 1376 | // bandCardsAt reports whether the selection of w is a maximized cascade | 1381 | // bandCardsAt reports whether the selection of w is a maximized cascade | ||
| 1377 | - | // column hiding cards of its band, not a stash card. | 1382 | + | // column hiding cards of its band: not a stash card nor a covering float. |
| 1378 | func (m *Monitor) bandCardsAt(w *Workspace) bool { | 1383 | func (m *Monitor) bandCardsAt(w *Workspace) bool { | ||
| 1379 | - | if m.cardAt(w) >= 0 || len(w.Columns) == 0 || !w.Columns[w.Focus].FullWidth { | 1384 | + | return m.cardAt(w) < 0 && w.hasBandCards() && m.stackFront(w).kind == stackColumns |
| 1380 | - | return false | |||
| 1381 | - | } | |||
| 1382 | - | first, last := w.laneBounds(w.laneOf(w.Focus)) | |||
| 1383 | - | return first < last | |||
| 1384 | } | 1385 | } | ||
| 1385 | 1386 | ||||
| 1386 | // overviewTurned reports whether a scroll step turned the overview from the | 1387 | // overviewTurned reports whether a scroll step turned the overview from the | ||
M internal/core/overview_cascade_test.go +32 -0
| @@ -663,6 +663,38 @@ func TestOverviewCascadeThreeFingerMaximizedCards(t *testing.T) { | |||
| 663 | 663 | if m.Current() != m.Workspaces[2] { | |
| 664 | 664 | t.Fatalf("swipe right past the band: workspace %d", indexOf(m.Workspaces, m.Current())) | |
| 665 | 665 | } | |
| 666 | + | ||
| 667 | + | // A covering float in front: the swipe changes workspace, as without | |
| 668 | + | // a maximized column. | |
| 669 | + | m.CancelOverview() | |
| 670 | + | w.AddFloating(90, 300, 200) | |
| 671 | + | m.ToggleOverview() | |
| 672 | + | if f := m.stackFront(w); f.kind != stackFloat || !w.Columns[w.Focus].FullWidth { | |
| 673 | + | t.Fatalf("setup: front %+v", f) | |
| 674 | + | } | |
| 675 | + | swipe(-40) | |
| 676 | + | if m.Current() != m.Workspaces[0] { | |
| 677 | + | t.Fatalf("swipe left over a float: workspace %d", indexOf(m.Workspaces, m.Current())) | |
| 678 | + | } | |
| 679 | + | } | |
| 680 | + | ||
| 681 | + | // Right from a stash card lands on the maximized column, which keeps its | |
| 682 | + | // maximization, not on the first card of its band. | |
| 683 | + | func TestOverviewCascadeStashToMaximized(t *testing.T) { | |
| 684 | + | m := cascadeOverview() | |
| 685 | + | withStash(m, 1) | |
| 686 | + | w := m.Current() | |
| 687 | + | w.FocusID(9) | |
| 688 | + | w.ToggleFullWidth() | |
| 689 | + | m.ToggleOverview() | |
| 690 | + | m.selectCard(w, w.stashAt) // As a click on the pile does. | |
| 691 | + | if m.cardAt(w) < 0 { | |
| 692 | + | t.Fatalf("setup: no stash card, target %d", m.overviewTarget()) | |
| 693 | + | } | |
| 694 | + | m.OverviewMove(1, 0) | |
| 695 | + | if m.overviewTarget() != 9 || !w.Columns[w.Focus].FullWidth || len(w.maximized) == 0 || w.maximized[0] != 9 { | |
| 696 | + | t.Fatalf("target %d, maximized %v; want 9", m.overviewTarget(), w.maximized) | |
| 697 | + | } | |
| 666 | 698 | } | |
| 667 | 699 | ||
| 668 | 700 | // T8: a named boundary of a cascade overview is a vertical rule strictly | |
| @@ -663,6 +663,38 @@ func TestOverviewCascadeThreeFingerMaximizedCards(t *testing.T) { | |||||
| 663 | if m.Current() != m.Workspaces[2] { | 663 | if m.Current() != m.Workspaces[2] { | ||
| 664 | t.Fatalf("swipe right past the band: workspace %d", indexOf(m.Workspaces, m.Current())) | 664 | t.Fatalf("swipe right past the band: workspace %d", indexOf(m.Workspaces, m.Current())) | ||
| 665 | } | 665 | } | ||
| 666 | + | ||||
| 667 | + | // A covering float in front: the swipe changes workspace, as without | |||
| 668 | + | // a maximized column. | |||
| 669 | + | m.CancelOverview() | |||
| 670 | + | w.AddFloating(90, 300, 200) | |||
| 671 | + | m.ToggleOverview() | |||
| 672 | + | if f := m.stackFront(w); f.kind != stackFloat || !w.Columns[w.Focus].FullWidth { | |||
| 673 | + | t.Fatalf("setup: front %+v", f) | |||
| 674 | + | } | |||
| 675 | + | swipe(-40) | |||
| 676 | + | if m.Current() != m.Workspaces[0] { | |||
| 677 | + | t.Fatalf("swipe left over a float: workspace %d", indexOf(m.Workspaces, m.Current())) | |||
| 678 | + | } | |||
| 679 | + | } | |||
| 680 | + | ||||
| 681 | + | // Right from a stash card lands on the maximized column, which keeps its | |||
| 682 | + | // maximization, not on the first card of its band. | |||
| 683 | + | func TestOverviewCascadeStashToMaximized(t *testing.T) { | |||
| 684 | + | m := cascadeOverview() | |||
| 685 | + | withStash(m, 1) | |||
| 686 | + | w := m.Current() | |||
| 687 | + | w.FocusID(9) | |||
| 688 | + | w.ToggleFullWidth() | |||
| 689 | + | m.ToggleOverview() | |||
| 690 | + | m.selectCard(w, w.stashAt) // As a click on the pile does. | |||
| 691 | + | if m.cardAt(w) < 0 { | |||
| 692 | + | t.Fatalf("setup: no stash card, target %d", m.overviewTarget()) | |||
| 693 | + | } | |||
| 694 | + | m.OverviewMove(1, 0) | |||
| 695 | + | if m.overviewTarget() != 9 || !w.Columns[w.Focus].FullWidth || len(w.maximized) == 0 || w.maximized[0] != 9 { | |||
| 696 | + | t.Fatalf("target %d, maximized %v; want 9", m.overviewTarget(), w.maximized) | |||
| 697 | + | } | |||
| 666 | } | 698 | } | ||
| 667 | 699 | ||||
| 668 | // T8: a named boundary of a cascade overview is a vertical rule strictly | 700 | // T8: a named boundary of a cascade overview is a vertical rule strictly | ||