refactor(core): keep stepWorkspace clamping and pick occupied workspaces in occupiedStep
- commit
- 40be84979d50062b0bc716ea571261d7cd357f88
- author
- brice · · 2026-10-12 14:15 UTC
- parent
- 6a8bd4f
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
5 files · +68 -18
M docs/config.md +1 -1
| @@ -227,7 +227,7 @@ HDR requires DRM HDR connector properties, suitable KMS planes, and Vulkan fp16 | |||
| 227 | 227 | ||
| 228 | 228 | Cascade is the default layout (`layout.overflow = cascade`); it can also be set 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 row of cells (see [Column widths](#column-widths)); auto columns share it equally, so 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. | |
| 229 | 229 | ||
| 230 | - | Left/right focus and moves stay in the band; at its edge, `focus-column-left/right` go to the previous or next occupied workspace, so the opposite key comes back; past the first or last one, to the neighbor monitor on that side; with none there, to the trailing empty workspace; `move-column-left/right` go to the neighbor monitor, or, with none, follow column order into the adjacent band. Up/down visits stacked windows first, then the closest column in the adjacent band, and stops at the first and last band; `move-window-up/down` takes the same path, moving the whole column to the adjacent band by swapping it with that column. In a partial last band that column is the closest one, so a column moved down and back up may land at another place in its band. Column widths are cells within the band (see [Column widths](#column-widths)); window height shares apply too. Maximizing a column hides its same-band siblings until focus moves or maximization ends. | |
| 230 | + | Left/right focus and moves stay in the band; at its edge, `focus-column-left/right` go first to the previous or next occupied workspace; past the first or last one, to the neighbor monitor on that side; with none there, to the trailing empty workspace; `move-column-left/right` go to the neighbor monitor, or, with none, follow column order into the adjacent band. Up/down visits stacked windows first, then the closest column in the adjacent band, and stops at the first and last band; `move-window-up/down` takes the same path, moving the whole column to the adjacent band by swapping it with that column. In a partial last band that column is the closest one, so a column moved down and back up may land at another place in its band. Column widths are cells within the band (see [Column widths](#column-widths)); window height shares apply too. Maximizing a column hides its same-band siblings until focus moves or maximization ends. | |
| 231 | 231 | ||
| 232 | 232 | 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. | |
| 233 | 233 | ||
| @@ -227,7 +227,7 @@ HDR requires DRM HDR connector properties, suitable KMS planes, and Vulkan fp16 | |||||
| 227 | 227 | ||||
| 228 | Cascade is the default layout (`layout.overflow = cascade`); it can also be set 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 row of cells (see [Column widths](#column-widths)); auto columns share it equally, so 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. | 228 | Cascade is the default layout (`layout.overflow = cascade`); it can also be set 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 row of cells (see [Column widths](#column-widths)); auto columns share it equally, so 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. | ||
| 229 | 229 | ||||
| 230 | - | Left/right focus and moves stay in the band; at its edge, `focus-column-left/right` go to the previous or next occupied workspace, so the opposite key comes back; past the first or last one, to the neighbor monitor on that side; with none there, to the trailing empty workspace; `move-column-left/right` go to the neighbor monitor, or, with none, follow column order into the adjacent band. Up/down visits stacked windows first, then the closest column in the adjacent band, and stops at the first and last band; `move-window-up/down` takes the same path, moving the whole column to the adjacent band by swapping it with that column. In a partial last band that column is the closest one, so a column moved down and back up may land at another place in its band. Column widths are cells within the band (see [Column widths](#column-widths)); window height shares apply too. Maximizing a column hides its same-band siblings until focus moves or maximization ends. | 230 | + | Left/right focus and moves stay in the band; at its edge, `focus-column-left/right` go first to the previous or next occupied workspace; past the first or last one, to the neighbor monitor on that side; with none there, to the trailing empty workspace; `move-column-left/right` go to the neighbor monitor, or, with none, follow column order into the adjacent band. Up/down visits stacked windows first, then the closest column in the adjacent band, and stops at the first and last band; `move-window-up/down` takes the same path, moving the whole column to the adjacent band by swapping it with that column. In a partial last band that column is the closest one, so a column moved down and back up may land at another place in its band. Column widths are cells within the band (see [Column widths](#column-widths)); window height shares apply too. Maximizing a column hides its same-band siblings until focus moves or maximization ends. |
| 231 | 231 | ||||
| 232 | 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. | 232 | 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. | ||
| 233 | 233 | ||||
M internal/core/action.go +8 -7
| @@ -301,19 +301,20 @@ func (c *Core) applyAction(a Action) Effect { | |||
| 301 | 301 | } | |
| 302 | 302 | ||
| 303 | 303 | // edgeFocus moves focus-column-left/right (dir -1/1, toward d) past the | |
| 304 | - | // content edge: to the occupied workspace on that side when the workspace | |
| 305 | - | // axis is horizontal, so the opposite key comes back; else to the neighbor | |
| 306 | - | // monitor; else to the trailing empty workspace. It reports whether focus | |
| 307 | - | // moved. | |
| 304 | + | // content edge, in order: to the occupied workspace on that side when the | |
| 305 | + | // workspace axis is horizontal; else to the neighbor monitor; else to the | |
| 306 | + | // trailing empty workspace. It reports whether focus moved. | |
| 308 | 307 | func (c *Core) edgeFocus(dir int, d direction) bool { | |
| 309 | - | if c.cur().mon.stepWorkspace(horizontalAxis, dir, false) { | |
| 308 | + | m := c.cur().mon | |
| 309 | + | if to := m.occupiedStep(horizontalAxis, dir); to >= 0 { | |
| 310 | + | m.Focus(to) | |
| 310 | 311 | return true | |
| 311 | 312 | } | |
| 312 | 313 | if i := c.neighbor(d); i >= 0 { | |
| 313 | 314 | c.focusScreen = i | |
| 314 | 315 | return true | |
| 315 | 316 | } | |
| 316 | - | return c.cur().mon.stepWorkspace(horizontalAxis, dir, true) | |
| 317 | + | return m.stepWorkspace(horizontalAxis, dir) | |
| 317 | 318 | } | |
| 318 | 319 | ||
| 319 | 320 | // Apply runs a bind action on the monitor. | |
| @@ -354,7 +355,7 @@ func (m *Monitor) Apply(a Action) Effect { | |||
| 354 | 355 | dir = -1 | |
| 355 | 356 | } | |
| 356 | 357 | if !m.Current().FocusWindow(dir) { | |
| 357 | - | m.stepWorkspace(verticalAxis, dir, true) | |
| 358 | + | m.stepWorkspace(verticalAxis, dir) | |
| 358 | 359 | } | |
| 359 | 360 | return Effect{} | |
| 360 | 361 | case ActionToggleFullscreen: | |
| @@ -301,19 +301,20 @@ func (c *Core) applyAction(a Action) Effect { | |||||
| 301 | } | 301 | } | ||
| 302 | 302 | ||||
| 303 | // edgeFocus moves focus-column-left/right (dir -1/1, toward d) past the | 303 | // edgeFocus moves focus-column-left/right (dir -1/1, toward d) past the | ||
| 304 | - | // content edge: to the occupied workspace on that side when the workspace | 304 | + | // content edge, in order: to the occupied workspace on that side when the |
| 305 | - | // axis is horizontal, so the opposite key comes back; else to the neighbor | 305 | + | // workspace axis is horizontal; else to the neighbor monitor; else to the |
| 306 | - | // monitor; else to the trailing empty workspace. It reports whether focus | 306 | + | // trailing empty workspace. It reports whether focus moved. |
| 307 | - | // moved. | |||
| 308 | func (c *Core) edgeFocus(dir int, d direction) bool { | 307 | func (c *Core) edgeFocus(dir int, d direction) bool { | ||
| 309 | - | if c.cur().mon.stepWorkspace(horizontalAxis, dir, false) { | 308 | + | m := c.cur().mon |
| 309 | + | if to := m.occupiedStep(horizontalAxis, dir); to >= 0 { | |||
| 310 | + | m.Focus(to) | |||
| 310 | return true | 311 | return true | ||
| 311 | } | 312 | } | ||
| 312 | if i := c.neighbor(d); i >= 0 { | 313 | if i := c.neighbor(d); i >= 0 { | ||
| 313 | c.focusScreen = i | 314 | c.focusScreen = i | ||
| 314 | return true | 315 | return true | ||
| 315 | } | 316 | } | ||
| 316 | - | return c.cur().mon.stepWorkspace(horizontalAxis, dir, true) | 317 | + | return m.stepWorkspace(horizontalAxis, dir) |
| 317 | } | 318 | } | ||
| 318 | 319 | ||||
| 319 | // Apply runs a bind action on the monitor. | 320 | // Apply runs a bind action on the monitor. | ||
| @@ -354,7 +355,7 @@ func (m *Monitor) Apply(a Action) Effect { | |||||
| 354 | dir = -1 | 355 | dir = -1 | ||
| 355 | } | 356 | } | ||
| 356 | if !m.Current().FocusWindow(dir) { | 357 | if !m.Current().FocusWindow(dir) { | ||
| 357 | - | m.stepWorkspace(verticalAxis, dir, true) | 358 | + | m.stepWorkspace(verticalAxis, dir) |
| 358 | } | 359 | } | ||
| 359 | return Effect{} | 360 | return Effect{} | ||
| 360 | case ActionToggleFullscreen: | 361 | case ActionToggleFullscreen: | ||
M internal/core/cascade_moves_test.go +1 -1
| @@ -23,6 +23,7 @@ func cascadeMovesCore(t *testing.T, n, maxCols int, sides bool) (*Core, *Workspa | |||
| 23 | 23 | for id := WindowID(1); id <= WindowID(n); id++ { | |
| 24 | 24 | w.AddWindow(id) | |
| 25 | 25 | } | |
| 26 | + | c.cur().mon.normalize() // the trailing empty workspace, as in a session | |
| 26 | 27 | return c, w | |
| 27 | 28 | } | |
| 28 | 29 | ||
| @@ -107,7 +108,6 @@ func TestCascadeInsideBandStaysOnMonitor(t *testing.T) { | |||
| 107 | 108 | func TestCascadeBandEdgeWithoutMonitorFocusStepsMoveCrosses(t *testing.T) { | |
| 108 | 109 | c, w := cascadeMovesCore(t, 5, 3, false) | |
| 109 | 110 | m := c.cur().mon | |
| 110 | - | m.normalize() // the trailing empty workspace, as in a running session | |
| 111 | 111 | w.FocusID(4) | |
| 112 | 112 | c.applyAction(ActionFocusColumnLeft) | |
| 113 | 113 | wantFocus(t, c, "M", 4) | |
| @@ -23,6 +23,7 @@ func cascadeMovesCore(t *testing.T, n, maxCols int, sides bool) (*Core, *Workspa | |||||
| 23 | for id := WindowID(1); id <= WindowID(n); id++ { | 23 | for id := WindowID(1); id <= WindowID(n); id++ { | ||
| 24 | w.AddWindow(id) | 24 | w.AddWindow(id) | ||
| 25 | } | 25 | } | ||
| 26 | + | c.cur().mon.normalize() // the trailing empty workspace, as in a session | |||
| 26 | return c, w | 27 | return c, w | ||
| 27 | } | 28 | } | ||
| 28 | 29 | ||||
| @@ -107,7 +108,6 @@ func TestCascadeInsideBandStaysOnMonitor(t *testing.T) { | |||||
| 107 | func TestCascadeBandEdgeWithoutMonitorFocusStepsMoveCrosses(t *testing.T) { | 108 | func TestCascadeBandEdgeWithoutMonitorFocusStepsMoveCrosses(t *testing.T) { | ||
| 108 | c, w := cascadeMovesCore(t, 5, 3, false) | 109 | c, w := cascadeMovesCore(t, 5, 3, false) | ||
| 109 | m := c.cur().mon | 110 | m := c.cur().mon | ||
| 110 | - | m.normalize() // the trailing empty workspace, as in a running session | |||
| 111 | w.FocusID(4) | 111 | w.FocusID(4) | ||
| 112 | c.applyAction(ActionFocusColumnLeft) | 112 | c.applyAction(ActionFocusColumnLeft) | ||
| 113 | wantFocus(t, c, "M", 4) | 113 | wantFocus(t, c, "M", 4) | ||
M internal/core/monitor.go +19 -9
| @@ -206,21 +206,31 @@ func (m *Monitor) Focus(i int) { | |||
| 206 | 206 | } | |
| 207 | 207 | ||
| 208 | 208 | // stepWorkspace switches to the previous or next workspace (dir -1/1) when | |
| 209 | - | // axis is the current layout's workspace axis, and reports whether it did. | |
| 210 | - | // Past the first or the trailing workspace there is none to step to; the | |
| 211 | - | // trailing empty workspace counts only with empty set. A hidden named | |
| 209 | + | // axis is the current layout's workspace axis, and reports whether it did; | |
| 210 | + | // Focus clamps past the first or the trailing workspace. A hidden named | |
| 212 | 211 | // workspace is outside the numbered list: it never steps. | |
| 213 | - | func (m *Monitor) stepWorkspace(axis layoutAxis, dir int, empty bool) bool { | |
| 212 | + | func (m *Monitor) stepWorkspace(axis layoutAxis, dir int) bool { | |
| 213 | + | step := m.Current().policy().workspaceStep(axis, dir) | |
| 214 | + | if m.shown != nil || step == 0 { | |
| 215 | + | return false | |
| 216 | + | } | |
| 217 | + | m.Focus(m.Active + step) | |
| 218 | + | return true | |
| 219 | + | } | |
| 220 | + | ||
| 221 | + | // occupiedStep is the numbered workspace dir (-1/1) away along axis when | |
| 222 | + | // it is the layout's workspace axis and that workspace is not the trailing | |
| 223 | + | // empty one, or -1. | |
| 224 | + | func (m *Monitor) occupiedStep(axis layoutAxis, dir int) int { | |
| 214 | 225 | step := m.Current().policy().workspaceStep(axis, dir) | |
| 215 | 226 | to := m.Active + step | |
| 216 | 227 | if m.shown != nil || step == 0 || to < 0 || to >= len(m.Workspaces) { | |
| 217 | - | return false | |
| 228 | + | return -1 | |
| 218 | 229 | } | |
| 219 | - | if w := m.Workspaces[to]; !empty && w.empty() && w.Name == "" { | |
| 220 | - | return false | |
| 230 | + | if w := m.Workspaces[to]; w.empty() && w.Name == "" { | |
| 231 | + | return -1 | |
| 221 | 232 | } | |
| 222 | - | m.Focus(to) | |
| 223 | - | return true | |
| 233 | + | return to | |
| 224 | 234 | } | |
| 225 | 235 | ||
| 226 | 236 | // MoveWorkspace swaps the active numbered workspace with its neighbor up | |
| @@ -206,21 +206,31 @@ func (m *Monitor) Focus(i int) { | |||||
| 206 | } | 206 | } | ||
| 207 | 207 | ||||
| 208 | // stepWorkspace switches to the previous or next workspace (dir -1/1) when | 208 | // stepWorkspace switches to the previous or next workspace (dir -1/1) when | ||
| 209 | - | // axis is the current layout's workspace axis, and reports whether it did. | 209 | + | // axis is the current layout's workspace axis, and reports whether it did; |
| 210 | - | // Past the first or the trailing workspace there is none to step to; the | 210 | + | // Focus clamps past the first or the trailing workspace. A hidden named |
| 211 | - | // trailing empty workspace counts only with empty set. A hidden named | |||
| 212 | // workspace is outside the numbered list: it never steps. | 211 | // workspace is outside the numbered list: it never steps. | ||
| 213 | - | func (m *Monitor) stepWorkspace(axis layoutAxis, dir int, empty bool) bool { | 212 | + | func (m *Monitor) stepWorkspace(axis layoutAxis, dir int) bool { |
| 213 | + | step := m.Current().policy().workspaceStep(axis, dir) | |||
| 214 | + | if m.shown != nil || step == 0 { | |||
| 215 | + | return false | |||
| 216 | + | } | |||
| 217 | + | m.Focus(m.Active + step) | |||
| 218 | + | return true | |||
| 219 | + | } | |||
| 220 | + | ||||
| 221 | + | // occupiedStep is the numbered workspace dir (-1/1) away along axis when | |||
| 222 | + | // it is the layout's workspace axis and that workspace is not the trailing | |||
| 223 | + | // empty one, or -1. | |||
| 224 | + | func (m *Monitor) occupiedStep(axis layoutAxis, dir int) int { | |||
| 214 | step := m.Current().policy().workspaceStep(axis, dir) | 225 | step := m.Current().policy().workspaceStep(axis, dir) | ||
| 215 | to := m.Active + step | 226 | to := m.Active + step | ||
| 216 | if m.shown != nil || step == 0 || to < 0 || to >= len(m.Workspaces) { | 227 | if m.shown != nil || step == 0 || to < 0 || to >= len(m.Workspaces) { | ||
| 217 | - | return false | 228 | + | return -1 |
| 218 | } | 229 | } | ||
| 219 | - | if w := m.Workspaces[to]; !empty && w.empty() && w.Name == "" { | 230 | + | if w := m.Workspaces[to]; w.empty() && w.Name == "" { |
| 220 | - | return false | 231 | + | return -1 |
| 221 | } | 232 | } | ||
| 222 | - | m.Focus(to) | 233 | + | return to |
| 223 | - | return true | |||
| 224 | } | 234 | } | ||
| 225 | 235 | ||||
| 226 | // MoveWorkspace swaps the active numbered workspace with its neighbor up | 236 | // MoveWorkspace swaps the active numbered workspace with its neighbor up | ||
M internal/core/workspace_axis_test.go +39 -0
| @@ -196,6 +196,45 @@ func TestCascadeFullscreenBandEdgeStepsWorkspace(t *testing.T) { | |||
| 196 | 196 | } | |
| 197 | 197 | } | |
| 198 | 198 | ||
| 199 | + | // A covering fullscreen tile follows the same order: the occupied workspace | |
| 200 | + | // before the monitor on that side, which comes back only past the last one. | |
| 201 | + | func TestCascadeFullscreenStepsWorkspaceBeforeMonitor(t *testing.T) { | |
| 202 | + | c, m := axisCore(t, true, OverflowCascade, 3) | |
| 203 | + | w := m.Current() | |
| 204 | + | w.FocusID(23) | |
| 205 | + | w.ToggleFullscreen() | |
| 206 | + | c.applyAction(ActionFocusColumnRight) | |
| 207 | + | wantActive(t, m, 2) | |
| 208 | + | if got := c.cur().name(); got != "M" { | |
| 209 | + | t.Fatalf("focused screen %q, want M", got) | |
| 210 | + | } | |
| 211 | + | if w.fullscreen != 23 { | |
| 212 | + | t.Fatalf("fullscreen %d, want 23", w.fullscreen) | |
| 213 | + | } | |
| 214 | + | last := m.Current() | |
| 215 | + | last.FocusID(35) | |
| 216 | + | last.ToggleFullscreen() | |
| 217 | + | c.applyAction(ActionFocusColumnRight) | |
| 218 | + | wantFocus(t, c, "R", 0) | |
| 219 | + | if last.fullscreen != 35 { | |
| 220 | + | t.Fatalf("fullscreen %d, want 35", last.fullscreen) | |
| 221 | + | } | |
| 222 | + | } | |
| 223 | + | ||
| 224 | + | // A workspace holding only a float is occupied: it comes before the monitor. | |
| 225 | + | func TestCascadeFloatOnlyWorkspaceIsOccupied(t *testing.T) { | |
| 226 | + | c, m := axisCore(t, true, OverflowCascade, 3) | |
| 227 | + | m.Focus(3) | |
| 228 | + | m.AddFloating(9, 100, 100) | |
| 229 | + | m.Focus(2) | |
| 230 | + | m.Current().FocusID(35) | |
| 231 | + | c.applyAction(ActionFocusColumnRight) | |
| 232 | + | wantActive(t, m, 3) | |
| 233 | + | if got := c.cur().name(); got != "M" { | |
| 234 | + | t.Fatalf("focused screen %q, want M", got) | |
| 235 | + | } | |
| 236 | + | } | |
| 237 | + | ||
| 199 | 238 | // AC7: a lone native float, or an empty cascade workspace, is at the edge on | |
| 200 | 239 | // both sides. | |
| 201 | 240 | func TestCascadeLoneFloatAndEmptyWorkspaceStepWorkspace(t *testing.T) { | |
| @@ -196,6 +196,45 @@ func TestCascadeFullscreenBandEdgeStepsWorkspace(t *testing.T) { | |||||
| 196 | } | 196 | } | ||
| 197 | } | 197 | } | ||
| 198 | 198 | ||||
| 199 | + | // A covering fullscreen tile follows the same order: the occupied workspace | |||
| 200 | + | // before the monitor on that side, which comes back only past the last one. | |||
| 201 | + | func TestCascadeFullscreenStepsWorkspaceBeforeMonitor(t *testing.T) { | |||
| 202 | + | c, m := axisCore(t, true, OverflowCascade, 3) | |||
| 203 | + | w := m.Current() | |||
| 204 | + | w.FocusID(23) | |||
| 205 | + | w.ToggleFullscreen() | |||
| 206 | + | c.applyAction(ActionFocusColumnRight) | |||
| 207 | + | wantActive(t, m, 2) | |||
| 208 | + | if got := c.cur().name(); got != "M" { | |||
| 209 | + | t.Fatalf("focused screen %q, want M", got) | |||
| 210 | + | } | |||
| 211 | + | if w.fullscreen != 23 { | |||
| 212 | + | t.Fatalf("fullscreen %d, want 23", w.fullscreen) | |||
| 213 | + | } | |||
| 214 | + | last := m.Current() | |||
| 215 | + | last.FocusID(35) | |||
| 216 | + | last.ToggleFullscreen() | |||
| 217 | + | c.applyAction(ActionFocusColumnRight) | |||
| 218 | + | wantFocus(t, c, "R", 0) | |||
| 219 | + | if last.fullscreen != 35 { | |||
| 220 | + | t.Fatalf("fullscreen %d, want 35", last.fullscreen) | |||
| 221 | + | } | |||
| 222 | + | } | |||
| 223 | + | ||||
| 224 | + | // A workspace holding only a float is occupied: it comes before the monitor. | |||
| 225 | + | func TestCascadeFloatOnlyWorkspaceIsOccupied(t *testing.T) { | |||
| 226 | + | c, m := axisCore(t, true, OverflowCascade, 3) | |||
| 227 | + | m.Focus(3) | |||
| 228 | + | m.AddFloating(9, 100, 100) | |||
| 229 | + | m.Focus(2) | |||
| 230 | + | m.Current().FocusID(35) | |||
| 231 | + | c.applyAction(ActionFocusColumnRight) | |||
| 232 | + | wantActive(t, m, 3) | |||
| 233 | + | if got := c.cur().name(); got != "M" { | |||
| 234 | + | t.Fatalf("focused screen %q, want M", got) | |||
| 235 | + | } | |||
| 236 | + | } | |||
| 237 | + | ||||
| 199 | // AC7: a lone native float, or an empty cascade workspace, is at the edge on | 238 | // AC7: a lone native float, or an empty cascade workspace, is at the edge on | ||
| 200 | // both sides. | 239 | // both sides. | ||
| 201 | func TestCascadeLoneFloatAndEmptyWorkspaceStepWorkspace(t *testing.T) { | 240 | func TestCascadeLoneFloatAndEmptyWorkspaceStepWorkspace(t *testing.T) { | ||