detached 5f7a9a5

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 VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · 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 {
125125 // A click focuses the window and its output.
126126 s, w := c.screenOf(id)
127127 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)
129130 w.Click(id)
130131 c.focusScreen = c.screenIndex(s.name())
131- c.transition(before, c.now())
132+ c.transition(before, now)
132133 if err := c.publish(ctx); err != nil {
133134 return err
134135 }
@@ -262,7 +263,8 @@ func (c *Core) handleInput(ctx context.Context, ev ports.InputEvent) error {
262263 }
263264 before := c.cur().mon.Current()
264265 swiped := c.swipedWorkspace()
265- shots := c.snapshot(c.now())
266+ now := c.now()
267+ shots := c.snapshot(now)
266268 c.keyboard.takeBack() // a bind acts on the windows
267269 effect := c.applyAction(action)
268270 if effect.Quit {
@@ -283,7 +285,7 @@ func (c *Core) handleInput(ctx context.Context, ev ports.InputEvent) error {
283285 if c.swipedWorkspace() != swiped {
284286 c.dropSwipe()
285287 }
286- c.transition(shots, c.now())
288+ c.transition(shots, now)
287289 if err := c.workspaceVisible(ctx, c.cur().mon.Current() != before); err != nil {
288290 return err
289291 }
@@ -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 err134 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 windows268 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 err290 return err
289 }291 }
M internal/core/transition_chain_internal_test.go +46 -0
@@ -391,3 +391,49 @@ func TestRetargetKeepsSubPixelOffsets(t *testing.T) {
391391 t.Fatalf("dx = %v equals the rounded rect's: the fraction was lost", got.dx)
392392 }
393393 }
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+}

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