refactor(core): read the clock once per action, test untouched motions of unmoved windows
- commit
- 5f7a9a59cd569fb6d0f17fc961cffca48287966e
- author
- brice · · 2026-10-04 21:04 UTC
- committer
- brice · · 2026-10-04 21:04 UTC
- parent
- e92a9c4
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +52 -4
M internal/core/input.go +6 -4
| @@ -125,10 +125,11 @@ func (c *Core) handleInput(ctx context.Context, ev ports.InputEvent) error { | |||
| 125 | 125 | // A click focuses the window and its output. | |
| 126 | 126 | s, w := c.screenOf(id) | |
| 127 | 127 | if v.Pressed && s != nil && w == s.mon.Current() && (c.keyboard.sent != id || s != c.cur()) { | |
| 128 | - | before := c.snapshot(c.now()) | |
| 128 | + | now := c.now() | |
| 129 | + | before := c.snapshot(now) | |
| 129 | 130 | w.Click(id) | |
| 130 | 131 | c.focusScreen = c.screenIndex(s.name()) | |
| 131 | - | c.transition(before, c.now()) | |
| 132 | + | c.transition(before, now) | |
| 132 | 133 | if err := c.publish(ctx); err != nil { | |
| 133 | 134 | return err | |
| 134 | 135 | } | |
| @@ -262,7 +263,8 @@ func (c *Core) handleInput(ctx context.Context, ev ports.InputEvent) error { | |||
| 262 | 263 | } | |
| 263 | 264 | before := c.cur().mon.Current() | |
| 264 | 265 | swiped := c.swipedWorkspace() | |
| 265 | - | shots := c.snapshot(c.now()) | |
| 266 | + | now := c.now() | |
| 267 | + | shots := c.snapshot(now) | |
| 266 | 268 | c.keyboard.takeBack() // a bind acts on the windows | |
| 267 | 269 | effect := c.applyAction(action) | |
| 268 | 270 | if effect.Quit { | |
| @@ -283,7 +285,7 @@ func (c *Core) handleInput(ctx context.Context, ev ports.InputEvent) error { | |||
| 283 | 285 | if c.swipedWorkspace() != swiped { | |
| 284 | 286 | c.dropSwipe() | |
| 285 | 287 | } | |
| 286 | - | c.transition(shots, c.now()) | |
| 288 | + | c.transition(shots, now) | |
| 287 | 289 | if err := c.workspaceVisible(ctx, c.cur().mon.Current() != before); err != nil { | |
| 288 | 290 | return err | |
| 289 | 291 | } | |
| @@ -125,10 +125,11 @@ func (c *Core) handleInput(ctx context.Context, ev ports.InputEvent) error { | |||||
| 125 | // A click focuses the window and its output. | 125 | // A click focuses the window and its output. | ||
| 126 | s, w := c.screenOf(id) | 126 | s, w := c.screenOf(id) | ||
| 127 | if v.Pressed && s != nil && w == s.mon.Current() && (c.keyboard.sent != id || s != c.cur()) { | 127 | if v.Pressed && s != nil && w == s.mon.Current() && (c.keyboard.sent != id || s != c.cur()) { | ||
| 128 | - | before := c.snapshot(c.now()) | 128 | + | now := c.now() |
| 129 | + | before := c.snapshot(now) | |||
| 129 | w.Click(id) | 130 | w.Click(id) | ||
| 130 | c.focusScreen = c.screenIndex(s.name()) | 131 | c.focusScreen = c.screenIndex(s.name()) | ||
| 131 | - | c.transition(before, c.now()) | 132 | + | c.transition(before, now) |
| 132 | if err := c.publish(ctx); err != nil { | 133 | if err := c.publish(ctx); err != nil { | ||
| 133 | return err | 134 | return err | ||
| 134 | } | 135 | } | ||
| @@ -262,7 +263,8 @@ func (c *Core) handleInput(ctx context.Context, ev ports.InputEvent) error { | |||||
| 262 | } | 263 | } | ||
| 263 | before := c.cur().mon.Current() | 264 | before := c.cur().mon.Current() | ||
| 264 | swiped := c.swipedWorkspace() | 265 | swiped := c.swipedWorkspace() | ||
| 265 | - | shots := c.snapshot(c.now()) | 266 | + | now := c.now() |
| 267 | + | shots := c.snapshot(now) | |||
| 266 | c.keyboard.takeBack() // a bind acts on the windows | 268 | c.keyboard.takeBack() // a bind acts on the windows | ||
| 267 | effect := c.applyAction(action) | 269 | effect := c.applyAction(action) | ||
| 268 | if effect.Quit { | 270 | if effect.Quit { | ||
| @@ -283,7 +285,7 @@ func (c *Core) handleInput(ctx context.Context, ev ports.InputEvent) error { | |||||
| 283 | if c.swipedWorkspace() != swiped { | 285 | if c.swipedWorkspace() != swiped { | ||
| 284 | c.dropSwipe() | 286 | c.dropSwipe() | ||
| 285 | } | 287 | } | ||
| 286 | - | c.transition(shots, c.now()) | 288 | + | c.transition(shots, now) |
| 287 | if err := c.workspaceVisible(ctx, c.cur().mon.Current() != before); err != nil { | 289 | if err := c.workspaceVisible(ctx, c.cur().mon.Current() != before); err != nil { | ||
| 288 | return err | 290 | return err | ||
| 289 | } | 291 | } | ||
M internal/core/transition_chain_internal_test.go +46 -0
| @@ -391,3 +391,49 @@ func TestRetargetKeepsSubPixelOffsets(t *testing.T) { | |||
| 391 | 391 | t.Fatalf("dx = %v equals the rounded rect's: the fraction was lost", got.dx) | |
| 392 | 392 | } | |
| 393 | 393 | } | |
| 394 | + | ||
| 395 | + | // An action that moves other windows leaves a running rect motion alone: it | |
| 396 | + | // is not restarted from the rounded rect nor resampled (same start, speed | |
| 397 | + | // and offsets), while the windows the action moved animate. | |
| 398 | + | func TestRectMotionOfUnmovedWindowIsUntouched(t *testing.T) { | |
| 399 | + | c, ic := indicatorCore(t) | |
| 400 | + | c.cfg.Animations.On = true | |
| 401 | + | m := c.cur().mon | |
| 402 | + | for id := WindowID(1); id <= 3; id++ { | |
| 403 | + | m.AddWindow(id) | |
| 404 | + | } | |
| 405 | + | // 2 and 3 share a column; 1 is a column of its own. | |
| 406 | + | c.applyAction(ActionConsumeOrExpelLeft) | |
| 407 | + | settleShown(t, c) | |
| 408 | + | sc := c.cur() | |
| 409 | + | ||
| 410 | + | // A runs a rect motion, one frame in. | |
| 411 | + | sp := viewSpring(60.5, 0) | |
| 412 | + | sc.rects = map[WindowID]rectMotion{1: {x: c.spring(sp, ic.now), dx: 60.5}} | |
| 413 | + | sc.rectsWS = m.Current() | |
| 414 | + | ic.now = ic.now.Add(30 * time.Millisecond) | |
| 415 | + | c.animate(ic.now, nil) | |
| 416 | + | settleShown(t, c) | |
| 417 | + | a := sc.rects[1] | |
| 418 | + | if !a.x.on || a.dx == 0 || a.dx == 60.5 || a.x.velocity() == 0 { | |
| 419 | + | t.Fatalf("setup: A's motion %+v is not mid-flight", a) | |
| 420 | + | } | |
| 421 | + | ||
| 422 | + | // B: swapping the stacked 2 and 3 moves those two only. | |
| 423 | + | ic.now = ic.now.Add(time.Millisecond) | |
| 424 | + | before := c.snapshot(ic.now) | |
| 425 | + | c.applyAction(ActionMoveWindowUp) | |
| 426 | + | c.transition(before, ic.now) | |
| 427 | + | if got, ok := sc.rects[1]; !ok || got != a { | |
| 428 | + | t.Fatalf("A's motion changed: %+v, was %+v", got, a) | |
| 429 | + | } | |
| 430 | + | for _, id := range []WindowID{2, 3} { | |
| 431 | + | b, ok := sc.rects[id] | |
| 432 | + | if !ok || !b.y.on || b.dy == 0 { | |
| 433 | + | t.Fatalf("window %d does not animate: %+v", id, b) | |
| 434 | + | } | |
| 435 | + | if b.x.on { | |
| 436 | + | t.Fatalf("window %d moved sideways: %+v", id, b) | |
| 437 | + | } | |
| 438 | + | } | |
| 439 | + | } | |
| @@ -391,3 +391,49 @@ func TestRetargetKeepsSubPixelOffsets(t *testing.T) { | |||||
| 391 | t.Fatalf("dx = %v equals the rounded rect's: the fraction was lost", got.dx) | 391 | t.Fatalf("dx = %v equals the rounded rect's: the fraction was lost", got.dx) | ||
| 392 | } | 392 | } | ||
| 393 | } | 393 | } | ||
| 394 | + | ||||
| 395 | + | // An action that moves other windows leaves a running rect motion alone: it | |||
| 396 | + | // is not restarted from the rounded rect nor resampled (same start, speed | |||
| 397 | + | // and offsets), while the windows the action moved animate. | |||
| 398 | + | func TestRectMotionOfUnmovedWindowIsUntouched(t *testing.T) { | |||
| 399 | + | c, ic := indicatorCore(t) | |||
| 400 | + | c.cfg.Animations.On = true | |||
| 401 | + | m := c.cur().mon | |||
| 402 | + | for id := WindowID(1); id <= 3; id++ { | |||
| 403 | + | m.AddWindow(id) | |||
| 404 | + | } | |||
| 405 | + | // 2 and 3 share a column; 1 is a column of its own. | |||
| 406 | + | c.applyAction(ActionConsumeOrExpelLeft) | |||
| 407 | + | settleShown(t, c) | |||
| 408 | + | sc := c.cur() | |||
| 409 | + | ||||
| 410 | + | // A runs a rect motion, one frame in. | |||
| 411 | + | sp := viewSpring(60.5, 0) | |||
| 412 | + | sc.rects = map[WindowID]rectMotion{1: {x: c.spring(sp, ic.now), dx: 60.5}} | |||
| 413 | + | sc.rectsWS = m.Current() | |||
| 414 | + | ic.now = ic.now.Add(30 * time.Millisecond) | |||
| 415 | + | c.animate(ic.now, nil) | |||
| 416 | + | settleShown(t, c) | |||
| 417 | + | a := sc.rects[1] | |||
| 418 | + | if !a.x.on || a.dx == 0 || a.dx == 60.5 || a.x.velocity() == 0 { | |||
| 419 | + | t.Fatalf("setup: A's motion %+v is not mid-flight", a) | |||
| 420 | + | } | |||
| 421 | + | ||||
| 422 | + | // B: swapping the stacked 2 and 3 moves those two only. | |||
| 423 | + | ic.now = ic.now.Add(time.Millisecond) | |||
| 424 | + | before := c.snapshot(ic.now) | |||
| 425 | + | c.applyAction(ActionMoveWindowUp) | |||
| 426 | + | c.transition(before, ic.now) | |||
| 427 | + | if got, ok := sc.rects[1]; !ok || got != a { | |||
| 428 | + | t.Fatalf("A's motion changed: %+v, was %+v", got, a) | |||
| 429 | + | } | |||
| 430 | + | for _, id := range []WindowID{2, 3} { | |||
| 431 | + | b, ok := sc.rects[id] | |||
| 432 | + | if !ok || !b.y.on || b.dy == 0 { | |||
| 433 | + | t.Fatalf("window %d does not animate: %+v", id, b) | |||
| 434 | + | } | |||
| 435 | + | if b.x.on { | |||
| 436 | + | t.Fatalf("window %d moved sideways: %+v", id, b) | |||
| 437 | + | } | |||
| 438 | + | } | |||
| 439 | + | } | |||