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, motion{} }
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.on {
27 return
28 }
29 w.shift += float64(before - w.ViewX)
30 w.motion = newMotion(viewSpring(w.shift, w.motion.velocity()), time.Time{}, w.motion.slow)
31}
32
33func (w *Workspace) shiftPixels() int { return int(math.Round(w.shift)) }
34
35// switchSpring is the workspace slide's spring from off: it snaps once it
36// is under half a logical pixel of the output height from its target
37// (slideLayout rounds the offset times the height).
38func (m *Monitor) switchSpring(off, velocity float64) spring {
39 s := workspaceSpring(off, velocity)
40 if h := m.template.Output.H; h > 0 {
41 s.Snap = pixelSnap / float64(h)
42 }
43 return s
44}
45
46// swipeScale turns touchpad distance into pixels: viewSwipeMovement
47// scrolls one usable width.
48func (w *Workspace) swipeScale() float64 {
49 return float64(w.Usable.W) / viewSwipeMovement
50}
51
52// snapPoints are the sorted views that align a column edge with the usable
53// area, never past the first or last column. Points closer than a
54// snapSpacing of the usable width merge into the first: a swipe step
55// always moves the view visibly.
56func (w *Workspace) snapPoints() []float64 {
57 g := w.gap()
58 minX, maxX := w.Usable.X+g, w.Usable.X+w.Usable.W-g
59 last := len(w.Columns) - 1
60 lo := w.columnX(0) - minX
61 hi := max(w.columnX(last)+w.columnWidth(last)-maxX, lo)
62 views := make([]int, 0, 2*len(w.Columns))
63 for i := range w.Columns {
64 views = append(views, min(max(w.columnX(i)-minX, lo), hi), min(max(w.columnX(i)+w.columnWidth(i)-maxX, lo), hi))
65 }
66 slices.Sort(views)
67 minStep := float64(w.Usable.W) * snapSpacing
68 points := []float64{float64(lo)}
69 for _, v := range views {
70 if float64(v)-points[len(points)-1] >= minStep {
71 points = append(points, float64(v))
72 }
73 }
74 // The last column's end stays a point; a point close before it goes.
75 if n := len(points); float64(hi) != points[n-1] {
76 if n > 1 && float64(hi)-points[n-1] < minStep {
77 points = points[:n-1]
78 }
79 points = append(points, float64(hi))
80 }
81 return points
82}
83
84// snapSpacing is the smallest distance between column snap points, in
85// usable widths.
86const snapSpacing = 0.1
87
88// snapFocus is the column to focus at view: the current one while it stays
89// fully shown at the current view, else the furthest fully shown column
90// toward the swipe (forward is rightward).
91func (w *Workspace) snapFocus(view int, forward bool) (focus int) {
92 g := w.gap()
93 minX, maxX := w.Usable.X+g, w.Usable.X+w.Usable.W-g
94 full := func(i int) bool {
95 x := w.columnX(i) - view
96 return x >= minX && x+w.columnWidth(i) <= maxX
97 }
98 if full(w.Focus) && view == w.ViewX {
99 return w.Focus
100 }
101 focus = -1
102 for i := range w.Columns {
103 if full(i) && (focus < 0 || forward) {
104 focus = i
105 }
106 }
107 if focus < 0 {
108 // Columns wider than the view: the one at the left edge.
109 focus = 0
110 for i := range w.Columns {
111 if w.columnX(i)-view <= minX {
112 focus = i
113 }
114 }
115 }
116 return focus
117}
118
119func (m *Monitor) stopSwitch() { m.switchOff, m.switchMotion, m.switchList = 0, motion{}, nil }
120
121// framedSwitch keeps transitions involving a sized workspace settled. A
122// monitor-wide animation has no per-workspace crop; it must not expose
123// columns outside their viewport while sliding between different frames.
124func (m *Monitor) framedSwitch() bool {
125 if m.Current().Output != m.Output() {
126 return true
127 }
128 list := m.Workspaces
129 if m.switchList != nil {
130 list = m.switchList
131 }
132 base := indexOf(list, m.Current())
133 if base < 0 {
134 return false
135 }
136 pos := float64(base) + m.switchOff
137 for _, i := range [2]int{int(math.Floor(pos)), int(math.Ceil(pos))} {
138 if i >= 0 && i < len(list) && m.has(list[i]) && list[i].Output != m.Output() {
139 return true
140 }
141 }
142 return false
143}
144
145// slideLayout adds the workspaces a workspace slide shows next to the
146// current one, moved by their distance, to the current layout (moved too).
147// A landing slide places them as the list was when the swipe began
148// (switchList): an empty workspace dropped since shows as background.
149func (m *Monitor) slideLayout(cur []Placement) []Placement {
150 if m.switchOff == 0 || m.shown != nil || m.framedSwitch() {
151 return cur
152 }
153 list := m.Workspaces
154 if m.switchList != nil {
155 list = m.switchList
156 }
157 base := indexOf(list, m.Current())
158 if base < 0 {
159 return cur
160 }
161 h := float64(m.template.Output.H)
162 pos := float64(base) + m.switchOff
163 offset := func(p []Placement, k int) []Placement {
164 dy := int(math.Round((float64(k) - pos) * h))
165 for i := range p {
166 if !p[i].Hidden {
167 p[i].Rect.Y += dy
168 }
169 }
170 return p
171 }
172 cur = offset(cur, base)
173 for _, k := range []int{int(math.Floor(pos)), int(math.Ceil(pos))} {
174 if k == base || k < 0 || k >= len(list) || !m.has(list[k]) {
175 continue
176 }
177 next := list[k].layoutInto(m.slideBuf)
178 m.slideBuf = next
179 for i := range next {
180 next[i].Focused = false
181 }
182 // Windows of the neighbor are already listed hidden: replace them.
183 if m.slideShown == nil {
184 m.slideShown = map[WindowID]Placement{}
185 }
186 shown := m.slideShown
187 clear(shown)
188 for _, p := range offset(next, k) {
189 shown[p.ID] = p
190 }
191 for i, p := range cur {
192 if v, ok := shown[p.ID]; ok {
193 cur[i] = v
194 }
195 }
196 }
197 return cur
198}