1package core
2
3import (
4 "math"
5 "slices"
6 "time"
7)
8
9// Slides move what is on screen smoothly: a workspace's columns (shift) and
10// the monitor between workspaces (switchOff). Both are pure presentation
11// over the settled state (ViewX, Focus, Active): the layout ends where the
12// state says as soon as a slide stops.
13
14// slidable reports whether a swipe can scroll the columns: only scroll
15// overflow, with the focus on the columns and no fullscreen window.
16func (w *Workspace) slidable() bool {
17 return w.Overflow != OverflowFixed && len(w.Columns) > 0 && w.fullscreen == 0 && !w.onFloat()
18}
19
20func (w *Workspace) stopSlide() { w.shift, w.motion = 0, nil }
21
22// retarget keeps the view where it is on screen when ViewX moved from
23// before during a landing slide: the slide heads for the new view.
24// Without a landing slide the view moves at once, as it always has.
25func (w *Workspace) retarget(before int) {
26 if w.ViewX == before || w.motion == nil {
27 return
28 }
29 w.shift += float64(before - w.ViewX)
30 w.motion = newMotion(viewSpring(w.shift, 0), time.Time{})
31}
32
33func (w *Workspace) shiftPixels() int { return int(math.Round(w.shift)) }
34
35// swipeScale turns touchpad distance into pixels: viewSwipeMovement
36// scrolls one usable width.
37func (w *Workspace) swipeScale() float64 {
38 return float64(w.Usable.W) / viewSwipeMovement
39}
40
41// snapPoints are the sorted views that align a column edge with the usable
42// area, never past the first or last column. Points closer than a
43// snapSpacing of the usable width merge into the first: a swipe step
44// always moves the view visibly.
45func (w *Workspace) snapPoints() []float64 {
46 g := w.gap()
47 minX, maxX := w.Usable.X+g, w.Usable.X+w.Usable.W-g
48 last := len(w.Columns) - 1
49 lo := w.columnX(0) - minX
50 hi := max(w.columnX(last)+w.columnWidth(last)-maxX, lo)
51 views := make([]int, 0, 2*len(w.Columns))
52 for i := range w.Columns {
53 views = append(views, min(max(w.columnX(i)-minX, lo), hi), min(max(w.columnX(i)+w.columnWidth(i)-maxX, lo), hi))
54 }
55 slices.Sort(views)
56 minStep := float64(w.Usable.W) * snapSpacing
57 points := []float64{float64(lo)}
58 for _, v := range views {
59 if float64(v)-points[len(points)-1] >= minStep {
60 points = append(points, float64(v))
61 }
62 }
63 // The last column's end stays a point; a point close before it goes.
64 if n := len(points); float64(hi) != points[n-1] {
65 if n > 1 && float64(hi)-points[n-1] < minStep {
66 points = points[:n-1]
67 }
68 points = append(points, float64(hi))
69 }
70 return points
71}
72
73// snapSpacing is the smallest distance between column snap points, in
74// usable widths.
75const snapSpacing = 0.1
76
77// snapFocus is the column to focus at view: the current one while it stays
78// fully shown at the current view, else the furthest fully shown column
79// toward the swipe (forward is rightward).
80func (w *Workspace) snapFocus(view int, forward bool) (focus int) {
81 g := w.gap()
82 minX, maxX := w.Usable.X+g, w.Usable.X+w.Usable.W-g
83 full := func(i int) bool {
84 x := w.columnX(i) - view
85 return x >= minX && x+w.columnWidth(i) <= maxX
86 }
87 if full(w.Focus) && view == w.ViewX {
88 return w.Focus
89 }
90 focus = -1
91 for i := range w.Columns {
92 if full(i) && (focus < 0 || forward) {
93 focus = i
94 }
95 }
96 if focus < 0 {
97 // Columns wider than the view: the one at the left edge.
98 focus = 0
99 for i := range w.Columns {
100 if w.columnX(i)-view <= minX {
101 focus = i
102 }
103 }
104 }
105 return focus
106}
107
108func (m *Monitor) stopSwitch() { m.switchOff, m.switchMotion, m.switchList = 0, nil, nil }
109
110// framedSwitch keeps transitions involving a sized workspace settled. A
111// monitor-wide animation has no per-workspace crop; it must not expose
112// columns outside their viewport while sliding between different frames.
113func (m *Monitor) framedSwitch() bool {
114 if m.Current().Output != m.Output() {
115 return true
116 }
117 list := m.Workspaces
118 if m.switchList != nil {
119 list = m.switchList
120 }
121 base := indexOf(list, m.Current())
122 if base < 0 {
123 return false
124 }
125 pos := float64(base) + m.switchOff
126 for _, i := range [2]int{int(math.Floor(pos)), int(math.Ceil(pos))} {
127 if i >= 0 && i < len(list) && m.has(list[i]) && list[i].Output != m.Output() {
128 return true
129 }
130 }
131 return false
132}
133
134// slideLayout adds the workspaces a workspace slide shows next to the
135// current one, moved by their distance, to the current layout (moved too).
136// A landing slide places them as the list was when the swipe began
137// (switchList): an empty workspace dropped since shows as background.
138func (m *Monitor) slideLayout(cur []Placement) []Placement {
139 if m.switchOff == 0 || m.shown != nil || m.framedSwitch() {
140 return cur
141 }
142 list := m.Workspaces
143 if m.switchList != nil {
144 list = m.switchList
145 }
146 base := indexOf(list, m.Current())
147 if base < 0 {
148 return cur
149 }
150 h := float64(m.template.Output.H)
151 pos := float64(base) + m.switchOff
152 offset := func(p []Placement, k int) []Placement {
153 dy := int(math.Round((float64(k) - pos) * h))
154 for i := range p {
155 if !p[i].Hidden {
156 p[i].Rect.Y += dy
157 }
158 }
159 return p
160 }
161 cur = offset(cur, base)
162 for _, k := range []int{int(math.Floor(pos)), int(math.Ceil(pos))} {
163 if k == base || k < 0 || k >= len(list) || !m.has(list[k]) {
164 continue
165 }
166 next := list[k].Layout()
167 for i := range next {
168 next[i].Focused = false
169 }
170 // Windows of the neighbor are already listed hidden: replace them.
171 shown := map[WindowID]Placement{}
172 for _, p := range offset(next, k) {
173 shown[p.ID] = p
174 }
175 for i, p := range cur {
176 if v, ok := shown[p.ID]; ok {
177 cur[i] = v
178 }
179 }
180 }
181 return cur
182}