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