feat(core): resize window heights within a column (#81)
Columns keep per-row height shares set by set-window-height; layout and overview previews split rows by them, and joins or leaves reset equal rows.
- commit
- b2a4809b8f8527825bd1c43dff4863bb47988014
- author
- bnema · · 2026-10-01 16:31 UTC
- committer
- GitHub · · 2026-10-01 16:31 UTC
- parent
- 186314e
- signature
- GPG Unverified
B5690EEEBB952194The key is unknown
6 files · +327 -15
M docs/config.md +3 -0
| @@ -128,6 +128,7 @@ HDR requires DRM HDR connector properties, suitable KMS planes, and Vulkan fp16 | |||
| 128 | 128 | | `move-column-left/right` | Move the focused column | | |
| 129 | 129 | | `move-window-up/down` | Move the focused window up or down in its column; focus follows it | | |
| 130 | 130 | | `set-column-width <+N%\|-N%>` | Widen or narrow the focused column by N% of the usable width (1-100), between 10% and 100%. The new width stays until changed; `cycle-column-width` then restarts at the first preset. Does nothing in fixed overflow | | |
| 131 | + | | `set-window-height <+N%\|-N%>` | Give the focused window N% more or less of its column's height; the other rows give or take it in proportion. Each row keeps at least 10%. A window joining or leaving the column resets equal rows. Does nothing past 9 rows | | |
| 131 | 132 | | `consume-or-expel-window-left/right` | A lone window joins the neighbor column; a stacked one leaves for a new column | | |
| 132 | 133 | | `focus-workspace <N>` / `focus-workspace-up/down` | Show a numbered or neighbor workspace | | |
| 133 | 134 | | `workspace <name>` | Toggle a named workspace | | |
| @@ -191,6 +192,8 @@ With `keyboard.cmd = alt`, the `cmd+alt+…` binds have the same keys as `cmd+ | |||
| 191 | 192 | | `cmd+shift+down` / `cmd+shift+j` | `move-window-down` | | |
| 192 | 193 | | `cmd+alt+left` / `cmd+alt+h` | `set-column-width -10%` | | |
| 193 | 194 | | `cmd+alt+right` / `cmd+alt+l` | `set-column-width +10%` | | |
| 195 | + | | `cmd+alt+up` / `cmd+alt+k` | `set-window-height -10%` | | |
| 196 | + | | `cmd+alt+down` / `cmd+alt+j` | `set-window-height +10%` | | |
| 194 | 197 | | `cmd+r` | `cycle-column-width` | | |
| 195 | 198 | | `cmd+f` | `maximize-column` | | |
| 196 | 199 | | `cmd+shift+f` | `toggle-fullscreen` | | |
| @@ -128,6 +128,7 @@ HDR requires DRM HDR connector properties, suitable KMS planes, and Vulkan fp16 | |||||
| 128 | | `move-column-left/right` | Move the focused column | | 128 | | `move-column-left/right` | Move the focused column | | ||
| 129 | | `move-window-up/down` | Move the focused window up or down in its column; focus follows it | | 129 | | `move-window-up/down` | Move the focused window up or down in its column; focus follows it | | ||
| 130 | | `set-column-width <+N%\|-N%>` | Widen or narrow the focused column by N% of the usable width (1-100), between 10% and 100%. The new width stays until changed; `cycle-column-width` then restarts at the first preset. Does nothing in fixed overflow | | 130 | | `set-column-width <+N%\|-N%>` | Widen or narrow the focused column by N% of the usable width (1-100), between 10% and 100%. The new width stays until changed; `cycle-column-width` then restarts at the first preset. Does nothing in fixed overflow | | ||
| 131 | + | | `set-window-height <+N%\|-N%>` | Give the focused window N% more or less of its column's height; the other rows give or take it in proportion. Each row keeps at least 10%. A window joining or leaving the column resets equal rows. Does nothing past 9 rows | | |||
| 131 | | `consume-or-expel-window-left/right` | A lone window joins the neighbor column; a stacked one leaves for a new column | | 132 | | `consume-or-expel-window-left/right` | A lone window joins the neighbor column; a stacked one leaves for a new column | | ||
| 132 | | `focus-workspace <N>` / `focus-workspace-up/down` | Show a numbered or neighbor workspace | | 133 | | `focus-workspace <N>` / `focus-workspace-up/down` | Show a numbered or neighbor workspace | | ||
| 133 | | `workspace <name>` | Toggle a named workspace | | 134 | | `workspace <name>` | Toggle a named workspace | | ||
| @@ -191,6 +192,8 @@ With `keyboard.cmd = alt`, the `cmd+alt+…` binds have the same keys as `cmd+ | |||||
| 191 | | `cmd+shift+down` / `cmd+shift+j` | `move-window-down` | | 192 | | `cmd+shift+down` / `cmd+shift+j` | `move-window-down` | | ||
| 192 | | `cmd+alt+left` / `cmd+alt+h` | `set-column-width -10%` | | 193 | | `cmd+alt+left` / `cmd+alt+h` | `set-column-width -10%` | | ||
| 193 | | `cmd+alt+right` / `cmd+alt+l` | `set-column-width +10%` | | 194 | | `cmd+alt+right` / `cmd+alt+l` | `set-column-width +10%` | | ||
| 195 | + | | `cmd+alt+up` / `cmd+alt+k` | `set-window-height -10%` | | |||
| 196 | + | | `cmd+alt+down` / `cmd+alt+j` | `set-window-height +10%` | | |||
| 194 | | `cmd+r` | `cycle-column-width` | | 197 | | `cmd+r` | `cycle-column-width` | | ||
| 195 | | `cmd+f` | `maximize-column` | | 198 | | `cmd+f` | `maximize-column` | | ||
| 196 | | `cmd+shift+f` | `toggle-fullscreen` | | 199 | | `cmd+shift+f` | `toggle-fullscreen` | | ||
M internal/core/action.go +2 -0
| @@ -384,6 +384,8 @@ func (w *Workspace) Apply(a Action) Effect { | |||
| 384 | 384 | if axis, pct, ok := ResizeArg(a); ok { | |
| 385 | 385 | if axis == ResizeWidth { | |
| 386 | 386 | w.ResizeColumn(pct) | |
| 387 | + | } else { | |
| 388 | + | w.ResizeRow(pct) | |
| 387 | 389 | } | |
| 388 | 390 | return Effect{} | |
| 389 | 391 | } | |
| @@ -384,6 +384,8 @@ func (w *Workspace) Apply(a Action) Effect { | |||||
| 384 | if axis, pct, ok := ResizeArg(a); ok { | 384 | if axis, pct, ok := ResizeArg(a); ok { | ||
| 385 | if axis == ResizeWidth { | 385 | if axis == ResizeWidth { | ||
| 386 | w.ResizeColumn(pct) | 386 | w.ResizeColumn(pct) | ||
| 387 | + | } else { | |||
| 388 | + | w.ResizeRow(pct) | |||
| 387 | } | 389 | } | ||
| 388 | return Effect{} | 390 | return Effect{} | ||
| 389 | } | 391 | } | ||
M internal/core/layout.go +124 -12
| @@ -99,6 +99,10 @@ type Column struct { | |||
| 99 | 99 | // Slot is the declared column number (workspace.<name>.column.N) of a | |
| 100 | 100 | // slot window; 0 for normal columns. | |
| 101 | 101 | Slot int | |
| 102 | + | // Shares are the row heights set by set-window-height, in percent, in | |
| 103 | + | // Windows order. They apply only while there is one per window and | |
| 104 | + | // they sum to 100; otherwise the rows share the height equally. | |
| 105 | + | Shares []int | |
| 102 | 106 | } | |
| 103 | 107 | type Placement struct { | |
| 104 | 108 | ID WindowID | |
| @@ -463,6 +467,7 @@ func (w *Workspace) RemoveWindow(id WindowID) { | |||
| 463 | 467 | } | |
| 464 | 468 | c := &w.Columns[i] | |
| 465 | 469 | c.Windows = append(c.Windows[:j], c.Windows[j+1:]...) | |
| 470 | + | c.Shares = nil | |
| 466 | 471 | if len(c.Windows) == 0 { | |
| 467 | 472 | w.Columns = append(w.Columns[:i], w.Columns[i+1:]...) | |
| 468 | 473 | if len(w.Columns) == 0 { | |
| @@ -688,6 +693,9 @@ func (w *Workspace) MoveWindow(dir int) { | |||
| 688 | 693 | w.dropSlotOf(c.Windows[0]) | |
| 689 | 694 | } | |
| 690 | 695 | c.Windows[c.Focus], c.Windows[to] = c.Windows[to], c.Windows[c.Focus] | |
| 696 | + | if validShares(*c) { | |
| 697 | + | c.Shares[c.Focus], c.Shares[to] = c.Shares[to], c.Shares[c.Focus] | |
| 698 | + | } | |
| 691 | 699 | c.Focus = to | |
| 692 | 700 | w.scroll() | |
| 693 | 701 | } | |
| @@ -711,6 +719,80 @@ func (w *Workspace) ResizeColumn(pct int) { | |||
| 711 | 719 | w.scroll() | |
| 712 | 720 | } | |
| 713 | 721 | ||
| 722 | + | // minShare is the smallest row height set-window-height leaves, percent. | |
| 723 | + | const minShare = 10 | |
| 724 | + | ||
| 725 | + | // ResizeRow changes the focused window's height share by pct percent; the | |
| 726 | + | // other rows of its column give or take it in proportion to their shares. | |
| 727 | + | // Every row keeps at least minShare percent. | |
| 728 | + | func (w *Workspace) ResizeRow(pct int) { | |
| 729 | + | if w.onFloat() || len(w.Columns) == 0 || w.fullscreenHides() { | |
| 730 | + | return | |
| 731 | + | } | |
| 732 | + | c := &w.Columns[w.Focus] | |
| 733 | + | n := len(c.Windows) | |
| 734 | + | if n < 2 || n*minShare > 100 { | |
| 735 | + | return | |
| 736 | + | } | |
| 737 | + | shares := c.Shares | |
| 738 | + | if !validShares(*c) { | |
| 739 | + | shares = make([]int, n) | |
| 740 | + | for i := range shares { | |
| 741 | + | shares[i] = 100 / n | |
| 742 | + | } | |
| 743 | + | shares[n-1] += 100 % n | |
| 744 | + | } else { | |
| 745 | + | shares = slices.Clone(shares) | |
| 746 | + | } | |
| 747 | + | f := c.Focus | |
| 748 | + | target := min(max(shares[f]+pct, minShare), 100-minShare*(n-1)) | |
| 749 | + | delta := target - shares[f] | |
| 750 | + | if delta == 0 { | |
| 751 | + | return | |
| 752 | + | } | |
| 753 | + | // The others share -delta in proportion to their room: what they have | |
| 754 | + | // above minShare when shrinking, their share when growing. | |
| 755 | + | weight := func(v int) int { | |
| 756 | + | if delta > 0 { | |
| 757 | + | return v - minShare | |
| 758 | + | } | |
| 759 | + | return v | |
| 760 | + | } | |
| 761 | + | total := 0 | |
| 762 | + | for i, v := range shares { | |
| 763 | + | if i != f { | |
| 764 | + | total += weight(v) | |
| 765 | + | } | |
| 766 | + | } | |
| 767 | + | left := delta | |
| 768 | + | for i, v := range shares { | |
| 769 | + | if i == f { | |
| 770 | + | continue | |
| 771 | + | } | |
| 772 | + | part := 0 | |
| 773 | + | if total > 0 { | |
| 774 | + | part = delta * weight(v) / total | |
| 775 | + | } | |
| 776 | + | shares[i] = v - part | |
| 777 | + | left -= part | |
| 778 | + | } | |
| 779 | + | shares[f] = target | |
| 780 | + | // Rounding: the remainder goes to the rows that still have room, last | |
| 781 | + | // first. | |
| 782 | + | for i := n - 1; left != 0 && i >= 0; i-- { | |
| 783 | + | if i == f { | |
| 784 | + | continue | |
| 785 | + | } | |
| 786 | + | step := left | |
| 787 | + | if left > 0 { | |
| 788 | + | step = min(left, shares[i]-minShare) | |
| 789 | + | } | |
| 790 | + | shares[i] -= step | |
| 791 | + | left -= step | |
| 792 | + | } | |
| 793 | + | c.Shares = shares | |
| 794 | + | } | |
| 795 | + | ||
| 714 | 796 | // takeColumn removes the focused column and returns it, as a normal column. | |
| 715 | 797 | func (w *Workspace) takeColumn() (Column, bool) { | |
| 716 | 798 | if len(w.Columns) == 0 || w.onFloat() { | |
| @@ -1213,6 +1295,46 @@ func stackRects(r Rect, n, gap int) []Rect { | |||
| 1213 | 1295 | return rows | |
| 1214 | 1296 | } | |
| 1215 | 1297 | ||
| 1298 | + | // rowRects splits column rect r into the rows of c: by c.Shares when they | |
| 1299 | + | // are valid, else equally (stackRects). | |
| 1300 | + | func rowRects(r Rect, c Column, gap int) []Rect { | |
| 1301 | + | n := len(c.Windows) | |
| 1302 | + | if !validShares(c) { | |
| 1303 | + | return stackRects(r, n, gap) | |
| 1304 | + | } | |
| 1305 | + | rows := make([]Rect, n) | |
| 1306 | + | avail := max(r.H-(n-1)*gap, 0) | |
| 1307 | + | yy, bottom, used := r.Y, r.Y+r.H, 0 | |
| 1308 | + | for i, share := range c.Shares { | |
| 1309 | + | hh := avail * share / 100 | |
| 1310 | + | if i == n-1 { | |
| 1311 | + | hh = avail - used | |
| 1312 | + | } | |
| 1313 | + | used += hh | |
| 1314 | + | yy = min(yy, bottom) | |
| 1315 | + | hh = min(hh, bottom-yy) | |
| 1316 | + | rows[i] = Rect{X: r.X, Y: yy, W: r.W, H: hh} | |
| 1317 | + | yy += hh + gap | |
| 1318 | + | } | |
| 1319 | + | return rows | |
| 1320 | + | } | |
| 1321 | + | ||
| 1322 | + | // validShares reports whether c.Shares has one share per window, each at | |
| 1323 | + | // least 1, summing to 100. | |
| 1324 | + | func validShares(c Column) bool { | |
| 1325 | + | if len(c.Shares) != len(c.Windows) || len(c.Shares) < 2 { | |
| 1326 | + | return false | |
| 1327 | + | } | |
| 1328 | + | sum := 0 | |
| 1329 | + | for _, v := range c.Shares { | |
| 1330 | + | if v < 1 { | |
| 1331 | + | return false | |
| 1332 | + | } | |
| 1333 | + | sum += v | |
| 1334 | + | } | |
| 1335 | + | return sum == 100 | |
| 1336 | + | } | |
| 1337 | + | ||
| 1216 | 1338 | // split cuts r in two halves with a gap: top/bottom when vertical, else left/right. | |
| 1217 | 1339 | func split(r Rect, gap int, vertical bool) (Rect, Rect) { | |
| 1218 | 1340 | if vertical { | |
| @@ -1239,18 +1361,9 @@ func (w *Workspace) Layout() []Placement { | |||
| 1239 | 1361 | if fullColumn { | |
| 1240 | 1362 | col.W = w.Output.W | |
| 1241 | 1363 | } | |
| 1242 | - | available := max(col.H-(n-1)*gap, 0) | |
| 1243 | - | height := available / n | |
| 1244 | - | y := col.Y | |
| 1245 | - | bottom := col.Y + col.H | |
| 1364 | + | rows := rowRects(col, c, gap) | |
| 1246 | 1365 | for j, id := range c.Windows { | |
| 1247 | - | h := height | |
| 1248 | - | if j == n-1 { | |
| 1249 | - | h = available - height*(n-1) | |
| 1250 | - | } | |
| 1251 | - | y = min(y, bottom) | |
| 1252 | - | h = min(h, bottom-y) | |
| 1253 | - | r := Rect{X: col.X, Y: y, W: col.W, H: h} | |
| 1366 | + | r := rows[j] | |
| 1254 | 1367 | full := w.fullscreen == id && id != 0 | |
| 1255 | 1368 | // Fixed overflow cannot scroll to other columns while one fills | |
| 1256 | 1369 | // the view. Keep them in place, but out of the scene. | |
| @@ -1267,7 +1380,6 @@ func (w *Workspace) Layout() []Placement { | |||
| 1267 | 1380 | r = Rect{} | |
| 1268 | 1381 | } | |
| 1269 | 1382 | result = append(result, Placement{ID: id, Rect: r, Fullscreen: full, Focused: id == focusedID, Hidden: hidden}) | |
| 1270 | - | y += h + gap | |
| 1271 | 1383 | } | |
| 1272 | 1384 | } | |
| 1273 | 1385 | setVisibleNeighbors(result, gap, w.Output) | |
| @@ -99,6 +99,10 @@ type Column struct { | |||||
| 99 | // Slot is the declared column number (workspace.<name>.column.N) of a | 99 | // Slot is the declared column number (workspace.<name>.column.N) of a | ||
| 100 | // slot window; 0 for normal columns. | 100 | // slot window; 0 for normal columns. | ||
| 101 | Slot int | 101 | Slot int | ||
| 102 | + | // Shares are the row heights set by set-window-height, in percent, in | |||
| 103 | + | // Windows order. They apply only while there is one per window and | |||
| 104 | + | // they sum to 100; otherwise the rows share the height equally. | |||
| 105 | + | Shares []int | |||
| 102 | } | 106 | } | ||
| 103 | type Placement struct { | 107 | type Placement struct { | ||
| 104 | ID WindowID | 108 | ID WindowID | ||
| @@ -463,6 +467,7 @@ func (w *Workspace) RemoveWindow(id WindowID) { | |||||
| 463 | } | 467 | } | ||
| 464 | c := &w.Columns[i] | 468 | c := &w.Columns[i] | ||
| 465 | c.Windows = append(c.Windows[:j], c.Windows[j+1:]...) | 469 | c.Windows = append(c.Windows[:j], c.Windows[j+1:]...) | ||
| 470 | + | c.Shares = nil | |||
| 466 | if len(c.Windows) == 0 { | 471 | if len(c.Windows) == 0 { | ||
| 467 | w.Columns = append(w.Columns[:i], w.Columns[i+1:]...) | 472 | w.Columns = append(w.Columns[:i], w.Columns[i+1:]...) | ||
| 468 | if len(w.Columns) == 0 { | 473 | if len(w.Columns) == 0 { | ||
| @@ -688,6 +693,9 @@ func (w *Workspace) MoveWindow(dir int) { | |||||
| 688 | w.dropSlotOf(c.Windows[0]) | 693 | w.dropSlotOf(c.Windows[0]) | ||
| 689 | } | 694 | } | ||
| 690 | c.Windows[c.Focus], c.Windows[to] = c.Windows[to], c.Windows[c.Focus] | 695 | c.Windows[c.Focus], c.Windows[to] = c.Windows[to], c.Windows[c.Focus] | ||
| 696 | + | if validShares(*c) { | |||
| 697 | + | c.Shares[c.Focus], c.Shares[to] = c.Shares[to], c.Shares[c.Focus] | |||
| 698 | + | } | |||
| 691 | c.Focus = to | 699 | c.Focus = to | ||
| 692 | w.scroll() | 700 | w.scroll() | ||
| 693 | } | 701 | } | ||
| @@ -711,6 +719,80 @@ func (w *Workspace) ResizeColumn(pct int) { | |||||
| 711 | w.scroll() | 719 | w.scroll() | ||
| 712 | } | 720 | } | ||
| 713 | 721 | ||||
| 722 | + | // minShare is the smallest row height set-window-height leaves, percent. | |||
| 723 | + | const minShare = 10 | |||
| 724 | + | ||||
| 725 | + | // ResizeRow changes the focused window's height share by pct percent; the | |||
| 726 | + | // other rows of its column give or take it in proportion to their shares. | |||
| 727 | + | // Every row keeps at least minShare percent. | |||
| 728 | + | func (w *Workspace) ResizeRow(pct int) { | |||
| 729 | + | if w.onFloat() || len(w.Columns) == 0 || w.fullscreenHides() { | |||
| 730 | + | return | |||
| 731 | + | } | |||
| 732 | + | c := &w.Columns[w.Focus] | |||
| 733 | + | n := len(c.Windows) | |||
| 734 | + | if n < 2 || n*minShare > 100 { | |||
| 735 | + | return | |||
| 736 | + | } | |||
| 737 | + | shares := c.Shares | |||
| 738 | + | if !validShares(*c) { | |||
| 739 | + | shares = make([]int, n) | |||
| 740 | + | for i := range shares { | |||
| 741 | + | shares[i] = 100 / n | |||
| 742 | + | } | |||
| 743 | + | shares[n-1] += 100 % n | |||
| 744 | + | } else { | |||
| 745 | + | shares = slices.Clone(shares) | |||
| 746 | + | } | |||
| 747 | + | f := c.Focus | |||
| 748 | + | target := min(max(shares[f]+pct, minShare), 100-minShare*(n-1)) | |||
| 749 | + | delta := target - shares[f] | |||
| 750 | + | if delta == 0 { | |||
| 751 | + | return | |||
| 752 | + | } | |||
| 753 | + | // The others share -delta in proportion to their room: what they have | |||
| 754 | + | // above minShare when shrinking, their share when growing. | |||
| 755 | + | weight := func(v int) int { | |||
| 756 | + | if delta > 0 { | |||
| 757 | + | return v - minShare | |||
| 758 | + | } | |||
| 759 | + | return v | |||
| 760 | + | } | |||
| 761 | + | total := 0 | |||
| 762 | + | for i, v := range shares { | |||
| 763 | + | if i != f { | |||
| 764 | + | total += weight(v) | |||
| 765 | + | } | |||
| 766 | + | } | |||
| 767 | + | left := delta | |||
| 768 | + | for i, v := range shares { | |||
| 769 | + | if i == f { | |||
| 770 | + | continue | |||
| 771 | + | } | |||
| 772 | + | part := 0 | |||
| 773 | + | if total > 0 { | |||
| 774 | + | part = delta * weight(v) / total | |||
| 775 | + | } | |||
| 776 | + | shares[i] = v - part | |||
| 777 | + | left -= part | |||
| 778 | + | } | |||
| 779 | + | shares[f] = target | |||
| 780 | + | // Rounding: the remainder goes to the rows that still have room, last | |||
| 781 | + | // first. | |||
| 782 | + | for i := n - 1; left != 0 && i >= 0; i-- { | |||
| 783 | + | if i == f { | |||
| 784 | + | continue | |||
| 785 | + | } | |||
| 786 | + | step := left | |||
| 787 | + | if left > 0 { | |||
| 788 | + | step = min(left, shares[i]-minShare) | |||
| 789 | + | } | |||
| 790 | + | shares[i] -= step | |||
| 791 | + | left -= step | |||
| 792 | + | } | |||
| 793 | + | c.Shares = shares | |||
| 794 | + | } | |||
| 795 | + | ||||
| 714 | // takeColumn removes the focused column and returns it, as a normal column. | 796 | // takeColumn removes the focused column and returns it, as a normal column. | ||
| 715 | func (w *Workspace) takeColumn() (Column, bool) { | 797 | func (w *Workspace) takeColumn() (Column, bool) { | ||
| 716 | if len(w.Columns) == 0 || w.onFloat() { | 798 | if len(w.Columns) == 0 || w.onFloat() { | ||
| @@ -1213,6 +1295,46 @@ func stackRects(r Rect, n, gap int) []Rect { | |||||
| 1213 | return rows | 1295 | return rows | ||
| 1214 | } | 1296 | } | ||
| 1215 | 1297 | ||||
| 1298 | + | // rowRects splits column rect r into the rows of c: by c.Shares when they | |||
| 1299 | + | // are valid, else equally (stackRects). | |||
| 1300 | + | func rowRects(r Rect, c Column, gap int) []Rect { | |||
| 1301 | + | n := len(c.Windows) | |||
| 1302 | + | if !validShares(c) { | |||
| 1303 | + | return stackRects(r, n, gap) | |||
| 1304 | + | } | |||
| 1305 | + | rows := make([]Rect, n) | |||
| 1306 | + | avail := max(r.H-(n-1)*gap, 0) | |||
| 1307 | + | yy, bottom, used := r.Y, r.Y+r.H, 0 | |||
| 1308 | + | for i, share := range c.Shares { | |||
| 1309 | + | hh := avail * share / 100 | |||
| 1310 | + | if i == n-1 { | |||
| 1311 | + | hh = avail - used | |||
| 1312 | + | } | |||
| 1313 | + | used += hh | |||
| 1314 | + | yy = min(yy, bottom) | |||
| 1315 | + | hh = min(hh, bottom-yy) | |||
| 1316 | + | rows[i] = Rect{X: r.X, Y: yy, W: r.W, H: hh} | |||
| 1317 | + | yy += hh + gap | |||
| 1318 | + | } | |||
| 1319 | + | return rows | |||
| 1320 | + | } | |||
| 1321 | + | ||||
| 1322 | + | // validShares reports whether c.Shares has one share per window, each at | |||
| 1323 | + | // least 1, summing to 100. | |||
| 1324 | + | func validShares(c Column) bool { | |||
| 1325 | + | if len(c.Shares) != len(c.Windows) || len(c.Shares) < 2 { | |||
| 1326 | + | return false | |||
| 1327 | + | } | |||
| 1328 | + | sum := 0 | |||
| 1329 | + | for _, v := range c.Shares { | |||
| 1330 | + | if v < 1 { | |||
| 1331 | + | return false | |||
| 1332 | + | } | |||
| 1333 | + | sum += v | |||
| 1334 | + | } | |||
| 1335 | + | return sum == 100 | |||
| 1336 | + | } | |||
| 1337 | + | ||||
| 1216 | // split cuts r in two halves with a gap: top/bottom when vertical, else left/right. | 1338 | // split cuts r in two halves with a gap: top/bottom when vertical, else left/right. | ||
| 1217 | func split(r Rect, gap int, vertical bool) (Rect, Rect) { | 1339 | func split(r Rect, gap int, vertical bool) (Rect, Rect) { | ||
| 1218 | if vertical { | 1340 | if vertical { | ||
| @@ -1239,18 +1361,9 @@ func (w *Workspace) Layout() []Placement { | |||||
| 1239 | if fullColumn { | 1361 | if fullColumn { | ||
| 1240 | col.W = w.Output.W | 1362 | col.W = w.Output.W | ||
| 1241 | } | 1363 | } | ||
| 1242 | - | available := max(col.H-(n-1)*gap, 0) | 1364 | + | rows := rowRects(col, c, gap) |
| 1243 | - | height := available / n | |||
| 1244 | - | y := col.Y | |||
| 1245 | - | bottom := col.Y + col.H | |||
| 1246 | for j, id := range c.Windows { | 1365 | for j, id := range c.Windows { | ||
| 1247 | - | h := height | 1366 | + | r := rows[j] |
| 1248 | - | if j == n-1 { | |||
| 1249 | - | h = available - height*(n-1) | |||
| 1250 | - | } | |||
| 1251 | - | y = min(y, bottom) | |||
| 1252 | - | h = min(h, bottom-y) | |||
| 1253 | - | r := Rect{X: col.X, Y: y, W: col.W, H: h} | |||
| 1254 | full := w.fullscreen == id && id != 0 | 1367 | full := w.fullscreen == id && id != 0 | ||
| 1255 | // Fixed overflow cannot scroll to other columns while one fills | 1368 | // Fixed overflow cannot scroll to other columns while one fills | ||
| 1256 | // the view. Keep them in place, but out of the scene. | 1369 | // the view. Keep them in place, but out of the scene. | ||
| @@ -1267,7 +1380,6 @@ func (w *Workspace) Layout() []Placement { | |||||
| 1267 | r = Rect{} | 1380 | r = Rect{} | ||
| 1268 | } | 1381 | } | ||
| 1269 | result = append(result, Placement{ID: id, Rect: r, Fullscreen: full, Focused: id == focusedID, Hidden: hidden}) | 1382 | result = append(result, Placement{ID: id, Rect: r, Fullscreen: full, Focused: id == focusedID, Hidden: hidden}) | ||
| 1270 | - | y += h + gap | |||
| 1271 | } | 1383 | } | ||
| 1272 | } | 1384 | } | ||
| 1273 | setVisibleNeighbors(result, gap, w.Output) | 1385 | setVisibleNeighbors(result, gap, w.Output) | ||
M internal/core/overview.go +1 -1
| @@ -741,7 +741,7 @@ func (w *Workspace) previewTiles() (tiles []Placement, span int, sel Rect) { | |||
| 741 | 741 | } | |
| 742 | 742 | continue | |
| 743 | 743 | } | |
| 744 | - | for j, t := range stackRects(r, len(c.Windows), g) { | |
| 744 | + | for j, t := range rowRects(r, c, g) { | |
| 745 | 745 | tiles = append(tiles, Placement{ID: c.Windows[j], Rect: t, Focused: i == w.Focus && j == c.Focus}) | |
| 746 | 746 | } | |
| 747 | 747 | if w.Overflow != OverflowFixed { | |
| @@ -741,7 +741,7 @@ func (w *Workspace) previewTiles() (tiles []Placement, span int, sel Rect) { | |||||
| 741 | } | 741 | } | ||
| 742 | continue | 742 | continue | ||
| 743 | } | 743 | } | ||
| 744 | - | for j, t := range stackRects(r, len(c.Windows), g) { | 744 | + | for j, t := range rowRects(r, c, g) { |
| 745 | tiles = append(tiles, Placement{ID: c.Windows[j], Rect: t, Focused: i == w.Focus && j == c.Focus}) | 745 | tiles = append(tiles, Placement{ID: c.Windows[j], Rect: t, Focused: i == w.Focus && j == c.Focus}) | ||
| 746 | } | 746 | } | ||
| 747 | if w.Overflow != OverflowFixed { | 747 | if w.Overflow != OverflowFixed { | ||
M internal/core/overview_stack.go +2 -2
| @@ -233,7 +233,7 @@ func (m *Monitor) previewItem(w *Workspace, item stackItem, y int, dim, lit bool | |||
| 233 | 233 | r := rects[i] | |
| 234 | 234 | r.X -= w.Usable.X | |
| 235 | 235 | r.Y -= w.Usable.Y | |
| 236 | - | for j, t := range stackRects(r, len(c.Windows), g) { | |
| 236 | + | for j, t := range rowRects(r, c, g) { | |
| 237 | 237 | tiles = append(tiles, Placement{ID: c.Windows[j], Rect: t, Focused: i == selected && j == c.Focus}) | |
| 238 | 238 | } | |
| 239 | 239 | } | |
| @@ -258,7 +258,7 @@ func (m *Monitor) previewItem(w *Workspace, item stackItem, y int, dim, lit bool | |||
| 258 | 258 | r.X = (w.Usable.W - r.W) / 2 | |
| 259 | 259 | r.Y = (w.Usable.H - r.H) / 2 | |
| 260 | 260 | tiles := make([]Placement, 0, len(c.Windows)) | |
| 261 | - | for j, t := range stackRects(r, len(c.Windows), g) { | |
| 261 | + | for j, t := range rowRects(r, c, g) { | |
| 262 | 262 | tiles = append(tiles, Placement{ID: c.Windows[j], Rect: t, Focused: j == c.Focus}) | |
| 263 | 263 | } | |
| 264 | 264 | return w.previewRowTiles(y, dim, lit, tiles, w.Usable.W, r) | |
| @@ -233,7 +233,7 @@ func (m *Monitor) previewItem(w *Workspace, item stackItem, y int, dim, lit bool | |||||
| 233 | r := rects[i] | 233 | r := rects[i] | ||
| 234 | r.X -= w.Usable.X | 234 | r.X -= w.Usable.X | ||
| 235 | r.Y -= w.Usable.Y | 235 | r.Y -= w.Usable.Y | ||
| 236 | - | for j, t := range stackRects(r, len(c.Windows), g) { | 236 | + | for j, t := range rowRects(r, c, g) { |
| 237 | tiles = append(tiles, Placement{ID: c.Windows[j], Rect: t, Focused: i == selected && j == c.Focus}) | 237 | tiles = append(tiles, Placement{ID: c.Windows[j], Rect: t, Focused: i == selected && j == c.Focus}) | ||
| 238 | } | 238 | } | ||
| 239 | } | 239 | } | ||
| @@ -258,7 +258,7 @@ func (m *Monitor) previewItem(w *Workspace, item stackItem, y int, dim, lit bool | |||||
| 258 | r.X = (w.Usable.W - r.W) / 2 | 258 | r.X = (w.Usable.W - r.W) / 2 | ||
| 259 | r.Y = (w.Usable.H - r.H) / 2 | 259 | r.Y = (w.Usable.H - r.H) / 2 | ||
| 260 | tiles := make([]Placement, 0, len(c.Windows)) | 260 | tiles := make([]Placement, 0, len(c.Windows)) | ||
| 261 | - | for j, t := range stackRects(r, len(c.Windows), g) { | 261 | + | for j, t := range rowRects(r, c, g) { |
| 262 | tiles = append(tiles, Placement{ID: c.Windows[j], Rect: t, Focused: j == c.Focus}) | 262 | tiles = append(tiles, Placement{ID: c.Windows[j], Rect: t, Focused: j == c.Focus}) | ||
| 263 | } | 263 | } | ||
| 264 | return w.previewRowTiles(y, dim, lit, tiles, w.Usable.W, r) | 264 | return w.previewRowTiles(y, dim, lit, tiles, w.Usable.W, r) | ||
A internal/core/row_shares_test.go +195 -0
| @@ -0,0 +1,195 @@ | |||
| 1 | + | package core | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "slices" | |
| 5 | + | "testing" | |
| 6 | + | ) | |
| 7 | + | ||
| 8 | + | func heights(w *Workspace) map[WindowID]int { | |
| 9 | + | out := map[WindowID]int{} | |
| 10 | + | for _, p := range w.Layout() { | |
| 11 | + | if !p.Floating { | |
| 12 | + | out[p.ID] = p.Rect.H | |
| 13 | + | } | |
| 14 | + | } | |
| 15 | + | return out | |
| 16 | + | } | |
| 17 | + | ||
| 18 | + | func sum(v []int) int { | |
| 19 | + | t := 0 | |
| 20 | + | for _, x := range v { | |
| 21 | + | t += x | |
| 22 | + | } | |
| 23 | + | return t | |
| 24 | + | } | |
| 25 | + | ||
| 26 | + | func TestRowRectsEqualWithoutShares(t *testing.T) { | |
| 27 | + | r := Rect{X: 0, Y: 0, W: 50, H: 103} | |
| 28 | + | c := Column{Windows: []WindowID{1, 2, 3}} | |
| 29 | + | if !slices.Equal(rowRects(r, c, 5), stackRects(r, 3, 5)) { | |
| 30 | + | t.Fatal(rowRects(r, c, 5)) | |
| 31 | + | } | |
| 32 | + | c.Shares = []int{50, 50} // wrong length: ignored | |
| 33 | + | if !slices.Equal(rowRects(r, c, 5), stackRects(r, 3, 5)) { | |
| 34 | + | t.Fatal(rowRects(r, c, 5)) | |
| 35 | + | } | |
| 36 | + | c.Shares = []int{50, 30, 30} // wrong sum: ignored | |
| 37 | + | if !slices.Equal(rowRects(r, c, 5), stackRects(r, 3, 5)) { | |
| 38 | + | t.Fatal(rowRects(r, c, 5)) | |
| 39 | + | } | |
| 40 | + | c.Shares = []int{50, 30, 20} | |
| 41 | + | rows := rowRects(r, c, 5) | |
| 42 | + | if rows[0].H != 46 || rows[1].H != 27 || rows[2].H != 93-46-27 || rows[2].Y+rows[2].H != 103 { | |
| 43 | + | t.Fatal(rows) | |
| 44 | + | } | |
| 45 | + | } | |
| 46 | + | ||
| 47 | + | func TestResizeRow(t *testing.T) { | |
| 48 | + | for _, o := range []Overflow{OverflowScroll, OverflowFixed} { | |
| 49 | + | w := workspace() | |
| 50 | + | w.Overflow = o | |
| 51 | + | w.SetOutput(1000, 800) | |
| 52 | + | w.Columns = []Column{{Windows: []WindowID{1, 2}}} | |
| 53 | + | w.Apply("set-window-height +10%") | |
| 54 | + | if !slices.Equal(w.Columns[0].Shares, []int{60, 40}) { | |
| 55 | + | t.Fatal(o, w.Columns[0].Shares) | |
| 56 | + | } | |
| 57 | + | h := heights(w) | |
| 58 | + | if h[1] <= h[2] { | |
| 59 | + | t.Fatal(o, h) | |
| 60 | + | } | |
| 61 | + | for range 10 { | |
| 62 | + | w.Apply("set-window-height +10%") | |
| 63 | + | } | |
| 64 | + | if !slices.Equal(w.Columns[0].Shares, []int{90, 10}) { | |
| 65 | + | t.Fatal(o, w.Columns[0].Shares) | |
| 66 | + | } | |
| 67 | + | for range 10 { | |
| 68 | + | w.Apply("set-window-height -10%") | |
| 69 | + | } | |
| 70 | + | if !slices.Equal(w.Columns[0].Shares, []int{10, 90}) { | |
| 71 | + | t.Fatal(o, w.Columns[0].Shares) | |
| 72 | + | } | |
| 73 | + | } | |
| 74 | + | } | |
| 75 | + | ||
| 76 | + | func TestResizeRowThree(t *testing.T) { | |
| 77 | + | w := workspace() | |
| 78 | + | w.SetOutput(1000, 800) | |
| 79 | + | w.Columns = []Column{{Windows: []WindowID{1, 2, 3}, Focus: 1}} | |
| 80 | + | w.Apply("set-window-height +10%") | |
| 81 | + | s := w.Columns[0].Shares | |
| 82 | + | if sum(s) != 100 || s[1] != 43 || s[0] < 10 || s[2] < 10 { | |
| 83 | + | t.Fatal(s) | |
| 84 | + | } | |
| 85 | + | for _, step := range []string{"+25%", "+25%", "-7%", "-100%", "+100%", "-3%"} { | |
| 86 | + | w.Apply(Action("set-window-height " + step)) | |
| 87 | + | s := w.Columns[0].Shares | |
| 88 | + | if sum(s) != 100 || slices.Min(s) < 10 { | |
| 89 | + | t.Fatal(step, s) | |
| 90 | + | } | |
| 91 | + | } | |
| 92 | + | if s := w.Columns[0].Shares; s[1] != 77 { | |
| 93 | + | t.Fatal(s) | |
| 94 | + | } | |
| 95 | + | // Proportional: the others shrink by their room above the minimum. | |
| 96 | + | w.Columns[0].Shares = []int{50, 20, 30} | |
| 97 | + | w.Columns[0].Focus = 1 | |
| 98 | + | w.Apply("set-window-height +20%") | |
| 99 | + | if s := w.Columns[0].Shares; !slices.Equal(s, []int{37, 40, 23}) { | |
| 100 | + | t.Fatal(s) | |
| 101 | + | } | |
| 102 | + | } | |
| 103 | + | ||
| 104 | + | func TestResizeRowNoops(t *testing.T) { | |
| 105 | + | w := workspace() | |
| 106 | + | w.Columns = []Column{{Windows: []WindowID{1}}} | |
| 107 | + | w.Apply("set-window-height +10%") | |
| 108 | + | if w.Columns[0].Shares != nil { | |
| 109 | + | t.Fatal(w.Columns[0].Shares) | |
| 110 | + | } | |
| 111 | + | w.Columns = []Column{{Windows: []WindowID{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}}} | |
| 112 | + | w.Apply("set-window-height +10%") | |
| 113 | + | if w.Columns[0].Shares != nil { | |
| 114 | + | t.Fatal(w.Columns[0].Shares) | |
| 115 | + | } | |
| 116 | + | w.Columns = []Column{{Windows: []WindowID{1, 2}}} | |
| 117 | + | w.AddFloating(3, 10, 10) | |
| 118 | + | w.Apply("set-window-height +10%") | |
| 119 | + | if w.Columns[0].Shares != nil { | |
| 120 | + | t.Fatal(w.Columns[0].Shares) | |
| 121 | + | } | |
| 122 | + | // A fullscreen window hides its column. | |
| 123 | + | w = workspace() | |
| 124 | + | w.Columns = []Column{{Windows: []WindowID{1, 2}}} | |
| 125 | + | w.SetFullscreen(1, true) | |
| 126 | + | w.Apply("set-window-height +10%") | |
| 127 | + | if w.Columns[0].Shares != nil { | |
| 128 | + | t.Fatal(w.Columns[0].Shares) | |
| 129 | + | } | |
| 130 | + | } | |
| 131 | + | ||
| 132 | + | func TestSharesResetAndTravel(t *testing.T) { | |
| 133 | + | m := monitor() | |
| 134 | + | m.SetOutput(1000, 800) | |
| 135 | + | m.AddWindow(1) | |
| 136 | + | m.AddWindow(2) | |
| 137 | + | w := m.Current() | |
| 138 | + | w.ConsumeOrExpel(-1) | |
| 139 | + | if len(w.Columns) != 1 { | |
| 140 | + | t.Fatal(w.Columns) | |
| 141 | + | } | |
| 142 | + | w.Apply("set-window-height +20%") | |
| 143 | + | if !slices.Equal(w.Columns[0].Shares, []int{30, 70}) { | |
| 144 | + | t.Fatal(w.Columns[0].Shares) | |
| 145 | + | } | |
| 146 | + | // Moving the window inside the column swaps the shares with it. | |
| 147 | + | w.Apply(ActionMoveWindowUp) | |
| 148 | + | if !slices.Equal(w.Columns[0].Shares, []int{70, 30}) || w.Columns[0].Windows[0] != 2 { | |
| 149 | + | t.Fatal(w.Columns[0]) | |
| 150 | + | } | |
| 151 | + | // The column takes its shares to another workspace. | |
| 152 | + | m.MoveToWorkspace(1, true) | |
| 153 | + | m.FocusNumber(2) | |
| 154 | + | if c := m.Current().Columns[0]; !slices.Equal(c.Shares, []int{70, 30}) { | |
| 155 | + | t.Fatal(c) | |
| 156 | + | } | |
| 157 | + | // MoveColumn keeps them. | |
| 158 | + | m.AddWindow(3) | |
| 159 | + | m.Current().FocusColumn(-1) | |
| 160 | + | m.Current().MoveColumn(1) | |
| 161 | + | if c := m.Current().Columns[1]; !slices.Equal(c.Shares, []int{70, 30}) { | |
| 162 | + | t.Fatal(m.Current().Columns) | |
| 163 | + | } | |
| 164 | + | // A window leaving resets the column to equal rows. | |
| 165 | + | m.RemoveWindow(2) | |
| 166 | + | m.AddWindow(4) | |
| 167 | + | w = m.Current() | |
| 168 | + | for _, c := range w.Columns { | |
| 169 | + | if c.Shares != nil { | |
| 170 | + | t.Fatal(w.Columns) | |
| 171 | + | } | |
| 172 | + | } | |
| 173 | + | // A window joining invalidates them: equal rows. | |
| 174 | + | w.Columns = []Column{{Windows: []WindowID{1, 3}, Shares: []int{80, 20}}} | |
| 175 | + | w.Columns[0].Windows = append(w.Columns[0].Windows, 4) | |
| 176 | + | h := heights(w) | |
| 177 | + | if h[1] != h[3] { | |
| 178 | + | t.Fatal(h) | |
| 179 | + | } | |
| 180 | + | } | |
| 181 | + | ||
| 182 | + | func TestOverviewPreviewUsesShares(t *testing.T) { | |
| 183 | + | w := workspace() | |
| 184 | + | w.SetOutput(1000, 800) | |
| 185 | + | w.Columns = []Column{{Windows: []WindowID{1, 2}, Shares: []int{70, 30}}} | |
| 186 | + | tiles, _, _ := w.previewTiles() | |
| 187 | + | h := map[WindowID]int{} | |
| 188 | + | for _, p := range tiles { | |
| 189 | + | h[p.ID] = p.Rect.H | |
| 190 | + | } | |
| 191 | + | live := heights(w) | |
| 192 | + | if h[1] != live[1] || h[2] != live[2] { | |
| 193 | + | t.Fatal(h, live) | |
| 194 | + | } | |
| 195 | + | } | |
| @@ -0,0 +1,195 @@ | |||||
| 1 | + | package core | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "slices" | |||
| 5 | + | "testing" | |||
| 6 | + | ) | |||
| 7 | + | ||||
| 8 | + | func heights(w *Workspace) map[WindowID]int { | |||
| 9 | + | out := map[WindowID]int{} | |||
| 10 | + | for _, p := range w.Layout() { | |||
| 11 | + | if !p.Floating { | |||
| 12 | + | out[p.ID] = p.Rect.H | |||
| 13 | + | } | |||
| 14 | + | } | |||
| 15 | + | return out | |||
| 16 | + | } | |||
| 17 | + | ||||
| 18 | + | func sum(v []int) int { | |||
| 19 | + | t := 0 | |||
| 20 | + | for _, x := range v { | |||
| 21 | + | t += x | |||
| 22 | + | } | |||
| 23 | + | return t | |||
| 24 | + | } | |||
| 25 | + | ||||
| 26 | + | func TestRowRectsEqualWithoutShares(t *testing.T) { | |||
| 27 | + | r := Rect{X: 0, Y: 0, W: 50, H: 103} | |||
| 28 | + | c := Column{Windows: []WindowID{1, 2, 3}} | |||
| 29 | + | if !slices.Equal(rowRects(r, c, 5), stackRects(r, 3, 5)) { | |||
| 30 | + | t.Fatal(rowRects(r, c, 5)) | |||
| 31 | + | } | |||
| 32 | + | c.Shares = []int{50, 50} // wrong length: ignored | |||
| 33 | + | if !slices.Equal(rowRects(r, c, 5), stackRects(r, 3, 5)) { | |||
| 34 | + | t.Fatal(rowRects(r, c, 5)) | |||
| 35 | + | } | |||
| 36 | + | c.Shares = []int{50, 30, 30} // wrong sum: ignored | |||
| 37 | + | if !slices.Equal(rowRects(r, c, 5), stackRects(r, 3, 5)) { | |||
| 38 | + | t.Fatal(rowRects(r, c, 5)) | |||
| 39 | + | } | |||
| 40 | + | c.Shares = []int{50, 30, 20} | |||
| 41 | + | rows := rowRects(r, c, 5) | |||
| 42 | + | if rows[0].H != 46 || rows[1].H != 27 || rows[2].H != 93-46-27 || rows[2].Y+rows[2].H != 103 { | |||
| 43 | + | t.Fatal(rows) | |||
| 44 | + | } | |||
| 45 | + | } | |||
| 46 | + | ||||
| 47 | + | func TestResizeRow(t *testing.T) { | |||
| 48 | + | for _, o := range []Overflow{OverflowScroll, OverflowFixed} { | |||
| 49 | + | w := workspace() | |||
| 50 | + | w.Overflow = o | |||
| 51 | + | w.SetOutput(1000, 800) | |||
| 52 | + | w.Columns = []Column{{Windows: []WindowID{1, 2}}} | |||
| 53 | + | w.Apply("set-window-height +10%") | |||
| 54 | + | if !slices.Equal(w.Columns[0].Shares, []int{60, 40}) { | |||
| 55 | + | t.Fatal(o, w.Columns[0].Shares) | |||
| 56 | + | } | |||
| 57 | + | h := heights(w) | |||
| 58 | + | if h[1] <= h[2] { | |||
| 59 | + | t.Fatal(o, h) | |||
| 60 | + | } | |||
| 61 | + | for range 10 { | |||
| 62 | + | w.Apply("set-window-height +10%") | |||
| 63 | + | } | |||
| 64 | + | if !slices.Equal(w.Columns[0].Shares, []int{90, 10}) { | |||
| 65 | + | t.Fatal(o, w.Columns[0].Shares) | |||
| 66 | + | } | |||
| 67 | + | for range 10 { | |||
| 68 | + | w.Apply("set-window-height -10%") | |||
| 69 | + | } | |||
| 70 | + | if !slices.Equal(w.Columns[0].Shares, []int{10, 90}) { | |||
| 71 | + | t.Fatal(o, w.Columns[0].Shares) | |||
| 72 | + | } | |||
| 73 | + | } | |||
| 74 | + | } | |||
| 75 | + | ||||
| 76 | + | func TestResizeRowThree(t *testing.T) { | |||
| 77 | + | w := workspace() | |||
| 78 | + | w.SetOutput(1000, 800) | |||
| 79 | + | w.Columns = []Column{{Windows: []WindowID{1, 2, 3}, Focus: 1}} | |||
| 80 | + | w.Apply("set-window-height +10%") | |||
| 81 | + | s := w.Columns[0].Shares | |||
| 82 | + | if sum(s) != 100 || s[1] != 43 || s[0] < 10 || s[2] < 10 { | |||
| 83 | + | t.Fatal(s) | |||
| 84 | + | } | |||
| 85 | + | for _, step := range []string{"+25%", "+25%", "-7%", "-100%", "+100%", "-3%"} { | |||
| 86 | + | w.Apply(Action("set-window-height " + step)) | |||
| 87 | + | s := w.Columns[0].Shares | |||
| 88 | + | if sum(s) != 100 || slices.Min(s) < 10 { | |||
| 89 | + | t.Fatal(step, s) | |||
| 90 | + | } | |||
| 91 | + | } | |||
| 92 | + | if s := w.Columns[0].Shares; s[1] != 77 { | |||
| 93 | + | t.Fatal(s) | |||
| 94 | + | } | |||
| 95 | + | // Proportional: the others shrink by their room above the minimum. | |||
| 96 | + | w.Columns[0].Shares = []int{50, 20, 30} | |||
| 97 | + | w.Columns[0].Focus = 1 | |||
| 98 | + | w.Apply("set-window-height +20%") | |||
| 99 | + | if s := w.Columns[0].Shares; !slices.Equal(s, []int{37, 40, 23}) { | |||
| 100 | + | t.Fatal(s) | |||
| 101 | + | } | |||
| 102 | + | } | |||
| 103 | + | ||||
| 104 | + | func TestResizeRowNoops(t *testing.T) { | |||
| 105 | + | w := workspace() | |||
| 106 | + | w.Columns = []Column{{Windows: []WindowID{1}}} | |||
| 107 | + | w.Apply("set-window-height +10%") | |||
| 108 | + | if w.Columns[0].Shares != nil { | |||
| 109 | + | t.Fatal(w.Columns[0].Shares) | |||
| 110 | + | } | |||
| 111 | + | w.Columns = []Column{{Windows: []WindowID{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}}} | |||
| 112 | + | w.Apply("set-window-height +10%") | |||
| 113 | + | if w.Columns[0].Shares != nil { | |||
| 114 | + | t.Fatal(w.Columns[0].Shares) | |||
| 115 | + | } | |||
| 116 | + | w.Columns = []Column{{Windows: []WindowID{1, 2}}} | |||
| 117 | + | w.AddFloating(3, 10, 10) | |||
| 118 | + | w.Apply("set-window-height +10%") | |||
| 119 | + | if w.Columns[0].Shares != nil { | |||
| 120 | + | t.Fatal(w.Columns[0].Shares) | |||
| 121 | + | } | |||
| 122 | + | // A fullscreen window hides its column. | |||
| 123 | + | w = workspace() | |||
| 124 | + | w.Columns = []Column{{Windows: []WindowID{1, 2}}} | |||
| 125 | + | w.SetFullscreen(1, true) | |||
| 126 | + | w.Apply("set-window-height +10%") | |||
| 127 | + | if w.Columns[0].Shares != nil { | |||
| 128 | + | t.Fatal(w.Columns[0].Shares) | |||
| 129 | + | } | |||
| 130 | + | } | |||
| 131 | + | ||||
| 132 | + | func TestSharesResetAndTravel(t *testing.T) { | |||
| 133 | + | m := monitor() | |||
| 134 | + | m.SetOutput(1000, 800) | |||
| 135 | + | m.AddWindow(1) | |||
| 136 | + | m.AddWindow(2) | |||
| 137 | + | w := m.Current() | |||
| 138 | + | w.ConsumeOrExpel(-1) | |||
| 139 | + | if len(w.Columns) != 1 { | |||
| 140 | + | t.Fatal(w.Columns) | |||
| 141 | + | } | |||
| 142 | + | w.Apply("set-window-height +20%") | |||
| 143 | + | if !slices.Equal(w.Columns[0].Shares, []int{30, 70}) { | |||
| 144 | + | t.Fatal(w.Columns[0].Shares) | |||
| 145 | + | } | |||
| 146 | + | // Moving the window inside the column swaps the shares with it. | |||
| 147 | + | w.Apply(ActionMoveWindowUp) | |||
| 148 | + | if !slices.Equal(w.Columns[0].Shares, []int{70, 30}) || w.Columns[0].Windows[0] != 2 { | |||
| 149 | + | t.Fatal(w.Columns[0]) | |||
| 150 | + | } | |||
| 151 | + | // The column takes its shares to another workspace. | |||
| 152 | + | m.MoveToWorkspace(1, true) | |||
| 153 | + | m.FocusNumber(2) | |||
| 154 | + | if c := m.Current().Columns[0]; !slices.Equal(c.Shares, []int{70, 30}) { | |||
| 155 | + | t.Fatal(c) | |||
| 156 | + | } | |||
| 157 | + | // MoveColumn keeps them. | |||
| 158 | + | m.AddWindow(3) | |||
| 159 | + | m.Current().FocusColumn(-1) | |||
| 160 | + | m.Current().MoveColumn(1) | |||
| 161 | + | if c := m.Current().Columns[1]; !slices.Equal(c.Shares, []int{70, 30}) { | |||
| 162 | + | t.Fatal(m.Current().Columns) | |||
| 163 | + | } | |||
| 164 | + | // A window leaving resets the column to equal rows. | |||
| 165 | + | m.RemoveWindow(2) | |||
| 166 | + | m.AddWindow(4) | |||
| 167 | + | w = m.Current() | |||
| 168 | + | for _, c := range w.Columns { | |||
| 169 | + | if c.Shares != nil { | |||
| 170 | + | t.Fatal(w.Columns) | |||
| 171 | + | } | |||
| 172 | + | } | |||
| 173 | + | // A window joining invalidates them: equal rows. | |||
| 174 | + | w.Columns = []Column{{Windows: []WindowID{1, 3}, Shares: []int{80, 20}}} | |||
| 175 | + | w.Columns[0].Windows = append(w.Columns[0].Windows, 4) | |||
| 176 | + | h := heights(w) | |||
| 177 | + | if h[1] != h[3] { | |||
| 178 | + | t.Fatal(h) | |||
| 179 | + | } | |||
| 180 | + | } | |||
| 181 | + | ||||
| 182 | + | func TestOverviewPreviewUsesShares(t *testing.T) { | |||
| 183 | + | w := workspace() | |||
| 184 | + | w.SetOutput(1000, 800) | |||
| 185 | + | w.Columns = []Column{{Windows: []WindowID{1, 2}, Shares: []int{70, 30}}} | |||
| 186 | + | tiles, _, _ := w.previewTiles() | |||
| 187 | + | h := map[WindowID]int{} | |||
| 188 | + | for _, p := range tiles { | |||
| 189 | + | h[p.ID] = p.Rect.H | |||
| 190 | + | } | |||
| 191 | + | live := heights(w) | |||
| 192 | + | if h[1] != live[1] || h[2] != live[2] { | |||
| 193 | + | t.Fatal(h, live) | |||
| 194 | + | } | |||
| 195 | + | } | |||