refactor(core): extract output placement and config lookup
- commit
- 67280b43a7940b62c3057ba3b721b14bd2d18e26
- author
- brice · · 2026-10-03 13:55 UTC
- parent
- 819389e
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
5 files · +189 -68
M internal/adapters/config/config.go +37 -1
| @@ -64,7 +64,43 @@ var defaultBinds = []struct{ combo, action string }{ | |||
| 64 | 64 | {"cmd+ctrl+shift+down", "move-workspace-down"}, | |
| 65 | 65 | {"cmd+shift+space", "toggle-floating"}, | |
| 66 | 66 | } | |
| 67 | - | 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, "toggle-window-stash": true, "toggle-stash-visible": true, "toggle-overview": true, "close-window": true, "quit": true, "move-window-up": true, "move-window-down": true, "move-workspace-up": true, "move-workspace-down": true, "toggle-floating": true} | |
| 67 | + | var actions = map[string]bool{ | |
| 68 | + | "focus-monitor-left": true, | |
| 69 | + | "focus-monitor-right": true, | |
| 70 | + | "move-workspace-to-monitor-left": true, | |
| 71 | + | "move-workspace-to-monitor-right": true, | |
| 72 | + | "scale-up": true, | |
| 73 | + | "scale-down": true, | |
| 74 | + | "focus-workspace-up": true, | |
| 75 | + | "focus-workspace-down": true, | |
| 76 | + | "move-column-to-workspace-up": true, | |
| 77 | + | "move-column-to-workspace-down": true, | |
| 78 | + | "move-window-to-workspace-up": true, | |
| 79 | + | "move-window-to-workspace-down": true, | |
| 80 | + | "none": true, | |
| 81 | + | "consume-or-expel-window-left": true, | |
| 82 | + | "consume-or-expel-window-right": true, | |
| 83 | + | "spawn-terminal": true, | |
| 84 | + | "focus-column-left": true, | |
| 85 | + | "focus-column-right": true, | |
| 86 | + | "focus-window-up": true, | |
| 87 | + | "focus-window-down": true, | |
| 88 | + | "move-column-left": true, | |
| 89 | + | "move-column-right": true, | |
| 90 | + | "cycle-column-width": true, | |
| 91 | + | "maximize-column": true, | |
| 92 | + | "toggle-fullscreen": true, | |
| 93 | + | "toggle-window-stash": true, | |
| 94 | + | "toggle-stash-visible": true, | |
| 95 | + | "toggle-overview": true, | |
| 96 | + | "close-window": true, | |
| 97 | + | "quit": true, | |
| 98 | + | "move-window-up": true, | |
| 99 | + | "move-window-down": true, | |
| 100 | + | "move-workspace-up": true, | |
| 101 | + | "move-workspace-down": true, | |
| 102 | + | "toggle-floating": true, | |
| 103 | + | } | |
| 68 | 104 | var components = map[string]bool{"core": true, "wayland": true, "input": true, "drm": true, "seat": true, "render": true, "sync": true, "config": true, "app": true} | |
| 69 | 105 | var color = regexp.MustCompile(`^#[0-9a-fA-F]{6}$`) | |
| 70 | 106 | ||
| @@ -64,7 +64,43 @@ var defaultBinds = []struct{ combo, action string }{ | |||||
| 64 | {"cmd+ctrl+shift+down", "move-workspace-down"}, | 64 | {"cmd+ctrl+shift+down", "move-workspace-down"}, | ||
| 65 | {"cmd+shift+space", "toggle-floating"}, | 65 | {"cmd+shift+space", "toggle-floating"}, | ||
| 66 | } | 66 | } | ||
| 67 | - | 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, "toggle-window-stash": true, "toggle-stash-visible": true, "toggle-overview": true, "close-window": true, "quit": true, "move-window-up": true, "move-window-down": true, "move-workspace-up": true, "move-workspace-down": true, "toggle-floating": true} | 67 | + | var actions = map[string]bool{ |
| 68 | + | "focus-monitor-left": true, | |||
| 69 | + | "focus-monitor-right": true, | |||
| 70 | + | "move-workspace-to-monitor-left": true, | |||
| 71 | + | "move-workspace-to-monitor-right": true, | |||
| 72 | + | "scale-up": true, | |||
| 73 | + | "scale-down": true, | |||
| 74 | + | "focus-workspace-up": true, | |||
| 75 | + | "focus-workspace-down": true, | |||
| 76 | + | "move-column-to-workspace-up": true, | |||
| 77 | + | "move-column-to-workspace-down": true, | |||
| 78 | + | "move-window-to-workspace-up": true, | |||
| 79 | + | "move-window-to-workspace-down": true, | |||
| 80 | + | "none": true, | |||
| 81 | + | "consume-or-expel-window-left": true, | |||
| 82 | + | "consume-or-expel-window-right": true, | |||
| 83 | + | "spawn-terminal": true, | |||
| 84 | + | "focus-column-left": true, | |||
| 85 | + | "focus-column-right": true, | |||
| 86 | + | "focus-window-up": true, | |||
| 87 | + | "focus-window-down": true, | |||
| 88 | + | "move-column-left": true, | |||
| 89 | + | "move-column-right": true, | |||
| 90 | + | "cycle-column-width": true, | |||
| 91 | + | "maximize-column": true, | |||
| 92 | + | "toggle-fullscreen": true, | |||
| 93 | + | "toggle-window-stash": true, | |||
| 94 | + | "toggle-stash-visible": true, | |||
| 95 | + | "toggle-overview": true, | |||
| 96 | + | "close-window": true, | |||
| 97 | + | "quit": true, | |||
| 98 | + | "move-window-up": true, | |||
| 99 | + | "move-window-down": true, | |||
| 100 | + | "move-workspace-up": true, | |||
| 101 | + | "move-workspace-down": true, | |||
| 102 | + | "toggle-floating": true, | |||
| 103 | + | } | |||
| 68 | var components = map[string]bool{"core": true, "wayland": true, "input": true, "drm": true, "seat": true, "render": true, "sync": true, "config": true, "app": true} | 104 | var components = map[string]bool{"core": true, "wayland": true, "input": true, "drm": true, "seat": true, "render": true, "sync": true, "config": true, "app": true} | ||
| 69 | var color = regexp.MustCompile(`^#[0-9a-fA-F]{6}$`) | 105 | var color = regexp.MustCompile(`^#[0-9a-fA-F]{6}$`) | ||
| 70 | 106 | ||||
M internal/core/action.go +22 -22
| @@ -173,35 +173,35 @@ func (c *Core) applyAction(a Action) Effect { | |||
| 173 | 173 | if name, ok := NamedArg(a); ok && c.bringNamed(name) { | |
| 174 | 174 | return Effect{} | |
| 175 | 175 | } | |
| 176 | - | dir := 0 | |
| 177 | - | switch a { | |
| 178 | - | case ActionFocusMonitorLeft, ActionMoveWorkspaceLeft: | |
| 179 | - | dir = -1 | |
| 180 | - | case ActionFocusMonitorRight, ActionMoveWorkspaceRight: | |
| 181 | - | dir = 1 | |
| 182 | - | } | |
| 183 | 176 | switch a { | |
| 184 | 177 | case ActionFocusMonitorLeft, ActionFocusMonitorRight: | |
| 185 | - | if i := c.neighbor(dir); i >= 0 { | |
| 178 | + | d := dirLeft | |
| 179 | + | if a == ActionFocusMonitorRight { | |
| 180 | + | d = dirRight | |
| 181 | + | } | |
| 182 | + | if i := c.neighbor(d); i >= 0 { | |
| 186 | 183 | c.focusScreen = i | |
| 187 | 184 | } | |
| 188 | 185 | return Effect{} | |
| 189 | 186 | case ActionMoveWorkspaceLeft, ActionMoveWorkspaceRight: | |
| 190 | - | c.moveWorkspace(dir) | |
| 187 | + | d := dirLeft | |
| 188 | + | if a == ActionMoveWorkspaceRight { | |
| 189 | + | d = dirRight | |
| 190 | + | } | |
| 191 | + | c.moveWorkspace(d) | |
| 191 | 192 | return Effect{} | |
| 192 | 193 | case ActionFocusColumnLeft, ActionFocusColumnRight: | |
| 193 | 194 | w := c.cur().mon.Current() | |
| 195 | + | dir, d := 1, dirRight | |
| 194 | 196 | if a == ActionFocusColumnLeft { | |
| 195 | - | dir = -1 | |
| 196 | - | } else { | |
| 197 | - | dir = 1 | |
| 197 | + | dir, d = -1, dirLeft | |
| 198 | 198 | } | |
| 199 | 199 | // A covering fullscreen window is left for the window on that side; | |
| 200 | 200 | // with none (the workspace or stash edge), the move goes to the | |
| 201 | 201 | // neighbor monitor and fullscreen stays. | |
| 202 | 202 | if w.pinned() { | |
| 203 | 203 | if !w.FocusColumn(dir) { | |
| 204 | - | if i := c.neighbor(dir); i >= 0 { | |
| 204 | + | if i := c.neighbor(d); i >= 0 { | |
| 205 | 205 | c.focusScreen = i | |
| 206 | 206 | } | |
| 207 | 207 | } | |
| @@ -211,14 +211,14 @@ func (c *Core) applyAction(a Action) Effect { | |||
| 211 | 211 | // a native float leaves for the stash or the columns, and the stash | |
| 212 | 212 | // keeps the focus at its ends. | |
| 213 | 213 | edge := w.columnToward(dir) < 0 && (!w.onFloat() || w.floatFocus && !w.canLeaveFloat()) | |
| 214 | - | if i := c.neighbor(dir); edge && i >= 0 { | |
| 214 | + | if i := c.neighbor(d); edge && i >= 0 { | |
| 215 | 215 | c.focusScreen = i | |
| 216 | 216 | return Effect{} | |
| 217 | 217 | } | |
| 218 | 218 | case ActionConsumeOrExpelLeft, ActionConsumeOrExpelRight: | |
| 219 | - | dir = 1 | |
| 219 | + | dir, d := 1, dirRight | |
| 220 | 220 | if a == ActionConsumeOrExpelLeft { | |
| 221 | - | dir = -1 | |
| 221 | + | dir, d = -1, dirLeft | |
| 222 | 222 | } | |
| 223 | 223 | from := c.cur() | |
| 224 | 224 | if from.mon.Current().ConsumeOrExpel(dir) { | |
| @@ -227,7 +227,7 @@ func (c *Core) applyAction(a Action) Effect { | |||
| 227 | 227 | } | |
| 228 | 228 | // Full fixed workspace, stacked edge column: one hop to the | |
| 229 | 229 | // neighbor monitor, if any; focus follows the window. | |
| 230 | - | i := c.neighbor(dir) | |
| 230 | + | i := c.neighbor(d) | |
| 231 | 231 | if i < 0 { | |
| 232 | 232 | return Effect{} | |
| 233 | 233 | } | |
| @@ -244,12 +244,12 @@ func (c *Core) applyAction(a Action) Effect { | |||
| 244 | 244 | // Past the edge the column moves to the neighbor screen, on the | |
| 245 | 245 | // side facing this one, and focus follows it. | |
| 246 | 246 | w := c.cur().mon.Current() | |
| 247 | - | dir = 1 | |
| 247 | + | d := dirRight | |
| 248 | 248 | if a == ActionMoveColumnLeft { | |
| 249 | - | dir = -1 | |
| 249 | + | d = dirLeft | |
| 250 | 250 | } | |
| 251 | - | edge := (dir < 0 && w.Focus == 0) || (dir > 0 && w.Focus == len(w.Columns)-1) | |
| 252 | - | if i := c.neighbor(dir); edge && i >= 0 { | |
| 251 | + | edge := (d == dirLeft && w.Focus == 0) || (d == dirRight && w.Focus == len(w.Columns)-1) | |
| 252 | + | if i := c.neighbor(d); edge && i >= 0 { | |
| 253 | 253 | col, ok := w.takeColumn() | |
| 254 | 254 | if !ok { | |
| 255 | 255 | return Effect{} | |
| @@ -258,7 +258,7 @@ func (c *Core) applyAction(a Action) Effect { | |||
| 258 | 258 | c.focusScreen = i | |
| 259 | 259 | to := c.cur().mon.Current() | |
| 260 | 260 | at := 0 | |
| 261 | - | if dir < 0 { | |
| 261 | + | if d == dirLeft { | |
| 262 | 262 | at = len(to.Columns) | |
| 263 | 263 | } | |
| 264 | 264 | to.receive(col, at) | |
| @@ -173,35 +173,35 @@ func (c *Core) applyAction(a Action) Effect { | |||||
| 173 | if name, ok := NamedArg(a); ok && c.bringNamed(name) { | 173 | if name, ok := NamedArg(a); ok && c.bringNamed(name) { | ||
| 174 | return Effect{} | 174 | return Effect{} | ||
| 175 | } | 175 | } | ||
| 176 | - | dir := 0 | |||
| 177 | - | switch a { | |||
| 178 | - | case ActionFocusMonitorLeft, ActionMoveWorkspaceLeft: | |||
| 179 | - | dir = -1 | |||
| 180 | - | case ActionFocusMonitorRight, ActionMoveWorkspaceRight: | |||
| 181 | - | dir = 1 | |||
| 182 | - | } | |||
| 183 | switch a { | 176 | switch a { | ||
| 184 | case ActionFocusMonitorLeft, ActionFocusMonitorRight: | 177 | case ActionFocusMonitorLeft, ActionFocusMonitorRight: | ||
| 185 | - | if i := c.neighbor(dir); i >= 0 { | 178 | + | d := dirLeft |
| 179 | + | if a == ActionFocusMonitorRight { | |||
| 180 | + | d = dirRight | |||
| 181 | + | } | |||
| 182 | + | if i := c.neighbor(d); i >= 0 { | |||
| 186 | c.focusScreen = i | 183 | c.focusScreen = i | ||
| 187 | } | 184 | } | ||
| 188 | return Effect{} | 185 | return Effect{} | ||
| 189 | case ActionMoveWorkspaceLeft, ActionMoveWorkspaceRight: | 186 | case ActionMoveWorkspaceLeft, ActionMoveWorkspaceRight: | ||
| 190 | - | c.moveWorkspace(dir) | 187 | + | d := dirLeft |
| 188 | + | if a == ActionMoveWorkspaceRight { | |||
| 189 | + | d = dirRight | |||
| 190 | + | } | |||
| 191 | + | c.moveWorkspace(d) | |||
| 191 | return Effect{} | 192 | return Effect{} | ||
| 192 | case ActionFocusColumnLeft, ActionFocusColumnRight: | 193 | case ActionFocusColumnLeft, ActionFocusColumnRight: | ||
| 193 | w := c.cur().mon.Current() | 194 | w := c.cur().mon.Current() | ||
| 195 | + | dir, d := 1, dirRight | |||
| 194 | if a == ActionFocusColumnLeft { | 196 | if a == ActionFocusColumnLeft { | ||
| 195 | - | dir = -1 | 197 | + | dir, d = -1, dirLeft |
| 196 | - | } else { | |||
| 197 | - | dir = 1 | |||
| 198 | } | 198 | } | ||
| 199 | // A covering fullscreen window is left for the window on that side; | 199 | // A covering fullscreen window is left for the window on that side; | ||
| 200 | // with none (the workspace or stash edge), the move goes to the | 200 | // with none (the workspace or stash edge), the move goes to the | ||
| 201 | // neighbor monitor and fullscreen stays. | 201 | // neighbor monitor and fullscreen stays. | ||
| 202 | if w.pinned() { | 202 | if w.pinned() { | ||
| 203 | if !w.FocusColumn(dir) { | 203 | if !w.FocusColumn(dir) { | ||
| 204 | - | if i := c.neighbor(dir); i >= 0 { | 204 | + | if i := c.neighbor(d); i >= 0 { |
| 205 | c.focusScreen = i | 205 | c.focusScreen = i | ||
| 206 | } | 206 | } | ||
| 207 | } | 207 | } | ||
| @@ -211,14 +211,14 @@ func (c *Core) applyAction(a Action) Effect { | |||||
| 211 | // a native float leaves for the stash or the columns, and the stash | 211 | // a native float leaves for the stash or the columns, and the stash | ||
| 212 | // keeps the focus at its ends. | 212 | // keeps the focus at its ends. | ||
| 213 | edge := w.columnToward(dir) < 0 && (!w.onFloat() || w.floatFocus && !w.canLeaveFloat()) | 213 | edge := w.columnToward(dir) < 0 && (!w.onFloat() || w.floatFocus && !w.canLeaveFloat()) | ||
| 214 | - | if i := c.neighbor(dir); edge && i >= 0 { | 214 | + | if i := c.neighbor(d); edge && i >= 0 { |
| 215 | c.focusScreen = i | 215 | c.focusScreen = i | ||
| 216 | return Effect{} | 216 | return Effect{} | ||
| 217 | } | 217 | } | ||
| 218 | case ActionConsumeOrExpelLeft, ActionConsumeOrExpelRight: | 218 | case ActionConsumeOrExpelLeft, ActionConsumeOrExpelRight: | ||
| 219 | - | dir = 1 | 219 | + | dir, d := 1, dirRight |
| 220 | if a == ActionConsumeOrExpelLeft { | 220 | if a == ActionConsumeOrExpelLeft { | ||
| 221 | - | dir = -1 | 221 | + | dir, d = -1, dirLeft |
| 222 | } | 222 | } | ||
| 223 | from := c.cur() | 223 | from := c.cur() | ||
| 224 | if from.mon.Current().ConsumeOrExpel(dir) { | 224 | if from.mon.Current().ConsumeOrExpel(dir) { | ||
| @@ -227,7 +227,7 @@ func (c *Core) applyAction(a Action) Effect { | |||||
| 227 | } | 227 | } | ||
| 228 | // Full fixed workspace, stacked edge column: one hop to the | 228 | // Full fixed workspace, stacked edge column: one hop to the | ||
| 229 | // neighbor monitor, if any; focus follows the window. | 229 | // neighbor monitor, if any; focus follows the window. | ||
| 230 | - | i := c.neighbor(dir) | 230 | + | i := c.neighbor(d) |
| 231 | if i < 0 { | 231 | if i < 0 { | ||
| 232 | return Effect{} | 232 | return Effect{} | ||
| 233 | } | 233 | } | ||
| @@ -244,12 +244,12 @@ func (c *Core) applyAction(a Action) Effect { | |||||
| 244 | // Past the edge the column moves to the neighbor screen, on the | 244 | // Past the edge the column moves to the neighbor screen, on the | ||
| 245 | // side facing this one, and focus follows it. | 245 | // side facing this one, and focus follows it. | ||
| 246 | w := c.cur().mon.Current() | 246 | w := c.cur().mon.Current() | ||
| 247 | - | dir = 1 | 247 | + | d := dirRight |
| 248 | if a == ActionMoveColumnLeft { | 248 | if a == ActionMoveColumnLeft { | ||
| 249 | - | dir = -1 | 249 | + | d = dirLeft |
| 250 | } | 250 | } | ||
| 251 | - | edge := (dir < 0 && w.Focus == 0) || (dir > 0 && w.Focus == len(w.Columns)-1) | 251 | + | edge := (d == dirLeft && w.Focus == 0) || (d == dirRight && w.Focus == len(w.Columns)-1) |
| 252 | - | if i := c.neighbor(dir); edge && i >= 0 { | 252 | + | if i := c.neighbor(d); edge && i >= 0 { |
| 253 | col, ok := w.takeColumn() | 253 | col, ok := w.takeColumn() | ||
| 254 | if !ok { | 254 | if !ok { | ||
| 255 | return Effect{} | 255 | return Effect{} | ||
| @@ -258,7 +258,7 @@ func (c *Core) applyAction(a Action) Effect { | |||||
| 258 | c.focusScreen = i | 258 | c.focusScreen = i | ||
| 259 | to := c.cur().mon.Current() | 259 | to := c.cur().mon.Current() | ||
| 260 | at := 0 | 260 | at := 0 | ||
| 261 | - | if dir < 0 { | 261 | + | if d == dirLeft { |
| 262 | at = len(to.Columns) | 262 | at = len(to.Columns) | ||
| 263 | } | 263 | } | ||
| 264 | to.receive(col, at) | 264 | to.receive(col, at) | ||
A internal/core/placement.go +32 -0
| @@ -0,0 +1,32 @@ | |||
| 1 | + | package core | |
| 2 | + | ||
| 3 | + | import "image" | |
| 4 | + | ||
| 5 | + | // placeItem is one output to place: its connector name, logical size and, when set, the | |
| 6 | + | // explicit origin from output management (output.<name> position). | |
| 7 | + | type placeItem struct { | |
| 8 | + | name string | |
| 9 | + | w, h int | |
| 10 | + | pos *image.Point | |
| 11 | + | } | |
| 12 | + | ||
| 13 | + | // placeOutputs returns the logical origin of every item, in item order. | |
| 14 | + | // Items with an explicit position keep it; the others follow, in order, from | |
| 15 | + | // the rightmost edge placed so far (never left of 0), at y=0. | |
| 16 | + | func placeOutputs(items []placeItem) []image.Point { | |
| 17 | + | out := make([]image.Point, len(items)) | |
| 18 | + | x := 0 | |
| 19 | + | for i, it := range items { | |
| 20 | + | if it.pos != nil { | |
| 21 | + | out[i] = *it.pos | |
| 22 | + | x = max(x, it.pos.X+it.w) | |
| 23 | + | } | |
| 24 | + | } | |
| 25 | + | for i, it := range items { | |
| 26 | + | if it.pos == nil { | |
| 27 | + | out[i] = image.Point{X: x} | |
| 28 | + | x += it.w | |
| 29 | + | } | |
| 30 | + | } | |
| 31 | + | return out | |
| 32 | + | } | |
| @@ -0,0 +1,32 @@ | |||||
| 1 | + | package core | |||
| 2 | + | ||||
| 3 | + | import "image" | |||
| 4 | + | ||||
| 5 | + | // placeItem is one output to place: its connector name, logical size and, when set, the | |||
| 6 | + | // explicit origin from output management (output.<name> position). | |||
| 7 | + | type placeItem struct { | |||
| 8 | + | name string | |||
| 9 | + | w, h int | |||
| 10 | + | pos *image.Point | |||
| 11 | + | } | |||
| 12 | + | ||||
| 13 | + | // placeOutputs returns the logical origin of every item, in item order. | |||
| 14 | + | // Items with an explicit position keep it; the others follow, in order, from | |||
| 15 | + | // the rightmost edge placed so far (never left of 0), at y=0. | |||
| 16 | + | func placeOutputs(items []placeItem) []image.Point { | |||
| 17 | + | out := make([]image.Point, len(items)) | |||
| 18 | + | x := 0 | |||
| 19 | + | for i, it := range items { | |||
| 20 | + | if it.pos != nil { | |||
| 21 | + | out[i] = *it.pos | |||
| 22 | + | x = max(x, it.pos.X+it.w) | |||
| 23 | + | } | |||
| 24 | + | } | |||
| 25 | + | for i, it := range items { | |||
| 26 | + | if it.pos == nil { | |||
| 27 | + | out[i] = image.Point{X: x} | |||
| 28 | + | x += it.w | |||
| 29 | + | } | |||
| 30 | + | } | |||
| 31 | + | return out | |||
| 32 | + | } | |||
A internal/core/placement_test.go +46 -0
| @@ -0,0 +1,46 @@ | |||
| 1 | + | package core | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "image" | |
| 5 | + | "slices" | |
| 6 | + | "testing" | |
| 7 | + | ) | |
| 8 | + | ||
| 9 | + | func TestPlaceOutputs(t *testing.T) { | |
| 10 | + | at := func(x, y int) *image.Point { return &image.Point{X: x, Y: y} } | |
| 11 | + | tests := []struct { | |
| 12 | + | name string | |
| 13 | + | items []placeItem | |
| 14 | + | want []image.Point | |
| 15 | + | }{ | |
| 16 | + | {"none", nil, []image.Point{}}, | |
| 17 | + | {"one automatic", []placeItem{{name: "A", w: 400, h: 300}}, []image.Point{{X: 0, Y: 0}}}, | |
| 18 | + | { | |
| 19 | + | "three automatic", | |
| 20 | + | []placeItem{{name: "A", w: 400, h: 300}, {name: "B", w: 200, h: 300}, {name: "C", w: 300, h: 300}}, | |
| 21 | + | []image.Point{{X: 0, Y: 0}, {X: 400, Y: 0}, {X: 600, Y: 0}}, | |
| 22 | + | }, | |
| 23 | + | { | |
| 24 | + | "explicit position then automatic", | |
| 25 | + | []placeItem{{name: "DP-1", w: 400, h: 300}, {name: "DP-2", w: 400, h: 300, pos: at(100, 40)}}, | |
| 26 | + | []image.Point{{X: 500, Y: 0}, {X: 100, Y: 40}}, | |
| 27 | + | }, | |
| 28 | + | { | |
| 29 | + | "explicit negative x leaves automatic at zero", | |
| 30 | + | []placeItem{{name: "A", w: 300, h: 300}, {name: "B", w: 400, h: 300, pos: at(-500, 0)}}, | |
| 31 | + | []image.Point{{X: 0, Y: 0}, {X: -500, Y: 0}}, | |
| 32 | + | }, | |
| 33 | + | { | |
| 34 | + | "all explicit with negative position", | |
| 35 | + | []placeItem{{name: "A", w: 400, h: 300, pos: at(-200, 12)}, {name: "B", w: 400, h: 300, pos: at(20, -120)}}, | |
| 36 | + | []image.Point{{X: -200, Y: 12}, {X: 20, Y: -120}}, | |
| 37 | + | }, | |
| 38 | + | } | |
| 39 | + | for _, tc := range tests { | |
| 40 | + | t.Run(tc.name, func(t *testing.T) { | |
| 41 | + | if got := placeOutputs(tc.items); !slices.Equal(got, tc.want) { | |
| 42 | + | t.Fatalf("got %v, want %v", got, tc.want) | |
| 43 | + | } | |
| 44 | + | }) | |
| 45 | + | } | |
| 46 | + | } | |
| @@ -0,0 +1,46 @@ | |||||
| 1 | + | package core | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "image" | |||
| 5 | + | "slices" | |||
| 6 | + | "testing" | |||
| 7 | + | ) | |||
| 8 | + | ||||
| 9 | + | func TestPlaceOutputs(t *testing.T) { | |||
| 10 | + | at := func(x, y int) *image.Point { return &image.Point{X: x, Y: y} } | |||
| 11 | + | tests := []struct { | |||
| 12 | + | name string | |||
| 13 | + | items []placeItem | |||
| 14 | + | want []image.Point | |||
| 15 | + | }{ | |||
| 16 | + | {"none", nil, []image.Point{}}, | |||
| 17 | + | {"one automatic", []placeItem{{name: "A", w: 400, h: 300}}, []image.Point{{X: 0, Y: 0}}}, | |||
| 18 | + | { | |||
| 19 | + | "three automatic", | |||
| 20 | + | []placeItem{{name: "A", w: 400, h: 300}, {name: "B", w: 200, h: 300}, {name: "C", w: 300, h: 300}}, | |||
| 21 | + | []image.Point{{X: 0, Y: 0}, {X: 400, Y: 0}, {X: 600, Y: 0}}, | |||
| 22 | + | }, | |||
| 23 | + | { | |||
| 24 | + | "explicit position then automatic", | |||
| 25 | + | []placeItem{{name: "DP-1", w: 400, h: 300}, {name: "DP-2", w: 400, h: 300, pos: at(100, 40)}}, | |||
| 26 | + | []image.Point{{X: 500, Y: 0}, {X: 100, Y: 40}}, | |||
| 27 | + | }, | |||
| 28 | + | { | |||
| 29 | + | "explicit negative x leaves automatic at zero", | |||
| 30 | + | []placeItem{{name: "A", w: 300, h: 300}, {name: "B", w: 400, h: 300, pos: at(-500, 0)}}, | |||
| 31 | + | []image.Point{{X: 0, Y: 0}, {X: -500, Y: 0}}, | |||
| 32 | + | }, | |||
| 33 | + | { | |||
| 34 | + | "all explicit with negative position", | |||
| 35 | + | []placeItem{{name: "A", w: 400, h: 300, pos: at(-200, 12)}, {name: "B", w: 400, h: 300, pos: at(20, -120)}}, | |||
| 36 | + | []image.Point{{X: -200, Y: 12}, {X: 20, Y: -120}}, | |||
| 37 | + | }, | |||
| 38 | + | } | |||
| 39 | + | for _, tc := range tests { | |||
| 40 | + | t.Run(tc.name, func(t *testing.T) { | |||
| 41 | + | if got := placeOutputs(tc.items); !slices.Equal(got, tc.want) { | |||
| 42 | + | t.Fatalf("got %v, want %v", got, tc.want) | |||
| 43 | + | } | |||
| 44 | + | }) | |||
| 45 | + | } | |||
| 46 | + | } | |||
M internal/core/screens.go +52 -45
| @@ -7,8 +7,8 @@ import ( | |||
| 7 | 7 | ) | |
| 8 | 8 | ||
| 9 | 9 | // screen is one connected output: its monitor tree, scale, layer surfaces and | |
| 10 | - | // place in the global layout (ADR 011). Outputs are placed left to right, in | |
| 11 | - | // config order (output.<name>.*), then in connection order. | |
| 10 | + | // place in the global layout (ADR 011). Outputs are ordered by config | |
| 11 | + | // (output.<name>.*), then by connection; order places them. | |
| 12 | 12 | type screen struct { | |
| 13 | 13 | info ports.OutputInfo | |
| 14 | 14 | mon *Monitor | |
| @@ -49,44 +49,39 @@ func (s *screen) arrange() { | |||
| 49 | 49 | s.mon.SetUsable(usable) | |
| 50 | 50 | } | |
| 51 | 51 | ||
| 52 | - | // order sorts screens by config order, then connection order, and assigns x. | |
| 52 | + | // order sorts screens by config order, then connection order, and places them | |
| 53 | + | // in the global layout (placeOutputs). | |
| 53 | 54 | func (c *Core) order() { | |
| 54 | 55 | rank := func(s *screen) int { | |
| 55 | - | for i, o := range c.cfg.Outputs { | |
| 56 | - | if o.Name == s.name() { | |
| 57 | - | return i | |
| 58 | - | } | |
| 56 | + | if i := slices.IndexFunc(c.cfg.Outputs, func(o ports.OutputConfig) bool { return o.Name == s.name() }); i >= 0 { | |
| 57 | + | return i | |
| 59 | 58 | } | |
| 60 | 59 | return len(c.cfg.Outputs) | |
| 61 | 60 | } | |
| 62 | 61 | focused := c.cur() | |
| 63 | 62 | slices.SortStableFunc(c.screens, func(a, b *screen) int { return rank(a) - rank(b) }) | |
| 64 | 63 | c.focusScreen = slices.Index(c.screens, focused) | |
| 65 | - | // Explicitly placed outputs reserve their position first; auto-placed | |
| 66 | - | // outputs follow the rightmost explicit edge in configuration order. | |
| 67 | - | x := 0 | |
| 68 | - | for _, s := range c.screens { | |
| 69 | - | for _, o := range c.cfg.Outputs { | |
| 70 | - | if o.Name == s.name() && o.Pos != nil { | |
| 71 | - | s.x, s.y = o.Pos.X, o.Pos.Y | |
| 72 | - | x = max(x, s.x+s.mon.Output().W) | |
| 73 | - | break | |
| 74 | - | } | |
| 64 | + | items := make([]placeItem, len(c.screens)) | |
| 65 | + | for i, s := range c.screens { | |
| 66 | + | o := s.mon.Output() | |
| 67 | + | items[i].name, items[i].w, items[i].h = s.name(), o.W, o.H | |
| 68 | + | if cfg, ok := c.outputConfig(s.name()); ok { | |
| 69 | + | items[i].pos = cfg.Pos | |
| 75 | 70 | } | |
| 76 | 71 | } | |
| 77 | - | for _, s := range c.screens { | |
| 78 | - | explicit := false | |
| 79 | - | for _, o := range c.cfg.Outputs { | |
| 80 | - | if o.Name == s.name() && o.Pos != nil { | |
| 81 | - | explicit = true | |
| 82 | - | break | |
| 83 | - | } | |
| 84 | - | } | |
| 85 | - | if !explicit { | |
| 86 | - | s.x, s.y = x, 0 | |
| 87 | - | x += s.mon.Output().W | |
| 88 | - | } | |
| 72 | + | for i, p := range placeOutputs(items) { | |
| 73 | + | c.screens[i].x, c.screens[i].y = p.X, p.Y | |
| 74 | + | } | |
| 75 | + | } | |
| 76 | + | ||
| 77 | + | // outputConfig returns the output.<name> entry; names are unique in the | |
| 78 | + | // config. | |
| 79 | + | func (c *Core) outputConfig(name string) (ports.OutputConfig, bool) { | |
| 80 | + | i := slices.IndexFunc(c.cfg.Outputs, func(o ports.OutputConfig) bool { return o.Name == name }) | |
| 81 | + | if i < 0 { | |
| 82 | + | return ports.OutputConfig{}, false | |
| 89 | 83 | } | |
| 84 | + | return c.cfg.Outputs[i], true | |
| 90 | 85 | } | |
| 91 | 86 | ||
| 92 | 87 | // layout lists the connected outputs; the placeholder is not one. | |
| @@ -129,12 +124,8 @@ func (c *Core) byName(name string) (*screen, *Workspace) { | |||
| 129 | 124 | ||
| 130 | 125 | // isPrimary reports output.<name>.primary. | |
| 131 | 126 | func (c *Core) isPrimary(name string) bool { | |
| 132 | - | for _, o := range c.cfg.Outputs { | |
| 133 | - | if o.Name == name && o.Primary { | |
| 134 | - | return true | |
| 135 | - | } | |
| 136 | - | } | |
| 137 | - | return false | |
| 127 | + | o, _ := c.outputConfig(name) | |
| 128 | + | return o.Primary | |
| 138 | 129 | } | |
| 139 | 130 | ||
| 140 | 131 | // anyWindow reports whether any workspace holds a window. | |
| @@ -152,10 +143,8 @@ func (c *Core) anyWindow() bool { | |||
| 152 | 143 | // configScale is output.<name>.scale, else 1. | |
| 153 | 144 | func (c *Core) configScale(name string) float64 { | |
| 154 | 145 | v := 1.0 | |
| 155 | - | for _, o := range c.cfg.Outputs { | |
| 156 | - | if o.Name == name && o.Scale != 0 { | |
| 157 | - | v = o.Scale | |
| 158 | - | } | |
| 146 | + | if o, ok := c.outputConfig(name); ok && o.Scale != 0 { | |
| 147 | + | v = o.Scale | |
| 159 | 148 | } | |
| 160 | 149 | return SnapScale(v) | |
| 161 | 150 | } | |
| @@ -407,10 +396,28 @@ func (c *Core) settings(m *Monitor) { | |||
| 407 | 396 | m.SetFollowMove(c.cfg.Focus.FollowMove) | |
| 408 | 397 | } | |
| 409 | 398 | ||
| 410 | - | // neighbor returns the screen index left (-1) or right (+1) of the focused | |
| 411 | - | // one, or -1 at the edge. | |
| 412 | - | func (c *Core) neighbor(dir int) int { | |
| 413 | - | i := c.focusScreen + dir | |
| 399 | + | // direction is where a monitor action looks for a neighbor screen. | |
| 400 | + | type direction uint8 | |
| 401 | + | ||
| 402 | + | const ( | |
| 403 | + | dirLeft direction = iota | |
| 404 | + | dirRight | |
| 405 | + | dirUp | |
| 406 | + | dirDown | |
| 407 | + | ) | |
| 408 | + | ||
| 409 | + | // neighbor returns the index of the screen next to the focused one in | |
| 410 | + | // direction d, or -1 at the edge. | |
| 411 | + | func (c *Core) neighbor(d direction) int { | |
| 412 | + | var i int | |
| 413 | + | switch d { | |
| 414 | + | case dirLeft: | |
| 415 | + | i = c.focusScreen - 1 | |
| 416 | + | case dirRight: | |
| 417 | + | i = c.focusScreen + 1 | |
| 418 | + | default: | |
| 419 | + | return -1 | |
| 420 | + | } | |
| 414 | 421 | if i < 0 || i >= len(c.screens) { | |
| 415 | 422 | return -1 | |
| 416 | 423 | } | |
| @@ -419,8 +426,8 @@ func (c *Core) neighbor(dir int) int { | |||
| 419 | 426 | ||
| 420 | 427 | // moveWorkspace moves the workspace on screen to the neighbor screen, where | |
| 421 | 428 | // it is appended and shown; it becomes its new home (ADR 011). | |
| 422 | - | func (c *Core) moveWorkspace(dir int) { | |
| 423 | - | to := c.neighbor(dir) | |
| 429 | + | func (c *Core) moveWorkspace(d direction) { | |
| 430 | + | to := c.neighbor(d) | |
| 424 | 431 | if to < 0 { | |
| 425 | 432 | return | |
| 426 | 433 | } | |
| @@ -7,8 +7,8 @@ import ( | |||||
| 7 | ) | 7 | ) | ||
| 8 | 8 | ||||
| 9 | // screen is one connected output: its monitor tree, scale, layer surfaces and | 9 | // screen is one connected output: its monitor tree, scale, layer surfaces and | ||
| 10 | - | // place in the global layout (ADR 011). Outputs are placed left to right, in | 10 | + | // place in the global layout (ADR 011). Outputs are ordered by config |
| 11 | - | // config order (output.<name>.*), then in connection order. | 11 | + | // (output.<name>.*), then by connection; order places them. |
| 12 | type screen struct { | 12 | type screen struct { | ||
| 13 | info ports.OutputInfo | 13 | info ports.OutputInfo | ||
| 14 | mon *Monitor | 14 | mon *Monitor | ||
| @@ -49,44 +49,39 @@ func (s *screen) arrange() { | |||||
| 49 | s.mon.SetUsable(usable) | 49 | s.mon.SetUsable(usable) | ||
| 50 | } | 50 | } | ||
| 51 | 51 | ||||
| 52 | - | // order sorts screens by config order, then connection order, and assigns x. | 52 | + | // order sorts screens by config order, then connection order, and places them |
| 53 | + | // in the global layout (placeOutputs). | |||
| 53 | func (c *Core) order() { | 54 | func (c *Core) order() { | ||
| 54 | rank := func(s *screen) int { | 55 | rank := func(s *screen) int { | ||
| 55 | - | for i, o := range c.cfg.Outputs { | 56 | + | if i := slices.IndexFunc(c.cfg.Outputs, func(o ports.OutputConfig) bool { return o.Name == s.name() }); i >= 0 { |
| 56 | - | if o.Name == s.name() { | 57 | + | return i |
| 57 | - | return i | |||
| 58 | - | } | |||
| 59 | } | 58 | } | ||
| 60 | return len(c.cfg.Outputs) | 59 | return len(c.cfg.Outputs) | ||
| 61 | } | 60 | } | ||
| 62 | focused := c.cur() | 61 | focused := c.cur() | ||
| 63 | slices.SortStableFunc(c.screens, func(a, b *screen) int { return rank(a) - rank(b) }) | 62 | slices.SortStableFunc(c.screens, func(a, b *screen) int { return rank(a) - rank(b) }) | ||
| 64 | c.focusScreen = slices.Index(c.screens, focused) | 63 | c.focusScreen = slices.Index(c.screens, focused) | ||
| 65 | - | // Explicitly placed outputs reserve their position first; auto-placed | 64 | + | items := make([]placeItem, len(c.screens)) |
| 66 | - | // outputs follow the rightmost explicit edge in configuration order. | 65 | + | for i, s := range c.screens { |
| 67 | - | x := 0 | 66 | + | o := s.mon.Output() |
| 68 | - | for _, s := range c.screens { | 67 | + | items[i].name, items[i].w, items[i].h = s.name(), o.W, o.H |
| 69 | - | for _, o := range c.cfg.Outputs { | 68 | + | if cfg, ok := c.outputConfig(s.name()); ok { |
| 70 | - | if o.Name == s.name() && o.Pos != nil { | 69 | + | items[i].pos = cfg.Pos |
| 71 | - | s.x, s.y = o.Pos.X, o.Pos.Y | |||
| 72 | - | x = max(x, s.x+s.mon.Output().W) | |||
| 73 | - | break | |||
| 74 | - | } | |||
| 75 | } | 70 | } | ||
| 76 | } | 71 | } | ||
| 77 | - | for _, s := range c.screens { | 72 | + | for i, p := range placeOutputs(items) { |
| 78 | - | explicit := false | 73 | + | c.screens[i].x, c.screens[i].y = p.X, p.Y |
| 79 | - | for _, o := range c.cfg.Outputs { | 74 | + | } |
| 80 | - | if o.Name == s.name() && o.Pos != nil { | 75 | + | } |
| 81 | - | explicit = true | 76 | + | |
| 82 | - | break | 77 | + | // outputConfig returns the output.<name> entry; names are unique in the |
| 83 | - | } | 78 | + | // config. |
| 84 | - | } | 79 | + | func (c *Core) outputConfig(name string) (ports.OutputConfig, bool) { |
| 85 | - | if !explicit { | 80 | + | i := slices.IndexFunc(c.cfg.Outputs, func(o ports.OutputConfig) bool { return o.Name == name }) |
| 86 | - | s.x, s.y = x, 0 | 81 | + | if i < 0 { |
| 87 | - | x += s.mon.Output().W | 82 | + | return ports.OutputConfig{}, false |
| 88 | - | } | |||
| 89 | } | 83 | } | ||
| 84 | + | return c.cfg.Outputs[i], true | |||
| 90 | } | 85 | } | ||
| 91 | 86 | ||||
| 92 | // layout lists the connected outputs; the placeholder is not one. | 87 | // layout lists the connected outputs; the placeholder is not one. | ||
| @@ -129,12 +124,8 @@ func (c *Core) byName(name string) (*screen, *Workspace) { | |||||
| 129 | 124 | ||||
| 130 | // isPrimary reports output.<name>.primary. | 125 | // isPrimary reports output.<name>.primary. | ||
| 131 | func (c *Core) isPrimary(name string) bool { | 126 | func (c *Core) isPrimary(name string) bool { | ||
| 132 | - | for _, o := range c.cfg.Outputs { | 127 | + | o, _ := c.outputConfig(name) |
| 133 | - | if o.Name == name && o.Primary { | 128 | + | return o.Primary |
| 134 | - | return true | |||
| 135 | - | } | |||
| 136 | - | } | |||
| 137 | - | return false | |||
| 138 | } | 129 | } | ||
| 139 | 130 | ||||
| 140 | // anyWindow reports whether any workspace holds a window. | 131 | // anyWindow reports whether any workspace holds a window. | ||
| @@ -152,10 +143,8 @@ func (c *Core) anyWindow() bool { | |||||
| 152 | // configScale is output.<name>.scale, else 1. | 143 | // configScale is output.<name>.scale, else 1. | ||
| 153 | func (c *Core) configScale(name string) float64 { | 144 | func (c *Core) configScale(name string) float64 { | ||
| 154 | v := 1.0 | 145 | v := 1.0 | ||
| 155 | - | for _, o := range c.cfg.Outputs { | 146 | + | if o, ok := c.outputConfig(name); ok && o.Scale != 0 { |
| 156 | - | if o.Name == name && o.Scale != 0 { | 147 | + | v = o.Scale |
| 157 | - | v = o.Scale | |||
| 158 | - | } | |||
| 159 | } | 148 | } | ||
| 160 | return SnapScale(v) | 149 | return SnapScale(v) | ||
| 161 | } | 150 | } | ||
| @@ -407,10 +396,28 @@ func (c *Core) settings(m *Monitor) { | |||||
| 407 | m.SetFollowMove(c.cfg.Focus.FollowMove) | 396 | m.SetFollowMove(c.cfg.Focus.FollowMove) | ||
| 408 | } | 397 | } | ||
| 409 | 398 | ||||
| 410 | - | // neighbor returns the screen index left (-1) or right (+1) of the focused | 399 | + | // direction is where a monitor action looks for a neighbor screen. |
| 411 | - | // one, or -1 at the edge. | 400 | + | type direction uint8 |
| 412 | - | func (c *Core) neighbor(dir int) int { | 401 | + | |
| 413 | - | i := c.focusScreen + dir | 402 | + | const ( |
| 403 | + | dirLeft direction = iota | |||
| 404 | + | dirRight | |||
| 405 | + | dirUp | |||
| 406 | + | dirDown | |||
| 407 | + | ) | |||
| 408 | + | ||||
| 409 | + | // neighbor returns the index of the screen next to the focused one in | |||
| 410 | + | // direction d, or -1 at the edge. | |||
| 411 | + | func (c *Core) neighbor(d direction) int { | |||
| 412 | + | var i int | |||
| 413 | + | switch d { | |||
| 414 | + | case dirLeft: | |||
| 415 | + | i = c.focusScreen - 1 | |||
| 416 | + | case dirRight: | |||
| 417 | + | i = c.focusScreen + 1 | |||
| 418 | + | default: | |||
| 419 | + | return -1 | |||
| 420 | + | } | |||
| 414 | if i < 0 || i >= len(c.screens) { | 421 | if i < 0 || i >= len(c.screens) { | ||
| 415 | return -1 | 422 | return -1 | ||
| 416 | } | 423 | } | ||
| @@ -419,8 +426,8 @@ func (c *Core) neighbor(dir int) int { | |||||
| 419 | 426 | ||||
| 420 | // moveWorkspace moves the workspace on screen to the neighbor screen, where | 427 | // moveWorkspace moves the workspace on screen to the neighbor screen, where | ||
| 421 | // it is appended and shown; it becomes its new home (ADR 011). | 428 | // it is appended and shown; it becomes its new home (ADR 011). | ||
| 422 | - | func (c *Core) moveWorkspace(dir int) { | 429 | + | func (c *Core) moveWorkspace(d direction) { |
| 423 | - | to := c.neighbor(dir) | 430 | + | to := c.neighbor(d) |
| 424 | if to < 0 { | 431 | if to < 0 { | ||
| 425 | return | 432 | return | ||
| 426 | } | 433 | } | ||