expel.go RawHistoryFolder
1package core
2
3import "slices"
4
5// Consume or expel (ADR 016): Cmd+[ and Cmd+] move the focused tiled window
6// one column left or right. Alone in its column it joins the bottom of the
7// neighbor column (consume); in a stacked column it leaves for a new column
8// (expel). A full fixed workspace never gets a new column: the window stacks
9// into the neighbor column instead, past the edge on the neighbor monitor.
10//
11// A slot column's slot window is its first window: windows only join a
12// column at the bottom, so the slot follows Windows[0].
13
14// full reports whether a fixed-overflow workspace has no room for a column.
15func (w *Workspace) full() bool {
16 return w.Overflow == OverflowFixed && len(w.Columns) >= max(w.MaxColumns, 1)
17}
18
19// ConsumeOrExpel moves the focused window in direction dir (-1 or 1). It
20// returns false when the window must leave for the neighbor monitor: a full
21// workspace with the window stacked in the edge column.
22func (w *Workspace) ConsumeOrExpel(dir int) bool {
23 if len(w.Columns) == 0 || w.onFloat() || (dir != -1 && dir != 1) {
24 return true
25 }
26 // A fullscreen window hides the others (fixed) or its column (scroll):
27 // moving would break its way back.
28 if w.fullscreenHides() {
29 return true
30 }
31 src := w.Focus
32 col := w.Columns[src]
33 id := col.Windows[col.Focus]
34 next := src + dir
35 edge := next < 0 || next >= len(w.Columns)
36 switch {
37 case len(col.Windows) == 1 && edge:
38 // Consume at the edge: move-column already crosses monitors.
39 case len(col.Windows) == 1:
40 // The column goes away, and a slot with it.
41 w.stackWindow(id, w.Columns[next].Windows[0])
42 case !w.full():
43 c := Column{Windows: []WindowID{id}, Cells: col.Cells}
44 if col.Focus == 0 && col.Slot != 0 {
45 // The slot and its width follow the slot window.
46 c.Slot = col.Slot
47 w.Columns[src].Slot, w.Columns[src].Cells = 0, 0
48 } else if col.Slot != 0 {
49 c.Cells = 0
50 }
51 w.RemoveWindow(id)
52 w.insertColumn(src+max(dir, 0), c)
53 case edge:
54 return false
55 default:
56 w.stackWindow(id, w.Columns[next].Windows[0])
57 }
58 return true
59}
60
61// stack moves tiled window id to the bottom of the column holding target
62// and focuses it. Leaving a slot column's first row releases the slot.
63func (w *Workspace) stackWindow(id, target WindowID) {
64 w.dropSlotOf(id)
65 w.RemoveWindow(id)
66 i := slices.IndexFunc(w.Columns, func(c Column) bool { return slices.Contains(c.Windows, target) })
67 w.Columns[i].Windows = append(w.Columns[i].Windows, id)
68 // Activate leaves a fullscreen of the target column, which would hide it.
69 w.Activate(id)
70}
71
72// dropSlotOf clears the slot of the column whose slot window is id.
73func (w *Workspace) dropSlotOf(id WindowID) {
74 for i := range w.Columns {
75 if w.Columns[i].Slot != 0 && w.Columns[i].Windows[0] == id {
76 w.Columns[i].Slot = 0
77 }
78 }
79}
80
81// takeWindow removes the focused tiled window from its column for another
82// workspace; its slot, if any, is released.
83func (w *Workspace) takeWindow() (WindowID, bool) {
84 id, ok := w.Focused()
85 if !ok || w.onFloat() {
86 return 0, false
87 }
88 w.dropSlotOf(id)
89 w.RemoveWindow(id)
90 return id, true
91}
92
93// expelTo puts a window expelled from the neighbor monitor on the side
94// facing it (dir > 0: it came from the left): as a new edge column, or at
95// the bottom of the edge column when the workspace is full.
96func (w *Workspace) expelTo(id WindowID, dir int) {
97 at := 0
98 if dir < 0 {
99 at = len(w.Columns)
100 }
101 if !w.full() {
102 w.receive(Column{Windows: []WindowID{id}}, at)
103 return
104 }
105 i := min(at, len(w.Columns)-1)
106 w.Columns[i].Windows = append(w.Columns[i].Windows, id)
107 w.Activate(id)
108}