fix(core): give an animating screen a fresh scene Seq and keep its fallback timer
An animating screen skips the Scene.SameAs Seq reuse, so the first frame after an action is flipped instead of waiting for the fallback timer. A flip of one output keeps the fallback timer while another still animates. Chained workspace keys keep their spring velocity (recorded in the snapshot), and rect motions of windows out of the layout are dropped. screen.stopRects joins the clearing sites; settled/isSettled are renamed.
- commit
- 3f1045df9503a191a41fd326c8ef40a0db4ce2b2
- author
- brice · · 2026-10-04 19:06 UTC
- committer
- brice · · 2026-10-04 19:08 UTC
- parent
- ff29f3d
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
7 files · +69 -29
M internal/core/core.go +24 -4
| @@ -562,7 +562,7 @@ func (c *Core) publish(ctx context.Context) error { | |||
| 562 | 562 | scene := ports.Scene{Security: c.security, Output: sc.name(), OutputWidth: o.W, OutputHeight: o.H, WorkspaceClip: clip, Scale: sc.scale, Transform: sc.transform, Off: sc.off, Background: c.cfg.Background.Color, Border: ports.Border{Width: c.cfg.Border.Width, Active: c.cfg.Border.Active, Inactive: c.cfg.Border.Inactive}, Windows: make([]ports.SceneWindow, 0), Layers: shownLayers(sc)} | |
| 563 | 563 | // layout is what is drawn; settled (same indexes) is where the | |
| 564 | 564 | // windows are going, and alone sizes the configures. | |
| 565 | - | layout, settled := sc.shown, sc.settled | |
| 565 | + | layout, settled := sc.shown, sc.settledLayout | |
| 566 | 566 | var real map[WindowID]Placement | |
| 567 | 567 | if sc.mon.ov.open { | |
| 568 | 568 | real = make(map[WindowID]Placement) | |
| @@ -629,11 +629,16 @@ func (c *Core) publish(ctx context.Context) error { | |||
| 629 | 629 | scene.Windows = append(scene.Windows, c.scenePopups(sc)...) | |
| 630 | 630 | scene.CaptureIndicators = c.captureIndicators(sc) | |
| 631 | 631 | scene.Capture = c.captureSceneFor(sc, capture) | |
| 632 | - | // A scene carrying a capture image always gets a fresh Seq; any other | |
| 632 | + | // A scene carrying a capture image always gets a fresh Seq, and so | |
| 633 | + | // does one of an animating screen (the one being stepped, or any | |
| 634 | + | // when none is): the outputs drop a scene they already show, and a | |
| 635 | + | // spring that starts at the previous scene, or rounds to it in its | |
| 636 | + | // tail, would get no flip and wait for the fallback timer. Any other | |
| 633 | 637 | // keeps its output's Seq while it draws the same, so an idle output | |
| 634 | 638 | // is not recomposed. | |
| 635 | 639 | withCapture := scene.Capture != nil && (sc == capture.hiddenScr || sc == capture.windowScr) | |
| 636 | - | if !withCapture && sc.last.Seq != 0 && scene.SameAs(sc.last) { | |
| 640 | + | animating := (sc.springing() || c.pulsing(sc)) && (c.stepping == nil || c.stepping == sc) | |
| 641 | + | if !withCapture && !animating && sc.last.Seq != 0 && scene.SameAs(sc.last) { | |
| 637 | 642 | scene.Seq = sc.last.Seq | |
| 638 | 643 | } else { | |
| 639 | 644 | c.seq++ | |
| @@ -696,13 +701,28 @@ func (c *Core) publish(ctx context.Context) error { | |||
| 696 | 701 | // step moves the running slides of only (nil: every screen) to now and | |
| 697 | 702 | // publishes the frame. | |
| 698 | 703 | func (c *Core) step(ctx context.Context, only *screen) error { | |
| 699 | - | c.stopFrame() | |
| 704 | + | // The fallback timer guards every animating output: a flip of one keeps | |
| 705 | + | // it running while another still animates, or that one could starve. | |
| 706 | + | if only == nil || !c.animatingOther(only) { | |
| 707 | + | c.stopFrame() | |
| 708 | + | } | |
| 700 | 709 | c.animate(c.now(), only) | |
| 701 | 710 | c.stepping = only | |
| 702 | 711 | defer func() { c.stepping = nil }() | |
| 703 | 712 | return c.slid(ctx, false) | |
| 704 | 713 | } | |
| 705 | 714 | ||
| 715 | + | // animatingOther reports whether a spring or the pulse runs on a screen | |
| 716 | + | // other than only. | |
| 717 | + | func (c *Core) animatingOther(only *screen) bool { | |
| 718 | + | for _, sc := range c.screens { | |
| 719 | + | if sc != only && (sc.springing() || c.pulsing(sc)) { | |
| 720 | + | return true | |
| 721 | + | } | |
| 722 | + | } | |
| 723 | + | return false | |
| 724 | + | } | |
| 725 | + | ||
| 706 | 726 | // slid publishes a moved view; shown reports another workspace came on | |
| 707 | 727 | // screen. | |
| 708 | 728 | func (c *Core) slid(ctx context.Context, shown bool) error { | |
| @@ -562,7 +562,7 @@ func (c *Core) publish(ctx context.Context) error { | |||||
| 562 | scene := ports.Scene{Security: c.security, Output: sc.name(), OutputWidth: o.W, OutputHeight: o.H, WorkspaceClip: clip, Scale: sc.scale, Transform: sc.transform, Off: sc.off, Background: c.cfg.Background.Color, Border: ports.Border{Width: c.cfg.Border.Width, Active: c.cfg.Border.Active, Inactive: c.cfg.Border.Inactive}, Windows: make([]ports.SceneWindow, 0), Layers: shownLayers(sc)} | 562 | scene := ports.Scene{Security: c.security, Output: sc.name(), OutputWidth: o.W, OutputHeight: o.H, WorkspaceClip: clip, Scale: sc.scale, Transform: sc.transform, Off: sc.off, Background: c.cfg.Background.Color, Border: ports.Border{Width: c.cfg.Border.Width, Active: c.cfg.Border.Active, Inactive: c.cfg.Border.Inactive}, Windows: make([]ports.SceneWindow, 0), Layers: shownLayers(sc)} | ||
| 563 | // layout is what is drawn; settled (same indexes) is where the | 563 | // layout is what is drawn; settled (same indexes) is where the | ||
| 564 | // windows are going, and alone sizes the configures. | 564 | // windows are going, and alone sizes the configures. | ||
| 565 | - | layout, settled := sc.shown, sc.settled | 565 | + | layout, settled := sc.shown, sc.settledLayout |
| 566 | var real map[WindowID]Placement | 566 | var real map[WindowID]Placement | ||
| 567 | if sc.mon.ov.open { | 567 | if sc.mon.ov.open { | ||
| 568 | real = make(map[WindowID]Placement) | 568 | real = make(map[WindowID]Placement) | ||
| @@ -629,11 +629,16 @@ func (c *Core) publish(ctx context.Context) error { | |||||
| 629 | scene.Windows = append(scene.Windows, c.scenePopups(sc)...) | 629 | scene.Windows = append(scene.Windows, c.scenePopups(sc)...) | ||
| 630 | scene.CaptureIndicators = c.captureIndicators(sc) | 630 | scene.CaptureIndicators = c.captureIndicators(sc) | ||
| 631 | scene.Capture = c.captureSceneFor(sc, capture) | 631 | scene.Capture = c.captureSceneFor(sc, capture) | ||
| 632 | - | // A scene carrying a capture image always gets a fresh Seq; any other | 632 | + | // A scene carrying a capture image always gets a fresh Seq, and so |
| 633 | + | // does one of an animating screen (the one being stepped, or any | |||
| 634 | + | // when none is): the outputs drop a scene they already show, and a | |||
| 635 | + | // spring that starts at the previous scene, or rounds to it in its | |||
| 636 | + | // tail, would get no flip and wait for the fallback timer. Any other | |||
| 633 | // keeps its output's Seq while it draws the same, so an idle output | 637 | // keeps its output's Seq while it draws the same, so an idle output | ||
| 634 | // is not recomposed. | 638 | // is not recomposed. | ||
| 635 | withCapture := scene.Capture != nil && (sc == capture.hiddenScr || sc == capture.windowScr) | 639 | withCapture := scene.Capture != nil && (sc == capture.hiddenScr || sc == capture.windowScr) | ||
| 636 | - | if !withCapture && sc.last.Seq != 0 && scene.SameAs(sc.last) { | 640 | + | animating := (sc.springing() || c.pulsing(sc)) && (c.stepping == nil || c.stepping == sc) |
| 641 | + | if !withCapture && !animating && sc.last.Seq != 0 && scene.SameAs(sc.last) { | |||
| 637 | scene.Seq = sc.last.Seq | 642 | scene.Seq = sc.last.Seq | ||
| 638 | } else { | 643 | } else { | ||
| 639 | c.seq++ | 644 | c.seq++ | ||
| @@ -696,13 +701,28 @@ func (c *Core) publish(ctx context.Context) error { | |||||
| 696 | // step moves the running slides of only (nil: every screen) to now and | 701 | // step moves the running slides of only (nil: every screen) to now and | ||
| 697 | // publishes the frame. | 702 | // publishes the frame. | ||
| 698 | func (c *Core) step(ctx context.Context, only *screen) error { | 703 | func (c *Core) step(ctx context.Context, only *screen) error { | ||
| 699 | - | c.stopFrame() | 704 | + | // The fallback timer guards every animating output: a flip of one keeps |
| 705 | + | // it running while another still animates, or that one could starve. | |||
| 706 | + | if only == nil || !c.animatingOther(only) { | |||
| 707 | + | c.stopFrame() | |||
| 708 | + | } | |||
| 700 | c.animate(c.now(), only) | 709 | c.animate(c.now(), only) | ||
| 701 | c.stepping = only | 710 | c.stepping = only | ||
| 702 | defer func() { c.stepping = nil }() | 711 | defer func() { c.stepping = nil }() | ||
| 703 | return c.slid(ctx, false) | 712 | return c.slid(ctx, false) | ||
| 704 | } | 713 | } | ||
| 705 | 714 | ||||
| 715 | + | // animatingOther reports whether a spring or the pulse runs on a screen | |||
| 716 | + | // other than only. | |||
| 717 | + | func (c *Core) animatingOther(only *screen) bool { | |||
| 718 | + | for _, sc := range c.screens { | |||
| 719 | + | if sc != only && (sc.springing() || c.pulsing(sc)) { | |||
| 720 | + | return true | |||
| 721 | + | } | |||
| 722 | + | } | |||
| 723 | + | return false | |||
| 724 | + | } | |||
| 725 | + | ||||
| 706 | // slid publishes a moved view; shown reports another workspace came on | 726 | // slid publishes a moved view; shown reports another workspace came on | ||
| 707 | // screen. | 727 | // screen. | ||
| 708 | func (c *Core) slid(ctx context.Context, shown bool) error { | 728 | func (c *Core) slid(ctx context.Context, shown bool) error { | ||
M internal/core/gesture.go +1 -1
| @@ -351,7 +351,7 @@ func (c *Core) stopAnimations() { | |||
| 351 | 351 | w.stopSlide() | |
| 352 | 352 | } | |
| 353 | 353 | } | |
| 354 | - | clear(sc.rects) | |
| 354 | + | sc.stopRects() | |
| 355 | 355 | } | |
| 356 | 356 | c.stopFrame() | |
| 357 | 357 | } | |
| @@ -351,7 +351,7 @@ func (c *Core) stopAnimations() { | |||||
| 351 | w.stopSlide() | 351 | w.stopSlide() | ||
| 352 | } | 352 | } | ||
| 353 | } | 353 | } | ||
| 354 | - | clear(sc.rects) | 354 | + | sc.stopRects() |
| 355 | } | 355 | } | ||
| 356 | c.stopFrame() | 356 | c.stopFrame() | ||
| 357 | } | 357 | } | ||
M internal/core/motion.go +2 -4
| @@ -194,10 +194,8 @@ func newMotion(s spring, now time.Time, slow float64) motion { | |||
| 194 | 194 | } | |
| 195 | 195 | v := s.Velocity | |
| 196 | 196 | s.Velocity *= slow | |
| 197 | - | end := time.Duration(math.MaxInt64) | |
| 198 | - | if f := float64(s.duration()) * slow; f < float64(math.MaxInt64) { | |
| 199 | - | end = time.Duration(f) | |
| 200 | - | } | |
| 197 | + | // The slowdown is capped by the config, so this cannot overflow. | |
| 198 | + | end := time.Duration(float64(s.duration()) * slow) | |
| 201 | 199 | return motion{spring: s, start: now, end: end, slow: slow, v: v, on: true} | |
| 202 | 200 | } | |
| 203 | 201 | ||
| @@ -194,10 +194,8 @@ func newMotion(s spring, now time.Time, slow float64) motion { | |||||
| 194 | } | 194 | } | ||
| 195 | v := s.Velocity | 195 | v := s.Velocity | ||
| 196 | s.Velocity *= slow | 196 | s.Velocity *= slow | ||
| 197 | - | end := time.Duration(math.MaxInt64) | 197 | + | // The slowdown is capped by the config, so this cannot overflow. |
| 198 | - | if f := float64(s.duration()) * slow; f < float64(math.MaxInt64) { | 198 | + | end := time.Duration(float64(s.duration()) * slow) |
| 199 | - | end = time.Duration(f) | |||
| 200 | - | } | |||
| 201 | return motion{spring: s, start: now, end: end, slow: slow, v: v, on: true} | 199 | return motion{spring: s, start: now, end: end, slow: slow, v: v, on: true} | ||
| 202 | } | 200 | } | ||
| 203 | 201 | ||||
M internal/core/pulse.go +2 -2
| @@ -104,7 +104,7 @@ func (c *Core) pulseTick() bool { | |||
| 104 | 104 | // A swipe or its landing slide still shows the neighbours: decide on | |
| 105 | 105 | // the settled layout, or a window left alone would start a pulse cut | |
| 106 | 106 | // short. | |
| 107 | - | if !c.cur().isSettled() { | |
| 107 | + | if !c.cur().settled() { | |
| 108 | 108 | p.timerC, p.timerStop = newTimer(c.ch.Clock, pulseRecheck) | |
| 109 | 109 | return false | |
| 110 | 110 | } | |
| @@ -138,7 +138,7 @@ func (c *Core) advancePulse(now time.Time) float64 { | |||
| 138 | 138 | } | |
| 139 | 139 | ||
| 140 | 140 | // settled reports whether no swipe or slide moves the monitor's layout | |
| 141 | - | // (screen.isSettled adds the rect motions). | |
| 141 | + | // (screen.settled adds the rect motions). | |
| 142 | 142 | func (m *Monitor) settled() bool { | |
| 143 | 143 | if m.switchMotion.on || m.switchOff != 0 { | |
| 144 | 144 | return false | |
| @@ -104,7 +104,7 @@ func (c *Core) pulseTick() bool { | |||||
| 104 | // A swipe or its landing slide still shows the neighbours: decide on | 104 | // A swipe or its landing slide still shows the neighbours: decide on | ||
| 105 | // the settled layout, or a window left alone would start a pulse cut | 105 | // the settled layout, or a window left alone would start a pulse cut | ||
| 106 | // short. | 106 | // short. | ||
| 107 | - | if !c.cur().isSettled() { | 107 | + | if !c.cur().settled() { |
| 108 | p.timerC, p.timerStop = newTimer(c.ch.Clock, pulseRecheck) | 108 | p.timerC, p.timerStop = newTimer(c.ch.Clock, pulseRecheck) | ||
| 109 | return false | 109 | return false | ||
| 110 | } | 110 | } | ||
| @@ -138,7 +138,7 @@ func (c *Core) advancePulse(now time.Time) float64 { | |||||
| 138 | } | 138 | } | ||
| 139 | 139 | ||||
| 140 | // settled reports whether no swipe or slide moves the monitor's layout | 140 | // settled reports whether no swipe or slide moves the monitor's layout | ||
| 141 | - | // (screen.isSettled adds the rect motions). | 141 | + | // (screen.settled adds the rect motions). |
| 142 | func (m *Monitor) settled() bool { | 142 | func (m *Monitor) settled() bool { | ||
| 143 | if m.switchMotion.on || m.switchOff != 0 { | 143 | if m.switchMotion.on || m.switchOff != 0 { | ||
| 144 | return false | 144 | return false | ||
M internal/core/screens.go +9 -6
| @@ -31,13 +31,13 @@ type screen struct { | |||
| 31 | 31 | // last is the scene published for this screen: an identical next one | |
| 32 | 32 | // keeps its Seq (Scene.SameAs). Zero after a protected publish. | |
| 33 | 33 | last ports.Scene | |
| 34 | - | // settled is the monitor's layout as of the last publish: what configures | |
| 35 | - | // are sized from. shown is the same with the rect motions applied: what is | |
| 34 | + | // settledLayout is the monitor's layout as of the last publish: what | |
| 35 | + | // configures are sized from. shown is the same with the rect motions applied: what is | |
| 36 | 36 | // drawn and hit-tested. They are the same slice while no rect motion | |
| 37 | 37 | // runs. Both are built once per publish (refreshShown); shown is nil | |
| 38 | 38 | // before the first publish, then callers measure the monitor's layout. | |
| 39 | - | settled, shown []Placement | |
| 40 | - | shownBuf []Placement | |
| 39 | + | settledLayout, shown []Placement | |
| 40 | + | shownBuf []Placement | |
| 41 | 41 | // rects are the running per-window rect motions (transition.go) of | |
| 42 | 42 | // rectsWS, the workspace they began on; created lazily. | |
| 43 | 43 | rects map[WindowID]rectMotion | |
| @@ -47,9 +47,12 @@ type screen struct { | |||
| 47 | 47 | // springing reports whether a camera or rect spring runs on the screen. | |
| 48 | 48 | func (s *screen) springing() bool { return s.mon.springing() || len(s.rects) > 0 } | |
| 49 | 49 | ||
| 50 | - | // isSettled reports whether nothing moves the screen's layout: no swipe, | |
| 50 | + | // settled reports whether nothing moves the screen's layout: no swipe, | |
| 51 | 51 | // no slide, no rect motion. | |
| 52 | - | func (s *screen) isSettled() bool { return s.mon.settled() && len(s.rects) == 0 } | |
| 52 | + | func (s *screen) settled() bool { return s.mon.settled() && len(s.rects) == 0 } | |
| 53 | + | ||
| 54 | + | // stopRects drops the rect motions: the windows are drawn where they are. | |
| 55 | + | func (s *screen) stopRects() { clear(s.rects); s.rectsWS = nil } | |
| 53 | 56 | ||
| 54 | 57 | // shownLayout is the layout as drawn, for hit-testing and popups; before | |
| 55 | 58 | // the first publish it is the monitor's. | |
| @@ -31,13 +31,13 @@ type screen struct { | |||||
| 31 | // last is the scene published for this screen: an identical next one | 31 | // last is the scene published for this screen: an identical next one | ||
| 32 | // keeps its Seq (Scene.SameAs). Zero after a protected publish. | 32 | // keeps its Seq (Scene.SameAs). Zero after a protected publish. | ||
| 33 | last ports.Scene | 33 | last ports.Scene | ||
| 34 | - | // settled is the monitor's layout as of the last publish: what configures | 34 | + | // settledLayout is the monitor's layout as of the last publish: what |
| 35 | - | // are sized from. shown is the same with the rect motions applied: what is | 35 | + | // configures are sized from. shown is the same with the rect motions applied: what is |
| 36 | // drawn and hit-tested. They are the same slice while no rect motion | 36 | // drawn and hit-tested. They are the same slice while no rect motion | ||
| 37 | // runs. Both are built once per publish (refreshShown); shown is nil | 37 | // runs. Both are built once per publish (refreshShown); shown is nil | ||
| 38 | // before the first publish, then callers measure the monitor's layout. | 38 | // before the first publish, then callers measure the monitor's layout. | ||
| 39 | - | settled, shown []Placement | 39 | + | settledLayout, shown []Placement |
| 40 | - | shownBuf []Placement | 40 | + | shownBuf []Placement |
| 41 | // rects are the running per-window rect motions (transition.go) of | 41 | // rects are the running per-window rect motions (transition.go) of | ||
| 42 | // rectsWS, the workspace they began on; created lazily. | 42 | // rectsWS, the workspace they began on; created lazily. | ||
| 43 | rects map[WindowID]rectMotion | 43 | rects map[WindowID]rectMotion | ||
| @@ -47,9 +47,12 @@ type screen struct { | |||||
| 47 | // springing reports whether a camera or rect spring runs on the screen. | 47 | // springing reports whether a camera or rect spring runs on the screen. | ||
| 48 | func (s *screen) springing() bool { return s.mon.springing() || len(s.rects) > 0 } | 48 | func (s *screen) springing() bool { return s.mon.springing() || len(s.rects) > 0 } | ||
| 49 | 49 | ||||
| 50 | - | // isSettled reports whether nothing moves the screen's layout: no swipe, | 50 | + | // settled reports whether nothing moves the screen's layout: no swipe, |
| 51 | // no slide, no rect motion. | 51 | // no slide, no rect motion. | ||
| 52 | - | func (s *screen) isSettled() bool { return s.mon.settled() && len(s.rects) == 0 } | 52 | + | func (s *screen) settled() bool { return s.mon.settled() && len(s.rects) == 0 } |
| 53 | + | ||||
| 54 | + | // stopRects drops the rect motions: the windows are drawn where they are. | |||
| 55 | + | func (s *screen) stopRects() { clear(s.rects); s.rectsWS = nil } | |||
| 53 | 56 | ||||
| 54 | // shownLayout is the layout as drawn, for hit-testing and popups; before | 57 | // shownLayout is the layout as drawn, for hit-testing and popups; before | ||
| 55 | // the first publish it is the monitor's. | 58 | // the first publish it is the monitor's. | ||
M internal/core/security.go +1 -1
| @@ -181,7 +181,7 @@ func (c *Core) publishProtected(ctx context.Context) error { | |||
| 181 | 181 | c.seq++ | |
| 182 | 182 | // The desktop scene that follows must not reuse a Seq from before. | |
| 183 | 183 | sc.last = ports.Scene{} | |
| 184 | - | sc.settled, sc.shown = nil, nil | |
| 184 | + | sc.settledLayout, sc.shown = nil, nil | |
| 185 | 185 | o := sc.mon.Output() | |
| 186 | 186 | scene := ports.Scene{Security: c.security, Output: sc.name(), Seq: c.seq, OutputWidth: o.W, OutputHeight: o.H, Scale: sc.scale, Transform: sc.transform, Off: sc.off, Background: "#000000"} | |
| 187 | 187 | if s, ok := c.lockSurface(sc); ok { | |
| @@ -181,7 +181,7 @@ func (c *Core) publishProtected(ctx context.Context) error { | |||||
| 181 | c.seq++ | 181 | c.seq++ | ||
| 182 | // The desktop scene that follows must not reuse a Seq from before. | 182 | // The desktop scene that follows must not reuse a Seq from before. | ||
| 183 | sc.last = ports.Scene{} | 183 | sc.last = ports.Scene{} | ||
| 184 | - | sc.settled, sc.shown = nil, nil | 184 | + | sc.settledLayout, sc.shown = nil, nil |
| 185 | o := sc.mon.Output() | 185 | o := sc.mon.Output() | ||
| 186 | scene := ports.Scene{Security: c.security, Output: sc.name(), Seq: c.seq, OutputWidth: o.W, OutputHeight: o.H, Scale: sc.scale, Transform: sc.transform, Off: sc.off, Background: "#000000"} | 186 | scene := ports.Scene{Security: c.security, Output: sc.name(), Seq: c.seq, OutputWidth: o.W, OutputHeight: o.H, Scale: sc.scale, Transform: sc.transform, Off: sc.off, Background: "#000000"} | ||
| 187 | if s, ok := c.lockSurface(sc); ok { | 187 | if s, ok := c.lockSurface(sc); ok { | ||
M internal/core/transition.go +30 -11
| @@ -13,7 +13,7 @@ import ( | |||
| 13 | 13 | // (Workspace.shift, Monitor.switchOff), starting from what was on screen. | |
| 14 | 14 | // Nothing here runs for events that come from clients or outputs. | |
| 15 | 15 | // | |
| 16 | - | // A window's rect transition (option B) is presentation too: configures keep | |
| 16 | + | // A window's rect transition (presentation-only rect transition) is presentation too: configures keep | |
| 17 | 17 | // using the settled rect, so the client resizes once to its final size; what | |
| 18 | 18 | // is drawn and hit-tested is the settled rect plus an offset that a spring | |
| 19 | 19 | // takes to zero. The offsets are relative to the camera-shifted layout, so a | |
| @@ -27,6 +27,10 @@ type viewShot struct { | |||
| 27 | 27 | // (viewX plus shift). | |
| 28 | 28 | viewX int | |
| 29 | 29 | view float64 | |
| 30 | + | // viewV and switchV are the speeds of the running view and workspace | |
| 31 | + | // springs (units per second, 0 without one). An action may stop them | |
| 32 | + | // (Monitor.Focus), so the transition reads them here. | |
| 33 | + | viewV, switchV float64 | |
| 30 | 34 | // list and pos are the numbered workspaces and the monitor's fractional | |
| 31 | 35 | // position in them (index of the current one plus switchOff); ok is | |
| 32 | 36 | // false when there is none (a shown stash workspace). | |
| @@ -108,14 +112,25 @@ func (c *Core) refreshShown() { | |||
| 108 | 112 | for _, sc := range c.screens { | |
| 109 | 113 | m := sc.mon | |
| 110 | 114 | if len(sc.rects) > 0 && (m.ov.open || sc.rectsWS != m.Current()) { | |
| 111 | - | clear(sc.rects) | |
| 115 | + | sc.stopRects() | |
| 116 | + | } | |
| 117 | + | sc.settledLayout = m.Layout() | |
| 118 | + | if len(sc.rects) > 0 { | |
| 119 | + | // A window that left the layout (or is hidden) keeps no motion: | |
| 120 | + | // it would ask for frames with nothing to move. | |
| 121 | + | for id := range sc.rects { | |
| 122 | + | if !slices.ContainsFunc(sc.settledLayout, func(p Placement) bool { | |
| 123 | + | return p.ID == id && !p.Hidden && p.Preview == 0 | |
| 124 | + | }) { | |
| 125 | + | delete(sc.rects, id) | |
| 126 | + | } | |
| 127 | + | } | |
| 112 | 128 | } | |
| 113 | - | sc.settled = m.Layout() | |
| 114 | 129 | if len(sc.rects) == 0 { | |
| 115 | - | sc.shown = sc.settled | |
| 130 | + | sc.shown = sc.settledLayout | |
| 116 | 131 | continue | |
| 117 | 132 | } | |
| 118 | - | buf := append(sc.shownBuf[:0], sc.settled...) | |
| 133 | + | buf := append(sc.shownBuf[:0], sc.settledLayout...) | |
| 119 | 134 | for i := range buf { | |
| 120 | 135 | p := &buf[i] | |
| 121 | 136 | if p.Hidden || p.Preview > 0 { | |
| @@ -146,6 +161,12 @@ func (c *Core) snapshot() []viewShot { | |||
| 146 | 161 | if !m.ov.open { | |
| 147 | 162 | w := m.Current() | |
| 148 | 163 | s.ws, s.viewX, s.view = w, w.ViewX, float64(w.ViewX)+w.shift | |
| 164 | + | if w.motion.on { | |
| 165 | + | s.viewV = w.motion.velocity() | |
| 166 | + | } | |
| 167 | + | if m.switchMotion.on { | |
| 168 | + | s.switchV = m.switchMotion.velocity() | |
| 169 | + | } | |
| 149 | 170 | for _, p := range sc.shown { | |
| 150 | 171 | if !p.Hidden && p.Preview == 0 { | |
| 151 | 172 | s.rects = append(s.rects, rectShot{p.ID, p.Rect}) | |
| @@ -207,16 +228,14 @@ func (c *Core) transitionCamera(b *viewShot, now time.Time) { | |||
| 207 | 228 | } | |
| 208 | 229 | // A running landing slide was retargeted by scroll(): its velocity | |
| 209 | 230 | // carries over, and the spring starts now like every action's. | |
| 210 | - | v := w.motion.velocity() | |
| 211 | 231 | w.shift = b.view - float64(w.ViewX) | |
| 212 | - | w.motion = c.spring(viewSpring(w.shift, v), now) | |
| 232 | + | w.motion = c.spring(viewSpring(w.shift, b.viewV), now) | |
| 213 | 233 | case b.ok && m.shown == nil && !c.movedAway(b.sc, b.ws): | |
| 214 | 234 | j := indexOf(b.list, w) | |
| 215 | 235 | if j < 0 { | |
| 216 | 236 | return | |
| 217 | 237 | } | |
| 218 | 238 | off := b.pos - float64(j) | |
| 219 | - | v := m.switchMotion.velocity() | |
| 220 | 239 | // The snapshot's list is reused by the next action: the slide keeps | |
| 221 | 240 | // its own copy. | |
| 222 | 241 | m.switchOff, m.switchList = off, slices.Clone(b.list) | |
| @@ -224,7 +243,7 @@ func (c *Core) transitionCamera(b *viewShot, now time.Time) { | |||
| 224 | 243 | m.stopSwitch() | |
| 225 | 244 | return | |
| 226 | 245 | } | |
| 227 | - | m.switchMotion = c.spring(workspaceSpring(off, v), now) | |
| 246 | + | m.switchMotion = c.spring(workspaceSpring(off, b.switchV), now) | |
| 228 | 247 | } | |
| 229 | 248 | } | |
| 230 | 249 | ||
| @@ -236,8 +255,8 @@ func (c *Core) transitionCamera(b *viewShot, now time.Time) { | |||
| 236 | 255 | func (c *Core) transitionRects(b *viewShot, now time.Time) { | |
| 237 | 256 | sc := b.sc | |
| 238 | 257 | m := sc.mon | |
| 239 | - | if m.ov.open || m.Current() != b.ws { | |
| 240 | - | clear(sc.rects) | |
| 258 | + | if m.Current() != b.ws { | |
| 259 | + | sc.stopRects() | |
| 241 | 260 | return | |
| 242 | 261 | } | |
| 243 | 262 | for _, p := range m.Layout() { | |
| @@ -13,7 +13,7 @@ import ( | |||||
| 13 | // (Workspace.shift, Monitor.switchOff), starting from what was on screen. | 13 | // (Workspace.shift, Monitor.switchOff), starting from what was on screen. | ||
| 14 | // Nothing here runs for events that come from clients or outputs. | 14 | // Nothing here runs for events that come from clients or outputs. | ||
| 15 | // | 15 | // | ||
| 16 | - | // A window's rect transition (option B) is presentation too: configures keep | 16 | + | // A window's rect transition (presentation-only rect transition) is presentation too: configures keep |
| 17 | // using the settled rect, so the client resizes once to its final size; what | 17 | // using the settled rect, so the client resizes once to its final size; what | ||
| 18 | // is drawn and hit-tested is the settled rect plus an offset that a spring | 18 | // is drawn and hit-tested is the settled rect plus an offset that a spring | ||
| 19 | // takes to zero. The offsets are relative to the camera-shifted layout, so a | 19 | // takes to zero. The offsets are relative to the camera-shifted layout, so a | ||
| @@ -27,6 +27,10 @@ type viewShot struct { | |||||
| 27 | // (viewX plus shift). | 27 | // (viewX plus shift). | ||
| 28 | viewX int | 28 | viewX int | ||
| 29 | view float64 | 29 | view float64 | ||
| 30 | + | // viewV and switchV are the speeds of the running view and workspace | |||
| 31 | + | // springs (units per second, 0 without one). An action may stop them | |||
| 32 | + | // (Monitor.Focus), so the transition reads them here. | |||
| 33 | + | viewV, switchV float64 | |||
| 30 | // list and pos are the numbered workspaces and the monitor's fractional | 34 | // list and pos are the numbered workspaces and the monitor's fractional | ||
| 31 | // position in them (index of the current one plus switchOff); ok is | 35 | // position in them (index of the current one plus switchOff); ok is | ||
| 32 | // false when there is none (a shown stash workspace). | 36 | // false when there is none (a shown stash workspace). | ||
| @@ -108,14 +112,25 @@ func (c *Core) refreshShown() { | |||||
| 108 | for _, sc := range c.screens { | 112 | for _, sc := range c.screens { | ||
| 109 | m := sc.mon | 113 | m := sc.mon | ||
| 110 | if len(sc.rects) > 0 && (m.ov.open || sc.rectsWS != m.Current()) { | 114 | if len(sc.rects) > 0 && (m.ov.open || sc.rectsWS != m.Current()) { | ||
| 111 | - | clear(sc.rects) | 115 | + | sc.stopRects() |
| 116 | + | } | |||
| 117 | + | sc.settledLayout = m.Layout() | |||
| 118 | + | if len(sc.rects) > 0 { | |||
| 119 | + | // A window that left the layout (or is hidden) keeps no motion: | |||
| 120 | + | // it would ask for frames with nothing to move. | |||
| 121 | + | for id := range sc.rects { | |||
| 122 | + | if !slices.ContainsFunc(sc.settledLayout, func(p Placement) bool { | |||
| 123 | + | return p.ID == id && !p.Hidden && p.Preview == 0 | |||
| 124 | + | }) { | |||
| 125 | + | delete(sc.rects, id) | |||
| 126 | + | } | |||
| 127 | + | } | |||
| 112 | } | 128 | } | ||
| 113 | - | sc.settled = m.Layout() | |||
| 114 | if len(sc.rects) == 0 { | 129 | if len(sc.rects) == 0 { | ||
| 115 | - | sc.shown = sc.settled | 130 | + | sc.shown = sc.settledLayout |
| 116 | continue | 131 | continue | ||
| 117 | } | 132 | } | ||
| 118 | - | buf := append(sc.shownBuf[:0], sc.settled...) | 133 | + | buf := append(sc.shownBuf[:0], sc.settledLayout...) |
| 119 | for i := range buf { | 134 | for i := range buf { | ||
| 120 | p := &buf[i] | 135 | p := &buf[i] | ||
| 121 | if p.Hidden || p.Preview > 0 { | 136 | if p.Hidden || p.Preview > 0 { | ||
| @@ -146,6 +161,12 @@ func (c *Core) snapshot() []viewShot { | |||||
| 146 | if !m.ov.open { | 161 | if !m.ov.open { | ||
| 147 | w := m.Current() | 162 | w := m.Current() | ||
| 148 | s.ws, s.viewX, s.view = w, w.ViewX, float64(w.ViewX)+w.shift | 163 | s.ws, s.viewX, s.view = w, w.ViewX, float64(w.ViewX)+w.shift | ||
| 164 | + | if w.motion.on { | |||
| 165 | + | s.viewV = w.motion.velocity() | |||
| 166 | + | } | |||
| 167 | + | if m.switchMotion.on { | |||
| 168 | + | s.switchV = m.switchMotion.velocity() | |||
| 169 | + | } | |||
| 149 | for _, p := range sc.shown { | 170 | for _, p := range sc.shown { | ||
| 150 | if !p.Hidden && p.Preview == 0 { | 171 | if !p.Hidden && p.Preview == 0 { | ||
| 151 | s.rects = append(s.rects, rectShot{p.ID, p.Rect}) | 172 | s.rects = append(s.rects, rectShot{p.ID, p.Rect}) | ||
| @@ -207,16 +228,14 @@ func (c *Core) transitionCamera(b *viewShot, now time.Time) { | |||||
| 207 | } | 228 | } | ||
| 208 | // A running landing slide was retargeted by scroll(): its velocity | 229 | // A running landing slide was retargeted by scroll(): its velocity | ||
| 209 | // carries over, and the spring starts now like every action's. | 230 | // carries over, and the spring starts now like every action's. | ||
| 210 | - | v := w.motion.velocity() | |||
| 211 | w.shift = b.view - float64(w.ViewX) | 231 | w.shift = b.view - float64(w.ViewX) | ||
| 212 | - | w.motion = c.spring(viewSpring(w.shift, v), now) | 232 | + | w.motion = c.spring(viewSpring(w.shift, b.viewV), now) |
| 213 | case b.ok && m.shown == nil && !c.movedAway(b.sc, b.ws): | 233 | case b.ok && m.shown == nil && !c.movedAway(b.sc, b.ws): | ||
| 214 | j := indexOf(b.list, w) | 234 | j := indexOf(b.list, w) | ||
| 215 | if j < 0 { | 235 | if j < 0 { | ||
| 216 | return | 236 | return | ||
| 217 | } | 237 | } | ||
| 218 | off := b.pos - float64(j) | 238 | off := b.pos - float64(j) | ||
| 219 | - | v := m.switchMotion.velocity() | |||
| 220 | // The snapshot's list is reused by the next action: the slide keeps | 239 | // The snapshot's list is reused by the next action: the slide keeps | ||
| 221 | // its own copy. | 240 | // its own copy. | ||
| 222 | m.switchOff, m.switchList = off, slices.Clone(b.list) | 241 | m.switchOff, m.switchList = off, slices.Clone(b.list) | ||
| @@ -224,7 +243,7 @@ func (c *Core) transitionCamera(b *viewShot, now time.Time) { | |||||
| 224 | m.stopSwitch() | 243 | m.stopSwitch() | ||
| 225 | return | 244 | return | ||
| 226 | } | 245 | } | ||
| 227 | - | m.switchMotion = c.spring(workspaceSpring(off, v), now) | 246 | + | m.switchMotion = c.spring(workspaceSpring(off, b.switchV), now) |
| 228 | } | 247 | } | ||
| 229 | } | 248 | } | ||
| 230 | 249 | ||||
| @@ -236,8 +255,8 @@ func (c *Core) transitionCamera(b *viewShot, now time.Time) { | |||||
| 236 | func (c *Core) transitionRects(b *viewShot, now time.Time) { | 255 | func (c *Core) transitionRects(b *viewShot, now time.Time) { | ||
| 237 | sc := b.sc | 256 | sc := b.sc | ||
| 238 | m := sc.mon | 257 | m := sc.mon | ||
| 239 | - | if m.ov.open || m.Current() != b.ws { | 258 | + | if m.Current() != b.ws { |
| 240 | - | clear(sc.rects) | 259 | + | sc.stopRects() |
| 241 | return | 260 | return | ||
| 242 | } | 261 | } | ||
| 243 | for _, p := range m.Layout() { | 262 | for _, p := range m.Layout() { | ||