fix: v0.6.0 pre-release review fixes (#146)
* fix(config): name the replacement for renamed workspace actions Binds using *-workspace-up/down now report the -prev/-next action they were renamed to instead of a generic unknown action. * test(config): check cascade inheritance and the ignored column widths warning TestAssembleSessionFailure uses a temporary capture allowlist and describes what it checks. * docs: describe cascade in the example config and literal ; in commands * fix(layout): reset the view on a content axis change and stop cascade focus at edge bands Switching a workspace between cascade and scroll or fixed kept the old axis offset, leaving part of the screen empty. focus-window-up/down in cascade no longer leave the workspace past the first or last band, matching three-finger swipes. * refactor(layout): drop the appendLast policy flag and ignore tiles under panels for drops appendLast always matched equalCells. Drop targets now use the usable area like separators and the pulse. * fix(overview): report no change for three-finger steps that move nothing Scroll previews no longer compute column positions that cascade and fixed overwrite. Card deselection goes through clearCard, stale axis comments are updated, and stash input tests send swipes through handleInput. * perf(overview): snapshot the selection only before a three-finger step overviewScroll took the snapshot on every scroll event; stackFront allocates, so wheel and sub-threshold finger updates now skip it.
- commit
- 2732965f7545ae8c156a98a37c9dce9b0b162833
- author
- bnema · · 2026-10-07 17:20 UTC
- committer
- GitHub · · 2026-10-07 17:20 UTC
- parent
- 76cf032
- signature
- GPG Unverified
B5690EEEBB952194The key is unknown - refs
- tag: v0.6.0
18 files · +223 -105
M docs/config.md +1 -1
| @@ -192,7 +192,7 @@ HDR requires DRM HDR connector properties, suitable KMS planes, and Vulkan fp16 | |||
| 192 | 192 | ||
| 193 | 193 | Set `layout.overflow = cascade` globally, per output, or on a named workspace. Columns fill bands of up to `max-columns`, then continue in a fresh band below. The columns of a band share its width equally, as in `fixed` overflow: a lone column takes the full width until another joins its band. New columns append last; closing a column compacts the following bands. Each band fills the usable height after panel reservations. | |
| 194 | 194 | ||
| 195 | - | Left/right focus follows column order across band boundaries. Up/down visits stacked windows first, then the closest column in the adjacent band. Column width presets and resizing do nothing; window height shares still apply. Maximizing a column hides its same-band siblings until focus moves or maximization ends. | |
| 195 | + | Left/right focus follows column order across band boundaries. Up/down visits stacked windows first, then the closest column in the adjacent band, and stops at the first and last band. Column width presets and resizing do nothing; window height shares still apply. Maximizing a column hides its same-band siblings until focus moves or maximization ends. | |
| 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 | ||
| @@ -192,7 +192,7 @@ HDR requires DRM HDR connector properties, suitable KMS planes, and Vulkan fp16 | |||||
| 192 | 192 | ||||
| 193 | Set `layout.overflow = cascade` globally, per output, or on a named workspace. Columns fill bands of up to `max-columns`, then continue in a fresh band below. The columns of a band share its width equally, as in `fixed` overflow: a lone column takes the full width until another joins its band. New columns append last; closing a column compacts the following bands. Each band fills the usable height after panel reservations. | 193 | Set `layout.overflow = cascade` globally, per output, or on a named workspace. Columns fill bands of up to `max-columns`, then continue in a fresh band below. The columns of a band share its width equally, as in `fixed` overflow: a lone column takes the full width until another joins its band. New columns append last; closing a column compacts the following bands. Each band fills the usable height after panel reservations. | ||
| 194 | 194 | ||||
| 195 | - | Left/right focus follows column order across band boundaries. Up/down visits stacked windows first, then the closest column in the adjacent band. Column width presets and resizing do nothing; window height shares still apply. Maximizing a column hides its same-band siblings until focus moves or maximization ends. | 195 | + | Left/right focus follows column order across band boundaries. Up/down visits stacked windows first, then the closest column in the adjacent band, and stops at the first and last band. Column width presets and resizing do nothing; window height shares still apply. Maximizing a column hides its same-band siblings until focus moves or maximization ends. |
| 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 | ||||
M docs/desktop.md +1 -1
| @@ -12,7 +12,7 @@ startup = waybar | |||
| 12 | 12 | startup = wl-paste --watch cliphist store | |
| 13 | 13 | ``` | |
| 14 | 14 | ||
| 15 | - | `spawn` binds and `startup` lines can hold several commands separated by `;`. They all start right away, in order; one does not wait for the previous one to finish. Empty commands are ignored. `terminal` takes a single command. | |
| 15 | + | `spawn` binds and `startup` lines can hold several commands separated by `;`. They all start right away, in order; one does not wait for the previous one to finish. Empty commands are ignored. A `;` always separates commands, so put an argument containing `;` in a script. `terminal` and workspace slot commands (`column.N`) take a single command. | |
| 16 | 16 | ||
| 17 | 17 | ```text | |
| 18 | 18 | startup = waybar; mako; wl-paste --watch cliphist store | |
| @@ -12,7 +12,7 @@ startup = waybar | |||||
| 12 | startup = wl-paste --watch cliphist store | 12 | startup = wl-paste --watch cliphist store | ||
| 13 | ``` | 13 | ``` | ||
| 14 | 14 | ||||
| 15 | - | `spawn` binds and `startup` lines can hold several commands separated by `;`. They all start right away, in order; one does not wait for the previous one to finish. Empty commands are ignored. `terminal` takes a single command. | 15 | + | `spawn` binds and `startup` lines can hold several commands separated by `;`. They all start right away, in order; one does not wait for the previous one to finish. Empty commands are ignored. A `;` always separates commands, so put an argument containing `;` in a script. `terminal` and workspace slot commands (`column.N`) take a single command. |
| 16 | 16 | ||||
| 17 | ```text | 17 | ```text | ||
| 18 | startup = waybar; mako; wl-paste --watch cliphist store | 18 | startup = waybar; mako; wl-paste --watch cliphist store | ||
M examples/config +11 -8
| @@ -19,7 +19,7 @@ terminal.auto-open = first | |||
| 19 | 19 | ||
| 20 | 20 | # Commands run once when the session starts; separate several with `;` or | |
| 21 | 21 | # use one line each. Like spawn, a command runs without a shell: see | |
| 22 | - | # "Running commands" in the README. | |
| 22 | + | # "Running commands" in docs/desktop.md. | |
| 23 | 23 | # Programs get WAYLAND_DISPLAY and NEFERWL_STATE. | |
| 24 | 24 | # startup = waybar; mako | |
| 25 | 25 | # startup = wl-paste --watch cliphist store | |
| @@ -41,15 +41,16 @@ layout.gaps = 0 | |||
| 41 | 41 | # A lone column fills the screen without separators. | |
| 42 | 42 | layout.max-columns = 2 | |
| 43 | 43 | # Past max-columns: scroll opens columns to the right; fixed keeps everything | |
| 44 | - | # on screen, each new window splitting the newest one (spiral). | |
| 44 | + | # on screen, each new window splitting the newest one (spiral); cascade starts | |
| 45 | + | # a new band of columns below, scrolling vertically. | |
| 45 | 46 | layout.overflow = scroll | |
| 46 | 47 | # cycle-column-width steps through these, then back to the equal share. | |
| 47 | 48 | layout.presets = 1/3, 1/2, 2/3, 1 | |
| 48 | 49 | ||
| 49 | 50 | # Touchpad. A three-finger swipe up or down slides between workspaces, left | |
| 50 | - | # or right scrolls the columns. natural-scroll = on moves the content with | |
| 51 | - | # the fingers, for scroll and swipes. tap = on clicks on a tap: one finger | |
| 52 | - | # left, two right, three middle. | |
| 51 | + | # or right scrolls the columns (cascade swaps the two axes). natural-scroll | |
| 52 | + | # = on moves the content with the fingers, for scroll and swipes. tap = on | |
| 53 | + | # clicks on a tap: one finger left, two right, three middle. | |
| 53 | 54 | touchpad.natural-scroll = off | |
| 54 | 55 | touchpad.tap = on | |
| 55 | 56 | # Pointer speed from -1 to 1; adaptive speeds up quick moves, flat does not. | |
| @@ -134,7 +135,8 @@ log.debug = | |||
| 134 | 135 | # Keys are what your layout prints (cmd+é on AZERTY). Write = # and space | |
| 135 | 136 | # by name: cmd+equal, cmd+numbersign, cmd+space. `none` removes a default. | |
| 136 | 137 | # `spawn <command>` runs a program; arguments split on spaces, `;` starts | |
| 137 | - | # another command, no shell (pipes and variables go in a script: see "Running commands" in the README). | |
| 138 | + | # another command, no shell (pipes and variables go in a script: see | |
| 139 | + | # "Running commands" in docs/desktop.md). | |
| 138 | 140 | bind.cmd+return = spawn-terminal | |
| 139 | 141 | bind.cmd+left = focus-column-left | |
| 140 | 142 | bind.cmd+right = focus-column-right | |
| @@ -180,8 +182,9 @@ bind.cmd+q = close-window | |||
| 180 | 182 | bind.ctrl+alt+backspace = quit | |
| 181 | 183 | bind.ctrl+cmd+space = spawn fuzzel | |
| 182 | 184 | ||
| 183 | - | # Workspaces stack vertically; numbers follow their order. An empty one always | |
| 184 | - | # waits below the last. cmd+up/down past the column edge also switch workspace. | |
| 185 | + | # Workspaces stack vertically (side by side with cascade); numbers follow | |
| 186 | + | # their order. An empty one always waits after the last. cmd+up/down past the | |
| 187 | + | # column edge also switch workspace. | |
| 185 | 188 | bind.cmd+pageup = focus-workspace-prev | |
| 186 | 189 | bind.cmd+pagedown = focus-workspace-next | |
| 187 | 190 | # move-column-to-workspace moves the whole column; move-window-to-workspace | |
| @@ -19,7 +19,7 @@ terminal.auto-open = first | |||||
| 19 | 19 | ||||
| 20 | # Commands run once when the session starts; separate several with `;` or | 20 | # Commands run once when the session starts; separate several with `;` or | ||
| 21 | # use one line each. Like spawn, a command runs without a shell: see | 21 | # use one line each. Like spawn, a command runs without a shell: see | ||
| 22 | - | # "Running commands" in the README. | 22 | + | # "Running commands" in docs/desktop.md. |
| 23 | # Programs get WAYLAND_DISPLAY and NEFERWL_STATE. | 23 | # Programs get WAYLAND_DISPLAY and NEFERWL_STATE. | ||
| 24 | # startup = waybar; mako | 24 | # startup = waybar; mako | ||
| 25 | # startup = wl-paste --watch cliphist store | 25 | # startup = wl-paste --watch cliphist store | ||
| @@ -41,15 +41,16 @@ layout.gaps = 0 | |||||
| 41 | # A lone column fills the screen without separators. | 41 | # A lone column fills the screen without separators. | ||
| 42 | layout.max-columns = 2 | 42 | layout.max-columns = 2 | ||
| 43 | # Past max-columns: scroll opens columns to the right; fixed keeps everything | 43 | # Past max-columns: scroll opens columns to the right; fixed keeps everything | ||
| 44 | - | # on screen, each new window splitting the newest one (spiral). | 44 | + | # on screen, each new window splitting the newest one (spiral); cascade starts |
| 45 | + | # a new band of columns below, scrolling vertically. | |||
| 45 | layout.overflow = scroll | 46 | layout.overflow = scroll | ||
| 46 | # cycle-column-width steps through these, then back to the equal share. | 47 | # cycle-column-width steps through these, then back to the equal share. | ||
| 47 | layout.presets = 1/3, 1/2, 2/3, 1 | 48 | layout.presets = 1/3, 1/2, 2/3, 1 | ||
| 48 | 49 | ||||
| 49 | # Touchpad. A three-finger swipe up or down slides between workspaces, left | 50 | # Touchpad. A three-finger swipe up or down slides between workspaces, left | ||
| 50 | - | # or right scrolls the columns. natural-scroll = on moves the content with | 51 | + | # or right scrolls the columns (cascade swaps the two axes). natural-scroll |
| 51 | - | # the fingers, for scroll and swipes. tap = on clicks on a tap: one finger | 52 | + | # = on moves the content with the fingers, for scroll and swipes. tap = on |
| 52 | - | # left, two right, three middle. | 53 | + | # clicks on a tap: one finger left, two right, three middle. |
| 53 | touchpad.natural-scroll = off | 54 | touchpad.natural-scroll = off | ||
| 54 | touchpad.tap = on | 55 | touchpad.tap = on | ||
| 55 | # Pointer speed from -1 to 1; adaptive speeds up quick moves, flat does not. | 56 | # Pointer speed from -1 to 1; adaptive speeds up quick moves, flat does not. | ||
| @@ -134,7 +135,8 @@ log.debug = | |||||
| 134 | # Keys are what your layout prints (cmd+é on AZERTY). Write = # and space | 135 | # Keys are what your layout prints (cmd+é on AZERTY). Write = # and space | ||
| 135 | # by name: cmd+equal, cmd+numbersign, cmd+space. `none` removes a default. | 136 | # by name: cmd+equal, cmd+numbersign, cmd+space. `none` removes a default. | ||
| 136 | # `spawn <command>` runs a program; arguments split on spaces, `;` starts | 137 | # `spawn <command>` runs a program; arguments split on spaces, `;` starts | ||
| 137 | - | # another command, no shell (pipes and variables go in a script: see "Running commands" in the README). | 138 | + | # another command, no shell (pipes and variables go in a script: see |
| 139 | + | # "Running commands" in docs/desktop.md). | |||
| 138 | bind.cmd+return = spawn-terminal | 140 | bind.cmd+return = spawn-terminal | ||
| 139 | bind.cmd+left = focus-column-left | 141 | bind.cmd+left = focus-column-left | ||
| 140 | bind.cmd+right = focus-column-right | 142 | bind.cmd+right = focus-column-right | ||
| @@ -180,8 +182,9 @@ bind.cmd+q = close-window | |||||
| 180 | bind.ctrl+alt+backspace = quit | 182 | bind.ctrl+alt+backspace = quit | ||
| 181 | bind.ctrl+cmd+space = spawn fuzzel | 183 | bind.ctrl+cmd+space = spawn fuzzel | ||
| 182 | 184 | ||||
| 183 | - | # Workspaces stack vertically; numbers follow their order. An empty one always | 185 | + | # Workspaces stack vertically (side by side with cascade); numbers follow |
| 184 | - | # waits below the last. cmd+up/down past the column edge also switch workspace. | 186 | + | # their order. An empty one always waits after the last. cmd+up/down past the |
| 187 | + | # column edge also switch workspace. | |||
| 185 | bind.cmd+pageup = focus-workspace-prev | 188 | bind.cmd+pageup = focus-workspace-prev | ||
| 186 | bind.cmd+pagedown = focus-workspace-next | 189 | bind.cmd+pagedown = focus-workspace-next | ||
| 187 | # move-column-to-workspace moves the whole column; move-window-to-workspace | 190 | # move-column-to-workspace moves the whole column; move-window-to-workspace | ||
M internal/adapters/config/cascade_test.go +13 -4
| @@ -5,16 +5,25 @@ import ( | |||
| 5 | 5 | "testing" | |
| 6 | 6 | ) | |
| 7 | 7 | ||
| 8 | - | func TestCascadeLayoutInheritance(t *testing.T) { | |
| 9 | - | c, w := parseString(t, "layout.overflow = cascade\nlayout.DP-1.overflow = cascade\nworkspace.work.overflow = cascade\nbind.cmd+w = workspace work\n") | |
| 8 | + | func TestCascadeOverflowInherited(t *testing.T) { | |
| 9 | + | c, w := parseString(t, "layout.overflow = cascade\nlayout.DP-1.max-columns = 3\nworkspace.work.max-columns = 2\nbind.cmd+w = workspace work\n") | |
| 10 | 10 | if len(w) != 0 { | |
| 11 | 11 | t.Fatalf("warnings=%v", w) | |
| 12 | 12 | } | |
| 13 | - | if c.Layout.Overflow != "cascade" || c.Layout.Outputs[0].Overflow != "cascade" || c.Workspaces[0].Overflow != "cascade" { | |
| 14 | - | t.Fatalf("cascade inheritance=%+v", c.Layout) | |
| 13 | + | out := c.Layout.Outputs[0].LayoutRules.Over(c.Layout.LayoutRules) | |
| 14 | + | ws := c.Workspaces[0].LayoutRules.Over(out) | |
| 15 | + | if out.Overflow != "cascade" || ws.Overflow != "cascade" { | |
| 16 | + | t.Fatalf("output=%q workspace=%q, want cascade", out.Overflow, ws.Overflow) | |
| 15 | 17 | } | |
| 16 | 18 | _, warnings, err := Parse(strings.NewReader("layout.overflow = unknown\n")) | |
| 17 | 19 | if err != nil || len(warnings) != 1 || !strings.Contains(warnings[0].Msg, "must be scroll, fixed or cascade") { | |
| 18 | 20 | t.Fatalf("invalid overflow diagnostic: %v, %v", warnings, err) | |
| 19 | 21 | } | |
| 20 | 22 | } | |
| 23 | + | ||
| 24 | + | func TestCascadeIgnoresColumnWidths(t *testing.T) { | |
| 25 | + | _, w := parseString(t, "workspace.work.overflow = cascade\nworkspace.work.column.1 = 50%, foot\nbind.cmd+w = workspace work\n") | |
| 26 | + | if len(w) != 1 || !strings.Contains(w[0].Msg, "ignored with overflow = cascade") { | |
| 27 | + | t.Fatalf("warnings=%v", w) | |
| 28 | + | } | |
| 29 | + | } | |
| @@ -5,16 +5,25 @@ import ( | |||||
| 5 | "testing" | 5 | "testing" | ||
| 6 | ) | 6 | ) | ||
| 7 | 7 | ||||
| 8 | - | func TestCascadeLayoutInheritance(t *testing.T) { | 8 | + | func TestCascadeOverflowInherited(t *testing.T) { |
| 9 | - | c, w := parseString(t, "layout.overflow = cascade\nlayout.DP-1.overflow = cascade\nworkspace.work.overflow = cascade\nbind.cmd+w = workspace work\n") | 9 | + | c, w := parseString(t, "layout.overflow = cascade\nlayout.DP-1.max-columns = 3\nworkspace.work.max-columns = 2\nbind.cmd+w = workspace work\n") |
| 10 | if len(w) != 0 { | 10 | if len(w) != 0 { | ||
| 11 | t.Fatalf("warnings=%v", w) | 11 | t.Fatalf("warnings=%v", w) | ||
| 12 | } | 12 | } | ||
| 13 | - | if c.Layout.Overflow != "cascade" || c.Layout.Outputs[0].Overflow != "cascade" || c.Workspaces[0].Overflow != "cascade" { | 13 | + | out := c.Layout.Outputs[0].LayoutRules.Over(c.Layout.LayoutRules) |
| 14 | - | t.Fatalf("cascade inheritance=%+v", c.Layout) | 14 | + | ws := c.Workspaces[0].LayoutRules.Over(out) |
| 15 | + | if out.Overflow != "cascade" || ws.Overflow != "cascade" { | |||
| 16 | + | t.Fatalf("output=%q workspace=%q, want cascade", out.Overflow, ws.Overflow) | |||
| 15 | } | 17 | } | ||
| 16 | _, warnings, err := Parse(strings.NewReader("layout.overflow = unknown\n")) | 18 | _, warnings, err := Parse(strings.NewReader("layout.overflow = unknown\n")) | ||
| 17 | if err != nil || len(warnings) != 1 || !strings.Contains(warnings[0].Msg, "must be scroll, fixed or cascade") { | 19 | if err != nil || len(warnings) != 1 || !strings.Contains(warnings[0].Msg, "must be scroll, fixed or cascade") { | ||
| 18 | t.Fatalf("invalid overflow diagnostic: %v, %v", warnings, err) | 20 | t.Fatalf("invalid overflow diagnostic: %v, %v", warnings, err) | ||
| 19 | } | 21 | } | ||
| 20 | } | 22 | } | ||
| 23 | + | ||||
| 24 | + | func TestCascadeIgnoresColumnWidths(t *testing.T) { | |||
| 25 | + | _, w := parseString(t, "workspace.work.overflow = cascade\nworkspace.work.column.1 = 50%, foot\nbind.cmd+w = workspace work\n") | |||
| 26 | + | if len(w) != 1 || !strings.Contains(w[0].Msg, "ignored with overflow = cascade") { | |||
| 27 | + | t.Fatalf("warnings=%v", w) | |||
| 28 | + | } | |||
| 29 | + | } | |||
M internal/adapters/config/config.go +5 -0
| @@ -1095,6 +1095,11 @@ func checkAction(v string) error { | |||
| 1095 | 1095 | return nil | |
| 1096 | 1096 | } | |
| 1097 | 1097 | } | |
| 1098 | + | for old, repl := range map[string]string{"-workspace-up": "-workspace-prev", "-workspace-down": "-workspace-next"} { | |
| 1099 | + | if base, ok := strings.CutSuffix(v, old); ok && actions[base+repl] { | |
| 1100 | + | return fmt.Errorf("action %q was renamed to %q", v, base+repl) | |
| 1101 | + | } | |
| 1102 | + | } | |
| 1098 | 1103 | if !actions[v] { | |
| 1099 | 1104 | return fmt.Errorf(`unknown action %q (or "spawn <command>", "workspace <name>")`, v) | |
| 1100 | 1105 | } | |
| @@ -1095,6 +1095,11 @@ func checkAction(v string) error { | |||||
| 1095 | return nil | 1095 | return nil | ||
| 1096 | } | 1096 | } | ||
| 1097 | } | 1097 | } | ||
| 1098 | + | for old, repl := range map[string]string{"-workspace-up": "-workspace-prev", "-workspace-down": "-workspace-next"} { | |||
| 1099 | + | if base, ok := strings.CutSuffix(v, old); ok && actions[base+repl] { | |||
| 1100 | + | return fmt.Errorf("action %q was renamed to %q", v, base+repl) | |||
| 1101 | + | } | |||
| 1102 | + | } | |||
| 1098 | if !actions[v] { | 1103 | if !actions[v] { | ||
| 1099 | return fmt.Errorf(`unknown action %q (or "spawn <command>", "workspace <name>")`, v) | 1104 | return fmt.Errorf(`unknown action %q (or "spawn <command>", "workspace <name>")`, v) | ||
| 1100 | } | 1105 | } | ||
M internal/adapters/config/config_test.go +3 -3
| @@ -283,9 +283,9 @@ func TestWarningsKeepDefaults(t *testing.T) { | |||
| 283 | 283 | {"bind.cmd+ = quit", "missing key"}, | |
| 284 | 284 | {"bind.hyper+x = quit", "unknown modifier"}, | |
| 285 | 285 | {"bind.cmd+x = bogus", "unknown action"}, | |
| 286 | - | {"bind.cmd+x = focus-workspace-up", "unknown action"}, | |
| 287 | - | {"bind.cmd+x = move-workspace-down", "unknown action"}, | |
| 288 | - | {"bind.cmd+x = move-column-to-workspace-up", "unknown action"}, | |
| 286 | + | {"bind.cmd+x = focus-workspace-up", `renamed to "focus-workspace-prev"`}, | |
| 287 | + | {"bind.cmd+x = move-workspace-down", `renamed to "move-workspace-next"`}, | |
| 288 | + | {"bind.cmd+x = move-column-to-workspace-up", `renamed to "move-column-to-workspace-prev"`}, | |
| 289 | 289 | {"bind.cmd+x = set-column-width 10%", "set-column-width needs"}, | |
| 290 | 290 | {"bind.cmd+x = set-column-width +0%", "set-column-width needs"}, | |
| 291 | 291 | {"bind.cmd+x = set-window-height -101%", "set-window-height needs"}, | |
| @@ -283,9 +283,9 @@ func TestWarningsKeepDefaults(t *testing.T) { | |||||
| 283 | {"bind.cmd+ = quit", "missing key"}, | 283 | {"bind.cmd+ = quit", "missing key"}, | ||
| 284 | {"bind.hyper+x = quit", "unknown modifier"}, | 284 | {"bind.hyper+x = quit", "unknown modifier"}, | ||
| 285 | {"bind.cmd+x = bogus", "unknown action"}, | 285 | {"bind.cmd+x = bogus", "unknown action"}, | ||
| 286 | - | {"bind.cmd+x = focus-workspace-up", "unknown action"}, | 286 | + | {"bind.cmd+x = focus-workspace-up", `renamed to "focus-workspace-prev"`}, |
| 287 | - | {"bind.cmd+x = move-workspace-down", "unknown action"}, | 287 | + | {"bind.cmd+x = move-workspace-down", `renamed to "move-workspace-next"`}, |
| 288 | - | {"bind.cmd+x = move-column-to-workspace-up", "unknown action"}, | 288 | + | {"bind.cmd+x = move-column-to-workspace-up", `renamed to "move-column-to-workspace-prev"`}, |
| 289 | {"bind.cmd+x = set-column-width 10%", "set-column-width needs"}, | 289 | {"bind.cmd+x = set-column-width 10%", "set-column-width needs"}, | ||
| 290 | {"bind.cmd+x = set-column-width +0%", "set-column-width needs"}, | 290 | {"bind.cmd+x = set-column-width +0%", "set-column-width needs"}, | ||
| 291 | {"bind.cmd+x = set-window-height -101%", "set-window-height needs"}, | 291 | {"bind.cmd+x = set-window-height -101%", "set-window-height needs"}, | ||
M internal/app/headless_test.go +6 -3
| @@ -1,6 +1,8 @@ | |||
| 1 | 1 | package app | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | + | "os" | |
| 5 | + | "path/filepath" | |
| 4 | 6 | "strings" | |
| 5 | 7 | "testing" | |
| 6 | 8 | ||
| @@ -40,11 +42,12 @@ func TestVirtualRendererExportsHDRTargets(t *testing.T) { | |||
| 40 | 42 | } | |
| 41 | 43 | } | |
| 42 | 44 | ||
| 43 | - | // A failed assembly returns an error and releases what it acquired: without | |
| 44 | - | // a runtime directory wayland.New, the last fallible step, fails. | |
| 45 | + | // A failed assembly returns its error and no session: without a runtime | |
| 46 | + | // directory wayland.New, the last fallible step, fails. | |
| 45 | 47 | func TestAssembleSessionFailure(t *testing.T) { | |
| 46 | 48 | t.Setenv("XDG_RUNTIME_DIR", "") | |
| 47 | - | s, err := assembleSession(t.Context(), Options{Backend: "headless", Config: config.Defaults(), NoXwayland: true}) | |
| 49 | + | allowPath := filepath.Join(t.TempDir(), "capture-allow") | |
| 50 | + | s, err := assembleSession(t.Context(), Options{Backend: "headless", Config: config.Defaults(), NoXwayland: true, captureAllowPath: allowPath, captureAllowOwner: uint32(os.Getuid())}) | |
| 48 | 51 | if err == nil || !strings.Contains(err.Error(), "XDG_RUNTIME_DIR") { | |
| 49 | 52 | t.Fatalf("expected runtime directory failure, got %v", err) | |
| 50 | 53 | } | |
| @@ -1,6 +1,8 @@ | |||||
| 1 | package app | 1 | package app | ||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | + | "os" | |||
| 5 | + | "path/filepath" | |||
| 4 | "strings" | 6 | "strings" | ||
| 5 | "testing" | 7 | "testing" | ||
| 6 | 8 | ||||
| @@ -40,11 +42,12 @@ func TestVirtualRendererExportsHDRTargets(t *testing.T) { | |||||
| 40 | } | 42 | } | ||
| 41 | } | 43 | } | ||
| 42 | 44 | ||||
| 43 | - | // A failed assembly returns an error and releases what it acquired: without | 45 | + | // A failed assembly returns its error and no session: without a runtime |
| 44 | - | // a runtime directory wayland.New, the last fallible step, fails. | 46 | + | // directory wayland.New, the last fallible step, fails. |
| 45 | func TestAssembleSessionFailure(t *testing.T) { | 47 | func TestAssembleSessionFailure(t *testing.T) { | ||
| 46 | t.Setenv("XDG_RUNTIME_DIR", "") | 48 | t.Setenv("XDG_RUNTIME_DIR", "") | ||
| 47 | - | s, err := assembleSession(t.Context(), Options{Backend: "headless", Config: config.Defaults(), NoXwayland: true}) | 49 | + | allowPath := filepath.Join(t.TempDir(), "capture-allow") |
| 50 | + | s, err := assembleSession(t.Context(), Options{Backend: "headless", Config: config.Defaults(), NoXwayland: true, captureAllowPath: allowPath, captureAllowOwner: uint32(os.Getuid())}) | |||
| 48 | if err == nil || !strings.Contains(err.Error(), "XDG_RUNTIME_DIR") { | 51 | if err == nil || !strings.Contains(err.Error(), "XDG_RUNTIME_DIR") { | ||
| 49 | t.Fatalf("expected runtime directory failure, got %v", err) | 52 | t.Fatalf("expected runtime directory failure, got %v", err) | ||
| 50 | } | 53 | } | ||
M internal/core/action.go +4 -2
| @@ -318,13 +318,15 @@ func (m *Monitor) Apply(a Action) Effect { | |||
| 318 | 318 | switch a { | |
| 319 | 319 | case ActionFocusWindowUp, ActionFocusWindowDown: | |
| 320 | 320 | // Past the column and any on-screen neighbor or demoted float, | |
| 321 | - | // move to the next workspace. | |
| 321 | + | // move to the next workspace. Cascade workspaces sit side by side: | |
| 322 | + | // up/down stop at the first and last band. | |
| 322 | 323 | dir := 1 | |
| 323 | 324 | if a == ActionFocusWindowUp { | |
| 324 | 325 | dir = -1 | |
| 325 | 326 | } | |
| 326 | 327 | // A hidden workspace is outside the vertical list. | |
| 327 | - | if !m.Current().FocusWindow(dir) && m.shown == nil { | |
| 328 | + | w := m.Current() | |
| 329 | + | if !w.FocusWindow(dir) && m.shown == nil && !w.policy().wraps { | |
| 328 | 330 | m.Focus(m.Active + dir) | |
| 329 | 331 | } | |
| 330 | 332 | return Effect{} | |
| @@ -318,13 +318,15 @@ func (m *Monitor) Apply(a Action) Effect { | |||||
| 318 | switch a { | 318 | switch a { | ||
| 319 | case ActionFocusWindowUp, ActionFocusWindowDown: | 319 | case ActionFocusWindowUp, ActionFocusWindowDown: | ||
| 320 | // Past the column and any on-screen neighbor or demoted float, | 320 | // Past the column and any on-screen neighbor or demoted float, | ||
| 321 | - | // move to the next workspace. | 321 | + | // move to the next workspace. Cascade workspaces sit side by side: |
| 322 | + | // up/down stop at the first and last band. | |||
| 322 | dir := 1 | 323 | dir := 1 | ||
| 323 | if a == ActionFocusWindowUp { | 324 | if a == ActionFocusWindowUp { | ||
| 324 | dir = -1 | 325 | dir = -1 | ||
| 325 | } | 326 | } | ||
| 326 | // A hidden workspace is outside the vertical list. | 327 | // A hidden workspace is outside the vertical list. | ||
| 327 | - | if !m.Current().FocusWindow(dir) && m.shown == nil { | 328 | + | w := m.Current() |
| 329 | + | if !w.FocusWindow(dir) && m.shown == nil && !w.policy().wraps { | |||
| 328 | m.Focus(m.Active + dir) | 330 | m.Focus(m.Active + dir) | ||
| 329 | } | 331 | } | ||
| 330 | return Effect{} | 332 | return Effect{} | ||
M internal/core/cascade_test.go +46 -11
| @@ -221,6 +221,47 @@ func TestCascadeReloadAndStashRestoreClearMaximization(t *testing.T) { | |||
| 221 | 221 | } | |
| 222 | 222 | } | |
| 223 | 223 | ||
| 224 | + | // Leaving cascade drops the band offset: scroll realigns on the focus | |
| 225 | + | // instead of keeping it as a horizontal offset past the last column. | |
| 226 | + | func TestReloadCascadeToScrollResetsView(t *testing.T) { | |
| 227 | + | m := NewMonitor() | |
| 228 | + | m.SetOutput(900, 600) | |
| 229 | + | m.SetMaxColumns(3) | |
| 230 | + | m.SetOverflow(OverflowCascade) | |
| 231 | + | for i := WindowID(1); i <= 4; i++ { | |
| 232 | + | m.AddWindow(i) | |
| 233 | + | } | |
| 234 | + | if m.Current().View != 600 { | |
| 235 | + | t.Fatalf("cascade view=%d, want 600", m.Current().View) | |
| 236 | + | } | |
| 237 | + | m.SetOverflow(OverflowScroll) | |
| 238 | + | if got := m.Current().View; got != 300 { | |
| 239 | + | t.Fatalf("scroll view=%d, want 300", got) | |
| 240 | + | } | |
| 241 | + | } | |
| 242 | + | ||
| 243 | + | // Up/down stop at the first and last band: cascade workspaces sit side by | |
| 244 | + | // side, so a vertical key never switches workspace. | |
| 245 | + | func TestCascadeFocusWindowStopsAtEdgeBands(t *testing.T) { | |
| 246 | + | m := NewMonitor() | |
| 247 | + | m.SetOutput(900, 600) | |
| 248 | + | m.SetMaxColumns(3) | |
| 249 | + | m.SetOverflow(OverflowCascade) | |
| 250 | + | for i := WindowID(1); i <= 4; i++ { | |
| 251 | + | m.AddWindow(i) | |
| 252 | + | } | |
| 253 | + | for _, tc := range []struct { | |
| 254 | + | focus WindowID | |
| 255 | + | a Action | |
| 256 | + | }{{4, ActionFocusWindowDown}, {1, ActionFocusWindowUp}} { | |
| 257 | + | m.Current().FocusID(tc.focus) | |
| 258 | + | m.Apply(tc.a) | |
| 259 | + | if id, _ := m.Current().Focused(); m.Active != 0 || id != tc.focus { | |
| 260 | + | t.Fatalf("%s from %d: active=%d focus=%d", tc.a, tc.focus, m.Active, id) | |
| 261 | + | } | |
| 262 | + | } | |
| 263 | + | } | |
| 264 | + | ||
| 224 | 265 | func TestCascadeGestureValidityDoesNotAllocate(t *testing.T) { | |
| 225 | 266 | m := NewMonitor() | |
| 226 | 267 | m.SetOutput(900, 600) | |
| @@ -302,7 +343,7 @@ func TestLaneGeometry(t *testing.T) { | |||
| 302 | 343 | } | |
| 303 | 344 | } | |
| 304 | 345 | ||
| 305 | - | func TestMixedLayoutSwitchKeepsSourceAxis(t *testing.T) { | |
| 346 | + | func TestSlideLayoutAxisOffset(t *testing.T) { | |
| 306 | 347 | m := NewMonitor() | |
| 307 | 348 | m.SetOutput(900, 600) | |
| 308 | 349 | m.Current().Overflow = OverflowCascade | |
| @@ -313,21 +354,15 @@ func TestMixedLayoutSwitchKeepsSourceAxis(t *testing.T) { | |||
| 313 | 354 | m.Focus(0) | |
| 314 | 355 | m.switchAxis = m.Current().policy().workspace | |
| 315 | 356 | m.switchView.off = .5 | |
| 316 | - | ps := m.layoutInto(nil) | |
| 317 | - | for _, p := range ps { | |
| 318 | - | if p.ID == 1 && (p.Rect.X != -450 || p.Rect.Y != 0) { | |
| 319 | - | t.Fatalf("cascade source offset=%v", p.Rect) | |
| 320 | - | } | |
| 357 | + | if p := previewOf(t, m.layoutInto(nil), 1); p.Rect.X != -450 || p.Rect.Y != 0 { | |
| 358 | + | t.Fatalf("cascade source offset=%v", p.Rect) | |
| 321 | 359 | } | |
| 322 | 360 | m.stopSwitch() | |
| 323 | 361 | m.Focus(1) | |
| 324 | 362 | m.switchAxis = m.Current().policy().workspace | |
| 325 | 363 | m.switchView.off = -.5 | |
| 326 | - | ps = m.layoutInto(nil) | |
| 327 | - | for _, p := range ps { | |
| 328 | - | if p.ID == 2 && (p.Rect.X != 0 || p.Rect.Y != 300) { | |
| 329 | - | t.Fatalf("scroll source offset=%v", p.Rect) | |
| 330 | - | } | |
| 364 | + | if p := previewOf(t, m.layoutInto(nil), 2); p.Rect.X != 0 || p.Rect.Y != 300 { | |
| 365 | + | t.Fatalf("scroll source offset=%v", p.Rect) | |
| 331 | 366 | } | |
| 332 | 367 | } | |
| 333 | 368 | ||
| @@ -221,6 +221,47 @@ func TestCascadeReloadAndStashRestoreClearMaximization(t *testing.T) { | |||||
| 221 | } | 221 | } | ||
| 222 | } | 222 | } | ||
| 223 | 223 | ||||
| 224 | + | // Leaving cascade drops the band offset: scroll realigns on the focus | |||
| 225 | + | // instead of keeping it as a horizontal offset past the last column. | |||
| 226 | + | func TestReloadCascadeToScrollResetsView(t *testing.T) { | |||
| 227 | + | m := NewMonitor() | |||
| 228 | + | m.SetOutput(900, 600) | |||
| 229 | + | m.SetMaxColumns(3) | |||
| 230 | + | m.SetOverflow(OverflowCascade) | |||
| 231 | + | for i := WindowID(1); i <= 4; i++ { | |||
| 232 | + | m.AddWindow(i) | |||
| 233 | + | } | |||
| 234 | + | if m.Current().View != 600 { | |||
| 235 | + | t.Fatalf("cascade view=%d, want 600", m.Current().View) | |||
| 236 | + | } | |||
| 237 | + | m.SetOverflow(OverflowScroll) | |||
| 238 | + | if got := m.Current().View; got != 300 { | |||
| 239 | + | t.Fatalf("scroll view=%d, want 300", got) | |||
| 240 | + | } | |||
| 241 | + | } | |||
| 242 | + | ||||
| 243 | + | // Up/down stop at the first and last band: cascade workspaces sit side by | |||
| 244 | + | // side, so a vertical key never switches workspace. | |||
| 245 | + | func TestCascadeFocusWindowStopsAtEdgeBands(t *testing.T) { | |||
| 246 | + | m := NewMonitor() | |||
| 247 | + | m.SetOutput(900, 600) | |||
| 248 | + | m.SetMaxColumns(3) | |||
| 249 | + | m.SetOverflow(OverflowCascade) | |||
| 250 | + | for i := WindowID(1); i <= 4; i++ { | |||
| 251 | + | m.AddWindow(i) | |||
| 252 | + | } | |||
| 253 | + | for _, tc := range []struct { | |||
| 254 | + | focus WindowID | |||
| 255 | + | a Action | |||
| 256 | + | }{{4, ActionFocusWindowDown}, {1, ActionFocusWindowUp}} { | |||
| 257 | + | m.Current().FocusID(tc.focus) | |||
| 258 | + | m.Apply(tc.a) | |||
| 259 | + | if id, _ := m.Current().Focused(); m.Active != 0 || id != tc.focus { | |||
| 260 | + | t.Fatalf("%s from %d: active=%d focus=%d", tc.a, tc.focus, m.Active, id) | |||
| 261 | + | } | |||
| 262 | + | } | |||
| 263 | + | } | |||
| 264 | + | ||||
| 224 | func TestCascadeGestureValidityDoesNotAllocate(t *testing.T) { | 265 | func TestCascadeGestureValidityDoesNotAllocate(t *testing.T) { | ||
| 225 | m := NewMonitor() | 266 | m := NewMonitor() | ||
| 226 | m.SetOutput(900, 600) | 267 | m.SetOutput(900, 600) | ||
| @@ -302,7 +343,7 @@ func TestLaneGeometry(t *testing.T) { | |||||
| 302 | } | 343 | } | ||
| 303 | } | 344 | } | ||
| 304 | 345 | ||||
| 305 | - | func TestMixedLayoutSwitchKeepsSourceAxis(t *testing.T) { | 346 | + | func TestSlideLayoutAxisOffset(t *testing.T) { |
| 306 | m := NewMonitor() | 347 | m := NewMonitor() | ||
| 307 | m.SetOutput(900, 600) | 348 | m.SetOutput(900, 600) | ||
| 308 | m.Current().Overflow = OverflowCascade | 349 | m.Current().Overflow = OverflowCascade | ||
| @@ -313,21 +354,15 @@ func TestMixedLayoutSwitchKeepsSourceAxis(t *testing.T) { | |||||
| 313 | m.Focus(0) | 354 | m.Focus(0) | ||
| 314 | m.switchAxis = m.Current().policy().workspace | 355 | m.switchAxis = m.Current().policy().workspace | ||
| 315 | m.switchView.off = .5 | 356 | m.switchView.off = .5 | ||
| 316 | - | ps := m.layoutInto(nil) | 357 | + | if p := previewOf(t, m.layoutInto(nil), 1); p.Rect.X != -450 || p.Rect.Y != 0 { |
| 317 | - | for _, p := range ps { | 358 | + | t.Fatalf("cascade source offset=%v", p.Rect) |
| 318 | - | if p.ID == 1 && (p.Rect.X != -450 || p.Rect.Y != 0) { | |||
| 319 | - | t.Fatalf("cascade source offset=%v", p.Rect) | |||
| 320 | - | } | |||
| 321 | } | 359 | } | ||
| 322 | m.stopSwitch() | 360 | m.stopSwitch() | ||
| 323 | m.Focus(1) | 361 | m.Focus(1) | ||
| 324 | m.switchAxis = m.Current().policy().workspace | 362 | m.switchAxis = m.Current().policy().workspace | ||
| 325 | m.switchView.off = -.5 | 363 | m.switchView.off = -.5 | ||
| 326 | - | ps = m.layoutInto(nil) | 364 | + | if p := previewOf(t, m.layoutInto(nil), 2); p.Rect.X != 0 || p.Rect.Y != 300 { |
| 327 | - | for _, p := range ps { | 365 | + | t.Fatalf("scroll source offset=%v", p.Rect) |
| 328 | - | if p.ID == 2 && (p.Rect.X != 0 || p.Rect.Y != 300) { | |||
| 329 | - | t.Fatalf("scroll source offset=%v", p.Rect) | |||
| 330 | - | } | |||
| 331 | } | 366 | } | ||
| 332 | } | 367 | } | ||
| 333 | 368 | ||||
M internal/core/drag.go +1 -1
| @@ -319,7 +319,7 @@ func (w *Workspace) dropIn(id WindowID, x, y, t int) dropTarget { | |||
| 319 | 319 | lone := src >= 0 && len(w.Columns[src].Windows) == 1 | |
| 320 | 320 | var cols []visibleColumn | |
| 321 | 321 | for _, p := range layout { | |
| 322 | - | if p.Floating || p.Hidden || !p.Rect.Overlaps(w.Output) { | |
| 322 | + | if p.Floating || p.Hidden || !p.Rect.Overlaps(w.Usable) { | |
| 323 | 323 | continue | |
| 324 | 324 | } | |
| 325 | 325 | ci := w.columnOf(p.ID) | |
| @@ -319,7 +319,7 @@ func (w *Workspace) dropIn(id WindowID, x, y, t int) dropTarget { | |||||
| 319 | lone := src >= 0 && len(w.Columns[src].Windows) == 1 | 319 | lone := src >= 0 && len(w.Columns[src].Windows) == 1 | ||
| 320 | var cols []visibleColumn | 320 | var cols []visibleColumn | ||
| 321 | for _, p := range layout { | 321 | for _, p := range layout { | ||
| 322 | - | if p.Floating || p.Hidden || !p.Rect.Overlaps(w.Output) { | 322 | + | if p.Floating || p.Hidden || !p.Rect.Overlaps(w.Usable) { |
| 323 | continue | 323 | continue | ||
| 324 | } | 324 | } | ||
| 325 | ci := w.columnOf(p.ID) | 325 | ci := w.columnOf(p.ID) | ||
M internal/core/layout.go +5 -4
| @@ -498,7 +498,7 @@ func (w *Workspace) AddWindow(id WindowID) { | |||
| 498 | 498 | // A new window never moves the view off a covering fullscreen | |
| 499 | 499 | // window (ADR 011): it waits, hidden, after the focused column. | |
| 500 | 500 | at := len(w.Columns) | |
| 501 | - | if !w.policy().appendLast { | |
| 501 | + | if !w.policy().equalCells { | |
| 502 | 502 | // A covering float or stashed window may have no column under it. | |
| 503 | 503 | at = min(w.Focus+1, len(w.Columns)) | |
| 504 | 504 | } | |
| @@ -511,7 +511,7 @@ func (w *Workspace) AddWindow(id WindowID) { | |||
| 511 | 511 | // addColumn places a new column like a new window and focuses it. | |
| 512 | 512 | func (w *Workspace) addColumn(col Column) { | |
| 513 | 513 | at := 0 | |
| 514 | - | if w.policy().appendLast { | |
| 514 | + | if w.policy().equalCells { | |
| 515 | 515 | // Ordered overflow depends only on window order: new windows go last. | |
| 516 | 516 | at = len(w.Columns) | |
| 517 | 517 | } else if len(w.Columns) > 0 { | |
| @@ -998,7 +998,8 @@ func (w *Workspace) insertColumn(at int, col Column) { | |||
| 998 | 998 | } | |
| 999 | 999 | ||
| 1000 | 1000 | // CycleWidth steps the focused column through the presets. Fixed overflow | |
| 1001 | - | // ignores presets: it toggles the expanded column instead. | |
| 1001 | + | // ignores presets: it toggles the expanded column instead; cascade does | |
| 1002 | + | // nothing. | |
| 1002 | 1003 | func (w *Workspace) CycleWidth() { | |
| 1003 | 1004 | if w.policy().wraps { | |
| 1004 | 1005 | return | |
| @@ -1643,7 +1644,7 @@ func (w *Workspace) appendLayout(dst []Placement) []Placement { | |||
| 1643 | 1644 | if full { | |
| 1644 | 1645 | // Scroll mode aligns the view on the column; fixed never scrolls. | |
| 1645 | 1646 | r = w.Output | |
| 1646 | - | if w.Overflow == OverflowScroll || w.Overflow == "" { | |
| 1647 | + | if !w.policy().equalCells { | |
| 1647 | 1648 | r.X = col.X | |
| 1648 | 1649 | } | |
| 1649 | 1650 | } | |
| @@ -498,7 +498,7 @@ func (w *Workspace) AddWindow(id WindowID) { | |||||
| 498 | // A new window never moves the view off a covering fullscreen | 498 | // A new window never moves the view off a covering fullscreen | ||
| 499 | // window (ADR 011): it waits, hidden, after the focused column. | 499 | // window (ADR 011): it waits, hidden, after the focused column. | ||
| 500 | at := len(w.Columns) | 500 | at := len(w.Columns) | ||
| 501 | - | if !w.policy().appendLast { | 501 | + | if !w.policy().equalCells { |
| 502 | // A covering float or stashed window may have no column under it. | 502 | // A covering float or stashed window may have no column under it. | ||
| 503 | at = min(w.Focus+1, len(w.Columns)) | 503 | at = min(w.Focus+1, len(w.Columns)) | ||
| 504 | } | 504 | } | ||
| @@ -511,7 +511,7 @@ func (w *Workspace) AddWindow(id WindowID) { | |||||
| 511 | // addColumn places a new column like a new window and focuses it. | 511 | // addColumn places a new column like a new window and focuses it. | ||
| 512 | func (w *Workspace) addColumn(col Column) { | 512 | func (w *Workspace) addColumn(col Column) { | ||
| 513 | at := 0 | 513 | at := 0 | ||
| 514 | - | if w.policy().appendLast { | 514 | + | if w.policy().equalCells { |
| 515 | // Ordered overflow depends only on window order: new windows go last. | 515 | // Ordered overflow depends only on window order: new windows go last. | ||
| 516 | at = len(w.Columns) | 516 | at = len(w.Columns) | ||
| 517 | } else if len(w.Columns) > 0 { | 517 | } else if len(w.Columns) > 0 { | ||
| @@ -998,7 +998,8 @@ func (w *Workspace) insertColumn(at int, col Column) { | |||||
| 998 | } | 998 | } | ||
| 999 | 999 | ||||
| 1000 | // CycleWidth steps the focused column through the presets. Fixed overflow | 1000 | // CycleWidth steps the focused column through the presets. Fixed overflow | ||
| 1001 | - | // ignores presets: it toggles the expanded column instead. | 1001 | + | // ignores presets: it toggles the expanded column instead; cascade does |
| 1002 | + | // nothing. | |||
| 1002 | func (w *Workspace) CycleWidth() { | 1003 | func (w *Workspace) CycleWidth() { | ||
| 1003 | if w.policy().wraps { | 1004 | if w.policy().wraps { | ||
| 1004 | return | 1005 | return | ||
| @@ -1643,7 +1644,7 @@ func (w *Workspace) appendLayout(dst []Placement) []Placement { | |||||
| 1643 | if full { | 1644 | if full { | ||
| 1644 | // Scroll mode aligns the view on the column; fixed never scrolls. | 1645 | // Scroll mode aligns the view on the column; fixed never scrolls. | ||
| 1645 | r = w.Output | 1646 | r = w.Output | ||
| 1646 | - | if w.Overflow == OverflowScroll || w.Overflow == "" { | 1647 | + | if !w.policy().equalCells { |
| 1647 | r.X = col.X | 1648 | r.X = col.X | ||
| 1648 | } | 1649 | } | ||
| 1649 | } | 1650 | } | ||
M internal/core/layout_policy.go +4 -4
| @@ -25,16 +25,16 @@ func (a layoutAxis) offset(r *Rect, pixels int) { | |||
| 25 | 25 | // layoutPolicy keeps geometry capabilities distinct from workspace navigation. | |
| 26 | 26 | // Window state remains owned by Workspace; policies contain no mutable state. | |
| 27 | 27 | type layoutPolicy struct { | |
| 28 | - | content, workspace layoutAxis | |
| 29 | - | equalCells, appendLast, wraps bool | |
| 28 | + | content, workspace layoutAxis | |
| 29 | + | equalCells, wraps bool | |
| 30 | 30 | } | |
| 31 | 31 | ||
| 32 | 32 | func (w *Workspace) policy() layoutPolicy { | |
| 33 | 33 | if w.Overflow == OverflowCascade { | |
| 34 | - | return layoutPolicy{content: verticalAxis, workspace: horizontalAxis, equalCells: true, appendLast: true, wraps: true} | |
| 34 | + | return layoutPolicy{content: verticalAxis, workspace: horizontalAxis, equalCells: true, wraps: true} | |
| 35 | 35 | } | |
| 36 | 36 | if w.Overflow == OverflowFixed { | |
| 37 | - | return layoutPolicy{content: horizontalAxis, workspace: verticalAxis, equalCells: true, appendLast: true} | |
| 37 | + | return layoutPolicy{content: horizontalAxis, workspace: verticalAxis, equalCells: true} | |
| 38 | 38 | } | |
| 39 | 39 | return layoutPolicy{content: horizontalAxis, workspace: verticalAxis} | |
| 40 | 40 | } | |
| @@ -25,16 +25,16 @@ func (a layoutAxis) offset(r *Rect, pixels int) { | |||||
| 25 | // layoutPolicy keeps geometry capabilities distinct from workspace navigation. | 25 | // layoutPolicy keeps geometry capabilities distinct from workspace navigation. | ||
| 26 | // Window state remains owned by Workspace; policies contain no mutable state. | 26 | // Window state remains owned by Workspace; policies contain no mutable state. | ||
| 27 | type layoutPolicy struct { | 27 | type layoutPolicy struct { | ||
| 28 | - | content, workspace layoutAxis | 28 | + | content, workspace layoutAxis |
| 29 | - | equalCells, appendLast, wraps bool | 29 | + | equalCells, wraps bool |
| 30 | } | 30 | } | ||
| 31 | 31 | ||||
| 32 | func (w *Workspace) policy() layoutPolicy { | 32 | func (w *Workspace) policy() layoutPolicy { | ||
| 33 | if w.Overflow == OverflowCascade { | 33 | if w.Overflow == OverflowCascade { | ||
| 34 | - | return layoutPolicy{content: verticalAxis, workspace: horizontalAxis, equalCells: true, appendLast: true, wraps: true} | 34 | + | return layoutPolicy{content: verticalAxis, workspace: horizontalAxis, equalCells: true, wraps: true} |
| 35 | } | 35 | } | ||
| 36 | if w.Overflow == OverflowFixed { | 36 | if w.Overflow == OverflowFixed { | ||
| 37 | - | return layoutPolicy{content: horizontalAxis, workspace: verticalAxis, equalCells: true, appendLast: true} | 37 | + | return layoutPolicy{content: horizontalAxis, workspace: verticalAxis, equalCells: true} |
| 38 | } | 38 | } | ||
| 39 | return layoutPolicy{content: horizontalAxis, workspace: verticalAxis} | 39 | return layoutPolicy{content: horizontalAxis, workspace: verticalAxis} | ||
| 40 | } | 40 | } | ||
M internal/core/monitor.go +7 -1
| @@ -510,7 +510,7 @@ func (m *Monitor) applyNamed() { | |||
| 510 | 510 | } | |
| 511 | 511 | m.each(func(w *Workspace) { | |
| 512 | 512 | s := specs[w.Name] | |
| 513 | - | before := w.Overflow | |
| 513 | + | before, axis := w.Overflow, w.policy().content | |
| 514 | 514 | w.Overflow = m.template.Overflow | |
| 515 | 515 | if s.Overflow != "" { | |
| 516 | 516 | w.Overflow = s.Overflow | |
| @@ -518,6 +518,12 @@ func (m *Monitor) applyNamed() { | |||
| 518 | 518 | if before != w.Overflow && w.policy().equalCells { | |
| 519 | 519 | w.unmaximize() | |
| 520 | 520 | } | |
| 521 | + | // The view offset belongs to the old content axis: SetMaxColumns | |
| 522 | + | // scrolls back to the focus along the new one. | |
| 523 | + | if w.policy().content != axis { | |
| 524 | + | w.view.stop() | |
| 525 | + | w.View = 0 | |
| 526 | + | } | |
| 521 | 527 | w.SetMaxColumns(m.template.MaxColumns) | |
| 522 | 528 | if s.MaxColumns > 0 { | |
| 523 | 529 | w.SetMaxColumns(s.MaxColumns) | |
| @@ -510,7 +510,7 @@ func (m *Monitor) applyNamed() { | |||||
| 510 | } | 510 | } | ||
| 511 | m.each(func(w *Workspace) { | 511 | m.each(func(w *Workspace) { | ||
| 512 | s := specs[w.Name] | 512 | s := specs[w.Name] | ||
| 513 | - | before := w.Overflow | 513 | + | before, axis := w.Overflow, w.policy().content |
| 514 | w.Overflow = m.template.Overflow | 514 | w.Overflow = m.template.Overflow | ||
| 515 | if s.Overflow != "" { | 515 | if s.Overflow != "" { | ||
| 516 | w.Overflow = s.Overflow | 516 | w.Overflow = s.Overflow | ||
| @@ -518,6 +518,12 @@ func (m *Monitor) applyNamed() { | |||||
| 518 | if before != w.Overflow && w.policy().equalCells { | 518 | if before != w.Overflow && w.policy().equalCells { | ||
| 519 | w.unmaximize() | 519 | w.unmaximize() | ||
| 520 | } | 520 | } | ||
| 521 | + | // The view offset belongs to the old content axis: SetMaxColumns | |||
| 522 | + | // scrolls back to the focus along the new one. | |||
| 523 | + | if w.policy().content != axis { | |||
| 524 | + | w.view.stop() | |||
| 525 | + | w.View = 0 | |||
| 526 | + | } | |||
| 521 | w.SetMaxColumns(m.template.MaxColumns) | 527 | w.SetMaxColumns(m.template.MaxColumns) | ||
| 522 | if s.MaxColumns > 0 { | 528 | if s.MaxColumns > 0 { | ||
| 523 | w.SetMaxColumns(s.MaxColumns) | 529 | w.SetMaxColumns(s.MaxColumns) | ||
M internal/core/overview.go +62 -34
| @@ -24,8 +24,9 @@ type overviewState struct { | |||
| 24 | 24 | selected WindowID | |
| 25 | 25 | selectedAt int | |
| 26 | 26 | scrollX, scrollY float64 | |
| 27 | - | // scrolled is set once a three-finger gesture stepped up or down: it | |
| 28 | - | // waits for the fingers to lift before the next step. sideways is set | |
| 27 | + | // scrolled is set once a three-finger gesture took its one step per | |
| 28 | + | // gesture (up or down, a cascade's horizontal step, or a turn of the | |
| 29 | + | // overview): it waits for the fingers to lift. sideways is set | |
| 29 | 30 | // once it stepped to a column: it stays on columns and steps once per | |
| 30 | 31 | // overviewScrollRepeat. | |
| 31 | 32 | scrolled, sideways bool | |
| @@ -48,9 +49,9 @@ type rowSnapshot struct { | |||
| 48 | 49 | ||
| 49 | 50 | // The overview scales workspace rows without resizing clients. Covering | |
| 50 | 51 | // floats and the columns form a linear stack of cards; small floats stay | |
| 51 | - | // hidden. Up/down traverse the stack before changing workspace. A pinned | |
| 52 | - | // fullscreen window is the front card of its row, the windows it hides | |
| 53 | - | // behind it; choosing one of them leaves fullscreen. | |
| 52 | + | // hidden. Moves along the workspace axis traverse the stack before changing | |
| 53 | + | // workspace. A pinned fullscreen window is the front card of its row, the | |
| 54 | + | // windows it hides behind it; choosing one of them leaves fullscreen. | |
| 54 | 55 | ||
| 55 | 56 | // A workspace's stash shows as a pile of cards pinned to the left edge of | |
| 56 | 57 | // its row: the selected stashed window in front at overviewCardZoom, up to | |
| @@ -63,9 +64,9 @@ const ( | |||
| 63 | 64 | overviewPeekStep = 0.07 | |
| 64 | 65 | ) | |
| 65 | 66 | ||
| 66 | - | // overviewMaxZoom caps the current row so its neighbors show above and | |
| 67 | - | // below; overviewMinZoom floors the shrink of wide workspaces, which then | |
| 68 | - | // scroll to keep the selected column in view. | |
| 67 | + | // overviewMaxZoom caps the current row so its neighbors show beside it | |
| 68 | + | // along the workspace axis; overviewMinZoom floors the shrink of wide | |
| 69 | + | // workspaces, which then scroll to keep the selected column in view. | |
| 69 | 70 | const overviewMaxZoom, overviewMinZoom = 0.6, 0.25 | |
| 70 | 71 | ||
| 71 | 72 | // ToggleOverview opens the overview, or closes it on the selection. Over | |
| @@ -110,7 +111,7 @@ func (m *Monitor) remember(w *Workspace) { | |||
| 110 | 111 | // selectRow resets provisional stack and stash selection for a new row. | |
| 111 | 112 | func (m *Monitor) selectRow() { | |
| 112 | 113 | m.remember(m.Current()) | |
| 113 | - | m.ov.card, m.ov.cardOf = 0, nil | |
| 114 | + | m.clearCard() | |
| 114 | 115 | m.ov.front, m.ov.row, m.ov.frontAt = stackItem{}, nil, 0 | |
| 115 | 116 | m.ov.selected, m.ov.selectedAt = 0, -1 | |
| 116 | 117 | w := m.Current() | |
| @@ -140,6 +141,9 @@ func (m *Monitor) card() WindowID { | |||
| 140 | 141 | return m.ov.card | |
| 141 | 142 | } | |
| 142 | 143 | ||
| 144 | + | // clearCard deselects the stash card. | |
| 145 | + | func (m *Monitor) clearCard() { m.ov.card, m.ov.cardOf = 0, nil } | |
| 146 | + | ||
| 143 | 147 | // cardAt is the pile entry of w that is selected, -1 when none. | |
| 144 | 148 | func (m *Monitor) cardAt(w *Workspace) int { | |
| 145 | 149 | if id := m.card(); id != 0 && w == m.ov.cardOf { | |
| @@ -382,7 +386,13 @@ func (m *Monitor) OverviewMove(dx, dy int) { | |||
| 382 | 386 | } | |
| 383 | 387 | return | |
| 384 | 388 | } | |
| 385 | - | m.overviewMoveVertical(dx, dy) | |
| 389 | + | // Workspaces stack vertically: vertical moves stay in a selected stash | |
| 390 | + | // pile; left stays, and right deselects it. | |
| 391 | + | if dy != 0 { | |
| 392 | + | m.overviewVertical(dy) | |
| 393 | + | } else if dx != 0 { | |
| 394 | + | m.overviewHorizontal(dx) | |
| 395 | + | } | |
| 386 | 396 | } | |
| 387 | 397 | ||
| 388 | 398 | // overviewWorkspaceStep is a focus-workspace bind. A vertical overview | |
| @@ -447,7 +457,7 @@ func (m *Monitor) overviewAcross(dx int) { | |||
| 447 | 457 | m.overviewStep(dx) | |
| 448 | 458 | return | |
| 449 | 459 | } | |
| 450 | - | m.ov.card, m.ov.cardOf = 0, nil | |
| 460 | + | m.clearCard() | |
| 451 | 461 | m.ov.selected = 0 | |
| 452 | 462 | if m.stackFront(w).kind == stackColumns && len(w.Columns) > 0 { | |
| 453 | 463 | // The front column: a maximized one keeps its cards behind. | |
| @@ -485,17 +495,9 @@ func (m *Monitor) laneEdge(w *Workspace, dx int) { | |||
| 485 | 495 | m.overviewStep(dx) | |
| 486 | 496 | } | |
| 487 | 497 | ||
| 488 | - | // overviewMoveVertical is the move of an overview whose workspaces stack | |
| 489 | - | // vertically. Vertical moves stay in a selected stash pile; left stays, and | |
| 490 | - | // right deselects it, even when the row has no main card. | |
| 491 | - | func (m *Monitor) overviewMoveVertical(dx, dy int) { | |
| 492 | - | if dy != 0 { | |
| 493 | - | m.overviewVertical(dy) | |
| 494 | - | } else if dx != 0 { | |
| 495 | - | m.overviewHorizontal(dx) | |
| 496 | - | } | |
| 497 | - | } | |
| 498 | - | ||
| 498 | + | // overviewVertical is the vertical move of an overview whose workspaces | |
| 499 | + | // stack vertically: through a selected stash pile, then the stack, then | |
| 500 | + | // to the next workspace. | |
| 499 | 501 | func (m *Monitor) overviewVertical(dy int) { | |
| 500 | 502 | if m.cycleCard(m.Current(), dy) { | |
| 501 | 503 | return | |
| @@ -522,7 +524,7 @@ func (m *Monitor) overviewHorizontal(dx int) { | |||
| 522 | 524 | w := m.Current() | |
| 523 | 525 | if m.cardAt(w) >= 0 { | |
| 524 | 526 | if dx > 0 { | |
| 525 | - | m.ov.card, m.ov.cardOf = 0, nil | |
| 527 | + | m.clearCard() | |
| 526 | 528 | m.ov.selected = 0 | |
| 527 | 529 | item := m.stackFront(w) | |
| 528 | 530 | if item.kind == stackColumns { | |
| @@ -594,7 +596,7 @@ func (m *Monitor) OverviewPick(id WindowID) { | |||
| 594 | 596 | } | |
| 595 | 597 | } | |
| 596 | 598 | m.showOverview(w) | |
| 597 | - | m.ov.card, m.ov.cardOf = 0, nil | |
| 599 | + | m.clearCard() | |
| 598 | 600 | if i := w.stashIndex(id); i >= 0 { | |
| 599 | 601 | m.selectCard(w, i) | |
| 600 | 602 | } else if item, ok := w.itemOf(id); ok { | |
| @@ -990,7 +992,7 @@ func (w *Workspace) previewTilesInto(tiles []Placement) ([]Placement, int, Rect) | |||
| 990 | 992 | } | |
| 991 | 993 | } | |
| 992 | 994 | for i, c := range w.Columns { | |
| 993 | - | r := Rect{X: w.columnX(i) - w.Usable.X, Y: g, W: w.columnWidth(i), H: h} | |
| 995 | + | r := Rect{Y: g, H: h} | |
| 994 | 996 | if w.policy().wraps { | |
| 995 | 997 | if w.hiddenByMaximized(i) { | |
| 996 | 998 | continue // A card, above. | |
| @@ -1012,9 +1014,12 @@ func (w *Workspace) previewTilesInto(tiles []Placement) ([]Placement, int, Rect) | |||
| 1012 | 1014 | } else if w.Overflow == OverflowFixed { | |
| 1013 | 1015 | r = rects[i] | |
| 1014 | 1016 | r.X, r.Y = r.X-w.Usable.X, r.Y-w.Usable.Y | |
| 1015 | - | } else if w.fullscreenColumn(i) { | |
| 1016 | - | // A fullscreen column shows at the width it had in the row. | |
| 1017 | - | r.W = max(w.Usable.W-2*g, 0) | |
| 1017 | + | } else { | |
| 1018 | + | r.X, r.W = w.columnX(i)-w.Usable.X, w.columnWidth(i) | |
| 1019 | + | if w.fullscreenColumn(i) { | |
| 1020 | + | // A fullscreen column shows at the width it had in the row. | |
| 1021 | + | r.W = max(w.Usable.W-2*g, 0) | |
| 1022 | + | } | |
| 1018 | 1023 | } | |
| 1019 | 1024 | if w.hiddenByMaximized(i) { | |
| 1020 | 1025 | // Fixed overflow shows the hidden columns on their own card | |
| @@ -1285,10 +1290,11 @@ const ( | |||
| 1285 | 1290 | // overviewScroll moves the overview selection with a scroll frame: a row | |
| 1286 | 1291 | // step when the vertical scroll reaches overviewScrollStep and leads, else | |
| 1287 | 1292 | // a column step. Three-finger updates (encoded here as AxisFinger) step | |
| 1288 | - | // to the next column after | |
| 1289 | - | // overviewScrollStep, then once per overviewScrollRepeat, and stay on | |
| 1290 | - | // columns until they lift; up or down they step once per scroll, so a card | |
| 1291 | - | // in a stack is easy to pick. | |
| 1293 | + | // to the next column after overviewScrollStep, then once per | |
| 1294 | + | // overviewScrollRepeat, and stay on columns until they lift; up or down they | |
| 1295 | + | // step once per scroll, so a card in a stack is easy to pick. In a cascade | |
| 1296 | + | // overview they take one step per gesture on both axes: a band vertically, | |
| 1297 | + | // a card or workspace horizontally. | |
| 1292 | 1298 | // Continuous scrolling steps once per overviewScrollStep and a wheel once | |
| 1293 | 1299 | // per notch (high-resolution wheels add up their fractions of a notch). | |
| 1294 | 1300 | // Vertical steps select cards first, then rows; natural scroll | |
| @@ -1328,6 +1334,10 @@ func (m *Monitor) overviewScroll(a ports.PointerAxis) (changed bool) { | |||
| 1328 | 1334 | // A step that turns the overview returns, so the axis holds for the loop. | |
| 1329 | 1335 | axis := m.overviewAxis() | |
| 1330 | 1336 | cascade := axis == horizontalAxis | |
| 1337 | + | // A finger step reports a change only when the selection moved: at an | |
| 1338 | + | // edge or over a float it does nothing, and the scene stays. The | |
| 1339 | + | // snapshot is taken only before a step: stackFront allocates. | |
| 1340 | + | var before overviewSelection | |
| 1331 | 1341 | for { | |
| 1332 | 1342 | switch { | |
| 1333 | 1343 | case math.Abs(m.ov.scrollY) >= overviewScrollStep && math.Abs(m.ov.scrollY) >= math.Abs(m.ov.scrollX): | |
| @@ -1336,6 +1346,9 @@ func (m *Monitor) overviewScroll(a ports.PointerAxis) (changed bool) { | |||
| 1336 | 1346 | // overview drops the scroll left (showOverview). | |
| 1337 | 1347 | m.ov.scrollY -= float64(d) * overviewScrollStep | |
| 1338 | 1348 | m.ov.scrollX = 0 | |
| 1349 | + | if finger { | |
| 1350 | + | before = m.overviewSelection() | |
| 1351 | + | } | |
| 1339 | 1352 | if cascade && !finger && m.cardAt(m.Current()) < 0 { | |
| 1340 | 1353 | m.overviewStep(d) | |
| 1341 | 1354 | } else { | |
| @@ -1346,11 +1359,12 @@ func (m *Monitor) overviewScroll(a ports.PointerAxis) (changed bool) { | |||
| 1346 | 1359 | } | |
| 1347 | 1360 | if finger { | |
| 1348 | 1361 | m.ov.scrollX, m.ov.scrollY, m.ov.scrolled = 0, 0, true | |
| 1349 | - | return true | |
| 1362 | + | return m.overviewSelection() != before | |
| 1350 | 1363 | } | |
| 1351 | 1364 | case math.Abs(m.ov.scrollX) >= stepX: | |
| 1352 | 1365 | d := sign(m.ov.scrollX) | |
| 1353 | 1366 | if cascade && finger { | |
| 1367 | + | before = m.overviewSelection() | |
| 1354 | 1368 | // One step per gesture: a card behind a maximized | |
| 1355 | 1369 | // column first, like the arrows, else one workspace, | |
| 1356 | 1370 | // even over a stash card. | |
| @@ -1360,7 +1374,7 @@ func (m *Monitor) overviewScroll(a ports.PointerAxis) (changed bool) { | |||
| 1360 | 1374 | m.overviewStep(d) | |
| 1361 | 1375 | } | |
| 1362 | 1376 | m.ov.scrollX, m.ov.scrollY, m.ov.scrolled = 0, 0, true | |
| 1363 | - | return true | |
| 1377 | + | return m.overviewSelection() != before | |
| 1364 | 1378 | } | |
| 1365 | 1379 | m.ov.scrollX -= float64(d) * stepX | |
| 1366 | 1380 | m.ov.scrollY = 0 | |
| @@ -1384,6 +1398,20 @@ func (m *Monitor) bandCardsAt(w *Workspace) bool { | |||
| 1384 | 1398 | return m.cardAt(w) < 0 && w.hasBandCards() && m.stackFront(w).kind == stackColumns | |
| 1385 | 1399 | } | |
| 1386 | 1400 | ||
| 1401 | + | // overviewSelection is what an overview step can move: the workspace, its | |
| 1402 | + | // stack front, stash card, focused column and selected window. | |
| 1403 | + | type overviewSelection struct { | |
| 1404 | + | w *Workspace | |
| 1405 | + | front stackItem | |
| 1406 | + | card, selected WindowID | |
| 1407 | + | focus int | |
| 1408 | + | } | |
| 1409 | + | ||
| 1410 | + | func (m *Monitor) overviewSelection() overviewSelection { | |
| 1411 | + | w := m.Current() | |
| 1412 | + | return overviewSelection{w: w, front: m.stackFront(w), card: m.card(), selected: m.ov.selected, focus: w.Focus} | |
| 1413 | + | } | |
| 1414 | + | ||
| 1387 | 1415 | // overviewTurned reports whether a scroll step turned the overview from the | |
| 1388 | 1416 | // axis before. A finger scroll then waits for the fingers to lift, so it | |
| 1389 | 1417 | // cannot bounce back. | |
| @@ -24,8 +24,9 @@ type overviewState struct { | |||||
| 24 | selected WindowID | 24 | selected WindowID | ||
| 25 | selectedAt int | 25 | selectedAt int | ||
| 26 | scrollX, scrollY float64 | 26 | scrollX, scrollY float64 | ||
| 27 | - | // scrolled is set once a three-finger gesture stepped up or down: it | 27 | + | // scrolled is set once a three-finger gesture took its one step per |
| 28 | - | // waits for the fingers to lift before the next step. sideways is set | 28 | + | // gesture (up or down, a cascade's horizontal step, or a turn of the |
| 29 | + | // overview): it waits for the fingers to lift. sideways is set | |||
| 29 | // once it stepped to a column: it stays on columns and steps once per | 30 | // once it stepped to a column: it stays on columns and steps once per | ||
| 30 | // overviewScrollRepeat. | 31 | // overviewScrollRepeat. | ||
| 31 | scrolled, sideways bool | 32 | scrolled, sideways bool | ||
| @@ -48,9 +49,9 @@ type rowSnapshot struct { | |||||
| 48 | 49 | ||||
| 49 | // The overview scales workspace rows without resizing clients. Covering | 50 | // The overview scales workspace rows without resizing clients. Covering | ||
| 50 | // floats and the columns form a linear stack of cards; small floats stay | 51 | // floats and the columns form a linear stack of cards; small floats stay | ||
| 51 | - | // hidden. Up/down traverse the stack before changing workspace. A pinned | 52 | + | // hidden. Moves along the workspace axis traverse the stack before changing |
| 52 | - | // fullscreen window is the front card of its row, the windows it hides | 53 | + | // workspace. A pinned fullscreen window is the front card of its row, the |
| 53 | - | // behind it; choosing one of them leaves fullscreen. | 54 | + | // windows it hides behind it; choosing one of them leaves fullscreen. |
| 54 | 55 | ||||
| 55 | // A workspace's stash shows as a pile of cards pinned to the left edge of | 56 | // A workspace's stash shows as a pile of cards pinned to the left edge of | ||
| 56 | // its row: the selected stashed window in front at overviewCardZoom, up to | 57 | // its row: the selected stashed window in front at overviewCardZoom, up to | ||
| @@ -63,9 +64,9 @@ const ( | |||||
| 63 | overviewPeekStep = 0.07 | 64 | overviewPeekStep = 0.07 | ||
| 64 | ) | 65 | ) | ||
| 65 | 66 | ||||
| 66 | - | // overviewMaxZoom caps the current row so its neighbors show above and | 67 | + | // overviewMaxZoom caps the current row so its neighbors show beside it |
| 67 | - | // below; overviewMinZoom floors the shrink of wide workspaces, which then | 68 | + | // along the workspace axis; overviewMinZoom floors the shrink of wide |
| 68 | - | // scroll to keep the selected column in view. | 69 | + | // workspaces, which then scroll to keep the selected column in view. |
| 69 | const overviewMaxZoom, overviewMinZoom = 0.6, 0.25 | 70 | const overviewMaxZoom, overviewMinZoom = 0.6, 0.25 | ||
| 70 | 71 | ||||
| 71 | // ToggleOverview opens the overview, or closes it on the selection. Over | 72 | // ToggleOverview opens the overview, or closes it on the selection. Over | ||
| @@ -110,7 +111,7 @@ func (m *Monitor) remember(w *Workspace) { | |||||
| 110 | // selectRow resets provisional stack and stash selection for a new row. | 111 | // selectRow resets provisional stack and stash selection for a new row. | ||
| 111 | func (m *Monitor) selectRow() { | 112 | func (m *Monitor) selectRow() { | ||
| 112 | m.remember(m.Current()) | 113 | m.remember(m.Current()) | ||
| 113 | - | m.ov.card, m.ov.cardOf = 0, nil | 114 | + | m.clearCard() |
| 114 | m.ov.front, m.ov.row, m.ov.frontAt = stackItem{}, nil, 0 | 115 | m.ov.front, m.ov.row, m.ov.frontAt = stackItem{}, nil, 0 | ||
| 115 | m.ov.selected, m.ov.selectedAt = 0, -1 | 116 | m.ov.selected, m.ov.selectedAt = 0, -1 | ||
| 116 | w := m.Current() | 117 | w := m.Current() | ||
| @@ -140,6 +141,9 @@ func (m *Monitor) card() WindowID { | |||||
| 140 | return m.ov.card | 141 | return m.ov.card | ||
| 141 | } | 142 | } | ||
| 142 | 143 | ||||
| 144 | + | // clearCard deselects the stash card. | |||
| 145 | + | func (m *Monitor) clearCard() { m.ov.card, m.ov.cardOf = 0, nil } | |||
| 146 | + | ||||
| 143 | // cardAt is the pile entry of w that is selected, -1 when none. | 147 | // cardAt is the pile entry of w that is selected, -1 when none. | ||
| 144 | func (m *Monitor) cardAt(w *Workspace) int { | 148 | func (m *Monitor) cardAt(w *Workspace) int { | ||
| 145 | if id := m.card(); id != 0 && w == m.ov.cardOf { | 149 | if id := m.card(); id != 0 && w == m.ov.cardOf { | ||
| @@ -382,7 +386,13 @@ func (m *Monitor) OverviewMove(dx, dy int) { | |||||
| 382 | } | 386 | } | ||
| 383 | return | 387 | return | ||
| 384 | } | 388 | } | ||
| 385 | - | m.overviewMoveVertical(dx, dy) | 389 | + | // Workspaces stack vertically: vertical moves stay in a selected stash |
| 390 | + | // pile; left stays, and right deselects it. | |||
| 391 | + | if dy != 0 { | |||
| 392 | + | m.overviewVertical(dy) | |||
| 393 | + | } else if dx != 0 { | |||
| 394 | + | m.overviewHorizontal(dx) | |||
| 395 | + | } | |||
| 386 | } | 396 | } | ||
| 387 | 397 | ||||
| 388 | // overviewWorkspaceStep is a focus-workspace bind. A vertical overview | 398 | // overviewWorkspaceStep is a focus-workspace bind. A vertical overview | ||
| @@ -447,7 +457,7 @@ func (m *Monitor) overviewAcross(dx int) { | |||||
| 447 | m.overviewStep(dx) | 457 | m.overviewStep(dx) | ||
| 448 | return | 458 | return | ||
| 449 | } | 459 | } | ||
| 450 | - | m.ov.card, m.ov.cardOf = 0, nil | 460 | + | m.clearCard() |
| 451 | m.ov.selected = 0 | 461 | m.ov.selected = 0 | ||
| 452 | if m.stackFront(w).kind == stackColumns && len(w.Columns) > 0 { | 462 | if m.stackFront(w).kind == stackColumns && len(w.Columns) > 0 { | ||
| 453 | // The front column: a maximized one keeps its cards behind. | 463 | // The front column: a maximized one keeps its cards behind. | ||
| @@ -485,17 +495,9 @@ func (m *Monitor) laneEdge(w *Workspace, dx int) { | |||||
| 485 | m.overviewStep(dx) | 495 | m.overviewStep(dx) | ||
| 486 | } | 496 | } | ||
| 487 | 497 | ||||
| 488 | - | // overviewMoveVertical is the move of an overview whose workspaces stack | 498 | + | // overviewVertical is the vertical move of an overview whose workspaces |
| 489 | - | // vertically. Vertical moves stay in a selected stash pile; left stays, and | 499 | + | // stack vertically: through a selected stash pile, then the stack, then |
| 490 | - | // right deselects it, even when the row has no main card. | 500 | + | // to the next workspace. |
| 491 | - | func (m *Monitor) overviewMoveVertical(dx, dy int) { | |||
| 492 | - | if dy != 0 { | |||
| 493 | - | m.overviewVertical(dy) | |||
| 494 | - | } else if dx != 0 { | |||
| 495 | - | m.overviewHorizontal(dx) | |||
| 496 | - | } | |||
| 497 | - | } | |||
| 498 | - | ||||
| 499 | func (m *Monitor) overviewVertical(dy int) { | 501 | func (m *Monitor) overviewVertical(dy int) { | ||
| 500 | if m.cycleCard(m.Current(), dy) { | 502 | if m.cycleCard(m.Current(), dy) { | ||
| 501 | return | 503 | return | ||
| @@ -522,7 +524,7 @@ func (m *Monitor) overviewHorizontal(dx int) { | |||||
| 522 | w := m.Current() | 524 | w := m.Current() | ||
| 523 | if m.cardAt(w) >= 0 { | 525 | if m.cardAt(w) >= 0 { | ||
| 524 | if dx > 0 { | 526 | if dx > 0 { | ||
| 525 | - | m.ov.card, m.ov.cardOf = 0, nil | 527 | + | m.clearCard() |
| 526 | m.ov.selected = 0 | 528 | m.ov.selected = 0 | ||
| 527 | item := m.stackFront(w) | 529 | item := m.stackFront(w) | ||
| 528 | if item.kind == stackColumns { | 530 | if item.kind == stackColumns { | ||
| @@ -594,7 +596,7 @@ func (m *Monitor) OverviewPick(id WindowID) { | |||||
| 594 | } | 596 | } | ||
| 595 | } | 597 | } | ||
| 596 | m.showOverview(w) | 598 | m.showOverview(w) | ||
| 597 | - | m.ov.card, m.ov.cardOf = 0, nil | 599 | + | m.clearCard() |
| 598 | if i := w.stashIndex(id); i >= 0 { | 600 | if i := w.stashIndex(id); i >= 0 { | ||
| 599 | m.selectCard(w, i) | 601 | m.selectCard(w, i) | ||
| 600 | } else if item, ok := w.itemOf(id); ok { | 602 | } else if item, ok := w.itemOf(id); ok { | ||
| @@ -990,7 +992,7 @@ func (w *Workspace) previewTilesInto(tiles []Placement) ([]Placement, int, Rect) | |||||
| 990 | } | 992 | } | ||
| 991 | } | 993 | } | ||
| 992 | for i, c := range w.Columns { | 994 | for i, c := range w.Columns { | ||
| 993 | - | r := Rect{X: w.columnX(i) - w.Usable.X, Y: g, W: w.columnWidth(i), H: h} | 995 | + | r := Rect{Y: g, H: h} |
| 994 | if w.policy().wraps { | 996 | if w.policy().wraps { | ||
| 995 | if w.hiddenByMaximized(i) { | 997 | if w.hiddenByMaximized(i) { | ||
| 996 | continue // A card, above. | 998 | continue // A card, above. | ||
| @@ -1012,9 +1014,12 @@ func (w *Workspace) previewTilesInto(tiles []Placement) ([]Placement, int, Rect) | |||||
| 1012 | } else if w.Overflow == OverflowFixed { | 1014 | } else if w.Overflow == OverflowFixed { | ||
| 1013 | r = rects[i] | 1015 | r = rects[i] | ||
| 1014 | r.X, r.Y = r.X-w.Usable.X, r.Y-w.Usable.Y | 1016 | r.X, r.Y = r.X-w.Usable.X, r.Y-w.Usable.Y | ||
| 1015 | - | } else if w.fullscreenColumn(i) { | 1017 | + | } else { |
| 1016 | - | // A fullscreen column shows at the width it had in the row. | 1018 | + | r.X, r.W = w.columnX(i)-w.Usable.X, w.columnWidth(i) |
| 1017 | - | r.W = max(w.Usable.W-2*g, 0) | 1019 | + | if w.fullscreenColumn(i) { |
| 1020 | + | // A fullscreen column shows at the width it had in the row. | |||
| 1021 | + | r.W = max(w.Usable.W-2*g, 0) | |||
| 1022 | + | } | |||
| 1018 | } | 1023 | } | ||
| 1019 | if w.hiddenByMaximized(i) { | 1024 | if w.hiddenByMaximized(i) { | ||
| 1020 | // Fixed overflow shows the hidden columns on their own card | 1025 | // Fixed overflow shows the hidden columns on their own card | ||
| @@ -1285,10 +1290,11 @@ const ( | |||||
| 1285 | // overviewScroll moves the overview selection with a scroll frame: a row | 1290 | // overviewScroll moves the overview selection with a scroll frame: a row | ||
| 1286 | // step when the vertical scroll reaches overviewScrollStep and leads, else | 1291 | // step when the vertical scroll reaches overviewScrollStep and leads, else | ||
| 1287 | // a column step. Three-finger updates (encoded here as AxisFinger) step | 1292 | // a column step. Three-finger updates (encoded here as AxisFinger) step | ||
| 1288 | - | // to the next column after | 1293 | + | // to the next column after overviewScrollStep, then once per |
| 1289 | - | // overviewScrollStep, then once per overviewScrollRepeat, and stay on | 1294 | + | // overviewScrollRepeat, and stay on columns until they lift; up or down they |
| 1290 | - | // columns until they lift; up or down they step once per scroll, so a card | 1295 | + | // step once per scroll, so a card in a stack is easy to pick. In a cascade |
| 1291 | - | // in a stack is easy to pick. | 1296 | + | // overview they take one step per gesture on both axes: a band vertically, |
| 1297 | + | // a card or workspace horizontally. | |||
| 1292 | // Continuous scrolling steps once per overviewScrollStep and a wheel once | 1298 | // Continuous scrolling steps once per overviewScrollStep and a wheel once | ||
| 1293 | // per notch (high-resolution wheels add up their fractions of a notch). | 1299 | // per notch (high-resolution wheels add up their fractions of a notch). | ||
| 1294 | // Vertical steps select cards first, then rows; natural scroll | 1300 | // Vertical steps select cards first, then rows; natural scroll | ||
| @@ -1328,6 +1334,10 @@ func (m *Monitor) overviewScroll(a ports.PointerAxis) (changed bool) { | |||||
| 1328 | // A step that turns the overview returns, so the axis holds for the loop. | 1334 | // A step that turns the overview returns, so the axis holds for the loop. | ||
| 1329 | axis := m.overviewAxis() | 1335 | axis := m.overviewAxis() | ||
| 1330 | cascade := axis == horizontalAxis | 1336 | cascade := axis == horizontalAxis | ||
| 1337 | + | // A finger step reports a change only when the selection moved: at an | |||
| 1338 | + | // edge or over a float it does nothing, and the scene stays. The | |||
| 1339 | + | // snapshot is taken only before a step: stackFront allocates. | |||
| 1340 | + | var before overviewSelection | |||
| 1331 | for { | 1341 | for { | ||
| 1332 | switch { | 1342 | switch { | ||
| 1333 | case math.Abs(m.ov.scrollY) >= overviewScrollStep && math.Abs(m.ov.scrollY) >= math.Abs(m.ov.scrollX): | 1343 | case math.Abs(m.ov.scrollY) >= overviewScrollStep && math.Abs(m.ov.scrollY) >= math.Abs(m.ov.scrollX): | ||
| @@ -1336,6 +1346,9 @@ func (m *Monitor) overviewScroll(a ports.PointerAxis) (changed bool) { | |||||
| 1336 | // overview drops the scroll left (showOverview). | 1346 | // overview drops the scroll left (showOverview). | ||
| 1337 | m.ov.scrollY -= float64(d) * overviewScrollStep | 1347 | m.ov.scrollY -= float64(d) * overviewScrollStep | ||
| 1338 | m.ov.scrollX = 0 | 1348 | m.ov.scrollX = 0 | ||
| 1349 | + | if finger { | |||
| 1350 | + | before = m.overviewSelection() | |||
| 1351 | + | } | |||
| 1339 | if cascade && !finger && m.cardAt(m.Current()) < 0 { | 1352 | if cascade && !finger && m.cardAt(m.Current()) < 0 { | ||
| 1340 | m.overviewStep(d) | 1353 | m.overviewStep(d) | ||
| 1341 | } else { | 1354 | } else { | ||
| @@ -1346,11 +1359,12 @@ func (m *Monitor) overviewScroll(a ports.PointerAxis) (changed bool) { | |||||
| 1346 | } | 1359 | } | ||
| 1347 | if finger { | 1360 | if finger { | ||
| 1348 | m.ov.scrollX, m.ov.scrollY, m.ov.scrolled = 0, 0, true | 1361 | m.ov.scrollX, m.ov.scrollY, m.ov.scrolled = 0, 0, true | ||
| 1349 | - | return true | 1362 | + | return m.overviewSelection() != before |
| 1350 | } | 1363 | } | ||
| 1351 | case math.Abs(m.ov.scrollX) >= stepX: | 1364 | case math.Abs(m.ov.scrollX) >= stepX: | ||
| 1352 | d := sign(m.ov.scrollX) | 1365 | d := sign(m.ov.scrollX) | ||
| 1353 | if cascade && finger { | 1366 | if cascade && finger { | ||
| 1367 | + | before = m.overviewSelection() | |||
| 1354 | // One step per gesture: a card behind a maximized | 1368 | // One step per gesture: a card behind a maximized | ||
| 1355 | // column first, like the arrows, else one workspace, | 1369 | // column first, like the arrows, else one workspace, | ||
| 1356 | // even over a stash card. | 1370 | // even over a stash card. | ||
| @@ -1360,7 +1374,7 @@ func (m *Monitor) overviewScroll(a ports.PointerAxis) (changed bool) { | |||||
| 1360 | m.overviewStep(d) | 1374 | m.overviewStep(d) | ||
| 1361 | } | 1375 | } | ||
| 1362 | m.ov.scrollX, m.ov.scrollY, m.ov.scrolled = 0, 0, true | 1376 | m.ov.scrollX, m.ov.scrollY, m.ov.scrolled = 0, 0, true | ||
| 1363 | - | return true | 1377 | + | return m.overviewSelection() != before |
| 1364 | } | 1378 | } | ||
| 1365 | m.ov.scrollX -= float64(d) * stepX | 1379 | m.ov.scrollX -= float64(d) * stepX | ||
| 1366 | m.ov.scrollY = 0 | 1380 | m.ov.scrollY = 0 | ||
| @@ -1384,6 +1398,20 @@ func (m *Monitor) bandCardsAt(w *Workspace) bool { | |||||
| 1384 | return m.cardAt(w) < 0 && w.hasBandCards() && m.stackFront(w).kind == stackColumns | 1398 | return m.cardAt(w) < 0 && w.hasBandCards() && m.stackFront(w).kind == stackColumns | ||
| 1385 | } | 1399 | } | ||
| 1386 | 1400 | ||||
| 1401 | + | // overviewSelection is what an overview step can move: the workspace, its | |||
| 1402 | + | // stack front, stash card, focused column and selected window. | |||
| 1403 | + | type overviewSelection struct { | |||
| 1404 | + | w *Workspace | |||
| 1405 | + | front stackItem | |||
| 1406 | + | card, selected WindowID | |||
| 1407 | + | focus int | |||
| 1408 | + | } | |||
| 1409 | + | ||||
| 1410 | + | func (m *Monitor) overviewSelection() overviewSelection { | |||
| 1411 | + | w := m.Current() | |||
| 1412 | + | return overviewSelection{w: w, front: m.stackFront(w), card: m.card(), selected: m.ov.selected, focus: w.Focus} | |||
| 1413 | + | } | |||
| 1414 | + | ||||
| 1387 | // overviewTurned reports whether a scroll step turned the overview from the | 1415 | // overviewTurned reports whether a scroll step turned the overview from the | ||
| 1388 | // axis before. A finger scroll then waits for the fingers to lift, so it | 1416 | // axis before. A finger scroll then waits for the fingers to lift, so it | ||
| 1389 | // cannot bounce back. | 1417 | // cannot bounce back. | ||
M internal/core/overview_cascade_test.go +24 -11
| @@ -68,7 +68,7 @@ func within1(a, b int) bool { return a-b <= 1 && b-a <= 1 } | |||
| 68 | 68 | ||
| 69 | 69 | func hiddenOff(p Placement) bool { return p.Hidden && p.Preview == 0 && !p.Focused } | |
| 70 | 70 | ||
| 71 | - | // T1: the zoom does not depend on the number of bands. | |
| 71 | + | // The zoom does not depend on the number of bands. | |
| 72 | 72 | func TestOverviewCascadeZoomIgnoresBandCount(t *testing.T) { | |
| 73 | 73 | if one, three := cascadeWorkspace(3).overviewZoom(), cascadeWorkspace(7).overviewZoom(); one != three || one != overviewMaxZoom { | |
| 74 | 74 | t.Fatalf("zoom with one band %v, with three %v", one, three) | |
| @@ -243,7 +243,7 @@ func TestOverviewCascadeMaximizedCardPick(t *testing.T) { | |||
| 243 | 243 | } | |
| 244 | 244 | } | |
| 245 | 245 | ||
| 246 | - | // T2: the selected band is the centre lane: undimmed, centred, selected tile | |
| 246 | + | // The selected band is the centre lane: undimmed, centred, selected tile | |
| 247 | 247 | // focused. | |
| 248 | 248 | func TestOverviewCascadeCentreLane(t *testing.T) { | |
| 249 | 249 | m := cascadeOverview() | |
| @@ -261,7 +261,7 @@ func TestOverviewCascadeCentreLane(t *testing.T) { | |||
| 261 | 261 | } | |
| 262 | 262 | } | |
| 263 | 263 | ||
| 264 | - | // T3: the other bands are dimmed lanes a screen (and a gap) away; those | |
| 264 | + | // The other bands are dimmed lanes a screen (and a gap) away; those | |
| 265 | 265 | // outside the area are hidden. | |
| 266 | 266 | func TestOverviewCascadeOtherLanes(t *testing.T) { | |
| 267 | 267 | m := cascadeOverview() | |
| @@ -309,7 +309,7 @@ func TestOverviewCascadeOtherLanes(t *testing.T) { | |||
| 309 | 309 | } | |
| 310 | 310 | } | |
| 311 | 311 | ||
| 312 | - | // T4: neighbor workspaces sit left and right of the current slot, dimmed, | |
| 312 | + | // Neighbor workspaces sit left and right of the current slot, dimmed, | |
| 313 | 313 | // their selected lane level with the centre lane. | |
| 314 | 314 | func TestOverviewCascadeNeighbors(t *testing.T) { | |
| 315 | 315 | m := cascadeOverview() | |
| @@ -355,7 +355,7 @@ func TestOverviewCascadeNeighbors(t *testing.T) { | |||
| 355 | 355 | } | |
| 356 | 356 | } | |
| 357 | 357 | ||
| 358 | - | // T5: lanes along the overview axis would overlap the other workspaces: a | |
| 358 | + | // Lanes along the overview axis would overlap the other workspaces: a | |
| 359 | 359 | // cascade neighbor of a scroll workspace shows its selected band only. | |
| 360 | 360 | func TestOverviewCascadeNeighborBelowScrollShowsOneBand(t *testing.T) { | |
| 361 | 361 | m := cascadeOverview() | |
| @@ -384,7 +384,7 @@ func TestOverviewCascadeNeighborBelowScrollShowsOneBand(t *testing.T) { | |||
| 384 | 384 | } | |
| 385 | 385 | } | |
| 386 | 386 | ||
| 387 | - | // T6: the stash pile is inside the current slot, left of the band; the | |
| 387 | + | // The stash pile is inside the current slot, left of the band; the | |
| 388 | 388 | // previous workspace stays clear of it. | |
| 389 | 389 | func TestOverviewCascadeStashInSlot(t *testing.T) { | |
| 390 | 390 | m := cascadeOverview() | |
| @@ -449,7 +449,7 @@ func focusedID(m *Monitor) WindowID { | |||
| 449 | 449 | return id | |
| 450 | 450 | } | |
| 451 | 451 | ||
| 452 | - | // T7: ↑/↓ change band and stop at the ends; ←/→ walk the band, then leave it | |
| 452 | + | // ↑/↓ change band and stop at the ends; ←/→ walk the band, then leave it | |
| 453 | 453 | // for the stash or the neighbor workspaces; Escape restores everything. | |
| 454 | 454 | func TestOverviewCascadeKeys(t *testing.T) { | |
| 455 | 455 | for _, d := range cascadeDrivers { | |
| @@ -530,7 +530,7 @@ func TestOverviewCascadeKeys(t *testing.T) { | |||
| 530 | 530 | } | |
| 531 | 531 | ||
| 532 | 532 | // T7b: with a stash card selected, up/down and the three-finger vertical | |
| 533 | - | // swipe cycle the pile as on main; confirming shows the picked window. | |
| 533 | + | // swipe cycle the pile as in a vertical overview; confirming shows the picked window. | |
| 534 | 534 | func TestOverviewCascadeStashPile(t *testing.T) { | |
| 535 | 535 | for _, d := range cascadeDrivers { | |
| 536 | 536 | t.Run(d.name, func(t *testing.T) { | |
| @@ -768,7 +768,7 @@ func TestOverviewCascadeStashToMaximized(t *testing.T) { | |||
| 768 | 768 | } | |
| 769 | 769 | } | |
| 770 | 770 | ||
| 771 | - | // T8: a named boundary of a cascade overview is a vertical rule strictly | |
| 771 | + | // A named boundary of a cascade overview is a vertical rule strictly | |
| 772 | 772 | // between the two slots. | |
| 773 | 773 | func TestOverviewCascadeNamedDivider(t *testing.T) { | |
| 774 | 774 | m := namedOverviewMonitor() | |
| @@ -787,7 +787,7 @@ func TestOverviewCascadeNamedDivider(t *testing.T) { | |||
| 787 | 787 | } | |
| 788 | 788 | } | |
| 789 | 789 | ||
| 790 | - | // T10: covering floats fan along the horizontal axis: left goes deeper in the | |
| 790 | + | // Covering floats fan along the horizontal axis: left goes deeper in the | |
| 791 | 791 | // stack (the cards behind show on the left), right comes back to the front. | |
| 792 | 792 | func TestOverviewCascadeCoveringFloat(t *testing.T) { | |
| 793 | 793 | for _, d := range cascadeDrivers { | |
| @@ -872,7 +872,7 @@ func TestOverviewCascadePinnedFullscreenTile(t *testing.T) { | |||
| 872 | 872 | } | |
| 873 | 873 | } | |
| 874 | 874 | ||
| 875 | - | // T11: the focus binds keep their meaning in a cascade overview. | |
| 875 | + | // The focus binds keep their meaning in a cascade overview. | |
| 876 | 876 | func TestOverviewCascadeFocusBinds(t *testing.T) { | |
| 877 | 877 | m := cascadeOverview() | |
| 878 | 878 | w := m.Current() | |
| @@ -913,6 +913,19 @@ func TestOverviewCascadeFocusBinds(t *testing.T) { | |||
| 913 | 913 | } | |
| 914 | 914 | } | |
| 915 | 915 | ||
| 916 | + | // A three-finger step that moves nothing reports no change: no scene | |
| 917 | + | // transition, no keyboard grab. Here the first band of a cascade has no | |
| 918 | + | // band above it. | |
| 919 | + | func TestOverviewCascadeFingerNoopReportsNoChange(t *testing.T) { | |
| 920 | + | m := cascadeOverview() | |
| 921 | + | m.Current().FocusID(5) | |
| 922 | + | m.ToggleOverview() | |
| 923 | + | up := ports.PointerAxis{Source: ports.AxisFinger, Vertical: ports.ScrollAxis{Set: true, Value: -overviewScrollStep}} | |
| 924 | + | if m.overviewScroll(up) { | |
| 925 | + | t.Fatal("a step past the first band reported a change") | |
| 926 | + | } | |
| 927 | + | } | |
| 928 | + | ||
| 916 | 929 | func TestOverviewMixedLayoutGestureAndCancel(t *testing.T) { | |
| 917 | 930 | m := overviewMonitor() | |
| 918 | 931 | m.SetOverflow(OverflowFixed) | |
| @@ -68,7 +68,7 @@ func within1(a, b int) bool { return a-b <= 1 && b-a <= 1 } | |||||
| 68 | 68 | ||||
| 69 | func hiddenOff(p Placement) bool { return p.Hidden && p.Preview == 0 && !p.Focused } | 69 | func hiddenOff(p Placement) bool { return p.Hidden && p.Preview == 0 && !p.Focused } | ||
| 70 | 70 | ||||
| 71 | - | // T1: the zoom does not depend on the number of bands. | 71 | + | // The zoom does not depend on the number of bands. |
| 72 | func TestOverviewCascadeZoomIgnoresBandCount(t *testing.T) { | 72 | func TestOverviewCascadeZoomIgnoresBandCount(t *testing.T) { | ||
| 73 | if one, three := cascadeWorkspace(3).overviewZoom(), cascadeWorkspace(7).overviewZoom(); one != three || one != overviewMaxZoom { | 73 | if one, three := cascadeWorkspace(3).overviewZoom(), cascadeWorkspace(7).overviewZoom(); one != three || one != overviewMaxZoom { | ||
| 74 | t.Fatalf("zoom with one band %v, with three %v", one, three) | 74 | t.Fatalf("zoom with one band %v, with three %v", one, three) | ||
| @@ -243,7 +243,7 @@ func TestOverviewCascadeMaximizedCardPick(t *testing.T) { | |||||
| 243 | } | 243 | } | ||
| 244 | } | 244 | } | ||
| 245 | 245 | ||||
| 246 | - | // T2: the selected band is the centre lane: undimmed, centred, selected tile | 246 | + | // The selected band is the centre lane: undimmed, centred, selected tile |
| 247 | // focused. | 247 | // focused. | ||
| 248 | func TestOverviewCascadeCentreLane(t *testing.T) { | 248 | func TestOverviewCascadeCentreLane(t *testing.T) { | ||
| 249 | m := cascadeOverview() | 249 | m := cascadeOverview() | ||
| @@ -261,7 +261,7 @@ func TestOverviewCascadeCentreLane(t *testing.T) { | |||||
| 261 | } | 261 | } | ||
| 262 | } | 262 | } | ||
| 263 | 263 | ||||
| 264 | - | // T3: the other bands are dimmed lanes a screen (and a gap) away; those | 264 | + | // The other bands are dimmed lanes a screen (and a gap) away; those |
| 265 | // outside the area are hidden. | 265 | // outside the area are hidden. | ||
| 266 | func TestOverviewCascadeOtherLanes(t *testing.T) { | 266 | func TestOverviewCascadeOtherLanes(t *testing.T) { | ||
| 267 | m := cascadeOverview() | 267 | m := cascadeOverview() | ||
| @@ -309,7 +309,7 @@ func TestOverviewCascadeOtherLanes(t *testing.T) { | |||||
| 309 | } | 309 | } | ||
| 310 | } | 310 | } | ||
| 311 | 311 | ||||
| 312 | - | // T4: neighbor workspaces sit left and right of the current slot, dimmed, | 312 | + | // Neighbor workspaces sit left and right of the current slot, dimmed, |
| 313 | // their selected lane level with the centre lane. | 313 | // their selected lane level with the centre lane. | ||
| 314 | func TestOverviewCascadeNeighbors(t *testing.T) { | 314 | func TestOverviewCascadeNeighbors(t *testing.T) { | ||
| 315 | m := cascadeOverview() | 315 | m := cascadeOverview() | ||
| @@ -355,7 +355,7 @@ func TestOverviewCascadeNeighbors(t *testing.T) { | |||||
| 355 | } | 355 | } | ||
| 356 | } | 356 | } | ||
| 357 | 357 | ||||
| 358 | - | // T5: lanes along the overview axis would overlap the other workspaces: a | 358 | + | // Lanes along the overview axis would overlap the other workspaces: a |
| 359 | // cascade neighbor of a scroll workspace shows its selected band only. | 359 | // cascade neighbor of a scroll workspace shows its selected band only. | ||
| 360 | func TestOverviewCascadeNeighborBelowScrollShowsOneBand(t *testing.T) { | 360 | func TestOverviewCascadeNeighborBelowScrollShowsOneBand(t *testing.T) { | ||
| 361 | m := cascadeOverview() | 361 | m := cascadeOverview() | ||
| @@ -384,7 +384,7 @@ func TestOverviewCascadeNeighborBelowScrollShowsOneBand(t *testing.T) { | |||||
| 384 | } | 384 | } | ||
| 385 | } | 385 | } | ||
| 386 | 386 | ||||
| 387 | - | // T6: the stash pile is inside the current slot, left of the band; the | 387 | + | // The stash pile is inside the current slot, left of the band; the |
| 388 | // previous workspace stays clear of it. | 388 | // previous workspace stays clear of it. | ||
| 389 | func TestOverviewCascadeStashInSlot(t *testing.T) { | 389 | func TestOverviewCascadeStashInSlot(t *testing.T) { | ||
| 390 | m := cascadeOverview() | 390 | m := cascadeOverview() | ||
| @@ -449,7 +449,7 @@ func focusedID(m *Monitor) WindowID { | |||||
| 449 | return id | 449 | return id | ||
| 450 | } | 450 | } | ||
| 451 | 451 | ||||
| 452 | - | // T7: ↑/↓ change band and stop at the ends; ←/→ walk the band, then leave it | 452 | + | // ↑/↓ change band and stop at the ends; ←/→ walk the band, then leave it |
| 453 | // for the stash or the neighbor workspaces; Escape restores everything. | 453 | // for the stash or the neighbor workspaces; Escape restores everything. | ||
| 454 | func TestOverviewCascadeKeys(t *testing.T) { | 454 | func TestOverviewCascadeKeys(t *testing.T) { | ||
| 455 | for _, d := range cascadeDrivers { | 455 | for _, d := range cascadeDrivers { | ||
| @@ -530,7 +530,7 @@ func TestOverviewCascadeKeys(t *testing.T) { | |||||
| 530 | } | 530 | } | ||
| 531 | 531 | ||||
| 532 | // T7b: with a stash card selected, up/down and the three-finger vertical | 532 | // T7b: with a stash card selected, up/down and the three-finger vertical | ||
| 533 | - | // swipe cycle the pile as on main; confirming shows the picked window. | 533 | + | // swipe cycle the pile as in a vertical overview; confirming shows the picked window. |
| 534 | func TestOverviewCascadeStashPile(t *testing.T) { | 534 | func TestOverviewCascadeStashPile(t *testing.T) { | ||
| 535 | for _, d := range cascadeDrivers { | 535 | for _, d := range cascadeDrivers { | ||
| 536 | t.Run(d.name, func(t *testing.T) { | 536 | t.Run(d.name, func(t *testing.T) { | ||
| @@ -768,7 +768,7 @@ func TestOverviewCascadeStashToMaximized(t *testing.T) { | |||||
| 768 | } | 768 | } | ||
| 769 | } | 769 | } | ||
| 770 | 770 | ||||
| 771 | - | // T8: a named boundary of a cascade overview is a vertical rule strictly | 771 | + | // A named boundary of a cascade overview is a vertical rule strictly |
| 772 | // between the two slots. | 772 | // between the two slots. | ||
| 773 | func TestOverviewCascadeNamedDivider(t *testing.T) { | 773 | func TestOverviewCascadeNamedDivider(t *testing.T) { | ||
| 774 | m := namedOverviewMonitor() | 774 | m := namedOverviewMonitor() | ||
| @@ -787,7 +787,7 @@ func TestOverviewCascadeNamedDivider(t *testing.T) { | |||||
| 787 | } | 787 | } | ||
| 788 | } | 788 | } | ||
| 789 | 789 | ||||
| 790 | - | // T10: covering floats fan along the horizontal axis: left goes deeper in the | 790 | + | // Covering floats fan along the horizontal axis: left goes deeper in the |
| 791 | // stack (the cards behind show on the left), right comes back to the front. | 791 | // stack (the cards behind show on the left), right comes back to the front. | ||
| 792 | func TestOverviewCascadeCoveringFloat(t *testing.T) { | 792 | func TestOverviewCascadeCoveringFloat(t *testing.T) { | ||
| 793 | for _, d := range cascadeDrivers { | 793 | for _, d := range cascadeDrivers { | ||
| @@ -872,7 +872,7 @@ func TestOverviewCascadePinnedFullscreenTile(t *testing.T) { | |||||
| 872 | } | 872 | } | ||
| 873 | } | 873 | } | ||
| 874 | 874 | ||||
| 875 | - | // T11: the focus binds keep their meaning in a cascade overview. | 875 | + | // The focus binds keep their meaning in a cascade overview. |
| 876 | func TestOverviewCascadeFocusBinds(t *testing.T) { | 876 | func TestOverviewCascadeFocusBinds(t *testing.T) { | ||
| 877 | m := cascadeOverview() | 877 | m := cascadeOverview() | ||
| 878 | w := m.Current() | 878 | w := m.Current() | ||
| @@ -913,6 +913,19 @@ func TestOverviewCascadeFocusBinds(t *testing.T) { | |||||
| 913 | } | 913 | } | ||
| 914 | } | 914 | } | ||
| 915 | 915 | ||||
| 916 | + | // A three-finger step that moves nothing reports no change: no scene | |||
| 917 | + | // transition, no keyboard grab. Here the first band of a cascade has no | |||
| 918 | + | // band above it. | |||
| 919 | + | func TestOverviewCascadeFingerNoopReportsNoChange(t *testing.T) { | |||
| 920 | + | m := cascadeOverview() | |||
| 921 | + | m.Current().FocusID(5) | |||
| 922 | + | m.ToggleOverview() | |||
| 923 | + | up := ports.PointerAxis{Source: ports.AxisFinger, Vertical: ports.ScrollAxis{Set: true, Value: -overviewScrollStep}} | |||
| 924 | + | if m.overviewScroll(up) { | |||
| 925 | + | t.Fatal("a step past the first band reported a change") | |||
| 926 | + | } | |||
| 927 | + | } | |||
| 928 | + | ||||
| 916 | func TestOverviewMixedLayoutGestureAndCancel(t *testing.T) { | 929 | func TestOverviewMixedLayoutGestureAndCancel(t *testing.T) { | ||
| 917 | m := overviewMonitor() | 930 | m := overviewMonitor() | ||
| 918 | m.SetOverflow(OverflowFixed) | 931 | m.SetOverflow(OverflowFixed) | ||
M internal/core/overview_stack.go +1 -1
| @@ -170,7 +170,7 @@ func (m *Monitor) moveStack(d int) bool { | |||
| 170 | 170 | return false | |
| 171 | 171 | } | |
| 172 | 172 | m.setFront(w, items[at]) | |
| 173 | - | m.ov.cardOf, m.ov.card = nil, 0 | |
| 173 | + | m.clearCard() | |
| 174 | 174 | return true | |
| 175 | 175 | } | |
| 176 | 176 | ||
| @@ -170,7 +170,7 @@ func (m *Monitor) moveStack(d int) bool { | |||||
| 170 | return false | 170 | return false | ||
| 171 | } | 171 | } | ||
| 172 | m.setFront(w, items[at]) | 172 | m.setFront(w, items[at]) | ||
| 173 | - | m.ov.cardOf, m.ov.card = nil, 0 | 173 | + | m.clearCard() |
| 174 | return true | 174 | return true | ||
| 175 | } | 175 | } | ||
| 176 | 176 | ||||
M internal/core/overview_stash_input_test.go +24 -11
| @@ -17,42 +17,55 @@ func TestOverviewStashInputNavigation(t *testing.T) { | |||
| 17 | 17 | before := stashIDs(w) | |
| 18 | 18 | m.ToggleOverview() | |
| 19 | 19 | var at time.Duration | |
| 20 | - | move := func(dx, dy int, cancelled bool) { | |
| 20 | + | send := func(ev ports.InputEvent) { | |
| 21 | + | t.Helper() | |
| 22 | + | if err := c.handleInput(t.Context(), ev); err != nil { | |
| 23 | + | t.Fatal(err) | |
| 24 | + | } | |
| 25 | + | } | |
| 26 | + | move := func(dx, dy int) { | |
| 21 | 27 | t.Helper() | |
| 22 | 28 | switch input { | |
| 23 | 29 | case "swipe": | |
| 24 | 30 | at += time.Second | |
| 25 | - | c.swipeBegin(ports.SwipeBegin{Fingers: 3, Time: at}) | |
| 31 | + | send(ports.SwipeBegin{Fingers: 3, Time: at}) | |
| 26 | 32 | for range 3 { | |
| 27 | 33 | at += 8 * time.Millisecond | |
| 28 | - | c.swipeUpdate(ports.SwipeUpdate{DX: float64(dx) * 40, DY: float64(dy) * 40, Time: at}) | |
| 34 | + | send(ports.SwipeUpdate{DX: float64(dx) * 40, DY: float64(dy) * 40, Time: at}) | |
| 29 | 35 | } | |
| 30 | - | c.swipeEnd(ports.SwipeEnd{Cancelled: cancelled, Time: at}) | |
| 36 | + | send(ports.SwipeEnd{Time: at}) | |
| 31 | 37 | case "wheel": | |
| 32 | 38 | m.overviewScroll(ports.PointerAxis{Source: ports.AxisWheel, | |
| 33 | 39 | Horizontal: ports.ScrollAxis{Set: dx != 0, Value: float64(dx) * 70, V120: int32(dx * 120)}, | |
| 34 | 40 | Vertical: ports.ScrollAxis{Set: dy != 0, Value: float64(dy) * 70, V120: int32(dy * 120)}}) | |
| 35 | 41 | c.scrollStop(ports.PointerAxis{Vertical: ports.ScrollAxis{Stop: true}}) | |
| 36 | 42 | case "bind": | |
| 37 | - | a := ActionFocusWorkspaceNext | |
| 38 | - | if dy < 0 { | |
| 43 | + | var a Action | |
| 44 | + | switch { | |
| 45 | + | case dx == 0 && dy < 0: | |
| 39 | 46 | a = ActionFocusWorkspacePrev | |
| 40 | - | } else if dx > 0 { | |
| 47 | + | case dx == 0 && dy > 0: | |
| 48 | + | a = ActionFocusWorkspaceNext | |
| 49 | + | case dx > 0 && dy == 0: | |
| 41 | 50 | a = ActionFocusColumnRight | |
| 51 | + | case dx < 0 && dy == 0: | |
| 52 | + | a = ActionFocusColumnLeft | |
| 53 | + | default: | |
| 54 | + | t.Fatalf("no bind for move %d,%d", dx, dy) | |
| 42 | 55 | } | |
| 43 | 56 | m.overviewFocus(a) | |
| 44 | 57 | } | |
| 45 | 58 | } | |
| 46 | - | move(0, -1, false) | |
| 59 | + | move(0, -1) | |
| 47 | 60 | if m.cardAt(w) != 1 { | |
| 48 | 61 | t.Fatalf("up selected card %d, want 1", m.cardAt(w)) | |
| 49 | 62 | } | |
| 50 | - | move(0, 1, false) | |
| 51 | - | move(0, 1, false) | |
| 63 | + | move(0, 1) | |
| 64 | + | move(0, 1) | |
| 52 | 65 | if m.Current() != w || m.cardAt(w) != 2 || !slices.Equal(stashIDs(w), before) { | |
| 53 | 66 | t.Fatal("down escaped or reordered the stash") | |
| 54 | 67 | } | |
| 55 | - | move(1, 0, false) | |
| 68 | + | move(1, 0) | |
| 56 | 69 | if m.cardAt(w) >= 0 { | |
| 57 | 70 | t.Fatal("right did not leave the stash") | |
| 58 | 71 | } | |
| @@ -17,42 +17,55 @@ func TestOverviewStashInputNavigation(t *testing.T) { | |||||
| 17 | before := stashIDs(w) | 17 | before := stashIDs(w) | ||
| 18 | m.ToggleOverview() | 18 | m.ToggleOverview() | ||
| 19 | var at time.Duration | 19 | var at time.Duration | ||
| 20 | - | move := func(dx, dy int, cancelled bool) { | 20 | + | send := func(ev ports.InputEvent) { |
| 21 | + | t.Helper() | |||
| 22 | + | if err := c.handleInput(t.Context(), ev); err != nil { | |||
| 23 | + | t.Fatal(err) | |||
| 24 | + | } | |||
| 25 | + | } | |||
| 26 | + | move := func(dx, dy int) { | |||
| 21 | t.Helper() | 27 | t.Helper() | ||
| 22 | switch input { | 28 | switch input { | ||
| 23 | case "swipe": | 29 | case "swipe": | ||
| 24 | at += time.Second | 30 | at += time.Second | ||
| 25 | - | c.swipeBegin(ports.SwipeBegin{Fingers: 3, Time: at}) | 31 | + | send(ports.SwipeBegin{Fingers: 3, Time: at}) |
| 26 | for range 3 { | 32 | for range 3 { | ||
| 27 | at += 8 * time.Millisecond | 33 | at += 8 * time.Millisecond | ||
| 28 | - | c.swipeUpdate(ports.SwipeUpdate{DX: float64(dx) * 40, DY: float64(dy) * 40, Time: at}) | 34 | + | send(ports.SwipeUpdate{DX: float64(dx) * 40, DY: float64(dy) * 40, Time: at}) |
| 29 | } | 35 | } | ||
| 30 | - | c.swipeEnd(ports.SwipeEnd{Cancelled: cancelled, Time: at}) | 36 | + | send(ports.SwipeEnd{Time: at}) |
| 31 | case "wheel": | 37 | case "wheel": | ||
| 32 | m.overviewScroll(ports.PointerAxis{Source: ports.AxisWheel, | 38 | m.overviewScroll(ports.PointerAxis{Source: ports.AxisWheel, | ||
| 33 | Horizontal: ports.ScrollAxis{Set: dx != 0, Value: float64(dx) * 70, V120: int32(dx * 120)}, | 39 | Horizontal: ports.ScrollAxis{Set: dx != 0, Value: float64(dx) * 70, V120: int32(dx * 120)}, | ||
| 34 | Vertical: ports.ScrollAxis{Set: dy != 0, Value: float64(dy) * 70, V120: int32(dy * 120)}}) | 40 | Vertical: ports.ScrollAxis{Set: dy != 0, Value: float64(dy) * 70, V120: int32(dy * 120)}}) | ||
| 35 | c.scrollStop(ports.PointerAxis{Vertical: ports.ScrollAxis{Stop: true}}) | 41 | c.scrollStop(ports.PointerAxis{Vertical: ports.ScrollAxis{Stop: true}}) | ||
| 36 | case "bind": | 42 | case "bind": | ||
| 37 | - | a := ActionFocusWorkspaceNext | 43 | + | var a Action |
| 38 | - | if dy < 0 { | 44 | + | switch { |
| 45 | + | case dx == 0 && dy < 0: | |||
| 39 | a = ActionFocusWorkspacePrev | 46 | a = ActionFocusWorkspacePrev | ||
| 40 | - | } else if dx > 0 { | 47 | + | case dx == 0 && dy > 0: |
| 48 | + | a = ActionFocusWorkspaceNext | |||
| 49 | + | case dx > 0 && dy == 0: | |||
| 41 | a = ActionFocusColumnRight | 50 | a = ActionFocusColumnRight | ||
| 51 | + | case dx < 0 && dy == 0: | |||
| 52 | + | a = ActionFocusColumnLeft | |||
| 53 | + | default: | |||
| 54 | + | t.Fatalf("no bind for move %d,%d", dx, dy) | |||
| 42 | } | 55 | } | ||
| 43 | m.overviewFocus(a) | 56 | m.overviewFocus(a) | ||
| 44 | } | 57 | } | ||
| 45 | } | 58 | } | ||
| 46 | - | move(0, -1, false) | 59 | + | move(0, -1) |
| 47 | if m.cardAt(w) != 1 { | 60 | if m.cardAt(w) != 1 { | ||
| 48 | t.Fatalf("up selected card %d, want 1", m.cardAt(w)) | 61 | t.Fatalf("up selected card %d, want 1", m.cardAt(w)) | ||
| 49 | } | 62 | } | ||
| 50 | - | move(0, 1, false) | 63 | + | move(0, 1) |
| 51 | - | move(0, 1, false) | 64 | + | move(0, 1) |
| 52 | if m.Current() != w || m.cardAt(w) != 2 || !slices.Equal(stashIDs(w), before) { | 65 | if m.Current() != w || m.cardAt(w) != 2 || !slices.Equal(stashIDs(w), before) { | ||
| 53 | t.Fatal("down escaped or reordered the stash") | 66 | t.Fatal("down escaped or reordered the stash") | ||
| 54 | } | 67 | } | ||
| 55 | - | move(1, 0, false) | 68 | + | move(1, 0) |
| 56 | if m.cardAt(w) >= 0 { | 69 | if m.cardAt(w) >= 0 { | ||
| 57 | t.Fatal("right did not leave the stash") | 70 | t.Fatal("right did not leave the stash") | ||
| 58 | } | 71 | } | ||
M internal/core/slide.go +5 -5
| @@ -37,7 +37,7 @@ func (s *slide) step(now time.Time) { | |||
| 37 | 37 | // busy reports whether the slide is off its state or its spring runs. | |
| 38 | 38 | func (s *slide) busy() bool { return s.motion.on || s.off != 0 } | |
| 39 | 39 | ||
| 40 | - | // slidable reports whether a swipe can scroll the columns: only scroll | |
| 40 | + | // slidable reports whether a swipe can scroll the columns: scroll or cascade | |
| 41 | 41 | // overflow, with the focus on the columns and no fullscreen window. | |
| 42 | 42 | func (w *Workspace) slidable() bool { | |
| 43 | 43 | return w.Overflow != OverflowFixed && len(w.Columns) > 0 && w.fullscreen == 0 && !w.onFloat() | |
| @@ -57,8 +57,8 @@ func (w *Workspace) retarget(before int) { | |||
| 57 | 57 | func (w *Workspace) shiftPixels() int { return int(math.Round(w.view.off)) } | |
| 58 | 58 | ||
| 59 | 59 | // switchSpring is the workspace slide's spring from off: it snaps once it | |
| 60 | - | // is under half a logical pixel of the output height from its target | |
| 61 | - | // (slideLayout rounds the offset times the height). | |
| 60 | + | // is under half a logical pixel of the output span along the switch axis | |
| 61 | + | // from its target (slideLayout rounds the offset times that span). | |
| 62 | 62 | func (m *Monitor) switchSpring(off, velocity float64) spring { | |
| 63 | 63 | s := workspaceSpring(off, velocity) | |
| 64 | 64 | if h := m.switchAxis.span(m.template.Output); h > 0 { | |
| @@ -192,10 +192,10 @@ func (m *Monitor) slideLayout(cur []Placement) []Placement { | |||
| 192 | 192 | h := float64(m.switchAxis.span(m.template.Output)) | |
| 193 | 193 | pos := float64(base) + m.switchView.off | |
| 194 | 194 | offset := func(p []Placement, k int) []Placement { | |
| 195 | - | dy := int(math.Round((float64(k) - pos) * h)) | |
| 195 | + | d := int(math.Round((float64(k) - pos) * h)) | |
| 196 | 196 | for i := range p { | |
| 197 | 197 | if !p[i].Hidden { | |
| 198 | - | m.switchAxis.offset(&p[i].Rect, dy) | |
| 198 | + | m.switchAxis.offset(&p[i].Rect, d) | |
| 199 | 199 | } | |
| 200 | 200 | } | |
| 201 | 201 | return p | |
| @@ -37,7 +37,7 @@ func (s *slide) step(now time.Time) { | |||||
| 37 | // busy reports whether the slide is off its state or its spring runs. | 37 | // busy reports whether the slide is off its state or its spring runs. | ||
| 38 | func (s *slide) busy() bool { return s.motion.on || s.off != 0 } | 38 | func (s *slide) busy() bool { return s.motion.on || s.off != 0 } | ||
| 39 | 39 | ||||
| 40 | - | // slidable reports whether a swipe can scroll the columns: only scroll | 40 | + | // slidable reports whether a swipe can scroll the columns: scroll or cascade |
| 41 | // overflow, with the focus on the columns and no fullscreen window. | 41 | // overflow, with the focus on the columns and no fullscreen window. | ||
| 42 | func (w *Workspace) slidable() bool { | 42 | func (w *Workspace) slidable() bool { | ||
| 43 | return w.Overflow != OverflowFixed && len(w.Columns) > 0 && w.fullscreen == 0 && !w.onFloat() | 43 | return w.Overflow != OverflowFixed && len(w.Columns) > 0 && w.fullscreen == 0 && !w.onFloat() | ||
| @@ -57,8 +57,8 @@ func (w *Workspace) retarget(before int) { | |||||
| 57 | func (w *Workspace) shiftPixels() int { return int(math.Round(w.view.off)) } | 57 | func (w *Workspace) shiftPixels() int { return int(math.Round(w.view.off)) } | ||
| 58 | 58 | ||||
| 59 | // switchSpring is the workspace slide's spring from off: it snaps once it | 59 | // switchSpring is the workspace slide's spring from off: it snaps once it | ||
| 60 | - | // is under half a logical pixel of the output height from its target | 60 | + | // is under half a logical pixel of the output span along the switch axis |
| 61 | - | // (slideLayout rounds the offset times the height). | 61 | + | // from its target (slideLayout rounds the offset times that span). |
| 62 | func (m *Monitor) switchSpring(off, velocity float64) spring { | 62 | func (m *Monitor) switchSpring(off, velocity float64) spring { | ||
| 63 | s := workspaceSpring(off, velocity) | 63 | s := workspaceSpring(off, velocity) | ||
| 64 | if h := m.switchAxis.span(m.template.Output); h > 0 { | 64 | if h := m.switchAxis.span(m.template.Output); h > 0 { | ||
| @@ -192,10 +192,10 @@ func (m *Monitor) slideLayout(cur []Placement) []Placement { | |||||
| 192 | h := float64(m.switchAxis.span(m.template.Output)) | 192 | h := float64(m.switchAxis.span(m.template.Output)) | ||
| 193 | pos := float64(base) + m.switchView.off | 193 | pos := float64(base) + m.switchView.off | ||
| 194 | offset := func(p []Placement, k int) []Placement { | 194 | offset := func(p []Placement, k int) []Placement { | ||
| 195 | - | dy := int(math.Round((float64(k) - pos) * h)) | 195 | + | d := int(math.Round((float64(k) - pos) * h)) |
| 196 | for i := range p { | 196 | for i := range p { | ||
| 197 | if !p[i].Hidden { | 197 | if !p[i].Hidden { | ||
| 198 | - | m.switchAxis.offset(&p[i].Rect, dy) | 198 | + | m.switchAxis.offset(&p[i].Rect, d) |
| 199 | } | 199 | } | ||
| 200 | } | 200 | } | ||
| 201 | return p | 201 | return p | ||