fix(layout): preserve cascade focus and transition invariants
- commit
- 8c17b301a23a063d339628b8eb3d7e2f70e50541
- author
- brice · · 2026-10-06 08:06 UTC
- parent
- d0eb8af
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
13 files · +210 -78
M docs/config.md +5 -4
| @@ -139,15 +139,16 @@ bind.Cmd+p = workspace presentation | |||
| 139 | 139 | With `animations = on`, a three-finger swipe follows the fingers and moves one step at most, so a quick swipe never skips a column or a workspace: | |
| 140 | 140 | ||
| 141 | 141 | - Left or right scrolls the columns (`scroll` overflow) to the next column edge; the focus moves to a column fully on screen. | |
| 142 | - | - Up or down slides to the next numbered workspace. | |
| 142 | + | - Up or down slides to the next numbered workspace in scroll and fixed layouts. | |
| 143 | + | - In [Cascade](#cascade), up/down scrolls one band and left/right switches workspaces. | |
| 143 | 144 | ||
| 144 | 145 | The view sticks a little near each step and catches up between them. Past the next step it resists; lift and swipe again to go further. When the fingers lift, a quick swipe goes on to the next step and a slow one settles on the closest. | |
| 145 | 146 | ||
| 146 | - | With `natural-scroll = off`, a swipe left shows the columns to the left and a swipe up shows the workspace above. `natural-scroll = on` moves the content with the fingers, so both are reversed. | |
| 147 | + | In scroll and fixed layouts with `natural-scroll = off`, a swipe left shows the columns to the left and a swipe up shows the workspace above. `natural-scroll = on` moves the content with the fingers, so both are reversed. | |
| 147 | 148 | ||
| 148 | - | With `animations = off`, nothing moves during the swipe: when the fingers lift, a quick or long enough swipe runs `focus-column-left/right` or `focus-workspace-up/down`. | |
| 149 | + | With `animations = off`, nothing moves during the swipe: when the fingers lift, a quick or long enough swipe navigates content or workspaces along the layout's axes. Cascade band swipes stop at the first and last band. | |
| 149 | 150 | ||
| 150 | - | Over the shown stash a sideways swipe slides it with the fingers, with a stop on each window, and moves one window at most. Where the view cannot scroll (`fixed` overflow, a floating or fullscreen window, a named workspace), a quick swipe runs `focus-column-left/right` or `focus-workspace-up/down` when the fingers lift. | |
| 151 | + | Over the shown stash a sideways swipe slides it with the fingers, with a stop on each window, and moves one window at most. In scroll and fixed layouts where the view cannot scroll (a floating or fullscreen window, fixed overflow, or a named workspace), a quick swipe runs `focus-column-left/right` or `focus-workspace-up/down` when the fingers lift. Cascade keeps horizontal workspace navigation; vertical band swipes do nothing while a floating or fullscreen window has focus. | |
| 151 | 152 | ||
| 152 | 153 | Two-finger scroll goes to the window under the pointer with the touchpad's timestamps, so apps with kinetic scrolling keep their inertia. | |
| 153 | 154 | ||
| @@ -139,15 +139,16 @@ bind.Cmd+p = workspace presentation | |||||
| 139 | With `animations = on`, a three-finger swipe follows the fingers and moves one step at most, so a quick swipe never skips a column or a workspace: | 139 | With `animations = on`, a three-finger swipe follows the fingers and moves one step at most, so a quick swipe never skips a column or a workspace: | ||
| 140 | 140 | ||||
| 141 | - Left or right scrolls the columns (`scroll` overflow) to the next column edge; the focus moves to a column fully on screen. | 141 | - Left or right scrolls the columns (`scroll` overflow) to the next column edge; the focus moves to a column fully on screen. | ||
| 142 | - | - Up or down slides to the next numbered workspace. | 142 | + | - Up or down slides to the next numbered workspace in scroll and fixed layouts. |
| 143 | + | - In [Cascade](#cascade), up/down scrolls one band and left/right switches workspaces. | |||
| 143 | 144 | ||||
| 144 | The view sticks a little near each step and catches up between them. Past the next step it resists; lift and swipe again to go further. When the fingers lift, a quick swipe goes on to the next step and a slow one settles on the closest. | 145 | The view sticks a little near each step and catches up between them. Past the next step it resists; lift and swipe again to go further. When the fingers lift, a quick swipe goes on to the next step and a slow one settles on the closest. | ||
| 145 | 146 | ||||
| 146 | - | With `natural-scroll = off`, a swipe left shows the columns to the left and a swipe up shows the workspace above. `natural-scroll = on` moves the content with the fingers, so both are reversed. | 147 | + | In scroll and fixed layouts with `natural-scroll = off`, a swipe left shows the columns to the left and a swipe up shows the workspace above. `natural-scroll = on` moves the content with the fingers, so both are reversed. |
| 147 | 148 | ||||
| 148 | - | With `animations = off`, nothing moves during the swipe: when the fingers lift, a quick or long enough swipe runs `focus-column-left/right` or `focus-workspace-up/down`. | 149 | + | With `animations = off`, nothing moves during the swipe: when the fingers lift, a quick or long enough swipe navigates content or workspaces along the layout's axes. Cascade band swipes stop at the first and last band. |
| 149 | 150 | ||||
| 150 | - | Over the shown stash a sideways swipe slides it with the fingers, with a stop on each window, and moves one window at most. Where the view cannot scroll (`fixed` overflow, a floating or fullscreen window, a named workspace), a quick swipe runs `focus-column-left/right` or `focus-workspace-up/down` when the fingers lift. | 151 | + | Over the shown stash a sideways swipe slides it with the fingers, with a stop on each window, and moves one window at most. In scroll and fixed layouts where the view cannot scroll (a floating or fullscreen window, fixed overflow, or a named workspace), a quick swipe runs `focus-column-left/right` or `focus-workspace-up/down` when the fingers lift. Cascade keeps horizontal workspace navigation; vertical band swipes do nothing while a floating or fullscreen window has focus. |
| 151 | 152 | ||||
| 152 | Two-finger scroll goes to the window under the pointer with the touchpad's timestamps, so apps with kinetic scrolling keep their inertia. | 153 | Two-finger scroll goes to the window under the pointer with the touchpad's timestamps, so apps with kinetic scrolling keep their inertia. | ||
| 153 | 154 | ||||
A internal/adapters/config/cascade_test.go +20 -0
| @@ -0,0 +1,20 @@ | |||
| 1 | + | package config | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "strings" | |
| 5 | + | "testing" | |
| 6 | + | ) | |
| 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") | |
| 10 | + | if len(w) != 0 { | |
| 11 | + | t.Fatalf("warnings=%v", w) | |
| 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) | |
| 15 | + | } | |
| 16 | + | _, 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") { | |
| 18 | + | t.Fatalf("invalid overflow diagnostic: %v, %v", warnings, err) | |
| 19 | + | } | |
| 20 | + | } | |
| @@ -0,0 +1,20 @@ | |||||
| 1 | + | package config | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "strings" | |||
| 5 | + | "testing" | |||
| 6 | + | ) | |||
| 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") | |||
| 10 | + | if len(w) != 0 { | |||
| 11 | + | t.Fatalf("warnings=%v", w) | |||
| 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) | |||
| 15 | + | } | |||
| 16 | + | _, 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") { | |||
| 18 | + | t.Fatalf("invalid overflow diagnostic: %v, %v", warnings, err) | |||
| 19 | + | } | |||
| 20 | + | } | |||
M internal/core/cascade.go +28 -5
| @@ -2,19 +2,43 @@ package core | |||
| 2 | 2 | ||
| 3 | 3 | import "slices" | |
| 4 | 4 | ||
| 5 | - | func (w *Workspace) band(i int) int { return i / max(w.MaxColumns, 1) } | |
| 5 | + | func (w *Workspace) band(i int) int { return i / max(w.MaxColumns, 1) } | |
| 6 | + | func (w *Workspace) sameBand(a, b int) bool { return !w.policy().wraps || w.band(a) == w.band(b) } | |
| 7 | + | func (w *Workspace) cellWidth() int { | |
| 8 | + | k := max(w.MaxColumns, 1) | |
| 9 | + | return max((w.Usable.W-w.gap()*(k+1))/k, 0) | |
| 10 | + | } | |
| 11 | + | func (w *Workspace) cellInBand(b int) int { | |
| 12 | + | k := max(w.MaxColumns, 1) | |
| 13 | + | return min(b*k+w.Focus%k, len(w.Columns)-1) | |
| 14 | + | } | |
| 15 | + | func (w *Workspace) focusBand(dir int) bool { | |
| 16 | + | i := w.bandNeighbor(dir) | |
| 17 | + | if i < 0 { | |
| 18 | + | return false | |
| 19 | + | } | |
| 20 | + | w.unmaximize() | |
| 21 | + | w.Focus = i | |
| 22 | + | w.Columns[i].Focus = 0 | |
| 23 | + | if dir < 0 { | |
| 24 | + | w.Columns[i].Focus = len(w.Columns[i].Windows) - 1 | |
| 25 | + | } | |
| 26 | + | w.raiseColumns() | |
| 27 | + | w.scroll() | |
| 28 | + | return true | |
| 29 | + | } | |
| 6 | 30 | func (w *Workspace) bandNeighbor(dir int) int { | |
| 7 | 31 | k := max(w.MaxColumns, 1) | |
| 8 | 32 | b := w.band(w.Focus) + dir | |
| 9 | 33 | if b < 0 || b*k >= len(w.Columns) { | |
| 10 | 34 | return -1 | |
| 11 | 35 | } | |
| 12 | - | return min(b*k+w.Focus%k, len(w.Columns)-1) | |
| 36 | + | return w.cellInBand(b) | |
| 13 | 37 | } | |
| 14 | 38 | ||
| 15 | 39 | func (w *Workspace) cascadeRectsInto(dst []Rect, ignoreFullWidth bool) []Rect { | |
| 16 | 40 | k, g := max(w.MaxColumns, 1), w.gap() | |
| 17 | - | width := max((w.Usable.W-g*(k+1))/k, 0) | |
| 41 | + | width := w.cellWidth() | |
| 18 | 42 | rects := slices.Grow(dst[:0], len(w.Columns))[:len(w.Columns)] | |
| 19 | 43 | view := w.View + w.shiftPixels() | |
| 20 | 44 | for i := range w.Columns { | |
| @@ -43,7 +67,6 @@ func (w *Workspace) cascadeFocus(view int) int { | |||
| 43 | 67 | if len(w.Columns) == 0 { | |
| 44 | 68 | return 0 | |
| 45 | 69 | } | |
| 46 | - | k := max(w.MaxColumns, 1) | |
| 47 | 70 | b := min(max(view/max(w.Usable.H, 1), 0), w.band(len(w.Columns)-1)) | |
| 48 | - | return min(b*k+w.Focus%k, len(w.Columns)-1) | |
| 71 | + | return w.cellInBand(b) | |
| 49 | 72 | } | |
| @@ -2,19 +2,43 @@ package core | |||||
| 2 | 2 | ||||
| 3 | import "slices" | 3 | import "slices" | ||
| 4 | 4 | ||||
| 5 | - | func (w *Workspace) band(i int) int { return i / max(w.MaxColumns, 1) } | 5 | + | func (w *Workspace) band(i int) int { return i / max(w.MaxColumns, 1) } |
| 6 | + | func (w *Workspace) sameBand(a, b int) bool { return !w.policy().wraps || w.band(a) == w.band(b) } | |||
| 7 | + | func (w *Workspace) cellWidth() int { | |||
| 8 | + | k := max(w.MaxColumns, 1) | |||
| 9 | + | return max((w.Usable.W-w.gap()*(k+1))/k, 0) | |||
| 10 | + | } | |||
| 11 | + | func (w *Workspace) cellInBand(b int) int { | |||
| 12 | + | k := max(w.MaxColumns, 1) | |||
| 13 | + | return min(b*k+w.Focus%k, len(w.Columns)-1) | |||
| 14 | + | } | |||
| 15 | + | func (w *Workspace) focusBand(dir int) bool { | |||
| 16 | + | i := w.bandNeighbor(dir) | |||
| 17 | + | if i < 0 { | |||
| 18 | + | return false | |||
| 19 | + | } | |||
| 20 | + | w.unmaximize() | |||
| 21 | + | w.Focus = i | |||
| 22 | + | w.Columns[i].Focus = 0 | |||
| 23 | + | if dir < 0 { | |||
| 24 | + | w.Columns[i].Focus = len(w.Columns[i].Windows) - 1 | |||
| 25 | + | } | |||
| 26 | + | w.raiseColumns() | |||
| 27 | + | w.scroll() | |||
| 28 | + | return true | |||
| 29 | + | } | |||
| 6 | func (w *Workspace) bandNeighbor(dir int) int { | 30 | func (w *Workspace) bandNeighbor(dir int) int { | ||
| 7 | k := max(w.MaxColumns, 1) | 31 | k := max(w.MaxColumns, 1) | ||
| 8 | b := w.band(w.Focus) + dir | 32 | b := w.band(w.Focus) + dir | ||
| 9 | if b < 0 || b*k >= len(w.Columns) { | 33 | if b < 0 || b*k >= len(w.Columns) { | ||
| 10 | return -1 | 34 | return -1 | ||
| 11 | } | 35 | } | ||
| 12 | - | return min(b*k+w.Focus%k, len(w.Columns)-1) | 36 | + | return w.cellInBand(b) |
| 13 | } | 37 | } | ||
| 14 | 38 | ||||
| 15 | func (w *Workspace) cascadeRectsInto(dst []Rect, ignoreFullWidth bool) []Rect { | 39 | func (w *Workspace) cascadeRectsInto(dst []Rect, ignoreFullWidth bool) []Rect { | ||
| 16 | k, g := max(w.MaxColumns, 1), w.gap() | 40 | k, g := max(w.MaxColumns, 1), w.gap() | ||
| 17 | - | width := max((w.Usable.W-g*(k+1))/k, 0) | 41 | + | width := w.cellWidth() |
| 18 | rects := slices.Grow(dst[:0], len(w.Columns))[:len(w.Columns)] | 42 | rects := slices.Grow(dst[:0], len(w.Columns))[:len(w.Columns)] | ||
| 19 | view := w.View + w.shiftPixels() | 43 | view := w.View + w.shiftPixels() | ||
| 20 | for i := range w.Columns { | 44 | for i := range w.Columns { | ||
| @@ -43,7 +67,6 @@ func (w *Workspace) cascadeFocus(view int) int { | |||||
| 43 | if len(w.Columns) == 0 { | 67 | if len(w.Columns) == 0 { | ||
| 44 | return 0 | 68 | return 0 | ||
| 45 | } | 69 | } | ||
| 46 | - | k := max(w.MaxColumns, 1) | |||
| 47 | b := min(max(view/max(w.Usable.H, 1), 0), w.band(len(w.Columns)-1)) | 70 | b := min(max(view/max(w.Usable.H, 1), 0), w.band(len(w.Columns)-1)) | ||
| 48 | - | return min(b*k+w.Focus%k, len(w.Columns)-1) | 71 | + | return w.cellInBand(b) |
| 49 | } | 72 | } | ||
M internal/core/cascade_swipe_test.go +21 -2
| @@ -1,8 +1,10 @@ | |||
| 1 | 1 | package core_test | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | - | "github.com/bnema/neferwl/internal/ports" | |
| 5 | 4 | "testing" | |
| 5 | + | "time" | |
| 6 | + | ||
| 7 | + | "github.com/bnema/neferwl/internal/ports" | |
| 6 | 8 | ) | |
| 7 | 9 | ||
| 8 | 10 | func TestCascadeSwipeAxes(t *testing.T) { | |
| @@ -21,9 +23,26 @@ func TestCascadeSwipeAxes(t *testing.T) { | |||
| 21 | 23 | r.begin() | |
| 22 | 24 | s = r.move(t, 400, 0) | |
| 23 | 25 | workspace, ok := rectOf(s, 7) | |
| 24 | - | if !ok || workspace.X == 0 || workspace.Y != 0 { | |
| 26 | + | if !ok || workspace.X >= 0 || workspace.Y != 0 { | |
| 25 | 27 | t.Fatalf("workspace swipe must move horizontally: %v, shown=%v", workspace, ok) | |
| 26 | 28 | } | |
| 27 | 29 | r.end(t, true) | |
| 28 | 30 | r.settleAll(t, r.outs...) | |
| 29 | 31 | } | |
| 32 | + | ||
| 33 | + | func TestCascadeDiscreteSwipeRevealsBand(t *testing.T) { | |
| 34 | + | r := startSwipe(t, func(c *ports.Config) { c.Layout.Overflow = "cascade"; c.Layout.MaxColumns = 3; c.Animations.On = false }) | |
| 35 | + | for i := 1; i <= 4; i++ { | |
| 36 | + | r.mapWindow(t, ports.WindowID(i)) | |
| 37 | + | } | |
| 38 | + | r.begin() | |
| 39 | + | for range 10 { | |
| 40 | + | r.at += 8 * time.Millisecond | |
| 41 | + | r.input <- ports.SwipeUpdate{DY: -40, Time: r.at} | |
| 42 | + | } | |
| 43 | + | s := r.end(t, false) | |
| 44 | + | p, ok := rectOf(s, 1) | |
| 45 | + | if !ok || p.Y != 0 { | |
| 46 | + | t.Fatalf("band not revealed: %v shown=%v", p, ok) | |
| 47 | + | } | |
| 48 | + | } | |
| @@ -1,8 +1,10 @@ | |||||
| 1 | package core_test | 1 | package core_test | ||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | - | "github.com/bnema/neferwl/internal/ports" | |||
| 5 | "testing" | 4 | "testing" | ||
| 5 | + | "time" | |||
| 6 | + | ||||
| 7 | + | "github.com/bnema/neferwl/internal/ports" | |||
| 6 | ) | 8 | ) | ||
| 7 | 9 | ||||
| 8 | func TestCascadeSwipeAxes(t *testing.T) { | 10 | func TestCascadeSwipeAxes(t *testing.T) { | ||
| @@ -21,9 +23,26 @@ func TestCascadeSwipeAxes(t *testing.T) { | |||||
| 21 | r.begin() | 23 | r.begin() | ||
| 22 | s = r.move(t, 400, 0) | 24 | s = r.move(t, 400, 0) | ||
| 23 | workspace, ok := rectOf(s, 7) | 25 | workspace, ok := rectOf(s, 7) | ||
| 24 | - | if !ok || workspace.X == 0 || workspace.Y != 0 { | 26 | + | if !ok || workspace.X >= 0 || workspace.Y != 0 { |
| 25 | t.Fatalf("workspace swipe must move horizontally: %v, shown=%v", workspace, ok) | 27 | t.Fatalf("workspace swipe must move horizontally: %v, shown=%v", workspace, ok) | ||
| 26 | } | 28 | } | ||
| 27 | r.end(t, true) | 29 | r.end(t, true) | ||
| 28 | r.settleAll(t, r.outs...) | 30 | r.settleAll(t, r.outs...) | ||
| 29 | } | 31 | } | ||
| 32 | + | ||||
| 33 | + | func TestCascadeDiscreteSwipeRevealsBand(t *testing.T) { | |||
| 34 | + | r := startSwipe(t, func(c *ports.Config) { c.Layout.Overflow = "cascade"; c.Layout.MaxColumns = 3; c.Animations.On = false }) | |||
| 35 | + | for i := 1; i <= 4; i++ { | |||
| 36 | + | r.mapWindow(t, ports.WindowID(i)) | |||
| 37 | + | } | |||
| 38 | + | r.begin() | |||
| 39 | + | for range 10 { | |||
| 40 | + | r.at += 8 * time.Millisecond | |||
| 41 | + | r.input <- ports.SwipeUpdate{DY: -40, Time: r.at} | |||
| 42 | + | } | |||
| 43 | + | s := r.end(t, false) | |||
| 44 | + | p, ok := rectOf(s, 1) | |||
| 45 | + | if !ok || p.Y != 0 { | |||
| 46 | + | t.Fatalf("band not revealed: %v shown=%v", p, ok) | |||
| 47 | + | } | |||
| 48 | + | } | |||
M internal/core/cascade_test.go +61 -4
| @@ -94,7 +94,67 @@ func TestCascadeCovers(t *testing.T) { | |||
| 94 | 94 | } | |
| 95 | 95 | } | |
| 96 | 96 | ||
| 97 | - | func TestCascadeViewportSnapAndPolicy(t *testing.T) { | |
| 97 | + | func TestCascadeClearsMaximizationOnBandChanges(t *testing.T) { | |
| 98 | + | for _, move := range []struct { | |
| 99 | + | name string | |
| 100 | + | run func(*Workspace) | |
| 101 | + | }{ | |
| 102 | + | {"focus", func(w *Workspace) { w.FocusWindow(1) }}, | |
| 103 | + | {"append", func(w *Workspace) { w.AddWindow(5) }}, | |
| 104 | + | {"insert", func(w *Workspace) { w.insertColumn(4, Column{Windows: []WindowID{5}}) }}, | |
| 105 | + | } { | |
| 106 | + | t.Run(move.name, func(t *testing.T) { | |
| 107 | + | w := cascadeWorkspace(4) | |
| 108 | + | w.FocusID(1) | |
| 109 | + | w.ToggleFullWidth() | |
| 110 | + | move.run(w) | |
| 111 | + | for _, col := range w.Columns { | |
| 112 | + | if col.FullWidth { | |
| 113 | + | t.Fatal("stale maximization") | |
| 114 | + | } | |
| 115 | + | } | |
| 116 | + | }) | |
| 117 | + | } | |
| 118 | + | } | |
| 119 | + | ||
| 120 | + | func TestCascadeGeometryReusesBuffer(t *testing.T) { | |
| 121 | + | w := cascadeWorkspace(7) | |
| 122 | + | buf := w.columnRectsInto(nil, false) | |
| 123 | + | if n := testing.AllocsPerRun(100, func() { buf = w.columnRectsInto(buf, false) }); n != 0 { | |
| 124 | + | t.Fatalf("allocations=%v", n) | |
| 125 | + | } | |
| 126 | + | } | |
| 127 | + | ||
| 128 | + | func TestMixedLayoutSwitchKeepsSourceAxis(t *testing.T) { | |
| 129 | + | m := NewMonitor() | |
| 130 | + | m.SetOutput(900, 600) | |
| 131 | + | m.Current().Overflow = OverflowCascade | |
| 132 | + | m.AddWindow(1) | |
| 133 | + | m.Focus(1) | |
| 134 | + | m.AddWindow(2) | |
| 135 | + | m.Current().Overflow = OverflowScroll | |
| 136 | + | m.Focus(0) | |
| 137 | + | m.switchAxis = m.Current().policy().workspace | |
| 138 | + | m.switchView.off = .5 | |
| 139 | + | ps := m.layoutInto(nil) | |
| 140 | + | for _, p := range ps { | |
| 141 | + | if p.ID == 1 && (p.Rect.X != -450 || p.Rect.Y != 0) { | |
| 142 | + | t.Fatalf("cascade source offset=%v", p.Rect) | |
| 143 | + | } | |
| 144 | + | } | |
| 145 | + | m.stopSwitch() | |
| 146 | + | m.Focus(1) | |
| 147 | + | m.switchAxis = m.Current().policy().workspace | |
| 148 | + | m.switchView.off = -.5 | |
| 149 | + | ps = m.layoutInto(nil) | |
| 150 | + | for _, p := range ps { | |
| 151 | + | if p.ID == 2 && (p.Rect.X != 0 || p.Rect.Y != 300) { | |
| 152 | + | t.Fatalf("scroll source offset=%v", p.Rect) | |
| 153 | + | } | |
| 154 | + | } | |
| 155 | + | } | |
| 156 | + | ||
| 157 | + | func TestCascadeViewportSnap(t *testing.T) { | |
| 98 | 158 | w := cascadeWorkspace(7) | |
| 99 | 159 | w.FocusID(3) | |
| 100 | 160 | if w.snapFocus(600, true) != 5 { | |
| @@ -103,9 +163,6 @@ func TestCascadeViewportSnapAndPolicy(t *testing.T) { | |||
| 103 | 163 | if w.snapFocus(1200, true) != 6 { | |
| 104 | 164 | t.Fatal("short band neighbour") | |
| 105 | 165 | } | |
| 106 | - | if w.policy().content != verticalAxis || w.policy().workspace != horizontalAxis { | |
| 107 | - | t.Fatal("axes") | |
| 108 | - | } | |
| 109 | 166 | w.SetUsable(Rect{}) | |
| 110 | 167 | if got := w.snapPoints(); len(got) != 1 || got[0] != 0 { | |
| 111 | 168 | t.Fatalf("zero area=%v", got) | |
| @@ -94,7 +94,67 @@ func TestCascadeCovers(t *testing.T) { | |||||
| 94 | } | 94 | } | ||
| 95 | } | 95 | } | ||
| 96 | 96 | ||||
| 97 | - | func TestCascadeViewportSnapAndPolicy(t *testing.T) { | 97 | + | func TestCascadeClearsMaximizationOnBandChanges(t *testing.T) { |
| 98 | + | for _, move := range []struct { | |||
| 99 | + | name string | |||
| 100 | + | run func(*Workspace) | |||
| 101 | + | }{ | |||
| 102 | + | {"focus", func(w *Workspace) { w.FocusWindow(1) }}, | |||
| 103 | + | {"append", func(w *Workspace) { w.AddWindow(5) }}, | |||
| 104 | + | {"insert", func(w *Workspace) { w.insertColumn(4, Column{Windows: []WindowID{5}}) }}, | |||
| 105 | + | } { | |||
| 106 | + | t.Run(move.name, func(t *testing.T) { | |||
| 107 | + | w := cascadeWorkspace(4) | |||
| 108 | + | w.FocusID(1) | |||
| 109 | + | w.ToggleFullWidth() | |||
| 110 | + | move.run(w) | |||
| 111 | + | for _, col := range w.Columns { | |||
| 112 | + | if col.FullWidth { | |||
| 113 | + | t.Fatal("stale maximization") | |||
| 114 | + | } | |||
| 115 | + | } | |||
| 116 | + | }) | |||
| 117 | + | } | |||
| 118 | + | } | |||
| 119 | + | ||||
| 120 | + | func TestCascadeGeometryReusesBuffer(t *testing.T) { | |||
| 121 | + | w := cascadeWorkspace(7) | |||
| 122 | + | buf := w.columnRectsInto(nil, false) | |||
| 123 | + | if n := testing.AllocsPerRun(100, func() { buf = w.columnRectsInto(buf, false) }); n != 0 { | |||
| 124 | + | t.Fatalf("allocations=%v", n) | |||
| 125 | + | } | |||
| 126 | + | } | |||
| 127 | + | ||||
| 128 | + | func TestMixedLayoutSwitchKeepsSourceAxis(t *testing.T) { | |||
| 129 | + | m := NewMonitor() | |||
| 130 | + | m.SetOutput(900, 600) | |||
| 131 | + | m.Current().Overflow = OverflowCascade | |||
| 132 | + | m.AddWindow(1) | |||
| 133 | + | m.Focus(1) | |||
| 134 | + | m.AddWindow(2) | |||
| 135 | + | m.Current().Overflow = OverflowScroll | |||
| 136 | + | m.Focus(0) | |||
| 137 | + | m.switchAxis = m.Current().policy().workspace | |||
| 138 | + | m.switchView.off = .5 | |||
| 139 | + | ps := m.layoutInto(nil) | |||
| 140 | + | for _, p := range ps { | |||
| 141 | + | if p.ID == 1 && (p.Rect.X != -450 || p.Rect.Y != 0) { | |||
| 142 | + | t.Fatalf("cascade source offset=%v", p.Rect) | |||
| 143 | + | } | |||
| 144 | + | } | |||
| 145 | + | m.stopSwitch() | |||
| 146 | + | m.Focus(1) | |||
| 147 | + | m.switchAxis = m.Current().policy().workspace | |||
| 148 | + | m.switchView.off = -.5 | |||
| 149 | + | ps = m.layoutInto(nil) | |||
| 150 | + | for _, p := range ps { | |||
| 151 | + | if p.ID == 2 && (p.Rect.X != 0 || p.Rect.Y != 300) { | |||
| 152 | + | t.Fatalf("scroll source offset=%v", p.Rect) | |||
| 153 | + | } | |||
| 154 | + | } | |||
| 155 | + | } | |||
| 156 | + | ||||
| 157 | + | func TestCascadeViewportSnap(t *testing.T) { | |||
| 98 | w := cascadeWorkspace(7) | 158 | w := cascadeWorkspace(7) | ||
| 99 | w.FocusID(3) | 159 | w.FocusID(3) | ||
| 100 | if w.snapFocus(600, true) != 5 { | 160 | if w.snapFocus(600, true) != 5 { | ||
| @@ -103,9 +163,6 @@ func TestCascadeViewportSnapAndPolicy(t *testing.T) { | |||||
| 103 | if w.snapFocus(1200, true) != 6 { | 163 | if w.snapFocus(1200, true) != 6 { | ||
| 104 | t.Fatal("short band neighbour") | 164 | t.Fatal("short band neighbour") | ||
| 105 | } | 165 | } | ||
| 106 | - | if w.policy().content != verticalAxis || w.policy().workspace != horizontalAxis { | |||
| 107 | - | t.Fatal("axes") | |||
| 108 | - | } | |||
| 109 | w.SetUsable(Rect{}) | 166 | w.SetUsable(Rect{}) | ||
| 110 | if got := w.snapPoints(); len(got) != 1 || got[0] != 0 { | 167 | if got := w.snapPoints(); len(got) != 1 || got[0] != 0 { | ||
| 111 | t.Fatalf("zero area=%v", got) | 168 | t.Fatalf("zero area=%v", got) | ||
M internal/core/gesture.go +45 -24
| @@ -90,7 +90,7 @@ type swipeGesture struct { | |||
| 90 | 90 | // listChanged reports whether the numbered workspaces or the active one | |
| 91 | 91 | // changed since a workspace slide began, or the overview opened over it. | |
| 92 | 92 | func (g *swipeGesture) listChanged(m *Monitor) bool { | |
| 93 | - | return m.overviewOpens != g.opens || m.shown != nil || m.Workspaces[m.Active] != g.ws || !slices.Equal(m.Workspaces, g.list) | |
| 93 | + | return m.overviewOpens != g.opens || m.shown != nil || m.Workspaces[m.Active] != g.ws || g.horizontal != (g.ws.policy().workspace == horizontalAxis) || !slices.Equal(m.Workspaces, g.list) | |
| 94 | 94 | } | |
| 95 | 95 | ||
| 96 | 96 | // columnsChanged reports whether a column swipe lost its workspace: it is | |
| @@ -98,7 +98,25 @@ func (g *swipeGesture) listChanged(m *Monitor) bool { | |||
| 98 | 98 | // panel, a window mapped or resized, other gaps), or the overview opened | |
| 99 | 99 | // over it. | |
| 100 | 100 | func (g *swipeGesture) columnsChanged(m *Monitor) bool { | |
| 101 | - | return m.overviewOpens != g.opens || m.Current() != g.ws || !g.ws.slidable() || !slices.Equal(g.ws.snapPoints(), g.points) | |
| 101 | + | if m.overviewOpens != g.opens || m.Current() != g.ws || !g.ws.slidable() || g.horizontal != (g.ws.policy().content == horizontalAxis) { | |
| 102 | + | return true | |
| 103 | + | } | |
| 104 | + | if g.ws.policy().wraps { | |
| 105 | + | n := 1 | |
| 106 | + | if len(g.ws.Columns) > 0 && g.ws.Usable.H > 0 { | |
| 107 | + | n = g.ws.band(len(g.ws.Columns)-1) + 1 | |
| 108 | + | } | |
| 109 | + | if len(g.points) != n { | |
| 110 | + | return true | |
| 111 | + | } | |
| 112 | + | for i, p := range g.points { | |
| 113 | + | if p != float64(i*g.ws.Usable.H) { | |
| 114 | + | return true | |
| 115 | + | } | |
| 116 | + | } | |
| 117 | + | return false | |
| 118 | + | } | |
| 119 | + | return !slices.Equal(g.ws.snapPoints(), g.points) | |
| 102 | 120 | } | |
| 103 | 121 | ||
| 104 | 122 | // stashChanged reports whether a stash swipe lost its stash: it is no | |
| @@ -251,7 +269,11 @@ func (c *Core) decide(g *swipeGesture) { | |||
| 251 | 269 | m.stopSwitch() | |
| 252 | 270 | } | |
| 253 | 271 | g.mode, g.ws = swipeWorkspaces, m.Workspaces[m.Active] | |
| 254 | - | m.switchAxis = w.policy().workspace | |
| 272 | + | axis := w.policy().workspace | |
| 273 | + | if m.switchView.busy() && m.switchAxis != axis { | |
| 274 | + | m.stopSwitch() | |
| 275 | + | } | |
| 276 | + | m.switchAxis = axis | |
| 255 | 277 | g.list = slices.Clone(m.Workspaces) | |
| 256 | 278 | m.switchView.motion, m.switchList = motion{}, nil | |
| 257 | 279 | g.snap = newSnapSwipe(float64(m.Active)+m.switchView.off, float64(m.Active), 1/workspaceSwipeMovement, indexPoints(len(m.Workspaces)), workspaceBand) | |
| @@ -287,7 +309,11 @@ func (c *Core) swipeEnd(e ports.SwipeEnd) (shown bool) { | |||
| 287 | 309 | target, velocity := g.snap.end(e.Cancelled, e.Time) | |
| 288 | 310 | if !e.Cancelled { | |
| 289 | 311 | view := int(math.Round(target)) | |
| 290 | - | w.Focus = w.snapFocus(view, target >= shown) | |
| 312 | + | focus := w.snapFocus(view, target >= shown) | |
| 313 | + | if focus != w.Focus && w.policy().equalCells { | |
| 314 | + | w.unmaximize() | |
| 315 | + | } | |
| 316 | + | w.Focus = focus | |
| 291 | 317 | w.View = view | |
| 292 | 318 | // A column wider than the view aligns as focus moves align it. | |
| 293 | 319 | w.scroll() | |
| @@ -381,32 +407,27 @@ func (c *Core) swipeEnd(e ports.SwipeEnd) (shown bool) { | |||
| 381 | 407 | if step == 0 { | |
| 382 | 408 | return false | |
| 383 | 409 | } | |
| 384 | - | if g.screen.mon.Current().Overflow == OverflowCascade && !g.horizontal { | |
| 385 | - | w := g.screen.mon.Current() | |
| 386 | - | if c.cur() != g.screen || w.onFloat() || w.fullscreen != 0 { | |
| 387 | - | return false | |
| 388 | - | } | |
| 389 | - | if next := w.bandNeighbor(step); next >= 0 { | |
| 390 | - | w.Focus = next | |
| 391 | - | w.scroll() | |
| 392 | - | return true | |
| 393 | - | } | |
| 394 | - | return false | |
| 395 | - | } | |
| 396 | - | a := ActionFocusWorkspaceDown | |
| 397 | - | switch { | |
| 398 | - | case g.horizontal && step < 0: | |
| 410 | + | w := g.screen.mon.Current() | |
| 411 | + | p := w.policy() | |
| 412 | + | workspace := g.horizontal == (p.workspace == horizontalAxis) | |
| 413 | + | a := ActionFocusColumnRight | |
| 414 | + | if step < 0 { | |
| 399 | 415 | a = ActionFocusColumnLeft | |
| 400 | - | case g.horizontal: | |
| 401 | - | a = ActionFocusColumnRight | |
| 402 | - | case step < 0: | |
| 403 | - | a = ActionFocusWorkspaceUp | |
| 404 | 416 | } | |
| 405 | - | if g.screen.mon.Current().Overflow == OverflowCascade && g.horizontal { | |
| 417 | + | if workspace { | |
| 406 | 418 | a = ActionFocusWorkspaceDown | |
| 407 | 419 | if step < 0 { | |
| 408 | 420 | a = ActionFocusWorkspaceUp | |
| 409 | 421 | } | |
| 422 | + | } else if p.wraps { | |
| 423 | + | // A content swipe cannot escape the last band to another workspace. | |
| 424 | + | if w.onFloat() || w.fullscreen != 0 || w.bandNeighbor(step) < 0 { | |
| 425 | + | return false | |
| 426 | + | } | |
| 427 | + | a = ActionFocusWindowDown | |
| 428 | + | if step < 0 { | |
| 429 | + | a = ActionFocusWindowUp | |
| 430 | + | } | |
| 410 | 431 | } | |
| 411 | 432 | // Focus actions act on the focused screen: the swipe's, unless the | |
| 412 | 433 | // pointer took the focus to another output meanwhile. | |
| @@ -90,7 +90,7 @@ type swipeGesture struct { | |||||
| 90 | // listChanged reports whether the numbered workspaces or the active one | 90 | // listChanged reports whether the numbered workspaces or the active one | ||
| 91 | // changed since a workspace slide began, or the overview opened over it. | 91 | // changed since a workspace slide began, or the overview opened over it. | ||
| 92 | func (g *swipeGesture) listChanged(m *Monitor) bool { | 92 | func (g *swipeGesture) listChanged(m *Monitor) bool { | ||
| 93 | - | return m.overviewOpens != g.opens || m.shown != nil || m.Workspaces[m.Active] != g.ws || !slices.Equal(m.Workspaces, g.list) | 93 | + | return m.overviewOpens != g.opens || m.shown != nil || m.Workspaces[m.Active] != g.ws || g.horizontal != (g.ws.policy().workspace == horizontalAxis) || !slices.Equal(m.Workspaces, g.list) |
| 94 | } | 94 | } | ||
| 95 | 95 | ||||
| 96 | // columnsChanged reports whether a column swipe lost its workspace: it is | 96 | // columnsChanged reports whether a column swipe lost its workspace: it is | ||
| @@ -98,7 +98,25 @@ func (g *swipeGesture) listChanged(m *Monitor) bool { | |||||
| 98 | // panel, a window mapped or resized, other gaps), or the overview opened | 98 | // panel, a window mapped or resized, other gaps), or the overview opened | ||
| 99 | // over it. | 99 | // over it. | ||
| 100 | func (g *swipeGesture) columnsChanged(m *Monitor) bool { | 100 | func (g *swipeGesture) columnsChanged(m *Monitor) bool { | ||
| 101 | - | return m.overviewOpens != g.opens || m.Current() != g.ws || !g.ws.slidable() || !slices.Equal(g.ws.snapPoints(), g.points) | 101 | + | if m.overviewOpens != g.opens || m.Current() != g.ws || !g.ws.slidable() || g.horizontal != (g.ws.policy().content == horizontalAxis) { |
| 102 | + | return true | |||
| 103 | + | } | |||
| 104 | + | if g.ws.policy().wraps { | |||
| 105 | + | n := 1 | |||
| 106 | + | if len(g.ws.Columns) > 0 && g.ws.Usable.H > 0 { | |||
| 107 | + | n = g.ws.band(len(g.ws.Columns)-1) + 1 | |||
| 108 | + | } | |||
| 109 | + | if len(g.points) != n { | |||
| 110 | + | return true | |||
| 111 | + | } | |||
| 112 | + | for i, p := range g.points { | |||
| 113 | + | if p != float64(i*g.ws.Usable.H) { | |||
| 114 | + | return true | |||
| 115 | + | } | |||
| 116 | + | } | |||
| 117 | + | return false | |||
| 118 | + | } | |||
| 119 | + | return !slices.Equal(g.ws.snapPoints(), g.points) | |||
| 102 | } | 120 | } | ||
| 103 | 121 | ||||
| 104 | // stashChanged reports whether a stash swipe lost its stash: it is no | 122 | // stashChanged reports whether a stash swipe lost its stash: it is no | ||
| @@ -251,7 +269,11 @@ func (c *Core) decide(g *swipeGesture) { | |||||
| 251 | m.stopSwitch() | 269 | m.stopSwitch() | ||
| 252 | } | 270 | } | ||
| 253 | g.mode, g.ws = swipeWorkspaces, m.Workspaces[m.Active] | 271 | g.mode, g.ws = swipeWorkspaces, m.Workspaces[m.Active] | ||
| 254 | - | m.switchAxis = w.policy().workspace | 272 | + | axis := w.policy().workspace |
| 273 | + | if m.switchView.busy() && m.switchAxis != axis { | |||
| 274 | + | m.stopSwitch() | |||
| 275 | + | } | |||
| 276 | + | m.switchAxis = axis | |||
| 255 | g.list = slices.Clone(m.Workspaces) | 277 | g.list = slices.Clone(m.Workspaces) | ||
| 256 | m.switchView.motion, m.switchList = motion{}, nil | 278 | m.switchView.motion, m.switchList = motion{}, nil | ||
| 257 | g.snap = newSnapSwipe(float64(m.Active)+m.switchView.off, float64(m.Active), 1/workspaceSwipeMovement, indexPoints(len(m.Workspaces)), workspaceBand) | 279 | g.snap = newSnapSwipe(float64(m.Active)+m.switchView.off, float64(m.Active), 1/workspaceSwipeMovement, indexPoints(len(m.Workspaces)), workspaceBand) | ||
| @@ -287,7 +309,11 @@ func (c *Core) swipeEnd(e ports.SwipeEnd) (shown bool) { | |||||
| 287 | target, velocity := g.snap.end(e.Cancelled, e.Time) | 309 | target, velocity := g.snap.end(e.Cancelled, e.Time) | ||
| 288 | if !e.Cancelled { | 310 | if !e.Cancelled { | ||
| 289 | view := int(math.Round(target)) | 311 | view := int(math.Round(target)) | ||
| 290 | - | w.Focus = w.snapFocus(view, target >= shown) | 312 | + | focus := w.snapFocus(view, target >= shown) |
| 313 | + | if focus != w.Focus && w.policy().equalCells { | |||
| 314 | + | w.unmaximize() | |||
| 315 | + | } | |||
| 316 | + | w.Focus = focus | |||
| 291 | w.View = view | 317 | w.View = view | ||
| 292 | // A column wider than the view aligns as focus moves align it. | 318 | // A column wider than the view aligns as focus moves align it. | ||
| 293 | w.scroll() | 319 | w.scroll() | ||
| @@ -381,32 +407,27 @@ func (c *Core) swipeEnd(e ports.SwipeEnd) (shown bool) { | |||||
| 381 | if step == 0 { | 407 | if step == 0 { | ||
| 382 | return false | 408 | return false | ||
| 383 | } | 409 | } | ||
| 384 | - | if g.screen.mon.Current().Overflow == OverflowCascade && !g.horizontal { | 410 | + | w := g.screen.mon.Current() |
| 385 | - | w := g.screen.mon.Current() | 411 | + | p := w.policy() |
| 386 | - | if c.cur() != g.screen || w.onFloat() || w.fullscreen != 0 { | 412 | + | workspace := g.horizontal == (p.workspace == horizontalAxis) |
| 387 | - | return false | 413 | + | a := ActionFocusColumnRight |
| 388 | - | } | 414 | + | if step < 0 { |
| 389 | - | if next := w.bandNeighbor(step); next >= 0 { | |||
| 390 | - | w.Focus = next | |||
| 391 | - | w.scroll() | |||
| 392 | - | return true | |||
| 393 | - | } | |||
| 394 | - | return false | |||
| 395 | - | } | |||
| 396 | - | a := ActionFocusWorkspaceDown | |||
| 397 | - | switch { | |||
| 398 | - | case g.horizontal && step < 0: | |||
| 399 | a = ActionFocusColumnLeft | 415 | a = ActionFocusColumnLeft | ||
| 400 | - | case g.horizontal: | |||
| 401 | - | a = ActionFocusColumnRight | |||
| 402 | - | case step < 0: | |||
| 403 | - | a = ActionFocusWorkspaceUp | |||
| 404 | } | 416 | } | ||
| 405 | - | if g.screen.mon.Current().Overflow == OverflowCascade && g.horizontal { | 417 | + | if workspace { |
| 406 | a = ActionFocusWorkspaceDown | 418 | a = ActionFocusWorkspaceDown | ||
| 407 | if step < 0 { | 419 | if step < 0 { | ||
| 408 | a = ActionFocusWorkspaceUp | 420 | a = ActionFocusWorkspaceUp | ||
| 409 | } | 421 | } | ||
| 422 | + | } else if p.wraps { | |||
| 423 | + | // A content swipe cannot escape the last band to another workspace. | |||
| 424 | + | if w.onFloat() || w.fullscreen != 0 || w.bandNeighbor(step) < 0 { | |||
| 425 | + | return false | |||
| 426 | + | } | |||
| 427 | + | a = ActionFocusWindowDown | |||
| 428 | + | if step < 0 { | |||
| 429 | + | a = ActionFocusWindowUp | |||
| 430 | + | } | |||
| 410 | } | 431 | } | ||
| 411 | // Focus actions act on the focused screen: the swipe's, unless the | 432 | // Focus actions act on the focused screen: the swipe's, unless the | ||
| 412 | // pointer took the focus to another output meanwhile. | 433 | // pointer took the focus to another output meanwhile. | ||
M internal/core/layout.go +10 -19
| @@ -517,7 +517,7 @@ func (w *Workspace) addColumn(col Column) { | |||
| 517 | 517 | } else if len(w.Columns) > 0 { | |
| 518 | 518 | at = w.Focus + 1 | |
| 519 | 519 | } | |
| 520 | - | if w.Overflow == OverflowFixed && len(w.Columns) > 0 { | |
| 520 | + | if w.policy().equalCells && len(w.Columns) > 0 { | |
| 521 | 521 | w.unmaximize() | |
| 522 | 522 | } | |
| 523 | 523 | w.Columns = append(w.Columns, Column{}) | |
| @@ -777,17 +777,8 @@ func (w *Workspace) focusWindow(dir int) bool { | |||
| 777 | 777 | w.scroll() | |
| 778 | 778 | return true | |
| 779 | 779 | } | |
| 780 | - | if w.Overflow == OverflowCascade { | |
| 781 | - | if i := w.bandNeighbor(dir); i >= 0 { | |
| 782 | - | w.Focus = i | |
| 783 | - | w.Columns[i].Focus = 0 | |
| 784 | - | if dir < 0 { | |
| 785 | - | w.Columns[i].Focus = len(w.Columns[i].Windows) - 1 | |
| 786 | - | } | |
| 787 | - | w.raiseColumns() | |
| 788 | - | w.scroll() | |
| 789 | - | return true | |
| 790 | - | } | |
| 780 | + | if w.policy().wraps && w.focusBand(dir) { | |
| 781 | + | return true | |
| 791 | 782 | } | |
| 792 | 783 | if w.onScreenFocus() { | |
| 793 | 784 | if i := w.screenNeighbor(0, dir); i >= 0 { | |
| @@ -998,7 +989,7 @@ func (w *Workspace) takeColumn() (Column, bool) { | |||
| 998 | 989 | // insertColumn adds a column at index at (clamped) and focuses it. | |
| 999 | 990 | func (w *Workspace) insertColumn(at int, col Column) { | |
| 1000 | 991 | at = min(max(at, 0), len(w.Columns)) | |
| 1001 | - | if w.Overflow == OverflowFixed && len(w.Columns) > 0 { | |
| 992 | + | if w.policy().equalCells && len(w.Columns) > 0 { | |
| 1002 | 993 | w.unmaximize() | |
| 1003 | 994 | } | |
| 1004 | 995 | w.Columns = slices.Insert(w.Columns, at, col) | |
| @@ -1009,7 +1000,7 @@ func (w *Workspace) insertColumn(at int, col Column) { | |||
| 1009 | 1000 | // CycleWidth steps the focused column through the presets. Fixed overflow | |
| 1010 | 1001 | // ignores presets: it toggles the expanded column instead. | |
| 1011 | 1002 | func (w *Workspace) CycleWidth() { | |
| 1012 | - | if w.Overflow == OverflowCascade { | |
| 1003 | + | if w.policy().wraps { | |
| 1013 | 1004 | return | |
| 1014 | 1005 | } | |
| 1015 | 1006 | if w.Overflow == OverflowFixed { | |
| @@ -1391,8 +1382,8 @@ func (w *Workspace) columnWidthFor(i int, ignoreFullWidth bool) int { | |||
| 1391 | 1382 | } | |
| 1392 | 1383 | g := w.gap() | |
| 1393 | 1384 | // Fixed overflow never scrolls, so presets would push columns off screen. | |
| 1394 | - | if w.Overflow == OverflowCascade { | |
| 1395 | - | return max((w.Usable.W-g*(max(w.MaxColumns, 1)+1))/max(w.MaxColumns, 1), 0) | |
| 1385 | + | if w.policy().wraps { | |
| 1386 | + | return w.cellWidth() | |
| 1396 | 1387 | } | |
| 1397 | 1388 | if w.Columns[i].Width == (Width{}) || w.policy().equalCells { | |
| 1398 | 1389 | if len(w.Columns) == 1 { | |
| @@ -1414,7 +1405,7 @@ func (w *Workspace) columnX(i int) int { | |||
| 1414 | 1405 | func (w *Workspace) scroll() { | |
| 1415 | 1406 | before := w.View | |
| 1416 | 1407 | defer func() { w.retarget(before) }() | |
| 1417 | - | if w.Overflow == OverflowCascade { | |
| 1408 | + | if w.policy().wraps { | |
| 1418 | 1409 | w.View = w.band(w.Focus) * w.Usable.H | |
| 1419 | 1410 | return | |
| 1420 | 1411 | } | |
| @@ -1455,7 +1446,7 @@ func (w *Workspace) columnRectsFor(ignoreFullWidth bool) []Rect { | |||
| 1455 | 1446 | // when too small): the result aliases dst, so a caller passing a reused | |
| 1456 | 1447 | // buffer must be done with the previous result. | |
| 1457 | 1448 | func (w *Workspace) columnRectsInto(dst []Rect, ignoreFullWidth bool) []Rect { | |
| 1458 | - | if w.Overflow == OverflowCascade { | |
| 1449 | + | if w.policy().wraps { | |
| 1459 | 1450 | return w.cascadeRectsInto(dst, ignoreFullWidth) | |
| 1460 | 1451 | } | |
| 1461 | 1452 | g := w.gap() | |
| @@ -1646,7 +1637,7 @@ func (w *Workspace) appendLayout(dst []Placement) []Placement { | |||
| 1646 | 1637 | full := w.fullscreen == id && id != 0 | |
| 1647 | 1638 | // Fixed overflow cannot scroll to other columns while one fills | |
| 1648 | 1639 | // the view. Keep them in place, but out of the scene. | |
| 1649 | - | maximized := (w.Overflow == OverflowFixed || w.Overflow == OverflowCascade) && w.Columns[w.Focus].FullWidth && i != w.Focus && (w.Overflow == OverflowFixed || w.band(i) == w.band(w.Focus)) | |
| 1640 | + | maximized := w.policy().equalCells && w.Columns[w.Focus].FullWidth && i != w.Focus && w.sameBand(i, w.Focus) | |
| 1650 | 1641 | hidden := (cover != 0 && id != cover) || ((fullColumn || w.policy().equalCells && w.fullscreen != 0) && !full) || maximized | |
| 1651 | 1642 | if full { | |
| 1652 | 1643 | // Scroll mode aligns the view on the column; fixed never scrolls. | |
| @@ -517,7 +517,7 @@ func (w *Workspace) addColumn(col Column) { | |||||
| 517 | } else if len(w.Columns) > 0 { | 517 | } else if len(w.Columns) > 0 { | ||
| 518 | at = w.Focus + 1 | 518 | at = w.Focus + 1 | ||
| 519 | } | 519 | } | ||
| 520 | - | if w.Overflow == OverflowFixed && len(w.Columns) > 0 { | 520 | + | if w.policy().equalCells && len(w.Columns) > 0 { |
| 521 | w.unmaximize() | 521 | w.unmaximize() | ||
| 522 | } | 522 | } | ||
| 523 | w.Columns = append(w.Columns, Column{}) | 523 | w.Columns = append(w.Columns, Column{}) | ||
| @@ -777,17 +777,8 @@ func (w *Workspace) focusWindow(dir int) bool { | |||||
| 777 | w.scroll() | 777 | w.scroll() | ||
| 778 | return true | 778 | return true | ||
| 779 | } | 779 | } | ||
| 780 | - | if w.Overflow == OverflowCascade { | 780 | + | if w.policy().wraps && w.focusBand(dir) { |
| 781 | - | if i := w.bandNeighbor(dir); i >= 0 { | 781 | + | return true |
| 782 | - | w.Focus = i | |||
| 783 | - | w.Columns[i].Focus = 0 | |||
| 784 | - | if dir < 0 { | |||
| 785 | - | w.Columns[i].Focus = len(w.Columns[i].Windows) - 1 | |||
| 786 | - | } | |||
| 787 | - | w.raiseColumns() | |||
| 788 | - | w.scroll() | |||
| 789 | - | return true | |||
| 790 | - | } | |||
| 791 | } | 782 | } | ||
| 792 | if w.onScreenFocus() { | 783 | if w.onScreenFocus() { | ||
| 793 | if i := w.screenNeighbor(0, dir); i >= 0 { | 784 | if i := w.screenNeighbor(0, dir); i >= 0 { | ||
| @@ -998,7 +989,7 @@ func (w *Workspace) takeColumn() (Column, bool) { | |||||
| 998 | // insertColumn adds a column at index at (clamped) and focuses it. | 989 | // insertColumn adds a column at index at (clamped) and focuses it. | ||
| 999 | func (w *Workspace) insertColumn(at int, col Column) { | 990 | func (w *Workspace) insertColumn(at int, col Column) { | ||
| 1000 | at = min(max(at, 0), len(w.Columns)) | 991 | at = min(max(at, 0), len(w.Columns)) | ||
| 1001 | - | if w.Overflow == OverflowFixed && len(w.Columns) > 0 { | 992 | + | if w.policy().equalCells && len(w.Columns) > 0 { |
| 1002 | w.unmaximize() | 993 | w.unmaximize() | ||
| 1003 | } | 994 | } | ||
| 1004 | w.Columns = slices.Insert(w.Columns, at, col) | 995 | w.Columns = slices.Insert(w.Columns, at, col) | ||
| @@ -1009,7 +1000,7 @@ func (w *Workspace) insertColumn(at int, col Column) { | |||||
| 1009 | // CycleWidth steps the focused column through the presets. Fixed overflow | 1000 | // CycleWidth steps the focused column through the presets. Fixed overflow | ||
| 1010 | // ignores presets: it toggles the expanded column instead. | 1001 | // ignores presets: it toggles the expanded column instead. | ||
| 1011 | func (w *Workspace) CycleWidth() { | 1002 | func (w *Workspace) CycleWidth() { | ||
| 1012 | - | if w.Overflow == OverflowCascade { | 1003 | + | if w.policy().wraps { |
| 1013 | return | 1004 | return | ||
| 1014 | } | 1005 | } | ||
| 1015 | if w.Overflow == OverflowFixed { | 1006 | if w.Overflow == OverflowFixed { | ||
| @@ -1391,8 +1382,8 @@ func (w *Workspace) columnWidthFor(i int, ignoreFullWidth bool) int { | |||||
| 1391 | } | 1382 | } | ||
| 1392 | g := w.gap() | 1383 | g := w.gap() | ||
| 1393 | // Fixed overflow never scrolls, so presets would push columns off screen. | 1384 | // Fixed overflow never scrolls, so presets would push columns off screen. | ||
| 1394 | - | if w.Overflow == OverflowCascade { | 1385 | + | if w.policy().wraps { |
| 1395 | - | return max((w.Usable.W-g*(max(w.MaxColumns, 1)+1))/max(w.MaxColumns, 1), 0) | 1386 | + | return w.cellWidth() |
| 1396 | } | 1387 | } | ||
| 1397 | if w.Columns[i].Width == (Width{}) || w.policy().equalCells { | 1388 | if w.Columns[i].Width == (Width{}) || w.policy().equalCells { | ||
| 1398 | if len(w.Columns) == 1 { | 1389 | if len(w.Columns) == 1 { | ||
| @@ -1414,7 +1405,7 @@ func (w *Workspace) columnX(i int) int { | |||||
| 1414 | func (w *Workspace) scroll() { | 1405 | func (w *Workspace) scroll() { | ||
| 1415 | before := w.View | 1406 | before := w.View | ||
| 1416 | defer func() { w.retarget(before) }() | 1407 | defer func() { w.retarget(before) }() | ||
| 1417 | - | if w.Overflow == OverflowCascade { | 1408 | + | if w.policy().wraps { |
| 1418 | w.View = w.band(w.Focus) * w.Usable.H | 1409 | w.View = w.band(w.Focus) * w.Usable.H | ||
| 1419 | return | 1410 | return | ||
| 1420 | } | 1411 | } | ||
| @@ -1455,7 +1446,7 @@ func (w *Workspace) columnRectsFor(ignoreFullWidth bool) []Rect { | |||||
| 1455 | // when too small): the result aliases dst, so a caller passing a reused | 1446 | // when too small): the result aliases dst, so a caller passing a reused | ||
| 1456 | // buffer must be done with the previous result. | 1447 | // buffer must be done with the previous result. | ||
| 1457 | func (w *Workspace) columnRectsInto(dst []Rect, ignoreFullWidth bool) []Rect { | 1448 | func (w *Workspace) columnRectsInto(dst []Rect, ignoreFullWidth bool) []Rect { | ||
| 1458 | - | if w.Overflow == OverflowCascade { | 1449 | + | if w.policy().wraps { |
| 1459 | return w.cascadeRectsInto(dst, ignoreFullWidth) | 1450 | return w.cascadeRectsInto(dst, ignoreFullWidth) | ||
| 1460 | } | 1451 | } | ||
| 1461 | g := w.gap() | 1452 | g := w.gap() | ||
| @@ -1646,7 +1637,7 @@ func (w *Workspace) appendLayout(dst []Placement) []Placement { | |||||
| 1646 | full := w.fullscreen == id && id != 0 | 1637 | full := w.fullscreen == id && id != 0 | ||
| 1647 | // Fixed overflow cannot scroll to other columns while one fills | 1638 | // Fixed overflow cannot scroll to other columns while one fills | ||
| 1648 | // the view. Keep them in place, but out of the scene. | 1639 | // the view. Keep them in place, but out of the scene. | ||
| 1649 | - | maximized := (w.Overflow == OverflowFixed || w.Overflow == OverflowCascade) && w.Columns[w.Focus].FullWidth && i != w.Focus && (w.Overflow == OverflowFixed || w.band(i) == w.band(w.Focus)) | 1640 | + | maximized := w.policy().equalCells && w.Columns[w.Focus].FullWidth && i != w.Focus && w.sameBand(i, w.Focus) |
| 1650 | hidden := (cover != 0 && id != cover) || ((fullColumn || w.policy().equalCells && w.fullscreen != 0) && !full) || maximized | 1641 | hidden := (cover != 0 && id != cover) || ((fullColumn || w.policy().equalCells && w.fullscreen != 0) && !full) || maximized | ||
| 1651 | if full { | 1642 | if full { | ||
| 1652 | // Scroll mode aligns the view on the column; fixed never scrolls. | 1643 | // Scroll mode aligns the view on the column; fixed never scrolls. | ||
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, spiral bool | |
| 28 | + | content, workspace layoutAxis | |
| 29 | + | equalCells, appendLast, 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} | |
| 34 | + | return layoutPolicy{content: verticalAxis, workspace: horizontalAxis, equalCells: true, appendLast: true, wraps: true} | |
| 35 | 35 | } | |
| 36 | 36 | if w.Overflow == OverflowFixed { | |
| 37 | - | return layoutPolicy{content: horizontalAxis, workspace: verticalAxis, equalCells: true, appendLast: true, spiral: true} | |
| 37 | + | return layoutPolicy{content: horizontalAxis, workspace: verticalAxis, equalCells: true, appendLast: 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, spiral bool | 29 | + | equalCells, appendLast, 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} | 34 | + | return layoutPolicy{content: verticalAxis, workspace: horizontalAxis, equalCells: true, appendLast: 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, spiral: true} | 37 | + | return layoutPolicy{content: horizontalAxis, workspace: verticalAxis, equalCells: true, appendLast: true} |
| 38 | } | 38 | } | ||
| 39 | return layoutPolicy{content: horizontalAxis, workspace: verticalAxis} | 39 | return layoutPolicy{content: horizontalAxis, workspace: verticalAxis} | ||
| 40 | } | 40 | } | ||
M internal/core/layout_test.go +2 -2
| @@ -352,7 +352,7 @@ func TestLargeWidthAndBounds(t *testing.T) { | |||
| 352 | 352 | t.Fatal(w.Output) | |
| 353 | 353 | } | |
| 354 | 354 | } | |
| 355 | - | func TestUnchangedUsablePreservesViewX(t *testing.T) { | |
| 355 | + | func TestUnchangedUsablePreservesView(t *testing.T) { | |
| 356 | 356 | w := workspace() | |
| 357 | 357 | w.AddWindow(1) | |
| 358 | 358 | w.AddWindow(2) | |
| @@ -363,7 +363,7 @@ func TestUnchangedUsablePreservesViewX(t *testing.T) { | |||
| 363 | 363 | } | |
| 364 | 364 | w.SetUsable(w.Usable) | |
| 365 | 365 | if w.View != before { | |
| 366 | - | t.Fatalf("ViewX changed from %d to %d", before, w.View) | |
| 366 | + | t.Fatalf("View changed from %d to %d", before, w.View) | |
| 367 | 367 | } | |
| 368 | 368 | } | |
| 369 | 369 | ||
| @@ -352,7 +352,7 @@ func TestLargeWidthAndBounds(t *testing.T) { | |||||
| 352 | t.Fatal(w.Output) | 352 | t.Fatal(w.Output) | ||
| 353 | } | 353 | } | ||
| 354 | } | 354 | } | ||
| 355 | - | func TestUnchangedUsablePreservesViewX(t *testing.T) { | 355 | + | func TestUnchangedUsablePreservesView(t *testing.T) { |
| 356 | w := workspace() | 356 | w := workspace() | ||
| 357 | w.AddWindow(1) | 357 | w.AddWindow(1) | ||
| 358 | w.AddWindow(2) | 358 | w.AddWindow(2) | ||
| @@ -363,7 +363,7 @@ func TestUnchangedUsablePreservesViewX(t *testing.T) { | |||||
| 363 | } | 363 | } | ||
| 364 | w.SetUsable(w.Usable) | 364 | w.SetUsable(w.Usable) | ||
| 365 | if w.View != before { | 365 | if w.View != before { | ||
| 366 | - | t.Fatalf("ViewX changed from %d to %d", before, w.View) | 366 | + | t.Fatalf("View changed from %d to %d", before, w.View) |
| 367 | } | 367 | } | ||
| 368 | } | 368 | } | ||
| 369 | 369 | ||||
M internal/core/overview.go +3 -3
| @@ -293,7 +293,7 @@ func restoreRow(w *Workspace, s rowSnapshot) { | |||
| 293 | 293 | // Fixed overflow maximizes at most one column: only the one holding | |
| 294 | 294 | // the anchor window, wherever active binds moved it. Scroll overflow | |
| 295 | 295 | // keeps each column's own width. | |
| 296 | - | if w.Overflow == OverflowFixed { | |
| 296 | + | if w.policy().equalCells { | |
| 297 | 297 | w.unmaximize() | |
| 298 | 298 | for i := range w.Columns { | |
| 299 | 299 | if s.fullWidth != 0 && slices.Contains(w.Columns[i].Windows, s.fullWidth) { | |
| @@ -748,9 +748,9 @@ func (w *Workspace) previewTilesInto(tiles []Placement) ([]Placement, int, Rect) | |||
| 748 | 748 | maximized := w.Overflow == OverflowFixed && len(w.Columns) > 0 && w.Columns[w.Focus].FullWidth | |
| 749 | 749 | for i, c := range w.Columns { | |
| 750 | 750 | r := Rect{X: w.columnX(i) - w.Usable.X, Y: g, W: w.columnWidth(i), H: h} | |
| 751 | - | if w.Overflow == OverflowCascade { | |
| 751 | + | if w.policy().wraps { | |
| 752 | 752 | k := max(w.MaxColumns, 1) | |
| 753 | - | width := max((w.Usable.W-g*(k+1))/k, 0) | |
| 753 | + | width := w.cellWidth() | |
| 754 | 754 | r.X = w.band(i)*w.Usable.W + g + (i%k)*(width+g) | |
| 755 | 755 | r.W = width | |
| 756 | 756 | } else if w.Overflow == OverflowFixed { | |
| @@ -293,7 +293,7 @@ func restoreRow(w *Workspace, s rowSnapshot) { | |||||
| 293 | // Fixed overflow maximizes at most one column: only the one holding | 293 | // Fixed overflow maximizes at most one column: only the one holding | ||
| 294 | // the anchor window, wherever active binds moved it. Scroll overflow | 294 | // the anchor window, wherever active binds moved it. Scroll overflow | ||
| 295 | // keeps each column's own width. | 295 | // keeps each column's own width. | ||
| 296 | - | if w.Overflow == OverflowFixed { | 296 | + | if w.policy().equalCells { |
| 297 | w.unmaximize() | 297 | w.unmaximize() | ||
| 298 | for i := range w.Columns { | 298 | for i := range w.Columns { | ||
| 299 | if s.fullWidth != 0 && slices.Contains(w.Columns[i].Windows, s.fullWidth) { | 299 | if s.fullWidth != 0 && slices.Contains(w.Columns[i].Windows, s.fullWidth) { | ||
| @@ -748,9 +748,9 @@ func (w *Workspace) previewTilesInto(tiles []Placement) ([]Placement, int, Rect) | |||||
| 748 | maximized := w.Overflow == OverflowFixed && len(w.Columns) > 0 && w.Columns[w.Focus].FullWidth | 748 | maximized := w.Overflow == OverflowFixed && len(w.Columns) > 0 && w.Columns[w.Focus].FullWidth | ||
| 749 | for i, c := range w.Columns { | 749 | for i, c := range w.Columns { | ||
| 750 | r := Rect{X: w.columnX(i) - w.Usable.X, Y: g, W: w.columnWidth(i), H: h} | 750 | r := Rect{X: w.columnX(i) - w.Usable.X, Y: g, W: w.columnWidth(i), H: h} | ||
| 751 | - | if w.Overflow == OverflowCascade { | 751 | + | if w.policy().wraps { |
| 752 | k := max(w.MaxColumns, 1) | 752 | k := max(w.MaxColumns, 1) | ||
| 753 | - | width := max((w.Usable.W-g*(k+1))/k, 0) | 753 | + | width := w.cellWidth() |
| 754 | r.X = w.band(i)*w.Usable.W + g + (i%k)*(width+g) | 754 | r.X = w.band(i)*w.Usable.W + g + (i%k)*(width+g) | ||
| 755 | r.W = width | 755 | r.W = width | ||
| 756 | } else if w.Overflow == OverflowFixed { | 756 | } else if w.Overflow == OverflowFixed { | ||
M internal/core/overview_stack.go +1 -1
| @@ -180,7 +180,7 @@ func (m *Monitor) hiddenColumn(w *Workspace) int { | |||
| 180 | 180 | // selectOverviewColumn retains the existing ordinary-row live focus semantics. | |
| 181 | 181 | func (w *Workspace) selectOverviewColumn(i int) { | |
| 182 | 182 | w.floatFocus, w.stashFocus = false, false | |
| 183 | - | if w.Overflow == OverflowFixed && w.Focus != i && w.Focus < len(w.Columns) { | |
| 183 | + | if w.policy().equalCells && w.Focus != i && w.Focus < len(w.Columns) { | |
| 184 | 184 | w.unmaximize() | |
| 185 | 185 | } | |
| 186 | 186 | w.Focus = i | |
| @@ -180,7 +180,7 @@ func (m *Monitor) hiddenColumn(w *Workspace) int { | |||||
| 180 | // selectOverviewColumn retains the existing ordinary-row live focus semantics. | 180 | // selectOverviewColumn retains the existing ordinary-row live focus semantics. | ||
| 181 | func (w *Workspace) selectOverviewColumn(i int) { | 181 | func (w *Workspace) selectOverviewColumn(i int) { | ||
| 182 | w.floatFocus, w.stashFocus = false, false | 182 | w.floatFocus, w.stashFocus = false, false | ||
| 183 | - | if w.Overflow == OverflowFixed && w.Focus != i && w.Focus < len(w.Columns) { | 183 | + | if w.policy().equalCells && w.Focus != i && w.Focus < len(w.Columns) { |
| 184 | w.unmaximize() | 184 | w.unmaximize() | ||
| 185 | } | 185 | } | ||
| 186 | w.Focus = i | 186 | w.Focus = i | ||
M internal/core/slide.go +4 -4
| @@ -8,7 +8,7 @@ import ( | |||
| 8 | 8 | ||
| 9 | 9 | // Slides move what is on screen smoothly: a workspace's columns (view), its | |
| 10 | 10 | // stash (stashView) and the monitor between workspaces (switchView). All | |
| 11 | - | // are pure presentation over the settled state (ViewX, Focus, stashAt, | |
| 11 | + | // are pure presentation over the settled state (View, Focus, stashAt, | |
| 12 | 12 | // Active): the layout ends where the state says as soon as a slide stops. | |
| 13 | 13 | ||
| 14 | 14 | // slide is a view offset that a swipe sets while the fingers follow it, and | |
| @@ -43,7 +43,7 @@ func (w *Workspace) slidable() bool { | |||
| 43 | 43 | return w.Overflow != OverflowFixed && len(w.Columns) > 0 && w.fullscreen == 0 && !w.onFloat() | |
| 44 | 44 | } | |
| 45 | 45 | ||
| 46 | - | // retarget keeps the view where it is on screen when ViewX moved from | |
| 46 | + | // retarget keeps the view where it is on screen when View moved from | |
| 47 | 47 | // before during a landing slide: the slide heads for the new view. | |
| 48 | 48 | // Without a landing slide the view moves at once, as it always has. | |
| 49 | 49 | func (w *Workspace) retarget(before int) { | |
| @@ -78,7 +78,7 @@ func (w *Workspace) swipeScale() float64 { | |||
| 78 | 78 | // snapSpacing of the usable width merge into the first: a swipe step | |
| 79 | 79 | // always moves the view visibly. | |
| 80 | 80 | func (w *Workspace) snapPoints() []float64 { | |
| 81 | - | if w.Overflow == OverflowCascade { | |
| 81 | + | if w.policy().wraps { | |
| 82 | 82 | return w.cascadePoints() | |
| 83 | 83 | } | |
| 84 | 84 | g := w.gap() | |
| @@ -116,7 +116,7 @@ const snapSpacing = 0.1 | |||
| 116 | 116 | // fully shown at the current view, else the furthest fully shown column | |
| 117 | 117 | // toward the swipe (forward is rightward). | |
| 118 | 118 | func (w *Workspace) snapFocus(view int, forward bool) (focus int) { | |
| 119 | - | if w.Overflow == OverflowCascade { | |
| 119 | + | if w.policy().wraps { | |
| 120 | 120 | return w.cascadeFocus(view) | |
| 121 | 121 | } | |
| 122 | 122 | g := w.gap() | |
| @@ -8,7 +8,7 @@ import ( | |||||
| 8 | 8 | ||||
| 9 | // Slides move what is on screen smoothly: a workspace's columns (view), its | 9 | // Slides move what is on screen smoothly: a workspace's columns (view), its | ||
| 10 | // stash (stashView) and the monitor between workspaces (switchView). All | 10 | // stash (stashView) and the monitor between workspaces (switchView). All | ||
| 11 | - | // are pure presentation over the settled state (ViewX, Focus, stashAt, | 11 | + | // are pure presentation over the settled state (View, Focus, stashAt, |
| 12 | // Active): the layout ends where the state says as soon as a slide stops. | 12 | // Active): the layout ends where the state says as soon as a slide stops. | ||
| 13 | 13 | ||||
| 14 | // slide is a view offset that a swipe sets while the fingers follow it, and | 14 | // slide is a view offset that a swipe sets while the fingers follow it, and | ||
| @@ -43,7 +43,7 @@ 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() | ||
| 44 | } | 44 | } | ||
| 45 | 45 | ||||
| 46 | - | // retarget keeps the view where it is on screen when ViewX moved from | 46 | + | // retarget keeps the view where it is on screen when View moved from |
| 47 | // before during a landing slide: the slide heads for the new view. | 47 | // before during a landing slide: the slide heads for the new view. | ||
| 48 | // Without a landing slide the view moves at once, as it always has. | 48 | // Without a landing slide the view moves at once, as it always has. | ||
| 49 | func (w *Workspace) retarget(before int) { | 49 | func (w *Workspace) retarget(before int) { | ||
| @@ -78,7 +78,7 @@ func (w *Workspace) swipeScale() float64 { | |||||
| 78 | // snapSpacing of the usable width merge into the first: a swipe step | 78 | // snapSpacing of the usable width merge into the first: a swipe step | ||
| 79 | // always moves the view visibly. | 79 | // always moves the view visibly. | ||
| 80 | func (w *Workspace) snapPoints() []float64 { | 80 | func (w *Workspace) snapPoints() []float64 { | ||
| 81 | - | if w.Overflow == OverflowCascade { | 81 | + | if w.policy().wraps { |
| 82 | return w.cascadePoints() | 82 | return w.cascadePoints() | ||
| 83 | } | 83 | } | ||
| 84 | g := w.gap() | 84 | g := w.gap() | ||
| @@ -116,7 +116,7 @@ const snapSpacing = 0.1 | |||||
| 116 | // fully shown at the current view, else the furthest fully shown column | 116 | // fully shown at the current view, else the furthest fully shown column | ||
| 117 | // toward the swipe (forward is rightward). | 117 | // toward the swipe (forward is rightward). | ||
| 118 | func (w *Workspace) snapFocus(view int, forward bool) (focus int) { | 118 | func (w *Workspace) snapFocus(view int, forward bool) (focus int) { | ||
| 119 | - | if w.Overflow == OverflowCascade { | 119 | + | if w.policy().wraps { |
| 120 | return w.cascadeFocus(view) | 120 | return w.cascadeFocus(view) | ||
| 121 | } | 121 | } | ||
| 122 | g := w.gap() | 122 | g := w.gap() | ||
M internal/core/transition.go +6 -6
| @@ -28,10 +28,10 @@ type viewShot struct { | |||
| 28 | 28 | // overview is set when the screen showed its overview: ws is then the | |
| 29 | 29 | // row on screen, the camera fields stay zero, and rects are the cards. | |
| 30 | 30 | overview bool | |
| 31 | - | // viewX is ws's settled view and view where its columns were on screen | |
| 32 | - | // (viewX plus shift). | |
| 33 | - | viewX int | |
| 34 | - | view float64 | |
| 31 | + | // settledView is ws's settled view and view where its columns were on screen | |
| 32 | + | // (settledView plus shift). | |
| 33 | + | settledView int | |
| 34 | + | view float64 | |
| 35 | 35 | // viewV and switchV are the speeds the running view and workspace | |
| 36 | 36 | // springs have at the snapshot's time (units per second, 0 without | |
| 37 | 37 | // one), not at their last frame. An action may stop them | |
| @@ -622,7 +622,7 @@ func (c *Core) snapshot(now time.Time) []viewShot { | |||
| 622 | 622 | s.ws, s.overview = w, m.ov.open | |
| 623 | 623 | if !m.ov.open { | |
| 624 | 624 | // The overview does not scroll or slide: no camera. | |
| 625 | - | s.viewX, s.view = w.View, float64(w.View)+w.view.off | |
| 625 | + | s.settledView, s.view = w.View, float64(w.View)+w.view.off | |
| 626 | 626 | s.switchAxis = w.policy().workspace | |
| 627 | 627 | if m.switchView.busy() { | |
| 628 | 628 | s.switchAxis = m.switchAxis | |
| @@ -733,7 +733,7 @@ func (c *Core) transitionCamera(b *viewShot, before []viewShot, now time.Time) { | |||
| 733 | 733 | w.stashView.motion = c.spring(w.stashSpring(w.stashView.off, b.stashV), now) | |
| 734 | 734 | return | |
| 735 | 735 | } | |
| 736 | - | if w.View == b.viewX || !w.slidable() { | |
| 736 | + | if w.View == b.settledView || !w.slidable() { | |
| 737 | 737 | return | |
| 738 | 738 | } | |
| 739 | 739 | // A running landing slide was retargeted by scroll(): its velocity | |
| @@ -28,10 +28,10 @@ type viewShot struct { | |||||
| 28 | // overview is set when the screen showed its overview: ws is then the | 28 | // overview is set when the screen showed its overview: ws is then the | ||
| 29 | // row on screen, the camera fields stay zero, and rects are the cards. | 29 | // row on screen, the camera fields stay zero, and rects are the cards. | ||
| 30 | overview bool | 30 | overview bool | ||
| 31 | - | // viewX is ws's settled view and view where its columns were on screen | 31 | + | // settledView is ws's settled view and view where its columns were on screen |
| 32 | - | // (viewX plus shift). | 32 | + | // (settledView plus shift). |
| 33 | - | viewX int | 33 | + | settledView int |
| 34 | - | view float64 | 34 | + | view float64 |
| 35 | // viewV and switchV are the speeds the running view and workspace | 35 | // viewV and switchV are the speeds the running view and workspace | ||
| 36 | // springs have at the snapshot's time (units per second, 0 without | 36 | // springs have at the snapshot's time (units per second, 0 without | ||
| 37 | // one), not at their last frame. An action may stop them | 37 | // one), not at their last frame. An action may stop them | ||
| @@ -622,7 +622,7 @@ func (c *Core) snapshot(now time.Time) []viewShot { | |||||
| 622 | s.ws, s.overview = w, m.ov.open | 622 | s.ws, s.overview = w, m.ov.open | ||
| 623 | if !m.ov.open { | 623 | if !m.ov.open { | ||
| 624 | // The overview does not scroll or slide: no camera. | 624 | // The overview does not scroll or slide: no camera. | ||
| 625 | - | s.viewX, s.view = w.View, float64(w.View)+w.view.off | 625 | + | s.settledView, s.view = w.View, float64(w.View)+w.view.off |
| 626 | s.switchAxis = w.policy().workspace | 626 | s.switchAxis = w.policy().workspace | ||
| 627 | if m.switchView.busy() { | 627 | if m.switchView.busy() { | ||
| 628 | s.switchAxis = m.switchAxis | 628 | s.switchAxis = m.switchAxis | ||
| @@ -733,7 +733,7 @@ func (c *Core) transitionCamera(b *viewShot, before []viewShot, now time.Time) { | |||||
| 733 | w.stashView.motion = c.spring(w.stashSpring(w.stashView.off, b.stashV), now) | 733 | w.stashView.motion = c.spring(w.stashSpring(w.stashView.off, b.stashV), now) | ||
| 734 | return | 734 | return | ||
| 735 | } | 735 | } | ||
| 736 | - | if w.View == b.viewX || !w.slidable() { | 736 | + | if w.View == b.settledView || !w.slidable() { |
| 737 | return | 737 | return | ||
| 738 | } | 738 | } | ||
| 739 | // A running landing slide was retargeted by scroll(): its velocity | 739 | // A running landing slide was retargeted by scroll(): its velocity | ||