1package core
2
3import (
4 "math"
5 "slices"
6 "time"
7)
8
9// Slides move what is on screen smoothly: a workspace's columns (view), its
10// stash (stashView) and the monitor between workspaces (switchView). All
11// are pure presentation over the settled state (View, Focus, stashAt,
12// Active): the layout ends where the state says as soon as a slide stops.
13
14// slide is a view offset that a swipe sets while the fingers follow it, and
15// a spring then takes to 0 (off: pixels for the columns, windows for the
16// stash, workspaces for the monitor). Pure presentation over settled state.
17type slide struct {
18 off float64
19 motion motion
20}
21
22// stop lands the slide where its state says.
23func (s *slide) stop() { *s = slide{} }
24
25// step moves a running spring to now and stops it once it settles.
26func (s *slide) step(now time.Time) {
27 if !s.motion.on {
28 return
29 }
30 v, done := s.motion.at(now)
31 s.off = v
32 if done {
33 s.stop()
34 }
35}
36
37// busy reports whether the slide is off its state or its spring runs.
38func (s *slide) busy() bool { return s.motion.on || s.off != 0 }
39
40// slidable reports whether a swipe can scroll the columns: scroll or cascade
41// overflow, with the focus on the columns and no fullscreen window.
42func (w *Workspace) slidable() bool {
43 return w.Overflow != OverflowFixed && len(w.Columns) > 0 && w.fullscreen == 0 && !w.onFloat()
44}
45
46// retarget keeps the view where it is on screen when View moved from
47// before during a landing slide: the slide heads for the new view.
48// Without a landing slide the view moves at once, as it always has.
49func (w *Workspace) retarget(before int) {
50 if w.View == before || !w.view.motion.on {
51 return
52 }
53 w.view.off += float64(before - w.View)
54 w.view.motion = newMotion(viewSpring(w.view.off, w.view.motion.velocity()), time.Time{}, w.view.motion.slow)
55}
56
57func (w *Workspace) shiftPixels() int { return int(math.Round(w.view.off)) }
58
59// switchSpring is the workspace slide's spring from off: it snaps once it
60// is under half a logical pixel of the output span along the switch axis
61// from its target (slideLayout rounds the offset times that span).
62func (m *Monitor) switchSpring(off, velocity float64) spring {
63 s := workspaceSpring(off, velocity)
64 if h := m.switchAxis.span(m.template.Output); h > 0 {
65 s.Snap = pixelSnap / float64(h)
66 }
67 return s
68}
69
70// swipeScale turns touchpad distance into pixels: viewSwipeMovement
71// scrolls one usable width.
72func (w *Workspace) swipeScale() float64 {
73 return float64(w.policy().content.span(w.Usable)) / viewSwipeMovement
74}
75
76// snapPoints are the sorted views that align a column edge with the usable
77// area, never past the first or last column. Points closer than a
78// snapSpacing of the usable width merge into the first: a swipe step
79// always moves the view visibly.
80func (w *Workspace) snapPoints() []float64 {
81 if w.policy().wraps {
82 return w.cascadePoints()
83 }
84 g := w.gap()
85 minX, maxX := w.Usable.X+g, w.Usable.X+w.Usable.W-g
86 last := len(w.Columns) - 1
87 lo := w.columnX(0) - minX
88 hi := max(w.columnX(last)+w.columnWidth(last)-maxX, lo)
89 views := make([]int, 0, 2*len(w.Columns))
90 for i := range w.Columns {
91 views = append(views, min(max(w.columnX(i)-minX, lo), hi), min(max(w.columnX(i)+w.columnWidth(i)-maxX, lo), hi))
92 }
93 slices.Sort(views)
94 minStep := float64(w.Usable.W) * snapSpacing
95 points := []float64{float64(lo)}
96 for _, v := range views {
97 if float64(v)-points[len(points)-1] >= minStep {
98 points = append(points, float64(v))
99 }
100 }
101 // The last column's end stays a point; a point close before it goes.
102 if n := len(points); float64(hi) != points[n-1] {
103 if n > 1 && float64(hi)-points[n-1] < minStep {
104 points = points[:n-1]
105 }
106 points = append(points, float64(hi))
107 }
108 return points
109}
110
111// snapSpacing is the smallest distance between column snap points, in
112// usable widths.
113const snapSpacing = 0.1
114
115// snapFocus is the column to focus at view: the current one while it stays
116// fully shown at the current view, else the furthest fully shown column
117// toward the swipe (forward is rightward).
118func (w *Workspace) snapFocus(view int, forward bool) (focus int) {
119 if w.policy().wraps {
120 return w.cascadeFocus(view)
121 }
122 g := w.gap()
123 minX, maxX := w.Usable.X+g, w.Usable.X+w.Usable.W-g
124 full := func(i int) bool {
125 x := w.columnX(i) - view
126 return x >= minX && x+w.columnWidth(i) <= maxX
127 }
128 if full(w.Focus) && view == w.View {
129 return w.Focus
130 }
131 focus = -1
132 for i := range w.Columns {
133 if full(i) && (focus < 0 || forward) {
134 focus = i
135 }
136 }
137 if focus < 0 {
138 // Columns wider than the view: the one at the left edge.
139 focus = 0
140 for i := range w.Columns {
141 if w.columnX(i)-view <= minX {
142 focus = i
143 }
144 }
145 }
146 return focus
147}
148
149// stopSwitch lands the workspace slide and forgets the list it measured in.
150func (m *Monitor) stopSwitch() { m.switchView.stop(); m.switchList = nil }
151
152// framedSwitch keeps transitions involving a sized workspace settled. A
153// monitor-wide animation has no per-workspace crop; it must not expose
154// columns outside their viewport while sliding between different frames.
155func (m *Monitor) framedSwitch() bool {
156 if m.Current().Output != m.Output() {
157 return true
158 }
159 list := m.Workspaces
160 if m.switchList != nil {
161 list = m.switchList
162 }
163 base := indexOf(list, m.Current())
164 if base < 0 {
165 return false
166 }
167 pos := float64(base) + m.switchView.off
168 for _, i := range [2]int{int(math.Floor(pos)), int(math.Ceil(pos))} {
169 if i >= 0 && i < len(list) && m.has(list[i]) && list[i].Output != m.Output() {
170 return true
171 }
172 }
173 return false
174}
175
176// slideLayout adds the workspaces a workspace slide shows next to the
177// current one, moved by their distance, to the current layout (moved too).
178// A landing slide places them as the list was when the swipe began
179// (switchList): an empty workspace dropped since shows as background.
180func (m *Monitor) slideLayout(cur []Placement) []Placement {
181 if m.switchView.off == 0 || m.shown != nil || m.framedSwitch() {
182 return cur
183 }
184 list := m.Workspaces
185 if m.switchList != nil {
186 list = m.switchList
187 }
188 base := indexOf(list, m.Current())
189 if base < 0 {
190 return cur
191 }
192 h := float64(m.switchAxis.span(m.template.Output))
193 pos := float64(base) + m.switchView.off
194 offset := func(p []Placement, k int) []Placement {
195 d := int(math.Round((float64(k) - pos) * h))
196 for i := range p {
197 if !p[i].Hidden {
198 m.switchAxis.offset(&p[i].Rect, d)
199 }
200 }
201 return p
202 }
203 cur = offset(cur, base)
204 for _, k := range []int{int(math.Floor(pos)), int(math.Ceil(pos))} {
205 if k == base || k < 0 || k >= len(list) || !m.has(list[k]) {
206 continue
207 }
208 next := list[k].layoutInto(m.slideBuf)
209 m.slideBuf = next
210 for i := range next {
211 next[i].Focused = false
212 }
213 // Windows of the neighbor are already listed hidden: replace them.
214 if m.slideShown == nil {
215 m.slideShown = map[WindowID]Placement{}
216 }
217 shown := m.slideShown
218 clear(shown)
219 for _, p := range offset(next, k) {
220 shown[p.ID] = p
221 }
222 for i, p := range cur {
223 if v, ok := shown[p.ID]; ok {
224 cur[i] = v
225 }
226 }
227 }
228 return cur
229}