detached 0b0d373

#14 test: fix flaky syncobj and capture keyboard tests

state
merged · 37f59a2 · merge commit
branch
fix/flaky-capture-syncobj → main
opened
bnema ·
updated

What

Fixes two tests that failed intermittently on CI (make race).

  • TestSyncobjProtocolErrors/surface destroyed (run 132): the first set_acquire_point already raises no_surface and the server closes the connection; the second request of setPoints could then hit EPIPE and t.Fatal. The subtest now sends only the erroring request.
  • TestCaptureExclusionKeyboardClamp (run 127): the test drained r.commands after sending LayerChanged, so it could drop the FocusWindow{11} the core emits for that change and then time out. drainCommands now runs before the send; TestCaptureExclusionKeyboardNone uses the same helper.

Checks

  • The core race reproduced on the old code (-race -count=300 -cpu 1,4 under load, capture_test.go:381: timed out); fixed, -count=500 -cpu 1,2,4 passes. The wayland EPIPE did not reproduce locally; the fix follows the CI trace.
  • make check, make race, make mocks-check, staticcheck ./... pass.
CommitSubjectAuthorDateSignatureStatus
0b0d373test(core): drain capture commands before the layer changebriceGPG Verified
0e8d58dtest(wayland): send a single request to trigger the no_surface errorbriceGPG Verified

Checks 1

8 files · +34 -142
1/1
M docs/config.md +1 -1
@@ -54,7 +54,7 @@ See [examples/config](../examples/config) for every key with its default.
5454 | `animations.speed` | `normal` | `slow`, `normal`, `fast`, or a factor from `0.1` to `10` |
5555 | `focus.follow-move` | `off` | Follow a column moved to another workspace |
5656 | `focus.position-hint` | `on` | Show workspace/band dots near the output bottom for 600ms after a position change, then fade for 150ms (no fade with animations off); hidden in overview, fullscreen and session lock; never captured |
57-| `focus.flash` | `on` | Briefly draw an inner border, inside the window border, on the window, column or output an action changed |
57+| `focus.flash` | `on` | Briefly draw an inner border on the window, column or output an action changed |
5858 | `focus.flash-width` | `2` | Flash border width in logical pixels, `1` to `8` |
5959 | `focus.flash-opacity` | `0.6` | Starting opacity of the flash, `0.1` to `1` |
6060 | `focus.flash-duration` | `200ms` | Fade duration, `50ms` to `1s`; not scaled by `animations.speed` |
@@ -54,7 +54,7 @@ See [examples/config](../examples/config) for every key with its default.
54 | `animations.speed` | `normal` | `slow`, `normal`, `fast`, or a factor from `0.1` to `10` |54 | `animations.speed` | `normal` | `slow`, `normal`, `fast`, or a factor from `0.1` to `10` |
55 | `focus.follow-move` | `off` | Follow a column moved to another workspace |55 | `focus.follow-move` | `off` | Follow a column moved to another workspace |
56 | `focus.position-hint` | `on` | Show workspace/band dots near the output bottom for 600ms after a position change, then fade for 150ms (no fade with animations off); hidden in overview, fullscreen and session lock; never captured |56 | `focus.position-hint` | `on` | Show workspace/band dots near the output bottom for 600ms after a position change, then fade for 150ms (no fade with animations off); hidden in overview, fullscreen and session lock; never captured |
57-| `focus.flash` | `on` | Briefly draw an inner border, inside the window border, on the window, column or output an action changed |57+| `focus.flash` | `on` | Briefly draw an inner border on the window, column or output an action changed |
58 | `focus.flash-width` | `2` | Flash border width in logical pixels, `1` to `8` |58 | `focus.flash-width` | `2` | Flash border width in logical pixels, `1` to `8` |
59 | `focus.flash-opacity` | `0.6` | Starting opacity of the flash, `0.1` to `1` |59 | `focus.flash-opacity` | `0.6` | Starting opacity of the flash, `0.1` to `1` |
60 | `focus.flash-duration` | `200ms` | Fade duration, `50ms` to `1s`; not scaled by `animations.speed` |60 | `focus.flash-duration` | `200ms` | Fade duration, `50ms` to `1s`; not scaled by `animations.speed` |
M internal/adapters/vulkan/focus_flash_test.go +0 -28
@@ -4,7 +4,6 @@ import (
44 "github.com/bnema/neferwl/internal/ports"
55 "github.com/stretchr/testify/require"
66 "image"
7- "math"
87 "testing"
98 )
109
@@ -44,30 +43,3 @@ func TestFocusFlashAbsentEmptyAndThin(t *testing.T) {
4443 require.Equal(t, [4]int32{2, 3, 3, 13}, ds[0].pc.rect)
4544 }
4645 }
47-
48-func TestFocusFlashRoundedRing(t *testing.T) {
49- for _, scale := range []int{1, 2} {
50- r := &Renderer{width: 128, height: 128}
51- s := ports.Scene{Scale: float64(scale),
52- Flash: &ports.FocusFlash{Rect: ports.Rect{X: 8, Y: 4, W: 40, H: 30}, Width: 3, Radius: 11, Color: "#ff0000", Alpha: 0.5}}
53- ds := r.draws(s, nil, newDamage(&target{}, s, image.Rect(0, 0, 128, 128)))
54- require.Len(t, ds, 1)
55- d := ds[0]
56- require.Equal(t, uint32(flagRounded|flagRing), d.pc.misc[1]&(flagRounded|flagRing))
57- require.Equal(t, [4]int32{8 * int32(scale), 4 * int32(scale), 48 * int32(scale), 34 * int32(scale)}, d.pc.rect)
58- require.Equal(t, float32(0.5), d.pc.color[3])
59- require.Equal(t, math.Float32bits(float32(11*scale)), d.radii[0])
60- require.Equal(t, math.Float32bits(float32(8*scale)), d.radii[1])
61- require.Equal(t, roundRect(image.Rect(11*scale, 7*scale, 45*scale, 31*scale)), d.roundInner)
62- }
63-}
64-
65-func TestFocusFlashZeroRadiusStaysSquare(t *testing.T) {
66- r := &Renderer{width: 64, height: 64}
67- s := ports.Scene{Scale: 1, Flash: &ports.FocusFlash{Rect: ports.Rect{X: 8, Y: 4, W: 16, H: 8}, Width: 2, Color: "#ff0000", Alpha: 1}}
68- ds := r.draws(s, nil, newDamage(&target{}, s, image.Rect(0, 0, 64, 64)))
69- require.Len(t, ds, 4)
70- for _, d := range ds {
71- require.Zero(t, d.pc.misc[1]&(flagRounded|flagRing))
72- }
73-}
@@ -4,7 +4,6 @@ import (
4 "github.com/bnema/neferwl/internal/ports"4 "github.com/bnema/neferwl/internal/ports"
5 "github.com/stretchr/testify/require"5 "github.com/stretchr/testify/require"
6 "image"6 "image"
7- "math"
8 "testing"7 "testing"
9 )8 )
10 9
@@ -44,30 +43,3 @@ func TestFocusFlashAbsentEmptyAndThin(t *testing.T) {
44 require.Equal(t, [4]int32{2, 3, 3, 13}, ds[0].pc.rect)43 require.Equal(t, [4]int32{2, 3, 3, 13}, ds[0].pc.rect)
45 }44 }
46 }45 }
47-
48-func TestFocusFlashRoundedRing(t *testing.T) {
49- for _, scale := range []int{1, 2} {
50- r := &Renderer{width: 128, height: 128}
51- s := ports.Scene{Scale: float64(scale),
52- Flash: &ports.FocusFlash{Rect: ports.Rect{X: 8, Y: 4, W: 40, H: 30}, Width: 3, Radius: 11, Color: "#ff0000", Alpha: 0.5}}
53- ds := r.draws(s, nil, newDamage(&target{}, s, image.Rect(0, 0, 128, 128)))
54- require.Len(t, ds, 1)
55- d := ds[0]
56- require.Equal(t, uint32(flagRounded|flagRing), d.pc.misc[1]&(flagRounded|flagRing))
57- require.Equal(t, [4]int32{8 * int32(scale), 4 * int32(scale), 48 * int32(scale), 34 * int32(scale)}, d.pc.rect)
58- require.Equal(t, float32(0.5), d.pc.color[3])
59- require.Equal(t, math.Float32bits(float32(11*scale)), d.radii[0])
60- require.Equal(t, math.Float32bits(float32(8*scale)), d.radii[1])
61- require.Equal(t, roundRect(image.Rect(11*scale, 7*scale, 45*scale, 31*scale)), d.roundInner)
62- }
63-}
64-
65-func TestFocusFlashZeroRadiusStaysSquare(t *testing.T) {
66- r := &Renderer{width: 64, height: 64}
67- s := ports.Scene{Scale: 1, Flash: &ports.FocusFlash{Rect: ports.Rect{X: 8, Y: 4, W: 16, H: 8}, Width: 2, Color: "#ff0000", Alpha: 1}}
68- ds := r.draws(s, nil, newDamage(&target{}, s, image.Rect(0, 0, 64, 64)))
69- require.Len(t, ds, 4)
70- for _, d := range ds {
71- require.Zero(t, d.pc.misc[1]&(flagRounded|flagRing))
72- }
73-}
M internal/adapters/vulkan/scene.go +1 -25
@@ -79,34 +79,10 @@ func (w *sceneWalk) focusFlash() {
7979 }
8080 alpha := w.alpha
8181 w.alpha = float32(f.Alpha)
82- b := min(f.Width, f.Rect.W/2, f.Rect.H/2)
83- if f.Radius > 0 && b > 0 {
84- w.roundedInnerBorder(f.Rect, b, f.Radius, parseColor(f.Color))
85- } else {
86- w.innerBorder(f.Rect, b, parseColor(f.Color))
87- }
82+ w.innerBorder(f.Rect, min(f.Width, f.Rect.W/2, f.Rect.H/2), parseColor(f.Color))
8883 w.alpha = alpha
8984 }
9085
91-// roundedInnerBorder draws the flash as one rounded ring: the target rect with
92-// radius, minus a hole inset by b with radius max(radius-b, 0).
93-func (w *sceneWalk) roundedInnerBorder(t ports.Rect, b, radius int, col [3]uint8) {
94- outer := w.physRect(t.X, t.Y, t.W, t.H)
95- n := len(w.draws)
96- w.fill(outer, col)
97- if len(w.draws) == n {
98- return
99- }
100- r := float32(math.Round(float64(radius) * w.scale))
101- dr := &w.draws[n]
102- roundedDraw(dr, outer, r)
103- dr.pc.misc[1] |= flagRing
104- hole := w.physRect(t.X+b, t.Y+b, t.W-2*b, t.H-2*b)
105- bPhys := min(hole.Min.X-outer.Min.X, hole.Min.Y-outer.Min.Y, outer.Max.X-hole.Max.X, outer.Max.Y-hole.Max.Y)
106- dr.roundInner = roundRect(hole)
107- dr.radii[1] = math.Float32bits(max(r-float32(max(bPhys, 0)), 0))
108-}
109-
11086 // innerBorder draws four non-overlapping strips, or fills a thin target.
11187 func (w *sceneWalk) innerBorder(t ports.Rect, b int, col [3]uint8) {
11288 if b <= 0 {
@@ -79,34 +79,10 @@ func (w *sceneWalk) focusFlash() {
79 }79 }
80 alpha := w.alpha80 alpha := w.alpha
81 w.alpha = float32(f.Alpha)81 w.alpha = float32(f.Alpha)
82- b := min(f.Width, f.Rect.W/2, f.Rect.H/2)82+ w.innerBorder(f.Rect, min(f.Width, f.Rect.W/2, f.Rect.H/2), parseColor(f.Color))
83- if f.Radius > 0 && b > 0 {
84- w.roundedInnerBorder(f.Rect, b, f.Radius, parseColor(f.Color))
85- } else {
86- w.innerBorder(f.Rect, b, parseColor(f.Color))
87- }
88 w.alpha = alpha83 w.alpha = alpha
89 }84 }
90 85
91-// roundedInnerBorder draws the flash as one rounded ring: the target rect with
92-// radius, minus a hole inset by b with radius max(radius-b, 0).
93-func (w *sceneWalk) roundedInnerBorder(t ports.Rect, b, radius int, col [3]uint8) {
94- outer := w.physRect(t.X, t.Y, t.W, t.H)
95- n := len(w.draws)
96- w.fill(outer, col)
97- if len(w.draws) == n {
98- return
99- }
100- r := float32(math.Round(float64(radius) * w.scale))
101- dr := &w.draws[n]
102- roundedDraw(dr, outer, r)
103- dr.pc.misc[1] |= flagRing
104- hole := w.physRect(t.X+b, t.Y+b, t.W-2*b, t.H-2*b)
105- bPhys := min(hole.Min.X-outer.Min.X, hole.Min.Y-outer.Min.Y, outer.Max.X-hole.Max.X, outer.Max.Y-hole.Max.Y)
106- dr.roundInner = roundRect(hole)
107- dr.radii[1] = math.Float32bits(max(r-float32(max(bPhys, 0)), 0))
108-}
109-
110 // innerBorder draws four non-overlapping strips, or fills a thin target.86 // innerBorder draws four non-overlapping strips, or fills a thin target.
111 func (w *sceneWalk) innerBorder(t ports.Rect, b int, col [3]uint8) {87 func (w *sceneWalk) innerBorder(t ports.Rect, b int, col [3]uint8) {
112 if b <= 0 {88 if b <= 0 {
M internal/adapters/wayland/syncobj_test.go +3 -1
@@ -443,7 +443,9 @@ func TestSyncobjProtocolErrors(t *testing.T) {
443443 }, func(h *syncHarness) uint32 { return h.syncSurf }, uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorUnsupportedBuffer)},
444444 {"surface destroyed", func(h *syncHarness) {
445445 requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestDestroy)
446- h.setPoints(1, 2)
446+ // The first request already raises the error and the server
447+ // then closes the connection: a second write could hit EPIPE.
448+ requestProtocol(t, h.c, h.syncSurf, linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1RequestSetAcquirePoint, h.timeline, uint32(0), uint32(1))
447449 }, func(h *syncHarness) uint32 { return h.syncSurf }, uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorNoSurface)},
448450 {"surface exists", func(h *syncHarness) {
449451 mgr := bindProtocol(t, h.c, "wp_linux_drm_syncobj_manager_v1")
@@ -443,7 +443,9 @@ func TestSyncobjProtocolErrors(t *testing.T) {
443 }, func(h *syncHarness) uint32 { return h.syncSurf }, uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorUnsupportedBuffer)},443 }, func(h *syncHarness) uint32 { return h.syncSurf }, uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorUnsupportedBuffer)},
444 {"surface destroyed", func(h *syncHarness) {444 {"surface destroyed", func(h *syncHarness) {
445 requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestDestroy)445 requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestDestroy)
446- h.setPoints(1, 2)446+ // The first request already raises the error and the server
447+ // then closes the connection: a second write could hit EPIPE.
448+ requestProtocol(t, h.c, h.syncSurf, linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1RequestSetAcquirePoint, h.timeline, uint32(0), uint32(1))
447 }, func(h *syncHarness) uint32 { return h.syncSurf }, uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorNoSurface)},449 }, func(h *syncHarness) uint32 { return h.syncSurf }, uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorNoSurface)},
448 {"surface exists", func(h *syncHarness) {450 {"surface exists", func(h *syncHarness) {
449 mgr := bindProtocol(t, h.c, "wp_linux_drm_syncobj_manager_v1")451 mgr := bindProtocol(t, h.c, "wp_linux_drm_syncobj_manager_v1")
M internal/core/capture_test.go +12 -6
@@ -68,6 +68,16 @@ func (r *captureRig) layer(id ports.WindowID, layer ports.Layer, keyboard uint32
6868 return ports.LayerSurface{ID: id, Layer: layer, Anchor: ports.AnchorTop | ports.AnchorLeft, Width: 10, Height: 10, Output: "OUT-1", Keyboard: keyboard}
6969 }
7070
71+// drainCommands drops the commands already queued. Call it only once the
72+// core is quiet (after receiving a scene, which is published after its
73+// commands) and before sending the event under test: draining after the
74+// send races with the core emitting the commands the test waits for.
75+func (r *captureRig) drainCommands() {
76+ for len(r.commands) > 0 {
77+ <-r.commands
78+ }
79+}
80+
7181 // excludedScene waits for a scene whose exclusion list has n entries.
7282 func (r *captureRig) excludedScene(t *testing.T, n int) ports.Scene {
7383 t.Helper()
@@ -372,10 +382,8 @@ func TestCaptureExclusionKeyboardClamp(t *testing.T) {
372382 r.client <- ports.CaptureExclusionBegin{Session: 1}
373383 r.client <- ports.CaptureExclusionLayer{Session: 1, Layer: 10, Attached: true}
374384 r.excludedScene(t, 1)
385+ r.drainCommands()
375386 r.client <- ports.LayerChanged{Layers: []ports.LayerSurface{r.layer(10, ports.LayerOverlay, 1), r.layer(11, ports.LayerOverlay, 1)}}
376- for len(r.commands) > 0 {
377- <-r.commands
378- }
379387 scene(t, r.scenes)
380388 for {
381389 if f, ok := receive(t, r.commands).(ports.FocusWindow); ok {
@@ -396,9 +404,7 @@ func TestCaptureExclusionKeyboardNone(t *testing.T) {
396404 r.client <- ports.CaptureExclusionBegin{Session: 1}
397405 r.client <- ports.CaptureExclusionLayer{Session: 1, Layer: 10, Attached: true}
398406 r.excludedScene(t, 1)
399- for len(r.commands) > 0 {
400- <-r.commands
401- }
407+ r.drainCommands()
402408 r.client <- ports.WindowMapped{ID: 1}
403409 scene(t, r.scenes)
404410 for len(r.commands) > 0 {
@@ -68,6 +68,16 @@ func (r *captureRig) layer(id ports.WindowID, layer ports.Layer, keyboard uint32
68 return ports.LayerSurface{ID: id, Layer: layer, Anchor: ports.AnchorTop | ports.AnchorLeft, Width: 10, Height: 10, Output: "OUT-1", Keyboard: keyboard}68 return ports.LayerSurface{ID: id, Layer: layer, Anchor: ports.AnchorTop | ports.AnchorLeft, Width: 10, Height: 10, Output: "OUT-1", Keyboard: keyboard}
69 }69 }
70 70
71+// drainCommands drops the commands already queued. Call it only once the
72+// core is quiet (after receiving a scene, which is published after its
73+// commands) and before sending the event under test: draining after the
74+// send races with the core emitting the commands the test waits for.
75+func (r *captureRig) drainCommands() {
76+ for len(r.commands) > 0 {
77+ <-r.commands
78+ }
79+}
80+
71 // excludedScene waits for a scene whose exclusion list has n entries.81 // excludedScene waits for a scene whose exclusion list has n entries.
72 func (r *captureRig) excludedScene(t *testing.T, n int) ports.Scene {82 func (r *captureRig) excludedScene(t *testing.T, n int) ports.Scene {
73 t.Helper()83 t.Helper()
@@ -372,10 +382,8 @@ func TestCaptureExclusionKeyboardClamp(t *testing.T) {
372 r.client <- ports.CaptureExclusionBegin{Session: 1}382 r.client <- ports.CaptureExclusionBegin{Session: 1}
373 r.client <- ports.CaptureExclusionLayer{Session: 1, Layer: 10, Attached: true}383 r.client <- ports.CaptureExclusionLayer{Session: 1, Layer: 10, Attached: true}
374 r.excludedScene(t, 1)384 r.excludedScene(t, 1)
385+ r.drainCommands()
375 r.client <- ports.LayerChanged{Layers: []ports.LayerSurface{r.layer(10, ports.LayerOverlay, 1), r.layer(11, ports.LayerOverlay, 1)}}386 r.client <- ports.LayerChanged{Layers: []ports.LayerSurface{r.layer(10, ports.LayerOverlay, 1), r.layer(11, ports.LayerOverlay, 1)}}
376- for len(r.commands) > 0 {
377- <-r.commands
378- }
379 scene(t, r.scenes)387 scene(t, r.scenes)
380 for {388 for {
381 if f, ok := receive(t, r.commands).(ports.FocusWindow); ok {389 if f, ok := receive(t, r.commands).(ports.FocusWindow); ok {
@@ -396,9 +404,7 @@ func TestCaptureExclusionKeyboardNone(t *testing.T) {
396 r.client <- ports.CaptureExclusionBegin{Session: 1}404 r.client <- ports.CaptureExclusionBegin{Session: 1}
397 r.client <- ports.CaptureExclusionLayer{Session: 1, Layer: 10, Attached: true}405 r.client <- ports.CaptureExclusionLayer{Session: 1, Layer: 10, Attached: true}
398 r.excludedScene(t, 1)406 r.excludedScene(t, 1)
399- for len(r.commands) > 0 {407+ r.drainCommands()
400- <-r.commands
401- }
402 r.client <- ports.WindowMapped{ID: 1}408 r.client <- ports.WindowMapped{ID: 1}
403 scene(t, r.scenes)409 scene(t, r.scenes)
404 for len(r.commands) > 0 {410 for len(r.commands) > 0 {
M internal/core/flash.go +15 -19
@@ -31,7 +31,6 @@ type focusFlash struct {
3131 timerStop func() bool
3232 running *flashTarget
3333 rect Rect
34- radius int // Corner radius of the target, as SceneWindow.Radius.
3534 start time.Time
3635 value float64 // Retained between this output's own flips.
3736 lastStarts map[flashTarget]time.Time
@@ -88,7 +87,7 @@ func (c *Core) flashTick() bool {
8887 f.timerC, f.timerStop = newTimer(c.opts.Clock, wait)
8988 return false
9089 }
91- rect, radius, ok := c.flashRect(target, sc)
90+ rect, ok := c.flashRect(target, sc)
9291 if !ok {
9392 return false
9493 }
@@ -104,7 +103,7 @@ func (c *Core) flashTick() bool {
104103 }
105104 }
106105 f.lastStarts[target] = now
107- f.running, f.rect, f.radius, f.start = &target, rect, radius, now
106+ f.running, f.rect, f.start = &target, rect, now
108107 f.value = c.cfg.Focus.FlashOpacity
109108 return true
110109 }
@@ -119,7 +118,7 @@ func (c *Core) advanceFlash(only *screen) {
119118 return
120119 }
121120 sc := c.screens[c.screenIndex(f.running.output)]
122- if rect, radius, ok := c.flashRect(*f.running, sc); !ok || rect != f.rect || radius != f.radius {
121+ if rect, ok := c.flashRect(*f.running, sc); !ok || rect != f.rect {
123122 c.stopFlash()
124123 return
125124 }
@@ -139,50 +138,47 @@ func (c *Core) advanceFlash(only *screen) {
139138 // flashRect resolves the current target, never retaining a border after its
140139 // visibility or geometry changes. stopFlash leaves the published sample intact
141140 // so the scene diff also damages the old border.
142-func (c *Core) flashRect(target flashTarget, sc *screen) (Rect, int, bool) {
141+func (c *Core) flashRect(target flashTarget, sc *screen) (Rect, bool) {
143142 if sc.mon.ov.open {
144- return Rect{}, 0, false
143+ return Rect{}, false
145144 }
146145 if target.level == flashMonitor {
147146 o := sc.mon.Output()
148- return Rect{W: o.W, H: o.H}, 0, true
147+ return Rect{W: o.W, H: o.H}, true
149148 }
150149 owner, _ := c.screenOf(target.window)
151150 if owner != sc || c.windows.lookup(target.window).client.ID == 0 {
152- return Rect{}, 0, false
151+ return Rect{}, false
153152 }
154153 view := sc.mon.viewport()
155154 c.flash.layout = sc.mon.layoutInto(c.flash.layout)
156155 var rect Rect
157- radius := 0
158156 found := false
159157 for _, p := range c.flash.layout {
160158 if p.ID == target.window {
161159 if p.Fullscreen || p.Preview != 0 || !view.shows(p) {
162- return Rect{}, 0, false
160+ return Rect{}, false
163161 }
164- // The flash sits inside the drawn border, hugging the body.
165- rect, found = c.clientRect(p), true
166- radius = max(windowRadius(p, sc.mon.Current().gap(), c.cfg.Border.Radius)-c.cfg.Border.Width, 0)
162+ rect, found = p.Rect, true
167163 break
168164 }
169165 }
170166 if !found {
171- return Rect{}, 0, false
167+ return Rect{}, false
172168 }
173169 if target.level == flashColumn {
174170 w := sc.mon.Current()
175171 col := w.columnOf(target.window)
176172 if col < 0 {
177- return Rect{}, 0, false
173+ return Rect{}, false
178174 }
179175 for _, p := range c.flash.layout {
180176 if !p.Hidden && slices.Contains(w.Columns[col].Windows, p.ID) {
181- rect = union(rect, c.clientRect(p))
177+ rect = union(rect, p.Rect)
182178 }
183179 }
184180 }
185- return rect, radius, true
181+ return rect, true
186182 }
187183
188184 func (c *Core) flashFor(sc *screen) *ports.FocusFlash {
@@ -194,9 +190,9 @@ func (c *Core) flashFor(sc *screen) *ports.FocusFlash {
194190 if color == "" {
195191 color = c.cfg.Border.Active
196192 }
197- sample := ports.FocusFlash{Rect: f.rect, Width: c.cfg.Focus.FlashWidth, Radius: f.radius, Color: color, Alpha: f.value}
193+ sample := ports.FocusFlash{Rect: f.rect, Width: c.cfg.Focus.FlashWidth, Color: color, Alpha: f.value}
198194 if f.published == nil || *f.published != sample {
199- f.published = &sample
195+ f.published = &ports.FocusFlash{Rect: sample.Rect, Width: sample.Width, Color: sample.Color, Alpha: sample.Alpha}
200196 }
201197 return f.published
202198 }
@@ -31,7 +31,6 @@ type focusFlash struct {
31 timerStop func() bool31 timerStop func() bool
32 running *flashTarget32 running *flashTarget
33 rect Rect33 rect Rect
34- radius int // Corner radius of the target, as SceneWindow.Radius.
35 start time.Time34 start time.Time
36 value float64 // Retained between this output's own flips.35 value float64 // Retained between this output's own flips.
37 lastStarts map[flashTarget]time.Time36 lastStarts map[flashTarget]time.Time
@@ -88,7 +87,7 @@ func (c *Core) flashTick() bool {
88 f.timerC, f.timerStop = newTimer(c.opts.Clock, wait)87 f.timerC, f.timerStop = newTimer(c.opts.Clock, wait)
89 return false88 return false
90 }89 }
91- rect, radius, ok := c.flashRect(target, sc)90+ rect, ok := c.flashRect(target, sc)
92 if !ok {91 if !ok {
93 return false92 return false
94 }93 }
@@ -104,7 +103,7 @@ func (c *Core) flashTick() bool {
104 }103 }
105 }104 }
106 f.lastStarts[target] = now105 f.lastStarts[target] = now
107- f.running, f.rect, f.radius, f.start = &target, rect, radius, now106+ f.running, f.rect, f.start = &target, rect, now
108 f.value = c.cfg.Focus.FlashOpacity107 f.value = c.cfg.Focus.FlashOpacity
109 return true108 return true
110 }109 }
@@ -119,7 +118,7 @@ func (c *Core) advanceFlash(only *screen) {
119 return118 return
120 }119 }
121 sc := c.screens[c.screenIndex(f.running.output)]120 sc := c.screens[c.screenIndex(f.running.output)]
122- if rect, radius, ok := c.flashRect(*f.running, sc); !ok || rect != f.rect || radius != f.radius {121+ if rect, ok := c.flashRect(*f.running, sc); !ok || rect != f.rect {
123 c.stopFlash()122 c.stopFlash()
124 return123 return
125 }124 }
@@ -139,50 +138,47 @@ func (c *Core) advanceFlash(only *screen) {
139 // flashRect resolves the current target, never retaining a border after its138 // flashRect resolves the current target, never retaining a border after its
140 // visibility or geometry changes. stopFlash leaves the published sample intact139 // visibility or geometry changes. stopFlash leaves the published sample intact
141 // so the scene diff also damages the old border.140 // so the scene diff also damages the old border.
142-func (c *Core) flashRect(target flashTarget, sc *screen) (Rect, int, bool) {141+func (c *Core) flashRect(target flashTarget, sc *screen) (Rect, bool) {
143 if sc.mon.ov.open {142 if sc.mon.ov.open {
144- return Rect{}, 0, false143+ return Rect{}, false
145 }144 }
146 if target.level == flashMonitor {145 if target.level == flashMonitor {
147 o := sc.mon.Output()146 o := sc.mon.Output()
148- return Rect{W: o.W, H: o.H}, 0, true147+ return Rect{W: o.W, H: o.H}, true
149 }148 }
150 owner, _ := c.screenOf(target.window)149 owner, _ := c.screenOf(target.window)
151 if owner != sc || c.windows.lookup(target.window).client.ID == 0 {150 if owner != sc || c.windows.lookup(target.window).client.ID == 0 {
152- return Rect{}, 0, false151+ return Rect{}, false
153 }152 }
154 view := sc.mon.viewport()153 view := sc.mon.viewport()
155 c.flash.layout = sc.mon.layoutInto(c.flash.layout)154 c.flash.layout = sc.mon.layoutInto(c.flash.layout)
156 var rect Rect155 var rect Rect
157- radius := 0
158 found := false156 found := false
159 for _, p := range c.flash.layout {157 for _, p := range c.flash.layout {
160 if p.ID == target.window {158 if p.ID == target.window {
161 if p.Fullscreen || p.Preview != 0 || !view.shows(p) {159 if p.Fullscreen || p.Preview != 0 || !view.shows(p) {
162- return Rect{}, 0, false160+ return Rect{}, false
163 }161 }
164- // The flash sits inside the drawn border, hugging the body.162+ rect, found = p.Rect, true
165- rect, found = c.clientRect(p), true
166- radius = max(windowRadius(p, sc.mon.Current().gap(), c.cfg.Border.Radius)-c.cfg.Border.Width, 0)
167 break163 break
168 }164 }
169 }165 }
170 if !found {166 if !found {
171- return Rect{}, 0, false167+ return Rect{}, false
172 }168 }
173 if target.level == flashColumn {169 if target.level == flashColumn {
174 w := sc.mon.Current()170 w := sc.mon.Current()
175 col := w.columnOf(target.window)171 col := w.columnOf(target.window)
176 if col < 0 {172 if col < 0 {
177- return Rect{}, 0, false173+ return Rect{}, false
178 }174 }
179 for _, p := range c.flash.layout {175 for _, p := range c.flash.layout {
180 if !p.Hidden && slices.Contains(w.Columns[col].Windows, p.ID) {176 if !p.Hidden && slices.Contains(w.Columns[col].Windows, p.ID) {
181- rect = union(rect, c.clientRect(p))177+ rect = union(rect, p.Rect)
182 }178 }
183 }179 }
184 }180 }
185- return rect, radius, true181+ return rect, true
186 }182 }
187 183
188 func (c *Core) flashFor(sc *screen) *ports.FocusFlash {184 func (c *Core) flashFor(sc *screen) *ports.FocusFlash {
@@ -194,9 +190,9 @@ func (c *Core) flashFor(sc *screen) *ports.FocusFlash {
194 if color == "" {190 if color == "" {
195 color = c.cfg.Border.Active191 color = c.cfg.Border.Active
196 }192 }
197- sample := ports.FocusFlash{Rect: f.rect, Width: c.cfg.Focus.FlashWidth, Radius: f.radius, Color: color, Alpha: f.value}193+ sample := ports.FocusFlash{Rect: f.rect, Width: c.cfg.Focus.FlashWidth, Color: color, Alpha: f.value}
198 if f.published == nil || *f.published != sample {194 if f.published == nil || *f.published != sample {
199- f.published = &sample195+ f.published = &ports.FocusFlash{Rect: sample.Rect, Width: sample.Width, Color: sample.Color, Alpha: sample.Alpha}
200 }196 }
201 return f.published197 return f.published
202 }198 }
M internal/core/flash_test.go +0 -57
@@ -717,60 +717,3 @@ func TestFlashGeometryRevalidationAllocations(t *testing.T) {
717717 })
718718 }
719719 }
720-
721-func TestFlashFollowsInnerBorderAndRadius(t *testing.T) {
722- for _, test := range []struct {
723- name string
724- gaps int
725- float bool
726- level flashLevel
727- radius int
728- }{
729- {"tile with gaps", 12, false, flashWindow, 11},
730- {"column with gaps", 12, false, flashColumn, 11},
731- {"tile without gaps", 0, false, flashWindow, 0},
732- {"float", 0, true, flashWindow, 11},
733- {"monitor", 12, false, flashMonitor, 0},
734- } {
735- t.Run(test.name, func(t *testing.T) {
736- c, clock := flashCore(t)
737- c.cfg.Border.Width, c.cfg.Border.Radius = 3, 14
738- w := c.cur().mon.Current()
739- w.Gaps = test.gaps
740- if test.float {
741- c.applyAction(ActionToggleFloating)
742- c.stopFlash()
743- c.stopAnimations()
744- }
745- id := WindowID(3)
746- if f, ok := w.Focused(); ok && test.float {
747- id = f
748- }
749- startFlash(t, c, clock, test.level, id)
750- want := Rect{W: 300, H: 200}
751- if test.level != flashMonitor {
752- for _, p := range c.cur().mon.Layout() {
753- if p.ID == id {
754- want = p.Rect.Inset(p.Inset, 3)
755- if test.level == flashColumn {
756- want = c.flash.rect
757- }
758- }
759- }
760- }
761- if c.flash.rect != want || c.flash.radius != test.radius {
762- t.Fatalf("rect %+v radius %d, want %+v radius %d", c.flash.rect, c.flash.radius, want, test.radius)
763- }
764- if f := c.flashFor(c.cur()); f == nil || f.Radius != test.radius {
765- t.Fatalf("published %+v", f)
766- }
767- if test.level == flashWindow && test.gaps > 0 {
768- for _, p := range c.cur().mon.Layout() {
769- if p.ID == id && (p.Inset != ports.SideAll || c.flash.rect.W != p.Rect.W-6 || c.flash.rect.H != p.Rect.H-6) {
770- t.Fatalf("tile %+v not inset by 3 on all sides: %+v", p, c.flash.rect)
771- }
772- }
773- }
774- })
775- }
776-}
@@ -717,60 +717,3 @@ func TestFlashGeometryRevalidationAllocations(t *testing.T) {
717 })717 })
718 }718 }
719 }719 }
720-
721-func TestFlashFollowsInnerBorderAndRadius(t *testing.T) {
722- for _, test := range []struct {
723- name string
724- gaps int
725- float bool
726- level flashLevel
727- radius int
728- }{
729- {"tile with gaps", 12, false, flashWindow, 11},
730- {"column with gaps", 12, false, flashColumn, 11},
731- {"tile without gaps", 0, false, flashWindow, 0},
732- {"float", 0, true, flashWindow, 11},
733- {"monitor", 12, false, flashMonitor, 0},
734- } {
735- t.Run(test.name, func(t *testing.T) {
736- c, clock := flashCore(t)
737- c.cfg.Border.Width, c.cfg.Border.Radius = 3, 14
738- w := c.cur().mon.Current()
739- w.Gaps = test.gaps
740- if test.float {
741- c.applyAction(ActionToggleFloating)
742- c.stopFlash()
743- c.stopAnimations()
744- }
745- id := WindowID(3)
746- if f, ok := w.Focused(); ok && test.float {
747- id = f
748- }
749- startFlash(t, c, clock, test.level, id)
750- want := Rect{W: 300, H: 200}
751- if test.level != flashMonitor {
752- for _, p := range c.cur().mon.Layout() {
753- if p.ID == id {
754- want = p.Rect.Inset(p.Inset, 3)
755- if test.level == flashColumn {
756- want = c.flash.rect
757- }
758- }
759- }
760- }
761- if c.flash.rect != want || c.flash.radius != test.radius {
762- t.Fatalf("rect %+v radius %d, want %+v radius %d", c.flash.rect, c.flash.radius, want, test.radius)
763- }
764- if f := c.flashFor(c.cur()); f == nil || f.Radius != test.radius {
765- t.Fatalf("published %+v", f)
766- }
767- if test.level == flashWindow && test.gaps > 0 {
768- for _, p := range c.cur().mon.Layout() {
769- if p.ID == id && (p.Inset != ports.SideAll || c.flash.rect.W != p.Rect.W-6 || c.flash.rect.H != p.Rect.H-6) {
770- t.Fatalf("tile %+v not inset by 3 on all sides: %+v", p, c.flash.rect)
771- }
772- }
773- }
774- })
775- }
776-}
M internal/ports/scene.go +2 -5
@@ -201,11 +201,8 @@ type SceneLayer struct {
201201 type FocusFlash struct {
202202 Rect Rect
203203 Width int
204- // Radius is the target's corner radius in logical pixels (0 = square),
205- // the same as SceneWindow.Radius; the border follows it.
206- Radius int
207- Color string
208- Alpha float64
204+ Color string
205+ Alpha float64
209206 }
210207
211208 // SceneWindow carries core → renderer window placement.
@@ -201,11 +201,8 @@ type SceneLayer struct {
201 type FocusFlash struct {201 type FocusFlash struct {
202 Rect Rect202 Rect Rect
203 Width int203 Width int
204- // Radius is the target's corner radius in logical pixels (0 = square),204+ Color string
205- // the same as SceneWindow.Radius; the border follows it.205+ Alpha float64
206- Radius int
207- Color string
208- Alpha float64
209 }206 }
210 207
211 // SceneWindow carries core → renderer window placement.208 // SceneWindow carries core → renderer window placement.

Comments0 open / 0

no comments yet.

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
l
merge request: the list of files (j k move, enter opens, esc back)
M
merge request: the merge menu; there 1–3 pick the mode, D deletes the branch, enter merges
R
merge request: mark the head read
esc
close a dialog, leave a field

Branches 25

Tags 10

↑↓ moveenter openesc close