detached 3f1045d

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 VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · 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 {
562562 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)}
563563 // layout is what is drawn; settled (same indexes) is where the
564564 // windows are going, and alone sizes the configures.
565- layout, settled := sc.shown, sc.settled
565+ layout, settled := sc.shown, sc.settledLayout
566566 var real map[WindowID]Placement
567567 if sc.mon.ov.open {
568568 real = make(map[WindowID]Placement)
@@ -629,11 +629,16 @@ func (c *Core) publish(ctx context.Context) error {
629629 scene.Windows = append(scene.Windows, c.scenePopups(sc)...)
630630 scene.CaptureIndicators = c.captureIndicators(sc)
631631 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
633637 // keeps its output's Seq while it draws the same, so an idle output
634638 // is not recomposed.
635639 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) {
637642 scene.Seq = sc.last.Seq
638643 } else {
639644 c.seq++
@@ -696,13 +701,28 @@ func (c *Core) publish(ctx context.Context) error {
696701 // step moves the running slides of only (nil: every screen) to now and
697702 // publishes the frame.
698703 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+ }
700709 c.animate(c.now(), only)
701710 c.stepping = only
702711 defer func() { c.stepping = nil }()
703712 return c.slid(ctx, false)
704713 }
705714
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+
706726 // slid publishes a moved view; shown reports another workspace came on
707727 // screen.
708728 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 the563 // 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.settled565+ layout, settled := sc.shown, sc.settledLayout
566 var real map[WindowID]Placement566 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 other632+ // 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 output637 // 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.Seq642 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 and701 // 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 = only710 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 on726 // 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() {
351351 w.stopSlide()
352352 }
353353 }
354- clear(sc.rects)
354+ sc.stopRects()
355355 }
356356 c.stopFrame()
357357 }
@@ -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 {
194194 }
195195 v := s.Velocity
196196 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)
201199 return motion{spring: s, start: now, end: end, slow: slow, v: v, on: true}
202200 }
203201
@@ -194,10 +194,8 @@ func newMotion(s spring, now time.Time, slow float64) motion {
194 }194 }
195 v := s.Velocity195 v := s.Velocity
196 s.Velocity *= slow196 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 {
104104 // A swipe or its landing slide still shows the neighbours: decide on
105105 // the settled layout, or a window left alone would start a pulse cut
106106 // short.
107- if !c.cur().isSettled() {
107+ if !c.cur().settled() {
108108 p.timerC, p.timerStop = newTimer(c.ch.Clock, pulseRecheck)
109109 return false
110110 }
@@ -138,7 +138,7 @@ func (c *Core) advancePulse(now time.Time) float64 {
138138 }
139139
140140 // 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).
142142 func (m *Monitor) settled() bool {
143143 if m.switchMotion.on || m.switchOff != 0 {
144144 return false
@@ -104,7 +104,7 @@ func (c *Core) pulseTick() bool {
104 // A swipe or its landing slide still shows the neighbours: decide on104 // 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 cut105 // 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 false109 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 layout140 // 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 false144 return false
M internal/core/screens.go +9 -6
@@ -31,13 +31,13 @@ type screen struct {
3131 // last is the scene published for this screen: an identical next one
3232 // keeps its Seq (Scene.SameAs). Zero after a protected publish.
3333 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
3636 // drawn and hit-tested. They are the same slice while no rect motion
3737 // runs. Both are built once per publish (refreshShown); shown is nil
3838 // 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
4141 // rects are the running per-window rect motions (transition.go) of
4242 // rectsWS, the workspace they began on; created lazily.
4343 rects map[WindowID]rectMotion
@@ -47,9 +47,12 @@ type screen struct {
4747 // springing reports whether a camera or rect spring runs on the screen.
4848 func (s *screen) springing() bool { return s.mon.springing() || len(s.rects) > 0 }
4949
50-// isSettled reports whether nothing moves the screen's layout: no swipe,
50+// settled reports whether nothing moves the screen's layout: no swipe,
5151 // 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 }
5356
5457 // shownLayout is the layout as drawn, for hit-testing and popups; before
5558 // 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 one31 // 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.Scene33 last ports.Scene
34- // settled is the monitor's layout as of the last publish: what configures34+ // 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 is35+ // 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 motion36 // drawn and hit-tested. They are the same slice while no rect motion
37 // runs. Both are built once per publish (refreshShown); shown is nil37 // 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 []Placement39+ settledLayout, shown []Placement
40- shownBuf []Placement40+ shownBuf []Placement
41 // rects are the running per-window rect motions (transition.go) of41 // 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]rectMotion43 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; before57 // 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 {
181181 c.seq++
182182 // The desktop scene that follows must not reuse a Seq from before.
183183 sc.last = ports.Scene{}
184- sc.settled, sc.shown = nil, nil
184+ sc.settledLayout, sc.shown = nil, nil
185185 o := sc.mon.Output()
186186 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"}
187187 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, nil184+ 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 (
1313 // (Workspace.shift, Monitor.switchOff), starting from what was on screen.
1414 // Nothing here runs for events that come from clients or outputs.
1515 //
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
1717 // using the settled rect, so the client resizes once to its final size; what
1818 // is drawn and hit-tested is the settled rect plus an offset that a spring
1919 // takes to zero. The offsets are relative to the camera-shifted layout, so a
@@ -27,6 +27,10 @@ type viewShot struct {
2727 // (viewX plus shift).
2828 viewX int
2929 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
3034 // list and pos are the numbered workspaces and the monitor's fractional
3135 // position in them (index of the current one plus switchOff); ok is
3236 // false when there is none (a shown stash workspace).
@@ -108,14 +112,25 @@ func (c *Core) refreshShown() {
108112 for _, sc := range c.screens {
109113 m := sc.mon
110114 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+ }
112128 }
113- sc.settled = m.Layout()
114129 if len(sc.rects) == 0 {
115- sc.shown = sc.settled
130+ sc.shown = sc.settledLayout
116131 continue
117132 }
118- buf := append(sc.shownBuf[:0], sc.settled...)
133+ buf := append(sc.shownBuf[:0], sc.settledLayout...)
119134 for i := range buf {
120135 p := &buf[i]
121136 if p.Hidden || p.Preview > 0 {
@@ -146,6 +161,12 @@ func (c *Core) snapshot() []viewShot {
146161 if !m.ov.open {
147162 w := m.Current()
148163 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+ }
149170 for _, p := range sc.shown {
150171 if !p.Hidden && p.Preview == 0 {
151172 s.rects = append(s.rects, rectShot{p.ID, p.Rect})
@@ -207,16 +228,14 @@ func (c *Core) transitionCamera(b *viewShot, now time.Time) {
207228 }
208229 // A running landing slide was retargeted by scroll(): its velocity
209230 // carries over, and the spring starts now like every action's.
210- v := w.motion.velocity()
211231 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)
213233 case b.ok && m.shown == nil && !c.movedAway(b.sc, b.ws):
214234 j := indexOf(b.list, w)
215235 if j < 0 {
216236 return
217237 }
218238 off := b.pos - float64(j)
219- v := m.switchMotion.velocity()
220239 // The snapshot's list is reused by the next action: the slide keeps
221240 // its own copy.
222241 m.switchOff, m.switchList = off, slices.Clone(b.list)
@@ -224,7 +243,7 @@ func (c *Core) transitionCamera(b *viewShot, now time.Time) {
224243 m.stopSwitch()
225244 return
226245 }
227- m.switchMotion = c.spring(workspaceSpring(off, v), now)
246+ m.switchMotion = c.spring(workspaceSpring(off, b.switchV), now)
228247 }
229248 }
230249
@@ -236,8 +255,8 @@ func (c *Core) transitionCamera(b *viewShot, now time.Time) {
236255 func (c *Core) transitionRects(b *viewShot, now time.Time) {
237256 sc := b.sc
238257 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()
241260 return
242261 }
243262 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 keep16+// 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; what17 // 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 spring18 // 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 a19 // 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 int28 viewX int
29 view float6429 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 fractional34 // list and pos are the numbered workspaces and the monitor's fractional
31 // position in them (index of the current one plus switchOff); ok is35 // 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.mon113 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.settled130+ sc.shown = sc.settledLayout
116 continue131 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.shift163 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 velocity229 // 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 return236 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 keeps239 // 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 return244 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.sc256 sc := b.sc
238 m := sc.mon257 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 return260 return
242 }261 }
243 for _, p := range m.Layout() {262 for _, p := range m.Layout() {

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 23

Tags 10

↑↓ moveenter openesc close