feat(layout): maximize focused column with Cmd+F (#29)
- commit
- 541e2c75b912a1737c08b2e29275a398b16948ac
- author
- bnema · · 2026-09-27 18:32 UTC
- committer
- GitHub · · 2026-09-27 18:32 UTC
- parent
- 77b3aae
- signature
- GPG Unverified
B5690EEEBB952194The key is unknown
8 files · +102 -10
M docs/config.md +2 -0
| @@ -75,6 +75,7 @@ HDR requires DRM HDR connector properties, suitable KMS planes, and Vulkan fp16 | |||
| 75 | 75 | | `spawn-terminal` | Run `terminal` | | |
| 76 | 76 | | `close-window` | Close the focused window | | |
| 77 | 77 | | `toggle-fullscreen` | Fullscreen the focused window | | |
| 78 | + | | `maximize-column` | Toggle full usable width for the focused column, preserving gaps and its saved width; in fixed overflow, other columns are hidden until focus moves or it is toggled off | | |
| 78 | 79 | | `cycle-column-width` | Step through `layout.presets` | | |
| 79 | 80 | | `focus-column-left/right` | Focus the neighbor column; at the edge, the neighbor monitor | | |
| 80 | 81 | | `focus-window-up/down` | Focus in the column; past the edge, the next workspace | | |
| @@ -109,6 +110,7 @@ HDR requires DRM HDR connector properties, suitable KMS planes, and Vulkan fp16 | |||
| 109 | 110 | | `cmd+bracketleft` | `consume-or-expel-window-left` | | |
| 110 | 111 | | `cmd+bracketright` | `consume-or-expel-window-right` | | |
| 111 | 112 | | `cmd+r` | `cycle-column-width` | | |
| 113 | + | | `cmd+f` | `maximize-column` | | |
| 112 | 114 | | `cmd+shift+f` | `toggle-fullscreen` | | |
| 113 | 115 | | `cmd+q` | `close-window` | | |
| 114 | 116 | | `ctrl+alt+backspace` | `quit` | | |
| @@ -75,6 +75,7 @@ HDR requires DRM HDR connector properties, suitable KMS planes, and Vulkan fp16 | |||||
| 75 | | `spawn-terminal` | Run `terminal` | | 75 | | `spawn-terminal` | Run `terminal` | | ||
| 76 | | `close-window` | Close the focused window | | 76 | | `close-window` | Close the focused window | | ||
| 77 | | `toggle-fullscreen` | Fullscreen the focused window | | 77 | | `toggle-fullscreen` | Fullscreen the focused window | | ||
| 78 | + | | `maximize-column` | Toggle full usable width for the focused column, preserving gaps and its saved width; in fixed overflow, other columns are hidden until focus moves or it is toggled off | | |||
| 78 | | `cycle-column-width` | Step through `layout.presets` | | 79 | | `cycle-column-width` | Step through `layout.presets` | | ||
| 79 | | `focus-column-left/right` | Focus the neighbor column; at the edge, the neighbor monitor | | 80 | | `focus-column-left/right` | Focus the neighbor column; at the edge, the neighbor monitor | | ||
| 80 | | `focus-window-up/down` | Focus in the column; past the edge, the next workspace | | 81 | | `focus-window-up/down` | Focus in the column; past the edge, the next workspace | | ||
| @@ -109,6 +110,7 @@ HDR requires DRM HDR connector properties, suitable KMS planes, and Vulkan fp16 | |||||
| 109 | | `cmd+bracketleft` | `consume-or-expel-window-left` | | 110 | | `cmd+bracketleft` | `consume-or-expel-window-left` | | ||
| 110 | | `cmd+bracketright` | `consume-or-expel-window-right` | | 111 | | `cmd+bracketright` | `consume-or-expel-window-right` | | ||
| 111 | | `cmd+r` | `cycle-column-width` | | 112 | | `cmd+r` | `cycle-column-width` | | ||
| 113 | + | | `cmd+f` | `maximize-column` | | |||
| 112 | | `cmd+shift+f` | `toggle-fullscreen` | | 114 | | `cmd+shift+f` | `toggle-fullscreen` | | ||
| 113 | | `cmd+q` | `close-window` | | 115 | | `cmd+q` | `close-window` | | ||
| 114 | | `ctrl+alt+backspace` | `quit` | | 116 | | `ctrl+alt+backspace` | `quit` | | ||
M examples/config +1 -0
| @@ -108,6 +108,7 @@ bind.cmd+shift+l = move-column-right | |||
| 108 | 108 | bind.cmd+bracketleft = consume-or-expel-window-left | |
| 109 | 109 | bind.cmd+bracketright = consume-or-expel-window-right | |
| 110 | 110 | bind.cmd+r = cycle-column-width | |
| 111 | + | bind.cmd+f = maximize-column | |
| 111 | 112 | bind.cmd+shift+f = toggle-fullscreen | |
| 112 | 113 | bind.cmd+q = close-window | |
| 113 | 114 | bind.ctrl+alt+backspace = quit | |
| @@ -108,6 +108,7 @@ bind.cmd+shift+l = move-column-right | |||||
| 108 | bind.cmd+bracketleft = consume-or-expel-window-left | 108 | bind.cmd+bracketleft = consume-or-expel-window-left | ||
| 109 | bind.cmd+bracketright = consume-or-expel-window-right | 109 | bind.cmd+bracketright = consume-or-expel-window-right | ||
| 110 | bind.cmd+r = cycle-column-width | 110 | bind.cmd+r = cycle-column-width | ||
| 111 | + | bind.cmd+f = maximize-column | |||
| 111 | bind.cmd+shift+f = toggle-fullscreen | 112 | bind.cmd+shift+f = toggle-fullscreen | ||
| 112 | bind.cmd+q = close-window | 113 | bind.cmd+q = close-window | ||
| 113 | bind.ctrl+alt+backspace = quit | 114 | bind.ctrl+alt+backspace = quit | ||
M internal/adapters/config/config.go +2 -1
| @@ -30,6 +30,7 @@ var defaultBinds = []struct{ combo, action string }{ | |||
| 30 | 30 | {"cmd+bracketleft", "consume-or-expel-window-left"}, | |
| 31 | 31 | {"cmd+bracketright", "consume-or-expel-window-right"}, | |
| 32 | 32 | {"cmd+r", "cycle-column-width"}, | |
| 33 | + | {"cmd+f", "maximize-column"}, | |
| 33 | 34 | {"cmd+shift+f", "toggle-fullscreen"}, | |
| 34 | 35 | {"cmd+q", "close-window"}, | |
| 35 | 36 | {"ctrl+alt+backspace", "quit"}, | |
| @@ -43,7 +44,7 @@ var defaultBinds = []struct{ combo, action string }{ | |||
| 43 | 44 | {"cmd+ctrl+shift+left", "move-workspace-to-monitor-left"}, | |
| 44 | 45 | {"cmd+ctrl+shift+right", "move-workspace-to-monitor-right"}, | |
| 45 | 46 | } | |
| 46 | - | var actions = map[string]bool{"focus-monitor-left": true, "focus-monitor-right": true, "move-workspace-to-monitor-left": true, "move-workspace-to-monitor-right": true, "scale-up": true, "scale-down": true, "focus-workspace-up": true, "focus-workspace-down": true, "move-column-to-workspace-up": true, "move-column-to-workspace-down": true, "move-window-to-workspace-up": true, "move-window-to-workspace-down": true, "none": true, "consume-or-expel-window-left": true, "consume-or-expel-window-right": true, "spawn-terminal": true, "focus-column-left": true, "focus-column-right": true, "focus-window-up": true, "focus-window-down": true, "move-column-left": true, "move-column-right": true, "cycle-column-width": true, "toggle-fullscreen": true, "close-window": true, "quit": true} | |
| 47 | + | var actions = map[string]bool{"focus-monitor-left": true, "focus-monitor-right": true, "move-workspace-to-monitor-left": true, "move-workspace-to-monitor-right": true, "scale-up": true, "scale-down": true, "focus-workspace-up": true, "focus-workspace-down": true, "move-column-to-workspace-up": true, "move-column-to-workspace-down": true, "move-window-to-workspace-up": true, "move-window-to-workspace-down": true, "none": true, "consume-or-expel-window-left": true, "consume-or-expel-window-right": true, "spawn-terminal": true, "focus-column-left": true, "focus-column-right": true, "focus-window-up": true, "focus-window-down": true, "move-column-left": true, "move-column-right": true, "cycle-column-width": true, "maximize-column": true, "toggle-fullscreen": true, "close-window": true, "quit": true} | |
| 47 | 48 | var components = map[string]bool{"core": true, "wayland": true, "input": true, "drm": true, "seat": true, "render": true, "sync": true, "config": true, "app": true} | |
| 48 | 49 | var color = regexp.MustCompile(`^#[0-9a-fA-F]{6}$`) | |
| 49 | 50 | ||
| @@ -30,6 +30,7 @@ var defaultBinds = []struct{ combo, action string }{ | |||||
| 30 | {"cmd+bracketleft", "consume-or-expel-window-left"}, | 30 | {"cmd+bracketleft", "consume-or-expel-window-left"}, | ||
| 31 | {"cmd+bracketright", "consume-or-expel-window-right"}, | 31 | {"cmd+bracketright", "consume-or-expel-window-right"}, | ||
| 32 | {"cmd+r", "cycle-column-width"}, | 32 | {"cmd+r", "cycle-column-width"}, | ||
| 33 | + | {"cmd+f", "maximize-column"}, | |||
| 33 | {"cmd+shift+f", "toggle-fullscreen"}, | 34 | {"cmd+shift+f", "toggle-fullscreen"}, | ||
| 34 | {"cmd+q", "close-window"}, | 35 | {"cmd+q", "close-window"}, | ||
| 35 | {"ctrl+alt+backspace", "quit"}, | 36 | {"ctrl+alt+backspace", "quit"}, | ||
| @@ -43,7 +44,7 @@ var defaultBinds = []struct{ combo, action string }{ | |||||
| 43 | {"cmd+ctrl+shift+left", "move-workspace-to-monitor-left"}, | 44 | {"cmd+ctrl+shift+left", "move-workspace-to-monitor-left"}, | ||
| 44 | {"cmd+ctrl+shift+right", "move-workspace-to-monitor-right"}, | 45 | {"cmd+ctrl+shift+right", "move-workspace-to-monitor-right"}, | ||
| 45 | } | 46 | } | ||
| 46 | - | var actions = map[string]bool{"focus-monitor-left": true, "focus-monitor-right": true, "move-workspace-to-monitor-left": true, "move-workspace-to-monitor-right": true, "scale-up": true, "scale-down": true, "focus-workspace-up": true, "focus-workspace-down": true, "move-column-to-workspace-up": true, "move-column-to-workspace-down": true, "move-window-to-workspace-up": true, "move-window-to-workspace-down": true, "none": true, "consume-or-expel-window-left": true, "consume-or-expel-window-right": true, "spawn-terminal": true, "focus-column-left": true, "focus-column-right": true, "focus-window-up": true, "focus-window-down": true, "move-column-left": true, "move-column-right": true, "cycle-column-width": true, "toggle-fullscreen": true, "close-window": true, "quit": true} | 47 | + | var actions = map[string]bool{"focus-monitor-left": true, "focus-monitor-right": true, "move-workspace-to-monitor-left": true, "move-workspace-to-monitor-right": true, "scale-up": true, "scale-down": true, "focus-workspace-up": true, "focus-workspace-down": true, "move-column-to-workspace-up": true, "move-column-to-workspace-down": true, "move-window-to-workspace-up": true, "move-window-to-workspace-down": true, "none": true, "consume-or-expel-window-left": true, "consume-or-expel-window-right": true, "spawn-terminal": true, "focus-column-left": true, "focus-column-right": true, "focus-window-up": true, "focus-window-down": true, "move-column-left": true, "move-column-right": true, "cycle-column-width": true, "maximize-column": true, "toggle-fullscreen": true, "close-window": true, "quit": true} |
| 47 | var components = map[string]bool{"core": true, "wayland": true, "input": true, "drm": true, "seat": true, "render": true, "sync": true, "config": true, "app": true} | 48 | var components = map[string]bool{"core": true, "wayland": true, "input": true, "drm": true, "seat": true, "render": true, "sync": true, "config": true, "app": true} | ||
| 48 | var color = regexp.MustCompile(`^#[0-9a-fA-F]{6}$`) | 49 | var color = regexp.MustCompile(`^#[0-9a-fA-F]{6}$`) | ||
| 49 | 50 | ||||
M internal/adapters/config/config_test.go +2 -2
| @@ -22,10 +22,10 @@ func parseString(t *testing.T, s string) (ports.Config, []Warning) { | |||
| 22 | 22 | func TestDefaultsAndLoad(t *testing.T) { | |
| 23 | 23 | t.Setenv("TERMINAL", "") | |
| 24 | 24 | d := Defaults() | |
| 25 | - | if d.Keyboard.RepeatRate != 25 || d.Keyboard.CmdKey != "super" || !d.Render.DirectScanout || len(d.Binds) != 52 || d.Layout.MaxColumns != 2 { | |
| 25 | + | if d.Keyboard.RepeatRate != 25 || d.Keyboard.CmdKey != "super" || !d.Render.DirectScanout || len(d.Binds) != 53 || d.Layout.MaxColumns != 2 { | |
| 26 | 26 | t.Fatalf("defaults: %+v", d) | |
| 27 | 27 | } | |
| 28 | - | if d.Binds["Cmd+Shift+h"] != "move-column-left" || d.Binds["Cmd+j"] != "focus-window-down" || d.Binds["Cmd+Shift+code:2"] != "move-column-to-workspace 1" || d.Focus.FollowMove { | |
| 28 | + | if d.Binds["Cmd+f"] != "maximize-column" || d.Binds["Cmd+Shift+f"] != "toggle-fullscreen" || d.Binds["Cmd+Shift+h"] != "move-column-left" || d.Binds["Cmd+j"] != "focus-window-down" || d.Binds["Cmd+Shift+code:2"] != "move-column-to-workspace 1" || d.Focus.FollowMove { | |
| 29 | 29 | t.Fatal(d.Binds) | |
| 30 | 30 | } | |
| 31 | 31 | if d.Binds["Cmd+Ctrl+space"] != "spawn fuzzel" || d.Binds["Alt+Ctrl+BackSpace"] != "quit" { | |
| @@ -22,10 +22,10 @@ func parseString(t *testing.T, s string) (ports.Config, []Warning) { | |||||
| 22 | func TestDefaultsAndLoad(t *testing.T) { | 22 | func TestDefaultsAndLoad(t *testing.T) { | ||
| 23 | t.Setenv("TERMINAL", "") | 23 | t.Setenv("TERMINAL", "") | ||
| 24 | d := Defaults() | 24 | d := Defaults() | ||
| 25 | - | if d.Keyboard.RepeatRate != 25 || d.Keyboard.CmdKey != "super" || !d.Render.DirectScanout || len(d.Binds) != 52 || d.Layout.MaxColumns != 2 { | 25 | + | if d.Keyboard.RepeatRate != 25 || d.Keyboard.CmdKey != "super" || !d.Render.DirectScanout || len(d.Binds) != 53 || d.Layout.MaxColumns != 2 { |
| 26 | t.Fatalf("defaults: %+v", d) | 26 | t.Fatalf("defaults: %+v", d) | ||
| 27 | } | 27 | } | ||
| 28 | - | if d.Binds["Cmd+Shift+h"] != "move-column-left" || d.Binds["Cmd+j"] != "focus-window-down" || d.Binds["Cmd+Shift+code:2"] != "move-column-to-workspace 1" || d.Focus.FollowMove { | 28 | + | if d.Binds["Cmd+f"] != "maximize-column" || d.Binds["Cmd+Shift+f"] != "toggle-fullscreen" || d.Binds["Cmd+Shift+h"] != "move-column-left" || d.Binds["Cmd+j"] != "focus-window-down" || d.Binds["Cmd+Shift+code:2"] != "move-column-to-workspace 1" || d.Focus.FollowMove { |
| 29 | t.Fatal(d.Binds) | 29 | t.Fatal(d.Binds) | ||
| 30 | } | 30 | } | ||
| 31 | if d.Binds["Cmd+Ctrl+space"] != "spawn fuzzel" || d.Binds["Alt+Ctrl+BackSpace"] != "quit" { | 31 | if d.Binds["Cmd+Ctrl+space"] != "spawn fuzzel" || d.Binds["Alt+Ctrl+BackSpace"] != "quit" { | ||
M internal/core/action.go +3 -0
| @@ -16,6 +16,7 @@ const ( | |||
| 16 | 16 | ActionMoveColumnLeft Action = "move-column-left" | |
| 17 | 17 | ActionMoveColumnRight Action = "move-column-right" | |
| 18 | 18 | ActionCycleColumnWidth Action = "cycle-column-width" | |
| 19 | + | ActionMaximizeColumn Action = "maximize-column" | |
| 19 | 20 | ActionToggleFullscreen Action = "toggle-fullscreen" | |
| 20 | 21 | ActionCloseWindow Action = "close-window" | |
| 21 | 22 | ActionQuit Action = "quit" | |
| @@ -260,6 +261,8 @@ func (w *Workspace) Apply(a Action) Effect { | |||
| 260 | 261 | w.MoveColumn(1) | |
| 261 | 262 | case ActionCycleColumnWidth: | |
| 262 | 263 | w.CycleWidth() | |
| 264 | + | case ActionMaximizeColumn: | |
| 265 | + | w.ToggleFullWidth() | |
| 263 | 266 | case ActionToggleFullscreen: | |
| 264 | 267 | // In place only: binds go through Monitor.Apply, which gives a | |
| 265 | 268 | // fixed-overflow fullscreen its own workspace. | |
| @@ -16,6 +16,7 @@ const ( | |||||
| 16 | ActionMoveColumnLeft Action = "move-column-left" | 16 | ActionMoveColumnLeft Action = "move-column-left" | ||
| 17 | ActionMoveColumnRight Action = "move-column-right" | 17 | ActionMoveColumnRight Action = "move-column-right" | ||
| 18 | ActionCycleColumnWidth Action = "cycle-column-width" | 18 | ActionCycleColumnWidth Action = "cycle-column-width" | ||
| 19 | + | ActionMaximizeColumn Action = "maximize-column" | |||
| 19 | ActionToggleFullscreen Action = "toggle-fullscreen" | 20 | ActionToggleFullscreen Action = "toggle-fullscreen" | ||
| 20 | ActionCloseWindow Action = "close-window" | 21 | ActionCloseWindow Action = "close-window" | ||
| 21 | ActionQuit Action = "quit" | 22 | ActionQuit Action = "quit" | ||
| @@ -260,6 +261,8 @@ func (w *Workspace) Apply(a Action) Effect { | |||||
| 260 | w.MoveColumn(1) | 261 | w.MoveColumn(1) | ||
| 261 | case ActionCycleColumnWidth: | 262 | case ActionCycleColumnWidth: | ||
| 262 | w.CycleWidth() | 263 | w.CycleWidth() | ||
| 264 | + | case ActionMaximizeColumn: | |||
| 265 | + | w.ToggleFullWidth() | |||
| 263 | case ActionToggleFullscreen: | 266 | case ActionToggleFullscreen: | ||
| 264 | // In place only: binds go through Monitor.Apply, which gives a | 267 | // In place only: binds go through Monitor.Apply, which gives a | ||
| 265 | // fixed-overflow fullscreen its own workspace. | 268 | // fixed-overflow fullscreen its own workspace. | ||
M internal/core/core.go +1 -1
| @@ -191,7 +191,7 @@ func (c *Core) apply(cfg ports.Config) error { | |||
| 191 | 191 | continue | |
| 192 | 192 | } | |
| 193 | 193 | switch Action(a) { | |
| 194 | - | case "none", ActionSpawnTerminal, ActionFocusColumnLeft, ActionFocusColumnRight, ActionFocusWindowUp, ActionFocusWindowDown, ActionMoveColumnLeft, ActionMoveColumnRight, ActionCycleColumnWidth, ActionToggleFullscreen, ActionCloseWindow, ActionQuit, ActionFocusWorkspaceUp, ActionFocusWorkspaceDown, ActionMoveColumnToWorkspaceUp, ActionMoveColumnToWorkspaceDown, ActionMoveWindowToWorkspaceUp, ActionMoveWindowToWorkspaceDown, ActionFocusMonitorLeft, ActionFocusMonitorRight, ActionMoveWorkspaceLeft, ActionMoveWorkspaceRight, ActionConsumeOrExpelLeft, ActionConsumeOrExpelRight: | |
| 194 | + | case "none", ActionSpawnTerminal, ActionFocusColumnLeft, ActionFocusColumnRight, ActionFocusWindowUp, ActionFocusWindowDown, ActionMoveColumnLeft, ActionMoveColumnRight, ActionCycleColumnWidth, ActionMaximizeColumn, ActionToggleFullscreen, ActionCloseWindow, ActionQuit, ActionFocusWorkspaceUp, ActionFocusWorkspaceDown, ActionMoveColumnToWorkspaceUp, ActionMoveColumnToWorkspaceDown, ActionMoveWindowToWorkspaceUp, ActionMoveWindowToWorkspaceDown, ActionFocusMonitorLeft, ActionFocusMonitorRight, ActionMoveWorkspaceLeft, ActionMoveWorkspaceRight, ActionConsumeOrExpelLeft, ActionConsumeOrExpelRight: | |
| 195 | 195 | default: | |
| 196 | 196 | return fmt.Errorf("invalid action %q", a) | |
| 197 | 197 | } | |
| @@ -191,7 +191,7 @@ func (c *Core) apply(cfg ports.Config) error { | |||||
| 191 | continue | 191 | continue | ||
| 192 | } | 192 | } | ||
| 193 | switch Action(a) { | 193 | switch Action(a) { | ||
| 194 | - | case "none", ActionSpawnTerminal, ActionFocusColumnLeft, ActionFocusColumnRight, ActionFocusWindowUp, ActionFocusWindowDown, ActionMoveColumnLeft, ActionMoveColumnRight, ActionCycleColumnWidth, ActionToggleFullscreen, ActionCloseWindow, ActionQuit, ActionFocusWorkspaceUp, ActionFocusWorkspaceDown, ActionMoveColumnToWorkspaceUp, ActionMoveColumnToWorkspaceDown, ActionMoveWindowToWorkspaceUp, ActionMoveWindowToWorkspaceDown, ActionFocusMonitorLeft, ActionFocusMonitorRight, ActionMoveWorkspaceLeft, ActionMoveWorkspaceRight, ActionConsumeOrExpelLeft, ActionConsumeOrExpelRight: | 194 | + | case "none", ActionSpawnTerminal, ActionFocusColumnLeft, ActionFocusColumnRight, ActionFocusWindowUp, ActionFocusWindowDown, ActionMoveColumnLeft, ActionMoveColumnRight, ActionCycleColumnWidth, ActionMaximizeColumn, ActionToggleFullscreen, ActionCloseWindow, ActionQuit, ActionFocusWorkspaceUp, ActionFocusWorkspaceDown, ActionMoveColumnToWorkspaceUp, ActionMoveColumnToWorkspaceDown, ActionMoveWindowToWorkspaceUp, ActionMoveWindowToWorkspaceDown, ActionFocusMonitorLeft, ActionFocusMonitorRight, ActionMoveWorkspaceLeft, ActionMoveWorkspaceRight, ActionConsumeOrExpelLeft, ActionConsumeOrExpelRight: |
| 195 | default: | 195 | default: | ||
| 196 | return fmt.Errorf("invalid action %q", a) | 196 | return fmt.Errorf("invalid action %q", a) | ||
| 197 | } | 197 | } | ||
M internal/core/layout.go +39 -6
| @@ -80,9 +80,10 @@ func (v Width) Resolve(usableW, gaps int) int { | |||
| 80 | 80 | // Column.Width is zero (auto) until the user picks a preset: auto columns share | |
| 81 | 81 | // the usable width equally, up to MaxColumns visible at once. | |
| 82 | 82 | type Column struct { | |
| 83 | - | Windows []WindowID | |
| 84 | - | Width Width | |
| 85 | - | Focus int | |
| 83 | + | Windows []WindowID | |
| 84 | + | Width Width | |
| 85 | + | FullWidth bool | |
| 86 | + | Focus int | |
| 86 | 87 | // Slot is the declared column number (workspace.<name>.column.N) of a | |
| 87 | 88 | // slot window; 0 for normal columns. | |
| 88 | 89 | Slot int | |
| @@ -246,6 +247,9 @@ func (w *Workspace) addColumn(col Column) { | |||
| 246 | 247 | } else if len(w.Columns) > 0 { | |
| 247 | 248 | at = w.Focus + 1 | |
| 248 | 249 | } | |
| 250 | + | if w.Overflow == OverflowFixed && len(w.Columns) > 0 { | |
| 251 | + | w.Columns[w.Focus].FullWidth = false | |
| 252 | + | } | |
| 249 | 253 | w.Columns = append(w.Columns, Column{}) | |
| 250 | 254 | copy(w.Columns[at+1:], w.Columns[at:]) | |
| 251 | 255 | w.Columns[at] = col | |
| @@ -360,6 +364,9 @@ func (w *Workspace) FocusID(id WindowID) bool { | |||
| 360 | 364 | for i := range w.Columns { | |
| 361 | 365 | for j, v := range w.Columns[i].Windows { | |
| 362 | 366 | if v == id { | |
| 367 | + | if w.Overflow == OverflowFixed && w.Focus != i && w.Focus < len(w.Columns) { | |
| 368 | + | w.Columns[w.Focus].FullWidth = false | |
| 369 | + | } | |
| 363 | 370 | w.Focus = i | |
| 364 | 371 | w.Columns[i].Focus = j | |
| 365 | 372 | w.scroll() | |
| @@ -376,6 +383,9 @@ func (w *Workspace) FocusColumn(dir int) { | |||
| 376 | 383 | return | |
| 377 | 384 | } | |
| 378 | 385 | if len(w.Columns) > 0 && (dir == -1 || dir == 1) && w.Focus+dir >= 0 && w.Focus+dir < len(w.Columns) { | |
| 386 | + | if w.Overflow == OverflowFixed { | |
| 387 | + | w.Columns[w.Focus].FullWidth = false | |
| 388 | + | } | |
| 379 | 389 | w.Focus += dir | |
| 380 | 390 | w.scroll() | |
| 381 | 391 | } | |
| @@ -433,6 +443,9 @@ func (w *Workspace) takeColumn() (Column, bool) { | |||
| 433 | 443 | // insertColumn adds a column at index at (clamped) and focuses it. | |
| 434 | 444 | func (w *Workspace) insertColumn(at int, col Column) { | |
| 435 | 445 | at = min(max(at, 0), len(w.Columns)) | |
| 446 | + | if w.Overflow == OverflowFixed && len(w.Columns) > 0 { | |
| 447 | + | w.Columns[w.Focus].FullWidth = false | |
| 448 | + | } | |
| 436 | 449 | w.Columns = slices.Insert(w.Columns, at, col) | |
| 437 | 450 | w.Focus = at | |
| 438 | 451 | w.scroll() | |
| @@ -455,8 +468,20 @@ func (w *Workspace) CycleWidth() { | |||
| 455 | 468 | } | |
| 456 | 469 | } | |
| 457 | 470 | c.Width = next | |
| 471 | + | c.FullWidth = false | |
| 458 | 472 | w.scroll() | |
| 459 | 473 | } | |
| 474 | + | ||
| 475 | + | // ToggleFullWidth expands the focused tiled column without changing its saved width. | |
| 476 | + | func (w *Workspace) ToggleFullWidth() { | |
| 477 | + | if len(w.Columns) == 0 || w.floatFocus { | |
| 478 | + | return | |
| 479 | + | } | |
| 480 | + | c := &w.Columns[w.Focus] | |
| 481 | + | c.FullWidth = !c.FullWidth | |
| 482 | + | w.scroll() | |
| 483 | + | } | |
| 484 | + | ||
| 460 | 485 | func (w *Workspace) ToggleFullscreen() { | |
| 461 | 486 | id, ok := w.Focused() | |
| 462 | 487 | if !ok { | |
| @@ -551,6 +576,9 @@ func (w *Workspace) fullscreenColumn(i int) bool { | |||
| 551 | 576 | return false | |
| 552 | 577 | } | |
| 553 | 578 | func (w *Workspace) columnWidth(i int) int { | |
| 579 | + | if w.Columns[i].FullWidth { | |
| 580 | + | return max(w.Usable.W-2*w.gap(), 0) | |
| 581 | + | } | |
| 554 | 582 | if w.fullscreenColumn(i) { | |
| 555 | 583 | return w.Output.W | |
| 556 | 584 | } | |
| @@ -604,6 +632,10 @@ func (w *Workspace) columnRects() []Rect { | |||
| 604 | 632 | for i := range w.Columns { | |
| 605 | 633 | rects[i] = Rect{X: w.columnX(i) - w.ViewX, Y: y, W: w.columnWidth(i), H: h} | |
| 606 | 634 | } | |
| 635 | + | if w.Overflow == OverflowFixed && len(w.Columns) > 0 && w.Columns[w.Focus].FullWidth { | |
| 636 | + | rects[w.Focus].X = w.Usable.X + g | |
| 637 | + | return rects | |
| 638 | + | } | |
| 607 | 639 | k := max(w.MaxColumns, 1) | |
| 608 | 640 | if w.Overflow != OverflowFixed || len(w.Columns) <= k { | |
| 609 | 641 | return rects | |
| @@ -656,9 +688,10 @@ func (w *Workspace) Layout() []Placement { | |||
| 656 | 688 | h = min(h, bottom-y) | |
| 657 | 689 | r := Rect{X: col.X, Y: y, W: col.W, H: h} | |
| 658 | 690 | full := w.fullscreen == id && id != 0 | |
| 659 | - | // Fixed overflow keeps every column on screen: hide them all | |
| 660 | - | // behind a fullscreen window. | |
| 661 | - | hidden := (fullColumn || w.Overflow == OverflowFixed && w.fullscreen != 0) && !full | |
| 691 | + | // Fixed overflow cannot scroll to other columns while one fills | |
| 692 | + | // the view. Keep them in place, but out of the scene. | |
| 693 | + | maximized := w.Overflow == OverflowFixed && w.Columns[w.Focus].FullWidth && i != w.Focus | |
| 694 | + | hidden := ((fullColumn || w.Overflow == OverflowFixed && w.fullscreen != 0) && !full) || maximized | |
| 662 | 695 | if full { | |
| 663 | 696 | // Scroll mode aligns the view on the column; fixed never scrolls. | |
| 664 | 697 | r = Rect{X: col.X, Y: 0, W: w.Output.W, H: w.Output.H} | |
| @@ -80,9 +80,10 @@ func (v Width) Resolve(usableW, gaps int) int { | |||||
| 80 | // Column.Width is zero (auto) until the user picks a preset: auto columns share | 80 | // Column.Width is zero (auto) until the user picks a preset: auto columns share | ||
| 81 | // the usable width equally, up to MaxColumns visible at once. | 81 | // the usable width equally, up to MaxColumns visible at once. | ||
| 82 | type Column struct { | 82 | type Column struct { | ||
| 83 | - | Windows []WindowID | 83 | + | Windows []WindowID |
| 84 | - | Width Width | 84 | + | Width Width |
| 85 | - | Focus int | 85 | + | FullWidth bool |
| 86 | + | Focus int | |||
| 86 | // Slot is the declared column number (workspace.<name>.column.N) of a | 87 | // Slot is the declared column number (workspace.<name>.column.N) of a | ||
| 87 | // slot window; 0 for normal columns. | 88 | // slot window; 0 for normal columns. | ||
| 88 | Slot int | 89 | Slot int | ||
| @@ -246,6 +247,9 @@ func (w *Workspace) addColumn(col Column) { | |||||
| 246 | } else if len(w.Columns) > 0 { | 247 | } else if len(w.Columns) > 0 { | ||
| 247 | at = w.Focus + 1 | 248 | at = w.Focus + 1 | ||
| 248 | } | 249 | } | ||
| 250 | + | if w.Overflow == OverflowFixed && len(w.Columns) > 0 { | |||
| 251 | + | w.Columns[w.Focus].FullWidth = false | |||
| 252 | + | } | |||
| 249 | w.Columns = append(w.Columns, Column{}) | 253 | w.Columns = append(w.Columns, Column{}) | ||
| 250 | copy(w.Columns[at+1:], w.Columns[at:]) | 254 | copy(w.Columns[at+1:], w.Columns[at:]) | ||
| 251 | w.Columns[at] = col | 255 | w.Columns[at] = col | ||
| @@ -360,6 +364,9 @@ func (w *Workspace) FocusID(id WindowID) bool { | |||||
| 360 | for i := range w.Columns { | 364 | for i := range w.Columns { | ||
| 361 | for j, v := range w.Columns[i].Windows { | 365 | for j, v := range w.Columns[i].Windows { | ||
| 362 | if v == id { | 366 | if v == id { | ||
| 367 | + | if w.Overflow == OverflowFixed && w.Focus != i && w.Focus < len(w.Columns) { | |||
| 368 | + | w.Columns[w.Focus].FullWidth = false | |||
| 369 | + | } | |||
| 363 | w.Focus = i | 370 | w.Focus = i | ||
| 364 | w.Columns[i].Focus = j | 371 | w.Columns[i].Focus = j | ||
| 365 | w.scroll() | 372 | w.scroll() | ||
| @@ -376,6 +383,9 @@ func (w *Workspace) FocusColumn(dir int) { | |||||
| 376 | return | 383 | return | ||
| 377 | } | 384 | } | ||
| 378 | if len(w.Columns) > 0 && (dir == -1 || dir == 1) && w.Focus+dir >= 0 && w.Focus+dir < len(w.Columns) { | 385 | if len(w.Columns) > 0 && (dir == -1 || dir == 1) && w.Focus+dir >= 0 && w.Focus+dir < len(w.Columns) { | ||
| 386 | + | if w.Overflow == OverflowFixed { | |||
| 387 | + | w.Columns[w.Focus].FullWidth = false | |||
| 388 | + | } | |||
| 379 | w.Focus += dir | 389 | w.Focus += dir | ||
| 380 | w.scroll() | 390 | w.scroll() | ||
| 381 | } | 391 | } | ||
| @@ -433,6 +443,9 @@ func (w *Workspace) takeColumn() (Column, bool) { | |||||
| 433 | // insertColumn adds a column at index at (clamped) and focuses it. | 443 | // insertColumn adds a column at index at (clamped) and focuses it. | ||
| 434 | func (w *Workspace) insertColumn(at int, col Column) { | 444 | func (w *Workspace) insertColumn(at int, col Column) { | ||
| 435 | at = min(max(at, 0), len(w.Columns)) | 445 | at = min(max(at, 0), len(w.Columns)) | ||
| 446 | + | if w.Overflow == OverflowFixed && len(w.Columns) > 0 { | |||
| 447 | + | w.Columns[w.Focus].FullWidth = false | |||
| 448 | + | } | |||
| 436 | w.Columns = slices.Insert(w.Columns, at, col) | 449 | w.Columns = slices.Insert(w.Columns, at, col) | ||
| 437 | w.Focus = at | 450 | w.Focus = at | ||
| 438 | w.scroll() | 451 | w.scroll() | ||
| @@ -455,8 +468,20 @@ func (w *Workspace) CycleWidth() { | |||||
| 455 | } | 468 | } | ||
| 456 | } | 469 | } | ||
| 457 | c.Width = next | 470 | c.Width = next | ||
| 471 | + | c.FullWidth = false | |||
| 458 | w.scroll() | 472 | w.scroll() | ||
| 459 | } | 473 | } | ||
| 474 | + | ||||
| 475 | + | // ToggleFullWidth expands the focused tiled column without changing its saved width. | |||
| 476 | + | func (w *Workspace) ToggleFullWidth() { | |||
| 477 | + | if len(w.Columns) == 0 || w.floatFocus { | |||
| 478 | + | return | |||
| 479 | + | } | |||
| 480 | + | c := &w.Columns[w.Focus] | |||
| 481 | + | c.FullWidth = !c.FullWidth | |||
| 482 | + | w.scroll() | |||
| 483 | + | } | |||
| 484 | + | ||||
| 460 | func (w *Workspace) ToggleFullscreen() { | 485 | func (w *Workspace) ToggleFullscreen() { | ||
| 461 | id, ok := w.Focused() | 486 | id, ok := w.Focused() | ||
| 462 | if !ok { | 487 | if !ok { | ||
| @@ -551,6 +576,9 @@ func (w *Workspace) fullscreenColumn(i int) bool { | |||||
| 551 | return false | 576 | return false | ||
| 552 | } | 577 | } | ||
| 553 | func (w *Workspace) columnWidth(i int) int { | 578 | func (w *Workspace) columnWidth(i int) int { | ||
| 579 | + | if w.Columns[i].FullWidth { | |||
| 580 | + | return max(w.Usable.W-2*w.gap(), 0) | |||
| 581 | + | } | |||
| 554 | if w.fullscreenColumn(i) { | 582 | if w.fullscreenColumn(i) { | ||
| 555 | return w.Output.W | 583 | return w.Output.W | ||
| 556 | } | 584 | } | ||
| @@ -604,6 +632,10 @@ func (w *Workspace) columnRects() []Rect { | |||||
| 604 | for i := range w.Columns { | 632 | for i := range w.Columns { | ||
| 605 | rects[i] = Rect{X: w.columnX(i) - w.ViewX, Y: y, W: w.columnWidth(i), H: h} | 633 | rects[i] = Rect{X: w.columnX(i) - w.ViewX, Y: y, W: w.columnWidth(i), H: h} | ||
| 606 | } | 634 | } | ||
| 635 | + | if w.Overflow == OverflowFixed && len(w.Columns) > 0 && w.Columns[w.Focus].FullWidth { | |||
| 636 | + | rects[w.Focus].X = w.Usable.X + g | |||
| 637 | + | return rects | |||
| 638 | + | } | |||
| 607 | k := max(w.MaxColumns, 1) | 639 | k := max(w.MaxColumns, 1) | ||
| 608 | if w.Overflow != OverflowFixed || len(w.Columns) <= k { | 640 | if w.Overflow != OverflowFixed || len(w.Columns) <= k { | ||
| 609 | return rects | 641 | return rects | ||
| @@ -656,9 +688,10 @@ func (w *Workspace) Layout() []Placement { | |||||
| 656 | h = min(h, bottom-y) | 688 | h = min(h, bottom-y) | ||
| 657 | r := Rect{X: col.X, Y: y, W: col.W, H: h} | 689 | r := Rect{X: col.X, Y: y, W: col.W, H: h} | ||
| 658 | full := w.fullscreen == id && id != 0 | 690 | full := w.fullscreen == id && id != 0 | ||
| 659 | - | // Fixed overflow keeps every column on screen: hide them all | 691 | + | // Fixed overflow cannot scroll to other columns while one fills |
| 660 | - | // behind a fullscreen window. | 692 | + | // the view. Keep them in place, but out of the scene. |
| 661 | - | hidden := (fullColumn || w.Overflow == OverflowFixed && w.fullscreen != 0) && !full | 693 | + | maximized := w.Overflow == OverflowFixed && w.Columns[w.Focus].FullWidth && i != w.Focus |
| 694 | + | hidden := ((fullColumn || w.Overflow == OverflowFixed && w.fullscreen != 0) && !full) || maximized | |||
| 662 | if full { | 695 | if full { | ||
| 663 | // Scroll mode aligns the view on the column; fixed never scrolls. | 696 | // Scroll mode aligns the view on the column; fixed never scrolls. | ||
| 664 | r = Rect{X: col.X, Y: 0, W: w.Output.W, H: w.Output.H} | 697 | r = Rect{X: col.X, Y: 0, W: w.Output.W, H: w.Output.H} | ||
M internal/core/layout_test.go +52 -0
| @@ -107,6 +107,58 @@ func TestStackAndLayout(t *testing.T) { | |||
| 107 | 107 | t.Fatal(w.Usable) | |
| 108 | 108 | } | |
| 109 | 109 | } | |
| 110 | + | func TestMaximizeColumnScroll(t *testing.T) { | |
| 111 | + | w := workspace() | |
| 112 | + | for id := WindowID(1); id <= 3; id++ { | |
| 113 | + | w.AddWindow(id) | |
| 114 | + | } | |
| 115 | + | w.Columns[1].Width = Width{Pixels: 30} | |
| 116 | + | w.FocusID(2) | |
| 117 | + | w.Apply(ActionMaximizeColumn) | |
| 118 | + | if !w.Columns[1].FullWidth || w.Columns[1].Width.Pixels != 30 || w.Layout()[1].Rect != (Rect{X: 5, Y: 5, W: 90, H: 71}) { | |
| 119 | + | t.Fatal(w.Columns, w.Layout()) | |
| 120 | + | } | |
| 121 | + | if w.Layout()[0].Rect.X >= 0 || w.Layout()[2].Rect.X < 100 || w.Layout()[1].Fullscreen { | |
| 122 | + | t.Fatal(w.Layout()) | |
| 123 | + | } | |
| 124 | + | w.Apply(ActionMaximizeColumn) | |
| 125 | + | if w.Columns[1].FullWidth || w.Columns[1].Width.Pixels != 30 || w.Layout()[1].Rect.W != 30 { | |
| 126 | + | t.Fatal(w.Columns, w.Layout()) | |
| 127 | + | } | |
| 128 | + | } | |
| 129 | + | ||
| 130 | + | func TestMaximizeColumnFixed(t *testing.T) { | |
| 131 | + | w := workspace() | |
| 132 | + | w.Overflow = OverflowFixed | |
| 133 | + | for id := WindowID(1); id <= 3; id++ { | |
| 134 | + | w.AddWindow(id) | |
| 135 | + | } | |
| 136 | + | w.FocusID(2) | |
| 137 | + | w.Apply(ActionMaximizeColumn) | |
| 138 | + | p := w.Layout() | |
| 139 | + | if p[1].Rect != (Rect{X: 5, Y: 5, W: 90, H: 71}) || p[1].Fullscreen || !p[0].Hidden || !p[2].Hidden { | |
| 140 | + | t.Fatal(p) | |
| 141 | + | } | |
| 142 | + | w.FocusColumn(1) | |
| 143 | + | p = w.Layout() | |
| 144 | + | if w.Columns[1].FullWidth || p[2].Hidden || p[0].Hidden || p[1].Hidden { | |
| 145 | + | t.Fatal(w.Columns, p) | |
| 146 | + | } | |
| 147 | + | } | |
| 148 | + | ||
| 149 | + | func TestMaximizeColumnFixedAddWindow(t *testing.T) { | |
| 150 | + | w := workspace() | |
| 151 | + | w.Overflow = OverflowFixed | |
| 152 | + | w.AddWindow(1) | |
| 153 | + | w.AddWindow(2) | |
| 154 | + | w.ToggleFullWidth() | |
| 155 | + | w.AddWindow(3) | |
| 156 | + | p := w.Layout() | |
| 157 | + | if w.Columns[1].FullWidth || p[0].Hidden || p[1].Hidden || p[2].Hidden || p[2].Rect.W == 0 { | |
| 158 | + | t.Fatal(w.Columns, p) | |
| 159 | + | } | |
| 160 | + | } | |
| 161 | + | ||
| 110 | 162 | func TestPresetsAndActions(t *testing.T) { | |
| 111 | 163 | w := workspace() | |
| 112 | 164 | w.AddWindow(1) | |
| @@ -107,6 +107,58 @@ func TestStackAndLayout(t *testing.T) { | |||||
| 107 | t.Fatal(w.Usable) | 107 | t.Fatal(w.Usable) | ||
| 108 | } | 108 | } | ||
| 109 | } | 109 | } | ||
| 110 | + | func TestMaximizeColumnScroll(t *testing.T) { | |||
| 111 | + | w := workspace() | |||
| 112 | + | for id := WindowID(1); id <= 3; id++ { | |||
| 113 | + | w.AddWindow(id) | |||
| 114 | + | } | |||
| 115 | + | w.Columns[1].Width = Width{Pixels: 30} | |||
| 116 | + | w.FocusID(2) | |||
| 117 | + | w.Apply(ActionMaximizeColumn) | |||
| 118 | + | if !w.Columns[1].FullWidth || w.Columns[1].Width.Pixels != 30 || w.Layout()[1].Rect != (Rect{X: 5, Y: 5, W: 90, H: 71}) { | |||
| 119 | + | t.Fatal(w.Columns, w.Layout()) | |||
| 120 | + | } | |||
| 121 | + | if w.Layout()[0].Rect.X >= 0 || w.Layout()[2].Rect.X < 100 || w.Layout()[1].Fullscreen { | |||
| 122 | + | t.Fatal(w.Layout()) | |||
| 123 | + | } | |||
| 124 | + | w.Apply(ActionMaximizeColumn) | |||
| 125 | + | if w.Columns[1].FullWidth || w.Columns[1].Width.Pixels != 30 || w.Layout()[1].Rect.W != 30 { | |||
| 126 | + | t.Fatal(w.Columns, w.Layout()) | |||
| 127 | + | } | |||
| 128 | + | } | |||
| 129 | + | ||||
| 130 | + | func TestMaximizeColumnFixed(t *testing.T) { | |||
| 131 | + | w := workspace() | |||
| 132 | + | w.Overflow = OverflowFixed | |||
| 133 | + | for id := WindowID(1); id <= 3; id++ { | |||
| 134 | + | w.AddWindow(id) | |||
| 135 | + | } | |||
| 136 | + | w.FocusID(2) | |||
| 137 | + | w.Apply(ActionMaximizeColumn) | |||
| 138 | + | p := w.Layout() | |||
| 139 | + | if p[1].Rect != (Rect{X: 5, Y: 5, W: 90, H: 71}) || p[1].Fullscreen || !p[0].Hidden || !p[2].Hidden { | |||
| 140 | + | t.Fatal(p) | |||
| 141 | + | } | |||
| 142 | + | w.FocusColumn(1) | |||
| 143 | + | p = w.Layout() | |||
| 144 | + | if w.Columns[1].FullWidth || p[2].Hidden || p[0].Hidden || p[1].Hidden { | |||
| 145 | + | t.Fatal(w.Columns, p) | |||
| 146 | + | } | |||
| 147 | + | } | |||
| 148 | + | ||||
| 149 | + | func TestMaximizeColumnFixedAddWindow(t *testing.T) { | |||
| 150 | + | w := workspace() | |||
| 151 | + | w.Overflow = OverflowFixed | |||
| 152 | + | w.AddWindow(1) | |||
| 153 | + | w.AddWindow(2) | |||
| 154 | + | w.ToggleFullWidth() | |||
| 155 | + | w.AddWindow(3) | |||
| 156 | + | p := w.Layout() | |||
| 157 | + | if w.Columns[1].FullWidth || p[0].Hidden || p[1].Hidden || p[2].Hidden || p[2].Rect.W == 0 { | |||
| 158 | + | t.Fatal(w.Columns, p) | |||
| 159 | + | } | |||
| 160 | + | } | |||
| 161 | + | ||||
| 110 | func TestPresetsAndActions(t *testing.T) { | 162 | func TestPresetsAndActions(t *testing.T) { | ||
| 111 | w := workspace() | 163 | w := workspace() | ||
| 112 | w.AddWindow(1) | 164 | w.AddWindow(1) | ||