feat(overview): show columns hidden by a maximized cascade column as cards
Columns a maximized cascade column hides are dimmed cards behind it in their band. Selecting one moves the maximization to it; Escape restores it. A shared hiddenByMaximized predicate replaces the inline checks.
- commit
- 079b695058d28118cb84c8e56577c34c1af9ee6a
- author
- brice · · 2026-10-07 13:55 UTC
- parent
- 92e4f5a
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
5 files · +173 -30
M docs/config.md +1 -1
| @@ -196,7 +196,7 @@ Left/right focus follows column order across band boundaries. Up/down visits sta | |||
| 196 | 196 | ||
| 197 | 197 | Three-finger vertical swipes scroll bands; horizontal swipes switch workspaces. Scroll and fixed workspaces retain their existing gesture axes. A switch keeps the source workspace's axis throughout its transition; the destination's axes apply to the next gesture. The `focus-workspace-prev/next` commands switch to the previous or next workspace, whether the transition is vertical or horizontal. Four-finger and stash gestures keep their existing behaviour. | |
| 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. | |
| 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; selecting one (left/right, Return or a click) moves the maximization to it, and Escape gives it back. | |
| 200 | 200 | ||
| 201 | 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. | |
| 202 | 202 | ||
| @@ -196,7 +196,7 @@ Left/right focus follows column order across band boundaries. Up/down visits sta | |||||
| 196 | 196 | ||||
| 197 | Three-finger vertical swipes scroll bands; horizontal swipes switch workspaces. Scroll and fixed workspaces retain their existing gesture axes. A switch keeps the source workspace's axis throughout its transition; the destination's axes apply to the next gesture. The `focus-workspace-prev/next` commands switch to the previous or next workspace, whether the transition is vertical or horizontal. Four-finger and stash gestures keep their existing behaviour. | 197 | Three-finger vertical swipes scroll bands; horizontal swipes switch workspaces. Scroll and fixed workspaces retain their existing gesture axes. A switch keeps the source workspace's axis throughout its transition; the destination's axes apply to the next gesture. The `focus-workspace-prev/next` commands switch to the previous or next workspace, whether the transition is vertical or horizontal. Four-finger and stash gestures keep their existing behaviour. | ||
| 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. | 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; selecting one (left/right, Return or a click) 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. 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 | ||||
M internal/core/layout.go +8 -2
| @@ -1052,6 +1052,13 @@ func (w *Workspace) maximize(i int) { | |||
| 1052 | 1052 | } | |
| 1053 | 1053 | } | |
| 1054 | 1054 | ||
| 1055 | + | // hiddenByMaximized reports whether the maximized focused column hides | |
| 1056 | + | // column i on screen: in equal-cell layouts it hides the other columns of | |
| 1057 | + | // its band (the whole row in fixed overflow). | |
| 1058 | + | func (w *Workspace) hiddenByMaximized(i int) bool { | |
| 1059 | + | return w.policy().equalCells && w.Focus < len(w.Columns) && w.Columns[w.Focus].FullWidth && i != w.Focus && w.sameBand(i, w.Focus) | |
| 1060 | + | } | |
| 1061 | + | ||
| 1055 | 1062 | // unmaximize clears the focused column in scroll mode, or all maximized | |
| 1056 | 1063 | // columns in fixed mode (including after Escape changed focus). | |
| 1057 | 1064 | func (w *Workspace) unmaximize() { | |
| @@ -1637,8 +1644,7 @@ func (w *Workspace) appendLayout(dst []Placement) []Placement { | |||
| 1637 | 1644 | full := w.fullscreen == id && id != 0 | |
| 1638 | 1645 | // Fixed overflow cannot scroll to other columns while one fills | |
| 1639 | 1646 | // the view. Keep them in place, but out of the scene. | |
| 1640 | - | maximized := w.policy().equalCells && w.Columns[w.Focus].FullWidth && i != w.Focus && w.sameBand(i, w.Focus) | |
| 1641 | - | hidden := (cover != 0 && id != cover) || ((fullColumn || w.policy().equalCells && w.fullscreen != 0) && !full) || maximized | |
| 1647 | + | hidden := (cover != 0 && id != cover) || ((fullColumn || w.policy().equalCells && w.fullscreen != 0) && !full) || w.hiddenByMaximized(i) | |
| 1642 | 1648 | if full { | |
| 1643 | 1649 | // Scroll mode aligns the view on the column; fixed never scrolls. | |
| 1644 | 1650 | r = w.Output | |
| @@ -1052,6 +1052,13 @@ func (w *Workspace) maximize(i int) { | |||||
| 1052 | } | 1052 | } | ||
| 1053 | } | 1053 | } | ||
| 1054 | 1054 | ||||
| 1055 | + | // hiddenByMaximized reports whether the maximized focused column hides | |||
| 1056 | + | // column i on screen: in equal-cell layouts it hides the other columns of | |||
| 1057 | + | // its band (the whole row in fixed overflow). | |||
| 1058 | + | func (w *Workspace) hiddenByMaximized(i int) bool { | |||
| 1059 | + | return w.policy().equalCells && w.Focus < len(w.Columns) && w.Columns[w.Focus].FullWidth && i != w.Focus && w.sameBand(i, w.Focus) | |||
| 1060 | + | } | |||
| 1061 | + | ||||
| 1055 | // unmaximize clears the focused column in scroll mode, or all maximized | 1062 | // unmaximize clears the focused column in scroll mode, or all maximized | ||
| 1056 | // columns in fixed mode (including after Escape changed focus). | 1063 | // columns in fixed mode (including after Escape changed focus). | ||
| 1057 | func (w *Workspace) unmaximize() { | 1064 | func (w *Workspace) unmaximize() { | ||
| @@ -1637,8 +1644,7 @@ func (w *Workspace) appendLayout(dst []Placement) []Placement { | |||||
| 1637 | full := w.fullscreen == id && id != 0 | 1644 | full := w.fullscreen == id && id != 0 | ||
| 1638 | // Fixed overflow cannot scroll to other columns while one fills | 1645 | // Fixed overflow cannot scroll to other columns while one fills | ||
| 1639 | // the view. Keep them in place, but out of the scene. | 1646 | // the view. Keep them in place, but out of the scene. | ||
| 1640 | - | maximized := w.policy().equalCells && w.Columns[w.Focus].FullWidth && i != w.Focus && w.sameBand(i, w.Focus) | 1647 | + | hidden := (cover != 0 && id != cover) || ((fullColumn || w.policy().equalCells && w.fullscreen != 0) && !full) || w.hiddenByMaximized(i) |
| 1641 | - | hidden := (cover != 0 && id != cover) || ((fullColumn || w.policy().equalCells && w.fullscreen != 0) && !full) || maximized | |||
| 1642 | if full { | 1648 | if full { | ||
| 1643 | // Scroll mode aligns the view on the column; fixed never scrolls. | 1649 | // Scroll mode aligns the view on the column; fixed never scrolls. | ||
| 1644 | r = w.Output | 1650 | r = w.Output | ||
M internal/core/overview.go +63 -7
| @@ -970,17 +970,34 @@ func (w *Workspace) previewTilesInto(tiles []Placement) ([]Placement, int, Rect) | |||
| 970 | 970 | rects = w.colBuf | |
| 971 | 971 | span = w.Usable.W // Fixed overflow never scrolls, even with a stash pile. | |
| 972 | 972 | } | |
| 973 | - | maximized := w.policy().equalCells && len(w.Columns) > 0 && w.Columns[w.Focus].FullWidth | |
| 973 | + | if w.policy().wraps && len(w.Columns) > 0 && w.Columns[w.Focus].FullWidth { | |
| 974 | + | // The columns the maximized one hides are cards behind it, | |
| 975 | + | // farthest first so the nearest paint over them. | |
| 976 | + | first, last := w.laneBounds(w.laneOf(w.Focus)) | |
| 977 | + | for d := last - first; d > 0; d-- { | |
| 978 | + | if i := w.Focus - d; i >= first { | |
| 979 | + | tiles = w.appendBandCard(tiles, i) | |
| 980 | + | } | |
| 981 | + | if i := w.Focus + d; i <= last { | |
| 982 | + | tiles = w.appendBandCard(tiles, i) | |
| 983 | + | } | |
| 984 | + | } | |
| 985 | + | } | |
| 974 | 986 | for i, c := range w.Columns { | |
| 975 | 987 | r := Rect{X: w.columnX(i) - w.Usable.X, Y: g, W: w.columnWidth(i), H: h} | |
| 976 | 988 | if w.policy().wraps { | |
| 989 | + | if w.hiddenByMaximized(i) { | |
| 990 | + | continue // A card, above. | |
| 991 | + | } | |
| 977 | 992 | // The band is the lane: it is encoded in Y, one screen apart. | |
| 978 | - | // A maximized column fills its band as on screen. | |
| 993 | + | // A maximized column fills its band as on screen, inset for | |
| 994 | + | // the cards it hides. | |
| 979 | 995 | r.X = w.cellX(i) | |
| 980 | 996 | r.Y = g + w.band(i)*w.Usable.H | |
| 981 | 997 | r.W = w.cellWidth() | |
| 982 | 998 | if c.FullWidth { | |
| 983 | - | r.X, r.W = g, max(w.Usable.W-2*g, 0) | |
| 999 | + | in := w.bandCardsInset(i) | |
| 1000 | + | r.X, r.W = g+in, max(w.Usable.W-2*g-2*in, 0) | |
| 984 | 1001 | } | |
| 985 | 1002 | } else if w.Overflow == OverflowFixed { | |
| 986 | 1003 | r = rects[i] | |
| @@ -989,10 +1006,10 @@ func (w *Workspace) previewTilesInto(tiles []Placement) ([]Placement, int, Rect) | |||
| 989 | 1006 | // A fullscreen column shows at the width it had in the row. | |
| 990 | 1007 | r.W = max(w.Usable.W-2*g, 0) | |
| 991 | 1008 | } | |
| 992 | - | if maximized && i != w.Focus && w.sameBand(i, w.Focus) { | |
| 993 | - | // FullWidth hides the other columns of its band on screen. | |
| 994 | - | // Their old buffers cannot fit their current rects; show only | |
| 995 | - | // the maximized column until navigation restores the layout. | |
| 1009 | + | if w.hiddenByMaximized(i) { | |
| 1010 | + | // Fixed overflow shows the hidden columns on their own card | |
| 1011 | + | // (previewItem). Their old buffers cannot fit their current | |
| 1012 | + | // rects; show only the maximized column here. | |
| 996 | 1013 | for _, id := range c.Windows { | |
| 997 | 1014 | tiles = append(tiles, Placement{ID: id, Hidden: true}) | |
| 998 | 1015 | } | |
| @@ -1015,6 +1032,45 @@ func (w *Workspace) previewTilesInto(tiles []Placement) ([]Placement, int, Rect) | |||
| 1015 | 1032 | return tiles, span, sel | |
| 1016 | 1033 | } | |
| 1017 | 1034 | ||
| 1035 | + | // bandCardStep is the unscaled step between the cards of a cascade band | |
| 1036 | + | // whose maximized column hides the others: the stack peek step. | |
| 1037 | + | func (w *Workspace) bandCardStep() int { | |
| 1038 | + | return max(1, int(math.Round(float64(w.Usable.H)*overviewPeekStep))) | |
| 1039 | + | } | |
| 1040 | + | ||
| 1041 | + | // bandCardsInset is how far the maximized column i is inset on each side | |
| 1042 | + | // of its band in the overview, leaving room for up to two cards peeking | |
| 1043 | + | // out on that side; 0 when it hides nothing. | |
| 1044 | + | func (w *Workspace) bandCardsInset(i int) int { | |
| 1045 | + | first, last := w.laneBounds(w.laneOf(i)) | |
| 1046 | + | if first >= last { | |
| 1047 | + | return 0 | |
| 1048 | + | } | |
| 1049 | + | return 2 * w.bandCardStep() | |
| 1050 | + | } | |
| 1051 | + | ||
| 1052 | + | // appendBandCard appends hidden column i of a cascade band as a dimmed card | |
| 1053 | + | // at its cell size behind the maximized column, on its side: the nearest | |
| 1054 | + | // column one step out from the maximized one, the next ones two steps out | |
| 1055 | + | // (on the band's edge), like the cards of a stack. | |
| 1056 | + | func (w *Workspace) appendBandCard(tiles []Placement, i int) []Placement { | |
| 1057 | + | g, s, c := w.gap(), w.bandCardStep(), w.Columns[i] | |
| 1058 | + | r := Rect{Y: g + w.band(i)*w.Usable.H, W: w.cellWidth(), H: max(w.Usable.H-2*g, 0)} | |
| 1059 | + | k := min(max(i-w.Focus, w.Focus-i), 2) | |
| 1060 | + | if i < w.Focus { | |
| 1061 | + | r.X = g + (2-k)*s | |
| 1062 | + | } else { | |
| 1063 | + | r.X = w.Usable.W - g - (2-k)*s - r.W | |
| 1064 | + | } | |
| 1065 | + | w.rowBuf = rowRectsInto(w.rowBuf[:0], r, c, g) | |
| 1066 | + | for j, t := range w.rowBuf { | |
| 1067 | + | p := Placement{ID: c.Windows[j], Rect: t} | |
| 1068 | + | p.peeking(1) | |
| 1069 | + | tiles = append(tiles, p) | |
| 1070 | + | } | |
| 1071 | + | return tiles | |
| 1072 | + | } | |
| 1073 | + | ||
| 1018 | 1074 | // previewRow scales the tiles of w into a row placed in slot, centred | |
| 1019 | 1075 | // in the slot's width, or scrolled so the focused column shows when the | |
| 1020 | 1076 | // row is wider, right of the stash pile. Dimmed rows (neighbor | |
| @@ -970,17 +970,34 @@ func (w *Workspace) previewTilesInto(tiles []Placement) ([]Placement, int, Rect) | |||||
| 970 | rects = w.colBuf | 970 | rects = w.colBuf | ||
| 971 | span = w.Usable.W // Fixed overflow never scrolls, even with a stash pile. | 971 | span = w.Usable.W // Fixed overflow never scrolls, even with a stash pile. | ||
| 972 | } | 972 | } | ||
| 973 | - | maximized := w.policy().equalCells && len(w.Columns) > 0 && w.Columns[w.Focus].FullWidth | 973 | + | if w.policy().wraps && len(w.Columns) > 0 && w.Columns[w.Focus].FullWidth { |
| 974 | + | // The columns the maximized one hides are cards behind it, | |||
| 975 | + | // farthest first so the nearest paint over them. | |||
| 976 | + | first, last := w.laneBounds(w.laneOf(w.Focus)) | |||
| 977 | + | for d := last - first; d > 0; d-- { | |||
| 978 | + | if i := w.Focus - d; i >= first { | |||
| 979 | + | tiles = w.appendBandCard(tiles, i) | |||
| 980 | + | } | |||
| 981 | + | if i := w.Focus + d; i <= last { | |||
| 982 | + | tiles = w.appendBandCard(tiles, i) | |||
| 983 | + | } | |||
| 984 | + | } | |||
| 985 | + | } | |||
| 974 | for i, c := range w.Columns { | 986 | for i, c := range w.Columns { | ||
| 975 | r := Rect{X: w.columnX(i) - w.Usable.X, Y: g, W: w.columnWidth(i), H: h} | 987 | r := Rect{X: w.columnX(i) - w.Usable.X, Y: g, W: w.columnWidth(i), H: h} | ||
| 976 | if w.policy().wraps { | 988 | if w.policy().wraps { | ||
| 989 | + | if w.hiddenByMaximized(i) { | |||
| 990 | + | continue // A card, above. | |||
| 991 | + | } | |||
| 977 | // The band is the lane: it is encoded in Y, one screen apart. | 992 | // The band is the lane: it is encoded in Y, one screen apart. | ||
| 978 | - | // A maximized column fills its band as on screen. | 993 | + | // A maximized column fills its band as on screen, inset for |
| 994 | + | // the cards it hides. | |||
| 979 | r.X = w.cellX(i) | 995 | r.X = w.cellX(i) | ||
| 980 | r.Y = g + w.band(i)*w.Usable.H | 996 | r.Y = g + w.band(i)*w.Usable.H | ||
| 981 | r.W = w.cellWidth() | 997 | r.W = w.cellWidth() | ||
| 982 | if c.FullWidth { | 998 | if c.FullWidth { | ||
| 983 | - | r.X, r.W = g, max(w.Usable.W-2*g, 0) | 999 | + | in := w.bandCardsInset(i) |
| 1000 | + | r.X, r.W = g+in, max(w.Usable.W-2*g-2*in, 0) | |||
| 984 | } | 1001 | } | ||
| 985 | } else if w.Overflow == OverflowFixed { | 1002 | } else if w.Overflow == OverflowFixed { | ||
| 986 | r = rects[i] | 1003 | r = rects[i] | ||
| @@ -989,10 +1006,10 @@ func (w *Workspace) previewTilesInto(tiles []Placement) ([]Placement, int, Rect) | |||||
| 989 | // A fullscreen column shows at the width it had in the row. | 1006 | // A fullscreen column shows at the width it had in the row. | ||
| 990 | r.W = max(w.Usable.W-2*g, 0) | 1007 | r.W = max(w.Usable.W-2*g, 0) | ||
| 991 | } | 1008 | } | ||
| 992 | - | if maximized && i != w.Focus && w.sameBand(i, w.Focus) { | 1009 | + | if w.hiddenByMaximized(i) { |
| 993 | - | // FullWidth hides the other columns of its band on screen. | 1010 | + | // Fixed overflow shows the hidden columns on their own card |
| 994 | - | // Their old buffers cannot fit their current rects; show only | 1011 | + | // (previewItem). Their old buffers cannot fit their current |
| 995 | - | // the maximized column until navigation restores the layout. | 1012 | + | // rects; show only the maximized column here. |
| 996 | for _, id := range c.Windows { | 1013 | for _, id := range c.Windows { | ||
| 997 | tiles = append(tiles, Placement{ID: id, Hidden: true}) | 1014 | tiles = append(tiles, Placement{ID: id, Hidden: true}) | ||
| 998 | } | 1015 | } | ||
| @@ -1015,6 +1032,45 @@ func (w *Workspace) previewTilesInto(tiles []Placement) ([]Placement, int, Rect) | |||||
| 1015 | return tiles, span, sel | 1032 | return tiles, span, sel | ||
| 1016 | } | 1033 | } | ||
| 1017 | 1034 | ||||
| 1035 | + | // bandCardStep is the unscaled step between the cards of a cascade band | |||
| 1036 | + | // whose maximized column hides the others: the stack peek step. | |||
| 1037 | + | func (w *Workspace) bandCardStep() int { | |||
| 1038 | + | return max(1, int(math.Round(float64(w.Usable.H)*overviewPeekStep))) | |||
| 1039 | + | } | |||
| 1040 | + | ||||
| 1041 | + | // bandCardsInset is how far the maximized column i is inset on each side | |||
| 1042 | + | // of its band in the overview, leaving room for up to two cards peeking | |||
| 1043 | + | // out on that side; 0 when it hides nothing. | |||
| 1044 | + | func (w *Workspace) bandCardsInset(i int) int { | |||
| 1045 | + | first, last := w.laneBounds(w.laneOf(i)) | |||
| 1046 | + | if first >= last { | |||
| 1047 | + | return 0 | |||
| 1048 | + | } | |||
| 1049 | + | return 2 * w.bandCardStep() | |||
| 1050 | + | } | |||
| 1051 | + | ||||
| 1052 | + | // appendBandCard appends hidden column i of a cascade band as a dimmed card | |||
| 1053 | + | // at its cell size behind the maximized column, on its side: the nearest | |||
| 1054 | + | // column one step out from the maximized one, the next ones two steps out | |||
| 1055 | + | // (on the band's edge), like the cards of a stack. | |||
| 1056 | + | func (w *Workspace) appendBandCard(tiles []Placement, i int) []Placement { | |||
| 1057 | + | g, s, c := w.gap(), w.bandCardStep(), w.Columns[i] | |||
| 1058 | + | r := Rect{Y: g + w.band(i)*w.Usable.H, W: w.cellWidth(), H: max(w.Usable.H-2*g, 0)} | |||
| 1059 | + | k := min(max(i-w.Focus, w.Focus-i), 2) | |||
| 1060 | + | if i < w.Focus { | |||
| 1061 | + | r.X = g + (2-k)*s | |||
| 1062 | + | } else { | |||
| 1063 | + | r.X = w.Usable.W - g - (2-k)*s - r.W | |||
| 1064 | + | } | |||
| 1065 | + | w.rowBuf = rowRectsInto(w.rowBuf[:0], r, c, g) | |||
| 1066 | + | for j, t := range w.rowBuf { | |||
| 1067 | + | p := Placement{ID: c.Windows[j], Rect: t} | |||
| 1068 | + | p.peeking(1) | |||
| 1069 | + | tiles = append(tiles, p) | |||
| 1070 | + | } | |||
| 1071 | + | return tiles | |||
| 1072 | + | } | |||
| 1073 | + | ||||
| 1018 | // previewRow scales the tiles of w into a row placed in slot, centred | 1074 | // previewRow scales the tiles of w into a row placed in slot, centred | ||
| 1019 | // in the slot's width, or scrolled so the focused column shows when the | 1075 | // in the slot's width, or scrolled so the focused column shows when the | ||
| 1020 | // row is wider, right of the stash pile. Dimmed rows (neighbor | 1076 | // row is wider, right of the stash pile. Dimmed rows (neighbor | ||
M internal/core/overview_cascade_test.go +96 -19
| @@ -75,39 +75,116 @@ func TestOverviewCascadeZoomIgnoresBandCount(t *testing.T) { | |||
| 75 | 75 | } | |
| 76 | 76 | } | |
| 77 | 77 | ||
| 78 | - | // A maximized column fills its band in the preview as on screen, and hides | |
| 79 | - | // only the other columns of its band. | |
| 80 | - | func TestOverviewCascadeMaximizedColumnFillsBand(t *testing.T) { | |
| 78 | + | // A maximized column fills its band in the preview, inset for the columns | |
| 79 | + | // it hides: they are dimmed cards at their cell size behind it, on their | |
| 80 | + | // side, the nearest one step out, the farther ones two (the band's edge). | |
| 81 | + | // Other bands keep their cells. | |
| 82 | + | func TestOverviewCascadeMaximizedColumnCards(t *testing.T) { | |
| 81 | 83 | // Four columns, three per band: band 0 holds 0..2, band 1 holds 3. | |
| 82 | 84 | for _, tc := range []struct { | |
| 83 | 85 | n, max int | |
| 84 | - | hidden []bool | |
| 86 | + | cards []int // per column: 0 not a card, -k/+k k steps left/right | |
| 87 | + | order []int // paint order of band 0's columns | |
| 85 | 88 | }{ | |
| 86 | - | {n: 1, max: 0, hidden: []bool{false}}, | |
| 87 | - | {n: 4, max: 0, hidden: []bool{false, true, true, false}}, | |
| 88 | - | {n: 4, max: 3, hidden: []bool{false, false, false, false}}, | |
| 89 | + | {n: 1, max: 0, cards: []int{0}, order: []int{0}}, | |
| 90 | + | {n: 4, max: 0, cards: []int{0, 1, 2, 0}, order: []int{2, 1, 0}}, | |
| 91 | + | {n: 4, max: 1, cards: []int{-1, 0, 1, 0}, order: []int{0, 2, 1}}, | |
| 92 | + | {n: 4, max: 3, cards: []int{0, 0, 0, 0}}, | |
| 89 | 93 | } { | |
| 90 | 94 | w := cascadeWorkspace(tc.n) | |
| 91 | 95 | w.Focus = tc.max | |
| 92 | 96 | w.maximize(tc.max) | |
| 93 | - | tiles, _, _ := w.previewTiles() | |
| 94 | - | g, h := w.gap(), w.Usable.H-2*w.gap() | |
| 95 | - | for i, p := range tiles { | |
| 96 | - | if p.Hidden != tc.hidden[i] { | |
| 97 | - | t.Fatalf("n=%d max=%d column %d hidden %v, want %v", tc.n, tc.max, i, p.Hidden, tc.hidden[i]) | |
| 98 | - | } | |
| 99 | - | if p.Hidden { | |
| 100 | - | continue | |
| 97 | + | tiles, _, sel := w.previewTiles() | |
| 98 | + | g, h, s := w.gap(), w.Usable.H-2*w.gap(), w.bandCardStep() | |
| 99 | + | in := 0 | |
| 100 | + | if slices.ContainsFunc(tc.cards, func(k int) bool { return k != 0 }) { | |
| 101 | + | in = 2 * s | |
| 102 | + | } | |
| 103 | + | var order []int | |
| 104 | + | for _, p := range tiles { | |
| 105 | + | i := int(p.ID) - 1 | |
| 106 | + | if i/3 == 0 && tc.order != nil { | |
| 107 | + | order = append(order, i) | |
| 101 | 108 | } | |
| 102 | 109 | band := i / 3 | |
| 103 | 110 | want := Rect{X: w.cellX(i), Y: g + band*w.Usable.H, W: w.cellWidth(), H: h} | |
| 104 | - | if i == tc.max { | |
| 105 | - | want.X, want.W = g, w.Usable.W-2*g | |
| 111 | + | switch k := tc.cards[i]; { | |
| 112 | + | case i == tc.max: | |
| 113 | + | want.X, want.W = g+in, w.Usable.W-2*g-2*in | |
| 114 | + | case k < 0: | |
| 115 | + | want.X = g + (2+k)*s | |
| 116 | + | case k > 0: | |
| 117 | + | want.X = w.Usable.W - g - (2-k)*s - want.W | |
| 118 | + | } | |
| 119 | + | if p.Hidden || p.Peek != (tc.cards[i] != 0) || p.Focused != (i == tc.max) || p.Rect != want { | |
| 120 | + | t.Fatalf("n=%d max=%d column %d: %+v, want rect %+v card %v", tc.n, tc.max, i, p, want, tc.cards[i] != 0) | |
| 106 | 121 | } | |
| 107 | - | if p.Rect != want { | |
| 108 | - | t.Fatalf("n=%d max=%d column %d rect %+v, want %+v", tc.n, tc.max, i, p.Rect, want) | |
| 122 | + | if i == tc.max && sel != want { | |
| 123 | + | t.Fatalf("n=%d max=%d sel %+v, want %+v", tc.n, tc.max, sel, want) | |
| 109 | 124 | } | |
| 110 | 125 | } | |
| 126 | + | if tc.order != nil && !slices.Equal(order, tc.order) { | |
| 127 | + | t.Fatalf("n=%d max=%d paint order %v, want %v", tc.n, tc.max, order, tc.order) | |
| 128 | + | } | |
| 129 | + | } | |
| 130 | + | } | |
| 131 | + | ||
| 132 | + | // Moving onto a card behind a maximized column hands it the maximization; | |
| 133 | + | // Return keeps it there, Escape gives it back. Moving to another band | |
| 134 | + | // unmaximizes as before. | |
| 135 | + | func TestOverviewCascadeMaximizedCardSelection(t *testing.T) { | |
| 136 | + | for _, d := range cascadeDrivers { | |
| 137 | + | t.Run(d.name, func(t *testing.T) { | |
| 138 | + | m := cascadeOverview() | |
| 139 | + | w := m.Current() | |
| 140 | + | w.FocusID(9) // band 2 holds 7, 8, 9 | |
| 141 | + | w.ToggleFullWidth() | |
| 142 | + | m.ToggleOverview() | |
| 143 | + | for _, id := range []WindowID{7, 8} { | |
| 144 | + | if p := previewOf(t, m.Layout(), id); p.Hidden || !p.Peek { | |
| 145 | + | t.Fatalf("card %d: %+v", id, p) | |
| 146 | + | } | |
| 147 | + | } | |
| 148 | + | d.move(m, -1, 0) | |
| 149 | + | if focusedID(m) != 8 || !w.Columns[w.Focus].FullWidth || w.Columns[w.columnOf(9)].FullWidth { | |
| 150 | + | t.Fatalf("left: focus %d, maximization not moved", focusedID(m)) | |
| 151 | + | } | |
| 152 | + | if p := previewOf(t, m.Layout(), 9); !p.Peek { | |
| 153 | + | t.Fatalf("former maximized column not a card: %+v", p) | |
| 154 | + | } | |
| 155 | + | m.CancelOverview() | |
| 156 | + | if focusedID(m) != 9 || !w.Columns[w.Focus].FullWidth || w.Columns[w.columnOf(8)].FullWidth { | |
| 157 | + | t.Fatalf("escape: focus %d, maximization not restored", focusedID(m)) | |
| 158 | + | } | |
| 159 | + | ||
| 160 | + | m.ToggleOverview() | |
| 161 | + | d.move(m, -1, 0) | |
| 162 | + | d.move(m, -1, 0) | |
| 163 | + | m.ToggleOverview() // return | |
| 164 | + | if focusedID(m) != 7 || !w.Columns[w.Focus].FullWidth || w.Columns[w.columnOf(9)].FullWidth { | |
| 165 | + | t.Fatalf("return: focus %d, maximization not on 7", focusedID(m)) | |
| 166 | + | } | |
| 167 | + | ||
| 168 | + | m.ToggleOverview() | |
| 169 | + | d.move(m, 0, -1) | |
| 170 | + | if focusedID(m) != 4 || slices.ContainsFunc(w.Columns, func(c Column) bool { return c.FullWidth }) { | |
| 171 | + | t.Fatalf("up: focus %d, a column still maximized", focusedID(m)) | |
| 172 | + | } | |
| 173 | + | }) | |
| 174 | + | } | |
| 175 | + | } | |
| 176 | + | ||
| 177 | + | // A click on a card behind a maximized cascade column hands it the | |
| 178 | + | // maximization. | |
| 179 | + | func TestOverviewCascadeMaximizedCardPick(t *testing.T) { | |
| 180 | + | m := cascadeOverview() | |
| 181 | + | w := m.Current() | |
| 182 | + | w.FocusID(7) | |
| 183 | + | w.ToggleFullWidth() | |
| 184 | + | m.ToggleOverview() | |
| 185 | + | m.OverviewPick(9) | |
| 186 | + | if m.ov.open || focusedID(m) != 9 || !w.Columns[w.Focus].FullWidth || w.Columns[w.columnOf(7)].FullWidth { | |
| 187 | + | t.Fatalf("pick: open %v, focus %d, maximization not moved", m.ov.open, focusedID(m)) | |
| 111 | 188 | } | |
| 112 | 189 | } | |
| 113 | 190 | ||
| @@ -75,39 +75,116 @@ func TestOverviewCascadeZoomIgnoresBandCount(t *testing.T) { | |||||
| 75 | } | 75 | } | ||
| 76 | } | 76 | } | ||
| 77 | 77 | ||||
| 78 | - | // A maximized column fills its band in the preview as on screen, and hides | 78 | + | // A maximized column fills its band in the preview, inset for the columns |
| 79 | - | // only the other columns of its band. | 79 | + | // it hides: they are dimmed cards at their cell size behind it, on their |
| 80 | - | func TestOverviewCascadeMaximizedColumnFillsBand(t *testing.T) { | 80 | + | // side, the nearest one step out, the farther ones two (the band's edge). |
| 81 | + | // Other bands keep their cells. | |||
| 82 | + | func TestOverviewCascadeMaximizedColumnCards(t *testing.T) { | |||
| 81 | // Four columns, three per band: band 0 holds 0..2, band 1 holds 3. | 83 | // Four columns, three per band: band 0 holds 0..2, band 1 holds 3. | ||
| 82 | for _, tc := range []struct { | 84 | for _, tc := range []struct { | ||
| 83 | n, max int | 85 | n, max int | ||
| 84 | - | hidden []bool | 86 | + | cards []int // per column: 0 not a card, -k/+k k steps left/right |
| 87 | + | order []int // paint order of band 0's columns | |||
| 85 | }{ | 88 | }{ | ||
| 86 | - | {n: 1, max: 0, hidden: []bool{false}}, | 89 | + | {n: 1, max: 0, cards: []int{0}, order: []int{0}}, |
| 87 | - | {n: 4, max: 0, hidden: []bool{false, true, true, false}}, | 90 | + | {n: 4, max: 0, cards: []int{0, 1, 2, 0}, order: []int{2, 1, 0}}, |
| 88 | - | {n: 4, max: 3, hidden: []bool{false, false, false, false}}, | 91 | + | {n: 4, max: 1, cards: []int{-1, 0, 1, 0}, order: []int{0, 2, 1}}, |
| 92 | + | {n: 4, max: 3, cards: []int{0, 0, 0, 0}}, | |||
| 89 | } { | 93 | } { | ||
| 90 | w := cascadeWorkspace(tc.n) | 94 | w := cascadeWorkspace(tc.n) | ||
| 91 | w.Focus = tc.max | 95 | w.Focus = tc.max | ||
| 92 | w.maximize(tc.max) | 96 | w.maximize(tc.max) | ||
| 93 | - | tiles, _, _ := w.previewTiles() | 97 | + | tiles, _, sel := w.previewTiles() |
| 94 | - | g, h := w.gap(), w.Usable.H-2*w.gap() | 98 | + | g, h, s := w.gap(), w.Usable.H-2*w.gap(), w.bandCardStep() |
| 95 | - | for i, p := range tiles { | 99 | + | in := 0 |
| 96 | - | if p.Hidden != tc.hidden[i] { | 100 | + | if slices.ContainsFunc(tc.cards, func(k int) bool { return k != 0 }) { |
| 97 | - | t.Fatalf("n=%d max=%d column %d hidden %v, want %v", tc.n, tc.max, i, p.Hidden, tc.hidden[i]) | 101 | + | in = 2 * s |
| 98 | - | } | 102 | + | } |
| 99 | - | if p.Hidden { | 103 | + | var order []int |
| 100 | - | continue | 104 | + | for _, p := range tiles { |
| 105 | + | i := int(p.ID) - 1 | |||
| 106 | + | if i/3 == 0 && tc.order != nil { | |||
| 107 | + | order = append(order, i) | |||
| 101 | } | 108 | } | ||
| 102 | band := i / 3 | 109 | band := i / 3 | ||
| 103 | want := Rect{X: w.cellX(i), Y: g + band*w.Usable.H, W: w.cellWidth(), H: h} | 110 | want := Rect{X: w.cellX(i), Y: g + band*w.Usable.H, W: w.cellWidth(), H: h} | ||
| 104 | - | if i == tc.max { | 111 | + | switch k := tc.cards[i]; { |
| 105 | - | want.X, want.W = g, w.Usable.W-2*g | 112 | + | case i == tc.max: |
| 113 | + | want.X, want.W = g+in, w.Usable.W-2*g-2*in | |||
| 114 | + | case k < 0: | |||
| 115 | + | want.X = g + (2+k)*s | |||
| 116 | + | case k > 0: | |||
| 117 | + | want.X = w.Usable.W - g - (2-k)*s - want.W | |||
| 118 | + | } | |||
| 119 | + | if p.Hidden || p.Peek != (tc.cards[i] != 0) || p.Focused != (i == tc.max) || p.Rect != want { | |||
| 120 | + | t.Fatalf("n=%d max=%d column %d: %+v, want rect %+v card %v", tc.n, tc.max, i, p, want, tc.cards[i] != 0) | |||
| 106 | } | 121 | } | ||
| 107 | - | if p.Rect != want { | 122 | + | if i == tc.max && sel != want { |
| 108 | - | t.Fatalf("n=%d max=%d column %d rect %+v, want %+v", tc.n, tc.max, i, p.Rect, want) | 123 | + | t.Fatalf("n=%d max=%d sel %+v, want %+v", tc.n, tc.max, sel, want) |
| 109 | } | 124 | } | ||
| 110 | } | 125 | } | ||
| 126 | + | if tc.order != nil && !slices.Equal(order, tc.order) { | |||
| 127 | + | t.Fatalf("n=%d max=%d paint order %v, want %v", tc.n, tc.max, order, tc.order) | |||
| 128 | + | } | |||
| 129 | + | } | |||
| 130 | + | } | |||
| 131 | + | ||||
| 132 | + | // Moving onto a card behind a maximized column hands it the maximization; | |||
| 133 | + | // Return keeps it there, Escape gives it back. Moving to another band | |||
| 134 | + | // unmaximizes as before. | |||
| 135 | + | func TestOverviewCascadeMaximizedCardSelection(t *testing.T) { | |||
| 136 | + | for _, d := range cascadeDrivers { | |||
| 137 | + | t.Run(d.name, func(t *testing.T) { | |||
| 138 | + | m := cascadeOverview() | |||
| 139 | + | w := m.Current() | |||
| 140 | + | w.FocusID(9) // band 2 holds 7, 8, 9 | |||
| 141 | + | w.ToggleFullWidth() | |||
| 142 | + | m.ToggleOverview() | |||
| 143 | + | for _, id := range []WindowID{7, 8} { | |||
| 144 | + | if p := previewOf(t, m.Layout(), id); p.Hidden || !p.Peek { | |||
| 145 | + | t.Fatalf("card %d: %+v", id, p) | |||
| 146 | + | } | |||
| 147 | + | } | |||
| 148 | + | d.move(m, -1, 0) | |||
| 149 | + | if focusedID(m) != 8 || !w.Columns[w.Focus].FullWidth || w.Columns[w.columnOf(9)].FullWidth { | |||
| 150 | + | t.Fatalf("left: focus %d, maximization not moved", focusedID(m)) | |||
| 151 | + | } | |||
| 152 | + | if p := previewOf(t, m.Layout(), 9); !p.Peek { | |||
| 153 | + | t.Fatalf("former maximized column not a card: %+v", p) | |||
| 154 | + | } | |||
| 155 | + | m.CancelOverview() | |||
| 156 | + | if focusedID(m) != 9 || !w.Columns[w.Focus].FullWidth || w.Columns[w.columnOf(8)].FullWidth { | |||
| 157 | + | t.Fatalf("escape: focus %d, maximization not restored", focusedID(m)) | |||
| 158 | + | } | |||
| 159 | + | ||||
| 160 | + | m.ToggleOverview() | |||
| 161 | + | d.move(m, -1, 0) | |||
| 162 | + | d.move(m, -1, 0) | |||
| 163 | + | m.ToggleOverview() // return | |||
| 164 | + | if focusedID(m) != 7 || !w.Columns[w.Focus].FullWidth || w.Columns[w.columnOf(9)].FullWidth { | |||
| 165 | + | t.Fatalf("return: focus %d, maximization not on 7", focusedID(m)) | |||
| 166 | + | } | |||
| 167 | + | ||||
| 168 | + | m.ToggleOverview() | |||
| 169 | + | d.move(m, 0, -1) | |||
| 170 | + | if focusedID(m) != 4 || slices.ContainsFunc(w.Columns, func(c Column) bool { return c.FullWidth }) { | |||
| 171 | + | t.Fatalf("up: focus %d, a column still maximized", focusedID(m)) | |||
| 172 | + | } | |||
| 173 | + | }) | |||
| 174 | + | } | |||
| 175 | + | } | |||
| 176 | + | ||||
| 177 | + | // A click on a card behind a maximized cascade column hands it the | |||
| 178 | + | // maximization. | |||
| 179 | + | func TestOverviewCascadeMaximizedCardPick(t *testing.T) { | |||
| 180 | + | m := cascadeOverview() | |||
| 181 | + | w := m.Current() | |||
| 182 | + | w.FocusID(7) | |||
| 183 | + | w.ToggleFullWidth() | |||
| 184 | + | m.ToggleOverview() | |||
| 185 | + | m.OverviewPick(9) | |||
| 186 | + | if m.ov.open || focusedID(m) != 9 || !w.Columns[w.Focus].FullWidth || w.Columns[w.columnOf(7)].FullWidth { | |||
| 187 | + | t.Fatalf("pick: open %v, focus %d, maximization not moved", m.ov.open, focusedID(m)) | |||
| 111 | } | 188 | } | ||
| 112 | } | 189 | } | ||
| 113 | 190 | ||||
M internal/core/overview_stack.go +5 -1
| @@ -214,7 +214,11 @@ func (m *Monitor) hiddenColumn(w *Workspace) int { | |||
| 214 | 214 | // selectOverviewColumn retains the existing ordinary-row live focus semantics. | |
| 215 | 215 | func (w *Workspace) selectOverviewColumn(i int) { | |
| 216 | 216 | w.floatFocus, w.stashFocus = false, false | |
| 217 | - | if w.policy().equalCells && w.Focus != i && w.Focus < len(w.Columns) { | |
| 217 | + | if w.policy().wraps && w.hiddenByMaximized(i) { | |
| 218 | + | // A card behind a maximized cascade column takes the | |
| 219 | + | // maximization; Escape gives it back (restoreRow). | |
| 220 | + | w.transferMaximization(i) | |
| 221 | + | } else if w.policy().equalCells && w.Focus != i && w.Focus < len(w.Columns) { | |
| 218 | 222 | w.unmaximize() | |
| 219 | 223 | } | |
| 220 | 224 | w.Focus = i | |
| @@ -214,7 +214,11 @@ func (m *Monitor) hiddenColumn(w *Workspace) int { | |||||
| 214 | // selectOverviewColumn retains the existing ordinary-row live focus semantics. | 214 | // selectOverviewColumn retains the existing ordinary-row live focus semantics. | ||
| 215 | func (w *Workspace) selectOverviewColumn(i int) { | 215 | func (w *Workspace) selectOverviewColumn(i int) { | ||
| 216 | w.floatFocus, w.stashFocus = false, false | 216 | w.floatFocus, w.stashFocus = false, false | ||
| 217 | - | if w.policy().equalCells && w.Focus != i && w.Focus < len(w.Columns) { | 217 | + | if w.policy().wraps && w.hiddenByMaximized(i) { |
| 218 | + | // A card behind a maximized cascade column takes the | |||
| 219 | + | // maximization; Escape gives it back (restoreRow). | |||
| 220 | + | w.transferMaximization(i) | |||
| 221 | + | } else if w.policy().equalCells && w.Focus != i && w.Focus < len(w.Columns) { | |||
| 218 | w.unmaximize() | 222 | w.unmaximize() | ||
| 219 | } | 223 | } | ||
| 220 | w.Focus = i | 224 | w.Focus = i | ||