detached 1466f78

feat(render): clip tiles to the usable area

Scene.TileClip carries the usable area exclusive layers leave. The
renderer cuts tiles and their lines at it, the overlay plane refuses a
tile it cuts, and core sends the pointer only to the drawn part. A tile
is never drawn over a panel, bottom layer ones included, so a window
the user does not see is the one viewport.shows marks invisible.
commit
1466f785a25f14a20234cdc49ecda4b3c99bcf88
author
brice · · 2026-10-08 13:56 UTC
parent
b264e57
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
13 files · +152 -26
M internal/adapters/drm/overlay.go +5 -0
@@ -153,6 +153,11 @@ func overlayCandidate(s ports.Scene, surfaces map[ports.WindowID]ports.SurfaceCo
153153 // The plane cannot apply the workspace's logical viewport crop.
154154 return ports.SceneWindow{}, ports.SurfaceContent{}, colorBypass, "workspace_clip"
155155 }
156+ if clip := s.TileClip; pick.Tile() && clip != (ports.Rect{}) &&
157+ (pick.Rect.X < clip.X || pick.Rect.Y < clip.Y || pick.Rect.X+pick.Rect.W > clip.X+clip.W || pick.Rect.Y+pick.Rect.H > clip.Y+clip.H) {
158+ // Nor the cut of a tile at a panel.
159+ return ports.SceneWindow{}, ports.SurfaceContent{}, colorBypass, "tile_clip"
160+ }
156161 c := surfaces[pick.ID]
157162 pw, ph := float64(pick.Rect.W)*scale, float64(pick.Rect.H)*scale
158163 if float64(c.Width) != pw || float64(c.Height) != ph || c.Geometry != (ports.Rect{}) && (c.Geometry.X != 0 || c.Geometry.Y != 0) {
@@ -153,6 +153,11 @@ func overlayCandidate(s ports.Scene, surfaces map[ports.WindowID]ports.SurfaceCo
153 // The plane cannot apply the workspace's logical viewport crop.153 // The plane cannot apply the workspace's logical viewport crop.
154 return ports.SceneWindow{}, ports.SurfaceContent{}, colorBypass, "workspace_clip"154 return ports.SceneWindow{}, ports.SurfaceContent{}, colorBypass, "workspace_clip"
155 }155 }
156+ if clip := s.TileClip; pick.Tile() && clip != (ports.Rect{}) &&
157+ (pick.Rect.X < clip.X || pick.Rect.Y < clip.Y || pick.Rect.X+pick.Rect.W > clip.X+clip.W || pick.Rect.Y+pick.Rect.H > clip.Y+clip.H) {
158+ // Nor the cut of a tile at a panel.
159+ return ports.SceneWindow{}, ports.SurfaceContent{}, colorBypass, "tile_clip"
160+ }
156 c := surfaces[pick.ID]161 c := surfaces[pick.ID]
157 pw, ph := float64(pick.Rect.W)*scale, float64(pick.Rect.H)*scale162 pw, ph := float64(pick.Rect.W)*scale, float64(pick.Rect.H)*scale
158 if float64(c.Width) != pw || float64(c.Height) != ph || c.Geometry != (ports.Rect{}) && (c.Geometry.X != 0 || c.Geometry.Y != 0) {163 if float64(c.Width) != pw || float64(c.Height) != ph || c.Geometry != (ports.Rect{}) && (c.Geometry.X != 0 || c.Geometry.Y != 0) {
M internal/adapters/drm/overlay_test.go +14 -0
@@ -63,6 +63,20 @@ func TestOverlayWorkspaceClip(t *testing.T) {
6363 }
6464 }
6565
66+// A tile cut by a panel (TileClip) is composed: the plane would show it
67+// whole, over the panel.
68+func TestOverlayTileClip(t *testing.T) {
69+ s, contents := overlayScene()
70+ s.TileClip = ports.Rect{W: 200, H: 80}
71+ if _, _, _, reason := overlayCandidate(s, contents, false, nil, nil); reason != "tile_clip" {
72+ t.Fatalf("tile under a panel on the overlay: %q", reason)
73+ }
74+ s.TileClip = ports.Rect{W: 200, H: 100}
75+ if win, _, _, reason := overlayCandidate(s, contents, false, nil, nil); reason != "" || win.ID != 2 {
76+ t.Fatalf("tile inside the clip: %v %q", win.ID, reason)
77+ }
78+}
79+
6680 func TestOverlayCandidate(t *testing.T) {
6781 s, c := overlayScene()
6882 if w, _, _, reason := overlayCandidate(s, c, false, nil, nil); reason != "" || w.ID != 2 {
@@ -63,6 +63,20 @@ func TestOverlayWorkspaceClip(t *testing.T) {
63 }63 }
64 }64 }
65 65
66+// A tile cut by a panel (TileClip) is composed: the plane would show it
67+// whole, over the panel.
68+func TestOverlayTileClip(t *testing.T) {
69+ s, contents := overlayScene()
70+ s.TileClip = ports.Rect{W: 200, H: 80}
71+ if _, _, _, reason := overlayCandidate(s, contents, false, nil, nil); reason != "tile_clip" {
72+ t.Fatalf("tile under a panel on the overlay: %q", reason)
73+ }
74+ s.TileClip = ports.Rect{W: 200, H: 100}
75+ if win, _, _, reason := overlayCandidate(s, contents, false, nil, nil); reason != "" || win.ID != 2 {
76+ t.Fatalf("tile inside the clip: %v %q", win.ID, reason)
77+ }
78+}
79+
66 func TestOverlayCandidate(t *testing.T) {80 func TestOverlayCandidate(t *testing.T) {
67 s, c := overlayScene()81 s, c := overlayScene()
68 if w, _, _, reason := overlayCandidate(s, c, false, nil, nil); reason != "" || w.ID != 2 {82 if w, _, _, reason := overlayCandidate(s, c, false, nil, nil); reason != "" || w.ID != 2 {
M internal/adapters/vulkan/damage.go +1 -1
@@ -230,7 +230,7 @@ func sceneDelta(old, cur ports.Scene, phys func(ports.Rect) image.Rectangle, lim
230230 old.OutputWidth != cur.OutputWidth || old.OutputHeight != cur.OutputHeight ||
231231 old.Scale != cur.Scale || old.Transform != cur.Transform || old.Off != cur.Off ||
232232 old.Background != cur.Background || old.Border != cur.Border ||
233- old.WorkspaceClip != cur.WorkspaceClip || old.Dim != cur.Dim || old.DimBehind != cur.DimBehind ||
233+ old.WorkspaceClip != cur.WorkspaceClip || old.TileClip != cur.TileClip || old.Dim != cur.Dim || old.DimBehind != cur.DimBehind ||
234234 old.Capture != nil || cur.Capture != nil || old.CaptureScene != nil || cur.CaptureScene != nil ||
235235 !slices.Equal(old.Layers, cur.Layers) || !slices.Equal(old.DropHints, cur.DropHints) ||
236236 !slices.Equal(old.CaptureIndicators, cur.CaptureIndicators) ||
@@ -230,7 +230,7 @@ func sceneDelta(old, cur ports.Scene, phys func(ports.Rect) image.Rectangle, lim
230 old.OutputWidth != cur.OutputWidth || old.OutputHeight != cur.OutputHeight ||230 old.OutputWidth != cur.OutputWidth || old.OutputHeight != cur.OutputHeight ||
231 old.Scale != cur.Scale || old.Transform != cur.Transform || old.Off != cur.Off ||231 old.Scale != cur.Scale || old.Transform != cur.Transform || old.Off != cur.Off ||
232 old.Background != cur.Background || old.Border != cur.Border ||232 old.Background != cur.Background || old.Border != cur.Border ||
233- old.WorkspaceClip != cur.WorkspaceClip || old.Dim != cur.Dim || old.DimBehind != cur.DimBehind ||233+ old.WorkspaceClip != cur.WorkspaceClip || old.TileClip != cur.TileClip || old.Dim != cur.Dim || old.DimBehind != cur.DimBehind ||
234 old.Capture != nil || cur.Capture != nil || old.CaptureScene != nil || cur.CaptureScene != nil ||234 old.Capture != nil || cur.Capture != nil || old.CaptureScene != nil || cur.CaptureScene != nil ||
235 !slices.Equal(old.Layers, cur.Layers) || !slices.Equal(old.DropHints, cur.DropHints) ||235 !slices.Equal(old.Layers, cur.Layers) || !slices.Equal(old.DropHints, cur.DropHints) ||
236 !slices.Equal(old.CaptureIndicators, cur.CaptureIndicators) ||236 !slices.Equal(old.CaptureIndicators, cur.CaptureIndicators) ||
M internal/adapters/vulkan/damage_delta_test.go +2 -1
@@ -17,7 +17,7 @@ import (
1717 // TestSceneFieldCount fails when Scene gains or loses a field: sceneDelta
1818 // lists the fields that force a full redraw.
1919 func TestSceneFieldCount(t *testing.T) {
20- const fields = 20
20+ const fields = 21
2121 assert.Equal(t, fields, len(reflect.VisibleFields(reflect.TypeOf(ports.Scene{}))),
2222 "Scene fields changed: check sceneDelta (a field it does not compare must force a full redraw or be covered), then this count")
2323 }
@@ -98,6 +98,7 @@ func TestDamageDeltaFullRedraws(t *testing.T) {
9898 "background": func(s *ports.Scene) { s.Background = "#111111" },
9999 "border": func(s *ports.Scene) { s.Border.Width = 2 },
100100 "workspace clip": func(s *ports.Scene) { s.WorkspaceClip = ports.Rect{W: 10, H: 10} },
101+ "tile clip": func(s *ports.Scene) { s.TileClip = ports.Rect{W: 10, H: 10} },
101102 "dim": func(s *ports.Scene) { s.Dim = 0.5 },
102103 "dim behind": func(s *ports.Scene) { s.DimBehind = true },
103104 "drop hints": func(s *ports.Scene) { s.DropHints = []ports.Rect{{W: 5, H: 5}} },
@@ -17,7 +17,7 @@ import (
17 // TestSceneFieldCount fails when Scene gains or loses a field: sceneDelta17 // TestSceneFieldCount fails when Scene gains or loses a field: sceneDelta
18 // lists the fields that force a full redraw.18 // lists the fields that force a full redraw.
19 func TestSceneFieldCount(t *testing.T) {19 func TestSceneFieldCount(t *testing.T) {
20- const fields = 2020+ const fields = 21
21 assert.Equal(t, fields, len(reflect.VisibleFields(reflect.TypeOf(ports.Scene{}))),21 assert.Equal(t, fields, len(reflect.VisibleFields(reflect.TypeOf(ports.Scene{}))),
22 "Scene fields changed: check sceneDelta (a field it does not compare must force a full redraw or be covered), then this count")22 "Scene fields changed: check sceneDelta (a field it does not compare must force a full redraw or be covered), then this count")
23 }23 }
@@ -98,6 +98,7 @@ func TestDamageDeltaFullRedraws(t *testing.T) {
98 "background": func(s *ports.Scene) { s.Background = "#111111" },98 "background": func(s *ports.Scene) { s.Background = "#111111" },
99 "border": func(s *ports.Scene) { s.Border.Width = 2 },99 "border": func(s *ports.Scene) { s.Border.Width = 2 },
100 "workspace clip": func(s *ports.Scene) { s.WorkspaceClip = ports.Rect{W: 10, H: 10} },100 "workspace clip": func(s *ports.Scene) { s.WorkspaceClip = ports.Rect{W: 10, H: 10} },
101+ "tile clip": func(s *ports.Scene) { s.TileClip = ports.Rect{W: 10, H: 10} },
101 "dim": func(s *ports.Scene) { s.Dim = 0.5 },102 "dim": func(s *ports.Scene) { s.Dim = 0.5 },
102 "dim behind": func(s *ports.Scene) { s.DimBehind = true },103 "dim behind": func(s *ports.Scene) { s.DimBehind = true },
103 "drop hints": func(s *ports.Scene) { s.DropHints = []ports.Rect{{W: 5, H: 5}} },104 "drop hints": func(s *ports.Scene) { s.DropHints = []ports.Rect{{W: 5, H: 5}} },
M internal/adapters/vulkan/scene.go +14 -2
@@ -28,7 +28,9 @@ type sceneWalk struct {
2828 // it (premultiplied); 1 otherwise.
2929 alpha float32
3030 bounds image.Rectangle
31- draws []draw
31+ // tileBounds is bounds cut to Scene.TileClip: where tiles draw.
32+ tileBounds image.Rectangle
33+ draws []draw
3234 }
3335
3436 // draws walks the scene into quads in paint order.
@@ -48,6 +50,11 @@ func (r *Renderer) draws(s ports.Scene, contents map[ports.WindowID]ports.Surfac
4850 c := s.WorkspaceClip
4951 w.bounds = w.bounds.Intersect(w.physRect(c.X, c.Y, c.W, c.H))
5052 }
53+ w.tileBounds = w.bounds
54+ if s.TileClip != (ports.Rect{}) {
55+ c := s.TileClip
56+ w.tileBounds = w.bounds.Intersect(w.physRect(c.X, c.Y, c.W, c.H))
57+ }
5158 w.windows()
5259 w.popups(false)
5360 w.dropHints()
@@ -219,6 +226,11 @@ func (w *sceneWalk) windows() {
219226 continue
220227 }
221228 w.alpha = 1 - float32(max(0, win.Fade))
229+ windowBounds := w.bounds
230+ if win.Tile() {
231+ // A tile never draws over a panel (Scene.TileClip).
232+ w.bounds = w.tileBounds
233+ }
222234 // Content sits inside the border; core sized the client to match.
223235 b, inset := 0, ports.Sides(0)
224236 if !win.Fullscreen {
@@ -249,7 +261,7 @@ func (w *sceneWalk) windows() {
249261 // A stashed window peeking in: dimmed with its border.
250262 w.dim(w.physRect(win.Rect.X, win.Rect.Y, win.Rect.W, win.Rect.H), win.Dim)
251263 }
252- w.alpha = 1
264+ w.alpha, w.bounds = 1, windowBounds
253265 }
254266 if !tileLines {
255267 w.separators(0)
@@ -28,7 +28,9 @@ type sceneWalk struct {
28 // it (premultiplied); 1 otherwise.28 // it (premultiplied); 1 otherwise.
29 alpha float3229 alpha float32
30 bounds image.Rectangle30 bounds image.Rectangle
31- draws []draw31+ // tileBounds is bounds cut to Scene.TileClip: where tiles draw.
32+ tileBounds image.Rectangle
33+ draws []draw
32 }34 }
33 35
34 // draws walks the scene into quads in paint order.36 // draws walks the scene into quads in paint order.
@@ -48,6 +50,11 @@ func (r *Renderer) draws(s ports.Scene, contents map[ports.WindowID]ports.Surfac
48 c := s.WorkspaceClip50 c := s.WorkspaceClip
49 w.bounds = w.bounds.Intersect(w.physRect(c.X, c.Y, c.W, c.H))51 w.bounds = w.bounds.Intersect(w.physRect(c.X, c.Y, c.W, c.H))
50 }52 }
53+ w.tileBounds = w.bounds
54+ if s.TileClip != (ports.Rect{}) {
55+ c := s.TileClip
56+ w.tileBounds = w.bounds.Intersect(w.physRect(c.X, c.Y, c.W, c.H))
57+ }
51 w.windows()58 w.windows()
52 w.popups(false)59 w.popups(false)
53 w.dropHints()60 w.dropHints()
@@ -219,6 +226,11 @@ func (w *sceneWalk) windows() {
219 continue226 continue
220 }227 }
221 w.alpha = 1 - float32(max(0, win.Fade))228 w.alpha = 1 - float32(max(0, win.Fade))
229+ windowBounds := w.bounds
230+ if win.Tile() {
231+ // A tile never draws over a panel (Scene.TileClip).
232+ w.bounds = w.tileBounds
233+ }
222 // Content sits inside the border; core sized the client to match.234 // Content sits inside the border; core sized the client to match.
223 b, inset := 0, ports.Sides(0)235 b, inset := 0, ports.Sides(0)
224 if !win.Fullscreen {236 if !win.Fullscreen {
@@ -249,7 +261,7 @@ func (w *sceneWalk) windows() {
249 // A stashed window peeking in: dimmed with its border.261 // A stashed window peeking in: dimmed with its border.
250 w.dim(w.physRect(win.Rect.X, win.Rect.Y, win.Rect.W, win.Rect.H), win.Dim)262 w.dim(w.physRect(win.Rect.X, win.Rect.Y, win.Rect.W, win.Rect.H), win.Dim)
251 }263 }
252- w.alpha = 1264+ w.alpha, w.bounds = 1, windowBounds
253 }265 }
254 if !tileLines {266 if !tileLines {
255 w.separators(0)267 w.separators(0)
M internal/adapters/vulkan/workspace_clip_test.go +35 -0
@@ -8,6 +8,41 @@ import (
88 "github.com/bnema/neferwl/internal/ports"
99 )
1010
11+// A tile is cut at TileClip, so a bottom layer panel under it stays
12+// visible; a float there is drawn over it.
13+func TestTileClipKeepsBottomPanel(t *testing.T) {
14+ r, err := New(64, 48)
15+ if err != nil {
16+ t.Skipf("Vulkan unavailable: %v", err)
17+ }
18+ defer r.Close()
19+ red := color.RGBA{255, 0, 0, 255}
20+ green := color.RGBA{0, 255, 0, 255}
21+ blue := color.RGBA{0, 0, 255, 255}
22+ contents := map[ports.WindowID]ports.SurfaceContent{1: solidContent(t, 64, 48, red), 2: solidContent(t, 64, 8, green), 3: solidContent(t, 8, 8, blue)}
23+ scene := ports.Scene{Seq: 1, OutputWidth: 64, OutputHeight: 48, Background: "#000000", TileClip: ports.Rect{W: 64, H: 40},
24+ Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 64, H: 48}}},
25+ Layers: []ports.SceneLayer{{ID: 2, Rect: ports.Rect{Y: 40, W: 64, H: 8}, Layer: ports.LayerBottom}},
26+ }
27+ if err := render(r, scene, contents); err != nil {
28+ t.Fatal(err)
29+ }
30+ pixels := readPixels(t, r)
31+ for point, want := range map[image.Point]color.RGBA{{10, 10}: red, {10, 39}: red, {10, 40}: green, {60, 47}: green} {
32+ if got := pixels.RGBAAt(point.X, point.Y); got != want {
33+ t.Errorf("pixel %v=%v, want %v", point, got, want)
34+ }
35+ }
36+ scene.Seq++
37+ scene.Windows = []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 64, H: 48}}, {ID: 3, Rect: ports.Rect{Y: 40, W: 8, H: 8}, Floating: true}}
38+ if err := render(r, scene, contents); err != nil {
39+ t.Fatal(err)
40+ }
41+ if got := readPixels(t, r).RGBAAt(2, 44); got != blue {
42+ t.Fatalf("float clipped at the panel: %v", got)
43+ }
44+}
45+
1146 func TestWorkspaceClipWindowsPopupsAndLayers(t *testing.T) {
1247 r, err := New(64, 48)
1348 if err != nil {
@@ -8,6 +8,41 @@ import (
8 "github.com/bnema/neferwl/internal/ports"8 "github.com/bnema/neferwl/internal/ports"
9 )9 )
10 10
11+// A tile is cut at TileClip, so a bottom layer panel under it stays
12+// visible; a float there is drawn over it.
13+func TestTileClipKeepsBottomPanel(t *testing.T) {
14+ r, err := New(64, 48)
15+ if err != nil {
16+ t.Skipf("Vulkan unavailable: %v", err)
17+ }
18+ defer r.Close()
19+ red := color.RGBA{255, 0, 0, 255}
20+ green := color.RGBA{0, 255, 0, 255}
21+ blue := color.RGBA{0, 0, 255, 255}
22+ contents := map[ports.WindowID]ports.SurfaceContent{1: solidContent(t, 64, 48, red), 2: solidContent(t, 64, 8, green), 3: solidContent(t, 8, 8, blue)}
23+ scene := ports.Scene{Seq: 1, OutputWidth: 64, OutputHeight: 48, Background: "#000000", TileClip: ports.Rect{W: 64, H: 40},
24+ Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 64, H: 48}}},
25+ Layers: []ports.SceneLayer{{ID: 2, Rect: ports.Rect{Y: 40, W: 64, H: 8}, Layer: ports.LayerBottom}},
26+ }
27+ if err := render(r, scene, contents); err != nil {
28+ t.Fatal(err)
29+ }
30+ pixels := readPixels(t, r)
31+ for point, want := range map[image.Point]color.RGBA{{10, 10}: red, {10, 39}: red, {10, 40}: green, {60, 47}: green} {
32+ if got := pixels.RGBAAt(point.X, point.Y); got != want {
33+ t.Errorf("pixel %v=%v, want %v", point, got, want)
34+ }
35+ }
36+ scene.Seq++
37+ scene.Windows = []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 64, H: 48}}, {ID: 3, Rect: ports.Rect{Y: 40, W: 8, H: 8}, Floating: true}}
38+ if err := render(r, scene, contents); err != nil {
39+ t.Fatal(err)
40+ }
41+ if got := readPixels(t, r).RGBAAt(2, 44); got != blue {
42+ t.Fatalf("float clipped at the panel: %v", got)
43+ }
44+}
45+
11 func TestWorkspaceClipWindowsPopupsAndLayers(t *testing.T) {46 func TestWorkspaceClipWindowsPopupsAndLayers(t *testing.T) {
12 r, err := New(64, 48)47 r, err := New(64, 48)
13 if err != nil {48 if err != nil {
M internal/core/core.go +17 -12
@@ -465,18 +465,21 @@ type viewport struct{ frame, usable Rect }
465465 // whole is a viewport with no panel: all of r is usable.
466466 func whole(r Rect) viewport { return viewport{frame: r, usable: r} }
467467
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- }
468+// area is where p can be seen, drawn and pointed at. A tile lives in the
469+// usable area and is cut at its edge (Scene.TileClip), so a cascade band
470+// or a column scrolled under a panel is not seen; a float, a fullscreen
471+// window and an overview preview are placed against the frame.
472+func (v viewport) area(p Placement) Rect {
476473 if p.Floating || p.Fullscreen || p.Preview > 0 {
477- return p.Rect.Overlaps(v.frame)
474+ return v.frame
478475 }
479- return p.Rect.Overlaps(v.usable)
476+ return v.usable
477+}
478+
479+// shows reports whether the user sees p: not hidden, and overlapping its
480+// area.
481+func (v viewport) shows(p Placement) bool {
482+ return !p.Hidden && p.Rect.Overlaps(v.area(p))
480483 }
481484
482485 // floatDim is the veil opacity of a layout: dim only when a float is
@@ -824,8 +827,7 @@ func (c *Core) shownClient(id WindowID) (full, visible Rect, ok bool) {
824827 }
825828 r := c.clientRect(p)
826829 full = Rect{X: r.X + s.x, Y: r.Y + s.y, W: r.W, H: r.H}
827- f := s.mon.Frame()
828- visible = intersect(r, f)
830+ visible = intersect(r, s.mon.viewport().area(p))
829831 visible.X, visible.Y = visible.X+s.x, visible.Y+s.y
830832 if visible.W <= 0 || visible.H <= 0 {
831833 return Rect{}, Rect{}, false
@@ -971,6 +973,7 @@ func (c *Core) hit(x, y float64) (WindowID, float64, float64) {
971973 }
972974 var id WindowID
973975 var sx, sy float64
976+ view := sc.mon.viewport()
974977 for _, p := range sc.shownLayout() {
975978 r := c.clientRect(p)
976979 // A peek is clickable wherever it shows, border included: it may
@@ -979,6 +982,8 @@ func (c *Core) hit(x, y float64) (WindowID, float64, float64) {
979982 if p.Peek {
980983 box = p.Rect
981984 }
985+ // Only what is drawn takes the pointer: a tile not over a panel.
986+ box = intersect(box, view.area(p))
982987 if !p.Hidden && r.W > 0 && r.H > 0 && lx >= float64(box.X) && lx < float64(box.X+box.W) && ly >= float64(box.Y) && ly < float64(box.Y+box.H) {
983988 if p.Peek {
984989 lx, ly = min(max(lx, float64(r.X)), float64(r.X+r.W-1)), min(max(ly, float64(r.Y)), float64(r.Y+r.H-1))
@@ -465,18 +465,21 @@ type viewport struct{ frame, usable Rect }
465 // whole is a viewport with no panel: all of r is usable.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} }466 func whole(r Rect) viewport { return viewport{frame: r, usable: r} }
467 467
468-// shows reports whether the user sees p: not hidden, and overlapping the468+// area is where p can be seen, drawn and pointed at. A tile lives in the
469-// area its kind is laid out in. A tile lives in the usable area, so a469+// usable area and is cut at its edge (Scene.TileClip), so a cascade band
470-// cascade band or a column scrolled under a panel is not seen; a float, a470+// or a column scrolled under a panel is not seen; a float, a fullscreen
471-// fullscreen window and an overview preview are placed against the frame.471+// window and an overview preview are placed against the frame.
472-func (v viewport) shows(p Placement) bool {472+func (v viewport) area(p Placement) Rect {
473- if p.Hidden {
474- return false
475- }
476 if p.Floating || p.Fullscreen || p.Preview > 0 {473 if p.Floating || p.Fullscreen || p.Preview > 0 {
477- return p.Rect.Overlaps(v.frame)474+ return v.frame
478 }475 }
479- return p.Rect.Overlaps(v.usable)476+ return v.usable
477+}
478+
479+// shows reports whether the user sees p: not hidden, and overlapping its
480+// area.
481+func (v viewport) shows(p Placement) bool {
482+ return !p.Hidden && p.Rect.Overlaps(v.area(p))
480 }483 }
481 484
482 // floatDim is the veil opacity of a layout: dim only when a float is485 // floatDim is the veil opacity of a layout: dim only when a float is
@@ -824,8 +827,7 @@ func (c *Core) shownClient(id WindowID) (full, visible Rect, ok bool) {
824 }827 }
825 r := c.clientRect(p)828 r := c.clientRect(p)
826 full = Rect{X: r.X + s.x, Y: r.Y + s.y, W: r.W, H: r.H}829 full = Rect{X: r.X + s.x, Y: r.Y + s.y, W: r.W, H: r.H}
827- f := s.mon.Frame()830+ visible = intersect(r, s.mon.viewport().area(p))
828- visible = intersect(r, f)
829 visible.X, visible.Y = visible.X+s.x, visible.Y+s.y831 visible.X, visible.Y = visible.X+s.x, visible.Y+s.y
830 if visible.W <= 0 || visible.H <= 0 {832 if visible.W <= 0 || visible.H <= 0 {
831 return Rect{}, Rect{}, false833 return Rect{}, Rect{}, false
@@ -971,6 +973,7 @@ func (c *Core) hit(x, y float64) (WindowID, float64, float64) {
971 }973 }
972 var id WindowID974 var id WindowID
973 var sx, sy float64975 var sx, sy float64
976+ view := sc.mon.viewport()
974 for _, p := range sc.shownLayout() {977 for _, p := range sc.shownLayout() {
975 r := c.clientRect(p)978 r := c.clientRect(p)
976 // A peek is clickable wherever it shows, border included: it may979 // A peek is clickable wherever it shows, border included: it may
@@ -979,6 +982,8 @@ func (c *Core) hit(x, y float64) (WindowID, float64, float64) {
979 if p.Peek {982 if p.Peek {
980 box = p.Rect983 box = p.Rect
981 }984 }
985+ // Only what is drawn takes the pointer: a tile not over a panel.
986+ box = intersect(box, view.area(p))
982 if !p.Hidden && r.W > 0 && r.H > 0 && lx >= float64(box.X) && lx < float64(box.X+box.W) && ly >= float64(box.Y) && ly < float64(box.Y+box.H) {987 if !p.Hidden && r.W > 0 && r.H > 0 && lx >= float64(box.X) && lx < float64(box.X+box.W) && ly >= float64(box.Y) && ly < float64(box.Y+box.H) {
983 if p.Peek {988 if p.Peek {
984 lx, ly = min(max(lx, float64(r.X)), float64(r.X+r.W-1)), min(max(ly, float64(r.Y)), float64(r.Y+r.H-1))989 lx, ly = min(max(lx, float64(r.X)), float64(r.X+r.W-1)), min(max(ly, float64(r.Y)), float64(r.Y+r.H-1))
M internal/core/scene.go +5 -0
@@ -22,6 +22,11 @@ func (c *Core) sceneFor(ctx context.Context, i int, sc *screen, only *screen, ca
2222 // windows are going, and alone sizes the configures.
2323 layout, settled := sc.shown, sc.settledLayout
2424 scene = ports.Scene{Security: c.security, Output: sc.name(), OutputWidth: o.W, OutputHeight: o.H, WorkspaceClip: clip, Scale: sc.scale, Transform: sc.transform, Off: sc.off, Background: c.cfg.Background.Color, Border: ports.Border{Width: c.cfg.Border.Width, Active: c.cfg.Border.Active, Inactive: c.cfg.Border.Inactive}, Windows: make([]ports.SceneWindow, 0, len(layout)+len(c.popupOrder)), Layers: shownLayers(sc)}
25+ if !sc.mon.ov.open && view.usable != view.frame {
26+ // Tiles stop at the panels; the overview lays out previews over
27+ // the whole output.
28+ scene.TileClip = view.usable
29+ }
2530 var real map[WindowID]Placement
2631 if sc.mon.ov.open {
2732 // Local to this screen's build: reused, cleared each time.
@@ -22,6 +22,11 @@ func (c *Core) sceneFor(ctx context.Context, i int, sc *screen, only *screen, ca
22 // windows are going, and alone sizes the configures.22 // windows are going, and alone sizes the configures.
23 layout, settled := sc.shown, sc.settledLayout23 layout, settled := sc.shown, sc.settledLayout
24 scene = ports.Scene{Security: c.security, Output: sc.name(), OutputWidth: o.W, OutputHeight: o.H, WorkspaceClip: clip, Scale: sc.scale, Transform: sc.transform, Off: sc.off, Background: c.cfg.Background.Color, Border: ports.Border{Width: c.cfg.Border.Width, Active: c.cfg.Border.Active, Inactive: c.cfg.Border.Inactive}, Windows: make([]ports.SceneWindow, 0, len(layout)+len(c.popupOrder)), Layers: shownLayers(sc)}24 scene = ports.Scene{Security: c.security, Output: sc.name(), OutputWidth: o.W, OutputHeight: o.H, WorkspaceClip: clip, Scale: sc.scale, Transform: sc.transform, Off: sc.off, Background: c.cfg.Background.Color, Border: ports.Border{Width: c.cfg.Border.Width, Active: c.cfg.Border.Active, Inactive: c.cfg.Border.Inactive}, Windows: make([]ports.SceneWindow, 0, len(layout)+len(c.popupOrder)), Layers: shownLayers(sc)}
25+ if !sc.mon.ov.open && view.usable != view.frame {
26+ // Tiles stop at the panels; the overview lays out previews over
27+ // the whole output.
28+ scene.TileClip = view.usable
29+ }
25 var real map[WindowID]Placement30 var real map[WindowID]Placement
26 if sc.mon.ov.open {31 if sc.mon.ov.open {
27 // Local to this screen's build: reused, cleared each time.32 // Local to this screen's build: reused, cleared each time.
M internal/core/separators.go +7 -5
@@ -18,9 +18,9 @@ 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 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).
21+// separators are the lines of the windows seen in v, each clipped to its
22+// window's area, inactive ones first so active ones draw over them. lit
23+// says whether the focused tile shows focus (only on the focused output).
2424 func separators(ps []Placement, width, gap int, v viewport, lit bool) []ports.Separator {
2525 if width <= 0 {
2626 return nil
@@ -28,8 +28,10 @@ func separators(ps []Placement, width, gap int, v viewport, lit bool) []ports.Se
2828 o := v.frame
2929 var out []ports.Separator
3030 add := func(p *Placement, r Rect, active bool) {
31- // A line cannot belong to a scrolled-off window or bleed onto
32- // another output, even when a neighboring tile remains visible.
31+ // A line cannot belong to a scrolled-off window, bleed onto
32+ // another output or, for a tile, over a panel, even when a
33+ // neighboring tile remains visible.
34+ o := v.area(*p)
3335 x0, y0 := max(r.X, o.X), max(r.Y, o.Y)
3436 x1, y1 := min(r.X+r.W, o.X+o.W), min(r.Y+r.H, o.Y+o.H)
3537 if x0 >= x1 || y0 >= y1 {
@@ -18,9 +18,9 @@ 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 the windows seen in v, clipped to its frame,21+// separators are the lines of the windows seen in v, each clipped to its
22-// inactive ones first so active ones draw over them. lit says whether the22+// window's area, inactive ones first so active ones draw over them. lit
23-// focused tile shows focus (only on the focused output).23+// says whether the focused tile shows focus (only on the focused output).
24 func separators(ps []Placement, width, gap int, v viewport, 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
@@ -28,8 +28,10 @@ func separators(ps []Placement, width, gap int, v viewport, lit bool) []ports.Se
28 o := v.frame28 o := v.frame
29 var out []ports.Separator29 var out []ports.Separator
30 add := func(p *Placement, r Rect, active bool) {30 add := func(p *Placement, r Rect, active bool) {
31- // A line cannot belong to a scrolled-off window or bleed onto31+ // A line cannot belong to a scrolled-off window, bleed onto
32- // another output, even when a neighboring tile remains visible.32+ // another output or, for a tile, over a panel, even when a
33+ // neighboring tile remains visible.
34+ o := v.area(*p)
33 x0, y0 := max(r.X, o.X), max(r.Y, o.Y)35 x0, y0 := max(r.X, o.X), max(r.Y, o.Y)
34 x1, y1 := min(r.X+r.W, o.X+o.W), min(r.Y+r.H, o.Y+o.H)36 x1, y1 := min(r.X+r.W, o.X+o.W), min(r.Y+r.H, o.Y+o.H)
35 if x0 >= x1 || y0 >= y1 {37 if x0 >= x1 || y0 >= y1 {
M internal/core/viewport_test.go +10 -0
@@ -103,6 +103,16 @@ func TestConfigureVisibleCascadeUnderPanel(t *testing.T) {
103103 if !found {
104104 t.Fatal("window under the panel left the scene")
105105 }
106+ // It is cut at the panel and takes no pointer there.
107+ if scene.TileClip != (Rect{W: 100, H: 60}) {
108+ t.Fatalf("tile clip %+v, want the usable area", scene.TileClip)
109+ }
110+ if id, _, _ := c.hit(10, 70); id != 0 {
111+ t.Fatalf("pointer over the panel went to window %d", id)
112+ }
113+ if id, _, _ := c.hit(10, 30); id != 1 {
114+ t.Fatalf("pointer over window 1 went to %d", id)
115+ }
106116 // Its band scrolled in, the first band is under nothing but off screen.
107117 w.FocusID(3)
108118 check(map[WindowID]bool{1: false, 2: false, 3: true})
@@ -103,6 +103,16 @@ func TestConfigureVisibleCascadeUnderPanel(t *testing.T) {
103 if !found {103 if !found {
104 t.Fatal("window under the panel left the scene")104 t.Fatal("window under the panel left the scene")
105 }105 }
106+ // It is cut at the panel and takes no pointer there.
107+ if scene.TileClip != (Rect{W: 100, H: 60}) {
108+ t.Fatalf("tile clip %+v, want the usable area", scene.TileClip)
109+ }
110+ if id, _, _ := c.hit(10, 70); id != 0 {
111+ t.Fatalf("pointer over the panel went to window %d", id)
112+ }
113+ if id, _, _ := c.hit(10, 30); id != 1 {
114+ t.Fatalf("pointer over window 1 went to %d", id)
115+ }
106 // Its band scrolled in, the first band is under nothing but off screen.116 // Its band scrolled in, the first band is under nothing but off screen.
107 w.FocusID(3)117 w.FocusID(3)
108 check(map[WindowID]bool{1: false, 2: false, 3: true})118 check(map[WindowID]bool{1: false, 2: false, 3: true})
M internal/ports/scene.go +17 -4
@@ -27,6 +27,11 @@ type Scene struct {
2727 // in logical output coordinates. Zero inherits the full output. Layers
2828 // remain output-wide; overview previews do not use this clip.
2929 WorkspaceClip Rect
30+ // TileClip bounds tiles (SceneWindow.Tile) and their lines inside
31+ // WorkspaceClip: the usable area exclusive layers leave, so a tile is
32+ // never drawn over a panel, whatever its layer. Zero does not clip
33+ // (overview, capture scenes).
34+ TileClip Rect
3035 // Dim darkens the background, bottom layers and tiles (tile lines
3136 // included) inside WorkspaceClip when set, with black at this opacity,
3237 // 0 to 1, under the first
@@ -74,7 +79,7 @@ func (s Scene) SameAs(o Scene) bool {
7479 s.OutputWidth != o.OutputWidth || s.OutputHeight != o.OutputHeight ||
7580 s.Scale != o.Scale || s.Transform != o.Transform || s.Off != o.Off ||
7681 s.Background != o.Background || s.Border != o.Border ||
77- s.WorkspaceClip != o.WorkspaceClip || s.Dim != o.Dim || s.DimBehind != o.DimBehind {
82+ s.WorkspaceClip != o.WorkspaceClip || s.TileClip != o.TileClip || s.Dim != o.Dim || s.DimBehind != o.DimBehind {
7883 return false
7984 }
8085 if s.CaptureScene != nil || o.CaptureScene != nil {
@@ -97,12 +102,13 @@ func (s Scene) SameAs(o Scene) bool {
97102
98103 // Shows reports whether the scene draws the surface of id: only its
99104 // content changes need a new frame. A window scrolled off the output is
100-// not drawn. WorkspaceClip also excludes off-viewport windows; OverLayers
101-// popups remain output-wide.
105+// not drawn. WorkspaceClip also excludes off-viewport windows, TileClip
106+// tiles under a panel; OverLayers popups remain output-wide.
102107 func (s Scene) Shows(id WindowID) bool {
103108 for _, w := range s.Windows {
104109 if w.ID == id && !w.Hidden && w.Rect.Overlaps(Rect{W: s.OutputWidth, H: s.OutputHeight}) &&
105- (w.OverLayers || s.WorkspaceClip == (Rect{}) || w.Rect.Overlaps(s.WorkspaceClip)) {
110+ (w.OverLayers || s.WorkspaceClip == (Rect{}) || w.Rect.Overlaps(s.WorkspaceClip)) &&
111+ (!w.Tile() || s.TileClip == (Rect{}) || w.Rect.Overlaps(s.TileClip)) {
106112 return true
107113 }
108114 }
@@ -180,3 +186,10 @@ type SceneWindow struct {
180186 // overlay layers. Window popups stay with the windows, under them.
181187 OverLayers bool
182188 }
189+
190+// Tile reports whether w is a tile: laid out in the usable area and
191+// clipped to Scene.TileClip. Floats, fullscreen windows, overview previews
192+// and popups are placed against the whole viewport.
193+func (w SceneWindow) Tile() bool {
194+ return !w.Floating && !w.Fullscreen && !w.Popup && w.Preview == 0
195+}
@@ -27,6 +27,11 @@ type Scene struct {
27 // in logical output coordinates. Zero inherits the full output. Layers27 // in logical output coordinates. Zero inherits the full output. Layers
28 // remain output-wide; overview previews do not use this clip.28 // remain output-wide; overview previews do not use this clip.
29 WorkspaceClip Rect29 WorkspaceClip Rect
30+ // TileClip bounds tiles (SceneWindow.Tile) and their lines inside
31+ // WorkspaceClip: the usable area exclusive layers leave, so a tile is
32+ // never drawn over a panel, whatever its layer. Zero does not clip
33+ // (overview, capture scenes).
34+ TileClip Rect
30 // Dim darkens the background, bottom layers and tiles (tile lines35 // Dim darkens the background, bottom layers and tiles (tile lines
31 // included) inside WorkspaceClip when set, with black at this opacity,36 // included) inside WorkspaceClip when set, with black at this opacity,
32 // 0 to 1, under the first37 // 0 to 1, under the first
@@ -74,7 +79,7 @@ func (s Scene) SameAs(o Scene) bool {
74 s.OutputWidth != o.OutputWidth || s.OutputHeight != o.OutputHeight ||79 s.OutputWidth != o.OutputWidth || s.OutputHeight != o.OutputHeight ||
75 s.Scale != o.Scale || s.Transform != o.Transform || s.Off != o.Off ||80 s.Scale != o.Scale || s.Transform != o.Transform || s.Off != o.Off ||
76 s.Background != o.Background || s.Border != o.Border ||81 s.Background != o.Background || s.Border != o.Border ||
77- s.WorkspaceClip != o.WorkspaceClip || s.Dim != o.Dim || s.DimBehind != o.DimBehind {82+ s.WorkspaceClip != o.WorkspaceClip || s.TileClip != o.TileClip || s.Dim != o.Dim || s.DimBehind != o.DimBehind {
78 return false83 return false
79 }84 }
80 if s.CaptureScene != nil || o.CaptureScene != nil {85 if s.CaptureScene != nil || o.CaptureScene != nil {
@@ -97,12 +102,13 @@ func (s Scene) SameAs(o Scene) bool {
97 102
98 // Shows reports whether the scene draws the surface of id: only its103 // Shows reports whether the scene draws the surface of id: only its
99 // content changes need a new frame. A window scrolled off the output is104 // content changes need a new frame. A window scrolled off the output is
100-// not drawn. WorkspaceClip also excludes off-viewport windows; OverLayers105+// not drawn. WorkspaceClip also excludes off-viewport windows, TileClip
101-// popups remain output-wide.106+// tiles under a panel; OverLayers popups remain output-wide.
102 func (s Scene) Shows(id WindowID) bool {107 func (s Scene) Shows(id WindowID) bool {
103 for _, w := range s.Windows {108 for _, w := range s.Windows {
104 if w.ID == id && !w.Hidden && w.Rect.Overlaps(Rect{W: s.OutputWidth, H: s.OutputHeight}) &&109 if w.ID == id && !w.Hidden && w.Rect.Overlaps(Rect{W: s.OutputWidth, H: s.OutputHeight}) &&
105- (w.OverLayers || s.WorkspaceClip == (Rect{}) || w.Rect.Overlaps(s.WorkspaceClip)) {110+ (w.OverLayers || s.WorkspaceClip == (Rect{}) || w.Rect.Overlaps(s.WorkspaceClip)) &&
111+ (!w.Tile() || s.TileClip == (Rect{}) || w.Rect.Overlaps(s.TileClip)) {
106 return true112 return true
107 }113 }
108 }114 }
@@ -180,3 +186,10 @@ type SceneWindow struct {
180 // overlay layers. Window popups stay with the windows, under them.186 // overlay layers. Window popups stay with the windows, under them.
181 OverLayers bool187 OverLayers bool
182 }188 }
189+
190+// Tile reports whether w is a tile: laid out in the usable area and
191+// clipped to Scene.TileClip. Floats, fullscreen windows, overview previews
192+// and popups are placed against the whole viewport.
193+func (w SceneWindow) Tile() bool {
194+ return !w.Floating && !w.Fullscreen && !w.Popup && w.Preview == 0
195+}
M internal/ports/scene_same_test.go +2 -1
@@ -48,6 +48,7 @@ func TestSceneSameAs(t *testing.T) {
4848 "scale": func(s *Scene) { s.Scale = 2 },
4949 "off": func(s *Scene) { s.Off = true },
5050 "clip": func(s *Scene) { s.WorkspaceClip.W = 1 },
51+ "tile clip": func(s *Scene) { s.TileClip.H = 1 },
5152 "dim": func(s *Scene) { s.Dim = 0.5 },
5253 "dim behind": func(s *Scene) { s.DimBehind = true },
5354 "separator": func(s *Scene) { s.Separators[0].Active = true },
@@ -82,7 +83,7 @@ func TestSceneSameAs(t *testing.T) {
8283 // TestSceneSameAsCoversEveryField fails when Scene gains or loses a field:
8384 // update Scene.SameAs, then this count.
8485 func TestSceneSameAsCoversEveryField(t *testing.T) {
85- const fields = 20
86+ const fields = 21
8687 assert.Equal(t, fields, len(reflect.VisibleFields(reflect.TypeOf(Scene{}))),
8788 "Scene fields changed: update Scene.SameAs (and TestSceneSameAs), then this count")
8889 }
@@ -48,6 +48,7 @@ func TestSceneSameAs(t *testing.T) {
48 "scale": func(s *Scene) { s.Scale = 2 },48 "scale": func(s *Scene) { s.Scale = 2 },
49 "off": func(s *Scene) { s.Off = true },49 "off": func(s *Scene) { s.Off = true },
50 "clip": func(s *Scene) { s.WorkspaceClip.W = 1 },50 "clip": func(s *Scene) { s.WorkspaceClip.W = 1 },
51+ "tile clip": func(s *Scene) { s.TileClip.H = 1 },
51 "dim": func(s *Scene) { s.Dim = 0.5 },52 "dim": func(s *Scene) { s.Dim = 0.5 },
52 "dim behind": func(s *Scene) { s.DimBehind = true },53 "dim behind": func(s *Scene) { s.DimBehind = true },
53 "separator": func(s *Scene) { s.Separators[0].Active = true },54 "separator": func(s *Scene) { s.Separators[0].Active = true },
@@ -82,7 +83,7 @@ func TestSceneSameAs(t *testing.T) {
82 // TestSceneSameAsCoversEveryField fails when Scene gains or loses a field:83 // TestSceneSameAsCoversEveryField fails when Scene gains or loses a field:
83 // update Scene.SameAs, then this count.84 // update Scene.SameAs, then this count.
84 func TestSceneSameAsCoversEveryField(t *testing.T) {85 func TestSceneSameAsCoversEveryField(t *testing.T) {
85- const fields = 2086+ const fields = 21
86 assert.Equal(t, fields, len(reflect.VisibleFields(reflect.TypeOf(Scene{}))),87 assert.Equal(t, fields, len(reflect.VisibleFields(reflect.TypeOf(Scene{}))),
87 "Scene fields changed: update Scene.SameAs (and TestSceneSameAs), then this count")88 "Scene fields changed: update Scene.SameAs (and TestSceneSameAs), then this count")
88 }89 }
M internal/ports/workspace_clip_test.go +23 -0
@@ -16,3 +16,26 @@ func TestSceneShowsWorkspaceClip(t *testing.T) {
1616 t.Fatal("inherited workspace excluded surface")
1717 }
1818 }
19+
20+// A tile under a panel (outside TileClip) is not drawn; a float, a
21+// fullscreen window, a preview or a popup there still is.
22+func TestSceneShowsTileClip(t *testing.T) {
23+ under := Rect{Y: 80, W: 50, H: 80}
24+ s := Scene{OutputWidth: 100, OutputHeight: 100, TileClip: Rect{W: 100, H: 80}, Windows: []SceneWindow{
25+ {ID: 1, Rect: Rect{W: 50, H: 80}},
26+ {ID: 2, Rect: under},
27+ {ID: 3, Rect: under, Floating: true},
28+ {ID: 4, Rect: under, Fullscreen: true},
29+ {ID: 5, Rect: under, Preview: 0.5},
30+ {ID: 6, Rect: under, Popup: true},
31+ }}
32+ for id, want := range map[WindowID]bool{1: true, 2: false, 3: true, 4: true, 5: true, 6: true} {
33+ if got := s.Shows(id); got != want {
34+ t.Errorf("window %d shown %v, want %v", id, got, want)
35+ }
36+ }
37+ s.TileClip = Rect{}
38+ if !s.Shows(2) {
39+ t.Fatal("tile under no clip not shown")
40+ }
41+}
@@ -16,3 +16,26 @@ func TestSceneShowsWorkspaceClip(t *testing.T) {
16 t.Fatal("inherited workspace excluded surface")16 t.Fatal("inherited workspace excluded surface")
17 }17 }
18 }18 }
19+
20+// A tile under a panel (outside TileClip) is not drawn; a float, a
21+// fullscreen window, a preview or a popup there still is.
22+func TestSceneShowsTileClip(t *testing.T) {
23+ under := Rect{Y: 80, W: 50, H: 80}
24+ s := Scene{OutputWidth: 100, OutputHeight: 100, TileClip: Rect{W: 100, H: 80}, Windows: []SceneWindow{
25+ {ID: 1, Rect: Rect{W: 50, H: 80}},
26+ {ID: 2, Rect: under},
27+ {ID: 3, Rect: under, Floating: true},
28+ {ID: 4, Rect: under, Fullscreen: true},
29+ {ID: 5, Rect: under, Preview: 0.5},
30+ {ID: 6, Rect: under, Popup: true},
31+ }}
32+ for id, want := range map[WindowID]bool{1: true, 2: false, 3: true, 4: true, 5: true, 6: true} {
33+ if got := s.Shows(id); got != want {
34+ t.Errorf("window %d shown %v, want %v", id, got, want)
35+ }
36+ }
37+ s.TileClip = Rect{}
38+ if !s.Shows(2) {
39+ t.Fatal("tile under no clip not shown")
40+ }
41+}

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