package core import ( "fmt" "slices" "testing" "github.com/bnema/neferwl/internal/ports" ) // cascadeOverview is overviewMonitor with workspace 2 (current) a cascade of // seven columns, 4..10, in three bands of three (the last holds 10 alone), // the focus on column 8 (band 2, middle). Workspace 1 (columns 1..3) is a // scroll workspace on its left, workspace 3 (column 30) on its right. func cascadeOverview() *Monitor { m := overviewMonitor() m.SetBorder(2) m.Focus(1) w := m.Current() w.Overflow = OverflowCascade for id := WindowID(5); id <= 10; id++ { w.AddWindow(id) } m.Focus(2) m.AddWindow(30) m.Focus(1) w.FocusID(8) return m } // withStash stashes windows 40..40+n-1 of the current workspace, with the // focus back on column 8. func withStash(m *Monitor, n int) { w := m.Current() for id := WindowID(40); id < WindowID(40+n); id++ { w.AddWindow(id) w.FocusID(id) w.ToggleWindowStash() } w.FocusID(8) } // cascadeCore is a Core on one output with workspace 0 holding window 1, // workspace 1 (current) a cascade of windows 2..6 in bands of two ({2,3} // {4,5} {6}), workspace 2 window 30, and window 2 focused. func cascadeCore(t *testing.T) (*Core, *Monitor) { t.Helper() c, _ := indicatorCore(t) m := c.cur().mon m.AddWindow(1) m.Focus(1) w := m.Current() w.Overflow = OverflowCascade for id := WindowID(2); id <= 6; id++ { w.AddWindow(id) } m.normalize() m.Focus(2) m.AddWindow(30) m.Focus(1) w.FocusID(2) return c, m } func centreY(r Rect) int { return r.Y + r.H/2 } func within1(a, b int) bool { return a-b <= 1 && b-a <= 1 } func hiddenOff(p Placement) bool { return p.Hidden && p.Preview == 0 && !p.Focused } // The zoom does not depend on the number of bands. func TestOverviewCascadeZoomIgnoresBandCount(t *testing.T) { if one, three := cascadeWorkspace(3).overviewZoom(), cascadeWorkspace(7).overviewZoom(); one != three || one != overviewMaxZoom { t.Fatalf("zoom with one band %v, with three %v", one, three) } } // The selected band is the centre lane: undimmed, centred, selected tile // focused. func TestOverviewCascadeCentreLane(t *testing.T) { m := cascadeOverview() m.ToggleOverview() ps := m.Layout() u := m.Current().overviewArea() for _, id := range []WindowID{7, 8, 9} { p := previewOf(t, ps, id) if p.Hidden || p.Peek || p.Preview != overviewMaxZoom || p.Focused != (id == 8) { t.Fatalf("band tile %d: %+v", id, p) } if !within1(centreY(p.Rect), u.Y+u.H/2) { t.Fatalf("band tile %d centre %d, area centre %d", id, centreY(p.Rect), u.Y+u.H/2) } } } // The other bands are dimmed lanes a screen (and a gap) away; those // outside the area are hidden. func TestOverviewCascadeOtherLanes(t *testing.T) { m := cascadeOverview() m.ToggleOverview() ps := m.Layout() sel := previewOf(t, ps, 8).Rect above, below := previewOf(t, ps, 5), previewOf(t, ps, 10) if above.Hidden || !above.Peek || above.Focused || below.Hidden || !below.Peek || below.Focused { t.Fatalf("adjacent bands: above %+v, below %+v", above, below) } // The lanes sit evenly around the selected one, apart from it (a gap), // and the area still shows part of each. up, down := sel.Y-above.Rect.Y, below.Rect.Y-sel.Y if !within1(up, down) { t.Fatalf("lanes not evenly spaced: %d above, %d below", up, down) } if gap := up - sel.H; gap <= 0 { t.Fatalf("lane above overlaps the selected one: %v over %v", above.Rect, sel) } u := m.Current().overviewArea() if above.Rect.Y+above.Rect.H <= u.Y || below.Rect.Y >= u.Y+u.H { t.Fatalf("adjacent lanes not visible in %v: %v, %v", u, above.Rect, below.Rect) } // Five bands (13 columns): the ones two lanes away are off the area. m = cascadeOverview() w := m.Current() for id := WindowID(11); id <= 16; id++ { w.AddWindow(id) } w.FocusID(8) m.ToggleOverview() // Columns 4..16: bands {4,5,6} {7,8,9} {10,11,12} {13,14,15} {16}. w.FocusID(11) ps = m.Layout() for _, id := range []WindowID{4, 5, 6, 16} { if p := previewOf(t, ps, id); !hiddenOff(p) { t.Fatalf("band two lanes away %d not hidden: %+v", id, p) } } for _, id := range []WindowID{7, 13} { if p := previewOf(t, ps, id); p.Hidden || !p.Peek { t.Fatalf("adjacent band %d: %+v", id, p) } } } // Neighbor workspaces sit left and right of the current slot, dimmed, // their selected lane level with the centre lane. func TestOverviewCascadeNeighbors(t *testing.T) { m := cascadeOverview() m.ToggleOverview() rows, _ := m.overviewRows() cur, left, right := rows[m.Workspaces[1]], rows[m.Workspaces[0]], rows[m.Workspaces[2]] if left.X+left.W > cur.X || right.X < cur.X+cur.W { t.Fatalf("slots overlap: %v %v %v", left, cur, right) } ps := m.Layout() centre := centreY(previewOf(t, ps, 8).Rect) shown := 0 for _, n := range []struct { id WindowID side int }{{1, -1}, {2, -1}, {3, -1}, {30, 1}} { id, side := n.id, n.side p := previewOf(t, ps, id) if p.Hidden { continue } shown++ if !p.Peek || p.Focused || p.Preview == 0 { t.Fatalf("neighbor %d not dimmed: %+v", id, p) } if side < 0 && p.Rect.X+p.Rect.W > cur.X || side > 0 && p.Rect.X < cur.X+cur.W { t.Fatalf("neighbor %d overlaps the current slot %v: %+v", id, cur, p.Rect) } if !within1(centreY(p.Rect), centre) { t.Fatalf("neighbor %d centre %d, centre lane %d", id, centreY(p.Rect), centre) } } if shown < 2 { t.Fatalf("only %d neighbor tiles drawn", shown) } for _, p := range ps { if p.Hidden || p.Peek { continue } if p.Rect.X < cur.X || p.Rect.X+p.Rect.W > cur.X+cur.W { t.Fatalf("current tile %d outside its slot %v: %+v", p.ID, cur, p.Rect) } } } // Lanes along the overview axis would overlap the other workspaces: a // cascade neighbor of a scroll workspace shows its selected band only. func TestOverviewCascadeNeighborBelowScrollShowsOneBand(t *testing.T) { m := cascadeOverview() m.Focus(0) m.ToggleOverview() if m.overviewAxis() != verticalAxis { t.Fatal("setup: scroll overview is not vertical") } ps := m.Layout() for _, id := range []WindowID{4, 5, 6} { if p := previewOf(t, ps, id); !hiddenOff(p) { t.Fatalf("band %d of the neighbor drawn: %+v", id, p) } } if p := previewOf(t, ps, 8); p.Hidden { // The neighbor's selected column is 8. t.Fatalf("selected band hidden: %+v", p) } for _, id := range []WindowID{7, 9} { if p := previewOf(t, ps, id); p.Hidden || !p.Peek { t.Fatalf("selected band tile %d: %+v", id, p) } } if p := previewOf(t, ps, 10); !hiddenOff(p) { t.Fatalf("band 3 drawn: %+v", p) } } // The stash pile is inside the current slot, left of the band; the // previous workspace stays clear of it. func TestOverviewCascadeStashInSlot(t *testing.T) { m := cascadeOverview() withStash(m, 2) m.ToggleOverview() rows, _ := m.overviewRows() cur, prev := rows[m.Current()], rows[m.Workspaces[0]] ps := m.Layout() left := cur.X + cur.W for _, id := range []WindowID{7, 8, 9} { left = min(left, previewOf(t, ps, id).Rect.X) } for _, id := range []WindowID{40, 41} { p := previewOf(t, ps, id) if p.Hidden || p.Preview != overviewCardZoom { t.Fatalf("pile card %d: %+v", id, p) } if p.Rect.X < cur.X || p.Rect.X+p.Rect.W > left { t.Fatalf("pile card %d not inside the slot, left of the band (%d..%d): %+v", id, cur.X, left, p.Rect) } // 41 is the front card of the pile. if id == 41 && !within1(centreY(p.Rect), centreY(previewOf(t, ps, 8).Rect)) { t.Fatalf("pile not level with the selected lane: %+v", p.Rect) } } // The pile does not shift the band: its row stays centred on the area. u := m.Current().overviewArea() lo, hi := previewOf(t, ps, 7).Rect, previewOf(t, ps, 9).Rect if mid := (lo.X + hi.X + hi.W) / 2; !within1(mid, u.X+u.W/2) { t.Fatalf("band centre %d with a stash, area centre %d", mid, u.X+u.W/2) } for _, id := range []WindowID{1, 2, 3} { if p := previewOf(t, ps, id); !p.Hidden && p.Rect.X+p.Rect.W > cur.X { t.Fatalf("previous workspace overlaps the slot with the pile: %+v", p.Rect) } } if prev.X+prev.W > cur.X { t.Fatalf("slots overlap: %v %v", prev, cur) } } type cascadeDriver struct { name string move func(m *Monitor, dx, dy int) } func keyDriver(left, right, up, down string) func(m *Monitor, dx, dy int) { return func(m *Monitor, dx, dy int) { name := map[[2]int]string{{-1, 0}: left, {1, 0}: right, {0, -1}: up, {0, 1}: down}[[2]int{dx, dy}] m.overviewKey(ports.KeyEvent{Keysym: name, Pressed: true}) } } var cascadeDrivers = []cascadeDriver{ {"move", func(m *Monitor, dx, dy int) { m.OverviewMove(dx, dy) }}, {"hjkl", keyDriver("h", "l", "k", "j")}, {"arrows", keyDriver("Left", "Right", "Up", "Down")}, } func focusedID(m *Monitor) WindowID { id, _ := m.Focused() return id } // ↑/↓ change band and stop at the ends; ←/→ walk the band, then leave it // for the stash or the neighbor workspaces; Escape restores everything. func TestOverviewCascadeKeys(t *testing.T) { for _, d := range cascadeDrivers { t.Run(d.name, func(t *testing.T) { m := cascadeOverview() w, prev, next := m.Current(), m.Workspaces[0], m.Workspaces[2] w.FocusID(5) m.ToggleOverview() sel := func() WindowID { return w.Columns[w.Focus].Windows[0] } d.move(m, 0, 1) if sel() != 8 { t.Fatalf("down: %d, want 8", sel()) } d.move(m, 0, 1) d.move(m, 0, 1) if m.Current() != w || sel() != 10 { t.Fatalf("down at the last band: %d on the current workspace %v", sel(), m.Current() == w) } d.move(m, 0, -1) if sel() != 7 { t.Fatalf("up: %d, want 7", sel()) } d.move(m, 0, -1) d.move(m, 0, -1) if m.Current() != w || sel() != 4 { t.Fatalf("up at the first band: %d", sel()) } d.move(m, 1, 0) d.move(m, 1, 0) if sel() != 6 || m.Current() != w { t.Fatalf("right in the band: %d", sel()) } d.move(m, 1, 0) if m.Current() != next { t.Fatal("right past the last column did not reach the next workspace") } m.CancelOverview() if m.Current() != w || focusedID(m) != 5 { t.Fatalf("cancel left %d", focusedID(m)) } m.ToggleOverview() d.move(m, -1, 0) if sel() != 4 || m.Current() != w { t.Fatal("left within the band") } d.move(m, -1, 0) if m.Current() != prev { t.Fatal("left from the first column did not reach the previous workspace") } m.CancelOverview() // With a stash: left from the first column enters it, right // comes back to the band's first column, left twice goes to // the previous workspace. withStash(m, 2) w.FocusID(7) m.ToggleOverview() d.move(m, -1, 0) if m.cardAt(w) < 0 { t.Fatal("left from the first column did not select the stash") } d.move(m, 1, 0) if m.cardAt(w) >= 0 || sel() != 7 { t.Fatalf("right from the stash: card %d, column %d", m.cardAt(w), sel()) } d.move(m, -1, 0) d.move(m, -1, 0) if m.Current() != prev { t.Fatal("left from the stash did not reach the previous workspace") } m.CancelOverview() if m.Current() != w || focusedID(m) != 7 { t.Fatalf("cancel left %d", focusedID(m)) } }) } } // T7b: with a stash card selected, up/down and the three-finger vertical // swipe cycle the pile as in a vertical overview; confirming shows the picked window. func TestOverviewCascadeStashPile(t *testing.T) { for _, d := range cascadeDrivers { t.Run(d.name, func(t *testing.T) { m := cascadeOverview() withStash(m, 3) w := m.Current() w.FocusID(7) m.ToggleOverview() d.move(m, -1, 0) if m.cardAt(w) != w.stashAt { t.Fatalf("entered at %d", m.cardAt(w)) } at := m.cardAt(w) d.move(m, 0, -1) if m.Current() != w || m.cardAt(w) != at-1 { t.Fatalf("up: card %d, was %d", m.cardAt(w), at) } d.move(m, 0, 1) d.move(m, 0, 1) d.move(m, 0, 1) if m.Current() != w || m.cardAt(w) != len(w.Stash)-1 { t.Fatalf("down: card %d", m.cardAt(w)) } d.move(m, 0, -1) picked := w.Stash[m.cardAt(w)].ID m.ToggleOverview() if focusedID(m) != picked || w.stashHidden { t.Fatalf("confirm on %d, want %d (hidden %v)", focusedID(m), picked, w.stashHidden) } }) } c, _, sc, _ := stashRig(t, true) m := sc.mon w := m.Current() w.Overflow = OverflowCascade m.Focus(1) m.AddWindow(50) m.Focus(0) m.ToggleOverview() if m.cardAt(w) < 0 { t.Fatal("setup: no card selected") } at, before := m.cardAt(w), stashIDs(w) c.swipeBegin(ports.SwipeBegin{Fingers: 3}) for range 3 { c.swipeUpdate(ports.SwipeUpdate{DY: -40}) } c.swipeEnd(ports.SwipeEnd{}) if m.Current() != w || m.cardAt(w) != at-1 { t.Fatalf("swipe up: card %d, was %d", m.cardAt(w), at) } c.swipeBegin(ports.SwipeBegin{Fingers: 3}) for range 3 { c.swipeUpdate(ports.SwipeUpdate{DX: 40}) } c.swipeEnd(ports.SwipeEnd{}) // A horizontal three-finger swipe is a workspace step, even over a // stash card. if m.Current() != m.Workspaces[1] { t.Fatalf("horizontal swipe over a stash card: workspace %d", indexOf(m.Workspaces, m.Current())) } if got := stashIDs(w); !slices.Equal(got, before) { t.Fatal("stash reordered") } } // T7c: three fingers follow the physical axes, once per gesture. func TestOverviewCascadeThreeFinger(t *testing.T) { c, m := cascadeCore(t) w := m.Current() m.ToggleOverview() swipe := func(dx, dy float64) { t.Helper() c.swipeBegin(ports.SwipeBegin{Fingers: 3}) for range 4 { c.swipeUpdate(ports.SwipeUpdate{DX: dx, DY: dy}) } c.swipeEnd(ports.SwipeEnd{}) } swipe(0, 40) if m.Current() != w || w.band(w.Focus) != 1 { t.Fatalf("one swipe down: band %d", w.band(w.Focus)) } swipe(0, -40) if w.band(w.Focus) != 0 { t.Fatalf("one swipe up: band %d", w.band(w.Focus)) } swipe(40, 0) if m.Current() != m.Workspaces[2] { t.Fatalf("one swipe right: workspace %d", indexOf(m.Workspaces, m.Current())) } // Onto a scroll workspace and back: the vertical swipe is the scroll // overview's workspace step; neither bounces during the gesture. swipe(0, -40) if m.Current() != w { t.Fatalf("swipe up from the scroll workspace: workspace %d", indexOf(m.Workspaces, m.Current())) } swipe(-40, 0) if m.Current() != m.Workspaces[0] { t.Fatalf("one swipe left: workspace %d", indexOf(m.Workspaces, m.Current())) } } // Right from a stash card lands on the maximized column, which keeps its // maximization: it is alone in its band. func TestOverviewCascadeStashToMaximized(t *testing.T) { m := cascadeOverview() withStash(m, 1) w := m.Current() w.FocusID(9) w.ToggleFullWidth() m.ToggleOverview() m.selectCard(w, w.stashAt) // As a click on the pile does. if m.cardAt(w) < 0 { t.Fatalf("setup: no stash card, target %d", m.overviewTarget()) } m.OverviewMove(1, 0) if m.overviewTarget() != 9 || !w.Columns[w.Focus].FullWidth || len(w.maximized) == 0 || w.maximized[0] != 9 { t.Fatalf("target %d, maximized %v; want 9", m.overviewTarget(), w.maximized) } } // A named boundary of a cascade overview is a vertical rule strictly // between the two slots. func TestOverviewCascadeNamedDivider(t *testing.T) { m := namedOverviewMonitor() m.Workspaces[1].Overflow = OverflowCascade m.ToggleOverview() rows, dividers := m.overviewRows() if len(dividers) != 1 { t.Fatalf("dividers: %+v", dividers) } d, cur, dev := dividers[0].Rect, rows[m.Workspaces[1]], rows[m.byName("dev")] if d.W != 1 || d.H <= 1 || dividers[0].Active { t.Fatalf("divider is not an inactive vertical rule: %+v", dividers[0]) } if d.X < cur.X+cur.W || d.X+d.W > dev.X { t.Fatalf("divider %v not between %v and %v", d, cur, dev) } } // Covering floats fan along the horizontal axis: left goes deeper in the // stack (the cards behind show on the left), right comes back to the front. func TestOverviewCascadeCoveringFloat(t *testing.T) { for _, d := range cascadeDrivers { t.Run(d.name, func(t *testing.T) { m := cascadeOverview() m.Current().AddFloating(90, 300, 200) w := m.Current() w.FocusID(10) m.ToggleOverview() if m.stackFront(w).kind != stackColumns { t.Fatal("setup: columns not in front") } d.move(m, 0, -1) // columns: up is a band d.move(m, -1, 0) if f := m.stackFront(w); f.kind != stackFloat || f.id != 90 { t.Fatalf("left from the first column: front %+v", f) } if p := previewOf(t, m.Layout(), 90); p.Peek || !p.Focused { t.Fatalf("float not in front: %+v", p) } d.move(m, 0, 1) // a float front takes no band move if f := m.stackFront(w); f.kind != stackFloat { t.Fatalf("down left the float: %+v", f) } d.move(m, 1, 0) if m.stackFront(w).kind != stackColumns || m.Current() != w { t.Fatalf("right from the float: front %+v", m.stackFront(w)) } // The float in front of the columns: they fan to its left. m.CancelOverview() w.FocusID(90) m.ToggleOverview() if f := m.stackFront(w); f.kind != stackFloat { t.Fatalf("setup: front %+v", f) } behind := previewOf(t, m.Layout(), 10) if !behind.Peek { t.Fatalf("columns not behind the float: %+v", behind) } d.move(m, -1, 0) if m.stackFront(w).kind != stackColumns || m.Current() != w { t.Fatalf("left from the front float: front %+v", m.stackFront(w)) } // The cards behind fan to the left: coming to the front, the // columns move right by one peek step. front := previewOf(t, m.Layout(), 10) if front.Peek || front.Rect.X-behind.Rect.X != w.peekStep() { t.Fatalf("columns behind %v, in front %v, peek step %d", behind.Rect, front.Rect, w.peekStep()) } d.move(m, 1, 0) if f := m.stackFront(w); f.kind != stackFloat || f.id != 90 { t.Fatalf("right from the columns behind the float: front %+v", f) } }) } } // T10b: a pinned fullscreen column keeps its Fullscreen tile in the band. func TestOverviewCascadePinnedFullscreenTile(t *testing.T) { m := cascadeOverview() w := m.Current() w.FocusID(8) m.ToggleFullscreen() if !w.pinned() { t.Fatal("setup: not pinned") } m.ToggleOverview() ps := m.Layout() fs := previewOf(t, ps, 8) if !fs.Fullscreen || fs.Hidden { t.Fatalf("fullscreen tile: %+v", fs) } for _, id := range []WindowID{7, 9} { if p := previewOf(t, ps, id); p.Hidden || p.Fullscreen || p.Peek != (fs.Peek) { t.Fatalf("band tile %d: %+v", id, p) } } u := w.overviewArea() if !within1(centreY(fs.Rect), u.Y+u.H/2) { t.Fatalf("fullscreen tile not on the centre lane: %+v", fs.Rect) } } // The focus binds keep their meaning in a cascade overview. func TestOverviewCascadeFocusBinds(t *testing.T) { m := cascadeOverview() w := m.Current() m.ToggleOverview() if !m.overviewFocus(ActionFocusWindowDown) || w.band(w.Focus) != 2 || m.Current() != w { t.Fatalf("focus-window-down: band %d", w.band(w.Focus)) } if !m.overviewFocus(ActionFocusWindowUp) || w.band(w.Focus) != 1 { t.Fatalf("focus-window-up: band %d", w.band(w.Focus)) } m.overviewFocus(ActionFocusWorkspaceNext) if m.Current() != m.Workspaces[2] { t.Fatal("focus-workspace-next did not change workspace") } m.overviewFocus(ActionFocusWorkspacePrev) if m.Current() != w { t.Fatal("focus-workspace-prev did not come back") } m.overviewFocus(ActionFocusWorkspacePrev) if m.Current() != m.Workspaces[0] { t.Fatal("focus-workspace-prev did not change workspace") } m.CancelOverview() // A selected stash card keeps the pile cycle. withStash(m, 3) w.FocusID(7) m.ToggleOverview() m.OverviewMove(-1, 0) at := m.cardAt(w) m.overviewFocus(ActionFocusWorkspacePrev) if m.Current() != w || m.cardAt(w) != at-1 { t.Fatalf("focus-workspace-prev over a card: card %d, was %d", m.cardAt(w), at) } m.overviewFocus(ActionFocusWorkspaceNext) if m.Current() != w || m.cardAt(w) != at { t.Fatalf("focus-workspace-next over a card: card %d", m.cardAt(w)) } } // A three-finger step that moves nothing reports no change: no scene // transition, no keyboard grab. Here the first band of a cascade has no // band above it. func TestOverviewCascadeFingerNoopReportsNoChange(t *testing.T) { m := cascadeOverview() m.Current().FocusID(5) m.ToggleOverview() up := ports.PointerAxis{Source: ports.AxisFinger, Vertical: ports.ScrollAxis{Set: true, Value: -overviewScrollStep}} if m.overviewScroll(up) { t.Fatal("a step past the first band reported a change") } } func TestOverviewMixedLayoutGestureAndCancel(t *testing.T) { m := overviewMonitor() m.SetOverflow(OverflowFixed) m.Current().Overflow = OverflowCascade from := m.Current() m.ToggleOverview() rows, _ := m.overviewRows() if rows[m.Workspaces[1]].X <= rows[from].X { t.Fatal("cascade neighbor not horizontal") } m.overviewScroll(ports.PointerAxis{Source: ports.AxisFinger, Horizontal: ports.ScrollAxis{Set: true, Value: overviewScrollStep}}) if m.Current() == from { t.Fatal("horizontal gesture did not change workspace") } if m.Current().Overflow != OverflowFixed { t.Fatal("destination lost fixed policy") } if m.ov.scrollX != 0 || m.ov.scrollY != 0 || !m.ov.scrolled { t.Fatalf("scroll state after the axis change: %+v", m.ov) } m.CancelOverview() if m.Current() != from { t.Fatal("cancel lost source workspace") } } // Showing a workspace with another overview axis drops the scroll leftovers // of the old one. func TestOverviewAxisChangeResetsScroll(t *testing.T) { m := cascadeOverview() m.ToggleOverview() m.ov.scrollX, m.ov.scrollY, m.ov.sideways, m.ov.scrolled = 20, 20, true, true m.showOverview(m.Workspaces[2]) if m.ov.scrollX != 0 || m.ov.scrollY != 0 || m.ov.sideways || !m.ov.scrolled { t.Fatalf("scroll kept or latch dropped: %+v", m.ov) } m.ov.scrollX = 20 m.showOverview(m.Workspaces[0]) if m.ov.scrollX != 20 { t.Fatal("scroll reset between workspaces of the same axis") } } // A reload that changes the current workspace's axes drops the scroll state; // one that does not, keeps it. func TestOverviewReloadResetsScrollOnAxisChange(t *testing.T) { c, _ := indicatorCore(t) m := c.cur().mon m.AddWindow(1) m.ToggleOverview() w := m.Current() set := func() { m.ov.scrollX, m.ov.scrollY, m.ov.scrolled, m.ov.sideways = 20, 10, true, true } set() cfg := c.cfg cfg.Layout.Gaps++ if err := c.apply(cfg); err != nil { t.Fatal(err) } if m.ov.scrollX != 20 || !m.ov.scrolled { t.Fatalf("unrelated reload reset the scroll: %+v", m.ov) } cfg.Layout.Overflow = string(OverflowCascade) if err := c.apply(cfg); err != nil { t.Fatal(err) } if m.Current().policy().workspace != horizontalAxis { t.Fatal("setup: not cascade") } // The reload keeps the overview open on its selection, and cancelling // still gives the original focus back. if id, _ := w.Focused(); !m.ov.open || id != 1 { t.Fatalf("reload lost the overview selection: open %v, focus %d", m.ov.open, id) } // The scroll is dropped, but not the latch that waits for the fingers // to lift. if m.ov.scrollX != 0 || m.ov.scrollY != 0 || !m.ov.scrolled || m.ov.sideways { t.Fatalf("axis change kept the scroll or dropped the latch: %+v", m.ov) } set() cfg.Layout.Overflow = string(OverflowFixed) if err := c.apply(cfg); err != nil { t.Fatal(err) } if m.ov.scrollX != 0 || !m.ov.scrolled { t.Fatalf("workspace axis change kept the scroll or dropped the latch: %+v", m.ov) } m.CancelOverview() if id, _ := w.Focused(); id != 1 { t.Fatalf("cancel left the focus on %d", id) } } // The finger latch outlives an axis change: a gesture that turned the // overview, then was turned back by a key, does not step again. func TestOverviewFingerLatchSurvivesAxisChange(t *testing.T) { c, m := cascadeCore(t) w := m.Current() m.ToggleOverview() c.swipeBegin(ports.SwipeBegin{Fingers: 3}) c.swipeUpdate(ports.SwipeUpdate{DX: 70}) if m.Current() != m.Workspaces[2] || m.overviewAxis() != verticalAxis { t.Fatalf("setup: workspace %d", indexOf(m.Workspaces, m.Current())) } // Back to the cascade workspace by a key, with the fingers still down. m.overviewKey(ports.KeyEvent{Keysym: "k", Pressed: true}) if m.Current() != w || m.overviewAxis() != horizontalAxis { t.Fatal("setup: the key did not turn the overview back") } for _, u := range []ports.SwipeUpdate{{DY: 70}, {DX: 70}, {DY: -70}, {DX: -70}} { c.swipeUpdate(u) } c.swipeEnd(ports.SwipeEnd{}) if m.Current() != w || w.Focus != 0 { t.Fatalf("the gesture stepped again: workspace %d, column %d", indexOf(m.Workspaces, m.Current()), w.Focus) } // The next gesture steps again. c.swipeBegin(ports.SwipeBegin{Fingers: 3}) c.swipeUpdate(ports.SwipeUpdate{DY: 70}) c.swipeEnd(ports.SwipeEnd{}) if w.band(w.Focus) != 1 { t.Fatalf("the next gesture did not step: band %d", w.band(w.Focus)) } } // A stash-only cascade workspace has no columns to select: right from its // pile goes straight to the next workspace. func TestOverviewCascadeStashOnlyRight(t *testing.T) { m := overviewMonitor() m.Focus(1) w := m.Current() w.Overflow = OverflowCascade w.ToggleWindowStash() m.Focus(2) m.AddWindow(30) m.Focus(1) m.ToggleOverview() if len(w.Columns) != 0 || m.cardAt(w) < 0 { t.Fatalf("setup: %d columns, card %d", len(w.Columns), m.cardAt(w)) } m.OverviewMove(1, 0) if m.Current() != m.Workspaces[2] { t.Fatalf("right from the pile: workspace %d", indexOf(m.Workspaces, m.Current())) } m.OverviewMove(0, -1) // the next workspace is a scroll one: up goes back if m.Current() != w || m.cardAt(w) < 0 { t.Fatal("setup: back on the stash-only workspace without its card") } m.OverviewMove(-1, 0) if m.Current() != m.Workspaces[0] { t.Fatalf("left from the pile: workspace %d", indexOf(m.Workspaces, m.Current())) } } // A cascade neighbor beside a cascade workspace draws its other lanes // dimmed, its selected lane level with the centre lane. func TestOverviewCascadeNeighborLanes(t *testing.T) { m := cascadeOverview() prev := m.Workspaces[0] prev.Overflow = OverflowCascade for id := WindowID(60); id <= 65; id++ { prev.AddWindow(id) // bands {1,2,3} {60,61,62} {63,64,65}, 65 selected } m.ToggleOverview() ps := m.Layout() centre := centreY(previewOf(t, ps, 8).Rect) sel := previewOf(t, ps, 65) if sel.Hidden || !sel.Peek || sel.Focused || !within1(centreY(sel.Rect), centre) { t.Fatalf("selected lane of the neighbor: %+v, centre lane %d", sel, centre) } above := previewOf(t, ps, 62) if above.Hidden || !above.Peek || above.Rect.Y+above.Rect.H > sel.Rect.Y { t.Fatalf("lane above in the neighbor: %+v, selected %v", above, sel.Rect) } if p := previewOf(t, ps, 3); !hiddenOff(p) { t.Fatalf("lane two away in the neighbor: %+v", p) } } // A scroll neighbor wider than its slot scrolls inside it: what leaves the // slot is hidden, so it never sits on the current slot. func TestOverviewCascadeWideScrollNeighborIsClipped(t *testing.T) { m := cascadeOverview() prev := m.Workspaces[0] for id := WindowID(60); id <= 67; id++ { prev.AddWindow(id) } m.ToggleOverview() rows, _ := m.overviewRows() cur, slot := rows[m.Current()], rows[prev] hidden, shown := 0, 0 for _, p := range m.Layout() { if !prev.has(p.ID) { continue } if p.Hidden { hidden++ continue } shown++ if p.Rect.X < slot.X || p.Rect.X+p.Rect.W > slot.X+slot.W || p.Rect.X+p.Rect.W > cur.X { t.Fatalf("tile %d leaves its slot %v or reaches the current slot %v: %v", p.ID, slot, cur, p.Rect) } } if hidden == 0 || shown == 0 { t.Fatalf("setup: %d tiles shown, %d hidden", shown, hidden) } } // A wheel and a continuous scroll in a cascade overview: vertical changes // workspace (and does not bounce back on a workspace with another axis), // horizontal moves along the band. func TestOverviewCascadeScroll(t *testing.T) { vertical := ports.ScrollAxis{Set: true, Value: overviewScrollStep, V120: 120} for _, source := range []ports.AxisSource{ports.AxisWheel, ports.AxisContinuous} { t.Run(fmt.Sprint(source), func(t *testing.T) { m := cascadeOverview() w := m.Current() m.ToggleOverview() m.overviewScroll(ports.PointerAxis{Source: source, Vertical: vertical}) if m.Current() != m.Workspaces[2] || w.band(w.Focus) != 1 || m.ov.scrollX != 0 || m.ov.scrollY != 0 { t.Fatalf("vertical: workspace %d, band %d, scroll %v,%v", indexOf(m.Workspaces, m.Current()), w.band(w.Focus), m.ov.scrollX, m.ov.scrollY) } m = cascadeOverview() w = m.Current() w.FocusID(7) m.ToggleOverview() m.overviewScroll(ports.PointerAxis{Source: source, Horizontal: vertical}) if m.Current() != w || w.Columns[w.Focus].Windows[0] != 8 { t.Fatalf("horizontal: column %d", w.Columns[w.Focus].Windows[0]) } // Onto a workspace with another axis: one step, no bounce. m = overviewMonitor() m.SetOverflow(OverflowFixed) m.Workspaces[1].Overflow = OverflowCascade m.ToggleOverview() m.overviewScroll(ports.PointerAxis{Source: source, Vertical: vertical}) if m.Current() != m.Workspaces[1] || m.ov.scrollX != 0 || m.ov.scrollY != 0 { t.Fatalf("mixed: workspace %d, scroll %v,%v", indexOf(m.Workspaces, m.Current()), m.ov.scrollX, m.ov.scrollY) } }) } }