test(core): cover the focus pulse above a panel
Also make the unfocused-inset test fail when its tile is missing, and describe the usable area in the separators and visibleCount docs.
- commit
- 0e2dc8ef94437d82359769ccdb37556a7279536c
- author
- brice · · 2026-10-07 14:29 UTC
- parent
- 29cd2a5
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
4 files · +28 -11
M internal/core/overview_cascade_test.go +4 -4
| @@ -194,15 +194,15 @@ func TestOverviewCascadeMaximizedCardPickRecordsWindow(t *testing.T) { | |||
| 194 | 194 | // elsewhere fills its band. | |
| 195 | 195 | func TestOverviewCascadeUnfocusedMaximizedNotInset(t *testing.T) { | |
| 196 | 196 | w := cascadeWorkspace(6) | |
| 197 | + | w.SetGaps(8) | |
| 197 | 198 | w.Focus = 4 | |
| 198 | 199 | w.maximize(4) | |
| 199 | 200 | w.Columns[0].FullWidth = true | |
| 200 | 201 | tiles, _, _ := w.previewTiles() | |
| 201 | 202 | g := w.gap() | |
| 202 | - | for _, p := range tiles { | |
| 203 | - | if p.ID == 1 && (p.Rect.X != g || p.Rect.W != w.Usable.W-2*g) { | |
| 204 | - | t.Fatalf("unfocused maximized column %+v, want X %d W %d", p.Rect, g, w.Usable.W-2*g) | |
| 205 | - | } | |
| 203 | + | p := previewOf(t, tiles, 1) | |
| 204 | + | if p.Hidden || p.Rect.X != g || p.Rect.W != w.Usable.W-2*g { | |
| 205 | + | t.Fatalf("unfocused maximized column %+v, want X %d W %d", p, g, w.Usable.W-2*g) | |
| 206 | 206 | } | |
| 207 | 207 | } | |
| 208 | 208 | ||
| @@ -194,15 +194,15 @@ func TestOverviewCascadeMaximizedCardPickRecordsWindow(t *testing.T) { | |||||
| 194 | // elsewhere fills its band. | 194 | // elsewhere fills its band. | ||
| 195 | func TestOverviewCascadeUnfocusedMaximizedNotInset(t *testing.T) { | 195 | func TestOverviewCascadeUnfocusedMaximizedNotInset(t *testing.T) { | ||
| 196 | w := cascadeWorkspace(6) | 196 | w := cascadeWorkspace(6) | ||
| 197 | + | w.SetGaps(8) | |||
| 197 | w.Focus = 4 | 198 | w.Focus = 4 | ||
| 198 | w.maximize(4) | 199 | w.maximize(4) | ||
| 199 | w.Columns[0].FullWidth = true | 200 | w.Columns[0].FullWidth = true | ||
| 200 | tiles, _, _ := w.previewTiles() | 201 | tiles, _, _ := w.previewTiles() | ||
| 201 | g := w.gap() | 202 | g := w.gap() | ||
| 202 | - | for _, p := range tiles { | 203 | + | p := previewOf(t, tiles, 1) |
| 203 | - | if p.ID == 1 && (p.Rect.X != g || p.Rect.W != w.Usable.W-2*g) { | 204 | + | if p.Hidden || p.Rect.X != g || p.Rect.W != w.Usable.W-2*g { |
| 204 | - | t.Fatalf("unfocused maximized column %+v, want X %d W %d", p.Rect, g, w.Usable.W-2*g) | 205 | + | t.Fatalf("unfocused maximized column %+v, want X %d W %d", p, g, w.Usable.W-2*g) |
| 205 | - | } | |||
| 206 | } | 206 | } | ||
| 207 | } | 207 | } | ||
| 208 | 208 | ||||
M internal/core/pulse.go +3 -3
| @@ -151,11 +151,11 @@ func (m *Monitor) settled() bool { | |||
| 151 | 151 | return true | |
| 152 | 152 | } | |
| 153 | 153 | ||
| 154 | - | // visibleCount is the number of windows of layout shown in frame. | |
| 155 | - | func visibleCount(layout []Placement, frame Rect) int { | |
| 154 | + | // visibleCount is the number of windows of layout shown in area. | |
| 155 | + | func visibleCount(layout []Placement, area Rect) int { | |
| 156 | 156 | n := 0 | |
| 157 | 157 | for _, p := range layout { | |
| 158 | - | if p.Preview == 0 && onScreen(p, frame) { | |
| 158 | + | if p.Preview == 0 && onScreen(p, area) { | |
| 159 | 159 | n++ | |
| 160 | 160 | } | |
| 161 | 161 | } | |
| @@ -151,11 +151,11 @@ func (m *Monitor) settled() bool { | |||||
| 151 | return true | 151 | return true | ||
| 152 | } | 152 | } | ||
| 153 | 153 | ||||
| 154 | - | // visibleCount is the number of windows of layout shown in frame. | 154 | + | // visibleCount is the number of windows of layout shown in area. |
| 155 | - | func visibleCount(layout []Placement, frame Rect) int { | 155 | + | func visibleCount(layout []Placement, area Rect) int { |
| 156 | n := 0 | 156 | n := 0 | ||
| 157 | for _, p := range layout { | 157 | for _, p := range layout { | ||
| 158 | - | if p.Preview == 0 && onScreen(p, frame) { | 158 | + | if p.Preview == 0 && onScreen(p, area) { |
| 159 | n++ | 159 | n++ | ||
| 160 | } | 160 | } | ||
| 161 | } | 161 | } | ||
M internal/core/pulse_test.go +16 -0
| @@ -185,6 +185,22 @@ func TestFocusPulseSkipsAloneOnScreen(t *testing.T) { | |||
| 185 | 185 | } | |
| 186 | 186 | } | |
| 187 | 187 | ||
| 188 | + | // A cascade window alone in its band above a bottom panel is alone on | |
| 189 | + | // screen: the next band, under the panel, does not count. | |
| 190 | + | func TestFocusPulseSkipsAloneAbovePanel(t *testing.T) { | |
| 191 | + | c, ic := pulseCore(t) | |
| 192 | + | m := c.cur().mon | |
| 193 | + | m.SetOverflow(OverflowCascade) | |
| 194 | + | m.SetMaxColumns(1) | |
| 195 | + | m.SetUsable(Rect{W: 300, H: 180}) | |
| 196 | + | m.Current().FocusID(1) | |
| 197 | + | indicatorScene(t, c) | |
| 198 | + | ic.now = ic.now.Add(pulseSettle) | |
| 199 | + | if c.pulseTick() || c.animating() { | |
| 200 | + | t.Fatal("a window alone in its band above a panel pulses") | |
| 201 | + | } | |
| 202 | + | } | |
| 203 | + | ||
| 188 | 204 | // A swipe, its landing slide or a transition still shows the neighbours: the | |
| 189 | 205 | // pulse waits for the layout to settle, then decides on it. | |
| 190 | 206 | func TestFocusPulseWaitsForSlide(t *testing.T) { | |
| @@ -185,6 +185,22 @@ func TestFocusPulseSkipsAloneOnScreen(t *testing.T) { | |||||
| 185 | } | 185 | } | ||
| 186 | } | 186 | } | ||
| 187 | 187 | ||||
| 188 | + | // A cascade window alone in its band above a bottom panel is alone on | |||
| 189 | + | // screen: the next band, under the panel, does not count. | |||
| 190 | + | func TestFocusPulseSkipsAloneAbovePanel(t *testing.T) { | |||
| 191 | + | c, ic := pulseCore(t) | |||
| 192 | + | m := c.cur().mon | |||
| 193 | + | m.SetOverflow(OverflowCascade) | |||
| 194 | + | m.SetMaxColumns(1) | |||
| 195 | + | m.SetUsable(Rect{W: 300, H: 180}) | |||
| 196 | + | m.Current().FocusID(1) | |||
| 197 | + | indicatorScene(t, c) | |||
| 198 | + | ic.now = ic.now.Add(pulseSettle) | |||
| 199 | + | if c.pulseTick() || c.animating() { | |||
| 200 | + | t.Fatal("a window alone in its band above a panel pulses") | |||
| 201 | + | } | |||
| 202 | + | } | |||
| 203 | + | ||||
| 188 | // A swipe, its landing slide or a transition still shows the neighbours: the | 204 | // A swipe, its landing slide or a transition still shows the neighbours: the | ||
| 189 | // pulse waits for the layout to settle, then decides on it. | 205 | // pulse waits for the layout to settle, then decides on it. | ||
| 190 | func TestFocusPulseWaitsForSlide(t *testing.T) { | 206 | func TestFocusPulseWaitsForSlide(t *testing.T) { | ||
M internal/core/separators.go +5 -4
| @@ -8,10 +8,11 @@ import "github.com/bnema/neferwl/internal/ports" | |||
| 8 | 8 | // sides of the focused tile, including corners. With exactly two visible | |
| 9 | 9 | // tiles and no gaps, each lights its own half of the shared line (left/top | |
| 10 | 10 | // first). With gaps, each tile owns its neighbor-facing lines and lights | |
| 11 | - | // them whole. Hidden, scrolled-off, other-workspace and other-output tiles | |
| 12 | - | // do not count. Lines at output edges are omitted. Floating windows differ | |
| 13 | - | // from tmux tiles: each gets its own full border, drawn with that window | |
| 14 | - | // (Separator.Window), even when it is the only visible window. Only the | |
| 11 | + | // them whole. Hidden, scrolled-off, under-panel, other-workspace and | |
| 12 | + | // other-output tiles do not count, nor do tiles that share no line. Lines | |
| 13 | + | // at output edges are omitted. Floating windows differ from tmux tiles: | |
| 14 | + | // each gets its own full border, drawn with that window (Separator.Window), | |
| 15 | + | // even when it is the only visible window. Only the | |
| 15 | 16 | // focused output's focused window can light lines; keyboard grabs by popups | |
| 16 | 17 | // and layers do not change the underlying window's visual focus. | |
| 17 | 18 | ||
| @@ -8,10 +8,11 @@ import "github.com/bnema/neferwl/internal/ports" | |||||
| 8 | // sides of the focused tile, including corners. With exactly two visible | 8 | // sides of the focused tile, including corners. With exactly two visible | ||
| 9 | // tiles and no gaps, each lights its own half of the shared line (left/top | 9 | // tiles and no gaps, each lights its own half of the shared line (left/top | ||
| 10 | // first). With gaps, each tile owns its neighbor-facing lines and lights | 10 | // first). With gaps, each tile owns its neighbor-facing lines and lights | ||
| 11 | - | // them whole. Hidden, scrolled-off, other-workspace and other-output tiles | 11 | + | // them whole. Hidden, scrolled-off, under-panel, other-workspace and |
| 12 | - | // do not count. Lines at output edges are omitted. Floating windows differ | 12 | + | // other-output tiles do not count, nor do tiles that share no line. Lines |
| 13 | - | // from tmux tiles: each gets its own full border, drawn with that window | 13 | + | // at output edges are omitted. Floating windows differ from tmux tiles: |
| 14 | - | // (Separator.Window), even when it is the only visible window. Only the | 14 | + | // each gets its own full border, drawn with that window (Separator.Window), |
| 15 | + | // even when it is the only visible window. Only the | |||
| 15 | // focused output's focused window can light lines; keyboard grabs by popups | 16 | // focused output's focused window can light lines; keyboard grabs by popups | ||
| 16 | // and layers do not change the underlying window's visual focus. | 17 | // and layers do not change the underlying window's visual focus. | ||
| 17 | 18 | ||||