detached 512d7fc

perf(core): drop the veil at once when a float closes or hides

The veil under a leaving float faded with it, which changed Scene.Dim on
every frame of the fade and forced a full redraw of the output for about
300 ms, where the float's own fade redraws only its rect. floatDim now
ignores leaving floats: the veil goes when the float starts to leave.
Deliberate spec change (C0 pinned the fading veil): the veil vanishing at
once is acceptable visually and restores partial redraws during float
close and stash hide. publish keeps a leaving window's configure only
when its workspace still holds it (a hidden stash window), as the comment
always said; a closed one stays forgotten.
commit
512d7fcf140f045de6467300cbef4a99887a9df4
author
brice · · 2026-10-05 02:45 UTC
parent
724a3a0
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +31 -32
M internal/core/core.go +14 -15
@@ -424,24 +424,20 @@ func onScreen(p Placement, o Rect) bool {
424424
425425 // floatDim is the veil opacity of a layout: dim only when a float is
426426 // drawn above the tiles, never for a demoted covering float alone nor for
427-// an overview preview.
428-// A leaving float (fading out after it closed or hid) keeps the veil,
429-// scaled by what is left of it, so the veil fades with the last float.
427+// an overview preview. A leaving float (fading out after it closed or hid)
428+// drops the veil at once: a veil that fades changes Scene.Dim every frame,
429+// which redraws the whole output for the length of the fade, where the
430+// float's own fade redraws only its rect.
430431 func floatDim(layout []Placement, o Rect, dim float64) float64 {
431- k := 0.0
432432 for _, p := range layout {
433- if p.Floating && !p.Below && p.Preview == 0 && (onScreen(p, o) || p.Leaving && p.Rect.Overlaps(o)) {
433+ if p.Floating && !p.Below && p.Preview == 0 && onScreen(p, o) {
434434 if p.Fullscreen {
435435 return 0
436436 }
437- if p.Leaving {
438- k = max(k, 1-p.Fade)
439- } else {
440- k = 1
441- }
437+ return dim
442438 }
443439 }
444- return dim * k
440+ return 0
445441 }
446442
447443 // visible reports whether the window, layer surface or popup is on screen
@@ -636,10 +632,13 @@ func (c *Core) publishFrame(ctx context.Context, only *screen) error {
636632 // hidden to everything else, its configures included.
637633 sw.Hidden = false
638634 scene.Windows = append(scene.Windows, sw)
639- // A window the layout still holds keeps its last configure
640- // (prune would forget it, and its next one would start from
641- // nothing).
642- c.configures.keep(p.ID)
635+ // A window the workspace still holds (a hidden stash
636+ // window) keeps its last configure: prune would forget it,
637+ // and its next one would start from nothing. A closed one
638+ // was forgotten at its unmap and stays so.
639+ if w, _ := sc.mon.find(p.ID); w != nil {
640+ c.configures.keep(p.ID)
641+ }
643642 continue
644643 }
645644 if focused && p.ID == c.pulse.target && !alone && !p.Fullscreen && !p.Hidden && p.Preview == 0 && !sc.mon.ov.open {
@@ -424,24 +424,20 @@ func onScreen(p Placement, o Rect) bool {
424 424
425 // floatDim is the veil opacity of a layout: dim only when a float is425 // floatDim is the veil opacity of a layout: dim only when a float is
426 // drawn above the tiles, never for a demoted covering float alone nor for426 // drawn above the tiles, never for a demoted covering float alone nor for
427-// an overview preview.427+// an overview preview. A leaving float (fading out after it closed or hid)
428-// A leaving float (fading out after it closed or hid) keeps the veil,428+// drops the veil at once: a veil that fades changes Scene.Dim every frame,
429-// scaled by what is left of it, so the veil fades with the last float.429+// which redraws the whole output for the length of the fade, where the
430+// float's own fade redraws only its rect.
430 func floatDim(layout []Placement, o Rect, dim float64) float64 {431 func floatDim(layout []Placement, o Rect, dim float64) float64 {
431- k := 0.0
432 for _, p := range layout {432 for _, p := range layout {
433- if p.Floating && !p.Below && p.Preview == 0 && (onScreen(p, o) || p.Leaving && p.Rect.Overlaps(o)) {433+ if p.Floating && !p.Below && p.Preview == 0 && onScreen(p, o) {
434 if p.Fullscreen {434 if p.Fullscreen {
435 return 0435 return 0
436 }436 }
437- if p.Leaving {437+ return dim
438- k = max(k, 1-p.Fade)
439- } else {
440- k = 1
441- }
442 }438 }
443 }439 }
444- return dim * k440+ return 0
445 }441 }
446 442
447 // visible reports whether the window, layer surface or popup is on screen443 // visible reports whether the window, layer surface or popup is on screen
@@ -636,10 +632,13 @@ func (c *Core) publishFrame(ctx context.Context, only *screen) error {
636 // hidden to everything else, its configures included.632 // hidden to everything else, its configures included.
637 sw.Hidden = false633 sw.Hidden = false
638 scene.Windows = append(scene.Windows, sw)634 scene.Windows = append(scene.Windows, sw)
639- // A window the layout still holds keeps its last configure635+ // A window the workspace still holds (a hidden stash
640- // (prune would forget it, and its next one would start from636+ // window) keeps its last configure: prune would forget it,
641- // nothing).637+ // and its next one would start from nothing. A closed one
642- c.configures.keep(p.ID)638+ // was forgotten at its unmap and stays so.
639+ if w, _ := sc.mon.find(p.ID); w != nil {
640+ c.configures.keep(p.ID)
641+ }
643 continue642 continue
644 }643 }
645 if focused && p.ID == c.pulse.target && !alone && !p.Fullscreen && !p.Hidden && p.Preview == 0 && !sc.mon.ov.open {644 if focused && p.ID == c.pulse.target && !alone && !p.Fullscreen && !p.Hidden && p.Preview == 0 && !sc.mon.ov.open {
M internal/core/transition_fade_internal_test.go +17 -17
@@ -41,7 +41,7 @@ func TestAppearDerivesFadeAndZoom(t *testing.T) {
4141 drainConfigures(cmds)
4242 settled := sc.settledLayout[slices.IndexFunc(sc.settledLayout, func(p Placement) bool { return p.ID == 2 })]
4343
44- c.appear(sc, 2, ic.now)
44+ c.appearAt(sc, settledOf(t, sc, 2), ic.now)
4545 s := indicatorScene(t, c)
4646 w := sceneWindow(t, s, 2)
4747 if w.Fade != 1 || w.Hidden {
@@ -98,7 +98,7 @@ func TestAppearOffIsInstant(t *testing.T) {
9898 sc := c.cur()
9999 sc.mon.AddWindow(1)
100100 settleShown(t, c)
101- c.appear(sc, 1, ic.now)
101+ c.appearAt(sc, settledOf(t, sc, 1), ic.now)
102102 if len(sc.rects) != 0 || c.animating() {
103103 t.Fatal("appear started a motion with animations off")
104104 }
@@ -122,7 +122,7 @@ func TestLeavingEntryLifecycle(t *testing.T) {
122122
123123 // Close window 2: the layout re-flows; its last placement leaves.
124124 sc.mon.RemoveWindow(2)
125- c.leave(sc, left, ic.now)
125+ c.leaveFrom(sc, left, nil, ic.now)
126126 s := indicatorScene(t, c)
127127 if len(sc.settledLayout) != len(sc.shown) {
128128 t.Fatalf("settled %d and shown %d entries", len(sc.settledLayout), len(sc.shown))
@@ -188,7 +188,7 @@ func TestLeavingEntryDroppedWhenShownAgain(t *testing.T) {
188188 settleShown(t, c)
189189 left := sc.settledLayout[1]
190190 sc.mon.RemoveWindow(2)
191- c.leave(sc, left, ic.now)
191+ c.leaveFrom(sc, left, nil, ic.now)
192192 if s := indicatorScene(t, c); len(s.Windows) != 2 || !sc.shown[1].Leaving {
193193 t.Fatalf("no leaving entry: %+v", s.Windows)
194194 }
@@ -219,7 +219,7 @@ func TestLeavingEntryDroppedByStopRects(t *testing.T) {
219219 settleShown(t, c)
220220 left := sc.settledLayout[1]
221221 sc.mon.RemoveWindow(2)
222- c.leave(sc, left, ic.now)
222+ c.leaveFrom(sc, left, nil, ic.now)
223223 if s := indicatorScene(t, c); len(s.Windows) != 2 {
224224 t.Fatalf("%d windows, want the tile and the leaving entry", len(s.Windows))
225225 }
@@ -238,7 +238,7 @@ func TestLeaveOffIsInstant(t *testing.T) {
238238 settleShown(t, c)
239239 left := sc.settledLayout[0]
240240 sc.mon.RemoveWindow(1)
241- c.leave(sc, left, ic.now)
241+ c.leaveFrom(sc, left, nil, ic.now)
242242 if s := indicatorScene(t, c); len(s.Windows) != 0 || len(sc.rects) != 0 {
243243 t.Fatalf("leaving entry with animations off: %+v", s.Windows)
244244 }
@@ -302,7 +302,7 @@ func TestSnapshotSkipsLeavingEntries(t *testing.T) {
302302 settleShown(t, c)
303303 left := sc.settledLayout[1]
304304 sc.mon.RemoveWindow(2)
305- c.leave(sc, left, ic.now)
305+ c.leaveFrom(sc, left, nil, ic.now)
306306 settleShown(t, c)
307307 shots := c.snapshot(ic.now)
308308 if len(shots) != 1 || len(shots[0].rects) != 1 || shots[0].rects[0].id != 1 {
@@ -331,7 +331,7 @@ func TestLeavingPaintOrder(t *testing.T) {
331331
332332 // Close the tile: its entry sits before the float.
333333 sc.mon.RemoveWindow(2)
334- c.leave(sc, tile, ic.now)
334+ c.leaveFrom(sc, tile, nil, ic.now)
335335 s := indicatorScene(t, c)
336336 order := func(s ports.Scene) []WindowID {
337337 ids := make([]WindowID, 0, len(s.Windows))
@@ -352,23 +352,23 @@ func TestLeavingPaintOrder(t *testing.T) {
352352 t.Fatalf("veil %v with the float still shown, want 0.4", s.Dim)
353353 }
354354
355- // Close the float too: its entry goes last and keeps the veil, which
356- // fades with it.
355+ // Close the float too: its entry goes last, and the veil goes at
356+ // once (a fading veil would redraw the whole output every frame of
357+ // the fade; the float's own fade redraws only its rect).
357358 sc.mon.RemoveWindow(3)
358- c.leave(sc, float, ic.now)
359+ c.leaveFrom(sc, float, nil, ic.now)
359360 s = indicatorScene(t, c)
360361 if got := order(s); !slices.Equal(got, []WindowID{1, 2, 3}) {
361362 t.Fatalf("order with a leaving float %v, want [1 2 3] (float last)", got)
362363 }
363- if s.Dim != 0.4 {
364- t.Fatalf("veil %v right after the float left, want 0.4", s.Dim)
364+ if s.Dim != 0 {
365+ t.Fatalf("veil %v right after the float left, want none", s.Dim)
365366 }
366367 ic.now = ic.now.Add(60 * time.Millisecond)
367368 c.animate(ic.now, nil)
368369 s = indicatorScene(t, c)
369- f := sceneWindow(t, s, 3)
370- if !(s.Dim > 0 && s.Dim < 0.4) || math.Abs(s.Dim-0.4*(1-f.Fade)) > 1e-9 {
371- t.Fatalf("veil %v mid-fade (float fade %v), want 0.4*(1-fade)", s.Dim, f.Fade)
370+ if f := sceneWindow(t, s, 3); s.Dim != 0 || !(f.Fade > 0 && f.Fade < 1) {
371+ t.Fatalf("veil %v mid-fade (float fade %v), want none while the float fades", s.Dim, f.Fade)
372372 }
373373 ic.now = ic.now.Add(5 * time.Second)
374374 c.animate(ic.now, nil)
@@ -386,7 +386,7 @@ func TestRetargetClearsScaleKeepsFade(t *testing.T) {
386386 sc := c.cur()
387387 sc.mon.AddWindow(1)
388388 settleShown(t, c)
389- c.appear(sc, 1, ic.now)
389+ c.appearAt(sc, settledOf(t, sc, 1), ic.now)
390390 settleShown(t, c)
391391 if rm := sc.rects[1]; !rm.scale || !rm.fade.on {
392392 t.Fatalf("setup: %+v", rm)
@@ -41,7 +41,7 @@ func TestAppearDerivesFadeAndZoom(t *testing.T) {
41 drainConfigures(cmds)41 drainConfigures(cmds)
42 settled := sc.settledLayout[slices.IndexFunc(sc.settledLayout, func(p Placement) bool { return p.ID == 2 })]42 settled := sc.settledLayout[slices.IndexFunc(sc.settledLayout, func(p Placement) bool { return p.ID == 2 })]
43 43
44- c.appear(sc, 2, ic.now)44+ c.appearAt(sc, settledOf(t, sc, 2), ic.now)
45 s := indicatorScene(t, c)45 s := indicatorScene(t, c)
46 w := sceneWindow(t, s, 2)46 w := sceneWindow(t, s, 2)
47 if w.Fade != 1 || w.Hidden {47 if w.Fade != 1 || w.Hidden {
@@ -98,7 +98,7 @@ func TestAppearOffIsInstant(t *testing.T) {
98 sc := c.cur()98 sc := c.cur()
99 sc.mon.AddWindow(1)99 sc.mon.AddWindow(1)
100 settleShown(t, c)100 settleShown(t, c)
101- c.appear(sc, 1, ic.now)101+ c.appearAt(sc, settledOf(t, sc, 1), ic.now)
102 if len(sc.rects) != 0 || c.animating() {102 if len(sc.rects) != 0 || c.animating() {
103 t.Fatal("appear started a motion with animations off")103 t.Fatal("appear started a motion with animations off")
104 }104 }
@@ -122,7 +122,7 @@ func TestLeavingEntryLifecycle(t *testing.T) {
122 122
123 // Close window 2: the layout re-flows; its last placement leaves.123 // Close window 2: the layout re-flows; its last placement leaves.
124 sc.mon.RemoveWindow(2)124 sc.mon.RemoveWindow(2)
125- c.leave(sc, left, ic.now)125+ c.leaveFrom(sc, left, nil, ic.now)
126 s := indicatorScene(t, c)126 s := indicatorScene(t, c)
127 if len(sc.settledLayout) != len(sc.shown) {127 if len(sc.settledLayout) != len(sc.shown) {
128 t.Fatalf("settled %d and shown %d entries", len(sc.settledLayout), len(sc.shown))128 t.Fatalf("settled %d and shown %d entries", len(sc.settledLayout), len(sc.shown))
@@ -188,7 +188,7 @@ func TestLeavingEntryDroppedWhenShownAgain(t *testing.T) {
188 settleShown(t, c)188 settleShown(t, c)
189 left := sc.settledLayout[1]189 left := sc.settledLayout[1]
190 sc.mon.RemoveWindow(2)190 sc.mon.RemoveWindow(2)
191- c.leave(sc, left, ic.now)191+ c.leaveFrom(sc, left, nil, ic.now)
192 if s := indicatorScene(t, c); len(s.Windows) != 2 || !sc.shown[1].Leaving {192 if s := indicatorScene(t, c); len(s.Windows) != 2 || !sc.shown[1].Leaving {
193 t.Fatalf("no leaving entry: %+v", s.Windows)193 t.Fatalf("no leaving entry: %+v", s.Windows)
194 }194 }
@@ -219,7 +219,7 @@ func TestLeavingEntryDroppedByStopRects(t *testing.T) {
219 settleShown(t, c)219 settleShown(t, c)
220 left := sc.settledLayout[1]220 left := sc.settledLayout[1]
221 sc.mon.RemoveWindow(2)221 sc.mon.RemoveWindow(2)
222- c.leave(sc, left, ic.now)222+ c.leaveFrom(sc, left, nil, ic.now)
223 if s := indicatorScene(t, c); len(s.Windows) != 2 {223 if s := indicatorScene(t, c); len(s.Windows) != 2 {
224 t.Fatalf("%d windows, want the tile and the leaving entry", len(s.Windows))224 t.Fatalf("%d windows, want the tile and the leaving entry", len(s.Windows))
225 }225 }
@@ -238,7 +238,7 @@ func TestLeaveOffIsInstant(t *testing.T) {
238 settleShown(t, c)238 settleShown(t, c)
239 left := sc.settledLayout[0]239 left := sc.settledLayout[0]
240 sc.mon.RemoveWindow(1)240 sc.mon.RemoveWindow(1)
241- c.leave(sc, left, ic.now)241+ c.leaveFrom(sc, left, nil, ic.now)
242 if s := indicatorScene(t, c); len(s.Windows) != 0 || len(sc.rects) != 0 {242 if s := indicatorScene(t, c); len(s.Windows) != 0 || len(sc.rects) != 0 {
243 t.Fatalf("leaving entry with animations off: %+v", s.Windows)243 t.Fatalf("leaving entry with animations off: %+v", s.Windows)
244 }244 }
@@ -302,7 +302,7 @@ func TestSnapshotSkipsLeavingEntries(t *testing.T) {
302 settleShown(t, c)302 settleShown(t, c)
303 left := sc.settledLayout[1]303 left := sc.settledLayout[1]
304 sc.mon.RemoveWindow(2)304 sc.mon.RemoveWindow(2)
305- c.leave(sc, left, ic.now)305+ c.leaveFrom(sc, left, nil, ic.now)
306 settleShown(t, c)306 settleShown(t, c)
307 shots := c.snapshot(ic.now)307 shots := c.snapshot(ic.now)
308 if len(shots) != 1 || len(shots[0].rects) != 1 || shots[0].rects[0].id != 1 {308 if len(shots) != 1 || len(shots[0].rects) != 1 || shots[0].rects[0].id != 1 {
@@ -331,7 +331,7 @@ func TestLeavingPaintOrder(t *testing.T) {
331 331
332 // Close the tile: its entry sits before the float.332 // Close the tile: its entry sits before the float.
333 sc.mon.RemoveWindow(2)333 sc.mon.RemoveWindow(2)
334- c.leave(sc, tile, ic.now)334+ c.leaveFrom(sc, tile, nil, ic.now)
335 s := indicatorScene(t, c)335 s := indicatorScene(t, c)
336 order := func(s ports.Scene) []WindowID {336 order := func(s ports.Scene) []WindowID {
337 ids := make([]WindowID, 0, len(s.Windows))337 ids := make([]WindowID, 0, len(s.Windows))
@@ -352,23 +352,23 @@ func TestLeavingPaintOrder(t *testing.T) {
352 t.Fatalf("veil %v with the float still shown, want 0.4", s.Dim)352 t.Fatalf("veil %v with the float still shown, want 0.4", s.Dim)
353 }353 }
354 354
355- // Close the float too: its entry goes last and keeps the veil, which355+ // Close the float too: its entry goes last, and the veil goes at
356- // fades with it.356+ // once (a fading veil would redraw the whole output every frame of
357+ // the fade; the float's own fade redraws only its rect).
357 sc.mon.RemoveWindow(3)358 sc.mon.RemoveWindow(3)
358- c.leave(sc, float, ic.now)359+ c.leaveFrom(sc, float, nil, ic.now)
359 s = indicatorScene(t, c)360 s = indicatorScene(t, c)
360 if got := order(s); !slices.Equal(got, []WindowID{1, 2, 3}) {361 if got := order(s); !slices.Equal(got, []WindowID{1, 2, 3}) {
361 t.Fatalf("order with a leaving float %v, want [1 2 3] (float last)", got)362 t.Fatalf("order with a leaving float %v, want [1 2 3] (float last)", got)
362 }363 }
363- if s.Dim != 0.4 {364+ if s.Dim != 0 {
364- t.Fatalf("veil %v right after the float left, want 0.4", s.Dim)365+ t.Fatalf("veil %v right after the float left, want none", s.Dim)
365 }366 }
366 ic.now = ic.now.Add(60 * time.Millisecond)367 ic.now = ic.now.Add(60 * time.Millisecond)
367 c.animate(ic.now, nil)368 c.animate(ic.now, nil)
368 s = indicatorScene(t, c)369 s = indicatorScene(t, c)
369- f := sceneWindow(t, s, 3)370+ if f := sceneWindow(t, s, 3); s.Dim != 0 || !(f.Fade > 0 && f.Fade < 1) {
370- if !(s.Dim > 0 && s.Dim < 0.4) || math.Abs(s.Dim-0.4*(1-f.Fade)) > 1e-9 {371+ t.Fatalf("veil %v mid-fade (float fade %v), want none while the float fades", s.Dim, f.Fade)
371- t.Fatalf("veil %v mid-fade (float fade %v), want 0.4*(1-fade)", s.Dim, f.Fade)
372 }372 }
373 ic.now = ic.now.Add(5 * time.Second)373 ic.now = ic.now.Add(5 * time.Second)
374 c.animate(ic.now, nil)374 c.animate(ic.now, nil)
@@ -386,7 +386,7 @@ func TestRetargetClearsScaleKeepsFade(t *testing.T) {
386 sc := c.cur()386 sc := c.cur()
387 sc.mon.AddWindow(1)387 sc.mon.AddWindow(1)
388 settleShown(t, c)388 settleShown(t, c)
389- c.appear(sc, 1, ic.now)389+ c.appearAt(sc, settledOf(t, sc, 1), ic.now)
390 settleShown(t, c)390 settleShown(t, c)
391 if rm := sc.rects[1]; !rm.scale || !rm.fade.on {391 if rm := sc.rects[1]; !rm.scale || !rm.fade.on {
392 t.Fatalf("setup: %+v", rm)392 t.Fatalf("setup: %+v", rm)

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