1package core
2
3import (
4 "cmp"
5 "math"
6 "slices"
7
8 "github.com/bnema/neferwl/internal/ports"
9)
10
11// The stash is a workspace's strip of windows set aside with
12// toggle-window-stash, left to right in arrival order. The selected one
13// is centred, stash.width% of the usable width and stashHeight% of its
14// height; its neighbors fill the margins beside it, stash.gap% away,
15// the sides, dimmed; the others wait off screen. toggle-stash-visible
16// hides and shows the whole strip. Native floats (dialogs) are not in it.
17
18// stashWidth is the default share of the usable width, in percent, a
19// stashed window takes (stash.width); stashHeight is its share of the
20// usable height.
21const stashWidth, stashHeight = 80, 80
22
23func (w *Workspace) stashIndex(id WindowID) int {
24 return slices.IndexFunc(w.Stash, func(f Float) bool { return f.ID == id })
25}
26
27// isFloat reports whether the window floats: native or stashed.
28func (w *Workspace) isFloat(id WindowID) bool {
29 return w.floatIndex(id) >= 0 || w.stashIndex(id) >= 0
30}
31
32// stashFocused reports whether the stash has the focus, under any native
33// float that has it. A shown stash without tiles always has it.
34func (w *Workspace) stashFocused() bool {
35 return (w.stashFocus || len(w.Columns) == 0) && !w.stashHidden && len(w.Stash) > 0
36}
37
38// onFloat reports whether the focus is on a floating window: column
39// actions do nothing then.
40func (w *Workspace) onFloat() bool { return w.floatFocus || w.stashFocused() }
41
42// stashWindow moves the focused tile id to the end of the stash,
43// remembering its place in its column to return there.
44func (w *Workspace) stashWindow(id WindowID) {
45 back := w.tilePlace(id)
46 w.RemoveWindow(id)
47 w.addStash(Float{ID: id, back: back})
48}
49
50// tilePlace is where the focused tile id is in its column, for restore.
51func (w *Workspace) tilePlace(id WindowID) *origPlace {
52 c := w.Columns[w.Focus]
53 lone := len(c.Windows) == 1
54 back := &origPlace{col: w.Focus, row: c.Focus, slot: c.Slot, width: c.Width, fullWidth: c.FullWidth && lone, expanded: c.Expanded && lone}
55 for _, v := range c.Windows {
56 if v != id {
57 back.stacked = append(back.stacked, v)
58 }
59 }
60 return back
61}
62
63// addStash appends a window to the stash, shows the stash and selects it.
64func (w *Workspace) addStash(f Float) {
65 w.Stash = append(w.Stash, f)
66 w.stashAt = len(w.Stash) - 1
67 w.showStash()
68}
69
70// captureStash puts a new window id in the shown stash, selected, and
71// reports whether it did. Like a new tile, it leaves a focused native
72// float focused. Under a covering fullscreen window the stash is not on
73// screen: the window tiles as usual.
74func (w *Workspace) captureStash(id WindowID) bool {
75 if id == 0 || w.stashHidden || len(w.Stash) == 0 || w.cover() != 0 {
76 return false
77 }
78 // No former column: toggle-window-stash tiles it as a new column.
79 w.Stash = append(w.Stash, Float{ID: id})
80 w.stashAt, w.stashFocus = len(w.Stash)-1, true
81 return true
82}
83
84// showStash shows the stash and gives it the focus; a member whose
85// fullscreen waited while hidden is fullscreen again, selected, as if it
86// asked now.
87func (w *Workspace) showStash() {
88 w.stashHidden, w.stashFocus, w.floatFocus = false, true, false
89 if i := w.stashIndex(w.hiddenFullscreen); i >= 0 {
90 w.fullscreen, w.stashAt = w.hiddenFullscreen, i
91 }
92 w.hiddenFullscreen = 0
93}
94
95// unstash returns stash entry i to its former column, focused; a free
96// float floats again where it was.
97func (w *Workspace) unstash(i int) {
98 f := w.Stash[i]
99 w.RemoveWindow(f.ID)
100 if f.free {
101 w.Floats = append(w.Floats, f)
102 w.floatFocus, w.stashFocus = true, false
103 return
104 }
105 w.restore(f.ID, f.back)
106}
107
108// holdsStack reports whether c holds a window of the stack left behind.
109func (p origPlace) holdsStack(c Column) bool {
110 return slices.ContainsFunc(c.Windows, func(v WindowID) bool { return slices.Contains(p.stacked, v) })
111}
112
113// restore tiles id back where back says: its row in the column that still
114// holds its former neighbors, else a column of its own at its former
115// index. It takes the focus.
116func (w *Workspace) restore(id WindowID, back *origPlace) {
117 w.dropStashOver()
118 w.floatFocus, w.stashFocus = false, false
119 w.raiseColumns()
120 if back == nil {
121 w.addColumn(Column{Windows: []WindowID{id}})
122 return
123 }
124 at := min(back.col, len(w.Columns))
125 if i := slices.IndexFunc(w.Columns, back.holdsStack); i >= 0 {
126 c := &w.Columns[i]
127 row := 0
128 for _, v := range back.stacked[:back.row] {
129 if slices.Contains(c.Windows, v) {
130 row++
131 }
132 }
133 c.Windows = slices.Insert(c.Windows, row, id)
134 c.Focus = row
135 if w.policy().equalCells && w.Focus != i {
136 w.unmaximize()
137 }
138 w.Focus = i
139 w.scroll()
140 return
141 }
142 // Its column is gone: it comes back expanded, unless another column
143 // was expanded meanwhile.
144 expanded := back.expanded && !slices.ContainsFunc(w.Columns, func(c Column) bool { return c.Expanded })
145 w.insertColumn(at, Column{Windows: []WindowID{id}, Width: back.width, Slot: back.slot, Expanded: expanded})
146 if back.fullWidth {
147 w.maximize(w.Focus)
148 }
149}
150
151// rehome is a stashed window moved to another workspace: its former column
152// is not there, so it returns as the last column, at its width.
153func (f Float) rehome() Float {
154 back := &origPlace{col: math.MaxInt}
155 if f.back != nil {
156 back.width = f.back.width
157 }
158 f.back = back
159 return f
160}
161
162// ToggleStashVisible hides the stash, or shows it and gives it the
163// focus. Native floats stay as they are. Over a covering fullscreen
164// window it is a user override: the stash shows over it, which stays
165// fullscreen behind (composed, no scanout) and returns when the stash
166// hides. A stashed fullscreen window is the stash on screen: no-op.
167func (w *Workspace) ToggleStashVisible() {
168 if len(w.Stash) == 0 {
169 return
170 }
171 if c := w.cover(); c != 0 {
172 if w.stashIndex(c) >= 0 {
173 return
174 }
175 if w.stashOverCover() {
176 w.dropStashOver()
177 return
178 }
179 // Not showStash: a pending stashed fullscreen must not replace
180 // the covering window.
181 w.stashOver, w.stashHidden, w.stashFocus, w.floatFocus = c, false, true, false
182 return
183 }
184 if w.stashHidden {
185 w.showStash()
186 return
187 }
188 w.stashHidden, w.stashFocus = true, false
189}
190
191// stashOverCover reports whether the stash shows over the covering
192// fullscreen window (ToggleStashVisible).
193func (w *Workspace) stashOverCover() bool {
194 return w.stashOver != 0 && w.stashOver == w.cover() && !w.stashHidden && len(w.Stash) > 0
195}
196
197// stashShown reports whether the stash is on screen: it holds windows, is
198// not hidden, and no covering fullscreen window hides it.
199func (w *Workspace) stashShown() bool {
200 return len(w.Stash) > 0 && !w.stashHidden && (w.cover() == 0 || w.stashOverCover())
201}
202
203// dropStashOver ends the stash override when the focus leaves it: the
204// stash hides again under the covering window, as it was.
205func (w *Workspace) dropStashOver() {
206 if w.stashOverCover() {
207 w.stashHidden, w.stashFocus = true, false
208 }
209 w.stashOver = 0
210}
211
212// Click focuses the clicked window. A click on a tile behind the shown
213// stash hides the stash, as a click outside a menu closes it.
214func (w *Workspace) Click(id WindowID) {
215 if !w.stashHidden && len(w.Stash) > 0 && !w.isFloat(id) && w.cover() == 0 {
216 w.stashHidden = true
217 }
218 w.FocusID(id)
219}
220
221// stashRect is where the selected stashed window is, border included.
222func (w *Workspace) stashRect() Rect {
223 u := w.Usable
224 fw := min(max(u.W*cmp.Or(w.stashWidth, stashWidth)/100-2*w.border, 0)+2*w.border, u.W)
225 fh := min(max(u.H*stashHeight/100-2*w.border, 0)+2*w.border, u.H)
226 return Rect{X: u.X + (u.W-fw)/2, Y: u.Y + (u.H-fh)/2, W: fw, H: fh}
227}
228
229// appendStash appends the placements of the stash to out: the selected window centred, its
230// neighbors gap% of the usable width away, showing what the margins
231// leave of them, the others hidden. cover is the covering fullscreen
232// window, if any.
233func (w *Workspace) appendStash(out []Placement, focusedID, cover WindowID) []Placement {
234 u := w.Usable
235 center := w.stashRect()
236 fw, fh := center.W, center.H
237 // Neighbors rest in the margins, never over the selected window: the
238 // side with the least room says whether there is one.
239 margin := (u.W-fw)/2 - w.stashGapPx()
240 // The view is moved by stashView.off pitches, rounded once so every
241 // window shares the offset.
242 pitch := w.stashPitch(fw)
243 shift := int(math.Round(w.stashView.off * float64(pitch)))
244 for i, f := range w.Stash {
245 p := Placement{ID: f.ID, Floating: true, Focused: f.ID == focusedID, Inset: ports.SideAll}
246 d := i - w.stashAt
247 r := Rect{X: center.X + d*pitch - shift, Y: center.Y, W: fw, H: fh}
248 switch {
249 case f.ID == cover:
250 p.Rect, p.Fullscreen, p.Inset = w.Output, true, 0
251 case w.stashHidden || cover != 0 && !w.stashOverCover():
252 p.Hidden = true
253 case d == 0:
254 // The selection is no peek, but its veil grows as it slides.
255 p.Rect, p.Veil = r, w.stashVeil(d)
256 case (shift != 0 || margin > 0 && (d == -1 || d == 1)) && r.Overlaps(u):
257 // A neighbor shows in its margin, if it leaves one. A slide
258 // shows what the view moves in, and keeps what it moves out
259 // until it is gone.
260 p.Rect = r
261 p.peeking(w.stashVeil(d))
262 default:
263 p.Hidden = true
264 }
265 out = append(out, p)
266 }
267 return out
268}
269
270// stashGapPx is the space between the selected stash window and its
271// neighbors, in pixels.
272func (w *Workspace) stashGapPx() int { return w.Usable.W * w.stashGap / 100 }
273
274// stashPitch is the distance between two stash windows of width fw.
275func (w *Workspace) stashPitch(fw int) int { return max(fw+w.stashGapPx(), 1) }
276
277// stashSpring lands a stash slide from off (in stash windows) with the
278// speed velocity; it snaps under half a pixel like the other slides.
279func (w *Workspace) stashSpring(off, velocity float64) spring {
280 s := workspaceSpring(off, velocity)
281 s.Snap = pixelSnap / float64(w.stashPitch(w.stashRect().W))
282 return s
283}
284
285// stashVeil is how much of the peek veil the stash window d places away
286// from stashAt draws: none at the centre of the view, all of it one window
287// away or more.
288func (w *Workspace) stashVeil(d int) float64 {
289 return min(math.Abs(float64(d)-w.stashView.off), 1)
290}