detached f7c2be9

refactor(core): decide window visibility from one viewport rule

A tile is seen only where it overlaps the usable area; floats,
fullscreen windows and overview previews keep the whole frame. The
next cascade band under a bottom panel, or a column scrolled under a
side panel, is now configured invisible, so its client is throttled
and stops rendering.

viewport{frame, usable} with shows(p) replaces onScreen and feeds
configures, the state snapshot, popups, float dim, separators and the
pulse's alone check.
commit
f7c2be933510effa2b2481fe05d6ea08c00aa4d8
author
brice · · 2026-10-08 13:21 UTC
parent
7ab00e3
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
15 files · +209 -55
M internal/core/capture_workspace.go +5 -3
@@ -91,7 +91,7 @@ func (c *Core) captureConfigure(sc *screen, p Placement, t configureTarget) (*Pl
9191 // An overview preview keeps its real size and physical state.
9292 return cp, t
9393 }
94- t.area = cw.frame
94+ t.view = viewport{frame: cw.frame, usable: cw.ws.Usable}
9595 t.focused = false
9696 t.client, t.imposed = Rect{}, false
9797 if !cp.Hidden {
@@ -146,6 +146,8 @@ func (c *Core) captureScene(seq uint64) *ports.Scene {
146146 return nil
147147 }
148148 f := cw.frame
149+ // The capture image has no panels: all of it is seen.
150+ view := whole(f)
149151 rebase := func(r Rect) Rect { r.X, r.Y = r.X-f.X, r.Y-f.Y; return r }
150152 s := &ports.Scene{
151153 Security: c.security,
@@ -153,7 +155,7 @@ func (c *Core) captureScene(seq uint64) *ports.Scene {
153155 Scale: cw.sc.scale, Background: c.cfg.Background.Color,
154156 Border: ports.Border{Width: c.cfg.Border.Width, Active: c.cfg.Border.Active, Inactive: c.cfg.Border.Inactive},
155157 Windows: make([]ports.SceneWindow, 0, len(cw.placements)),
156- Dim: floatDim(cw.placements, f, c.cfg.Floating.Dim),
158+ Dim: floatDim(cw.placements, view, c.cfg.Floating.Dim),
157159 }
158160 // A hidden workspace lights nothing: its focus is not shown.
159161 for _, p := range cw.placements {
@@ -164,7 +166,7 @@ func (c *Core) captureScene(seq uint64) *ports.Scene {
164166 w.Dim = c.peekDim(p)
165167 s.Windows = append(s.Windows, w)
166168 }
167- s.Separators = separators(cw.placements, c.cfg.Border.Width, cw.ws.gap(), f, false)
169+ s.Separators = separators(cw.placements, c.cfg.Border.Width, cw.ws.gap(), view, false)
168170 for i := range s.Separators {
169171 s.Separators[i].Rect = rebase(s.Separators[i].Rect)
170172 }
@@ -91,7 +91,7 @@ func (c *Core) captureConfigure(sc *screen, p Placement, t configureTarget) (*Pl
91 // An overview preview keeps its real size and physical state.91 // An overview preview keeps its real size and physical state.
92 return cp, t92 return cp, t
93 }93 }
94- t.area = cw.frame94+ t.view = viewport{frame: cw.frame, usable: cw.ws.Usable}
95 t.focused = false95 t.focused = false
96 t.client, t.imposed = Rect{}, false96 t.client, t.imposed = Rect{}, false
97 if !cp.Hidden {97 if !cp.Hidden {
@@ -146,6 +146,8 @@ func (c *Core) captureScene(seq uint64) *ports.Scene {
146 return nil146 return nil
147 }147 }
148 f := cw.frame148 f := cw.frame
149+ // The capture image has no panels: all of it is seen.
150+ view := whole(f)
149 rebase := func(r Rect) Rect { r.X, r.Y = r.X-f.X, r.Y-f.Y; return r }151 rebase := func(r Rect) Rect { r.X, r.Y = r.X-f.X, r.Y-f.Y; return r }
150 s := &ports.Scene{152 s := &ports.Scene{
151 Security: c.security,153 Security: c.security,
@@ -153,7 +155,7 @@ func (c *Core) captureScene(seq uint64) *ports.Scene {
153 Scale: cw.sc.scale, Background: c.cfg.Background.Color,155 Scale: cw.sc.scale, Background: c.cfg.Background.Color,
154 Border: ports.Border{Width: c.cfg.Border.Width, Active: c.cfg.Border.Active, Inactive: c.cfg.Border.Inactive},156 Border: ports.Border{Width: c.cfg.Border.Width, Active: c.cfg.Border.Active, Inactive: c.cfg.Border.Inactive},
155 Windows: make([]ports.SceneWindow, 0, len(cw.placements)),157 Windows: make([]ports.SceneWindow, 0, len(cw.placements)),
156- Dim: floatDim(cw.placements, f, c.cfg.Floating.Dim),158+ Dim: floatDim(cw.placements, view, c.cfg.Floating.Dim),
157 }159 }
158 // A hidden workspace lights nothing: its focus is not shown.160 // A hidden workspace lights nothing: its focus is not shown.
159 for _, p := range cw.placements {161 for _, p := range cw.placements {
@@ -164,7 +166,7 @@ func (c *Core) captureScene(seq uint64) *ports.Scene {
164 w.Dim = c.peekDim(p)166 w.Dim = c.peekDim(p)
165 s.Windows = append(s.Windows, w)167 s.Windows = append(s.Windows, w)
166 }168 }
167- s.Separators = separators(cw.placements, c.cfg.Border.Width, cw.ws.gap(), f, false)169+ s.Separators = separators(cw.placements, c.cfg.Border.Width, cw.ws.gap(), view, false)
168 for i := range s.Separators {170 for i := range s.Separators {
169 s.Separators[i].Rect = rebase(s.Separators[i].Rect)171 s.Separators[i].Rect = rebase(s.Separators[i].Rect)
170 }172 }
M internal/core/capture_workspace_test.go +2 -2
@@ -47,7 +47,7 @@ func TestCaptureConfigureSizesHiddenWindowWithoutShowingIt(t *testing.T) {
4747 c, _ := hiddenCapture(t)
4848 sc := c.screens[0]
4949 phys := Placement{ID: 1, Hidden: true}
50- t0 := configureTarget{output: "A", area: sc.mon.Frame()}
50+ t0 := configureTarget{output: "A", view: sc.mon.viewport()}
5151 cp, tt := c.captureConfigure(sc, phys, t0)
5252 if cp == nil || cp.Hidden {
5353 t.Fatalf("no real placement: %+v", cp)
@@ -80,7 +80,7 @@ func TestCaptureConfigureLeavesOtherWindowsAlone(t *testing.T) {
8080 c, _ := hiddenCapture(t)
8181 sc := c.screens[0]
8282 phys := Placement{ID: 99, Hidden: true}
83- t0 := configureTarget{output: "A", area: sc.mon.Frame()}
83+ t0 := configureTarget{output: "A", view: sc.mon.viewport()}
8484 if cp, tt := c.captureConfigure(sc, phys, t0); cp != nil || tt != t0 {
8585 t.Fatalf("window of another workspace captured: %+v", cp)
8686 }
@@ -47,7 +47,7 @@ func TestCaptureConfigureSizesHiddenWindowWithoutShowingIt(t *testing.T) {
47 c, _ := hiddenCapture(t)47 c, _ := hiddenCapture(t)
48 sc := c.screens[0]48 sc := c.screens[0]
49 phys := Placement{ID: 1, Hidden: true}49 phys := Placement{ID: 1, Hidden: true}
50- t0 := configureTarget{output: "A", area: sc.mon.Frame()}50+ t0 := configureTarget{output: "A", view: sc.mon.viewport()}
51 cp, tt := c.captureConfigure(sc, phys, t0)51 cp, tt := c.captureConfigure(sc, phys, t0)
52 if cp == nil || cp.Hidden {52 if cp == nil || cp.Hidden {
53 t.Fatalf("no real placement: %+v", cp)53 t.Fatalf("no real placement: %+v", cp)
@@ -80,7 +80,7 @@ func TestCaptureConfigureLeavesOtherWindowsAlone(t *testing.T) {
80 c, _ := hiddenCapture(t)80 c, _ := hiddenCapture(t)
81 sc := c.screens[0]81 sc := c.screens[0]
82 phys := Placement{ID: 99, Hidden: true}82 phys := Placement{ID: 99, Hidden: true}
83- t0 := configureTarget{output: "A", area: sc.mon.Frame()}83+ t0 := configureTarget{output: "A", view: sc.mon.viewport()}
84 if cp, tt := c.captureConfigure(sc, phys, t0); cp != nil || tt != t0 {84 if cp, tt := c.captureConfigure(sc, phys, t0); cp != nil || tt != t0 {
85 t.Fatalf("window of another workspace captured: %+v", cp)85 t.Fatalf("window of another workspace captured: %+v", cp)
86 }86 }
M internal/core/configures.go +5 -5
@@ -5,8 +5,8 @@ import "github.com/bnema/neferwl/internal/ports"
55 // configureTarget is where a placement is shown, as the configure needs it.
66 type configureTarget struct {
77 output string
8- area Rect // the output, to tell whether the window is on screen
9- focused bool // activated: focused on the focused output
8+ view viewport // where the window is seen, to tell whether it is
9+ focused bool // activated: focused on the focused output
1010 // client is the placement minus borders; imposed reports core sized a
1111 // floating window (native floats pick their own size).
1212 client Rect
@@ -109,14 +109,14 @@ func build(p Placement, t configureTarget, old ports.ConfigureWindow, ok bool) p
109109 if p.Fullscreen && !v.Fullscreen {
110110 // A float made fullscreen off screen: its preview row shows
111111 // it fullscreen, sized for the output, never the preview.
112- v.Width, v.Height, v.Fullscreen, v.Floating = t.area.W, t.area.H, true, false
112+ v.Width, v.Height, v.Fullscreen, v.Floating = t.view.frame.W, t.view.frame.H, true, false
113113 }
114114 if t.realTiled {
115115 v.Width, v.Height = t.client.W, t.client.H
116116 }
117- v.Activated, v.Visible, v.Output, v.Captured = t.focused, onScreen(p, t.area), t.output, false
117+ v.Activated, v.Visible, v.Output, v.Captured = t.focused, t.view.shows(p), t.output, false
118118 default:
119- v = ports.ConfigureWindow{ID: p.ID, Width: t.client.W, Height: t.client.H, Fullscreen: p.Fullscreen, Activated: t.focused, Floating: p.Floating && !p.Fullscreen, Output: t.output, Visible: onScreen(p, t.area)}
119+ v = ports.ConfigureWindow{ID: p.ID, Width: t.client.W, Height: t.client.H, Fullscreen: p.Fullscreen, Activated: t.focused, Floating: p.Floating && !p.Fullscreen, Output: t.output, Visible: t.view.shows(p)}
120120 if v.Floating && !t.imposed {
121121 v.Width, v.Height = 0, 0
122122 }
@@ -5,8 +5,8 @@ import "github.com/bnema/neferwl/internal/ports"
5 // configureTarget is where a placement is shown, as the configure needs it.5 // configureTarget is where a placement is shown, as the configure needs it.
6 type configureTarget struct {6 type configureTarget struct {
7 output string7 output string
8- area Rect // the output, to tell whether the window is on screen8+ view viewport // where the window is seen, to tell whether it is
9- focused bool // activated: focused on the focused output9+ focused bool // activated: focused on the focused output
10 // client is the placement minus borders; imposed reports core sized a10 // client is the placement minus borders; imposed reports core sized a
11 // floating window (native floats pick their own size).11 // floating window (native floats pick their own size).
12 client Rect12 client Rect
@@ -109,14 +109,14 @@ func build(p Placement, t configureTarget, old ports.ConfigureWindow, ok bool) p
109 if p.Fullscreen && !v.Fullscreen {109 if p.Fullscreen && !v.Fullscreen {
110 // A float made fullscreen off screen: its preview row shows110 // A float made fullscreen off screen: its preview row shows
111 // it fullscreen, sized for the output, never the preview.111 // it fullscreen, sized for the output, never the preview.
112- v.Width, v.Height, v.Fullscreen, v.Floating = t.area.W, t.area.H, true, false112+ v.Width, v.Height, v.Fullscreen, v.Floating = t.view.frame.W, t.view.frame.H, true, false
113 }113 }
114 if t.realTiled {114 if t.realTiled {
115 v.Width, v.Height = t.client.W, t.client.H115 v.Width, v.Height = t.client.W, t.client.H
116 }116 }
117- v.Activated, v.Visible, v.Output, v.Captured = t.focused, onScreen(p, t.area), t.output, false117+ v.Activated, v.Visible, v.Output, v.Captured = t.focused, t.view.shows(p), t.output, false
118 default:118 default:
119- v = ports.ConfigureWindow{ID: p.ID, Width: t.client.W, Height: t.client.H, Fullscreen: p.Fullscreen, Activated: t.focused, Floating: p.Floating && !p.Fullscreen, Output: t.output, Visible: onScreen(p, t.area)}119+ v = ports.ConfigureWindow{ID: p.ID, Width: t.client.W, Height: t.client.H, Fullscreen: p.Fullscreen, Activated: t.focused, Floating: p.Floating && !p.Fullscreen, Output: t.output, Visible: t.view.shows(p)}
120 if v.Floating && !t.imposed {120 if v.Floating && !t.imposed {
121 v.Width, v.Height = 0, 0121 v.Width, v.Height = 0, 0
122 }122 }
M internal/core/configures_test.go +6 -6
@@ -9,7 +9,7 @@ import (
99 func TestConfiguresNext(t *testing.T) {
1010 area := Rect{W: 1000, H: 800}
1111 tiled := Placement{ID: 1, Rect: Rect{W: 500, H: 800}}
12- target := configureTarget{output: "A", area: area, focused: true, client: Rect{W: 496, H: 796}}
12+ target := configureTarget{output: "A", view: whole(area), focused: true, client: Rect{W: 496, H: 796}}
1313 tests := []struct {
1414 name string
1515 first *Placement // sent before p, with target
@@ -22,14 +22,14 @@ func TestConfiguresNext(t *testing.T) {
2222 {name: "unchanged", first: &tiled, p: tiled, t: target, want: ports.ConfigureWindow{ID: 1, Width: 496, Height: 796, Activated: true, Output: "A", Visible: true}},
2323 {name: "scrolled off", p: Placement{ID: 1, Rect: Rect{X: 1000, W: 500, H: 800}}, t: target, want: ports.ConfigureWindow{ID: 1, Width: 496, Height: 796, Activated: true, Output: "A"}, send: true},
2424 {name: "native float", p: Placement{ID: 1, Rect: Rect{W: 300, H: 200}, Floating: true}, t: target, want: ports.ConfigureWindow{ID: 1, Activated: true, Floating: true, Output: "A", Visible: true}, send: true},
25- {name: "imposed float", p: Placement{ID: 1, Rect: Rect{W: 300, H: 200}, Floating: true}, t: configureTarget{output: "A", area: area, client: Rect{W: 300, H: 200}, imposed: true}, want: ports.ConfigureWindow{ID: 1, Width: 300, Height: 200, Floating: true, Output: "A", Visible: true}, send: true},
26- {name: "fullscreen float", p: Placement{ID: 1, Rect: area, Floating: true, Fullscreen: true}, t: configureTarget{output: "A", area: area, client: area}, want: ports.ConfigureWindow{ID: 1, Width: 1000, Height: 800, Fullscreen: true, Output: "A", Visible: true}, send: true},
25+ {name: "imposed float", p: Placement{ID: 1, Rect: Rect{W: 300, H: 200}, Floating: true}, t: configureTarget{output: "A", view: whole(area), client: Rect{W: 300, H: 200}, imposed: true}, want: ports.ConfigureWindow{ID: 1, Width: 300, Height: 200, Floating: true, Output: "A", Visible: true}, send: true},
26+ {name: "fullscreen float", p: Placement{ID: 1, Rect: area, Floating: true, Fullscreen: true}, t: configureTarget{output: "A", view: whole(area), client: area}, want: ports.ConfigureWindow{ID: 1, Width: 1000, Height: 800, Fullscreen: true, Output: "A", Visible: true}, send: true},
2727 {name: "hidden new", p: Placement{ID: 1, Hidden: true, Floating: true}, t: target, want: ports.ConfigureWindow{ID: 1, Floating: true, Output: "A"}, send: true},
2828 {name: "hidden keeps size", first: &tiled, p: Placement{ID: 1, Hidden: true}, t: configureTarget{output: "B"}, want: ports.ConfigureWindow{ID: 1, Width: 496, Height: 796, Output: "B"}, send: true},
2929 {name: "preview new", p: Placement{ID: 1, Rect: Rect{W: 250, H: 200}, Preview: 0.5}, t: target, want: ports.ConfigureWindow{ID: 1, Width: 500, Height: 400, Activated: true, Output: "A", Visible: true}, send: true},
30- {name: "fullscreen preview keeps size and state", first: &Placement{ID: 1, Rect: area, Floating: true, Fullscreen: true}, p: Placement{ID: 1, Rect: Rect{W: 100, H: 80}, Preview: 0.1}, t: configureTarget{output: "A", area: area}, want: ports.ConfigureWindow{ID: 1, Width: 496, Height: 796, Fullscreen: true, Output: "A", Visible: true}, send: true},
31- {name: "hidden float made fullscreen previews fullscreen", first: &Placement{ID: 1, Hidden: true, Floating: true}, p: Placement{ID: 1, Rect: Rect{W: 100, H: 80}, Preview: 0.1, Fullscreen: true}, t: configureTarget{output: "A", area: area}, want: ports.ConfigureWindow{ID: 1, Width: 1000, Height: 800, Fullscreen: true, Output: "A", Visible: true}, send: true},
32- {name: "preview keeps size", first: &tiled, p: Placement{ID: 1, Rect: Rect{W: 100, H: 100}, Preview: 0.5}, t: configureTarget{output: "A", area: area}, want: ports.ConfigureWindow{ID: 1, Width: 496, Height: 796, Output: "A", Visible: true}, send: true},
30+ {name: "fullscreen preview keeps size and state", first: &Placement{ID: 1, Rect: area, Floating: true, Fullscreen: true}, p: Placement{ID: 1, Rect: Rect{W: 100, H: 80}, Preview: 0.1}, t: configureTarget{output: "A", view: whole(area)}, want: ports.ConfigureWindow{ID: 1, Width: 496, Height: 796, Fullscreen: true, Output: "A", Visible: true}, send: true},
31+ {name: "hidden float made fullscreen previews fullscreen", first: &Placement{ID: 1, Hidden: true, Floating: true}, p: Placement{ID: 1, Rect: Rect{W: 100, H: 80}, Preview: 0.1, Fullscreen: true}, t: configureTarget{output: "A", view: whole(area)}, want: ports.ConfigureWindow{ID: 1, Width: 1000, Height: 800, Fullscreen: true, Output: "A", Visible: true}, send: true},
32+ {name: "preview keeps size", first: &tiled, p: Placement{ID: 1, Rect: Rect{W: 100, H: 100}, Preview: 0.5}, t: configureTarget{output: "A", view: whole(area)}, want: ports.ConfigureWindow{ID: 1, Width: 496, Height: 796, Output: "A", Visible: true}, send: true},
3333 {name: "new float preview stays floating", p: Placement{ID: 1, Rect: Rect{W: 600, H: 480}, Preview: 0.6, Floating: true}, t: target, want: ports.ConfigureWindow{ID: 1, Floating: true, Activated: true, Output: "A", Visible: true}, send: true},
3434 {name: "covering float preview keeps native size", first: &Placement{ID: 1, Rect: area, Floating: true}, p: Placement{ID: 1, Rect: Rect{W: 600, H: 480}, Preview: 0.6, Floating: true}, t: target, want: ports.ConfigureWindow{ID: 1, Floating: true, Activated: true, Output: "A", Visible: true}},
3535 {name: "covering float peek keeps native size", first: &Placement{ID: 1, Rect: area, Floating: true}, p: Placement{ID: 1, Rect: Rect{X: 20, Y: 20, W: 600, H: 480}, Preview: 0.6, Floating: true, Peek: true}, t: target, want: ports.ConfigureWindow{ID: 1, Floating: true, Activated: true, Output: "A", Visible: true}},
@@ -9,7 +9,7 @@ import (
9 func TestConfiguresNext(t *testing.T) {9 func TestConfiguresNext(t *testing.T) {
10 area := Rect{W: 1000, H: 800}10 area := Rect{W: 1000, H: 800}
11 tiled := Placement{ID: 1, Rect: Rect{W: 500, H: 800}}11 tiled := Placement{ID: 1, Rect: Rect{W: 500, H: 800}}
12- target := configureTarget{output: "A", area: area, focused: true, client: Rect{W: 496, H: 796}}12+ target := configureTarget{output: "A", view: whole(area), focused: true, client: Rect{W: 496, H: 796}}
13 tests := []struct {13 tests := []struct {
14 name string14 name string
15 first *Placement // sent before p, with target15 first *Placement // sent before p, with target
@@ -22,14 +22,14 @@ func TestConfiguresNext(t *testing.T) {
22 {name: "unchanged", first: &tiled, p: tiled, t: target, want: ports.ConfigureWindow{ID: 1, Width: 496, Height: 796, Activated: true, Output: "A", Visible: true}},22 {name: "unchanged", first: &tiled, p: tiled, t: target, want: ports.ConfigureWindow{ID: 1, Width: 496, Height: 796, Activated: true, Output: "A", Visible: true}},
23 {name: "scrolled off", p: Placement{ID: 1, Rect: Rect{X: 1000, W: 500, H: 800}}, t: target, want: ports.ConfigureWindow{ID: 1, Width: 496, Height: 796, Activated: true, Output: "A"}, send: true},23 {name: "scrolled off", p: Placement{ID: 1, Rect: Rect{X: 1000, W: 500, H: 800}}, t: target, want: ports.ConfigureWindow{ID: 1, Width: 496, Height: 796, Activated: true, Output: "A"}, send: true},
24 {name: "native float", p: Placement{ID: 1, Rect: Rect{W: 300, H: 200}, Floating: true}, t: target, want: ports.ConfigureWindow{ID: 1, Activated: true, Floating: true, Output: "A", Visible: true}, send: true},24 {name: "native float", p: Placement{ID: 1, Rect: Rect{W: 300, H: 200}, Floating: true}, t: target, want: ports.ConfigureWindow{ID: 1, Activated: true, Floating: true, Output: "A", Visible: true}, send: true},
25- {name: "imposed float", p: Placement{ID: 1, Rect: Rect{W: 300, H: 200}, Floating: true}, t: configureTarget{output: "A", area: area, client: Rect{W: 300, H: 200}, imposed: true}, want: ports.ConfigureWindow{ID: 1, Width: 300, Height: 200, Floating: true, Output: "A", Visible: true}, send: true},25+ {name: "imposed float", p: Placement{ID: 1, Rect: Rect{W: 300, H: 200}, Floating: true}, t: configureTarget{output: "A", view: whole(area), client: Rect{W: 300, H: 200}, imposed: true}, want: ports.ConfigureWindow{ID: 1, Width: 300, Height: 200, Floating: true, Output: "A", Visible: true}, send: true},
26- {name: "fullscreen float", p: Placement{ID: 1, Rect: area, Floating: true, Fullscreen: true}, t: configureTarget{output: "A", area: area, client: area}, want: ports.ConfigureWindow{ID: 1, Width: 1000, Height: 800, Fullscreen: true, Output: "A", Visible: true}, send: true},26+ {name: "fullscreen float", p: Placement{ID: 1, Rect: area, Floating: true, Fullscreen: true}, t: configureTarget{output: "A", view: whole(area), client: area}, want: ports.ConfigureWindow{ID: 1, Width: 1000, Height: 800, Fullscreen: true, Output: "A", Visible: true}, send: true},
27 {name: "hidden new", p: Placement{ID: 1, Hidden: true, Floating: true}, t: target, want: ports.ConfigureWindow{ID: 1, Floating: true, Output: "A"}, send: true},27 {name: "hidden new", p: Placement{ID: 1, Hidden: true, Floating: true}, t: target, want: ports.ConfigureWindow{ID: 1, Floating: true, Output: "A"}, send: true},
28 {name: "hidden keeps size", first: &tiled, p: Placement{ID: 1, Hidden: true}, t: configureTarget{output: "B"}, want: ports.ConfigureWindow{ID: 1, Width: 496, Height: 796, Output: "B"}, send: true},28 {name: "hidden keeps size", first: &tiled, p: Placement{ID: 1, Hidden: true}, t: configureTarget{output: "B"}, want: ports.ConfigureWindow{ID: 1, Width: 496, Height: 796, Output: "B"}, send: true},
29 {name: "preview new", p: Placement{ID: 1, Rect: Rect{W: 250, H: 200}, Preview: 0.5}, t: target, want: ports.ConfigureWindow{ID: 1, Width: 500, Height: 400, Activated: true, Output: "A", Visible: true}, send: true},29 {name: "preview new", p: Placement{ID: 1, Rect: Rect{W: 250, H: 200}, Preview: 0.5}, t: target, want: ports.ConfigureWindow{ID: 1, Width: 500, Height: 400, Activated: true, Output: "A", Visible: true}, send: true},
30- {name: "fullscreen preview keeps size and state", first: &Placement{ID: 1, Rect: area, Floating: true, Fullscreen: true}, p: Placement{ID: 1, Rect: Rect{W: 100, H: 80}, Preview: 0.1}, t: configureTarget{output: "A", area: area}, want: ports.ConfigureWindow{ID: 1, Width: 496, Height: 796, Fullscreen: true, Output: "A", Visible: true}, send: true},30+ {name: "fullscreen preview keeps size and state", first: &Placement{ID: 1, Rect: area, Floating: true, Fullscreen: true}, p: Placement{ID: 1, Rect: Rect{W: 100, H: 80}, Preview: 0.1}, t: configureTarget{output: "A", view: whole(area)}, want: ports.ConfigureWindow{ID: 1, Width: 496, Height: 796, Fullscreen: true, Output: "A", Visible: true}, send: true},
31- {name: "hidden float made fullscreen previews fullscreen", first: &Placement{ID: 1, Hidden: true, Floating: true}, p: Placement{ID: 1, Rect: Rect{W: 100, H: 80}, Preview: 0.1, Fullscreen: true}, t: configureTarget{output: "A", area: area}, want: ports.ConfigureWindow{ID: 1, Width: 1000, Height: 800, Fullscreen: true, Output: "A", Visible: true}, send: true},31+ {name: "hidden float made fullscreen previews fullscreen", first: &Placement{ID: 1, Hidden: true, Floating: true}, p: Placement{ID: 1, Rect: Rect{W: 100, H: 80}, Preview: 0.1, Fullscreen: true}, t: configureTarget{output: "A", view: whole(area)}, want: ports.ConfigureWindow{ID: 1, Width: 1000, Height: 800, Fullscreen: true, Output: "A", Visible: true}, send: true},
32- {name: "preview keeps size", first: &tiled, p: Placement{ID: 1, Rect: Rect{W: 100, H: 100}, Preview: 0.5}, t: configureTarget{output: "A", area: area}, want: ports.ConfigureWindow{ID: 1, Width: 496, Height: 796, Output: "A", Visible: true}, send: true},32+ {name: "preview keeps size", first: &tiled, p: Placement{ID: 1, Rect: Rect{W: 100, H: 100}, Preview: 0.5}, t: configureTarget{output: "A", view: whole(area)}, want: ports.ConfigureWindow{ID: 1, Width: 496, Height: 796, Output: "A", Visible: true}, send: true},
33 {name: "new float preview stays floating", p: Placement{ID: 1, Rect: Rect{W: 600, H: 480}, Preview: 0.6, Floating: true}, t: target, want: ports.ConfigureWindow{ID: 1, Floating: true, Activated: true, Output: "A", Visible: true}, send: true},33 {name: "new float preview stays floating", p: Placement{ID: 1, Rect: Rect{W: 600, H: 480}, Preview: 0.6, Floating: true}, t: target, want: ports.ConfigureWindow{ID: 1, Floating: true, Activated: true, Output: "A", Visible: true}, send: true},
34 {name: "covering float preview keeps native size", first: &Placement{ID: 1, Rect: area, Floating: true}, p: Placement{ID: 1, Rect: Rect{W: 600, H: 480}, Preview: 0.6, Floating: true}, t: target, want: ports.ConfigureWindow{ID: 1, Floating: true, Activated: true, Output: "A", Visible: true}},34 {name: "covering float preview keeps native size", first: &Placement{ID: 1, Rect: area, Floating: true}, p: Placement{ID: 1, Rect: Rect{W: 600, H: 480}, Preview: 0.6, Floating: true}, t: target, want: ports.ConfigureWindow{ID: 1, Floating: true, Activated: true, Output: "A", Visible: true}},
35 {name: "covering float peek keeps native size", first: &Placement{ID: 1, Rect: area, Floating: true}, p: Placement{ID: 1, Rect: Rect{X: 20, Y: 20, W: 600, H: 480}, Preview: 0.6, Floating: true, Peek: true}, t: target, want: ports.ConfigureWindow{ID: 1, Floating: true, Activated: true, Output: "A", Visible: true}},35 {name: "covering float peek keeps native size", first: &Placement{ID: 1, Rect: area, Floating: true}, p: Placement{ID: 1, Rect: Rect{X: 20, Y: 20, W: 600, H: 480}, Preview: 0.6, Floating: true, Peek: true}, t: target, want: ports.ConfigureWindow{ID: 1, Floating: true, Activated: true, Output: "A", Visible: true}},
M internal/core/core.go +21 -6
@@ -458,10 +458,25 @@ func (c *Core) clientRect(p Placement) Rect {
458458 return p.Rect.Inset(p.Inset, c.cfg.Border.Width)
459459 }
460460
461-// onScreen reports whether a placement is drawn on its output o: not
462-// hidden and not scrolled off.
463-func onScreen(p Placement, o Rect) bool {
464- return !p.Hidden && p.Rect.Overlaps(o)
461+// viewport is where a workspace is seen: frame is the whole viewport,
462+// usable the part exclusive layers (panels) leave to the tiles.
463+type viewport struct{ frame, usable Rect }
464+
465+// whole is a viewport with no panel: all of r is usable.
466+func whole(r Rect) viewport { return viewport{frame: r, usable: r} }
467+
468+// shows reports whether the user sees p: not hidden, and overlapping the
469+// area its kind is laid out in. A tile lives in the usable area, so a
470+// cascade band or a column scrolled under a panel is not seen; a float, a
471+// fullscreen window and an overview preview are placed against the frame.
472+func (v viewport) shows(p Placement) bool {
473+ if p.Hidden {
474+ return false
475+ }
476+ if p.Floating || p.Fullscreen || p.Preview > 0 {
477+ return p.Rect.Overlaps(v.frame)
478+ }
479+ return p.Rect.Overlaps(v.usable)
465480 }
466481
467482 // floatDim is the veil opacity of a layout: dim only when a float is
@@ -470,9 +485,9 @@ func onScreen(p Placement, o Rect) bool {
470485 // drops the veil at once: a veil that fades changes Scene.Dim every frame,
471486 // which redraws the whole output for the length of the fade, where the
472487 // float's own fade redraws only its rect.
473-func floatDim(layout []Placement, o Rect, dim float64) float64 {
488+func floatDim(layout []Placement, v viewport, dim float64) float64 {
474489 for _, p := range layout {
475- if p.Floating && !p.Below && p.Preview == 0 && onScreen(p, o) {
490+ if p.Floating && !p.Below && p.Preview == 0 && v.shows(p) {
476491 if p.Fullscreen {
477492 return 0
478493 }
@@ -458,10 +458,25 @@ func (c *Core) clientRect(p Placement) Rect {
458 return p.Rect.Inset(p.Inset, c.cfg.Border.Width)458 return p.Rect.Inset(p.Inset, c.cfg.Border.Width)
459 }459 }
460 460
461-// onScreen reports whether a placement is drawn on its output o: not461+// viewport is where a workspace is seen: frame is the whole viewport,
462-// hidden and not scrolled off.462+// usable the part exclusive layers (panels) leave to the tiles.
463-func onScreen(p Placement, o Rect) bool {463+type viewport struct{ frame, usable Rect }
464- return !p.Hidden && p.Rect.Overlaps(o)464+
465+// whole is a viewport with no panel: all of r is usable.
466+func whole(r Rect) viewport { return viewport{frame: r, usable: r} }
467+
468+// shows reports whether the user sees p: not hidden, and overlapping the
469+// area its kind is laid out in. A tile lives in the usable area, so a
470+// cascade band or a column scrolled under a panel is not seen; a float, a
471+// fullscreen window and an overview preview are placed against the frame.
472+func (v viewport) shows(p Placement) bool {
473+ if p.Hidden {
474+ return false
475+ }
476+ if p.Floating || p.Fullscreen || p.Preview > 0 {
477+ return p.Rect.Overlaps(v.frame)
478+ }
479+ return p.Rect.Overlaps(v.usable)
465 }480 }
466 481
467 // floatDim is the veil opacity of a layout: dim only when a float is482 // floatDim is the veil opacity of a layout: dim only when a float is
@@ -470,9 +485,9 @@ func onScreen(p Placement, o Rect) bool {
470 // drops the veil at once: a veil that fades changes Scene.Dim every frame,485 // drops the veil at once: a veil that fades changes Scene.Dim every frame,
471 // which redraws the whole output for the length of the fade, where the486 // which redraws the whole output for the length of the fade, where the
472 // float's own fade redraws only its rect.487 // float's own fade redraws only its rect.
473-func floatDim(layout []Placement, o Rect, dim float64) float64 {488+func floatDim(layout []Placement, v viewport, dim float64) float64 {
474 for _, p := range layout {489 for _, p := range layout {
475- if p.Floating && !p.Below && p.Preview == 0 && onScreen(p, o) {490+ if p.Floating && !p.Below && p.Preview == 0 && v.shows(p) {
476 if p.Fullscreen {491 if p.Fullscreen {
477 return 0492 return 0
478 }493 }
M internal/core/monitor.go +6 -0
@@ -451,6 +451,12 @@ func (m *Monitor) Frame() Rect {
451451 return m.Current().Output
452452 }
453453
454+// viewport is where the workspace on screen is seen: Frame, and the
455+// usable area its tiles live in.
456+func (m *Monitor) viewport() viewport {
457+ return viewport{frame: m.Frame(), usable: m.Current().Usable}
458+}
459+
454460 // frameHas reports whether the output-local point is inside the frame.
455461 func (m *Monitor) frameHas(x, y float64) bool {
456462 f := m.Frame()
@@ -451,6 +451,12 @@ func (m *Monitor) Frame() Rect {
451 return m.Current().Output451 return m.Current().Output
452 }452 }
453 453
454+// viewport is where the workspace on screen is seen: Frame, and the
455+// usable area its tiles live in.
456+func (m *Monitor) viewport() viewport {
457+ return viewport{frame: m.Frame(), usable: m.Current().Usable}
458+}
459+
454 // frameHas reports whether the output-local point is inside the frame.460 // frameHas reports whether the output-local point is inside the frame.
455 func (m *Monitor) frameHas(x, y float64) bool {461 func (m *Monitor) frameHas(x, y float64) bool {
456 f := m.Frame()462 f := m.Frame()
M internal/core/overview_maximized_test.go +2 -2
@@ -273,7 +273,7 @@ func TestOverviewMaximizedPreviewsKeepConfigures(t *testing.T) {
273273 r := w.columnRectsFor(true)[int(p.ID)-1]
274274 p.Rect, p.Hidden = r, false
275275 }
276- v, _ := s.next(p, configureTarget{output: "test", client: p.Rect, area: w.Usable})
276+ v, _ := s.next(p, configureTarget{output: "test", client: p.Rect, view: whole(w.Usable)})
277277 s.mark(v)
278278 before[p.ID] = v
279279 }
@@ -283,7 +283,7 @@ func TestOverviewMaximizedPreviewsKeepConfigures(t *testing.T) {
283283 if p.ID < 1 || p.ID > 4 {
284284 continue
285285 }
286- v, _ := s.next(p, configureTarget{output: "test", area: w.Usable})
286+ v, _ := s.next(p, configureTarget{output: "test", view: whole(w.Usable)})
287287 if v.Width != before[p.ID].Width || v.Height != before[p.ID].Height {
288288 t.Fatalf("%d resized from %+v to %+v", p.ID, before[p.ID], v)
289289 }
@@ -273,7 +273,7 @@ func TestOverviewMaximizedPreviewsKeepConfigures(t *testing.T) {
273 r := w.columnRectsFor(true)[int(p.ID)-1]273 r := w.columnRectsFor(true)[int(p.ID)-1]
274 p.Rect, p.Hidden = r, false274 p.Rect, p.Hidden = r, false
275 }275 }
276- v, _ := s.next(p, configureTarget{output: "test", client: p.Rect, area: w.Usable})276+ v, _ := s.next(p, configureTarget{output: "test", client: p.Rect, view: whole(w.Usable)})
277 s.mark(v)277 s.mark(v)
278 before[p.ID] = v278 before[p.ID] = v
279 }279 }
@@ -283,7 +283,7 @@ func TestOverviewMaximizedPreviewsKeepConfigures(t *testing.T) {
283 if p.ID < 1 || p.ID > 4 {283 if p.ID < 1 || p.ID > 4 {
284 continue284 continue
285 }285 }
286- v, _ := s.next(p, configureTarget{output: "test", area: w.Usable})286+ v, _ := s.next(p, configureTarget{output: "test", view: whole(w.Usable)})
287 if v.Width != before[p.ID].Width || v.Height != before[p.ID].Height {287 if v.Width != before[p.ID].Width || v.Height != before[p.ID].Height {
288 t.Fatalf("%d resized from %+v to %+v", p.ID, before[p.ID], v)288 t.Fatalf("%d resized from %+v to %+v", p.ID, before[p.ID], v)
289 }289 }
M internal/core/overview_stack_test.go +1 -1
@@ -273,7 +273,7 @@ func TestOverviewStashRightBringsColumnsFront(t *testing.T) {
273273 func TestOverviewStackNoFloatVeil(t *testing.T) {
274274 m := stackMonitor()
275275 m.ToggleOverview()
276- if d := floatDim(m.Layout(), Rect{W: 300, H: 200}, 0.3); d != 0 {
276+ if d := floatDim(m.Layout(), whole(Rect{W: 300, H: 200}), 0.3); d != 0 {
277277 t.Fatalf("veil %v in the overview", d)
278278 }
279279 }
@@ -273,7 +273,7 @@ func TestOverviewStashRightBringsColumnsFront(t *testing.T) {
273 func TestOverviewStackNoFloatVeil(t *testing.T) {273 func TestOverviewStackNoFloatVeil(t *testing.T) {
274 m := stackMonitor()274 m := stackMonitor()
275 m.ToggleOverview()275 m.ToggleOverview()
276- if d := floatDim(m.Layout(), Rect{W: 300, H: 200}, 0.3); d != 0 {276+ if d := floatDim(m.Layout(), whole(Rect{W: 300, H: 200}), 0.3); d != 0 {
277 t.Fatalf("veil %v in the overview", d)277 t.Fatalf("veil %v in the overview", d)
278 }278 }
279 }279 }
M internal/core/popup.go +4 -3
@@ -146,9 +146,10 @@ func (c *Core) windowRect(id WindowID) (*screen, Rect, bool) {
146146 for _, sc := range c.screens {
147147 for _, pl := range sc.shownLayout() {
148148 if pl.ID == id {
149- // Hidden or scrolled off: not on screen, like its popups.
150- // A preview takes no input and shows no popups.
151- if !onScreen(pl, sc.mon.Frame()) || pl.Preview > 0 {
149+ // Hidden, scrolled off or under a panel: not on screen,
150+ // like its popups. A preview takes no input and shows no
151+ // popups.
152+ if !sc.mon.viewport().shows(pl) || pl.Preview > 0 {
152153 return nil, Rect{}, false
153154 }
154155 return sc, c.clientRect(pl), true
@@ -146,9 +146,10 @@ func (c *Core) windowRect(id WindowID) (*screen, Rect, bool) {
146 for _, sc := range c.screens {146 for _, sc := range c.screens {
147 for _, pl := range sc.shownLayout() {147 for _, pl := range sc.shownLayout() {
148 if pl.ID == id {148 if pl.ID == id {
149- // Hidden or scrolled off: not on screen, like its popups.149+ // Hidden, scrolled off or under a panel: not on screen,
150- // A preview takes no input and shows no popups.150+ // like its popups. A preview takes no input and shows no
151- if !onScreen(pl, sc.mon.Frame()) || pl.Preview > 0 {151+ // popups.
152+ if !sc.mon.viewport().shows(pl) || pl.Preview > 0 {
152 return nil, Rect{}, false153 return nil, Rect{}, false
153 }154 }
154 return sc, c.clientRect(pl), true155 return sc, c.clientRect(pl), true
M internal/core/pulse.go +3 -3
@@ -151,11 +151,11 @@ func (m *Monitor) settled() bool {
151151 return true
152152 }
153153
154-// visibleCount is the number of windows of layout shown in area.
155-func visibleCount(layout []Placement, area Rect) int {
154+// visibleCount is the number of windows of layout seen in v.
155+func visibleCount(layout []Placement, v viewport) int {
156156 n := 0
157157 for _, p := range layout {
158- if p.Preview == 0 && onScreen(p, area) {
158+ if p.Preview == 0 && v.shows(p) {
159159 n++
160160 }
161161 }
@@ -151,11 +151,11 @@ func (m *Monitor) settled() bool {
151 return true151 return true
152 }152 }
153 153
154-// visibleCount is the number of windows of layout shown in area.154+// visibleCount is the number of windows of layout seen in v.
155-func visibleCount(layout []Placement, area Rect) int {155+func visibleCount(layout []Placement, v viewport) int {
156 n := 0156 n := 0
157 for _, p := range layout {157 for _, p := range layout {
158- if p.Preview == 0 && onScreen(p, area) {158+ if p.Preview == 0 && v.shows(p) {
159 n++159 n++
160 }160 }
161 }161 }
M internal/core/scene.go +9 -10
@@ -11,12 +11,12 @@ import (
1111 // drawable reports that it can show the focus pulse on the focused window.
1212 func (c *Core) sceneFor(ctx context.Context, i int, sc *screen, only *screen, capture *capView, pulse float64) (scene ports.Scene, drawable bool, err error) {
1313 o := sc.mon.Output()
14- // frame is the viewport of the workspace on screen: the whole output
15- // unless it has a size override (never in the overview).
16- frame := sc.mon.Frame()
14+ // view.frame is the viewport of the workspace on screen: the whole
15+ // output unless it has a size override (never in the overview).
16+ view := sc.mon.viewport()
1717 var clip Rect
18- if frame != (Rect{W: o.W, H: o.H}) {
19- clip = frame
18+ if view.frame != (Rect{W: o.W, H: o.H}) {
19+ clip = view.frame
2020 }
2121 // layout is what is drawn; settled (same indexes) is where the
2222 // windows are going, and alone sizes the configures.
@@ -37,7 +37,7 @@ func (c *Core) sceneFor(ctx context.Context, i int, sc *screen, only *screen, ca
3737 }
3838 }
3939 }
40- scene.Dim = floatDim(layout, frame, c.cfg.Floating.Dim)
40+ scene.Dim = floatDim(layout, view, c.cfg.Floating.Dim)
4141 if sc.mon.ov.open {
4242 // Darken the wallpaper around the previews.
4343 scene.Dim = c.cfg.Floating.Dim
@@ -52,11 +52,10 @@ func (c *Core) sceneFor(ctx context.Context, i int, sc *screen, only *screen, ca
5252 scene.Separators = append(scene.Separators, overviewOutline(layout, max(c.cfg.Border.Width, 2))...)
5353 } else {
5454 // Only the focused output lights the focused window's lines.
55- scene.Separators = separators(layout, c.cfg.Border.Width, sc.mon.Current().gap(), frame, i == c.focusScreen)
55+ scene.Separators = separators(layout, c.cfg.Border.Width, sc.mon.Current().gap(), view, i == c.focusScreen)
5656 }
5757 // A window alone on screen needs no pulse to show it has the focus.
58- // A tile under a panel (the next cascade band) does not count.
59- alone := i == c.focusScreen && c.pulse.target != 0 && visibleCount(layout, sc.mon.Current().Usable) == 1
58+ alone := i == c.focusScreen && c.pulse.target != 0 && visibleCount(layout, view) == 1
6059 for k, p := range layout {
6160 ps := settled[k]
6261 // Only the focused output has an activated window.
@@ -91,7 +90,7 @@ func (c *Core) sceneFor(ctx context.Context, i int, sc *screen, only *screen, ca
9190 }
9291 }
9392 scene.Windows = append(scene.Windows, sw)
94- t := configureTarget{output: sc.name(), area: frame, focused: focused, captured: capture != nil && capture.window == p.ID}
93+ t := configureTarget{output: sc.name(), view: view, focused: focused, captured: capture != nil && capture.window == p.ID}
9594 if !p.Hidden && p.Preview == 0 {
9695 // Only a sized configure needs the client size.
9796 t.client, t.imposed = c.clientRect(ps), sc.mon.Current().imposedFloat(p.ID)
@@ -11,12 +11,12 @@ import (
11 // drawable reports that it can show the focus pulse on the focused window.11 // drawable reports that it can show the focus pulse on the focused window.
12 func (c *Core) sceneFor(ctx context.Context, i int, sc *screen, only *screen, capture *capView, pulse float64) (scene ports.Scene, drawable bool, err error) {12 func (c *Core) sceneFor(ctx context.Context, i int, sc *screen, only *screen, capture *capView, pulse float64) (scene ports.Scene, drawable bool, err error) {
13 o := sc.mon.Output()13 o := sc.mon.Output()
14- // frame is the viewport of the workspace on screen: the whole output14+ // view.frame is the viewport of the workspace on screen: the whole
15- // unless it has a size override (never in the overview).15+ // output unless it has a size override (never in the overview).
16- frame := sc.mon.Frame()16+ view := sc.mon.viewport()
17 var clip Rect17 var clip Rect
18- if frame != (Rect{W: o.W, H: o.H}) {18+ if view.frame != (Rect{W: o.W, H: o.H}) {
19- clip = frame19+ clip = view.frame
20 }20 }
21 // layout is what is drawn; settled (same indexes) is where the21 // layout is what is drawn; settled (same indexes) is where the
22 // windows are going, and alone sizes the configures.22 // windows are going, and alone sizes the configures.
@@ -37,7 +37,7 @@ func (c *Core) sceneFor(ctx context.Context, i int, sc *screen, only *screen, ca
37 }37 }
38 }38 }
39 }39 }
40- scene.Dim = floatDim(layout, frame, c.cfg.Floating.Dim)40+ scene.Dim = floatDim(layout, view, c.cfg.Floating.Dim)
41 if sc.mon.ov.open {41 if sc.mon.ov.open {
42 // Darken the wallpaper around the previews.42 // Darken the wallpaper around the previews.
43 scene.Dim = c.cfg.Floating.Dim43 scene.Dim = c.cfg.Floating.Dim
@@ -52,11 +52,10 @@ func (c *Core) sceneFor(ctx context.Context, i int, sc *screen, only *screen, ca
52 scene.Separators = append(scene.Separators, overviewOutline(layout, max(c.cfg.Border.Width, 2))...)52 scene.Separators = append(scene.Separators, overviewOutline(layout, max(c.cfg.Border.Width, 2))...)
53 } else {53 } else {
54 // Only the focused output lights the focused window's lines.54 // Only the focused output lights the focused window's lines.
55- scene.Separators = separators(layout, c.cfg.Border.Width, sc.mon.Current().gap(), frame, i == c.focusScreen)55+ scene.Separators = separators(layout, c.cfg.Border.Width, sc.mon.Current().gap(), view, i == c.focusScreen)
56 }56 }
57 // A window alone on screen needs no pulse to show it has the focus.57 // A window alone on screen needs no pulse to show it has the focus.
58- // A tile under a panel (the next cascade band) does not count.58+ alone := i == c.focusScreen && c.pulse.target != 0 && visibleCount(layout, view) == 1
59- alone := i == c.focusScreen && c.pulse.target != 0 && visibleCount(layout, sc.mon.Current().Usable) == 1
60 for k, p := range layout {59 for k, p := range layout {
61 ps := settled[k]60 ps := settled[k]
62 // Only the focused output has an activated window.61 // Only the focused output has an activated window.
@@ -91,7 +90,7 @@ func (c *Core) sceneFor(ctx context.Context, i int, sc *screen, only *screen, ca
91 }90 }
92 }91 }
93 scene.Windows = append(scene.Windows, sw)92 scene.Windows = append(scene.Windows, sw)
94- t := configureTarget{output: sc.name(), area: frame, focused: focused, captured: capture != nil && capture.window == p.ID}93+ t := configureTarget{output: sc.name(), view: view, focused: focused, captured: capture != nil && capture.window == p.ID}
95 if !p.Hidden && p.Preview == 0 {94 if !p.Hidden && p.Preview == 0 {
96 // Only a sized configure needs the client size.95 // Only a sized configure needs the client size.
97 t.client, t.imposed = c.clientRect(ps), sc.mon.Current().imposedFloat(p.ID)96 t.client, t.imposed = c.clientRect(ps), sc.mon.Current().imposedFloat(p.ID)
M internal/core/separators.go +6 -5
@@ -18,13 +18,14 @@ import "github.com/bnema/neferwl/internal/ports"
1818
1919 var sides = [...]ports.Sides{ports.SideLeft, ports.SideRight, ports.SideTop, ports.SideBottom}
2020
21-// separators are the lines of a layout on output o, inactive ones first
22-// so active ones draw over them. lit says whether the focused tile shows
23-// focus (only on the focused output).
24-func separators(ps []Placement, width, gap int, o Rect, lit bool) []ports.Separator {
21+// separators are the lines of the windows seen in v, clipped to its frame,
22+// inactive ones first so active ones draw over them. lit says whether the
23+// focused tile shows focus (only on the focused output).
24+func separators(ps []Placement, width, gap int, v viewport, lit bool) []ports.Separator {
2525 if width <= 0 {
2626 return nil
2727 }
28+ o := v.frame
2829 var out []ports.Separator
2930 add := func(p *Placement, r Rect, active bool) {
3031 // A line cannot belong to a scrolled-off window or bleed onto
@@ -52,7 +53,7 @@ func separators(ps []Placement, width, gap int, o Rect, lit bool) []ports.Separa
5253 p := &ps[i]
5354 // A leaving float keeps its own lines while it fades; a leaving
5455 // tile's were shared with its neighbours, which re-flowed.
55- if p.Fullscreen || !(onScreen(*p, o) || p.Leaving && p.Floating && p.Rect.Overlaps(o)) {
56+ if p.Fullscreen || !(v.shows(*p) || p.Leaving && p.Floating && p.Rect.Overlaps(o)) {
5657 continue
5758 }
5859 // Tiles sharing a line: Neighbors only counts tiles visible in
@@ -18,13 +18,14 @@ import "github.com/bnema/neferwl/internal/ports"
18 18
19 var sides = [...]ports.Sides{ports.SideLeft, ports.SideRight, ports.SideTop, ports.SideBottom}19 var sides = [...]ports.Sides{ports.SideLeft, ports.SideRight, ports.SideTop, ports.SideBottom}
20 20
21-// separators are the lines of a layout on output o, inactive ones first21+// separators are the lines of the windows seen in v, clipped to its frame,
22-// so active ones draw over them. lit says whether the focused tile shows22+// inactive ones first so active ones draw over them. lit says whether the
23-// focus (only on the focused output).23+// focused tile shows focus (only on the focused output).
24-func separators(ps []Placement, width, gap int, o Rect, lit bool) []ports.Separator {24+func separators(ps []Placement, width, gap int, v viewport, lit bool) []ports.Separator {
25 if width <= 0 {25 if width <= 0 {
26 return nil26 return nil
27 }27 }
28+ o := v.frame
28 var out []ports.Separator29 var out []ports.Separator
29 add := func(p *Placement, r Rect, active bool) {30 add := func(p *Placement, r Rect, active bool) {
30 // A line cannot belong to a scrolled-off window or bleed onto31 // A line cannot belong to a scrolled-off window or bleed onto
@@ -52,7 +53,7 @@ func separators(ps []Placement, width, gap int, o Rect, lit bool) []ports.Separa
52 p := &ps[i]53 p := &ps[i]
53 // A leaving float keeps its own lines while it fades; a leaving54 // A leaving float keeps its own lines while it fades; a leaving
54 // tile's were shared with its neighbours, which re-flowed.55 // tile's were shared with its neighbours, which re-flowed.
55- if p.Fullscreen || !(onScreen(*p, o) || p.Leaving && p.Floating && p.Rect.Overlaps(o)) {56+ if p.Fullscreen || !(v.shows(*p) || p.Leaving && p.Floating && p.Rect.Overlaps(o)) {
56 continue57 continue
57 }58 }
58 // Tiles sharing a line: Neighbors only counts tiles visible in59 // Tiles sharing a line: Neighbors only counts tiles visible in
M internal/core/separators_test.go +8 -8
@@ -66,7 +66,7 @@ func TestSeparators(t *testing.T) {
6666 {"left column: whole line, split column untouched", tiles(0, grid...), true, []px{{14, 3, "lit"}, {14, 30, "lit"}, {20, 14, "gray"}}},
6767 } {
6868 t.Run(tc.name, func(t *testing.T) {
69- seps := separators(tc.ps, 2, 0, bigOutput, tc.lit)
69+ seps := separators(tc.ps, 2, 0, whole(bigOutput), tc.lit)
7070 for _, c := range tc.check {
7171 if got := colorAt(seps, c.x, c.y); got != c.want {
7272 t.Errorf("(%d,%d) = %q, want %q; %v", c.x, c.y, got, c.want, seps)
@@ -77,22 +77,22 @@ func TestSeparators(t *testing.T) {
7777 }
7878
7979 func TestSeparatorsSkip(t *testing.T) {
80- if s := separators(tiles(0, Rect{W: 32, H: 16}), 2, 0, bigOutput, true); len(s) != 0 {
80+ if s := separators(tiles(0, Rect{W: 32, H: 16}), 2, 0, whole(bigOutput), true); len(s) != 0 {
8181 t.Fatal("lone window:", s)
8282 }
83- if s := separators(tiles(0, Rect{W: 16, H: 16}, Rect{X: 16, W: 16, H: 16}), 0, 0, bigOutput, true); len(s) != 0 {
83+ if s := separators(tiles(0, Rect{W: 16, H: 16}, Rect{X: 16, W: 16, H: 16}), 0, 0, whole(bigOutput), true); len(s) != 0 {
8484 t.Fatal("width 0:", s)
8585 }
8686 full := tiles(0, Rect{W: 16, H: 16}, Rect{X: 16, W: 16, H: 16})
8787 full[0].Fullscreen, full[1].Hidden = true, true
88- if s := separators(full, 2, 0, bigOutput, true); len(s) != 0 {
88+ if s := separators(full, 2, 0, whole(bigOutput), true); len(s) != 0 {
8989 t.Fatal("fullscreen:", s)
9090 }
9191 }
9292
9393 func TestSeparatorsFloat(t *testing.T) {
9494 float := Placement{ID: 1, Rect: Rect{X: 10, Y: 10, W: 20, H: 20}, Floating: true, Focused: true, Inset: ports.SideAll}
95- seps := separators([]Placement{float}, 2, 0, bigOutput, true)
95+ seps := separators([]Placement{float}, 2, 0, whole(bigOutput), true)
9696 if seps[0].Window != 1 {
9797 t.Fatal("float border not tied to its window:", seps)
9898 }
@@ -134,7 +134,7 @@ func TestSeparatorsScroll(t *testing.T) {
134134 t.Fatalf("expected visible borders to change with the view: %v then %v", before, after)
135135 }
136136 out := Rect{W: 100, H: 80}
137- seps := separators(w.Layout(), 2, 0, out, true)
137+ seps := separators(w.Layout(), 2, 0, whole(out), true)
138138 if got := colorAt(seps, 99, 40); got != "" {
139139 t.Fatalf("line at the output edge: %v", seps)
140140 }
@@ -183,7 +183,7 @@ func TestSeparatorsCascadeBottomPanel(t *testing.T) {
183183 if p := previewOf(t, ps, 1); p.Neighbors != ports.SideRight {
184184 t.Fatalf("neighbors %04b, want right only", p.Neighbors)
185185 }
186- seps := separators(ps, 2, 0, w.Output, true)
186+ seps := separators(ps, 2, 0, whole(w.Output), true)
187187 if got := colorAt(seps, 200, 578); got != "" {
188188 t.Fatalf("line along the panel: %v", seps)
189189 }
@@ -200,7 +200,7 @@ func TestSeparatorsCascadeBottomPanel(t *testing.T) {
200200 func TestSeparatorsGaps(t *testing.T) {
201201 ps := []Placement{{ID: 1, Rect: Rect{X: 0, W: 16, H: 16}, Focused: true}, {ID: 2, Rect: Rect{X: 20, W: 16, H: 16}}}
202202 setVisibleNeighbors(ps, 4, bigOutput)
203- seps := separators(ps, 2, 4, bigOutput, true)
203+ seps := separators(ps, 2, 4, whole(bigOutput), true)
204204 for _, c := range []px{{14, 2, "lit"}, {14, 13, "lit"}, {20, 8, "gray"}, {17, 8, ""}} {
205205 if got := colorAt(seps, c.x, c.y); got != c.want {
206206 t.Errorf("(%d,%d) = %q, want %q", c.x, c.y, got, c.want)
@@ -66,7 +66,7 @@ func TestSeparators(t *testing.T) {
66 {"left column: whole line, split column untouched", tiles(0, grid...), true, []px{{14, 3, "lit"}, {14, 30, "lit"}, {20, 14, "gray"}}},66 {"left column: whole line, split column untouched", tiles(0, grid...), true, []px{{14, 3, "lit"}, {14, 30, "lit"}, {20, 14, "gray"}}},
67 } {67 } {
68 t.Run(tc.name, func(t *testing.T) {68 t.Run(tc.name, func(t *testing.T) {
69- seps := separators(tc.ps, 2, 0, bigOutput, tc.lit)69+ seps := separators(tc.ps, 2, 0, whole(bigOutput), tc.lit)
70 for _, c := range tc.check {70 for _, c := range tc.check {
71 if got := colorAt(seps, c.x, c.y); got != c.want {71 if got := colorAt(seps, c.x, c.y); got != c.want {
72 t.Errorf("(%d,%d) = %q, want %q; %v", c.x, c.y, got, c.want, seps)72 t.Errorf("(%d,%d) = %q, want %q; %v", c.x, c.y, got, c.want, seps)
@@ -77,22 +77,22 @@ func TestSeparators(t *testing.T) {
77 }77 }
78 78
79 func TestSeparatorsSkip(t *testing.T) {79 func TestSeparatorsSkip(t *testing.T) {
80- if s := separators(tiles(0, Rect{W: 32, H: 16}), 2, 0, bigOutput, true); len(s) != 0 {80+ if s := separators(tiles(0, Rect{W: 32, H: 16}), 2, 0, whole(bigOutput), true); len(s) != 0 {
81 t.Fatal("lone window:", s)81 t.Fatal("lone window:", s)
82 }82 }
83- if s := separators(tiles(0, Rect{W: 16, H: 16}, Rect{X: 16, W: 16, H: 16}), 0, 0, bigOutput, true); len(s) != 0 {83+ if s := separators(tiles(0, Rect{W: 16, H: 16}, Rect{X: 16, W: 16, H: 16}), 0, 0, whole(bigOutput), true); len(s) != 0 {
84 t.Fatal("width 0:", s)84 t.Fatal("width 0:", s)
85 }85 }
86 full := tiles(0, Rect{W: 16, H: 16}, Rect{X: 16, W: 16, H: 16})86 full := tiles(0, Rect{W: 16, H: 16}, Rect{X: 16, W: 16, H: 16})
87 full[0].Fullscreen, full[1].Hidden = true, true87 full[0].Fullscreen, full[1].Hidden = true, true
88- if s := separators(full, 2, 0, bigOutput, true); len(s) != 0 {88+ if s := separators(full, 2, 0, whole(bigOutput), true); len(s) != 0 {
89 t.Fatal("fullscreen:", s)89 t.Fatal("fullscreen:", s)
90 }90 }
91 }91 }
92 92
93 func TestSeparatorsFloat(t *testing.T) {93 func TestSeparatorsFloat(t *testing.T) {
94 float := Placement{ID: 1, Rect: Rect{X: 10, Y: 10, W: 20, H: 20}, Floating: true, Focused: true, Inset: ports.SideAll}94 float := Placement{ID: 1, Rect: Rect{X: 10, Y: 10, W: 20, H: 20}, Floating: true, Focused: true, Inset: ports.SideAll}
95- seps := separators([]Placement{float}, 2, 0, bigOutput, true)95+ seps := separators([]Placement{float}, 2, 0, whole(bigOutput), true)
96 if seps[0].Window != 1 {96 if seps[0].Window != 1 {
97 t.Fatal("float border not tied to its window:", seps)97 t.Fatal("float border not tied to its window:", seps)
98 }98 }
@@ -134,7 +134,7 @@ func TestSeparatorsScroll(t *testing.T) {
134 t.Fatalf("expected visible borders to change with the view: %v then %v", before, after)134 t.Fatalf("expected visible borders to change with the view: %v then %v", before, after)
135 }135 }
136 out := Rect{W: 100, H: 80}136 out := Rect{W: 100, H: 80}
137- seps := separators(w.Layout(), 2, 0, out, true)137+ seps := separators(w.Layout(), 2, 0, whole(out), true)
138 if got := colorAt(seps, 99, 40); got != "" {138 if got := colorAt(seps, 99, 40); got != "" {
139 t.Fatalf("line at the output edge: %v", seps)139 t.Fatalf("line at the output edge: %v", seps)
140 }140 }
@@ -183,7 +183,7 @@ func TestSeparatorsCascadeBottomPanel(t *testing.T) {
183 if p := previewOf(t, ps, 1); p.Neighbors != ports.SideRight {183 if p := previewOf(t, ps, 1); p.Neighbors != ports.SideRight {
184 t.Fatalf("neighbors %04b, want right only", p.Neighbors)184 t.Fatalf("neighbors %04b, want right only", p.Neighbors)
185 }185 }
186- seps := separators(ps, 2, 0, w.Output, true)186+ seps := separators(ps, 2, 0, whole(w.Output), true)
187 if got := colorAt(seps, 200, 578); got != "" {187 if got := colorAt(seps, 200, 578); got != "" {
188 t.Fatalf("line along the panel: %v", seps)188 t.Fatalf("line along the panel: %v", seps)
189 }189 }
@@ -200,7 +200,7 @@ func TestSeparatorsCascadeBottomPanel(t *testing.T) {
200 func TestSeparatorsGaps(t *testing.T) {200 func TestSeparatorsGaps(t *testing.T) {
201 ps := []Placement{{ID: 1, Rect: Rect{X: 0, W: 16, H: 16}, Focused: true}, {ID: 2, Rect: Rect{X: 20, W: 16, H: 16}}}201 ps := []Placement{{ID: 1, Rect: Rect{X: 0, W: 16, H: 16}, Focused: true}, {ID: 2, Rect: Rect{X: 20, W: 16, H: 16}}}
202 setVisibleNeighbors(ps, 4, bigOutput)202 setVisibleNeighbors(ps, 4, bigOutput)
203- seps := separators(ps, 2, 4, bigOutput, true)203+ seps := separators(ps, 2, 4, whole(bigOutput), true)
204 for _, c := range []px{{14, 2, "lit"}, {14, 13, "lit"}, {20, 8, "gray"}, {17, 8, ""}} {204 for _, c := range []px{{14, 2, "lit"}, {14, 13, "lit"}, {20, 8, "gray"}, {17, 8, ""}} {
205 if got := colorAt(seps, c.x, c.y); got != c.want {205 if got := colorAt(seps, c.x, c.y); got != c.want {
206 t.Errorf("(%d,%d) = %q, want %q", c.x, c.y, got, c.want)206 t.Errorf("(%d,%d) = %q, want %q", c.x, c.y, got, c.want)
M internal/core/state.go +2 -1
@@ -54,8 +54,9 @@ func (c *Core) stateInto(st ports.State) ports.State {
5454 // Read at once, into publish's scratch: the snapshot holds no
5555 // placement.
5656 c.realBuf = m.layoutInto(c.realBuf)
57+ view := m.viewport()
5758 for _, p := range c.realBuf {
58- shown[p.ID] = onScreen(p, m.Frame())
59+ shown[p.ID] = view.shows(p)
5960 }
6061 for w := range m.all() {
6162 n := indexOf(m.Workspaces, w) + 1
@@ -54,8 +54,9 @@ func (c *Core) stateInto(st ports.State) ports.State {
54 // Read at once, into publish's scratch: the snapshot holds no54 // Read at once, into publish's scratch: the snapshot holds no
55 // placement.55 // placement.
56 c.realBuf = m.layoutInto(c.realBuf)56 c.realBuf = m.layoutInto(c.realBuf)
57+ view := m.viewport()
57 for _, p := range c.realBuf {58 for _, p := range c.realBuf {
58- shown[p.ID] = onScreen(p, m.Frame())59+ shown[p.ID] = view.shows(p)
59 }60 }
60 for w := range m.all() {61 for w := range m.all() {
61 n := indexOf(m.Workspaces, w) + 162 n := indexOf(m.Workspaces, w) + 1
A internal/core/viewport_test.go +129 -0
@@ -0,0 +1,129 @@
1+package core
2+
3+import (
4+ "context"
5+ "testing"
6+
7+ "github.com/bnema/neferwl/internal/ports"
8+)
9+
10+// A tile is seen in the usable area; a float, a fullscreen window and a
11+// preview are placed against the whole frame.
12+func TestViewportShows(t *testing.T) {
13+ v := viewport{frame: Rect{W: 100, H: 80}, usable: Rect{W: 100, H: 60}}
14+ under := Rect{Y: 60, W: 50, H: 60} // the next cascade band, under a bottom panel
15+ for _, tc := range []struct {
16+ name string
17+ v viewport
18+ p Placement
19+ want bool
20+ }{
21+ {"tile in usable", v, Placement{Rect: Rect{W: 50, H: 60}}, true},
22+ {"tile under panel", v, Placement{Rect: under}, false},
23+ {"tile across the panel line", v, Placement{Rect: Rect{Y: 30, W: 50, H: 60}}, true},
24+ {"tile off frame", v, Placement{Rect: Rect{Y: 80, W: 50, H: 60}}, false},
25+ {"hidden tile", v, Placement{Rect: Rect{W: 50, H: 60}, Hidden: true}, false},
26+ {"float under panel", v, Placement{Rect: under, Floating: true}, true},
27+ {"hidden float", v, Placement{Rect: Rect{W: 50, H: 60}, Floating: true, Hidden: true}, false},
28+ {"fullscreen tile", v, Placement{Rect: Rect{W: 100, H: 80}, Fullscreen: true}, true},
29+ {"preview under panel", v, Placement{Rect: under, Preview: 0.5}, true},
30+ {"tile with no usable area", viewport{frame: v.frame}, Placement{Rect: Rect{W: 50, H: 60}}, false},
31+ {"float with no usable area", viewport{frame: v.frame}, Placement{Rect: Rect{W: 50, H: 60}, Floating: true}, true},
32+ } {
33+ if got := tc.v.shows(tc.p); got != tc.want {
34+ t.Errorf("%s: shows %v, want %v", tc.name, got, tc.want)
35+ }
36+ }
37+}
38+
39+// viewportCore is a core with one 100x80 output whose usable area is
40+// usable, publishing into the returned check: it asserts what each
41+// window's configure and the state snapshot say about its visibility.
42+func viewportCore(t *testing.T, layout func(*ports.Config), usable Rect) (*Core, func(map[WindowID]bool) ports.Scene) {
43+ t.Helper()
44+ cfg := ports.Config{}
45+ cfg.Keyboard.CmdKey = "super"
46+ layout(&cfg)
47+ commands := make(chan ports.ClientCommand, 256)
48+ scenes := make(chan []ports.Scene, 1)
49+ c, err := New(cfg, Channels{Commands: commands, Scenes: scenes}, Options{})
50+ if err != nil {
51+ t.Fatal(err)
52+ }
53+ c.addScreen(ports.OutputInfo{Name: "OUT-1", Width: 100, Height: 80})
54+ c.cur().mon.SetUsable(usable)
55+ sent := map[WindowID]ports.ConfigureWindow{}
56+ check := func(want map[WindowID]bool) ports.Scene {
57+ t.Helper()
58+ if err := c.publish(context.Background()); err != nil {
59+ t.Fatal(err)
60+ }
61+ scene := (<-scenes)[0]
62+ for len(commands) > 0 {
63+ if v, ok := (<-commands).(ports.ConfigureWindow); ok {
64+ sent[v.ID] = v
65+ }
66+ }
67+ state := map[WindowID]bool{}
68+ for _, s := range c.state().Windows {
69+ state[s.ID] = s.Visible
70+ }
71+ for id, vis := range want {
72+ if v := sent[id]; v.Visible != vis {
73+ t.Fatalf("window %d: configure %+v, want visible %v", id, v, vis)
74+ }
75+ if state[id] != vis {
76+ t.Fatalf("window %d: state visible %v, want %v", id, state[id], vis)
77+ }
78+ }
79+ return scene
80+ }
81+ return c, check
82+}
83+
84+// The next cascade band starts right under a bottom panel: its windows are
85+// not seen, so they are told they are invisible (and throttled), while the
86+// scene still carries them.
87+func TestConfigureVisibleCascadeUnderPanel(t *testing.T) {
88+ c, check := viewportCore(t, func(cfg *ports.Config) {
89+ cfg.Layout.Overflow = "cascade"
90+ cfg.Layout.MaxColumns = 2
91+ }, Rect{W: 100, H: 60})
92+ m := c.cur().mon
93+ w := m.Current()
94+ for id := WindowID(1); id <= 3; id++ {
95+ m.AddWindow(id)
96+ }
97+ w.FocusID(1)
98+ scene := check(map[WindowID]bool{1: true, 2: true, 3: false})
99+ found := false
100+ for _, sw := range scene.Windows {
101+ found = found || sw.ID == 3
102+ }
103+ if !found {
104+ t.Fatal("window under the panel left the scene")
105+ }
106+ // Its band scrolled in, the first band is under nothing but off screen.
107+ w.FocusID(3)
108+ check(map[WindowID]bool{1: false, 2: false, 3: true})
109+ // Fullscreen covers the panel: always seen.
110+ w.FocusID(1)
111+ check(map[WindowID]bool{1: true, 3: false})
112+ w.FocusID(3)
113+ w.ToggleFullscreen()
114+ check(map[WindowID]bool{3: true})
115+}
116+
117+// A column scrolled under a left panel is not seen.
118+func TestConfigureVisibleScrollUnderSidePanel(t *testing.T) {
119+ c, check := viewportCore(t, func(cfg *ports.Config) {
120+ cfg.Layout.MaxColumns = 1
121+ }, Rect{X: 20, W: 80, H: 80})
122+ m := c.cur().mon
123+ m.AddWindow(1)
124+ m.AddWindow(2)
125+ if r := m.Layout()[0].Rect; r.X >= 20 || r.X+r.W <= 0 {
126+ t.Fatalf("column 1 at %+v, want partly under the panel", r)
127+ }
128+ check(map[WindowID]bool{1: false, 2: true})
129+}
@@ -0,0 +1,129 @@
1+package core
2+
3+import (
4+ "context"
5+ "testing"
6+
7+ "github.com/bnema/neferwl/internal/ports"
8+)
9+
10+// A tile is seen in the usable area; a float, a fullscreen window and a
11+// preview are placed against the whole frame.
12+func TestViewportShows(t *testing.T) {
13+ v := viewport{frame: Rect{W: 100, H: 80}, usable: Rect{W: 100, H: 60}}
14+ under := Rect{Y: 60, W: 50, H: 60} // the next cascade band, under a bottom panel
15+ for _, tc := range []struct {
16+ name string
17+ v viewport
18+ p Placement
19+ want bool
20+ }{
21+ {"tile in usable", v, Placement{Rect: Rect{W: 50, H: 60}}, true},
22+ {"tile under panel", v, Placement{Rect: under}, false},
23+ {"tile across the panel line", v, Placement{Rect: Rect{Y: 30, W: 50, H: 60}}, true},
24+ {"tile off frame", v, Placement{Rect: Rect{Y: 80, W: 50, H: 60}}, false},
25+ {"hidden tile", v, Placement{Rect: Rect{W: 50, H: 60}, Hidden: true}, false},
26+ {"float under panel", v, Placement{Rect: under, Floating: true}, true},
27+ {"hidden float", v, Placement{Rect: Rect{W: 50, H: 60}, Floating: true, Hidden: true}, false},
28+ {"fullscreen tile", v, Placement{Rect: Rect{W: 100, H: 80}, Fullscreen: true}, true},
29+ {"preview under panel", v, Placement{Rect: under, Preview: 0.5}, true},
30+ {"tile with no usable area", viewport{frame: v.frame}, Placement{Rect: Rect{W: 50, H: 60}}, false},
31+ {"float with no usable area", viewport{frame: v.frame}, Placement{Rect: Rect{W: 50, H: 60}, Floating: true}, true},
32+ } {
33+ if got := tc.v.shows(tc.p); got != tc.want {
34+ t.Errorf("%s: shows %v, want %v", tc.name, got, tc.want)
35+ }
36+ }
37+}
38+
39+// viewportCore is a core with one 100x80 output whose usable area is
40+// usable, publishing into the returned check: it asserts what each
41+// window's configure and the state snapshot say about its visibility.
42+func viewportCore(t *testing.T, layout func(*ports.Config), usable Rect) (*Core, func(map[WindowID]bool) ports.Scene) {
43+ t.Helper()
44+ cfg := ports.Config{}
45+ cfg.Keyboard.CmdKey = "super"
46+ layout(&cfg)
47+ commands := make(chan ports.ClientCommand, 256)
48+ scenes := make(chan []ports.Scene, 1)
49+ c, err := New(cfg, Channels{Commands: commands, Scenes: scenes}, Options{})
50+ if err != nil {
51+ t.Fatal(err)
52+ }
53+ c.addScreen(ports.OutputInfo{Name: "OUT-1", Width: 100, Height: 80})
54+ c.cur().mon.SetUsable(usable)
55+ sent := map[WindowID]ports.ConfigureWindow{}
56+ check := func(want map[WindowID]bool) ports.Scene {
57+ t.Helper()
58+ if err := c.publish(context.Background()); err != nil {
59+ t.Fatal(err)
60+ }
61+ scene := (<-scenes)[0]
62+ for len(commands) > 0 {
63+ if v, ok := (<-commands).(ports.ConfigureWindow); ok {
64+ sent[v.ID] = v
65+ }
66+ }
67+ state := map[WindowID]bool{}
68+ for _, s := range c.state().Windows {
69+ state[s.ID] = s.Visible
70+ }
71+ for id, vis := range want {
72+ if v := sent[id]; v.Visible != vis {
73+ t.Fatalf("window %d: configure %+v, want visible %v", id, v, vis)
74+ }
75+ if state[id] != vis {
76+ t.Fatalf("window %d: state visible %v, want %v", id, state[id], vis)
77+ }
78+ }
79+ return scene
80+ }
81+ return c, check
82+}
83+
84+// The next cascade band starts right under a bottom panel: its windows are
85+// not seen, so they are told they are invisible (and throttled), while the
86+// scene still carries them.
87+func TestConfigureVisibleCascadeUnderPanel(t *testing.T) {
88+ c, check := viewportCore(t, func(cfg *ports.Config) {
89+ cfg.Layout.Overflow = "cascade"
90+ cfg.Layout.MaxColumns = 2
91+ }, Rect{W: 100, H: 60})
92+ m := c.cur().mon
93+ w := m.Current()
94+ for id := WindowID(1); id <= 3; id++ {
95+ m.AddWindow(id)
96+ }
97+ w.FocusID(1)
98+ scene := check(map[WindowID]bool{1: true, 2: true, 3: false})
99+ found := false
100+ for _, sw := range scene.Windows {
101+ found = found || sw.ID == 3
102+ }
103+ if !found {
104+ t.Fatal("window under the panel left the scene")
105+ }
106+ // Its band scrolled in, the first band is under nothing but off screen.
107+ w.FocusID(3)
108+ check(map[WindowID]bool{1: false, 2: false, 3: true})
109+ // Fullscreen covers the panel: always seen.
110+ w.FocusID(1)
111+ check(map[WindowID]bool{1: true, 3: false})
112+ w.FocusID(3)
113+ w.ToggleFullscreen()
114+ check(map[WindowID]bool{3: true})
115+}
116+
117+// A column scrolled under a left panel is not seen.
118+func TestConfigureVisibleScrollUnderSidePanel(t *testing.T) {
119+ c, check := viewportCore(t, func(cfg *ports.Config) {
120+ cfg.Layout.MaxColumns = 1
121+ }, Rect{X: 20, W: 80, H: 80})
122+ m := c.cur().mon
123+ m.AddWindow(1)
124+ m.AddWindow(2)
125+ if r := m.Layout()[0].Rect; r.X >= 20 || r.X+r.W <= 0 {
126+ t.Fatalf("column 1 at %+v, want partly under the panel", r)
127+ }
128+ check(map[WindowID]bool{1: false, 2: true})
129+}

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 26

Tags 10

↑↓ moveenter openesc close