fix(core): publish the settled scene when only Animating changes
- commit
- 6e5187ec6bdc0d3c1ab62a51b24434eb75e7b003
- author
- brice · · 2026-10-13 17:58 UTC
- parent
- 587fa9e
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
3 files · +57 -7
M internal/core/core.go +3 -2
| @@ -43,8 +43,9 @@ type Channels struct { | |||
| 43 | 43 | // Scales, when set, receives each scale a scale bind sets, to persist it. | |
| 44 | 44 | // Core does not wait: a full channel drops the change. | |
| 45 | 45 | Scales chan<- ports.ScaleChanged | |
| 46 | - | // Frames, when set, reports outputs' page flips: a running slide moves | |
| 47 | - | // one step per flip. Without flips it moves on a timer. | |
| 46 | + | // Frames, when set, carries the outputs' reports of each flip of an | |
| 47 | + | // animating scene (Scene.Animating): a running slide moves one step per | |
| 48 | + | // report. Without them it moves on a timer. | |
| 48 | 49 | Frames <-chan ports.OutputFrame | |
| 49 | 50 | } | |
| 50 | 51 | ||
| @@ -43,8 +43,9 @@ type Channels struct { | |||||
| 43 | // Scales, when set, receives each scale a scale bind sets, to persist it. | 43 | // Scales, when set, receives each scale a scale bind sets, to persist it. | ||
| 44 | // Core does not wait: a full channel drops the change. | 44 | // Core does not wait: a full channel drops the change. | ||
| 45 | Scales chan<- ports.ScaleChanged | 45 | Scales chan<- ports.ScaleChanged | ||
| 46 | - | // Frames, when set, reports outputs' page flips: a running slide moves | 46 | + | // Frames, when set, carries the outputs' reports of each flip of an |
| 47 | - | // one step per flip. Without flips it moves on a timer. | 47 | + | // animating scene (Scene.Animating): a running slide moves one step per |
| 48 | + | // report. Without them it moves on a timer. | |||
| 48 | Frames <-chan ports.OutputFrame | 49 | Frames <-chan ports.OutputFrame | ||
| 49 | } | 50 | } | ||
| 50 | 51 | ||||
M internal/core/scene.go +7 -5
| @@ -130,12 +130,14 @@ func (c *Core) sceneFor(ctx context.Context, i int, sc *screen, only *screen, ca | |||
| 130 | 130 | // does one of an animating screen (the one being stepped, or any | |
| 131 | 131 | // when none is): the outputs drop a scene they already show, and a | |
| 132 | 132 | // spring that starts at the previous scene, or rounds to it in its | |
| 133 | - | // tail, would get no flip and wait for the fallback timer. Any other | |
| 134 | - | // keeps its output's Seq while it draws the same, so an idle output | |
| 135 | - | // is not recomposed. | |
| 133 | + | // tail, would get no flip and wait for the fallback timer. So does the | |
| 134 | + | // scene that flips Animating off: SameAs ignores the flag, and keeping | |
| 135 | + | // the Seq of the animating scene would leave outputs showing it, and | |
| 136 | + | // waking core on every later flip. Any other keeps its output's Seq | |
| 137 | + | // while it draws the same, so an idle output is not recomposed. | |
| 136 | 138 | withCapture := scene.Capture != nil && (sc == capture.hiddenScr || sc == capture.windowScr) | |
| 137 | - | animating := (sc.springing() || c.pulsing(sc)) && (only == nil || only == sc) | |
| 138 | - | if !withCapture && !animating && sc.last.Seq != 0 && scene.SameAs(sc.last) { | |
| 139 | + | animating := scene.Animating && (only == nil || only == sc) | |
| 140 | + | if !withCapture && !animating && sc.last.Animating == scene.Animating && sc.last.Seq != 0 && scene.SameAs(sc.last) { | |
| 139 | 141 | scene.Seq = sc.last.Seq | |
| 140 | 142 | } else { | |
| 141 | 143 | c.seq++ | |
| @@ -130,12 +130,14 @@ func (c *Core) sceneFor(ctx context.Context, i int, sc *screen, only *screen, ca | |||||
| 130 | // does one of an animating screen (the one being stepped, or any | 130 | // does one of an animating screen (the one being stepped, or any | ||
| 131 | // when none is): the outputs drop a scene they already show, and a | 131 | // when none is): the outputs drop a scene they already show, and a | ||
| 132 | // spring that starts at the previous scene, or rounds to it in its | 132 | // spring that starts at the previous scene, or rounds to it in its | ||
| 133 | - | // tail, would get no flip and wait for the fallback timer. Any other | 133 | + | // tail, would get no flip and wait for the fallback timer. So does the |
| 134 | - | // keeps its output's Seq while it draws the same, so an idle output | 134 | + | // scene that flips Animating off: SameAs ignores the flag, and keeping |
| 135 | - | // is not recomposed. | 135 | + | // the Seq of the animating scene would leave outputs showing it, and |
| 136 | + | // waking core on every later flip. Any other keeps its output's Seq | |||
| 137 | + | // while it draws the same, so an idle output is not recomposed. | |||
| 136 | withCapture := scene.Capture != nil && (sc == capture.hiddenScr || sc == capture.windowScr) | 138 | withCapture := scene.Capture != nil && (sc == capture.hiddenScr || sc == capture.windowScr) | ||
| 137 | - | animating := (sc.springing() || c.pulsing(sc)) && (only == nil || only == sc) | 139 | + | animating := scene.Animating && (only == nil || only == sc) |
| 138 | - | if !withCapture && !animating && sc.last.Seq != 0 && scene.SameAs(sc.last) { | 140 | + | if !withCapture && !animating && sc.last.Animating == scene.Animating && sc.last.Seq != 0 && scene.SameAs(sc.last) { |
| 139 | scene.Seq = sc.last.Seq | 141 | scene.Seq = sc.last.Seq | ||
| 140 | } else { | 142 | } else { | ||
| 141 | c.seq++ | 143 | c.seq++ | ||
M internal/core/scene_animating_test.go +47 -0
| @@ -74,3 +74,50 @@ func TestSceneAnimatingIsPerOutput(t *testing.T) { | |||
| 74 | 74 | t.Fatalf("animating A=%v B=%v, want B only", got["A"].Animating, got["B"].Animating) | |
| 75 | 75 | } | |
| 76 | 76 | } | |
| 77 | + | ||
| 78 | + | // A spring that settles on the very scene its last running step drew (the | |
| 79 | + | // tail rounds to the final pixel) must still publish a scene with Animating | |
| 80 | + | // off, with a fresh Seq: SameAs ignores the flag, and the outputs would | |
| 81 | + | // otherwise keep the animating scene they show and wake core on every later | |
| 82 | + | // flip. | |
| 83 | + | func TestSettledSceneSameAsPreviousClearsAnimating(t *testing.T) { | |
| 84 | + | // Commands (rehit) are not read: room for every step. | |
| 85 | + | c, ic := indicatorCoreWith(t, make(chan ports.ClientCommand, 4096)) | |
| 86 | + | c.cfg.Animations.On = true | |
| 87 | + | a := c.cur() | |
| 88 | + | a.mon.AddWindow(1) | |
| 89 | + | a.mon.AddWindow(2) | |
| 90 | + | indicatorScene(t, c) | |
| 91 | + | ||
| 92 | + | w := a.mon.Current() | |
| 93 | + | w.view.off = 100 | |
| 94 | + | w.view.motion = newMotion(viewSpring(w.view.off, 0), ic.now, 1) | |
| 95 | + | // Step in 1 ms until the spring settles: the snap settles it once it is | |
| 96 | + | // within half a pixel of its target, where the last running scene can | |
| 97 | + | // draw the same pixels as the settled one. | |
| 98 | + | start := ic.now | |
| 99 | + | var running, settled ports.Scene | |
| 100 | + | for ms := 1; ; ms++ { | |
| 101 | + | if ms > 2000 { | |
| 102 | + | t.Fatal("the spring never settles") | |
| 103 | + | } | |
| 104 | + | ic.now = start.Add(time.Duration(ms) * time.Millisecond) | |
| 105 | + | settled = stepScenes(t, c, a)["A"] | |
| 106 | + | if !a.springing() { | |
| 107 | + | break | |
| 108 | + | } | |
| 109 | + | running = settled | |
| 110 | + | } | |
| 111 | + | if !running.Animating { | |
| 112 | + | t.Fatalf("bad setup: last running scene animating=%v", running.Animating) | |
| 113 | + | } | |
| 114 | + | if !settled.SameAs(running) { | |
| 115 | + | t.Fatal("bad setup: the settled scene differs from the last running one") | |
| 116 | + | } | |
| 117 | + | if settled.Animating { | |
| 118 | + | t.Fatal("the settled scene is animating") | |
| 119 | + | } | |
| 120 | + | if settled.Seq == running.Seq { | |
| 121 | + | t.Fatalf("settled scene kept the animating scene's Seq %d", running.Seq) | |
| 122 | + | } | |
| 123 | + | } | |
| @@ -74,3 +74,50 @@ func TestSceneAnimatingIsPerOutput(t *testing.T) { | |||||
| 74 | t.Fatalf("animating A=%v B=%v, want B only", got["A"].Animating, got["B"].Animating) | 74 | t.Fatalf("animating A=%v B=%v, want B only", got["A"].Animating, got["B"].Animating) | ||
| 75 | } | 75 | } | ||
| 76 | } | 76 | } | ||
| 77 | + | ||||
| 78 | + | // A spring that settles on the very scene its last running step drew (the | |||
| 79 | + | // tail rounds to the final pixel) must still publish a scene with Animating | |||
| 80 | + | // off, with a fresh Seq: SameAs ignores the flag, and the outputs would | |||
| 81 | + | // otherwise keep the animating scene they show and wake core on every later | |||
| 82 | + | // flip. | |||
| 83 | + | func TestSettledSceneSameAsPreviousClearsAnimating(t *testing.T) { | |||
| 84 | + | // Commands (rehit) are not read: room for every step. | |||
| 85 | + | c, ic := indicatorCoreWith(t, make(chan ports.ClientCommand, 4096)) | |||
| 86 | + | c.cfg.Animations.On = true | |||
| 87 | + | a := c.cur() | |||
| 88 | + | a.mon.AddWindow(1) | |||
| 89 | + | a.mon.AddWindow(2) | |||
| 90 | + | indicatorScene(t, c) | |||
| 91 | + | ||||
| 92 | + | w := a.mon.Current() | |||
| 93 | + | w.view.off = 100 | |||
| 94 | + | w.view.motion = newMotion(viewSpring(w.view.off, 0), ic.now, 1) | |||
| 95 | + | // Step in 1 ms until the spring settles: the snap settles it once it is | |||
| 96 | + | // within half a pixel of its target, where the last running scene can | |||
| 97 | + | // draw the same pixels as the settled one. | |||
| 98 | + | start := ic.now | |||
| 99 | + | var running, settled ports.Scene | |||
| 100 | + | for ms := 1; ; ms++ { | |||
| 101 | + | if ms > 2000 { | |||
| 102 | + | t.Fatal("the spring never settles") | |||
| 103 | + | } | |||
| 104 | + | ic.now = start.Add(time.Duration(ms) * time.Millisecond) | |||
| 105 | + | settled = stepScenes(t, c, a)["A"] | |||
| 106 | + | if !a.springing() { | |||
| 107 | + | break | |||
| 108 | + | } | |||
| 109 | + | running = settled | |||
| 110 | + | } | |||
| 111 | + | if !running.Animating { | |||
| 112 | + | t.Fatalf("bad setup: last running scene animating=%v", running.Animating) | |||
| 113 | + | } | |||
| 114 | + | if !settled.SameAs(running) { | |||
| 115 | + | t.Fatal("bad setup: the settled scene differs from the last running one") | |||
| 116 | + | } | |||
| 117 | + | if settled.Animating { | |||
| 118 | + | t.Fatal("the settled scene is animating") | |||
| 119 | + | } | |||
| 120 | + | if settled.Seq == running.Seq { | |||
| 121 | + | t.Fatalf("settled scene kept the animating scene's Seq %d", running.Seq) | |||
| 122 | + | } | |||
| 123 | + | } | |||