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 Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· 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 | |||
| 91 | 91 | // An overview preview keeps its real size and physical state. | |
| 92 | 92 | return cp, t | |
| 93 | 93 | } | |
| 94 | - | t.area = cw.frame | |
| 94 | + | t.view = viewport{frame: cw.frame, usable: cw.ws.Usable} | |
| 95 | 95 | t.focused = false | |
| 96 | 96 | t.client, t.imposed = Rect{}, false | |
| 97 | 97 | if !cp.Hidden { | |
| @@ -146,6 +146,8 @@ func (c *Core) captureScene(seq uint64) *ports.Scene { | |||
| 146 | 146 | return nil | |
| 147 | 147 | } | |
| 148 | 148 | f := cw.frame | |
| 149 | + | // The capture image has no panels: all of it is seen. | |
| 150 | + | view := whole(f) | |
| 149 | 151 | rebase := func(r Rect) Rect { r.X, r.Y = r.X-f.X, r.Y-f.Y; return r } | |
| 150 | 152 | s := &ports.Scene{ | |
| 151 | 153 | Security: c.security, | |
| @@ -153,7 +155,7 @@ func (c *Core) captureScene(seq uint64) *ports.Scene { | |||
| 153 | 155 | Scale: cw.sc.scale, Background: c.cfg.Background.Color, | |
| 154 | 156 | Border: ports.Border{Width: c.cfg.Border.Width, Active: c.cfg.Border.Active, Inactive: c.cfg.Border.Inactive}, | |
| 155 | 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 | 160 | // A hidden workspace lights nothing: its focus is not shown. | |
| 159 | 161 | for _, p := range cw.placements { | |
| @@ -164,7 +166,7 @@ func (c *Core) captureScene(seq uint64) *ports.Scene { | |||
| 164 | 166 | w.Dim = c.peekDim(p) | |
| 165 | 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 | 170 | for i := range s.Separators { | |
| 169 | 171 | s.Separators[i].Rect = rebase(s.Separators[i].Rect) | |
| 170 | 172 | } | |
| @@ -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, t | 92 | return cp, t | ||
| 93 | } | 93 | } | ||
| 94 | - | t.area = cw.frame | 94 | + | t.view = viewport{frame: cw.frame, usable: cw.ws.Usable} |
| 95 | t.focused = false | 95 | t.focused = false | ||
| 96 | t.client, t.imposed = Rect{}, false | 96 | 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 nil | 146 | return nil | ||
| 147 | } | 147 | } | ||
| 148 | f := cw.frame | 148 | 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) { | |||
| 47 | 47 | c, _ := hiddenCapture(t) | |
| 48 | 48 | sc := c.screens[0] | |
| 49 | 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 | 51 | cp, tt := c.captureConfigure(sc, phys, t0) | |
| 52 | 52 | if cp == nil || cp.Hidden { | |
| 53 | 53 | t.Fatalf("no real placement: %+v", cp) | |
| @@ -80,7 +80,7 @@ func TestCaptureConfigureLeavesOtherWindowsAlone(t *testing.T) { | |||
| 80 | 80 | c, _ := hiddenCapture(t) | |
| 81 | 81 | sc := c.screens[0] | |
| 82 | 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 | 84 | if cp, tt := c.captureConfigure(sc, phys, t0); cp != nil || tt != t0 { | |
| 85 | 85 | t.Fatalf("window of another workspace captured: %+v", cp) | |
| 86 | 86 | } | |
| @@ -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" | |||
| 5 | 5 | // configureTarget is where a placement is shown, as the configure needs it. | |
| 6 | 6 | type configureTarget struct { | |
| 7 | 7 | 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 | |
| 10 | 10 | // client is the placement minus borders; imposed reports core sized a | |
| 11 | 11 | // floating window (native floats pick their own size). | |
| 12 | 12 | client Rect | |
| @@ -109,14 +109,14 @@ func build(p Placement, t configureTarget, old ports.ConfigureWindow, ok bool) p | |||
| 109 | 109 | if p.Fullscreen && !v.Fullscreen { | |
| 110 | 110 | // A float made fullscreen off screen: its preview row shows | |
| 111 | 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, false | |
| 112 | + | v.Width, v.Height, v.Fullscreen, v.Floating = t.view.frame.W, t.view.frame.H, true, false | |
| 113 | 113 | } | |
| 114 | 114 | if t.realTiled { | |
| 115 | 115 | 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, false | |
| 117 | + | v.Activated, v.Visible, v.Output, v.Captured = t.focused, t.view.shows(p), t.output, false | |
| 118 | 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 | 120 | if v.Floating && !t.imposed { | |
| 121 | 121 | v.Width, v.Height = 0, 0 | |
| 122 | 122 | } | |
| @@ -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 string | 7 | output string | ||
| 8 | - | area Rect // the output, to tell whether the window is on screen | 8 | + | view viewport // where the window is seen, to tell whether it is |
| 9 | - | focused bool // activated: focused on the focused output | 9 | + | focused bool // activated: focused on the focused output |
| 10 | // client is the placement minus borders; imposed reports core sized a | 10 | // 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 Rect | 12 | 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 shows | 110 | // 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, false | 112 | + | 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.H | 115 | 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, false | 117 | + | 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, 0 | 121 | v.Width, v.Height = 0, 0 | ||
| 122 | } | 122 | } | ||
M internal/core/configures_test.go +6 -6
| @@ -9,7 +9,7 @@ import ( | |||
| 9 | 9 | func TestConfiguresNext(t *testing.T) { | |
| 10 | 10 | area := Rect{W: 1000, H: 800} | |
| 11 | 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 | 13 | tests := []struct { | |
| 14 | 14 | name string | |
| 15 | 15 | first *Placement // sent before p, with target | |
| @@ -22,14 +22,14 @@ func TestConfiguresNext(t *testing.T) { | |||
| 22 | 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 | 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 | 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}, | |
| 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}, | |
| 27 | 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 | 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 | 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}, | |
| 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}, | |
| 33 | 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 | 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 | 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}}, | |
| @@ -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 string | 14 | name string | ||
| 15 | first *Placement // sent before p, with target | 15 | 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 { | |||
| 458 | 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: 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) | |
| 465 | 480 | } | |
| 466 | 481 | ||
| 467 | 482 | // 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 | 485 | // drops the veil at once: a veil that fades changes Scene.Dim every frame, | |
| 471 | 486 | // which redraws the whole output for the length of the fade, where the | |
| 472 | 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 | 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 | 491 | if p.Fullscreen { | |
| 477 | 492 | return 0 | |
| 478 | 493 | } | |
| @@ -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: not | 461 | + | // 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 is | 482 | // 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 the | 486 | // 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 0 | 492 | return 0 | ||
| 478 | } | 493 | } | ||
M internal/core/monitor.go +6 -0
| @@ -451,6 +451,12 @@ func (m *Monitor) Frame() Rect { | |||
| 451 | 451 | 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 | 460 | // frameHas reports whether the output-local point is inside the frame. | |
| 455 | 461 | func (m *Monitor) frameHas(x, y float64) bool { | |
| 456 | 462 | f := m.Frame() | |
| @@ -451,6 +451,12 @@ func (m *Monitor) Frame() Rect { | |||||
| 451 | return m.Current().Output | 451 | 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) { | |||
| 273 | 273 | r := w.columnRectsFor(true)[int(p.ID)-1] | |
| 274 | 274 | 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 | 277 | s.mark(v) | |
| 278 | 278 | before[p.ID] = v | |
| 279 | 279 | } | |
| @@ -283,7 +283,7 @@ func TestOverviewMaximizedPreviewsKeepConfigures(t *testing.T) { | |||
| 283 | 283 | if p.ID < 1 || p.ID > 4 { | |
| 284 | 284 | 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 | 287 | if v.Width != before[p.ID].Width || v.Height != before[p.ID].Height { | |
| 288 | 288 | t.Fatalf("%d resized from %+v to %+v", p.ID, before[p.ID], v) | |
| 289 | 289 | } | |
| @@ -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, false | 274 | 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] = v | 278 | 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 | continue | 284 | 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) { | |||
| 273 | 273 | func TestOverviewStackNoFloatVeil(t *testing.T) { | |
| 274 | 274 | m := stackMonitor() | |
| 275 | 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 | 277 | t.Fatalf("veil %v in the overview", d) | |
| 278 | 278 | } | |
| 279 | 279 | } | |
| @@ -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) { | |||
| 146 | 146 | for _, sc := range c.screens { | |
| 147 | 147 | for _, pl := range sc.shownLayout() { | |
| 148 | 148 | 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 { | |
| 152 | 153 | return nil, Rect{}, false | |
| 153 | 154 | } | |
| 154 | 155 | 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{}, false | 153 | return nil, Rect{}, false | ||
| 153 | } | 154 | } | ||
| 154 | return sc, c.clientRect(pl), true | 155 | return sc, c.clientRect(pl), true | ||
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 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 { | |
| 156 | 156 | n := 0 | |
| 157 | 157 | for _, p := range layout { | |
| 158 | - | if p.Preview == 0 && onScreen(p, area) { | |
| 158 | + | if p.Preview == 0 && v.shows(p) { | |
| 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 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 := 0 | 156 | 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 ( | |||
| 11 | 11 | // drawable reports that it can show the focus pulse on the focused window. | |
| 12 | 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 | 13 | 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() | |
| 17 | 17 | 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 | |
| 20 | 20 | } | |
| 21 | 21 | // layout is what is drawn; settled (same indexes) is where the | |
| 22 | 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 | 41 | if sc.mon.ov.open { | |
| 42 | 42 | // Darken the wallpaper around the previews. | |
| 43 | 43 | 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 | 52 | scene.Separators = append(scene.Separators, overviewOutline(layout, max(c.cfg.Border.Width, 2))...) | |
| 53 | 53 | } else { | |
| 54 | 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 | 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. | |
| 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 | |
| 60 | 59 | for k, p := range layout { | |
| 61 | 60 | ps := settled[k] | |
| 62 | 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 | 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 | 94 | if !p.Hidden && p.Preview == 0 { | |
| 96 | 95 | // Only a sized configure needs the client size. | |
| 97 | 96 | 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 output | 14 | + | // 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 Rect | 17 | 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 = frame | 19 | + | clip = view.frame |
| 20 | } | 20 | } | ||
| 21 | // layout is what is drawn; settled (same indexes) is where the | 21 | // 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.Dim | 43 | 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" | |||
| 18 | 18 | ||
| 19 | 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 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 { | |
| 25 | 25 | if width <= 0 { | |
| 26 | 26 | return nil | |
| 27 | 27 | } | |
| 28 | + | o := v.frame | |
| 28 | 29 | var out []ports.Separator | |
| 29 | 30 | add := func(p *Placement, r Rect, active bool) { | |
| 30 | 31 | // 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 | 53 | p := &ps[i] | |
| 53 | 54 | // A leaving float keeps its own lines while it fades; a leaving | |
| 54 | 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 | 57 | continue | |
| 57 | 58 | } | |
| 58 | 59 | // 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 first | 21 | + | // 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 shows | 22 | + | // 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 nil | 26 | return nil | ||
| 27 | } | 27 | } | ||
| 28 | + | o := v.frame | |||
| 28 | var out []ports.Separator | 29 | 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 onto | 31 | // 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 leaving | 54 | // 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 | continue | 57 | continue | ||
| 57 | } | 58 | } | ||
| 58 | // Tiles sharing a line: Neighbors only counts tiles visible in | 59 | // 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) { | |||
| 66 | 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 | 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 | 70 | for _, c := range tc.check { | |
| 71 | 71 | if got := colorAt(seps, c.x, c.y); got != c.want { | |
| 72 | 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 | 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 | 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 | 84 | t.Fatal("width 0:", s) | |
| 85 | 85 | } | |
| 86 | 86 | full := tiles(0, Rect{W: 16, H: 16}, Rect{X: 16, W: 16, H: 16}) | |
| 87 | 87 | 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 | 89 | t.Fatal("fullscreen:", s) | |
| 90 | 90 | } | |
| 91 | 91 | } | |
| 92 | 92 | ||
| 93 | 93 | func TestSeparatorsFloat(t *testing.T) { | |
| 94 | 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 | 96 | if seps[0].Window != 1 { | |
| 97 | 97 | t.Fatal("float border not tied to its window:", seps) | |
| 98 | 98 | } | |
| @@ -134,7 +134,7 @@ func TestSeparatorsScroll(t *testing.T) { | |||
| 134 | 134 | t.Fatalf("expected visible borders to change with the view: %v then %v", before, after) | |
| 135 | 135 | } | |
| 136 | 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 | 138 | if got := colorAt(seps, 99, 40); got != "" { | |
| 139 | 139 | t.Fatalf("line at the output edge: %v", seps) | |
| 140 | 140 | } | |
| @@ -183,7 +183,7 @@ func TestSeparatorsCascadeBottomPanel(t *testing.T) { | |||
| 183 | 183 | if p := previewOf(t, ps, 1); p.Neighbors != ports.SideRight { | |
| 184 | 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 | 187 | if got := colorAt(seps, 200, 578); got != "" { | |
| 188 | 188 | t.Fatalf("line along the panel: %v", seps) | |
| 189 | 189 | } | |
| @@ -200,7 +200,7 @@ func TestSeparatorsCascadeBottomPanel(t *testing.T) { | |||
| 200 | 200 | func TestSeparatorsGaps(t *testing.T) { | |
| 201 | 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 | 202 | setVisibleNeighbors(ps, 4, bigOutput) | |
| 203 | - | seps := separators(ps, 2, 4, bigOutput, true) | |
| 203 | + | seps := separators(ps, 2, 4, whole(bigOutput), true) | |
| 204 | 204 | for _, c := range []px{{14, 2, "lit"}, {14, 13, "lit"}, {20, 8, "gray"}, {17, 8, ""}} { | |
| 205 | 205 | if got := colorAt(seps, c.x, c.y); got != c.want { | |
| 206 | 206 | 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, true | 87 | 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 { | |||
| 54 | 54 | // Read at once, into publish's scratch: the snapshot holds no | |
| 55 | 55 | // placement. | |
| 56 | 56 | c.realBuf = m.layoutInto(c.realBuf) | |
| 57 | + | view := m.viewport() | |
| 57 | 58 | for _, p := range c.realBuf { | |
| 58 | - | shown[p.ID] = onScreen(p, m.Frame()) | |
| 59 | + | shown[p.ID] = view.shows(p) | |
| 59 | 60 | } | |
| 60 | 61 | for w := range m.all() { | |
| 61 | 62 | 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 no | 54 | // 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) + 1 | 62 | 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 | + | } | |||