1package core
2
3import (
4 "math"
5 "slices"
6 "time"
7
8 "github.com/bnema/neferwl/internal/ports"
9)
10
11// Transitions start after a user action (a bind, a click that focuses, a
12// discrete swipe step): a snapshot of what each screen shows is taken before
13// the action and diffed after it. The state the action leaves is the
14// settled one; the transition only adds presentation springs over it
15// (Workspace.shift, Monitor.switchOff), starting from what was on screen.
16// Nothing here runs for events that come from clients or outputs.
17//
18// A window's rect transition (presentation-only rect transition) is presentation too: configures keep
19// using the settled rect, so the client resizes once to its final size; what
20// is drawn and hit-tested is the settled rect plus an offset that a spring
21// takes to zero. The offsets are relative to the camera-shifted layout, so a
22// window the view scrolls moves with the camera spring only.
23
24// viewShot is what one screen showed before an action.
25type viewShot struct {
26 sc *screen
27 ws *Workspace
28 // overview is set when the screen showed its overview: ws is then the
29 // row on screen, the camera fields stay zero, and rects are the cards.
30 overview bool
31 // viewX is ws's settled view and view where its columns were on screen
32 // (viewX plus shift).
33 viewX int
34 view float64
35 // viewV and switchV are the speeds the running view and workspace
36 // springs have at the snapshot's time (units per second, 0 without
37 // one), not at their last frame. An action may stop them
38 // (Monitor.Focus), so the transition reads them here.
39 viewV, switchV float64
40 // list and pos are the numbered workspaces and the monitor's fractional
41 // position in them (index of the current one plus switchOff); ok is
42 // false when there is none (a shown stash workspace).
43 list []*Workspace
44 pos float64
45 ok bool
46 // rects are the windows the screen drew (cards in the overview),
47 // hidden windows left out. The slice is reused like list.
48 rects []rectShot
49 // stash holds the settled placements of the stash windows the screen
50 // drew, for the ones an action hides (transitionStash). Reused too.
51 stash []Placement
52}
53
54// rectOffsets are the four components of a rect motion: x, y, w, h.
55type rectOffsets struct{ x, y, w, h float64 }
56
57// rectShot is where a window was drawn: its settled rect plus the float
58// offsets of its running rect motion (zero without one), which the drawn
59// rect only rounds. vel is the speed of each offset component, in units per
60// second. Offsets and speeds are sampled at the snapshot's time.
61type rectShot struct {
62 id WindowID
63 rect Rect
64 off, vel rectOffsets
65 // fade and fadeV are the fade offset of a running fade motion and its
66 // speed, so an overview toggle carries it over (transitionOverview).
67 fade, fadeV float64
68 // dim and dimV are the dim offset of a running dim motion and its
69 // speed; veil is the peek veil the window's settled placement drew
70 // (Stash.Dim, 0 for another window). A stash navigation takes the
71 // veil from what was on screen to the new one (transitionRects).
72 dim, dimV, veil float64
73}
74
75// rectMotion takes a window's drawn rect to its settled one: each component
76// is an offset from the settled value, sprung to 0 (dx, dy, dw, dh hold the
77// current offsets, updated by advance). fade and dim are offsets too, from
78// the settled Fade (0, or 1 for a leaving window) and the settled Dim.
79//
80// With scale, the window's content follows its drawn size: the shown
81// placement gets a Zoom of the settled zoom (its Preview, or 1) times
82// shown.W / settled.W (refreshShown), which the scene carries as Preview.
83// The walk shrinks content toward the content rect's top-left, so this
84// suits aspect-preserving changes: the zoom follows the smaller of the width
85// and height ratios, so content never exceeds its frame when the aspect
86// differs a little (rounding) or a lot (the frame is then letterboxed). An overview card's settled zoom is
87// its Preview: a window that opens into a card goes from 1 to Preview with
88// its width, and back.
89//
90// A leaving motion draws a window that closed or hid while it fades out:
91// left is its last settled placement, which refreshShown appends (Hidden
92// and Leaving) to the settled and shown layouts, keeping them aligned.
93type rectMotion struct {
94 x, y, w, h motion
95 dx, dy, dw, dh float64
96 fade, dim motion
97 df, ddim float64
98 scale bool
99 leaving bool
100 left Placement
101}
102
103// apply is r moved by the current offsets; a size never drops below 1.
104func (m *rectMotion) apply(r Rect) Rect {
105 return Rect{
106 X: r.X + int(math.Round(m.dx)), Y: r.Y + int(math.Round(m.dy)),
107 W: max(r.W+int(math.Round(m.dw)), 1), H: max(r.H+int(math.Round(m.dh)), 1),
108 }
109}
110
111// sample is where the offsets and their speeds would be at now, without
112// moving the motion: a running component is sampled on a copy.
113func (m *rectMotion) sample(now time.Time) (off, vel rectOffsets) {
114 off = rectOffsets{m.dx, m.dy, m.dw, m.dh}
115 sampleComponent(m.x, &off.x, &vel.x, now)
116 sampleComponent(m.y, &off.y, &vel.y, now)
117 sampleComponent(m.w, &off.w, &vel.w, now)
118 sampleComponent(m.h, &off.h, &vel.h, now)
119 return off, vel
120}
121
122func sampleComponent(m motion, off, vel *float64, now time.Time) {
123 if m.on {
124 *off, *vel = m.sampleAt(now)
125 }
126}
127
128// advance moves the offsets to now; a settled component stops at 0.
129func (m *rectMotion) advance(now time.Time) {
130 m.dx = stepComponent(&m.x, m.dx, now)
131 m.dy = stepComponent(&m.y, m.dy, now)
132 m.dw = stepComponent(&m.w, m.dw, now)
133 m.dh = stepComponent(&m.h, m.dh, now)
134 m.df = stepComponent(&m.fade, m.df, now)
135 m.ddim = stepComponent(&m.dim, m.ddim, now)
136}
137
138func stepComponent(m *motion, cur float64, now time.Time) float64 {
139 if !m.on {
140 return cur
141 }
142 v, done := m.at(now)
143 if done {
144 *m = motion{}
145 return 0
146 }
147 return v
148}
149
150// on reports whether any component still runs.
151func (m *rectMotion) on() bool {
152 return m.x.on || m.y.on || m.w.on || m.h.on || m.fade.on || m.dim.on
153}
154
155// settledFade is the Fade the motion springs to: a leaving window fades
156// out, any other is opaque.
157func (m *rectMotion) settledFade() float64 {
158 if m.leaving {
159 return 1
160 }
161 return 0
162}
163
164// show is p as drawn: its rect moved by the offsets, its fade and dim
165// offsets added, and its content zoomed with its size when scale is set.
166func (m *rectMotion) show(p *Placement) {
167 settled := p.Rect
168 p.Rect = m.apply(settled)
169 p.Fade = max(0, min(1, m.settledFade()+m.df))
170 // A peek's Dim is an offset from its veil (publish adds Stash.Dim and
171 // clamps the sum): it may go below 0, down to cancelling the veil.
172 lo := 0.0
173 if p.Peek {
174 lo = -1
175 }
176 p.Dim = max(lo, min(1, p.Dim+m.ddim))
177 if m.scale && settled.W > 0 && settled.H > 0 {
178 zoom := p.Preview
179 if zoom == 0 {
180 zoom = 1
181 if m.leaving {
182 // A leaving window settles shrunk, its content with it.
183 zoom = appearScale
184 }
185 }
186 zoom *= min(float64(p.Rect.W)/float64(settled.W), float64(p.Rect.H)/float64(settled.H))
187 if zoom >= 0.999 {
188 // Drawn at its size: content is never magnified. publish
189 // emits no Zoom for a window, and Zoom 1 for a card (whose
190 // Preview would shrink it).
191 zoom = 1
192 }
193 p.Zoom = zoom
194 }
195}
196
197// retargetComponent starts m for an offset of off from its settled value at
198// now, with the speed v it had then. A component with nothing to move and no
199// speed stops.
200func (c *Core) retargetComponent(m *motion, cur *float64, off, v float64, now time.Time) {
201 c.retargetWith(m, cur, viewSpring(off, v), now)
202}
203
204// retargetLevel is retargetComponent for a fade or veil offset (a level in
205// 0..1): its spring runs to its fine epsilon, no pixel snap.
206func (c *Core) retargetLevel(m *motion, cur *float64, off, v float64, now time.Time) {
207 c.retargetWith(m, cur, levelSpring(off, v), now)
208}
209
210func (c *Core) retargetWith(m *motion, cur *float64, s spring, now time.Time) {
211 *cur = s.From
212 if s.From == 0 && s.Velocity == 0 {
213 *m = motion{}
214 return
215 }
216 *m = c.spring(s, now)
217}
218
219// refreshShown builds the layouts of every screen once per publish: the
220// settled one (what configures are sized from) and the shown one (the same
221// with the rect motions applied: what is drawn and hit-tested). Rect motions
222// belong to the workspace they began on, or to the open overview (rectsOwner):
223// any other change drops them.
224//
225// Leaving motions add an entry for a window the layout no longer shows:
226// appended to both layouts in ID order, Hidden (no input, focus, popups or
227// configure) and Leaving (drawn, fading). The two layouts stay
228// index-aligned: publish reads the settled entry at the shown one's index.
229func (c *Core) refreshShown() {
230 for _, sc := range c.screens {
231 m := sc.mon
232 if len(sc.rects) > 0 && sc.rectsWS != sc.rectsOwner() {
233 sc.stopRects()
234 }
235 sc.layoutBuf = m.layoutInto(sc.layoutBuf)
236 sc.settledLayout = sc.layoutBuf
237 if len(sc.settledLayout) == 0 {
238 // Empty stays nil, as a fresh Layout() was: shown nil falls
239 // back to the live layout until the next publish.
240 sc.settledLayout = nil
241 }
242 if len(sc.rects) > 0 {
243 // A window that left the layout (or is hidden) keeps no motion:
244 // it would ask for frames with nothing to move. A leaving one
245 // is the opposite: it goes once the layout shows the window.
246 for id, rm := range sc.rects {
247 shown := slices.ContainsFunc(sc.settledLayout, func(p Placement) bool {
248 return p.ID == id && !p.Hidden
249 })
250 if shown == rm.leaving {
251 delete(sc.rects, id)
252 }
253 }
254 }
255 if len(sc.rects) == 0 {
256 sc.shown = sc.settledLayout
257 continue
258 }
259 sc.settledLayout = sc.withLeaving(sc.settledLayout)
260 buf := append(sc.shownBuf[:0], sc.settledLayout...)
261 for i := range buf {
262 p := &buf[i]
263 if p.Hidden && !p.Leaving {
264 continue
265 }
266 if rm, ok := sc.rects[p.ID]; ok {
267 rm.show(p)
268 }
269 }
270 sc.shownBuf, sc.shown = buf, buf
271 }
272}
273
274// withLeaving is layout with the leaving windows' last placements added,
275// Hidden and Leaving, into the screen's reused buffer (the layout itself
276// is left alone). A window the layout still lists, Hidden (a hidden stash),
277// is replaced where it is. A leaving tile goes where the tiles are painted: before
278// the first window that opens the floats (a float above the tiles, as the
279// renderer orders them), so it never flashes over a float; a leaving float
280// goes last, over the tiles. Within a group the absent ones go smallest ID
281// first: a stable order keeps the scenes comparable frame to frame.
282func (s *screen) withLeaving(layout []Placement) []Placement {
283 out := append(s.settledBuf[:0], layout...)
284 // Pass 1: an entry the layout still lists, Hidden (a stash that hid),
285 // replaces it where it is, never twice. Where the pass goes through the
286 // map does not matter: every entry has its own place. The absent ones
287 // are collected for pass 2.
288 ids := s.leaveIDs[:0]
289 for id, rm := range s.rects {
290 if !rm.leaving {
291 continue
292 }
293 j := slices.IndexFunc(out, func(q Placement) bool { return q.ID == id && q.Hidden && !q.Leaving })
294 if j < 0 {
295 ids = append(ids, id)
296 continue
297 }
298 out[j] = rm.leavingPlacement()
299 }
300 // Pass 2: the absent ones in ID order, each at the end of its group
301 // (tiles before the first float, a leaving stash float included;
302 // floats last), so the order never depends on the map's.
303 floats := len(out)
304 for i, p := range out {
305 if p.Floating && !p.Below && p.Preview == 0 && (!p.Hidden || p.Leaving) {
306 floats = i
307 break
308 }
309 }
310 slices.Sort(ids)
311 for _, id := range ids {
312 p := s.rects[id].leavingPlacement()
313 if p.Floating {
314 out = append(out, p)
315 continue
316 }
317 out = slices.Insert(out, floats, p)
318 floats++
319 }
320 s.leaveIDs, s.settledBuf = ids, out
321 return out
322}
323
324// leavingPlacement is the placement a leaving motion draws: its last
325// settled one, Hidden and Leaving (the scene draws it; input, focus,
326// popups and configures see a hidden window, as the C0 design has it).
327// Hidden stays set on purpose: the leaving entry never takes input, even
328// though its stash window is still in the layout.
329func (m rectMotion) leavingPlacement() Placement {
330 p := m.left
331 p.Hidden, p.Leaving, p.Focused, p.Fade, p.Dim, p.Zoom = true, true, false, 0, 0, 0
332 return p
333}
334
335// appearAt starts the entrance of a window the action made visible on sc,
336// whose settled placement is p: it fades in from invisible and grows from
337// 90 % of its settled rect, around its centre. Nothing with animations off
338// or for a hidden placement. A leaving motion of the same window (a show
339// during a hide) is continued from, not replaced: the entrance starts at
340// the fade, rect and speeds it was drawn with.
341func (c *Core) appearAt(sc *screen, p Placement, now time.Time) {
342 if !c.animOn() || p.Hidden {
343 return
344 }
345 if sc.rects == nil {
346 sc.rects = make(map[WindowID]rectMotion)
347 }
348 var rm rectMotion
349 rm.scale = true
350 if old, ok := sc.rects[p.ID]; ok && old.leaving {
351 c.appearFrom(&rm, p, &old, now)
352 } else {
353 c.scaleFrom(&rm, p.Rect, appearScale, now)
354 c.retargetLevel(&rm.fade, &rm.df, 1, 0, now)
355 }
356 sc.rects[p.ID], sc.rectsWS = rm, sc.mon.Current()
357}
358
359// appearFrom starts rm, the entrance to the settled placement p, from where
360// the leaving motion old draws its window at now: its rect (old.left.Rect
361// plus its offsets, sampled), fade, veil and their speeds. The leaving fade
362// is an offset from 1, the entrance's from 0.
363func (c *Core) appearFrom(rm *rectMotion, p Placement, old *rectMotion, now time.Time) {
364 settled := p.Rect
365 off, vel := old.sample(now)
366 base := old.left.Rect
367 fade, fadeV := old.df, 0.0
368 sampleComponent(old.fade, &fade, &fadeV, now)
369 dim, dimV := old.ddim, 0.0
370 sampleComponent(old.dim, &dim, &dimV, now)
371 // The veil the leaving entry drew (its peek's, plus the offset) to the
372 // one the settled placement draws.
373 dim += c.peekDim(old.left) - c.peekDim(p)
374 c.retargetComponent(&rm.x, &rm.dx, float64(base.X-settled.X)+off.x, vel.x, now)
375 c.retargetComponent(&rm.y, &rm.dy, float64(base.Y-settled.Y)+off.y, vel.y, now)
376 c.retargetComponent(&rm.w, &rm.dw, float64(base.W-settled.W)+off.w, vel.w, now)
377 c.retargetComponent(&rm.h, &rm.dh, float64(base.H-settled.H)+off.h, vel.h, now)
378 c.retargetLevel(&rm.fade, &rm.df, 1+fade, fadeV, now)
379 c.retargetLevel(&rm.dim, &rm.ddim, dim, dimV, now)
380}
381
382// mapWindow handles a window's map: it joins its workspace, the tiles the
383// new one displaces re-flow with rect motions, and a window that comes on
384// screen appears (appearMapped). A map is the one client event that
385// animates; the settled layout is the same with animations off, so the
386// client is configured once, to its final size.
387func (c *Core) mapWindow(v ports.WindowMapped) {
388 now := c.now()
389 c.windows.mapped(v, now)
390 if s, _ := c.screenOf(v.ID); s != nil || !c.animOn() || c.security.Protected {
391 // Mapped before (a re-map keeps its place), animations off or a
392 // session lock: nothing to animate.
393 c.placement.place(c, v)
394 return
395 }
396 shots := c.snapshot(now)
397 for i := range shots {
398 // A screen with its overview open re-lays its cards at once: a map
399 // is not an overview action (transition skips a shot without ws).
400 if shots[i].overview {
401 shots[i].ws = nil
402 }
403 }
404 c.placement.place(c, v)
405 c.transition(shots, now)
406 c.appearMapped(v.ID, now)
407}
408
409// appearMapped starts the entrance of a window that just mapped when it
410// shows on its screen's current workspace as a tile or an ordinary float:
411// not with the overview open or a session lock, not fullscreen (it keeps
412// the direct scanout path), not a float below the columns, not a stash
413// peek.
414func (c *Core) appearMapped(id WindowID, now time.Time) {
415 sc, w := c.screenOf(id)
416 if sc == nil || c.security.Protected || sc.mon.ov.open || sc.mon.Current() != w {
417 return
418 }
419 for _, p := range sc.mon.Layout() {
420 if p.ID != id {
421 continue
422 }
423 if !p.Hidden && !p.Fullscreen && !p.Below && !p.Peek && p.Preview == 0 {
424 c.appearAt(sc, p, now)
425 }
426 return
427 }
428}
429
430// unmapWindow handles a window's unmap: it leaves its workspace, the tiles
431// around it re-flow with rect motions, and a window that was drawn as a
432// tile or an ordinary float keeps fading out from where it was drawn
433// (leaveFrom with the snapshot's record of it: a window closed mid-motion
434// leaves from its drawn rect, fade and speed, not from its settled rect).
435// Like a map, an unmap is a client event that animates; the settled layout
436// is the same with animations off. A fullscreen window, one under a
437// session lock or with the overview open just goes.
438func (c *Core) unmapWindow(v ports.WindowUnmapped) {
439 // The window is unmapped: a toplevel that maps again keeps its ID and
440 // must be configured from nothing (a leaving entry keeps no configure).
441 c.configures.forget(v.ID)
442 sc, w := c.screenOf(v.ID)
443 if sc == nil {
444 return
445 }
446 if !c.animOn() || c.security.Protected || sc.mon.ov.open {
447 sc.mon.RemoveWindow(v.ID)
448 delete(sc.rects, v.ID)
449 return
450 }
451 now := c.now()
452 // The placement that leaves is the settled one of the last publish:
453 // where the window was going.
454 var left Placement
455 if sc.mon.Current() == w {
456 for _, p := range sc.settledLayout {
457 if p.ID == v.ID {
458 left = p
459 break
460 }
461 }
462 }
463 shots := c.snapshot(now)
464 var shot *rectShot
465 for i := range shots {
466 if shots[i].overview {
467 shots[i].ws = nil
468 }
469 if shots[i].sc == sc {
470 if j := slices.IndexFunc(shots[i].rects, func(r rectShot) bool { return r.id == v.ID }); j >= 0 {
471 shot = &shots[i].rects[j]
472 }
473 }
474 }
475 sc.mon.RemoveWindow(v.ID)
476 // A window already leaving (a stash hide still fading) keeps its
477 // motion: the fade goes on from where it is, drawn from the content
478 // the renderer kept. Any other motion goes with the window.
479 if rm, ok := sc.rects[v.ID]; !ok || !rm.leaving {
480 delete(sc.rects, v.ID)
481 }
482 c.transition(shots, now)
483 if left.ID == v.ID && !left.Leaving && !left.Fullscreen && !left.Below && !left.Peek {
484 c.leaveFrom(sc, left, shot, now)
485 }
486}
487
488// resizeFloating applies a floating window's committed size. A float maps
489// before its first commit, so core first lays it out at a default size;
490// when its entrance runs, the settled rect changing under it would break
491// the aspect of the 90 % rect. The entrance's rect offsets (and their
492// springs, which are linear in their start and speed) are scaled by the
493// settled size ratio: the progress and speed carry over, and the window
494// still grows from 90 % of its real size around its centre.
495func (c *Core) resizeFloating(v ports.WindowResized) {
496 sc, w := c.screenOf(v.ID)
497 if w == nil {
498 return
499 }
500 rm, running := sc.rects[v.ID]
501 running = running && rm.scale && !rm.leaving
502 var old Rect
503 if running {
504 old = settledRect(sc.mon, v.ID)
505 }
506 w.ResizeFloating(v.ID, v.Width, v.Height)
507 if !running {
508 return
509 }
510 cur := settledRect(sc.mon, v.ID)
511 if old.W <= 0 || old.H <= 0 || cur.W <= 0 || cur.H <= 0 || old == cur {
512 return
513 }
514 kw, kh := float64(cur.W)/float64(old.W), float64(cur.H)/float64(old.H)
515 rm.dx, rm.dw = rm.dx*kw, rm.dw*kw
516 rm.dy, rm.dh = rm.dy*kh, rm.dh*kh
517 rm.x.scale(kw)
518 rm.w.scale(kw)
519 rm.y.scale(kh)
520 rm.h.scale(kh)
521 sc.rects[v.ID] = rm
522}
523
524// settledRect is where m's layout puts the visible window id, zero if it
525// shows none.
526func settledRect(m *Monitor, id WindowID) Rect {
527 for _, p := range m.Layout() {
528 if p.ID == id && !p.Hidden {
529 return p.Rect
530 }
531 }
532 return Rect{}
533}
534
535// leaveFrom keeps drawing a window the action closed or hid, from its last
536// settled placement p: it fades out and shrinks to 90 % around its centre,
537// then its entry goes. Nothing with animations off, or for a placement that
538// was not drawn. shot is the snapshot's record of how the window was drawn
539// (nil for a window at rest): the exit starts at the rect, fade and dim the
540// window had on screen, with their speeds, so a hide
541// during an entrance or a slide does not jump.
542func (c *Core) leaveFrom(sc *screen, p Placement, shot *rectShot, now time.Time) {
543 if !c.animOn() || p.Hidden || p.Preview > 0 || p.Rect.W <= 0 || p.Rect.H <= 0 {
544 return
545 }
546 if sc.rects == nil {
547 sc.rects = make(map[WindowID]rectMotion)
548 }
549 rm := rectMotion{scale: true, leaving: true, left: p}
550 rm.left.Hidden, rm.left.Leaving, rm.left.Focused = true, true, false
551 var off, vel rectOffsets
552 fade, fadeV, dim, dimV := 0.0, 0.0, 0.0, 0.0
553 if shot != nil {
554 off, vel, fade, fadeV, dim, dimV = shot.off, shot.vel, shot.fade, shot.fadeV, shot.dim, shot.dimV
555 }
556 // The entry settles at 90 % of the rect it left, its content zoomed
557 // to match (show): the offsets run from the rect it was drawn at (the
558 // full one at rest) to that.
559 end := scaledRect(p.Rect, appearScale)
560 c.retargetComponent(&rm.x, &rm.dx, float64(p.Rect.X-end.X)+off.x, vel.x, now)
561 c.retargetComponent(&rm.y, &rm.dy, float64(p.Rect.Y-end.Y)+off.y, vel.y, now)
562 c.retargetComponent(&rm.w, &rm.dw, float64(p.Rect.W-end.W)+off.w, vel.w, now)
563 c.retargetComponent(&rm.h, &rm.dh, float64(p.Rect.H-end.H)+off.h, vel.h, now)
564 rm.left.Rect = end
565 // The leaving fade is an offset from 1: the window drawn at fade f is
566 // at f-1.
567 c.retargetLevel(&rm.fade, &rm.df, fade-1, fadeV, now)
568 c.retargetLevel(&rm.dim, &rm.ddim, dim, dimV, now)
569 sc.rects[p.ID], sc.rectsWS = rm, sc.mon.Current()
570}
571
572// appearScale is the size, relative to the settled rect, a window appears
573// from and leaves to.
574const appearScale = 0.9
575
576// scaledRect is r scaled by k around its centre, in float.
577func scaledRect(r Rect, k float64) Rect {
578 w, h := float64(r.W)*k, float64(r.H)*k
579 x := float64(r.X) + (float64(r.W)-w)/2
580 y := float64(r.Y) + (float64(r.H)-h)/2
581 return Rect{X: int(math.Round(x)), Y: int(math.Round(y)), W: int(math.Round(w)), H: int(math.Round(h))}
582}
583
584// scaleFrom starts rm's rect components from settled scaled by k around
585// its centre, toward settled.
586func (c *Core) scaleFrom(rm *rectMotion, settled Rect, k float64, now time.Time) {
587 w, h := float64(settled.W)*k, float64(settled.H)*k
588 c.retargetComponent(&rm.x, &rm.dx, (float64(settled.W)-w)/2, 0, now)
589 c.retargetComponent(&rm.y, &rm.dy, (float64(settled.H)-h)/2, 0, now)
590 c.retargetComponent(&rm.w, &rm.dw, w-float64(settled.W), 0, now)
591 c.retargetComponent(&rm.h, &rm.dh, h-float64(settled.H), 0, now)
592}
593
594// snapshot records what every screen without an open overview shows at now.
595// Running motions are sampled at now on a copy, so a transition that
596// follows chains from their speed at the time of the action, not from the
597// last frame's. The view is the one drawn (the layouts it is measured
598// against are), only its speed is sampled. The returned slice and the lists
599// in it belong to Core and are reused by the next snapshot: use it before
600// the next action only.
601func (c *Core) snapshot(now time.Time) []viewShot {
602 full := c.shots[:cap(c.shots)]
603 out := c.shots[:0]
604 for _, sc := range c.screens {
605 m := sc.mon
606 var s viewShot
607 if len(out) < len(full) {
608 s.list = full[len(out)].list[:0]
609 s.rects = full[len(out)].rects[:0]
610 s.stash = full[len(out)].stash[:0]
611 }
612 s.sc = sc
613 w := m.Current()
614 s.ws, s.overview = w, m.ov.open
615 if !m.ov.open {
616 // The overview does not scroll or slide: no camera.
617 s.viewX, s.view = w.ViewX, float64(w.ViewX)+w.shift
618 }
619 if w.motion.on {
620 _, s.viewV = w.motion.sampleAt(now)
621 }
622 if m.switchMotion.on {
623 _, s.switchV = m.switchMotion.sampleAt(now)
624 }
625 // settledLayout and shown are index-aligned and differ only by
626 // the offsets of sc.rects: the shot keeps both unrounded. The
627 // overview's cards are recorded like windows.
628 for _, p := range sc.settledLayout {
629 if p.Hidden {
630 continue
631 }
632 r := rectShot{id: p.ID, rect: p.Rect}
633 if rm, ok := sc.rects[p.ID]; ok {
634 r.off, r.vel = rm.sample(now)
635 r.fade = rm.df
636 sampleComponent(rm.fade, &r.fade, &r.fadeV, now)
637 r.dim = rm.ddim
638 sampleComponent(rm.dim, &r.dim, &r.dimV, now)
639 }
640 r.veil = c.peekDim(p)
641 s.rects = append(s.rects, r)
642 if !m.ov.open && w.stashIndex(p.ID) >= 0 {
643 s.stash = append(s.stash, p)
644 }
645 }
646 if m.shown == nil && !m.ov.open {
647 // A landing slide measures in the list it began on.
648 list := m.Workspaces
649 if m.switchList != nil {
650 list = m.switchList
651 }
652 if i := indexOf(list, w); i >= 0 {
653 s.list = append(s.list, list...)
654 s.pos, s.ok = float64(i)+m.switchOff, true
655 }
656 }
657 out = append(out, s)
658 }
659 for i := len(out); i < len(full); i++ {
660 // Shots of unplugged screens keep nothing of them alive.
661 full[i].sc, full[i].ws = nil, nil
662 clear(full[i].list[:cap(full[i].list)])
663 full[i].list = full[i].list[:0]
664 }
665 c.shots = out
666 return out
667}
668
669// transition starts the springs that take each screen from what before
670// showed to what the action left, at now. It does nothing with animations
671// off, or while a swipe follows the fingers on that screen. Per-window rect
672// motions join the camera here; with the overview open before or after
673// there is no camera, only the cards' (transitionOverview).
674//
675// A workspace moved to another monitor (move-workspace-to-monitor-*, or a
676// named workspace pulled to the focused one by a "workspace <name>" bind
677// while it shows on another) is animated on both screens: the destination's
678// windows slide in from where the source drew them (no workspace slide
679// there), the source slides to its next workspace. The focus-monitor-*
680// actions move nothing: no window changes place, so they are not animated
681// (the focus pulse marks the new focus).
682func (c *Core) transition(before []viewShot, now time.Time) {
683 if !c.animOn() {
684 return
685 }
686 for i := range before {
687 b := &before[i]
688 if b.ws == nil || !c.hasScreen(b.sc) || c.following(b.sc) {
689 continue
690 }
691 if b.overview || b.sc.mon.ov.open {
692 c.transitionOverview(b, now)
693 continue
694 }
695 c.transitionCamera(b, before, now)
696 c.transitionRects(b, before, now)
697 c.transitionStash(b, now)
698 }
699}
700
701// following reports whether a swipe moves sc's view with the fingers.
702func (c *Core) following(sc *screen) bool {
703 g := c.swipe
704 return g != nil && g.screen == sc && (g.mode == swipeColumns || g.mode == swipeWorkspaces)
705}
706
707// transitionCamera slides the columns or the monitor from what b showed.
708func (c *Core) transitionCamera(b *viewShot, before []viewShot, now time.Time) {
709 m := b.sc.mon
710 w := m.Current()
711 switch {
712 case w == b.ws:
713 if w.ViewX == b.viewX || !w.slidable() {
714 return
715 }
716 // A running landing slide was retargeted by scroll(): its velocity
717 // carries over, and the spring starts now like every action's.
718 w.shift = b.view - float64(w.ViewX)
719 w.motion = c.spring(viewSpring(w.shift, b.viewV), now)
720 case b.ok && m.shown == nil && movedFrom(b, before) == nil:
721 // A screen that received its workspace from another monitor does
722 // not slide to it: a running slide's list (switchList) may still
723 // hold the workspace, which would start a slide from nowhere. Only
724 // its windows move (transitionRects).
725 j := indexOf(b.list, w)
726 if j < 0 {
727 return
728 }
729 off := b.pos - float64(j)
730 // The snapshot's list is reused by the next action: the slide keeps
731 // its own copy.
732 m.switchOff, m.switchList = off, slices.Clone(b.list)
733 if off == 0 || m.framedSwitch() {
734 m.stopSwitch()
735 return
736 }
737 m.switchMotion = c.spring(m.switchSpring(off, b.switchV), now)
738 }
739}
740
741// transitionRects starts or retargets the rect motions of the windows whose
742// layout the action changed. The new layout is measured with the camera as
743// transitionCamera left it (the view where it was on screen), so a window
744// the view scrolls differs only by what the layout itself changed. Windows
745// that were not drawn before (new, or hidden) or are hidden now get none.
746//
747// A workspace that arrived from another monitor starts from the rects its
748// source screen drew (found in before), translated to this screen's
749// coordinates; a source whose workspace left, or any other change of
750// workspace, gets none.
751func (c *Core) transitionRects(b *viewShot, before []viewShot, now time.Time) {
752 sc := b.sc
753 m := sc.mon
754 from, dx, dy := b, 0, 0
755 if m.Current() != b.ws {
756 // The motions are those of the workspace this screen left.
757 sc.stopRects()
758 if from = movedFrom(b, before); from == nil {
759 return
760 }
761 dx, dy = from.sc.x-sc.x, from.sc.y-sc.y
762 }
763 for _, p := range m.Layout() {
764 if p.Hidden || p.Preview > 0 {
765 continue
766 }
767 i := slices.IndexFunc(from.rects, func(r rectShot) bool { return r.id == p.ID })
768 if i < 0 {
769 continue
770 }
771 old := &from.rects[i]
772 oldRect := Rect{X: old.rect.X + dx, Y: old.rect.Y + dy, W: old.rect.W, H: old.rect.H}
773 // A window that becomes a peek or stops being one (a stash
774 // navigation) changes its veil: the offset takes the one drawn to
775 // the new one. Otherwise a running dim motion goes on as it is.
776 dimOff := old.dim + old.veil - c.peekDim(p)
777 dimMoves := old.veil != c.peekDim(p)
778 if oldRect == p.Rect && !dimMoves {
779 // The settled rect did not change: a running motion goes on
780 // as it is, and there is none to start.
781 continue
782 }
783 if sc.rects == nil {
784 sc.rects = make(map[WindowID]rectMotion)
785 }
786 // Where the window was drawn, unrounded, relative to where it goes.
787 rm := sc.rects[p.ID]
788 if oldRect != p.Rect {
789 // A re-flow may change the aspect: the content zoom (appear)
790 // stops following the frame and snaps to the settled size; the
791 // fade goes on.
792 rm.scale = false
793 c.retargetComponent(&rm.x, &rm.dx, float64(oldRect.X-p.Rect.X)+old.off.x, old.vel.x, now)
794 c.retargetComponent(&rm.y, &rm.dy, float64(oldRect.Y-p.Rect.Y)+old.off.y, old.vel.y, now)
795 c.retargetComponent(&rm.w, &rm.dw, float64(oldRect.W-p.Rect.W)+old.off.w, old.vel.w, now)
796 c.retargetComponent(&rm.h, &rm.dh, float64(oldRect.H-p.Rect.H)+old.off.h, old.vel.h, now)
797 }
798 if dimMoves {
799 c.retargetLevel(&rm.dim, &rm.ddim, dimOff, old.dimV, now)
800 }
801 if rm.on() {
802 sc.rects[p.ID], sc.rectsWS = rm, m.Current()
803 } else {
804 delete(sc.rects, p.ID)
805 }
806 }
807}
808
809// transitionStash animates the stash windows an action shows or hides on
810// the workspace b's screen keeps on screen (a toggle-stash-visible, a click
811// that closes the stash, a navigation that brings a window to a margin or
812// sends it off): one drawn after and not before appears (appearAt), one
813// drawn before and hidden after leaves from its last settled placement
814// (leave). A window that is gone from the layout (unstashed, moved) or
815// fullscreen is not touched: its rect motion, if any, is transitionRects's.
816// Nothing during a session lock or with a covering fullscreen window (it
817// keeps its direct scanout path).
818func (c *Core) transitionStash(b *viewShot, now time.Time) {
819 sc := b.sc
820 m := sc.mon
821 w := m.Current()
822 if w != b.ws || len(w.Stash) == 0 || c.security.Protected || w.cover() != 0 {
823 return
824 }
825 layout := m.Layout()
826 for _, p := range b.stash {
827 i := slices.IndexFunc(layout, func(q Placement) bool { return q.ID == p.ID })
828 if i >= 0 && layout[i].Hidden && !p.Fullscreen {
829 var shot *rectShot
830 if j := slices.IndexFunc(b.rects, func(r rectShot) bool { return r.id == p.ID }); j >= 0 {
831 shot = &b.rects[j]
832 }
833 c.leaveFrom(sc, p, shot, now)
834 }
835 }
836 for _, p := range layout {
837 if p.Hidden || p.Fullscreen || w.stashIndex(p.ID) < 0 || slices.ContainsFunc(b.rects, func(r rectShot) bool { return r.id == p.ID }) {
838 continue
839 }
840 c.appearAt(sc, p, now)
841 }
842}
843
844// peekDim is the veil the settled placement p draws: the configured stash
845// dim for a peek, none for another window. The placement's Dim is an
846// offset from it.
847func (c *Core) peekDim(p Placement) float64 {
848 if p.Peek {
849 return c.cfg.Stash.Dim
850 }
851 return 0
852}
853
854// transitionOverview starts the motions of an action that opened or closed
855// the overview, or moved inside it (the selection, the rows). The camera
856// springs stay stopped (ToggleOverview): every window drawn before and
857// after moves from its shown rect to its settled one, its content scaled
858// with it (a card's Zoom, rectMotion.show). A window drawn after and not
859// before (another row, a column the overview hid) fades in with no motion
860// of its own; one drawn before and not after just goes. Motions of the
861// other state are dropped first: their rects are not the ones snapshot
862// measured from. A fade that ran on a window drawn in both states carries
863// over (picking a card that still fades in does not pop it).
864func (c *Core) transitionOverview(b *viewShot, now time.Time) {
865 sc := b.sc
866 changed := b.overview != sc.mon.ov.open
867 if changed {
868 sc.stopRects()
869 }
870 for _, p := range sc.mon.Layout() {
871 if p.Hidden {
872 continue
873 }
874 rm := sc.rects[p.ID]
875 i := slices.IndexFunc(b.rects, func(r rectShot) bool { return r.id == p.ID })
876 if i >= 0 && changed {
877 c.retargetLevel(&rm.fade, &rm.df, b.rects[i].fade, b.rects[i].fadeV, now)
878 }
879 if i < 0 {
880 c.retargetLevel(&rm.fade, &rm.df, 1, 0, now)
881 } else if old := &b.rects[i]; old.rect != p.Rect {
882 rm.scale = true
883 c.retargetComponent(&rm.x, &rm.dx, float64(old.rect.X-p.Rect.X)+old.off.x, old.vel.x, now)
884 c.retargetComponent(&rm.y, &rm.dy, float64(old.rect.Y-p.Rect.Y)+old.off.y, old.vel.y, now)
885 c.retargetComponent(&rm.w, &rm.dw, float64(old.rect.W-p.Rect.W)+old.off.w, old.vel.w, now)
886 c.retargetComponent(&rm.h, &rm.dh, float64(old.rect.H-p.Rect.H)+old.off.h, old.vel.h, now)
887 } else {
888 continue
889 }
890 if !rm.on() {
891 delete(sc.rects, p.ID)
892 continue
893 }
894 if sc.rects == nil {
895 sc.rects = make(map[WindowID]rectMotion)
896 }
897 sc.rects[p.ID], sc.rectsWS = rm, sc.rectsOwner()
898 }
899}
900
901// movedFrom is the shot of the other screen that showed the workspace b's
902// screen shows now (moved to it by an action), or nil.
903func movedFrom(b *viewShot, before []viewShot) *viewShot {
904 w := b.sc.mon.Current()
905 for i := range before {
906 if o := &before[i]; o.ws == w && o.sc != b.sc && !o.overview {
907 return o
908 }
909 }
910 return nil
911}