test(overview): cover side panels, partial bands with gaps and maximized cascade edges
- commit
- e6e645bfce2f2f0df49155ea351d6e8beafc0908
- author
- brice · · 2026-10-07 16:02 UTC
- parent
- 9e14d33
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
3 files · +145 -0
M internal/core/cascade_test.go +53 -0
| @@ -3,6 +3,8 @@ package core | |||
| 3 | 3 | import ( | |
| 4 | 4 | "slices" | |
| 5 | 5 | "testing" | |
| 6 | + | ||
| 7 | + | "github.com/bnema/neferwl/internal/ports" | |
| 6 | 8 | ) | |
| 7 | 9 | ||
| 8 | 10 | func cascadeWorkspace(n int) *Workspace { | |
| @@ -35,6 +37,57 @@ func TestCascadeBandWidthWithGaps(t *testing.T) { | |||
| 35 | 37 | } | |
| 36 | 38 | } | |
| 37 | 39 | ||
| 40 | + | // A partial last band after a full one shares the band width with gaps: | |
| 41 | + | // each band divides by its own column count. | |
| 42 | + | func TestCascadePartialLastBandWithGaps(t *testing.T) { | |
| 43 | + | w := &Workspace{Overflow: OverflowCascade, MaxColumns: 3} | |
| 44 | + | w.SetOutput(1000, 600) | |
| 45 | + | w.SetGaps(8) | |
| 46 | + | for id := WindowID(1); id <= 5; id++ { | |
| 47 | + | w.AddWindow(id) | |
| 48 | + | } | |
| 49 | + | third, half := (1000-4*8)/3, (1000-3*8)/2 | |
| 50 | + | rects := w.columnRectsFor(false) | |
| 51 | + | for i, want := range []struct{ x, w int }{ | |
| 52 | + | {8, third}, {8 + third + 8, third}, {8 + 2*(third+8), third}, | |
| 53 | + | {8, half}, {8 + half + 8, half}, | |
| 54 | + | } { | |
| 55 | + | if r := rects[i]; r.X != want.x || r.W != want.w { | |
| 56 | + | t.Fatalf("column %d rect %v, want X %d W %d", i, r, want.x, want.w) | |
| 57 | + | } | |
| 58 | + | } | |
| 59 | + | if rects[3].Y-rects[0].Y != 600 { | |
| 60 | + | t.Fatalf("band spacing %d, want 600", rects[3].Y-rects[0].Y) | |
| 61 | + | } | |
| 62 | + | } | |
| 63 | + | ||
| 64 | + | // A side panel moves and narrows every band: cells start at the usable | |
| 65 | + | // area's X and share its width. | |
| 66 | + | func TestCascadeSidePanel(t *testing.T) { | |
| 67 | + | w := &Workspace{Overflow: OverflowCascade, MaxColumns: 2} | |
| 68 | + | w.SetOutput(1000, 600) | |
| 69 | + | w.SetUsable(Rect{X: 100, W: 900, H: 600}) | |
| 70 | + | for id := WindowID(1); id <= 3; id++ { | |
| 71 | + | w.AddWindow(id) | |
| 72 | + | } | |
| 73 | + | rects := w.columnRectsFor(false) | |
| 74 | + | for i, want := range []Rect{ | |
| 75 | + | {X: 100, Y: 0, W: 450, H: 600}, | |
| 76 | + | {X: 550, Y: 0, W: 450, H: 600}, | |
| 77 | + | {X: 100, Y: 600, W: 900, H: 600}, | |
| 78 | + | } { | |
| 79 | + | if rects[i].X != want.X || rects[i].W != want.W || rects[i].Y-rects[0].Y != want.Y { | |
| 80 | + | t.Fatalf("column %d rect %v, want %v", i, rects[i], want) | |
| 81 | + | } | |
| 82 | + | } | |
| 83 | + | // The panel edge at x=100 is no shared line: column 1 has only its | |
| 84 | + | // right neighbor. | |
| 85 | + | w.FocusID(1) | |
| 86 | + | if p := previewOf(t, w.Layout(), 1); p.Neighbors != ports.SideRight { | |
| 87 | + | t.Fatalf("neighbors %04b, want right only", p.Neighbors) | |
| 88 | + | } | |
| 89 | + | } | |
| 90 | + | ||
| 38 | 91 | // Each band's columns share its width equally, as in fixed overflow: a | |
| 39 | 92 | // lone column fills it, a full band of three has thirds. | |
| 40 | 93 | func TestCascadeBandsAndReveal(t *testing.T) { | |
| @@ -3,6 +3,8 @@ package core | |||||
| 3 | import ( | 3 | import ( | ||
| 4 | "slices" | 4 | "slices" | ||
| 5 | "testing" | 5 | "testing" | ||
| 6 | + | ||||
| 7 | + | "github.com/bnema/neferwl/internal/ports" | |||
| 6 | ) | 8 | ) | ||
| 7 | 9 | ||||
| 8 | func cascadeWorkspace(n int) *Workspace { | 10 | func cascadeWorkspace(n int) *Workspace { | ||
| @@ -35,6 +37,57 @@ func TestCascadeBandWidthWithGaps(t *testing.T) { | |||||
| 35 | } | 37 | } | ||
| 36 | } | 38 | } | ||
| 37 | 39 | ||||
| 40 | + | // A partial last band after a full one shares the band width with gaps: | |||
| 41 | + | // each band divides by its own column count. | |||
| 42 | + | func TestCascadePartialLastBandWithGaps(t *testing.T) { | |||
| 43 | + | w := &Workspace{Overflow: OverflowCascade, MaxColumns: 3} | |||
| 44 | + | w.SetOutput(1000, 600) | |||
| 45 | + | w.SetGaps(8) | |||
| 46 | + | for id := WindowID(1); id <= 5; id++ { | |||
| 47 | + | w.AddWindow(id) | |||
| 48 | + | } | |||
| 49 | + | third, half := (1000-4*8)/3, (1000-3*8)/2 | |||
| 50 | + | rects := w.columnRectsFor(false) | |||
| 51 | + | for i, want := range []struct{ x, w int }{ | |||
| 52 | + | {8, third}, {8 + third + 8, third}, {8 + 2*(third+8), third}, | |||
| 53 | + | {8, half}, {8 + half + 8, half}, | |||
| 54 | + | } { | |||
| 55 | + | if r := rects[i]; r.X != want.x || r.W != want.w { | |||
| 56 | + | t.Fatalf("column %d rect %v, want X %d W %d", i, r, want.x, want.w) | |||
| 57 | + | } | |||
| 58 | + | } | |||
| 59 | + | if rects[3].Y-rects[0].Y != 600 { | |||
| 60 | + | t.Fatalf("band spacing %d, want 600", rects[3].Y-rects[0].Y) | |||
| 61 | + | } | |||
| 62 | + | } | |||
| 63 | + | ||||
| 64 | + | // A side panel moves and narrows every band: cells start at the usable | |||
| 65 | + | // area's X and share its width. | |||
| 66 | + | func TestCascadeSidePanel(t *testing.T) { | |||
| 67 | + | w := &Workspace{Overflow: OverflowCascade, MaxColumns: 2} | |||
| 68 | + | w.SetOutput(1000, 600) | |||
| 69 | + | w.SetUsable(Rect{X: 100, W: 900, H: 600}) | |||
| 70 | + | for id := WindowID(1); id <= 3; id++ { | |||
| 71 | + | w.AddWindow(id) | |||
| 72 | + | } | |||
| 73 | + | rects := w.columnRectsFor(false) | |||
| 74 | + | for i, want := range []Rect{ | |||
| 75 | + | {X: 100, Y: 0, W: 450, H: 600}, | |||
| 76 | + | {X: 550, Y: 0, W: 450, H: 600}, | |||
| 77 | + | {X: 100, Y: 600, W: 900, H: 600}, | |||
| 78 | + | } { | |||
| 79 | + | if rects[i].X != want.X || rects[i].W != want.W || rects[i].Y-rects[0].Y != want.Y { | |||
| 80 | + | t.Fatalf("column %d rect %v, want %v", i, rects[i], want) | |||
| 81 | + | } | |||
| 82 | + | } | |||
| 83 | + | // The panel edge at x=100 is no shared line: column 1 has only its | |||
| 84 | + | // right neighbor. | |||
| 85 | + | w.FocusID(1) | |||
| 86 | + | if p := previewOf(t, w.Layout(), 1); p.Neighbors != ports.SideRight { | |||
| 87 | + | t.Fatalf("neighbors %04b, want right only", p.Neighbors) | |||
| 88 | + | } | |||
| 89 | + | } | |||
| 90 | + | ||||
| 38 | // Each band's columns share its width equally, as in fixed overflow: a | 91 | // Each band's columns share its width equally, as in fixed overflow: a | ||
| 39 | // lone column fills it, a full band of three has thirds. | 92 | // lone column fills it, a full band of three has thirds. | ||
| 40 | func TestCascadeBandsAndReveal(t *testing.T) { | 93 | func TestCascadeBandsAndReveal(t *testing.T) { | ||
M internal/core/overview_cascade_test.go +71 -0
| @@ -678,6 +678,77 @@ func TestOverviewCascadeThreeFingerMaximizedCards(t *testing.T) { | |||
| 678 | 678 | } | |
| 679 | 679 | } | |
| 680 | 680 | ||
| 681 | + | // Left from the first column of a band whose maximized column hides it: | |
| 682 | + | // the arrows and three fingers walk the cards, then enter the stash, or | |
| 683 | + | // change workspace without one. | |
| 684 | + | func TestOverviewCascadeMaximizedLeftEdge(t *testing.T) { | |
| 685 | + | for _, stash := range []bool{false, true} { | |
| 686 | + | c, m := cascadeCore(t) | |
| 687 | + | w := m.Current() | |
| 688 | + | if stash { | |
| 689 | + | w.AddWindow(40) | |
| 690 | + | w.FocusID(40) | |
| 691 | + | w.ToggleWindowStash() | |
| 692 | + | } | |
| 693 | + | w.FocusID(3) // Band {2, 3}: 3 hides 2. | |
| 694 | + | w.ToggleFullWidth() | |
| 695 | + | m.ToggleOverview() | |
| 696 | + | swipe := func() { | |
| 697 | + | c.swipeBegin(ports.SwipeBegin{Fingers: 3}) | |
| 698 | + | for range 4 { | |
| 699 | + | c.swipeUpdate(ports.SwipeUpdate{DX: -40}) | |
| 700 | + | } | |
| 701 | + | c.swipeEnd(ports.SwipeEnd{}) | |
| 702 | + | } | |
| 703 | + | swipe() | |
| 704 | + | if m.Current() != w || m.overviewTarget() != 2 || !w.Columns[w.Focus].FullWidth { | |
| 705 | + | t.Fatalf("stash %v: first swipe: target %d", stash, m.overviewTarget()) | |
| 706 | + | } | |
| 707 | + | swipe() | |
| 708 | + | if stash { | |
| 709 | + | if m.Current() != w || m.cardAt(w) < 0 { | |
| 710 | + | t.Fatalf("past the band: workspace %d, card %d; want the stash", indexOf(m.Workspaces, m.Current()), m.cardAt(w)) | |
| 711 | + | } | |
| 712 | + | } else if m.Current() != m.Workspaces[0] { | |
| 713 | + | t.Fatalf("past the band: workspace %d, want 0", indexOf(m.Workspaces, m.Current())) | |
| 714 | + | } | |
| 715 | + | } | |
| 716 | + | } | |
| 717 | + | ||
| 718 | + | // With four or more columns in a band, the cards three or more columns | |
| 719 | + | // away sit under the two-step card: arrows still reach every one, in | |
| 720 | + | // order, each taking the maximization. | |
| 721 | + | func TestOverviewCascadeFarCardsByArrows(t *testing.T) { | |
| 722 | + | m := newMonitor("", "") | |
| 723 | + | m.SetOutput(1200, 600) | |
| 724 | + | m.SetMaxColumns(5) | |
| 725 | + | w := m.Current() | |
| 726 | + | w.Overflow = OverflowCascade | |
| 727 | + | for id := WindowID(1); id <= 5; id++ { | |
| 728 | + | m.AddWindow(id) | |
| 729 | + | } | |
| 730 | + | w.FocusID(1) | |
| 731 | + | w.ToggleFullWidth() | |
| 732 | + | m.ToggleOverview() | |
| 733 | + | ps := m.Layout() | |
| 734 | + | edge := previewOf(t, ps, 3).Rect | |
| 735 | + | for _, id := range []WindowID{4, 5} { | |
| 736 | + | if p := previewOf(t, ps, id); p.Hidden || !p.Peek || p.Rect.X != edge.X { | |
| 737 | + | t.Fatalf("far card %d: %+v, want a card under 3 at x %d", id, p, edge.X) | |
| 738 | + | } | |
| 739 | + | } | |
| 740 | + | for _, want := range []WindowID{2, 3, 4, 5} { | |
| 741 | + | m.OverviewMove(1, 0) | |
| 742 | + | if m.Current() != w || m.overviewTarget() != want || !w.Columns[w.Focus].FullWidth { | |
| 743 | + | t.Fatalf("right: target %d, want maximized %d", m.overviewTarget(), want) | |
| 744 | + | } | |
| 745 | + | } | |
| 746 | + | m.CancelOverview() | |
| 747 | + | if focusedID(m) != 1 || !w.Columns[w.Focus].FullWidth { | |
| 748 | + | t.Fatalf("escape: focus %d, want maximized 1", focusedID(m)) | |
| 749 | + | } | |
| 750 | + | } | |
| 751 | + | ||
| 681 | 752 | // Right from a stash card lands on the maximized column, which keeps its | |
| 682 | 753 | // maximization, not on the first card of its band. | |
| 683 | 754 | func TestOverviewCascadeStashToMaximized(t *testing.T) { | |
| @@ -678,6 +678,77 @@ func TestOverviewCascadeThreeFingerMaximizedCards(t *testing.T) { | |||||
| 678 | } | 678 | } | ||
| 679 | } | 679 | } | ||
| 680 | 680 | ||||
| 681 | + | // Left from the first column of a band whose maximized column hides it: | |||
| 682 | + | // the arrows and three fingers walk the cards, then enter the stash, or | |||
| 683 | + | // change workspace without one. | |||
| 684 | + | func TestOverviewCascadeMaximizedLeftEdge(t *testing.T) { | |||
| 685 | + | for _, stash := range []bool{false, true} { | |||
| 686 | + | c, m := cascadeCore(t) | |||
| 687 | + | w := m.Current() | |||
| 688 | + | if stash { | |||
| 689 | + | w.AddWindow(40) | |||
| 690 | + | w.FocusID(40) | |||
| 691 | + | w.ToggleWindowStash() | |||
| 692 | + | } | |||
| 693 | + | w.FocusID(3) // Band {2, 3}: 3 hides 2. | |||
| 694 | + | w.ToggleFullWidth() | |||
| 695 | + | m.ToggleOverview() | |||
| 696 | + | swipe := func() { | |||
| 697 | + | c.swipeBegin(ports.SwipeBegin{Fingers: 3}) | |||
| 698 | + | for range 4 { | |||
| 699 | + | c.swipeUpdate(ports.SwipeUpdate{DX: -40}) | |||
| 700 | + | } | |||
| 701 | + | c.swipeEnd(ports.SwipeEnd{}) | |||
| 702 | + | } | |||
| 703 | + | swipe() | |||
| 704 | + | if m.Current() != w || m.overviewTarget() != 2 || !w.Columns[w.Focus].FullWidth { | |||
| 705 | + | t.Fatalf("stash %v: first swipe: target %d", stash, m.overviewTarget()) | |||
| 706 | + | } | |||
| 707 | + | swipe() | |||
| 708 | + | if stash { | |||
| 709 | + | if m.Current() != w || m.cardAt(w) < 0 { | |||
| 710 | + | t.Fatalf("past the band: workspace %d, card %d; want the stash", indexOf(m.Workspaces, m.Current()), m.cardAt(w)) | |||
| 711 | + | } | |||
| 712 | + | } else if m.Current() != m.Workspaces[0] { | |||
| 713 | + | t.Fatalf("past the band: workspace %d, want 0", indexOf(m.Workspaces, m.Current())) | |||
| 714 | + | } | |||
| 715 | + | } | |||
| 716 | + | } | |||
| 717 | + | ||||
| 718 | + | // With four or more columns in a band, the cards three or more columns | |||
| 719 | + | // away sit under the two-step card: arrows still reach every one, in | |||
| 720 | + | // order, each taking the maximization. | |||
| 721 | + | func TestOverviewCascadeFarCardsByArrows(t *testing.T) { | |||
| 722 | + | m := newMonitor("", "") | |||
| 723 | + | m.SetOutput(1200, 600) | |||
| 724 | + | m.SetMaxColumns(5) | |||
| 725 | + | w := m.Current() | |||
| 726 | + | w.Overflow = OverflowCascade | |||
| 727 | + | for id := WindowID(1); id <= 5; id++ { | |||
| 728 | + | m.AddWindow(id) | |||
| 729 | + | } | |||
| 730 | + | w.FocusID(1) | |||
| 731 | + | w.ToggleFullWidth() | |||
| 732 | + | m.ToggleOverview() | |||
| 733 | + | ps := m.Layout() | |||
| 734 | + | edge := previewOf(t, ps, 3).Rect | |||
| 735 | + | for _, id := range []WindowID{4, 5} { | |||
| 736 | + | if p := previewOf(t, ps, id); p.Hidden || !p.Peek || p.Rect.X != edge.X { | |||
| 737 | + | t.Fatalf("far card %d: %+v, want a card under 3 at x %d", id, p, edge.X) | |||
| 738 | + | } | |||
| 739 | + | } | |||
| 740 | + | for _, want := range []WindowID{2, 3, 4, 5} { | |||
| 741 | + | m.OverviewMove(1, 0) | |||
| 742 | + | if m.Current() != w || m.overviewTarget() != want || !w.Columns[w.Focus].FullWidth { | |||
| 743 | + | t.Fatalf("right: target %d, want maximized %d", m.overviewTarget(), want) | |||
| 744 | + | } | |||
| 745 | + | } | |||
| 746 | + | m.CancelOverview() | |||
| 747 | + | if focusedID(m) != 1 || !w.Columns[w.Focus].FullWidth { | |||
| 748 | + | t.Fatalf("escape: focus %d, want maximized 1", focusedID(m)) | |||
| 749 | + | } | |||
| 750 | + | } | |||
| 751 | + | ||||
| 681 | // Right from a stash card lands on the maximized column, which keeps its | 752 | // Right from a stash card lands on the maximized column, which keeps its | ||
| 682 | // maximization, not on the first card of its band. | 753 | // maximization, not on the first card of its band. | ||
| 683 | func TestOverviewCascadeStashToMaximized(t *testing.T) { | 754 | func TestOverviewCascadeStashToMaximized(t *testing.T) { | ||
M internal/core/separators_test.go +21 -0
| @@ -147,6 +147,27 @@ func TestSeparatorsScroll(t *testing.T) { | |||
| 147 | 147 | } | |
| 148 | 148 | } | |
| 149 | 149 | ||
| 150 | + | // A scroll column scrolled under a side panel is no neighbor: the column | |
| 151 | + | // next to it in the usable area gets no line on that side. | |
| 152 | + | // With a gap, the edge the two share lies inside the usable area: only | |
| 153 | + | // the tile under the panel being out of it keeps the line away. | |
| 154 | + | func TestSeparatorsScrollUnderSidePanel(t *testing.T) { | |
| 155 | + | for _, gap := range []int{0, 8} { | |
| 156 | + | ps := []Placement{ | |
| 157 | + | {ID: 1, Rect: Rect{X: 0, W: 100, H: 600}}, | |
| 158 | + | {ID: 2, Rect: Rect{X: 100 + gap, W: 400, H: 600}, Focused: true}, | |
| 159 | + | {ID: 3, Rect: Rect{X: 500 + 2*gap, W: 380, H: 600}}, | |
| 160 | + | } | |
| 161 | + | setVisibleNeighbors(ps, gap, Rect{X: 100, W: 900, H: 600}) | |
| 162 | + | if ps[0].Neighbors != 0 { | |
| 163 | + | t.Fatalf("gap %d: tile under the panel: neighbors %04b, want none", gap, ps[0].Neighbors) | |
| 164 | + | } | |
| 165 | + | if ps[1].Neighbors != ports.SideRight { | |
| 166 | + | t.Fatalf("gap %d: tile at the panel: neighbors %04b, want right only", gap, ps[1].Neighbors) | |
| 167 | + | } | |
| 168 | + | } | |
| 169 | + | } | |
| 170 | + | ||
| 150 | 171 | // A cascade band above a bottom panel: the next band starts under the | |
| 151 | 172 | // panel, so the focused tile gets no line along the panel, only the one it | |
| 152 | 173 | // shares with its band neighbor. | |
| @@ -147,6 +147,27 @@ func TestSeparatorsScroll(t *testing.T) { | |||||
| 147 | } | 147 | } | ||
| 148 | } | 148 | } | ||
| 149 | 149 | ||||
| 150 | + | // A scroll column scrolled under a side panel is no neighbor: the column | |||
| 151 | + | // next to it in the usable area gets no line on that side. | |||
| 152 | + | // With a gap, the edge the two share lies inside the usable area: only | |||
| 153 | + | // the tile under the panel being out of it keeps the line away. | |||
| 154 | + | func TestSeparatorsScrollUnderSidePanel(t *testing.T) { | |||
| 155 | + | for _, gap := range []int{0, 8} { | |||
| 156 | + | ps := []Placement{ | |||
| 157 | + | {ID: 1, Rect: Rect{X: 0, W: 100, H: 600}}, | |||
| 158 | + | {ID: 2, Rect: Rect{X: 100 + gap, W: 400, H: 600}, Focused: true}, | |||
| 159 | + | {ID: 3, Rect: Rect{X: 500 + 2*gap, W: 380, H: 600}}, | |||
| 160 | + | } | |||
| 161 | + | setVisibleNeighbors(ps, gap, Rect{X: 100, W: 900, H: 600}) | |||
| 162 | + | if ps[0].Neighbors != 0 { | |||
| 163 | + | t.Fatalf("gap %d: tile under the panel: neighbors %04b, want none", gap, ps[0].Neighbors) | |||
| 164 | + | } | |||
| 165 | + | if ps[1].Neighbors != ports.SideRight { | |||
| 166 | + | t.Fatalf("gap %d: tile at the panel: neighbors %04b, want right only", gap, ps[1].Neighbors) | |||
| 167 | + | } | |||
| 168 | + | } | |||
| 169 | + | } | |||
| 170 | + | ||||
| 150 | // A cascade band above a bottom panel: the next band starts under the | 171 | // A cascade band above a bottom panel: the next band starts under the | ||
| 151 | // panel, so the focused tile gets no line along the panel, only the one it | 172 | // panel, so the focused tile gets no line along the panel, only the one it | ||
| 152 | // shares with its band neighbor. | 173 | // shares with its band neighbor. | ||