fix(layout): ignore tiles under panels for line halves and pulse
Separators count only tiles with a visible neighbor, and the focus pulse counts windows in the usable area. A cascade band above a panel lights its half of a shared line again.
- commit
- 29cd2a57237c240b61be408af29f3295266ee67d
- author
- brice · · 2026-10-07 14:17 UTC
- parent
- 4079024
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
4 files · +22 -12
M internal/core/core.go +2 -1
| @@ -632,7 +632,8 @@ func (c *Core) publishFrame(ctx context.Context, only *screen) error { | |||
| 632 | 632 | scene.Separators = separators(layout, c.cfg.Border.Width, sc.mon.Current().gap(), frame, i == c.focusScreen) | |
| 633 | 633 | } | |
| 634 | 634 | // A window alone on screen needs no pulse to show it has the focus. | |
| 635 | - | alone := i == c.focusScreen && c.pulse.target != 0 && visibleCount(layout, frame) == 1 | |
| 635 | + | // A tile under a panel (the next cascade band) does not count. | |
| 636 | + | alone := i == c.focusScreen && c.pulse.target != 0 && visibleCount(layout, sc.mon.Current().Usable) == 1 | |
| 636 | 637 | for k, p := range layout { | |
| 637 | 638 | ps := settled[k] | |
| 638 | 639 | // Only the focused output has an activated window. | |
| @@ -632,7 +632,8 @@ func (c *Core) publishFrame(ctx context.Context, only *screen) error { | |||||
| 632 | scene.Separators = separators(layout, c.cfg.Border.Width, sc.mon.Current().gap(), frame, i == c.focusScreen) | 632 | scene.Separators = separators(layout, c.cfg.Border.Width, sc.mon.Current().gap(), frame, i == c.focusScreen) | ||
| 633 | } | 633 | } | ||
| 634 | // A window alone on screen needs no pulse to show it has the focus. | 634 | // A window alone on screen needs no pulse to show it has the focus. | ||
| 635 | - | alone := i == c.focusScreen && c.pulse.target != 0 && visibleCount(layout, frame) == 1 | 635 | + | // A tile under a panel (the next cascade band) does not count. |
| 636 | + | alone := i == c.focusScreen && c.pulse.target != 0 && visibleCount(layout, sc.mon.Current().Usable) == 1 | |||
| 636 | for k, p := range layout { | 637 | for k, p := range layout { | ||
| 637 | ps := settled[k] | 638 | ps := settled[k] | ||
| 638 | // Only the focused output has an activated window. | 639 | // Only the focused output has an activated window. | ||
M internal/core/layout.go +10 -10
| @@ -122,7 +122,7 @@ type Placement struct { | |||
| 122 | 122 | // Preview is the scale of an overview preview: its buffer is drawn | |
| 123 | 123 | // that much smaller in Rect, the client keeps its size. 0 otherwise. | |
| 124 | 124 | Preview float64 | |
| 125 | - | // Neighbors are the sides touching a visible tile on this output. | |
| 125 | + | // Neighbors are the sides touching a tile visible in the usable area. | |
| 126 | 126 | // Inset reserves room for drawn lines: all sides of a float; for | |
| 127 | 127 | // tiles without gaps, only the right and bottom shared sides. | |
| 128 | 128 | Neighbors, Inset ports.Sides | |
| @@ -1715,11 +1715,11 @@ func (w *Workspace) appendLayout(dst []Placement) []Placement { | |||
| 1715 | 1715 | } | |
| 1716 | 1716 | ||
| 1717 | 1717 | // setVisibleNeighbors reserves client space only for shared lines that | |
| 1718 | - | // are actually on this output. A scrolled-off or hidden tile cannot give a | |
| 1719 | - | // lone visible tile a border (or shrink its client). | |
| 1720 | - | func setVisibleNeighbors(tiles []Placement, gap int, output Rect) { | |
| 1718 | + | // are actually in the usable area. A scrolled-off, hidden or under-panel | |
| 1719 | + | // tile cannot give a lone visible tile a border (or shrink its client). | |
| 1720 | + | func setVisibleNeighbors(tiles []Placement, gap int, area Rect) { | |
| 1721 | 1721 | visible := func(p Placement) bool { | |
| 1722 | - | return !p.Hidden && !p.Fullscreen && p.Rect.Overlaps(output) | |
| 1722 | + | return !p.Hidden && !p.Fullscreen && p.Rect.Overlaps(area) | |
| 1723 | 1723 | } | |
| 1724 | 1724 | overlap := func(a0, a1, b0, b1 int) bool { return a0 < b1 && b0 < a1 } | |
| 1725 | 1725 | for i := range tiles { | |
| @@ -1733,15 +1733,15 @@ func setVisibleNeighbors(tiles []Placement, gap int, output Rect) { | |||
| 1733 | 1733 | continue | |
| 1734 | 1734 | } | |
| 1735 | 1735 | b, r := tiles[j].Rect, a.Rect | |
| 1736 | - | // Only count a shared edge strictly within this output. | |
| 1736 | + | // Only count a shared edge strictly within the area. | |
| 1737 | 1737 | switch { | |
| 1738 | - | case r.X+r.W+gap == b.X && overlap(r.Y, r.Y+r.H, b.Y, b.Y+b.H) && r.X+r.W > output.X && r.X+r.W < output.X+output.W: | |
| 1738 | + | case r.X+r.W+gap == b.X && overlap(r.Y, r.Y+r.H, b.Y, b.Y+b.H) && r.X+r.W > area.X && r.X+r.W < area.X+area.W: | |
| 1739 | 1739 | a.Neighbors |= ports.SideRight | |
| 1740 | - | case b.X+b.W+gap == r.X && overlap(r.Y, r.Y+r.H, b.Y, b.Y+b.H) && r.X > output.X && r.X < output.X+output.W: | |
| 1740 | + | case b.X+b.W+gap == r.X && overlap(r.Y, r.Y+r.H, b.Y, b.Y+b.H) && r.X > area.X && r.X < area.X+area.W: | |
| 1741 | 1741 | a.Neighbors |= ports.SideLeft | |
| 1742 | - | case r.Y+r.H+gap == b.Y && overlap(r.X, r.X+r.W, b.X, b.X+b.W) && r.Y+r.H > output.Y && r.Y+r.H < output.Y+output.H: | |
| 1742 | + | case r.Y+r.H+gap == b.Y && overlap(r.X, r.X+r.W, b.X, b.X+b.W) && r.Y+r.H > area.Y && r.Y+r.H < area.Y+area.H: | |
| 1743 | 1743 | a.Neighbors |= ports.SideBottom | |
| 1744 | - | case b.Y+b.H+gap == r.Y && overlap(r.X, r.X+r.W, b.X, b.X+b.W) && r.Y > output.Y && r.Y < output.Y+output.H: | |
| 1744 | + | case b.Y+b.H+gap == r.Y && overlap(r.X, r.X+r.W, b.X, b.X+b.W) && r.Y > area.Y && r.Y < area.Y+area.H: | |
| 1745 | 1745 | a.Neighbors |= ports.SideTop | |
| 1746 | 1746 | } | |
| 1747 | 1747 | } | |
| @@ -122,7 +122,7 @@ type Placement struct { | |||||
| 122 | // Preview is the scale of an overview preview: its buffer is drawn | 122 | // Preview is the scale of an overview preview: its buffer is drawn | ||
| 123 | // that much smaller in Rect, the client keeps its size. 0 otherwise. | 123 | // that much smaller in Rect, the client keeps its size. 0 otherwise. | ||
| 124 | Preview float64 | 124 | Preview float64 | ||
| 125 | - | // Neighbors are the sides touching a visible tile on this output. | 125 | + | // Neighbors are the sides touching a tile visible in the usable area. |
| 126 | // Inset reserves room for drawn lines: all sides of a float; for | 126 | // Inset reserves room for drawn lines: all sides of a float; for | ||
| 127 | // tiles without gaps, only the right and bottom shared sides. | 127 | // tiles without gaps, only the right and bottom shared sides. | ||
| 128 | Neighbors, Inset ports.Sides | 128 | Neighbors, Inset ports.Sides | ||
| @@ -1715,11 +1715,11 @@ func (w *Workspace) appendLayout(dst []Placement) []Placement { | |||||
| 1715 | } | 1715 | } | ||
| 1716 | 1716 | ||||
| 1717 | // setVisibleNeighbors reserves client space only for shared lines that | 1717 | // setVisibleNeighbors reserves client space only for shared lines that | ||
| 1718 | - | // are actually on this output. A scrolled-off or hidden tile cannot give a | 1718 | + | // are actually in the usable area. A scrolled-off, hidden or under-panel |
| 1719 | - | // lone visible tile a border (or shrink its client). | 1719 | + | // tile cannot give a lone visible tile a border (or shrink its client). |
| 1720 | - | func setVisibleNeighbors(tiles []Placement, gap int, output Rect) { | 1720 | + | func setVisibleNeighbors(tiles []Placement, gap int, area Rect) { |
| 1721 | visible := func(p Placement) bool { | 1721 | visible := func(p Placement) bool { | ||
| 1722 | - | return !p.Hidden && !p.Fullscreen && p.Rect.Overlaps(output) | 1722 | + | return !p.Hidden && !p.Fullscreen && p.Rect.Overlaps(area) |
| 1723 | } | 1723 | } | ||
| 1724 | overlap := func(a0, a1, b0, b1 int) bool { return a0 < b1 && b0 < a1 } | 1724 | overlap := func(a0, a1, b0, b1 int) bool { return a0 < b1 && b0 < a1 } | ||
| 1725 | for i := range tiles { | 1725 | for i := range tiles { | ||
| @@ -1733,15 +1733,15 @@ func setVisibleNeighbors(tiles []Placement, gap int, output Rect) { | |||||
| 1733 | continue | 1733 | continue | ||
| 1734 | } | 1734 | } | ||
| 1735 | b, r := tiles[j].Rect, a.Rect | 1735 | b, r := tiles[j].Rect, a.Rect | ||
| 1736 | - | // Only count a shared edge strictly within this output. | 1736 | + | // Only count a shared edge strictly within the area. |
| 1737 | switch { | 1737 | switch { | ||
| 1738 | - | case r.X+r.W+gap == b.X && overlap(r.Y, r.Y+r.H, b.Y, b.Y+b.H) && r.X+r.W > output.X && r.X+r.W < output.X+output.W: | 1738 | + | case r.X+r.W+gap == b.X && overlap(r.Y, r.Y+r.H, b.Y, b.Y+b.H) && r.X+r.W > area.X && r.X+r.W < area.X+area.W: |
| 1739 | a.Neighbors |= ports.SideRight | 1739 | a.Neighbors |= ports.SideRight | ||
| 1740 | - | case b.X+b.W+gap == r.X && overlap(r.Y, r.Y+r.H, b.Y, b.Y+b.H) && r.X > output.X && r.X < output.X+output.W: | 1740 | + | case b.X+b.W+gap == r.X && overlap(r.Y, r.Y+r.H, b.Y, b.Y+b.H) && r.X > area.X && r.X < area.X+area.W: |
| 1741 | a.Neighbors |= ports.SideLeft | 1741 | a.Neighbors |= ports.SideLeft | ||
| 1742 | - | case r.Y+r.H+gap == b.Y && overlap(r.X, r.X+r.W, b.X, b.X+b.W) && r.Y+r.H > output.Y && r.Y+r.H < output.Y+output.H: | 1742 | + | case r.Y+r.H+gap == b.Y && overlap(r.X, r.X+r.W, b.X, b.X+b.W) && r.Y+r.H > area.Y && r.Y+r.H < area.Y+area.H: |
| 1743 | a.Neighbors |= ports.SideBottom | 1743 | a.Neighbors |= ports.SideBottom | ||
| 1744 | - | case b.Y+b.H+gap == r.Y && overlap(r.X, r.X+r.W, b.X, b.X+b.W) && r.Y > output.Y && r.Y < output.Y+output.H: | 1744 | + | case b.Y+b.H+gap == r.Y && overlap(r.X, r.X+r.W, b.X, b.X+b.W) && r.Y > area.Y && r.Y < area.Y+area.H: |
| 1745 | a.Neighbors |= ports.SideTop | 1745 | a.Neighbors |= ports.SideTop | ||
| 1746 | } | 1746 | } | ||
| 1747 | } | 1747 | } | ||
M internal/core/separators.go +3 -1
| @@ -54,7 +54,9 @@ func separators(ps []Placement, width, gap int, o Rect, lit bool) []ports.Separa | |||
| 54 | 54 | if p.Fullscreen || !(onScreen(*p, o) || p.Leaving && p.Floating && p.Rect.Overlaps(o)) { | |
| 55 | 55 | continue | |
| 56 | 56 | } | |
| 57 | - | if !p.Floating { | |
| 57 | + | // Tiles sharing a line: Neighbors only counts tiles visible in | |
| 58 | + | // the usable area, not those under a panel. | |
| 59 | + | if !p.Floating && p.Neighbors != 0 { | |
| 58 | 60 | tiles++ | |
| 59 | 61 | } | |
| 60 | 62 | for _, s := range sides { | |
| @@ -54,7 +54,9 @@ func separators(ps []Placement, width, gap int, o Rect, lit bool) []ports.Separa | |||||
| 54 | if p.Fullscreen || !(onScreen(*p, o) || p.Leaving && p.Floating && p.Rect.Overlaps(o)) { | 54 | if p.Fullscreen || !(onScreen(*p, o) || p.Leaving && p.Floating && p.Rect.Overlaps(o)) { | ||
| 55 | continue | 55 | continue | ||
| 56 | } | 56 | } | ||
| 57 | - | if !p.Floating { | 57 | + | // Tiles sharing a line: Neighbors only counts tiles visible in |
| 58 | + | // the usable area, not those under a panel. | |||
| 59 | + | if !p.Floating && p.Neighbors != 0 { | |||
| 58 | tiles++ | 60 | tiles++ | ||
| 59 | } | 61 | } | ||
| 60 | for _, s := range sides { | 62 | for _, s := range sides { | ||
M internal/core/separators_test.go +7 -0
| @@ -166,9 +166,16 @@ func TestSeparatorsCascadeBottomPanel(t *testing.T) { | |||
| 166 | 166 | if got := colorAt(seps, 200, 578); got != "" { | |
| 167 | 167 | t.Fatalf("line along the panel: %v", seps) | |
| 168 | 168 | } | |
| 169 | + | // Two tiles on screen: each lights its half of the shared line. | |
| 169 | 170 | if got := colorAt(seps, 449, 100); got != "lit" { | |
| 170 | 171 | t.Fatalf("shared line (449,100) = %q; %v", got, seps) | |
| 171 | 172 | } | |
| 173 | + | if got := colorAt(seps, 449, 400); got != "gray" { | |
| 174 | + | t.Fatalf("shared line (449,400) = %q; %v", got, seps) | |
| 175 | + | } | |
| 176 | + | if n := visibleCount(ps, w.Usable); n != 2 { | |
| 177 | + | t.Fatalf("visible in the usable area: %d, want 2", n) | |
| 178 | + | } | |
| 172 | 179 | } | |
| 173 | 180 | ||
| 174 | 181 | // With gaps each tile has its own line: the focused one lights it whole. | |
| @@ -166,9 +166,16 @@ func TestSeparatorsCascadeBottomPanel(t *testing.T) { | |||||
| 166 | if got := colorAt(seps, 200, 578); got != "" { | 166 | if got := colorAt(seps, 200, 578); got != "" { | ||
| 167 | t.Fatalf("line along the panel: %v", seps) | 167 | t.Fatalf("line along the panel: %v", seps) | ||
| 168 | } | 168 | } | ||
| 169 | + | // Two tiles on screen: each lights its half of the shared line. | |||
| 169 | if got := colorAt(seps, 449, 100); got != "lit" { | 170 | if got := colorAt(seps, 449, 100); got != "lit" { | ||
| 170 | t.Fatalf("shared line (449,100) = %q; %v", got, seps) | 171 | t.Fatalf("shared line (449,100) = %q; %v", got, seps) | ||
| 171 | } | 172 | } | ||
| 173 | + | if got := colorAt(seps, 449, 400); got != "gray" { | |||
| 174 | + | t.Fatalf("shared line (449,400) = %q; %v", got, seps) | |||
| 175 | + | } | |||
| 176 | + | if n := visibleCount(ps, w.Usable); n != 2 { | |||
| 177 | + | t.Fatalf("visible in the usable area: %d, want 2", n) | |||
| 178 | + | } | |||
| 172 | } | 179 | } | ||
| 173 | 180 | ||||
| 174 | // With gaps each tile has its own line: the focused one lights it whole. | 181 | // With gaps each tile has its own line: the focused one lights it whole. | ||