refactor(render): carry panel insets as Scene.TileInset
TileInset replaces TileClip: zero means no panel, and insets covering the output mean no room for tiles, so core no longer hides tiles by hand. Scene.Draws and Scene.TileArea own the rule. The renderer cuts tile lines and damage at the tile area too, the overlay plane skips a tile wholly under a panel instead of picking it, and Rect.Contains replaces two copies of the containment check. The focus pulse needs room to be drawn.
- commit
- 50771574b1d96598c25abe70e2c7e25bb14ddfd7
- author
- brice · · 2026-10-08 14:49 UTC
- parent
- e06b149
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
13 files · +179 -70
M internal/adapters/drm/overlay.go +7 -4
| @@ -79,6 +79,11 @@ func overlayCandidate(s ports.Scene, surfaces map[ports.WindowID]ports.SurfaceCo | |||
| 79 | 79 | if w.Hidden || w.Rect.W <= 0 || w.Rect.H <= 0 { | |
| 80 | 80 | continue | |
| 81 | 81 | } | |
| 82 | + | if a, ok := s.TileArea(); ok && w.Tile() && !w.Rect.Overlaps(a) { | |
| 83 | + | // A tile wholly under a panel is not drawn: neither a | |
| 84 | + | // candidate nor over one. | |
| 85 | + | continue | |
| 86 | + | } | |
| 82 | 87 | if w.Popup { | |
| 83 | 88 | // Popups draw after every window, wherever they are listed: | |
| 84 | 89 | // checked once the candidate is known. | |
| @@ -148,13 +153,11 @@ func overlayCandidate(s ports.Scene, surfaces map[ports.WindowID]ports.SurfaceCo | |||
| 148 | 153 | } | |
| 149 | 154 | } | |
| 150 | 155 | } | |
| 151 | - | if clip := s.WorkspaceClip; clip != (ports.Rect{}) && | |
| 152 | - | (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) { | |
| 156 | + | if clip := s.WorkspaceClip; clip != (ports.Rect{}) && !clip.Contains(pick.Rect) { | |
| 153 | 157 | // The plane cannot apply the workspace's logical viewport crop. | |
| 154 | 158 | return ports.SceneWindow{}, ports.SurfaceContent{}, colorBypass, "workspace_clip" | |
| 155 | 159 | } | |
| 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) { | |
| 160 | + | if a, ok := s.TileArea(); ok && pick.Tile() && !a.Contains(pick.Rect) { | |
| 158 | 161 | // Nor the cut of a tile at a panel. | |
| 159 | 162 | return ports.SceneWindow{}, ports.SurfaceContent{}, colorBypass, "tile_clip" | |
| 160 | 163 | } | |
| @@ -79,6 +79,11 @@ func overlayCandidate(s ports.Scene, surfaces map[ports.WindowID]ports.SurfaceCo | |||||
| 79 | if w.Hidden || w.Rect.W <= 0 || w.Rect.H <= 0 { | 79 | if w.Hidden || w.Rect.W <= 0 || w.Rect.H <= 0 { | ||
| 80 | continue | 80 | continue | ||
| 81 | } | 81 | } | ||
| 82 | + | if a, ok := s.TileArea(); ok && w.Tile() && !w.Rect.Overlaps(a) { | |||
| 83 | + | // A tile wholly under a panel is not drawn: neither a | |||
| 84 | + | // candidate nor over one. | |||
| 85 | + | continue | |||
| 86 | + | } | |||
| 82 | if w.Popup { | 87 | if w.Popup { | ||
| 83 | // Popups draw after every window, wherever they are listed: | 88 | // Popups draw after every window, wherever they are listed: | ||
| 84 | // checked once the candidate is known. | 89 | // checked once the candidate is known. | ||
| @@ -148,13 +153,11 @@ func overlayCandidate(s ports.Scene, surfaces map[ports.WindowID]ports.SurfaceCo | |||||
| 148 | } | 153 | } | ||
| 149 | } | 154 | } | ||
| 150 | } | 155 | } | ||
| 151 | - | if clip := s.WorkspaceClip; clip != (ports.Rect{}) && | 156 | + | if clip := s.WorkspaceClip; clip != (ports.Rect{}) && !clip.Contains(pick.Rect) { |
| 152 | - | (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) { | |||
| 153 | // The plane cannot apply the workspace's logical viewport crop. | 157 | // The plane cannot apply the workspace's logical viewport crop. | ||
| 154 | return ports.SceneWindow{}, ports.SurfaceContent{}, colorBypass, "workspace_clip" | 158 | return ports.SceneWindow{}, ports.SurfaceContent{}, colorBypass, "workspace_clip" | ||
| 155 | } | 159 | } | ||
| 156 | - | if clip := s.TileClip; pick.Tile() && clip != (ports.Rect{}) && | 160 | + | if a, ok := s.TileArea(); ok && pick.Tile() && !a.Contains(pick.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. | 161 | // Nor the cut of a tile at a panel. | ||
| 159 | return ports.SceneWindow{}, ports.SurfaceContent{}, colorBypass, "tile_clip" | 162 | return ports.SceneWindow{}, ports.SurfaceContent{}, colorBypass, "tile_clip" | ||
| 160 | } | 163 | } | ||
M internal/adapters/drm/overlay_test.go +12 -6
| @@ -63,17 +63,23 @@ 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) { | |
| 66 | + | // A tile cut by a panel (TileInset) is composed: the plane would show it | |
| 67 | + | // whole, over the panel. A tile wholly under the panel is not drawn, so it | |
| 68 | + | // neither takes the plane nor blocks the visible tile. | |
| 69 | + | func TestOverlayTileInset(t *testing.T) { | |
| 69 | 70 | s, contents := overlayScene() | |
| 70 | - | s.TileClip = ports.Rect{W: 200, H: 80} | |
| 71 | + | s.TileInset = ports.Insets{Bottom: 20} | |
| 71 | 72 | if _, _, _, reason := overlayCandidate(s, contents, false, nil, nil); reason != "tile_clip" { | |
| 72 | 73 | t.Fatalf("tile under a panel on the overlay: %q", reason) | |
| 73 | 74 | } | |
| 74 | - | s.TileClip = ports.Rect{W: 200, H: 100} | |
| 75 | + | // The next cascade band starts under the panel. | |
| 76 | + | s, contents = overlayScene() | |
| 77 | + | s.OutputHeight = 120 | |
| 78 | + | s.TileInset = ports.Insets{Bottom: 20} | |
| 79 | + | s.Windows = append(s.Windows, ports.SceneWindow{ID: 3, Rect: ports.Rect{Y: 100, W: 100, H: 100}}) | |
| 80 | + | contents[3] = ports.SurfaceContent{ID: 3, Width: 100, Height: 100, Opaque: true, DMABuf: &ports.DMABuf{ID: 10, Width: 100, Height: 100, Format: fourccXRGB}} | |
| 75 | 81 | 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) | |
| 82 | + | t.Fatalf("visible tile with one under the panel: %v %q", win.ID, reason) | |
| 77 | 83 | } | |
| 78 | 84 | } | |
| 79 | 85 | ||
| @@ -63,17 +63,23 @@ 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 | 66 | + | // A tile cut by a panel (TileInset) is composed: the plane would show it |
| 67 | - | // whole, over the panel. | 67 | + | // whole, over the panel. A tile wholly under the panel is not drawn, so it |
| 68 | - | func TestOverlayTileClip(t *testing.T) { | 68 | + | // neither takes the plane nor blocks the visible tile. |
| 69 | + | func TestOverlayTileInset(t *testing.T) { | |||
| 69 | s, contents := overlayScene() | 70 | s, contents := overlayScene() | ||
| 70 | - | s.TileClip = ports.Rect{W: 200, H: 80} | 71 | + | s.TileInset = ports.Insets{Bottom: 20} |
| 71 | if _, _, _, reason := overlayCandidate(s, contents, false, nil, nil); reason != "tile_clip" { | 72 | if _, _, _, reason := overlayCandidate(s, contents, false, nil, nil); reason != "tile_clip" { | ||
| 72 | t.Fatalf("tile under a panel on the overlay: %q", reason) | 73 | t.Fatalf("tile under a panel on the overlay: %q", reason) | ||
| 73 | } | 74 | } | ||
| 74 | - | s.TileClip = ports.Rect{W: 200, H: 100} | 75 | + | // The next cascade band starts under the panel. |
| 76 | + | s, contents = overlayScene() | |||
| 77 | + | s.OutputHeight = 120 | |||
| 78 | + | s.TileInset = ports.Insets{Bottom: 20} | |||
| 79 | + | s.Windows = append(s.Windows, ports.SceneWindow{ID: 3, Rect: ports.Rect{Y: 100, W: 100, H: 100}}) | |||
| 80 | + | contents[3] = ports.SurfaceContent{ID: 3, Width: 100, Height: 100, Opaque: true, DMABuf: &ports.DMABuf{ID: 10, Width: 100, Height: 100, Format: fourccXRGB}} | |||
| 75 | if win, _, _, reason := overlayCandidate(s, contents, false, nil, nil); reason != "" || win.ID != 2 { | 81 | 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) | 82 | + | t.Fatalf("visible tile with one under the panel: %v %q", win.ID, reason) |
| 77 | } | 83 | } | ||
| 78 | } | 84 | } | ||
| 79 | 85 | ||||
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 | |||
| 230 | 230 | old.OutputWidth != cur.OutputWidth || old.OutputHeight != cur.OutputHeight || | |
| 231 | 231 | old.Scale != cur.Scale || old.Transform != cur.Transform || old.Off != cur.Off || | |
| 232 | 232 | old.Background != cur.Background || old.Border != cur.Border || | |
| 233 | - | old.WorkspaceClip != cur.WorkspaceClip || old.TileClip != cur.TileClip || old.Dim != cur.Dim || old.DimBehind != cur.DimBehind || | |
| 233 | + | old.WorkspaceClip != cur.WorkspaceClip || old.TileInset != cur.TileInset || old.Dim != cur.Dim || old.DimBehind != cur.DimBehind || | |
| 234 | 234 | old.Capture != nil || cur.Capture != nil || old.CaptureScene != nil || cur.CaptureScene != nil || | |
| 235 | 235 | !slices.Equal(old.Layers, cur.Layers) || !slices.Equal(old.DropHints, cur.DropHints) || | |
| 236 | 236 | !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.TileClip != cur.TileClip || old.Dim != cur.Dim || old.DimBehind != cur.DimBehind || | 233 | + | old.WorkspaceClip != cur.WorkspaceClip || old.TileInset != cur.TileInset || 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 +1 -1
| @@ -98,7 +98,7 @@ func TestDamageDeltaFullRedraws(t *testing.T) { | |||
| 98 | 98 | "background": func(s *ports.Scene) { s.Background = "#111111" }, | |
| 99 | 99 | "border": func(s *ports.Scene) { s.Border.Width = 2 }, | |
| 100 | 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 | + | "tile inset": func(s *ports.Scene) { s.TileInset = ports.Insets{Bottom: 10} }, | |
| 102 | 102 | "dim": func(s *ports.Scene) { s.Dim = 0.5 }, | |
| 103 | 103 | "dim behind": func(s *ports.Scene) { s.DimBehind = true }, | |
| 104 | 104 | "drop hints": func(s *ports.Scene) { s.DropHints = []ports.Rect{{W: 5, H: 5}} }, | |
| @@ -98,7 +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 | + | "tile inset": func(s *ports.Scene) { s.TileInset = ports.Insets{Bottom: 10} }, |
| 102 | "dim": func(s *ports.Scene) { s.Dim = 0.5 }, | 102 | "dim": func(s *ports.Scene) { s.Dim = 0.5 }, | ||
| 103 | "dim behind": func(s *ports.Scene) { s.DimBehind = true }, | 103 | "dim behind": func(s *ports.Scene) { s.DimBehind = true }, | ||
| 104 | "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 +17 -8
| @@ -28,7 +28,8 @@ type sceneWalk struct { | |||
| 28 | 28 | // it (premultiplied); 1 otherwise. | |
| 29 | 29 | alpha float32 | |
| 30 | 30 | bounds image.Rectangle | |
| 31 | - | // tileBounds is bounds cut to Scene.TileClip: where tiles draw. | |
| 31 | + | // tileBounds is bounds cut to Scene.TileArea: where tiles and their | |
| 32 | + | // lines draw. | |
| 32 | 33 | tileBounds image.Rectangle | |
| 33 | 34 | draws []draw | |
| 34 | 35 | } | |
| @@ -51,9 +52,8 @@ func (r *Renderer) draws(s ports.Scene, contents map[ports.WindowID]ports.Surfac | |||
| 51 | 52 | w.bounds = w.bounds.Intersect(w.physRect(c.X, c.Y, c.W, c.H)) | |
| 52 | 53 | } | |
| 53 | 54 | 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)) | |
| 55 | + | if a, ok := s.TileArea(); ok { | |
| 56 | + | w.tileBounds = w.bounds.Intersect(w.physRect(a.X, a.Y, a.W, a.H)) | |
| 57 | 57 | } | |
| 58 | 58 | w.windows() | |
| 59 | 59 | w.popups(false) | |
| @@ -214,7 +214,7 @@ func (w *sceneWalk) windows() { | |||
| 214 | 214 | continue | |
| 215 | 215 | } | |
| 216 | 216 | if opensFloats(win) && !tileLines { | |
| 217 | - | w.separators(0) | |
| 217 | + | w.tileSeparators() | |
| 218 | 218 | tileLines = true | |
| 219 | 219 | if w.s.Dim > 0 && !w.s.DimBehind { | |
| 220 | 220 | w.dim(w.bounds, w.s.Dim) | |
| @@ -228,7 +228,7 @@ func (w *sceneWalk) windows() { | |||
| 228 | 228 | w.alpha = 1 - float32(max(0, win.Fade)) | |
| 229 | 229 | windowBounds := w.bounds | |
| 230 | 230 | if win.Tile() { | |
| 231 | - | // A tile never draws over a panel (Scene.TileClip). | |
| 231 | + | // A tile never draws over a panel (Scene.TileArea). | |
| 232 | 232 | w.bounds = w.tileBounds | |
| 233 | 233 | } | |
| 234 | 234 | // Content sits inside the border; core sized the client to match. | |
| @@ -239,7 +239,8 @@ func (w *sceneWalk) windows() { | |||
| 239 | 239 | c := win.Rect.Inset(inset, b) | |
| 240 | 240 | body := w.physRect(c.X, c.Y, c.W, c.H) | |
| 241 | 241 | content := w.contents[win.ID] | |
| 242 | - | w.dmg.window(win.ID, content, w.physRect(win.Rect.X, win.Rect.Y, win.Rect.W, win.Rect.H)) | |
| 242 | + | // Damage counts only what is drawn: never the panel under a tile. | |
| 243 | + | w.dmg.window(win.ID, content, w.physRect(win.Rect.X, win.Rect.Y, win.Rect.W, win.Rect.H).Intersect(w.bounds)) | |
| 243 | 244 | // Until its first buffer, a window shows the background: no flash. | |
| 244 | 245 | w.fill(body, parseColor(w.s.Background)) | |
| 245 | 246 | if !content.Empty() { | |
| @@ -264,10 +265,18 @@ func (w *sceneWalk) windows() { | |||
| 264 | 265 | w.alpha, w.bounds = 1, windowBounds | |
| 265 | 266 | } | |
| 266 | 267 | if !tileLines { | |
| 267 | - | w.separators(0) | |
| 268 | + | w.tileSeparators() | |
| 268 | 269 | } | |
| 269 | 270 | } | |
| 270 | 271 | ||
| 272 | + | // tileSeparators draws the tile lines, cut like the tiles at the panels. | |
| 273 | + | func (w *sceneWalk) tileSeparators() { | |
| 274 | + | b := w.bounds | |
| 275 | + | w.bounds = w.tileBounds | |
| 276 | + | w.separators(0) | |
| 277 | + | w.bounds = b | |
| 278 | + | } | |
| 279 | + | ||
| 271 | 280 | // separators draws a window's border lines in order, active lines over | |
| 272 | 281 | // inactive ones. | |
| 273 | 282 | func (w *sceneWalk) separators(id ports.WindowID) { | |
| @@ -28,7 +28,8 @@ type sceneWalk struct { | |||||
| 28 | // it (premultiplied); 1 otherwise. | 28 | // it (premultiplied); 1 otherwise. | ||
| 29 | alpha float32 | 29 | alpha float32 | ||
| 30 | bounds image.Rectangle | 30 | bounds image.Rectangle | ||
| 31 | - | // tileBounds is bounds cut to Scene.TileClip: where tiles draw. | 31 | + | // tileBounds is bounds cut to Scene.TileArea: where tiles and their |
| 32 | + | // lines draw. | |||
| 32 | tileBounds image.Rectangle | 33 | tileBounds image.Rectangle | ||
| 33 | draws []draw | 34 | draws []draw | ||
| 34 | } | 35 | } | ||
| @@ -51,9 +52,8 @@ func (r *Renderer) draws(s ports.Scene, contents map[ports.WindowID]ports.Surfac | |||||
| 51 | w.bounds = w.bounds.Intersect(w.physRect(c.X, c.Y, c.W, c.H)) | 52 | w.bounds = w.bounds.Intersect(w.physRect(c.X, c.Y, c.W, c.H)) | ||
| 52 | } | 53 | } | ||
| 53 | w.tileBounds = w.bounds | 54 | w.tileBounds = w.bounds | ||
| 54 | - | if s.TileClip != (ports.Rect{}) { | 55 | + | if a, ok := s.TileArea(); ok { |
| 55 | - | c := s.TileClip | 56 | + | w.tileBounds = w.bounds.Intersect(w.physRect(a.X, a.Y, a.W, a.H)) |
| 56 | - | w.tileBounds = w.bounds.Intersect(w.physRect(c.X, c.Y, c.W, c.H)) | |||
| 57 | } | 57 | } | ||
| 58 | w.windows() | 58 | w.windows() | ||
| 59 | w.popups(false) | 59 | w.popups(false) | ||
| @@ -214,7 +214,7 @@ func (w *sceneWalk) windows() { | |||||
| 214 | continue | 214 | continue | ||
| 215 | } | 215 | } | ||
| 216 | if opensFloats(win) && !tileLines { | 216 | if opensFloats(win) && !tileLines { | ||
| 217 | - | w.separators(0) | 217 | + | w.tileSeparators() |
| 218 | tileLines = true | 218 | tileLines = true | ||
| 219 | if w.s.Dim > 0 && !w.s.DimBehind { | 219 | if w.s.Dim > 0 && !w.s.DimBehind { | ||
| 220 | w.dim(w.bounds, w.s.Dim) | 220 | w.dim(w.bounds, w.s.Dim) | ||
| @@ -228,7 +228,7 @@ func (w *sceneWalk) windows() { | |||||
| 228 | w.alpha = 1 - float32(max(0, win.Fade)) | 228 | w.alpha = 1 - float32(max(0, win.Fade)) | ||
| 229 | windowBounds := w.bounds | 229 | windowBounds := w.bounds | ||
| 230 | if win.Tile() { | 230 | if win.Tile() { | ||
| 231 | - | // A tile never draws over a panel (Scene.TileClip). | 231 | + | // A tile never draws over a panel (Scene.TileArea). |
| 232 | w.bounds = w.tileBounds | 232 | w.bounds = w.tileBounds | ||
| 233 | } | 233 | } | ||
| 234 | // Content sits inside the border; core sized the client to match. | 234 | // Content sits inside the border; core sized the client to match. | ||
| @@ -239,7 +239,8 @@ func (w *sceneWalk) windows() { | |||||
| 239 | c := win.Rect.Inset(inset, b) | 239 | c := win.Rect.Inset(inset, b) | ||
| 240 | body := w.physRect(c.X, c.Y, c.W, c.H) | 240 | body := w.physRect(c.X, c.Y, c.W, c.H) | ||
| 241 | content := w.contents[win.ID] | 241 | content := w.contents[win.ID] | ||
| 242 | - | w.dmg.window(win.ID, content, w.physRect(win.Rect.X, win.Rect.Y, win.Rect.W, win.Rect.H)) | 242 | + | // Damage counts only what is drawn: never the panel under a tile. |
| 243 | + | w.dmg.window(win.ID, content, w.physRect(win.Rect.X, win.Rect.Y, win.Rect.W, win.Rect.H).Intersect(w.bounds)) | |||
| 243 | // Until its first buffer, a window shows the background: no flash. | 244 | // Until its first buffer, a window shows the background: no flash. | ||
| 244 | w.fill(body, parseColor(w.s.Background)) | 245 | w.fill(body, parseColor(w.s.Background)) | ||
| 245 | if !content.Empty() { | 246 | if !content.Empty() { | ||
| @@ -264,10 +265,18 @@ func (w *sceneWalk) windows() { | |||||
| 264 | w.alpha, w.bounds = 1, windowBounds | 265 | w.alpha, w.bounds = 1, windowBounds | ||
| 265 | } | 266 | } | ||
| 266 | if !tileLines { | 267 | if !tileLines { | ||
| 267 | - | w.separators(0) | 268 | + | w.tileSeparators() |
| 268 | } | 269 | } | ||
| 269 | } | 270 | } | ||
| 270 | 271 | ||||
| 272 | + | // tileSeparators draws the tile lines, cut like the tiles at the panels. | |||
| 273 | + | func (w *sceneWalk) tileSeparators() { | |||
| 274 | + | b := w.bounds | |||
| 275 | + | w.bounds = w.tileBounds | |||
| 276 | + | w.separators(0) | |||
| 277 | + | w.bounds = b | |||
| 278 | + | } | |||
| 279 | + | ||||
| 271 | // separators draws a window's border lines in order, active lines over | 280 | // separators draws a window's border lines in order, active lines over | ||
| 272 | // inactive ones. | 281 | // inactive ones. | ||
| 273 | func (w *sceneWalk) separators(id ports.WindowID) { | 282 | func (w *sceneWalk) separators(id ports.WindowID) { | ||
M internal/adapters/vulkan/workspace_clip_test.go +32 -7
| @@ -8,9 +8,9 @@ import ( | |||
| 8 | 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) { | |
| 11 | + | // A tile and its lines are cut at TileArea, so a bottom layer panel under | |
| 12 | + | // them stays visible; a float there is drawn over it. | |
| 13 | + | func TestTileInsetKeepsBottomPanel(t *testing.T) { | |
| 14 | 14 | r, err := New(64, 48) | |
| 15 | 15 | if err != nil { | |
| 16 | 16 | t.Skipf("Vulkan unavailable: %v", err) | |
| @@ -20,15 +20,18 @@ func TestTileClipKeepsBottomPanel(t *testing.T) { | |||
| 20 | 20 | green := color.RGBA{0, 255, 0, 255} | |
| 21 | 21 | blue := color.RGBA{0, 0, 255, 255} | |
| 22 | 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}}, | |
| 23 | + | scene := ports.Scene{Seq: 1, OutputWidth: 64, OutputHeight: 48, Background: "#000000", TileInset: ports.Insets{Bottom: 8}, | |
| 24 | + | Border: ports.Border{Width: 2, Inactive: "#ffffff"}, | |
| 25 | + | Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 64, H: 48}}}, | |
| 26 | + | Separators: []ports.Separator{{Rect: ports.Rect{X: 30, W: 2, H: 48}}}, | |
| 27 | + | Layers: []ports.SceneLayer{{ID: 2, Rect: ports.Rect{Y: 40, W: 64, H: 8}, Layer: ports.LayerBottom}}, | |
| 26 | 28 | } | |
| 27 | 29 | if err := render(r, scene, contents); err != nil { | |
| 28 | 30 | t.Fatal(err) | |
| 29 | 31 | } | |
| 30 | 32 | 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} { | |
| 33 | + | white := color.RGBA{255, 255, 255, 255} | |
| 34 | + | for point, want := range map[image.Point]color.RGBA{{10, 10}: red, {10, 39}: red, {10, 40}: green, {60, 47}: green, {30, 39}: white, {30, 44}: green} { | |
| 32 | 35 | if got := pixels.RGBAAt(point.X, point.Y); got != want { | |
| 33 | 36 | t.Errorf("pixel %v=%v, want %v", point, got, want) | |
| 34 | 37 | } | |
| @@ -43,6 +46,28 @@ func TestTileClipKeepsBottomPanel(t *testing.T) { | |||
| 43 | 46 | } | |
| 44 | 47 | } | |
| 45 | 48 | ||
| 49 | + | // A tile whose content changes damages only what is drawn of it, never the | |
| 50 | + | // panel under it. | |
| 51 | + | func TestTileDamageStopsAtPanel(t *testing.T) { | |
| 52 | + | r, err := New(64, 48) | |
| 53 | + | if err != nil { | |
| 54 | + | t.Skipf("Vulkan unavailable: %v", err) | |
| 55 | + | } | |
| 56 | + | defer r.Close() | |
| 57 | + | scene := ports.Scene{Seq: 1, Scale: 1, OutputWidth: 64, OutputHeight: 48, Background: "#000000", TileInset: ports.Insets{Bottom: 8}, | |
| 58 | + | Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 64, H: 48}}}} | |
| 59 | + | c := solidContent(t, 64, 48, color.RGBA{255, 0, 0, 255}) | |
| 60 | + | c.ID, c.Seq = 1, 2 // the target holds Seq 1, with no damage history | |
| 61 | + | bounds := image.Rect(0, 0, 64, 48) | |
| 62 | + | held := &target{valid: true, sceneSeq: scene.Seq, scene: scene, windows: map[ports.WindowID]heldWindow{1: {seq: 1, rect: image.Rect(0, 0, 64, 40)}}} | |
| 63 | + | dmg := r.frameDamage(held, scene, bounds) | |
| 64 | + | _ = r.draws(scene, map[ports.WindowID]ports.SurfaceContent{1: c}, dmg) | |
| 65 | + | dmg.finish() | |
| 66 | + | if dmg.all() || dmg.area != image.Rect(0, 0, 64, 40) { | |
| 67 | + | t.Fatalf("damage %v (all %v), want the tile above the panel", dmg.area, dmg.all()) | |
| 68 | + | } | |
| 69 | + | } | |
| 70 | + | ||
| 46 | 71 | func TestWorkspaceClipWindowsPopupsAndLayers(t *testing.T) { | |
| 47 | 72 | r, err := New(64, 48) | |
| 48 | 73 | if err != nil { | |
| @@ -8,9 +8,9 @@ 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 | 11 | + | // A tile and its lines are cut at TileArea, so a bottom layer panel under |
| 12 | - | // visible; a float there is drawn over it. | 12 | + | // them stays visible; a float there is drawn over it. |
| 13 | - | func TestTileClipKeepsBottomPanel(t *testing.T) { | 13 | + | func TestTileInsetKeepsBottomPanel(t *testing.T) { |
| 14 | r, err := New(64, 48) | 14 | r, err := New(64, 48) | ||
| 15 | if err != nil { | 15 | if err != nil { | ||
| 16 | t.Skipf("Vulkan unavailable: %v", err) | 16 | t.Skipf("Vulkan unavailable: %v", err) | ||
| @@ -20,15 +20,18 @@ func TestTileClipKeepsBottomPanel(t *testing.T) { | |||||
| 20 | green := color.RGBA{0, 255, 0, 255} | 20 | green := color.RGBA{0, 255, 0, 255} | ||
| 21 | blue := color.RGBA{0, 0, 255, 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)} | 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}, | 23 | + | scene := ports.Scene{Seq: 1, OutputWidth: 64, OutputHeight: 48, Background: "#000000", TileInset: ports.Insets{Bottom: 8}, |
| 24 | - | Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 64, H: 48}}}, | 24 | + | Border: ports.Border{Width: 2, Inactive: "#ffffff"}, |
| 25 | - | Layers: []ports.SceneLayer{{ID: 2, Rect: ports.Rect{Y: 40, W: 64, H: 8}, Layer: ports.LayerBottom}}, | 25 | + | Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 64, H: 48}}}, |
| 26 | + | Separators: []ports.Separator{{Rect: ports.Rect{X: 30, W: 2, H: 48}}}, | |||
| 27 | + | Layers: []ports.SceneLayer{{ID: 2, Rect: ports.Rect{Y: 40, W: 64, H: 8}, Layer: ports.LayerBottom}}, | |||
| 26 | } | 28 | } | ||
| 27 | if err := render(r, scene, contents); err != nil { | 29 | if err := render(r, scene, contents); err != nil { | ||
| 28 | t.Fatal(err) | 30 | t.Fatal(err) | ||
| 29 | } | 31 | } | ||
| 30 | pixels := readPixels(t, r) | 32 | 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} { | 33 | + | white := color.RGBA{255, 255, 255, 255} |
| 34 | + | for point, want := range map[image.Point]color.RGBA{{10, 10}: red, {10, 39}: red, {10, 40}: green, {60, 47}: green, {30, 39}: white, {30, 44}: green} { | |||
| 32 | if got := pixels.RGBAAt(point.X, point.Y); got != want { | 35 | if got := pixels.RGBAAt(point.X, point.Y); got != want { | ||
| 33 | t.Errorf("pixel %v=%v, want %v", point, got, want) | 36 | t.Errorf("pixel %v=%v, want %v", point, got, want) | ||
| 34 | } | 37 | } | ||
| @@ -43,6 +46,28 @@ func TestTileClipKeepsBottomPanel(t *testing.T) { | |||||
| 43 | } | 46 | } | ||
| 44 | } | 47 | } | ||
| 45 | 48 | ||||
| 49 | + | // A tile whose content changes damages only what is drawn of it, never the | |||
| 50 | + | // panel under it. | |||
| 51 | + | func TestTileDamageStopsAtPanel(t *testing.T) { | |||
| 52 | + | r, err := New(64, 48) | |||
| 53 | + | if err != nil { | |||
| 54 | + | t.Skipf("Vulkan unavailable: %v", err) | |||
| 55 | + | } | |||
| 56 | + | defer r.Close() | |||
| 57 | + | scene := ports.Scene{Seq: 1, Scale: 1, OutputWidth: 64, OutputHeight: 48, Background: "#000000", TileInset: ports.Insets{Bottom: 8}, | |||
| 58 | + | Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 64, H: 48}}}} | |||
| 59 | + | c := solidContent(t, 64, 48, color.RGBA{255, 0, 0, 255}) | |||
| 60 | + | c.ID, c.Seq = 1, 2 // the target holds Seq 1, with no damage history | |||
| 61 | + | bounds := image.Rect(0, 0, 64, 48) | |||
| 62 | + | held := &target{valid: true, sceneSeq: scene.Seq, scene: scene, windows: map[ports.WindowID]heldWindow{1: {seq: 1, rect: image.Rect(0, 0, 64, 40)}}} | |||
| 63 | + | dmg := r.frameDamage(held, scene, bounds) | |||
| 64 | + | _ = r.draws(scene, map[ports.WindowID]ports.SurfaceContent{1: c}, dmg) | |||
| 65 | + | dmg.finish() | |||
| 66 | + | if dmg.all() || dmg.area != image.Rect(0, 0, 64, 40) { | |||
| 67 | + | t.Fatalf("damage %v (all %v), want the tile above the panel", dmg.area, dmg.all()) | |||
| 68 | + | } | |||
| 69 | + | } | |||
| 70 | + | ||||
| 46 | func TestWorkspaceClipWindowsPopupsAndLayers(t *testing.T) { | 71 | func TestWorkspaceClipWindowsPopupsAndLayers(t *testing.T) { | ||
| 47 | r, err := New(64, 48) | 72 | r, err := New(64, 48) | ||
| 48 | if err != nil { | 73 | if err != nil { | ||
M internal/core/core.go +1 -1
| @@ -466,7 +466,7 @@ type viewport struct{ frame, usable Rect } | |||
| 466 | 466 | func whole(r Rect) viewport { return viewport{frame: r, usable: r} } | |
| 467 | 467 | ||
| 468 | 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 | |
| 469 | + | // usable area and is cut at its edge (Scene.TileInset), so a cascade band | |
| 470 | 470 | // or a column scrolled under a panel is not seen; a float, a fullscreen | |
| 471 | 471 | // window and an overview preview are placed against the frame. | |
| 472 | 472 | func (v viewport) area(p Placement) Rect { | |
| @@ -466,7 +466,7 @@ type viewport struct{ frame, usable Rect } | |||||
| 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 | // area is where p can be seen, drawn and pointed at. A tile lives in the | 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 | 469 | + | // usable area and is cut at its edge (Scene.TileInset), so a cascade band |
| 470 | // or a column scrolled under a panel is not seen; a float, a fullscreen | 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. | 471 | // window and an overview preview are placed against the frame. | ||
| 472 | func (v viewport) area(p Placement) Rect { | 472 | func (v viewport) area(p Placement) Rect { | ||
M internal/core/scene.go +10 -11
| @@ -22,12 +22,11 @@ func (c *Core) sceneFor(ctx context.Context, i int, sc *screen, only *screen, ca | |||
| 22 | 22 | // windows are going, and alone sizes the configures. | |
| 23 | 23 | layout, settled := sc.shown, sc.settledLayout | |
| 24 | 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 | - | // Tiles stop at the panels; the overview lays out previews over the | |
| 26 | - | // whole output. Panels leaving no room at all hide the tiles: a zero | |
| 27 | - | // TileClip would not clip. | |
| 28 | - | noTiles := !sc.mon.ov.open && view.usable.W*view.usable.H <= 0 | |
| 29 | - | if !sc.mon.ov.open && view.usable != view.frame { | |
| 30 | - | scene.TileClip = view.usable | |
| 25 | + | if !sc.mon.ov.open { | |
| 26 | + | // Tiles stop at the panels; the overview lays out previews over | |
| 27 | + | // the whole output. | |
| 28 | + | u := view.usable | |
| 29 | + | scene.TileInset = ports.Insets{Top: u.Y, Left: u.X, Right: o.W - u.X - u.W, Bottom: o.H - u.Y - u.H} | |
| 31 | 30 | } | |
| 32 | 31 | var real map[WindowID]Placement | |
| 33 | 32 | if sc.mon.ov.open { | |
| @@ -68,9 +67,6 @@ func (c *Core) sceneFor(ctx context.Context, i int, sc *screen, only *screen, ca | |||
| 68 | 67 | // Only the focused output has an activated window. | |
| 69 | 68 | focused := p.Focused && i == c.focusScreen | |
| 70 | 69 | sw := ports.SceneWindow{ID: p.ID, Rect: p.Rect, Focused: focused, Fullscreen: p.Fullscreen, Hidden: p.Hidden, Floating: p.Floating, Below: p.Below, Inset: p.Inset, Preview: p.Preview, Fade: p.Fade} | |
| 71 | - | if noTiles && sw.Tile() { | |
| 72 | - | sw.Hidden = true | |
| 73 | - | } | |
| 74 | 70 | if p.Zoom > 0 && (p.Zoom < 1 || p.Preview > 0) { | |
| 75 | 71 | // A scale motion: the content follows the drawn size. A | |
| 76 | 72 | // card in flight drawn at its size still needs Zoom 1: | |
| @@ -82,7 +78,7 @@ func (c *Core) sceneFor(ctx context.Context, i int, sc *screen, only *screen, ca | |||
| 82 | 78 | if p.Leaving { | |
| 83 | 79 | // A window fading out after it closed or hid: drawn, but | |
| 84 | 80 | // hidden to everything else, its configures included. | |
| 85 | - | sw.Hidden = noTiles && sw.Tile() | |
| 81 | + | sw.Hidden = false | |
| 86 | 82 | scene.Windows = append(scene.Windows, sw) | |
| 87 | 83 | // A window the workspace still holds (a hidden stash | |
| 88 | 84 | // window) keeps its last configure: prune would forget it, | |
| @@ -93,7 +89,10 @@ func (c *Core) sceneFor(ctx context.Context, i int, sc *screen, only *screen, ca | |||
| 93 | 89 | } | |
| 94 | 90 | continue | |
| 95 | 91 | } | |
| 96 | - | if focused && p.ID == c.pulse.target && !alone && !p.Fullscreen && !p.Hidden && p.Preview == 0 && !sc.mon.ov.open { | |
| 92 | + | // A window sliding in still waits for its pulse; one with no room | |
| 93 | + | // to be drawn (panels leave no usable area) never gets it. | |
| 94 | + | room := view.area(p) | |
| 95 | + | if focused && p.ID == c.pulse.target && !alone && !p.Fullscreen && !p.Hidden && room.W > 0 && room.H > 0 && p.Preview == 0 && !sc.mon.ov.open { | |
| 97 | 96 | drawable = true | |
| 98 | 97 | if p.ID == c.pulse.id { | |
| 99 | 98 | sw.FocusEffect = pulse | |
| @@ -22,12 +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.settledLayout | 23 | 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 | - | // Tiles stop at the panels; the overview lays out previews over the | 25 | + | if !sc.mon.ov.open { |
| 26 | - | // whole output. Panels leaving no room at all hide the tiles: a zero | 26 | + | // Tiles stop at the panels; the overview lays out previews over |
| 27 | - | // TileClip would not clip. | 27 | + | // the whole output. |
| 28 | - | noTiles := !sc.mon.ov.open && view.usable.W*view.usable.H <= 0 | 28 | + | u := view.usable |
| 29 | - | if !sc.mon.ov.open && view.usable != view.frame { | 29 | + | scene.TileInset = ports.Insets{Top: u.Y, Left: u.X, Right: o.W - u.X - u.W, Bottom: o.H - u.Y - u.H} |
| 30 | - | scene.TileClip = view.usable | |||
| 31 | } | 30 | } | ||
| 32 | var real map[WindowID]Placement | 31 | var real map[WindowID]Placement | ||
| 33 | if sc.mon.ov.open { | 32 | if sc.mon.ov.open { | ||
| @@ -68,9 +67,6 @@ func (c *Core) sceneFor(ctx context.Context, i int, sc *screen, only *screen, ca | |||||
| 68 | // Only the focused output has an activated window. | 67 | // Only the focused output has an activated window. | ||
| 69 | focused := p.Focused && i == c.focusScreen | 68 | focused := p.Focused && i == c.focusScreen | ||
| 70 | sw := ports.SceneWindow{ID: p.ID, Rect: p.Rect, Focused: focused, Fullscreen: p.Fullscreen, Hidden: p.Hidden, Floating: p.Floating, Below: p.Below, Inset: p.Inset, Preview: p.Preview, Fade: p.Fade} | 69 | sw := ports.SceneWindow{ID: p.ID, Rect: p.Rect, Focused: focused, Fullscreen: p.Fullscreen, Hidden: p.Hidden, Floating: p.Floating, Below: p.Below, Inset: p.Inset, Preview: p.Preview, Fade: p.Fade} | ||
| 71 | - | if noTiles && sw.Tile() { | |||
| 72 | - | sw.Hidden = true | |||
| 73 | - | } | |||
| 74 | if p.Zoom > 0 && (p.Zoom < 1 || p.Preview > 0) { | 70 | if p.Zoom > 0 && (p.Zoom < 1 || p.Preview > 0) { | ||
| 75 | // A scale motion: the content follows the drawn size. A | 71 | // A scale motion: the content follows the drawn size. A | ||
| 76 | // card in flight drawn at its size still needs Zoom 1: | 72 | // card in flight drawn at its size still needs Zoom 1: | ||
| @@ -82,7 +78,7 @@ func (c *Core) sceneFor(ctx context.Context, i int, sc *screen, only *screen, ca | |||||
| 82 | if p.Leaving { | 78 | if p.Leaving { | ||
| 83 | // A window fading out after it closed or hid: drawn, but | 79 | // A window fading out after it closed or hid: drawn, but | ||
| 84 | // hidden to everything else, its configures included. | 80 | // hidden to everything else, its configures included. | ||
| 85 | - | sw.Hidden = noTiles && sw.Tile() | 81 | + | sw.Hidden = false |
| 86 | scene.Windows = append(scene.Windows, sw) | 82 | scene.Windows = append(scene.Windows, sw) | ||
| 87 | // A window the workspace still holds (a hidden stash | 83 | // A window the workspace still holds (a hidden stash | ||
| 88 | // window) keeps its last configure: prune would forget it, | 84 | // window) keeps its last configure: prune would forget it, | ||
| @@ -93,7 +89,10 @@ func (c *Core) sceneFor(ctx context.Context, i int, sc *screen, only *screen, ca | |||||
| 93 | } | 89 | } | ||
| 94 | continue | 90 | continue | ||
| 95 | } | 91 | } | ||
| 96 | - | if focused && p.ID == c.pulse.target && !alone && !p.Fullscreen && !p.Hidden && p.Preview == 0 && !sc.mon.ov.open { | 92 | + | // A window sliding in still waits for its pulse; one with no room |
| 93 | + | // to be drawn (panels leave no usable area) never gets it. | |||
| 94 | + | room := view.area(p) | |||
| 95 | + | if focused && p.ID == c.pulse.target && !alone && !p.Fullscreen && !p.Hidden && room.W > 0 && room.H > 0 && p.Preview == 0 && !sc.mon.ov.open { | |||
| 97 | drawable = true | 96 | drawable = true | ||
| 98 | if p.ID == c.pulse.id { | 97 | if p.ID == c.pulse.id { | ||
| 99 | sw.FocusEffect = pulse | 98 | sw.FocusEffect = pulse | ||
M internal/core/viewport_test.go +32 -12
| @@ -105,8 +105,8 @@ func TestConfigureVisibleCascadeUnderPanel(t *testing.T) { | |||
| 105 | 105 | t.Fatal("window under the panel left the scene") | |
| 106 | 106 | } | |
| 107 | 107 | // It is cut at the panel and takes no pointer there. | |
| 108 | - | if scene.TileClip != (Rect{W: 100, H: 60}) { | |
| 109 | - | t.Fatalf("tile clip %+v, want the usable area", scene.TileClip) | |
| 108 | + | if scene.TileInset != (ports.Insets{Bottom: 20}) { | |
| 109 | + | t.Fatalf("tile inset %+v, want the panel's", scene.TileInset) | |
| 110 | 110 | } | |
| 111 | 111 | if id, _, _ := c.hit(10, 70); id != 0 { | |
| 112 | 112 | t.Fatalf("pointer over the panel went to window %d", id) | |
| @@ -114,13 +114,24 @@ func TestConfigureVisibleCascadeUnderPanel(t *testing.T) { | |||
| 114 | 114 | if id, _, _ := c.hit(10, 30); id != 1 { | |
| 115 | 115 | t.Fatalf("pointer over window 1 went to %d", id) | |
| 116 | 116 | } | |
| 117 | + | // Not on screen: its popups are not either, and it loses the pointer. | |
| 118 | + | c.popups[10] = &popupState{id: 10, parent: 3, mapped: true, rect: Rect{W: 5, H: 5}} | |
| 119 | + | c.popupOrder = append(c.popupOrder, 10) | |
| 120 | + | c.pointer = 3 | |
| 121 | + | check(nil) | |
| 122 | + | if c.visible(3) || c.visible(10) || c.pointer != 0 { | |
| 123 | + | t.Fatalf("tile under the panel: visible %v, popup %v, pointer %d", c.visible(3), c.visible(10), c.pointer) | |
| 124 | + | } | |
| 125 | + | // The overview lays previews over the whole output: no inset. | |
| 126 | + | m.ToggleOverview() | |
| 127 | + | if s := check(nil); s.TileInset != (ports.Insets{}) { | |
| 128 | + | t.Fatalf("tile inset %+v in the overview", s.TileInset) | |
| 129 | + | } | |
| 130 | + | m.ToggleOverview() | |
| 117 | 131 | // Its band scrolled in, the first band is under nothing but off screen. | |
| 118 | 132 | w.FocusID(3) | |
| 119 | 133 | check(map[WindowID]bool{1: false, 2: false, 3: true}) | |
| 120 | 134 | // Fullscreen covers the panel: always seen. | |
| 121 | - | w.FocusID(1) | |
| 122 | - | check(map[WindowID]bool{1: true, 3: false}) | |
| 123 | - | w.FocusID(3) | |
| 124 | 135 | w.ToggleFullscreen() | |
| 125 | 136 | check(map[WindowID]bool{3: true}) | |
| 126 | 137 | } | |
| @@ -167,16 +178,25 @@ func TestTileStopsAtPanel(t *testing.T) { | |||
| 167 | 178 | } | |
| 168 | 179 | } | |
| 169 | 180 | ||
| 170 | - | // Panels that leave no room hide every tile: a zero TileClip would not | |
| 171 | - | // clip them. | |
| 172 | - | func TestNoUsableAreaHidesTiles(t *testing.T) { | |
| 181 | + | // Panels that leave no room draw no tile, a leaving one included, and the | |
| 182 | + | // focus pulse has nothing to show. | |
| 183 | + | func TestNoUsableAreaDrawsNoTile(t *testing.T) { | |
| 173 | 184 | c, check := viewportCore(t, func(cfg *ports.Config) { cfg.Layout.MaxColumns = 1 }, Rect{}) | |
| 174 | 185 | c.cur().mon.AddWindow(1) | |
| 186 | + | c.pulse.target = 1 | |
| 175 | 187 | scene := check(map[WindowID]bool{1: false}) | |
| 176 | - | for _, sw := range scene.Windows { | |
| 177 | - | if sw.ID == 1 && !sw.Hidden { | |
| 178 | - | t.Fatalf("tile drawn with no usable area: %+v", sw) | |
| 179 | - | } | |
| 188 | + | if scene.Draws(scene.Windows[0]) || c.pulse.drawable { | |
| 189 | + | t.Fatalf("tile drawn %v, pulse drawable %v with no usable area", scene.Draws(scene.Windows[0]), c.pulse.drawable) | |
| 190 | + | } | |
| 191 | + | sc := c.cur() | |
| 192 | + | sc.shown = slices.Clone(sc.shownLayout()) | |
| 193 | + | sc.shown[0].Leaving = true | |
| 194 | + | scene, _, err := c.sceneFor(context.Background(), 0, sc, nil, nil, 0) | |
| 195 | + | if err != nil { | |
| 196 | + | t.Fatal(err) | |
| 197 | + | } | |
| 198 | + | if scene.Draws(scene.Windows[0]) { | |
| 199 | + | t.Fatalf("leaving tile drawn with no usable area: %+v", scene.Windows[0]) | |
| 180 | 200 | } | |
| 181 | 201 | } | |
| 182 | 202 | ||
| @@ -105,8 +105,8 @@ func TestConfigureVisibleCascadeUnderPanel(t *testing.T) { | |||||
| 105 | t.Fatal("window under the panel left the scene") | 105 | t.Fatal("window under the panel left the scene") | ||
| 106 | } | 106 | } | ||
| 107 | // It is cut at the panel and takes no pointer there. | 107 | // It is cut at the panel and takes no pointer there. | ||
| 108 | - | if scene.TileClip != (Rect{W: 100, H: 60}) { | 108 | + | if scene.TileInset != (ports.Insets{Bottom: 20}) { |
| 109 | - | t.Fatalf("tile clip %+v, want the usable area", scene.TileClip) | 109 | + | t.Fatalf("tile inset %+v, want the panel's", scene.TileInset) |
| 110 | } | 110 | } | ||
| 111 | if id, _, _ := c.hit(10, 70); id != 0 { | 111 | if id, _, _ := c.hit(10, 70); id != 0 { | ||
| 112 | t.Fatalf("pointer over the panel went to window %d", id) | 112 | t.Fatalf("pointer over the panel went to window %d", id) | ||
| @@ -114,13 +114,24 @@ func TestConfigureVisibleCascadeUnderPanel(t *testing.T) { | |||||
| 114 | if id, _, _ := c.hit(10, 30); id != 1 { | 114 | if id, _, _ := c.hit(10, 30); id != 1 { | ||
| 115 | t.Fatalf("pointer over window 1 went to %d", id) | 115 | t.Fatalf("pointer over window 1 went to %d", id) | ||
| 116 | } | 116 | } | ||
| 117 | + | // Not on screen: its popups are not either, and it loses the pointer. | |||
| 118 | + | c.popups[10] = &popupState{id: 10, parent: 3, mapped: true, rect: Rect{W: 5, H: 5}} | |||
| 119 | + | c.popupOrder = append(c.popupOrder, 10) | |||
| 120 | + | c.pointer = 3 | |||
| 121 | + | check(nil) | |||
| 122 | + | if c.visible(3) || c.visible(10) || c.pointer != 0 { | |||
| 123 | + | t.Fatalf("tile under the panel: visible %v, popup %v, pointer %d", c.visible(3), c.visible(10), c.pointer) | |||
| 124 | + | } | |||
| 125 | + | // The overview lays previews over the whole output: no inset. | |||
| 126 | + | m.ToggleOverview() | |||
| 127 | + | if s := check(nil); s.TileInset != (ports.Insets{}) { | |||
| 128 | + | t.Fatalf("tile inset %+v in the overview", s.TileInset) | |||
| 129 | + | } | |||
| 130 | + | m.ToggleOverview() | |||
| 117 | // Its band scrolled in, the first band is under nothing but off screen. | 131 | // Its band scrolled in, the first band is under nothing but off screen. | ||
| 118 | w.FocusID(3) | 132 | w.FocusID(3) | ||
| 119 | check(map[WindowID]bool{1: false, 2: false, 3: true}) | 133 | check(map[WindowID]bool{1: false, 2: false, 3: true}) | ||
| 120 | // Fullscreen covers the panel: always seen. | 134 | // Fullscreen covers the panel: always seen. | ||
| 121 | - | w.FocusID(1) | |||
| 122 | - | check(map[WindowID]bool{1: true, 3: false}) | |||
| 123 | - | w.FocusID(3) | |||
| 124 | w.ToggleFullscreen() | 135 | w.ToggleFullscreen() | ||
| 125 | check(map[WindowID]bool{3: true}) | 136 | check(map[WindowID]bool{3: true}) | ||
| 126 | } | 137 | } | ||
| @@ -167,16 +178,25 @@ func TestTileStopsAtPanel(t *testing.T) { | |||||
| 167 | } | 178 | } | ||
| 168 | } | 179 | } | ||
| 169 | 180 | ||||
| 170 | - | // Panels that leave no room hide every tile: a zero TileClip would not | 181 | + | // Panels that leave no room draw no tile, a leaving one included, and the |
| 171 | - | // clip them. | 182 | + | // focus pulse has nothing to show. |
| 172 | - | func TestNoUsableAreaHidesTiles(t *testing.T) { | 183 | + | func TestNoUsableAreaDrawsNoTile(t *testing.T) { |
| 173 | c, check := viewportCore(t, func(cfg *ports.Config) { cfg.Layout.MaxColumns = 1 }, Rect{}) | 184 | c, check := viewportCore(t, func(cfg *ports.Config) { cfg.Layout.MaxColumns = 1 }, Rect{}) | ||
| 174 | c.cur().mon.AddWindow(1) | 185 | c.cur().mon.AddWindow(1) | ||
| 186 | + | c.pulse.target = 1 | |||
| 175 | scene := check(map[WindowID]bool{1: false}) | 187 | scene := check(map[WindowID]bool{1: false}) | ||
| 176 | - | for _, sw := range scene.Windows { | 188 | + | if scene.Draws(scene.Windows[0]) || c.pulse.drawable { |
| 177 | - | if sw.ID == 1 && !sw.Hidden { | 189 | + | t.Fatalf("tile drawn %v, pulse drawable %v with no usable area", scene.Draws(scene.Windows[0]), c.pulse.drawable) |
| 178 | - | t.Fatalf("tile drawn with no usable area: %+v", sw) | 190 | + | } |
| 179 | - | } | 191 | + | sc := c.cur() |
| 192 | + | sc.shown = slices.Clone(sc.shownLayout()) | |||
| 193 | + | sc.shown[0].Leaving = true | |||
| 194 | + | scene, _, err := c.sceneFor(context.Background(), 0, sc, nil, nil, 0) | |||
| 195 | + | if err != nil { | |||
| 196 | + | t.Fatal(err) | |||
| 197 | + | } | |||
| 198 | + | if scene.Draws(scene.Windows[0]) { | |||
| 199 | + | t.Fatalf("leaving tile drawn with no usable area: %+v", scene.Windows[0]) | |||
| 180 | } | 200 | } | ||
| 181 | } | 201 | } | ||
| 182 | 202 | ||||
M internal/ports/scene.go +37 -12
| @@ -27,11 +27,12 @@ type Scene struct { | |||
| 27 | 27 | // in logical output coordinates. Zero inherits the full output. Layers | |
| 28 | 28 | // remain output-wide; overview previews do not use this clip. | |
| 29 | 29 | 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 | + | // TileInset is what exclusive layers (panels) take from the output's | |
| 31 | + | // edges. Tiles (SceneWindow.Tile) and their lines are drawn only in | |
| 32 | + | // what is left (TileArea), so a tile is never drawn over a panel, | |
| 33 | + | // whatever its layer; insets covering the output leave no tile drawn. | |
| 34 | + | // Zero: no panel (and the overview, capture scenes). | |
| 35 | + | TileInset Insets | |
| 35 | 36 | // Dim darkens the background, bottom layers and tiles (tile lines | |
| 36 | 37 | // included) inside WorkspaceClip when set, with black at this opacity, | |
| 37 | 38 | // 0 to 1, under the first | |
| @@ -79,7 +80,7 @@ func (s Scene) SameAs(o Scene) bool { | |||
| 79 | 80 | s.OutputWidth != o.OutputWidth || s.OutputHeight != o.OutputHeight || | |
| 80 | 81 | s.Scale != o.Scale || s.Transform != o.Transform || s.Off != o.Off || | |
| 81 | 82 | s.Background != o.Background || s.Border != o.Border || | |
| 82 | - | s.WorkspaceClip != o.WorkspaceClip || s.TileClip != o.TileClip || s.Dim != o.Dim || s.DimBehind != o.DimBehind { | |
| 83 | + | s.WorkspaceClip != o.WorkspaceClip || s.TileInset != o.TileInset || s.Dim != o.Dim || s.DimBehind != o.DimBehind { | |
| 83 | 84 | return false | |
| 84 | 85 | } | |
| 85 | 86 | if s.CaptureScene != nil || o.CaptureScene != nil { | |
| @@ -102,13 +103,10 @@ func (s Scene) SameAs(o Scene) bool { | |||
| 102 | 103 | ||
| 103 | 104 | // Shows reports whether the scene draws the surface of id: only its | |
| 104 | 105 | // content changes need a new frame. A window scrolled off the output is | |
| 105 | - | // not drawn. WorkspaceClip also excludes off-viewport windows, TileClip | |
| 106 | - | // tiles under a panel; OverLayers popups remain output-wide. | |
| 106 | + | // not drawn (Draws). | |
| 107 | 107 | func (s Scene) Shows(id WindowID) bool { | |
| 108 | 108 | for _, w := range s.Windows { | |
| 109 | - | if w.ID == id && !w.Hidden && w.Rect.Overlaps(Rect{W: s.OutputWidth, H: s.OutputHeight}) && | |
| 110 | - | (w.OverLayers || s.WorkspaceClip == (Rect{}) || w.Rect.Overlaps(s.WorkspaceClip)) && | |
| 111 | - | (!w.Tile() || s.TileClip == (Rect{}) || w.Rect.Overlaps(s.TileClip)) { | |
| 109 | + | if w.ID == id && s.Draws(w) { | |
| 112 | 110 | return true | |
| 113 | 111 | } | |
| 114 | 112 | } | |
| @@ -120,6 +118,33 @@ func (s Scene) Shows(id WindowID) bool { | |||
| 120 | 118 | return false | |
| 121 | 119 | } | |
| 122 | 120 | ||
| 121 | + | // Draws reports whether any of w is drawn: not hidden, on the output, | |
| 122 | + | // inside WorkspaceClip (OverLayers popups remain output-wide) and, for a | |
| 123 | + | // tile, inside TileArea. | |
| 124 | + | func (s Scene) Draws(w SceneWindow) bool { | |
| 125 | + | if w.Hidden || !w.Rect.Overlaps(Rect{W: s.OutputWidth, H: s.OutputHeight}) { | |
| 126 | + | return false | |
| 127 | + | } | |
| 128 | + | if !w.OverLayers && s.WorkspaceClip != (Rect{}) && !w.Rect.Overlaps(s.WorkspaceClip) { | |
| 129 | + | return false | |
| 130 | + | } | |
| 131 | + | if a, ok := s.TileArea(); ok && w.Tile() { | |
| 132 | + | return w.Rect.Overlaps(a) | |
| 133 | + | } | |
| 134 | + | return true | |
| 135 | + | } | |
| 136 | + | ||
| 137 | + | // TileArea is where tiles are drawn: the output minus TileInset, empty | |
| 138 | + | // when the panels cover it. ok is false when TileInset is zero: tiles are | |
| 139 | + | // not clipped beyond the output and WorkspaceClip. | |
| 140 | + | func (s Scene) TileArea() (r Rect, ok bool) { | |
| 141 | + | in := s.TileInset | |
| 142 | + | if in == (Insets{}) { | |
| 143 | + | return Rect{}, false | |
| 144 | + | } | |
| 145 | + | return Rect{X: in.Left, Y: in.Top, W: max(s.OutputWidth-in.Left-in.Right, 0), H: max(s.OutputHeight-in.Top-in.Bottom, 0)}, true | |
| 146 | + | } | |
| 147 | + | ||
| 123 | 148 | // Border carries the window border style; colors are #rrggbb, "" skips | |
| 124 | 149 | // drawing. Width is in logical pixels. | |
| 125 | 150 | type Border struct { | |
| @@ -188,7 +213,7 @@ type SceneWindow struct { | |||
| 188 | 213 | } | |
| 189 | 214 | ||
| 190 | 215 | // Tile reports whether w is a tile: laid out in the usable area and | |
| 191 | - | // clipped to Scene.TileClip. Floats, fullscreen windows, overview previews | |
| 216 | + | // clipped to Scene.TileArea. Floats, fullscreen windows, overview previews | |
| 192 | 217 | // and popups are placed against the whole viewport. | |
| 193 | 218 | func (w SceneWindow) Tile() bool { | |
| 194 | 219 | return !w.Floating && !w.Fullscreen && !w.Popup && w.Preview == 0 | |
| @@ -27,11 +27,12 @@ type Scene struct { | |||||
| 27 | // in logical output coordinates. Zero inherits the full output. Layers | 27 | // 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 Rect | 29 | WorkspaceClip Rect | ||
| 30 | - | // TileClip bounds tiles (SceneWindow.Tile) and their lines inside | 30 | + | // TileInset is what exclusive layers (panels) take from the output's |
| 31 | - | // WorkspaceClip: the usable area exclusive layers leave, so a tile is | 31 | + | // edges. Tiles (SceneWindow.Tile) and their lines are drawn only in |
| 32 | - | // never drawn over a panel, whatever its layer. Zero does not clip | 32 | + | // what is left (TileArea), so a tile is never drawn over a panel, |
| 33 | - | // (overview, capture scenes). | 33 | + | // whatever its layer; insets covering the output leave no tile drawn. |
| 34 | - | TileClip Rect | 34 | + | // Zero: no panel (and the overview, capture scenes). |
| 35 | + | TileInset Insets | |||
| 35 | // Dim darkens the background, bottom layers and tiles (tile lines | 36 | // Dim darkens the background, bottom layers and tiles (tile lines | ||
| 36 | // included) inside WorkspaceClip when set, with black at this opacity, | 37 | // included) inside WorkspaceClip when set, with black at this opacity, | ||
| 37 | // 0 to 1, under the first | 38 | // 0 to 1, under the first | ||
| @@ -79,7 +80,7 @@ func (s Scene) SameAs(o Scene) bool { | |||||
| 79 | s.OutputWidth != o.OutputWidth || s.OutputHeight != o.OutputHeight || | 80 | s.OutputWidth != o.OutputWidth || s.OutputHeight != o.OutputHeight || | ||
| 80 | s.Scale != o.Scale || s.Transform != o.Transform || s.Off != o.Off || | 81 | s.Scale != o.Scale || s.Transform != o.Transform || s.Off != o.Off || | ||
| 81 | s.Background != o.Background || s.Border != o.Border || | 82 | s.Background != o.Background || s.Border != o.Border || | ||
| 82 | - | s.WorkspaceClip != o.WorkspaceClip || s.TileClip != o.TileClip || s.Dim != o.Dim || s.DimBehind != o.DimBehind { | 83 | + | s.WorkspaceClip != o.WorkspaceClip || s.TileInset != o.TileInset || s.Dim != o.Dim || s.DimBehind != o.DimBehind { |
| 83 | return false | 84 | return false | ||
| 84 | } | 85 | } | ||
| 85 | if s.CaptureScene != nil || o.CaptureScene != nil { | 86 | if s.CaptureScene != nil || o.CaptureScene != nil { | ||
| @@ -102,13 +103,10 @@ func (s Scene) SameAs(o Scene) bool { | |||||
| 102 | 103 | ||||
| 103 | // Shows reports whether the scene draws the surface of id: only its | 104 | // Shows reports whether the scene draws the surface of id: only its | ||
| 104 | // content changes need a new frame. A window scrolled off the output is | 105 | // content changes need a new frame. A window scrolled off the output is | ||
| 105 | - | // not drawn. WorkspaceClip also excludes off-viewport windows, TileClip | 106 | + | // not drawn (Draws). |
| 106 | - | // tiles under a panel; OverLayers popups remain output-wide. | |||
| 107 | func (s Scene) Shows(id WindowID) bool { | 107 | func (s Scene) Shows(id WindowID) bool { | ||
| 108 | for _, w := range s.Windows { | 108 | for _, w := range s.Windows { | ||
| 109 | - | if w.ID == id && !w.Hidden && w.Rect.Overlaps(Rect{W: s.OutputWidth, H: s.OutputHeight}) && | 109 | + | if w.ID == id && s.Draws(w) { |
| 110 | - | (w.OverLayers || s.WorkspaceClip == (Rect{}) || w.Rect.Overlaps(s.WorkspaceClip)) && | |||
| 111 | - | (!w.Tile() || s.TileClip == (Rect{}) || w.Rect.Overlaps(s.TileClip)) { | |||
| 112 | return true | 110 | return true | ||
| 113 | } | 111 | } | ||
| 114 | } | 112 | } | ||
| @@ -120,6 +118,33 @@ func (s Scene) Shows(id WindowID) bool { | |||||
| 120 | return false | 118 | return false | ||
| 121 | } | 119 | } | ||
| 122 | 120 | ||||
| 121 | + | // Draws reports whether any of w is drawn: not hidden, on the output, | |||
| 122 | + | // inside WorkspaceClip (OverLayers popups remain output-wide) and, for a | |||
| 123 | + | // tile, inside TileArea. | |||
| 124 | + | func (s Scene) Draws(w SceneWindow) bool { | |||
| 125 | + | if w.Hidden || !w.Rect.Overlaps(Rect{W: s.OutputWidth, H: s.OutputHeight}) { | |||
| 126 | + | return false | |||
| 127 | + | } | |||
| 128 | + | if !w.OverLayers && s.WorkspaceClip != (Rect{}) && !w.Rect.Overlaps(s.WorkspaceClip) { | |||
| 129 | + | return false | |||
| 130 | + | } | |||
| 131 | + | if a, ok := s.TileArea(); ok && w.Tile() { | |||
| 132 | + | return w.Rect.Overlaps(a) | |||
| 133 | + | } | |||
| 134 | + | return true | |||
| 135 | + | } | |||
| 136 | + | ||||
| 137 | + | // TileArea is where tiles are drawn: the output minus TileInset, empty | |||
| 138 | + | // when the panels cover it. ok is false when TileInset is zero: tiles are | |||
| 139 | + | // not clipped beyond the output and WorkspaceClip. | |||
| 140 | + | func (s Scene) TileArea() (r Rect, ok bool) { | |||
| 141 | + | in := s.TileInset | |||
| 142 | + | if in == (Insets{}) { | |||
| 143 | + | return Rect{}, false | |||
| 144 | + | } | |||
| 145 | + | return Rect{X: in.Left, Y: in.Top, W: max(s.OutputWidth-in.Left-in.Right, 0), H: max(s.OutputHeight-in.Top-in.Bottom, 0)}, true | |||
| 146 | + | } | |||
| 147 | + | ||||
| 123 | // Border carries the window border style; colors are #rrggbb, "" skips | 148 | // Border carries the window border style; colors are #rrggbb, "" skips | ||
| 124 | // drawing. Width is in logical pixels. | 149 | // drawing. Width is in logical pixels. | ||
| 125 | type Border struct { | 150 | type Border struct { | ||
| @@ -188,7 +213,7 @@ type SceneWindow struct { | |||||
| 188 | } | 213 | } | ||
| 189 | 214 | ||||
| 190 | // Tile reports whether w is a tile: laid out in the usable area and | 215 | // Tile reports whether w is a tile: laid out in the usable area and | ||
| 191 | - | // clipped to Scene.TileClip. Floats, fullscreen windows, overview previews | 216 | + | // clipped to Scene.TileArea. Floats, fullscreen windows, overview previews |
| 192 | // and popups are placed against the whole viewport. | 217 | // and popups are placed against the whole viewport. | ||
| 193 | func (w SceneWindow) Tile() bool { | 218 | func (w SceneWindow) Tile() bool { | ||
| 194 | return !w.Floating && !w.Fullscreen && !w.Popup && w.Preview == 0 | 219 | return !w.Floating && !w.Fullscreen && !w.Popup && w.Preview == 0 | ||
M internal/ports/scene_same_test.go +1 -1
| @@ -48,7 +48,7 @@ func TestSceneSameAs(t *testing.T) { | |||
| 48 | 48 | "scale": func(s *Scene) { s.Scale = 2 }, | |
| 49 | 49 | "off": func(s *Scene) { s.Off = true }, | |
| 50 | 50 | "clip": func(s *Scene) { s.WorkspaceClip.W = 1 }, | |
| 51 | - | "tile clip": func(s *Scene) { s.TileClip.H = 1 }, | |
| 51 | + | "tile inset": func(s *Scene) { s.TileInset.Bottom = 1 }, | |
| 52 | 52 | "dim": func(s *Scene) { s.Dim = 0.5 }, | |
| 53 | 53 | "dim behind": func(s *Scene) { s.DimBehind = true }, | |
| 54 | 54 | "separator": func(s *Scene) { s.Separators[0].Active = true }, | |
| @@ -48,7 +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 | + | "tile inset": func(s *Scene) { s.TileInset.Bottom = 1 }, |
| 52 | "dim": func(s *Scene) { s.Dim = 0.5 }, | 52 | "dim": func(s *Scene) { s.Dim = 0.5 }, | ||
| 53 | "dim behind": func(s *Scene) { s.DimBehind = true }, | 53 | "dim behind": func(s *Scene) { s.DimBehind = true }, | ||
| 54 | "separator": func(s *Scene) { s.Separators[0].Active = true }, | 54 | "separator": func(s *Scene) { s.Separators[0].Active = true }, | ||
M internal/ports/types.go +8 -0
| @@ -24,6 +24,14 @@ func (r Rect) Overlaps(o Rect) bool { | |||
| 24 | 24 | r.X < o.X+o.W && o.X < r.X+r.W && r.Y < o.Y+o.H && o.Y < r.Y+r.H | |
| 25 | 25 | } | |
| 26 | 26 | ||
| 27 | + | // Contains reports whether o lies wholly inside r. | |
| 28 | + | func (r Rect) Contains(o Rect) bool { | |
| 29 | + | return o.X >= r.X && o.Y >= r.Y && o.X+o.W <= r.X+r.W && o.Y+o.H <= r.Y+r.H | |
| 30 | + | } | |
| 31 | + | ||
| 32 | + | // Insets are distances from the four edges of an area. | |
| 33 | + | type Insets struct{ Top, Right, Bottom, Left int } | |
| 34 | + | ||
| 27 | 35 | // Inset shrinks r by b on each side in s. | |
| 28 | 36 | func (r Rect) Inset(s Sides, b int) Rect { | |
| 29 | 37 | b = min(max(b, 0), r.W/2, r.H/2) | |
| @@ -24,6 +24,14 @@ func (r Rect) Overlaps(o Rect) bool { | |||||
| 24 | r.X < o.X+o.W && o.X < r.X+r.W && r.Y < o.Y+o.H && o.Y < r.Y+r.H | 24 | r.X < o.X+o.W && o.X < r.X+r.W && r.Y < o.Y+o.H && o.Y < r.Y+r.H | ||
| 25 | } | 25 | } | ||
| 26 | 26 | ||||
| 27 | + | // Contains reports whether o lies wholly inside r. | |||
| 28 | + | func (r Rect) Contains(o Rect) bool { | |||
| 29 | + | return o.X >= r.X && o.Y >= r.Y && o.X+o.W <= r.X+r.W && o.Y+o.H <= r.Y+r.H | |||
| 30 | + | } | |||
| 31 | + | ||||
| 32 | + | // Insets are distances from the four edges of an area. | |||
| 33 | + | type Insets struct{ Top, Right, Bottom, Left int } | |||
| 34 | + | ||||
| 27 | // Inset shrinks r by b on each side in s. | 35 | // Inset shrinks r by b on each side in s. | ||
| 28 | func (r Rect) Inset(s Sides, b int) Rect { | 36 | func (r Rect) Inset(s Sides, b int) Rect { | ||
| 29 | b = min(max(b, 0), r.W/2, r.H/2) | 37 | b = min(max(b, 0), r.W/2, r.H/2) | ||
M internal/ports/workspace_clip_test.go +20 -6
| @@ -17,11 +17,12 @@ func TestSceneShowsWorkspaceClip(t *testing.T) { | |||
| 17 | 17 | } | |
| 18 | 18 | } | |
| 19 | 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) { | |
| 20 | + | // A tile under a panel (outside TileArea) is not drawn; a float, a | |
| 21 | + | // fullscreen window, a preview or a popup there still is. Panels covering | |
| 22 | + | // the whole output leave no tile drawn. | |
| 23 | + | func TestSceneShowsTileInset(t *testing.T) { | |
| 23 | 24 | under := Rect{Y: 80, W: 50, H: 80} | |
| 24 | - | s := Scene{OutputWidth: 100, OutputHeight: 100, TileClip: Rect{W: 100, H: 80}, Windows: []SceneWindow{ | |
| 25 | + | s := Scene{OutputWidth: 100, OutputHeight: 100, TileInset: Insets{Bottom: 20}, Windows: []SceneWindow{ | |
| 25 | 26 | {ID: 1, Rect: Rect{W: 50, H: 80}}, | |
| 26 | 27 | {ID: 2, Rect: under}, | |
| 27 | 28 | {ID: 3, Rect: under, Floating: true}, | |
| @@ -34,8 +35,21 @@ func TestSceneShowsTileClip(t *testing.T) { | |||
| 34 | 35 | t.Errorf("window %d shown %v, want %v", id, got, want) | |
| 35 | 36 | } | |
| 36 | 37 | } | |
| 37 | - | s.TileClip = Rect{} | |
| 38 | + | s.TileInset = Insets{Right: 100, Bottom: 100} | |
| 39 | + | if s.Shows(1) || !s.Shows(3) { | |
| 40 | + | t.Fatalf("no room for tiles: tile %v, float %v", s.Shows(1), s.Shows(3)) | |
| 41 | + | } | |
| 42 | + | s.TileInset = Insets{} | |
| 38 | 43 | if !s.Shows(2) { | |
| 39 | - | t.Fatal("tile under no clip not shown") | |
| 44 | + | t.Fatal("tile with no panel not shown") | |
| 45 | + | } | |
| 46 | + | } | |
| 47 | + | ||
| 48 | + | func TestRectContains(t *testing.T) { | |
| 49 | + | r := Rect{X: 10, Y: 10, W: 20, H: 20} | |
| 50 | + | for o, want := range map[Rect]bool{r: true, {X: 15, Y: 15, W: 5, H: 5}: true, {X: 9, Y: 10, W: 5, H: 5}: false, {X: 25, Y: 25, W: 6, H: 5}: false} { | |
| 51 | + | if got := r.Contains(o); got != want { | |
| 52 | + | t.Errorf("%+v contains %+v = %v, want %v", r, o, got, want) | |
| 53 | + | } | |
| 40 | 54 | } | |
| 41 | 55 | } | |
| @@ -17,11 +17,12 @@ func TestSceneShowsWorkspaceClip(t *testing.T) { | |||||
| 17 | } | 17 | } | ||
| 18 | } | 18 | } | ||
| 19 | 19 | ||||
| 20 | - | // A tile under a panel (outside TileClip) is not drawn; a float, a | 20 | + | // A tile under a panel (outside TileArea) is not drawn; a float, a |
| 21 | - | // fullscreen window, a preview or a popup there still is. | 21 | + | // fullscreen window, a preview or a popup there still is. Panels covering |
| 22 | - | func TestSceneShowsTileClip(t *testing.T) { | 22 | + | // the whole output leave no tile drawn. |
| 23 | + | func TestSceneShowsTileInset(t *testing.T) { | |||
| 23 | under := Rect{Y: 80, W: 50, H: 80} | 24 | under := Rect{Y: 80, W: 50, H: 80} | ||
| 24 | - | s := Scene{OutputWidth: 100, OutputHeight: 100, TileClip: Rect{W: 100, H: 80}, Windows: []SceneWindow{ | 25 | + | s := Scene{OutputWidth: 100, OutputHeight: 100, TileInset: Insets{Bottom: 20}, Windows: []SceneWindow{ |
| 25 | {ID: 1, Rect: Rect{W: 50, H: 80}}, | 26 | {ID: 1, Rect: Rect{W: 50, H: 80}}, | ||
| 26 | {ID: 2, Rect: under}, | 27 | {ID: 2, Rect: under}, | ||
| 27 | {ID: 3, Rect: under, Floating: true}, | 28 | {ID: 3, Rect: under, Floating: true}, | ||
| @@ -34,8 +35,21 @@ func TestSceneShowsTileClip(t *testing.T) { | |||||
| 34 | t.Errorf("window %d shown %v, want %v", id, got, want) | 35 | t.Errorf("window %d shown %v, want %v", id, got, want) | ||
| 35 | } | 36 | } | ||
| 36 | } | 37 | } | ||
| 37 | - | s.TileClip = Rect{} | 38 | + | s.TileInset = Insets{Right: 100, Bottom: 100} |
| 39 | + | if s.Shows(1) || !s.Shows(3) { | |||
| 40 | + | t.Fatalf("no room for tiles: tile %v, float %v", s.Shows(1), s.Shows(3)) | |||
| 41 | + | } | |||
| 42 | + | s.TileInset = Insets{} | |||
| 38 | if !s.Shows(2) { | 43 | if !s.Shows(2) { | ||
| 39 | - | t.Fatal("tile under no clip not shown") | 44 | + | t.Fatal("tile with no panel not shown") |
| 45 | + | } | |||
| 46 | + | } | |||
| 47 | + | ||||
| 48 | + | func TestRectContains(t *testing.T) { | |||
| 49 | + | r := Rect{X: 10, Y: 10, W: 20, H: 20} | |||
| 50 | + | for o, want := range map[Rect]bool{r: true, {X: 15, Y: 15, W: 5, H: 5}: true, {X: 9, Y: 10, W: 5, H: 5}: false, {X: 25, Y: 25, W: 6, H: 5}: false} { | |||
| 51 | + | if got := r.Contains(o); got != want { | |||
| 52 | + | t.Errorf("%+v contains %+v = %v, want %v", r, o, got, want) | |||
| 53 | + | } | |||
| 40 | } | 54 | } | ||
| 41 | } | 55 | } | ||