Merge pull request 'refactor(core): one viewport visibility rule; tiles never draw over panels' (#26) from refactor/viewport-visibility into main
Reviewed-on: https://git.bnema.dev/bnema/neferwl/pulls/26
28 files · +574 -81
M internal/adapters/drm/overlay.go +9 -4
| @@ -76,7 +76,9 @@ func overlayCandidate(s ports.Scene, surfaces map[ports.WindowID]ports.SurfaceCo | |||
| 76 | 76 | var mode colorMode | |
| 77 | 77 | for i := range s.Windows { | |
| 78 | 78 | w := &s.Windows[i] | |
| 79 | - | if w.Hidden || w.Rect.W <= 0 || w.Rect.H <= 0 { | |
| 79 | + | if !s.Draws(*w) { | |
| 80 | + | // Not drawn (a tile wholly under a panel included): neither | |
| 81 | + | // a candidate nor over one. | |
| 80 | 82 | continue | |
| 81 | 83 | } | |
| 82 | 84 | if w.Popup { | |
| @@ -104,7 +106,7 @@ func overlayCandidate(s ports.Scene, surfaces map[ports.WindowID]ports.SurfaceCo | |||
| 104 | 106 | } | |
| 105 | 107 | if pick != nil { | |
| 106 | 108 | for i := range s.Windows { | |
| 107 | - | if w := &s.Windows[i]; w.Popup && !w.Hidden && w.Rect.Overlaps(pick.Rect) { | |
| 109 | + | if w := &s.Windows[i]; w.Popup && s.Draws(*w) && w.Rect.Overlaps(pick.Rect) { | |
| 108 | 110 | return ports.SceneWindow{}, ports.SurfaceContent{}, colorBypass, "window_above" | |
| 109 | 111 | } | |
| 110 | 112 | } | |
| @@ -148,11 +150,14 @@ func overlayCandidate(s ports.Scene, surfaces map[ports.WindowID]ports.SurfaceCo | |||
| 148 | 150 | } | |
| 149 | 151 | } | |
| 150 | 152 | } | |
| 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) { | |
| 153 | + | if clip := s.WorkspaceClip; clip != (ports.Rect{}) && !clip.Contains(pick.Rect) { | |
| 153 | 154 | // The plane cannot apply the workspace's logical viewport crop. | |
| 154 | 155 | return ports.SceneWindow{}, ports.SurfaceContent{}, colorBypass, "workspace_clip" | |
| 155 | 156 | } | |
| 157 | + | if a, ok := s.TileArea(); ok && pick.Tile() && !a.Contains(pick.Rect) { | |
| 158 | + | // Nor the cut of a tile at a panel. | |
| 159 | + | return ports.SceneWindow{}, ports.SurfaceContent{}, colorBypass, "tile_clip" | |
| 160 | + | } | |
| 156 | 161 | c := surfaces[pick.ID] | |
| 157 | 162 | pw, ph := float64(pick.Rect.W)*scale, float64(pick.Rect.H)*scale | |
| 158 | 163 | if float64(c.Width) != pw || float64(c.Height) != ph || c.Geometry != (ports.Rect{}) && (c.Geometry.X != 0 || c.Geometry.Y != 0) { | |
| @@ -76,7 +76,9 @@ func overlayCandidate(s ports.Scene, surfaces map[ports.WindowID]ports.SurfaceCo | |||||
| 76 | var mode colorMode | 76 | var mode colorMode | ||
| 77 | for i := range s.Windows { | 77 | for i := range s.Windows { | ||
| 78 | w := &s.Windows[i] | 78 | w := &s.Windows[i] | ||
| 79 | - | if w.Hidden || w.Rect.W <= 0 || w.Rect.H <= 0 { | 79 | + | if !s.Draws(*w) { |
| 80 | + | // Not drawn (a tile wholly under a panel included): neither | |||
| 81 | + | // a candidate nor over one. | |||
| 80 | continue | 82 | continue | ||
| 81 | } | 83 | } | ||
| 82 | if w.Popup { | 84 | if w.Popup { | ||
| @@ -104,7 +106,7 @@ func overlayCandidate(s ports.Scene, surfaces map[ports.WindowID]ports.SurfaceCo | |||||
| 104 | } | 106 | } | ||
| 105 | if pick != nil { | 107 | if pick != nil { | ||
| 106 | for i := range s.Windows { | 108 | for i := range s.Windows { | ||
| 107 | - | if w := &s.Windows[i]; w.Popup && !w.Hidden && w.Rect.Overlaps(pick.Rect) { | 109 | + | if w := &s.Windows[i]; w.Popup && s.Draws(*w) && w.Rect.Overlaps(pick.Rect) { |
| 108 | return ports.SceneWindow{}, ports.SurfaceContent{}, colorBypass, "window_above" | 110 | return ports.SceneWindow{}, ports.SurfaceContent{}, colorBypass, "window_above" | ||
| 109 | } | 111 | } | ||
| 110 | } | 112 | } | ||
| @@ -148,11 +150,14 @@ func overlayCandidate(s ports.Scene, surfaces map[ports.WindowID]ports.SurfaceCo | |||||
| 148 | } | 150 | } | ||
| 149 | } | 151 | } | ||
| 150 | } | 152 | } | ||
| 151 | - | if clip := s.WorkspaceClip; clip != (ports.Rect{}) && | 153 | + | 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. | 154 | // The plane cannot apply the workspace's logical viewport crop. | ||
| 154 | return ports.SceneWindow{}, ports.SurfaceContent{}, colorBypass, "workspace_clip" | 155 | return ports.SceneWindow{}, ports.SurfaceContent{}, colorBypass, "workspace_clip" | ||
| 155 | } | 156 | } | ||
| 157 | + | if a, ok := s.TileArea(); ok && pick.Tile() && !a.Contains(pick.Rect) { | |||
| 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)*scale | 162 | 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 +31 -0
| @@ -63,6 +63,37 @@ func TestOverlayWorkspaceClip(t *testing.T) { | |||
| 63 | 63 | } | |
| 64 | 64 | } | |
| 65 | 65 | ||
| 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) { | |
| 70 | + | s, contents := overlayScene() | |
| 71 | + | s.TileInset = ports.Insets{Bottom: 20} | |
| 72 | + | if _, _, _, reason := overlayCandidate(s, contents, false, nil, nil); reason != "tile_clip" { | |
| 73 | + | t.Fatalf("tile under a panel on the overlay: %q", reason) | |
| 74 | + | } | |
| 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}} | |
| 81 | + | if win, _, _, reason := overlayCandidate(s, contents, false, nil, nil); reason != "" || win.ID != 2 { | |
| 82 | + | t.Fatalf("visible tile with one under the panel: %v %q", win.ID, reason) | |
| 83 | + | } | |
| 84 | + | } | |
| 85 | + | ||
| 86 | + | // A fullscreen window is scanned out when the only other window is a tile | |
| 87 | + | // the scene does not draw (no room left by the panels). | |
| 88 | + | func TestScanoutIgnoresUndrawnTile(t *testing.T) { | |
| 89 | + | s, contents := fullscreenScene() | |
| 90 | + | s.TileInset = ports.Insets{Bottom: 100} | |
| 91 | + | s.Windows = append([]ports.SceneWindow{{ID: 2, Rect: ports.Rect{W: 200, H: 100}}}, s.Windows...) | |
| 92 | + | if _, reason := scanoutCandidate(s, contents, 200, 100, nil); reason != "" { | |
| 93 | + | t.Fatalf("undrawn tile blocked scanout: %q", reason) | |
| 94 | + | } | |
| 95 | + | } | |
| 96 | + | ||
| 66 | 97 | func TestOverlayCandidate(t *testing.T) { | |
| 67 | 98 | s, c := overlayScene() | |
| 68 | 99 | if w, _, _, reason := overlayCandidate(s, c, false, nil, nil); reason != "" || w.ID != 2 { | |
| @@ -63,6 +63,37 @@ func TestOverlayWorkspaceClip(t *testing.T) { | |||||
| 63 | } | 63 | } | ||
| 64 | } | 64 | } | ||
| 65 | 65 | ||||
| 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) { | |||
| 70 | + | s, contents := overlayScene() | |||
| 71 | + | s.TileInset = ports.Insets{Bottom: 20} | |||
| 72 | + | if _, _, _, reason := overlayCandidate(s, contents, false, nil, nil); reason != "tile_clip" { | |||
| 73 | + | t.Fatalf("tile under a panel on the overlay: %q", reason) | |||
| 74 | + | } | |||
| 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}} | |||
| 81 | + | if win, _, _, reason := overlayCandidate(s, contents, false, nil, nil); reason != "" || win.ID != 2 { | |||
| 82 | + | t.Fatalf("visible tile with one under the panel: %v %q", win.ID, reason) | |||
| 83 | + | } | |||
| 84 | + | } | |||
| 85 | + | ||||
| 86 | + | // A fullscreen window is scanned out when the only other window is a tile | |||
| 87 | + | // the scene does not draw (no room left by the panels). | |||
| 88 | + | func TestScanoutIgnoresUndrawnTile(t *testing.T) { | |||
| 89 | + | s, contents := fullscreenScene() | |||
| 90 | + | s.TileInset = ports.Insets{Bottom: 100} | |||
| 91 | + | s.Windows = append([]ports.SceneWindow{{ID: 2, Rect: ports.Rect{W: 200, H: 100}}}, s.Windows...) | |||
| 92 | + | if _, reason := scanoutCandidate(s, contents, 200, 100, nil); reason != "" { | |||
| 93 | + | t.Fatalf("undrawn tile blocked scanout: %q", reason) | |||
| 94 | + | } | |||
| 95 | + | } | |||
| 96 | + | ||||
| 66 | func TestOverlayCandidate(t *testing.T) { | 97 | func TestOverlayCandidate(t *testing.T) { | ||
| 67 | s, c := overlayScene() | 98 | s, c := overlayScene() | ||
| 68 | if w, _, _, reason := overlayCandidate(s, c, false, nil, nil); reason != "" || w.ID != 2 { | 99 | if w, _, _, reason := overlayCandidate(s, c, false, nil, nil); reason != "" || w.ID != 2 { | ||
M internal/adapters/drm/scanout.go +1 -1
| @@ -100,7 +100,7 @@ func scanoutCandidate(s ports.Scene, surfaces map[ports.WindowID]ports.SurfaceCo | |||
| 100 | 100 | var full *ports.SceneWindow | |
| 101 | 101 | for i := range s.Windows { | |
| 102 | 102 | win := &s.Windows[i] | |
| 103 | - | if win.Hidden || win.Rect.W <= 0 || win.Rect.H <= 0 { | |
| 103 | + | if !s.Draws(*win) { | |
| 104 | 104 | continue | |
| 105 | 105 | } | |
| 106 | 106 | if !win.Fullscreen || !covers(&s, win) || full != nil { | |
| @@ -100,7 +100,7 @@ func scanoutCandidate(s ports.Scene, surfaces map[ports.WindowID]ports.SurfaceCo | |||||
| 100 | var full *ports.SceneWindow | 100 | var full *ports.SceneWindow | ||
| 101 | for i := range s.Windows { | 101 | for i := range s.Windows { | ||
| 102 | win := &s.Windows[i] | 102 | win := &s.Windows[i] | ||
| 103 | - | if win.Hidden || win.Rect.W <= 0 || win.Rect.H <= 0 { | 103 | + | if !s.Draws(*win) { |
| 104 | continue | 104 | continue | ||
| 105 | } | 105 | } | ||
| 106 | if !win.Fullscreen || !covers(&s, win) || full != nil { | 106 | if !win.Fullscreen || !covers(&s, win) || full != nil { | ||
M internal/adapters/drm/vrr.go +1 -1
| @@ -68,7 +68,7 @@ func (o *Output) wantContent(s ports.Scene, surfaces map[ports.WindowID]ports.Su | |||
| 68 | 68 | var full *ports.SceneWindow | |
| 69 | 69 | for i := range s.Windows { | |
| 70 | 70 | w := &s.Windows[i] | |
| 71 | - | if w.Hidden || w.Rect.W <= 0 || w.Rect.H <= 0 { | |
| 71 | + | if !s.Draws(*w) { | |
| 72 | 72 | continue | |
| 73 | 73 | } | |
| 74 | 74 | if !w.Fullscreen || !covers(&s, w) || full != nil { | |
| @@ -68,7 +68,7 @@ func (o *Output) wantContent(s ports.Scene, surfaces map[ports.WindowID]ports.Su | |||||
| 68 | var full *ports.SceneWindow | 68 | var full *ports.SceneWindow | ||
| 69 | for i := range s.Windows { | 69 | for i := range s.Windows { | ||
| 70 | w := &s.Windows[i] | 70 | w := &s.Windows[i] | ||
| 71 | - | if w.Hidden || w.Rect.W <= 0 || w.Rect.H <= 0 { | 71 | + | if !s.Draws(*w) { |
| 72 | continue | 72 | continue | ||
| 73 | } | 73 | } | ||
| 74 | if !w.Fullscreen || !covers(&s, w) || full != nil { | 74 | if !w.Fullscreen || !covers(&s, w) || full != nil { | ||
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.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.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 +2 -1
| @@ -17,7 +17,7 @@ import ( | |||
| 17 | 17 | // TestSceneFieldCount fails when Scene gains or loses a field: sceneDelta | |
| 18 | 18 | // lists the fields that force a full redraw. | |
| 19 | 19 | func TestSceneFieldCount(t *testing.T) { | |
| 20 | - | const fields = 20 | |
| 20 | + | const fields = 21 | |
| 21 | 21 | assert.Equal(t, fields, len(reflect.VisibleFields(reflect.TypeOf(ports.Scene{}))), | |
| 22 | 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 | 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 inset": func(s *ports.Scene) { s.TileInset = ports.Insets{Bottom: 10} }, | |
| 101 | 102 | "dim": func(s *ports.Scene) { s.Dim = 0.5 }, | |
| 102 | 103 | "dim behind": func(s *ports.Scene) { s.DimBehind = true }, | |
| 103 | 104 | "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: sceneDelta | 17 | // 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 = 20 | 20 | + | 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 inset": func(s *ports.Scene) { s.TileInset = ports.Insets{Bottom: 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 +26 -5
| @@ -28,7 +28,10 @@ type sceneWalk struct { | |||
| 28 | 28 | // it (premultiplied); 1 otherwise. | |
| 29 | 29 | alpha float32 | |
| 30 | 30 | bounds image.Rectangle | |
| 31 | - | draws []draw | |
| 31 | + | // tileBounds is bounds cut to Scene.TileArea: where tiles and their | |
| 32 | + | // lines draw. | |
| 33 | + | tileBounds image.Rectangle | |
| 34 | + | draws []draw | |
| 32 | 35 | } | |
| 33 | 36 | ||
| 34 | 37 | // draws walks the scene into quads in paint order. | |
| @@ -48,6 +51,10 @@ func (r *Renderer) draws(s ports.Scene, contents map[ports.WindowID]ports.Surfac | |||
| 48 | 51 | c := s.WorkspaceClip | |
| 49 | 52 | w.bounds = w.bounds.Intersect(w.physRect(c.X, c.Y, c.W, c.H)) | |
| 50 | 53 | } | |
| 54 | + | w.tileBounds = w.bounds | |
| 55 | + | if a, ok := s.TileArea(); ok { | |
| 56 | + | w.tileBounds = w.bounds.Intersect(w.physRect(a.X, a.Y, a.W, a.H)) | |
| 57 | + | } | |
| 51 | 58 | w.windows() | |
| 52 | 59 | w.popups(false) | |
| 53 | 60 | w.dropHints() | |
| @@ -207,7 +214,7 @@ func (w *sceneWalk) windows() { | |||
| 207 | 214 | continue | |
| 208 | 215 | } | |
| 209 | 216 | if opensFloats(win) && !tileLines { | |
| 210 | - | w.separators(0) | |
| 217 | + | w.tileSeparators() | |
| 211 | 218 | tileLines = true | |
| 212 | 219 | if w.s.Dim > 0 && !w.s.DimBehind { | |
| 213 | 220 | w.dim(w.bounds, w.s.Dim) | |
| @@ -219,6 +226,11 @@ func (w *sceneWalk) windows() { | |||
| 219 | 226 | continue | |
| 220 | 227 | } | |
| 221 | 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.TileArea). | |
| 232 | + | w.bounds = w.tileBounds | |
| 233 | + | } | |
| 222 | 234 | // Content sits inside the border; core sized the client to match. | |
| 223 | 235 | b, inset := 0, ports.Sides(0) | |
| 224 | 236 | if !win.Fullscreen { | |
| @@ -227,7 +239,8 @@ func (w *sceneWalk) windows() { | |||
| 227 | 239 | c := win.Rect.Inset(inset, b) | |
| 228 | 240 | body := w.physRect(c.X, c.Y, c.W, c.H) | |
| 229 | 241 | content := w.contents[win.ID] | |
| 230 | - | 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)) | |
| 231 | 244 | // Until its first buffer, a window shows the background: no flash. | |
| 232 | 245 | w.fill(body, parseColor(w.s.Background)) | |
| 233 | 246 | if !content.Empty() { | |
| @@ -249,13 +262,21 @@ func (w *sceneWalk) windows() { | |||
| 249 | 262 | // A stashed window peeking in: dimmed with its border. | |
| 250 | 263 | w.dim(w.physRect(win.Rect.X, win.Rect.Y, win.Rect.W, win.Rect.H), win.Dim) | |
| 251 | 264 | } | |
| 252 | - | w.alpha = 1 | |
| 265 | + | w.alpha, w.bounds = 1, windowBounds | |
| 253 | 266 | } | |
| 254 | 267 | if !tileLines { | |
| 255 | - | w.separators(0) | |
| 268 | + | w.tileSeparators() | |
| 256 | 269 | } | |
| 257 | 270 | } | |
| 258 | 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 | + | ||
| 259 | 280 | // separators draws a window's border lines in order, active lines over | |
| 260 | 281 | // inactive ones. | |
| 261 | 282 | func (w *sceneWalk) separators(id ports.WindowID) { | |
| @@ -28,7 +28,10 @@ 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 | - | draws []draw | 31 | + | // tileBounds is bounds cut to Scene.TileArea: where tiles and their |
| 32 | + | // lines draw. | |||
| 33 | + | tileBounds image.Rectangle | |||
| 34 | + | draws []draw | |||
| 32 | } | 35 | } | ||
| 33 | 36 | ||||
| 34 | // draws walks the scene into quads in paint order. | 37 | // draws walks the scene into quads in paint order. | ||
| @@ -48,6 +51,10 @@ func (r *Renderer) draws(s ports.Scene, contents map[ports.WindowID]ports.Surfac | |||||
| 48 | c := s.WorkspaceClip | 51 | c := s.WorkspaceClip | ||
| 49 | 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)) | ||
| 50 | } | 53 | } | ||
| 54 | + | w.tileBounds = w.bounds | |||
| 55 | + | if a, ok := s.TileArea(); ok { | |||
| 56 | + | w.tileBounds = w.bounds.Intersect(w.physRect(a.X, a.Y, a.W, a.H)) | |||
| 57 | + | } | |||
| 51 | w.windows() | 58 | w.windows() | ||
| 52 | w.popups(false) | 59 | w.popups(false) | ||
| 53 | w.dropHints() | 60 | w.dropHints() | ||
| @@ -207,7 +214,7 @@ func (w *sceneWalk) windows() { | |||||
| 207 | continue | 214 | continue | ||
| 208 | } | 215 | } | ||
| 209 | if opensFloats(win) && !tileLines { | 216 | if opensFloats(win) && !tileLines { | ||
| 210 | - | w.separators(0) | 217 | + | w.tileSeparators() |
| 211 | tileLines = true | 218 | tileLines = true | ||
| 212 | if w.s.Dim > 0 && !w.s.DimBehind { | 219 | if w.s.Dim > 0 && !w.s.DimBehind { | ||
| 213 | w.dim(w.bounds, w.s.Dim) | 220 | w.dim(w.bounds, w.s.Dim) | ||
| @@ -219,6 +226,11 @@ func (w *sceneWalk) windows() { | |||||
| 219 | continue | 226 | 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.TileArea). | |||
| 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 { | ||
| @@ -227,7 +239,8 @@ func (w *sceneWalk) windows() { | |||||
| 227 | c := win.Rect.Inset(inset, b) | 239 | c := win.Rect.Inset(inset, b) | ||
| 228 | body := w.physRect(c.X, c.Y, c.W, c.H) | 240 | body := w.physRect(c.X, c.Y, c.W, c.H) | ||
| 229 | content := w.contents[win.ID] | 241 | content := w.contents[win.ID] | ||
| 230 | - | 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)) | |||
| 231 | // Until its first buffer, a window shows the background: no flash. | 244 | // Until its first buffer, a window shows the background: no flash. | ||
| 232 | w.fill(body, parseColor(w.s.Background)) | 245 | w.fill(body, parseColor(w.s.Background)) | ||
| 233 | if !content.Empty() { | 246 | if !content.Empty() { | ||
| @@ -249,13 +262,21 @@ func (w *sceneWalk) windows() { | |||||
| 249 | // A stashed window peeking in: dimmed with its border. | 262 | // 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) | 263 | w.dim(w.physRect(win.Rect.X, win.Rect.Y, win.Rect.W, win.Rect.H), win.Dim) | ||
| 251 | } | 264 | } | ||
| 252 | - | w.alpha = 1 | 265 | + | w.alpha, w.bounds = 1, windowBounds |
| 253 | } | 266 | } | ||
| 254 | if !tileLines { | 267 | if !tileLines { | ||
| 255 | - | w.separators(0) | 268 | + | w.tileSeparators() |
| 256 | } | 269 | } | ||
| 257 | } | 270 | } | ||
| 258 | 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 | + | ||||
| 259 | // 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 | ||
| 260 | // inactive ones. | 281 | // inactive ones. | ||
| 261 | func (w *sceneWalk) separators(id ports.WindowID) { | 282 | func (w *sceneWalk) separators(id ports.WindowID) { | ||
M internal/adapters/vulkan/workspace_clip_test.go +60 -0
| @@ -8,6 +8,66 @@ import ( | |||
| 8 | 8 | "github.com/bnema/neferwl/internal/ports" | |
| 9 | 9 | ) | |
| 10 | 10 | ||
| 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 | + | 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", 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}}, | |
| 28 | + | } | |
| 29 | + | if err := render(r, scene, contents); err != nil { | |
| 30 | + | t.Fatal(err) | |
| 31 | + | } | |
| 32 | + | pixels := readPixels(t, r) | |
| 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} { | |
| 35 | + | if got := pixels.RGBAAt(point.X, point.Y); got != want { | |
| 36 | + | t.Errorf("pixel %v=%v, want %v", point, got, want) | |
| 37 | + | } | |
| 38 | + | } | |
| 39 | + | scene.Seq++ | |
| 40 | + | 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}} | |
| 41 | + | if err := render(r, scene, contents); err != nil { | |
| 42 | + | t.Fatal(err) | |
| 43 | + | } | |
| 44 | + | if got := readPixels(t, r).RGBAAt(2, 44); got != blue { | |
| 45 | + | t.Fatalf("float clipped at the panel: %v", got) | |
| 46 | + | } | |
| 47 | + | } | |
| 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 | + | ||
| 11 | 71 | func TestWorkspaceClipWindowsPopupsAndLayers(t *testing.T) { | |
| 12 | 72 | r, err := New(64, 48) | |
| 13 | 73 | if err != nil { | |
| @@ -8,6 +8,66 @@ import ( | |||||
| 8 | "github.com/bnema/neferwl/internal/ports" | 8 | "github.com/bnema/neferwl/internal/ports" | ||
| 9 | ) | 9 | ) | ||
| 10 | 10 | ||||
| 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 | + | 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", 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}}, | |||
| 28 | + | } | |||
| 29 | + | if err := render(r, scene, contents); err != nil { | |||
| 30 | + | t.Fatal(err) | |||
| 31 | + | } | |||
| 32 | + | pixels := readPixels(t, r) | |||
| 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} { | |||
| 35 | + | if got := pixels.RGBAAt(point.X, point.Y); got != want { | |||
| 36 | + | t.Errorf("pixel %v=%v, want %v", point, got, want) | |||
| 37 | + | } | |||
| 38 | + | } | |||
| 39 | + | scene.Seq++ | |||
| 40 | + | 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}} | |||
| 41 | + | if err := render(r, scene, contents); err != nil { | |||
| 42 | + | t.Fatal(err) | |||
| 43 | + | } | |||
| 44 | + | if got := readPixels(t, r).RGBAAt(2, 44); got != blue { | |||
| 45 | + | t.Fatalf("float clipped at the panel: %v", got) | |||
| 46 | + | } | |||
| 47 | + | } | |||
| 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 | + | ||||
| 11 | func TestWorkspaceClipWindowsPopupsAndLayers(t *testing.T) { | 71 | func TestWorkspaceClipWindowsPopupsAndLayers(t *testing.T) { | ||
| 12 | r, err := New(64, 48) | 72 | r, err := New(64, 48) | ||
| 13 | if err != nil { | 73 | if err != nil { | ||
M internal/adapters/wayland/scale_test.go +4 -0
| @@ -118,6 +118,10 @@ func TestPreferredBufferTransform(t *testing.T) { | |||
| 118 | 118 | } | |
| 119 | 119 | cursorSeen() | |
| 120 | 120 | requestProtocol(t, c, pointer, wayland.PointerRequestSetCursor, uint32(1), cursor, int32(0), int32(0)) | |
| 121 | + | // Roundtrip so the server has made it a cursor before the output changes. | |
| 122 | + | if got := cursorSeen(); len(got) != 0 { | |
| 123 | + | t.Fatalf("cursor surface got transforms %v before rotation", got) | |
| 124 | + | } | |
| 121 | 125 | ||
| 122 | 126 | setOutputs(1, 1) | |
| 123 | 127 | if got := normal(); !slices.Equal(got, []uint32{1}) { | |
| @@ -118,6 +118,10 @@ func TestPreferredBufferTransform(t *testing.T) { | |||||
| 118 | } | 118 | } | ||
| 119 | cursorSeen() | 119 | cursorSeen() | ||
| 120 | requestProtocol(t, c, pointer, wayland.PointerRequestSetCursor, uint32(1), cursor, int32(0), int32(0)) | 120 | requestProtocol(t, c, pointer, wayland.PointerRequestSetCursor, uint32(1), cursor, int32(0), int32(0)) | ||
| 121 | + | // Roundtrip so the server has made it a cursor before the output changes. | |||
| 122 | + | if got := cursorSeen(); len(got) != 0 { | |||
| 123 | + | t.Fatalf("cursor surface got transforms %v before rotation", got) | |||
| 124 | + | } | |||
| 121 | 125 | ||||
| 122 | setOutputs(1, 1) | 126 | setOutputs(1, 1) | ||
| 123 | if got := normal(); !slices.Equal(got, []uint32{1}) { | 127 | if got := normal(); !slices.Equal(got, []uint32{1}) { | ||
M internal/core/capture_workspace.go +5 -3
| @@ -91,7 +91,7 @@ func (c *Core) captureConfigure(sc *screen, p Placement, t configureTarget) (*Pl | |||
| 91 | 91 | // An overview preview keeps its real size and physical state. | |
| 92 | 92 | return cp, t | |
| 93 | 93 | } | |
| 94 | - | t.area = cw.frame | |
| 94 | + | t.view = whole(cw.frame) // the capture image has no panels | |
| 95 | 95 | t.focused = false | |
| 96 | 96 | t.client, t.imposed = Rect{}, false | |
| 97 | 97 | if !cp.Hidden { | |
| @@ -146,6 +146,8 @@ func (c *Core) captureScene(seq uint64) *ports.Scene { | |||
| 146 | 146 | return nil | |
| 147 | 147 | } | |
| 148 | 148 | f := cw.frame | |
| 149 | + | // The capture image has no panels: all of it is seen. | |
| 150 | + | view := whole(f) | |
| 149 | 151 | rebase := func(r Rect) Rect { r.X, r.Y = r.X-f.X, r.Y-f.Y; return r } | |
| 150 | 152 | s := &ports.Scene{ | |
| 151 | 153 | Security: c.security, | |
| @@ -153,7 +155,7 @@ func (c *Core) captureScene(seq uint64) *ports.Scene { | |||
| 153 | 155 | Scale: cw.sc.scale, Background: c.cfg.Background.Color, | |
| 154 | 156 | Border: ports.Border{Width: c.cfg.Border.Width, Active: c.cfg.Border.Active, Inactive: c.cfg.Border.Inactive}, | |
| 155 | 157 | Windows: make([]ports.SceneWindow, 0, len(cw.placements)), | |
| 156 | - | Dim: floatDim(cw.placements, f, c.cfg.Floating.Dim), | |
| 158 | + | Dim: floatDim(cw.placements, view, c.cfg.Floating.Dim), | |
| 157 | 159 | } | |
| 158 | 160 | // A hidden workspace lights nothing: its focus is not shown. | |
| 159 | 161 | for _, p := range cw.placements { | |
| @@ -164,7 +166,7 @@ func (c *Core) captureScene(seq uint64) *ports.Scene { | |||
| 164 | 166 | w.Dim = c.peekDim(p) | |
| 165 | 167 | s.Windows = append(s.Windows, w) | |
| 166 | 168 | } | |
| 167 | - | s.Separators = separators(cw.placements, c.cfg.Border.Width, cw.ws.gap(), f, false) | |
| 169 | + | s.Separators = separators(cw.placements, c.cfg.Border.Width, cw.ws.gap(), view, false) | |
| 168 | 170 | for i := range s.Separators { | |
| 169 | 171 | s.Separators[i].Rect = rebase(s.Separators[i].Rect) | |
| 170 | 172 | } | |
| @@ -91,7 +91,7 @@ func (c *Core) captureConfigure(sc *screen, p Placement, t configureTarget) (*Pl | |||||
| 91 | // An overview preview keeps its real size and physical state. | 91 | // An overview preview keeps its real size and physical state. | ||
| 92 | return cp, t | 92 | return cp, t | ||
| 93 | } | 93 | } | ||
| 94 | - | t.area = cw.frame | 94 | + | t.view = whole(cw.frame) // the capture image has no panels |
| 95 | t.focused = false | 95 | t.focused = false | ||
| 96 | t.client, t.imposed = Rect{}, false | 96 | t.client, t.imposed = Rect{}, false | ||
| 97 | if !cp.Hidden { | 97 | if !cp.Hidden { | ||
| @@ -146,6 +146,8 @@ func (c *Core) captureScene(seq uint64) *ports.Scene { | |||||
| 146 | return nil | 146 | return nil | ||
| 147 | } | 147 | } | ||
| 148 | f := cw.frame | 148 | f := cw.frame | ||
| 149 | + | // The capture image has no panels: all of it is seen. | |||
| 150 | + | view := whole(f) | |||
| 149 | rebase := func(r Rect) Rect { r.X, r.Y = r.X-f.X, r.Y-f.Y; return r } | 151 | rebase := func(r Rect) Rect { r.X, r.Y = r.X-f.X, r.Y-f.Y; return r } | ||
| 150 | s := &ports.Scene{ | 152 | s := &ports.Scene{ | ||
| 151 | Security: c.security, | 153 | Security: c.security, | ||
| @@ -153,7 +155,7 @@ func (c *Core) captureScene(seq uint64) *ports.Scene { | |||||
| 153 | Scale: cw.sc.scale, Background: c.cfg.Background.Color, | 155 | Scale: cw.sc.scale, Background: c.cfg.Background.Color, | ||
| 154 | Border: ports.Border{Width: c.cfg.Border.Width, Active: c.cfg.Border.Active, Inactive: c.cfg.Border.Inactive}, | 156 | Border: ports.Border{Width: c.cfg.Border.Width, Active: c.cfg.Border.Active, Inactive: c.cfg.Border.Inactive}, | ||
| 155 | Windows: make([]ports.SceneWindow, 0, len(cw.placements)), | 157 | Windows: make([]ports.SceneWindow, 0, len(cw.placements)), | ||
| 156 | - | Dim: floatDim(cw.placements, f, c.cfg.Floating.Dim), | 158 | + | Dim: floatDim(cw.placements, view, c.cfg.Floating.Dim), |
| 157 | } | 159 | } | ||
| 158 | // A hidden workspace lights nothing: its focus is not shown. | 160 | // A hidden workspace lights nothing: its focus is not shown. | ||
| 159 | for _, p := range cw.placements { | 161 | for _, p := range cw.placements { | ||
| @@ -164,7 +166,7 @@ func (c *Core) captureScene(seq uint64) *ports.Scene { | |||||
| 164 | w.Dim = c.peekDim(p) | 166 | w.Dim = c.peekDim(p) | ||
| 165 | s.Windows = append(s.Windows, w) | 167 | s.Windows = append(s.Windows, w) | ||
| 166 | } | 168 | } | ||
| 167 | - | s.Separators = separators(cw.placements, c.cfg.Border.Width, cw.ws.gap(), f, false) | 169 | + | s.Separators = separators(cw.placements, c.cfg.Border.Width, cw.ws.gap(), view, false) |
| 168 | for i := range s.Separators { | 170 | for i := range s.Separators { | ||
| 169 | s.Separators[i].Rect = rebase(s.Separators[i].Rect) | 171 | s.Separators[i].Rect = rebase(s.Separators[i].Rect) | ||
| 170 | } | 172 | } | ||
M internal/core/capture_workspace_test.go +2 -2
| @@ -47,7 +47,7 @@ func TestCaptureConfigureSizesHiddenWindowWithoutShowingIt(t *testing.T) { | |||
| 47 | 47 | c, _ := hiddenCapture(t) | |
| 48 | 48 | sc := c.screens[0] | |
| 49 | 49 | phys := Placement{ID: 1, Hidden: true} | |
| 50 | - | t0 := configureTarget{output: "A", area: sc.mon.Frame()} | |
| 50 | + | t0 := configureTarget{output: "A", view: sc.mon.viewport()} | |
| 51 | 51 | cp, tt := c.captureConfigure(sc, phys, t0) | |
| 52 | 52 | if cp == nil || cp.Hidden { | |
| 53 | 53 | t.Fatalf("no real placement: %+v", cp) | |
| @@ -80,7 +80,7 @@ func TestCaptureConfigureLeavesOtherWindowsAlone(t *testing.T) { | |||
| 80 | 80 | c, _ := hiddenCapture(t) | |
| 81 | 81 | sc := c.screens[0] | |
| 82 | 82 | phys := Placement{ID: 99, Hidden: true} | |
| 83 | - | t0 := configureTarget{output: "A", area: sc.mon.Frame()} | |
| 83 | + | t0 := configureTarget{output: "A", view: sc.mon.viewport()} | |
| 84 | 84 | if cp, tt := c.captureConfigure(sc, phys, t0); cp != nil || tt != t0 { | |
| 85 | 85 | t.Fatalf("window of another workspace captured: %+v", cp) | |
| 86 | 86 | } | |
| @@ -47,7 +47,7 @@ func TestCaptureConfigureSizesHiddenWindowWithoutShowingIt(t *testing.T) { | |||||
| 47 | c, _ := hiddenCapture(t) | 47 | c, _ := hiddenCapture(t) | ||
| 48 | sc := c.screens[0] | 48 | sc := c.screens[0] | ||
| 49 | phys := Placement{ID: 1, Hidden: true} | 49 | phys := Placement{ID: 1, Hidden: true} | ||
| 50 | - | t0 := configureTarget{output: "A", area: sc.mon.Frame()} | 50 | + | t0 := configureTarget{output: "A", view: sc.mon.viewport()} |
| 51 | cp, tt := c.captureConfigure(sc, phys, t0) | 51 | cp, tt := c.captureConfigure(sc, phys, t0) | ||
| 52 | if cp == nil || cp.Hidden { | 52 | if cp == nil || cp.Hidden { | ||
| 53 | t.Fatalf("no real placement: %+v", cp) | 53 | t.Fatalf("no real placement: %+v", cp) | ||
| @@ -80,7 +80,7 @@ func TestCaptureConfigureLeavesOtherWindowsAlone(t *testing.T) { | |||||
| 80 | c, _ := hiddenCapture(t) | 80 | c, _ := hiddenCapture(t) | ||
| 81 | sc := c.screens[0] | 81 | sc := c.screens[0] | ||
| 82 | phys := Placement{ID: 99, Hidden: true} | 82 | phys := Placement{ID: 99, Hidden: true} | ||
| 83 | - | t0 := configureTarget{output: "A", area: sc.mon.Frame()} | 83 | + | t0 := configureTarget{output: "A", view: sc.mon.viewport()} |
| 84 | if cp, tt := c.captureConfigure(sc, phys, t0); cp != nil || tt != t0 { | 84 | if cp, tt := c.captureConfigure(sc, phys, t0); cp != nil || tt != t0 { | ||
| 85 | t.Fatalf("window of another workspace captured: %+v", cp) | 85 | t.Fatalf("window of another workspace captured: %+v", cp) | ||
| 86 | } | 86 | } | ||
M internal/core/configures.go +5 -5
| @@ -5,8 +5,8 @@ import "github.com/bnema/neferwl/internal/ports" | |||
| 5 | 5 | // configureTarget is where a placement is shown, as the configure needs it. | |
| 6 | 6 | type configureTarget struct { | |
| 7 | 7 | output string | |
| 8 | - | area Rect // the output, to tell whether the window is on screen | |
| 9 | - | focused bool // activated: focused on the focused output | |
| 8 | + | view viewport // where the window is seen, to tell whether it is | |
| 9 | + | focused bool // activated: focused on the focused output | |
| 10 | 10 | // client is the placement minus borders; imposed reports core sized a | |
| 11 | 11 | // floating window (native floats pick their own size). | |
| 12 | 12 | client Rect | |
| @@ -109,14 +109,14 @@ func build(p Placement, t configureTarget, old ports.ConfigureWindow, ok bool) p | |||
| 109 | 109 | if p.Fullscreen && !v.Fullscreen { | |
| 110 | 110 | // A float made fullscreen off screen: its preview row shows | |
| 111 | 111 | // it fullscreen, sized for the output, never the preview. | |
| 112 | - | v.Width, v.Height, v.Fullscreen, v.Floating = t.area.W, t.area.H, true, false | |
| 112 | + | v.Width, v.Height, v.Fullscreen, v.Floating = t.view.frame.W, t.view.frame.H, true, false | |
| 113 | 113 | } | |
| 114 | 114 | if t.realTiled { | |
| 115 | 115 | v.Width, v.Height = t.client.W, t.client.H | |
| 116 | 116 | } | |
| 117 | - | v.Activated, v.Visible, v.Output, v.Captured = t.focused, onScreen(p, t.area), t.output, false | |
| 117 | + | v.Activated, v.Visible, v.Output, v.Captured = t.focused, t.view.shows(p), t.output, false | |
| 118 | 118 | default: | |
| 119 | - | v = ports.ConfigureWindow{ID: p.ID, Width: t.client.W, Height: t.client.H, Fullscreen: p.Fullscreen, Activated: t.focused, Floating: p.Floating && !p.Fullscreen, Output: t.output, Visible: onScreen(p, t.area)} | |
| 119 | + | v = ports.ConfigureWindow{ID: p.ID, Width: t.client.W, Height: t.client.H, Fullscreen: p.Fullscreen, Activated: t.focused, Floating: p.Floating && !p.Fullscreen, Output: t.output, Visible: t.view.shows(p)} | |
| 120 | 120 | if v.Floating && !t.imposed { | |
| 121 | 121 | v.Width, v.Height = 0, 0 | |
| 122 | 122 | } | |
| @@ -5,8 +5,8 @@ import "github.com/bnema/neferwl/internal/ports" | |||||
| 5 | // configureTarget is where a placement is shown, as the configure needs it. | 5 | // configureTarget is where a placement is shown, as the configure needs it. | ||
| 6 | type configureTarget struct { | 6 | type configureTarget struct { | ||
| 7 | output string | 7 | output string | ||
| 8 | - | area Rect // the output, to tell whether the window is on screen | 8 | + | view viewport // where the window is seen, to tell whether it is |
| 9 | - | focused bool // activated: focused on the focused output | 9 | + | focused bool // activated: focused on the focused output |
| 10 | // client is the placement minus borders; imposed reports core sized a | 10 | // client is the placement minus borders; imposed reports core sized a | ||
| 11 | // floating window (native floats pick their own size). | 11 | // floating window (native floats pick their own size). | ||
| 12 | client Rect | 12 | client Rect | ||
| @@ -109,14 +109,14 @@ func build(p Placement, t configureTarget, old ports.ConfigureWindow, ok bool) p | |||||
| 109 | if p.Fullscreen && !v.Fullscreen { | 109 | if p.Fullscreen && !v.Fullscreen { | ||
| 110 | // A float made fullscreen off screen: its preview row shows | 110 | // A float made fullscreen off screen: its preview row shows | ||
| 111 | // it fullscreen, sized for the output, never the preview. | 111 | // it fullscreen, sized for the output, never the preview. | ||
| 112 | - | v.Width, v.Height, v.Fullscreen, v.Floating = t.area.W, t.area.H, true, false | 112 | + | v.Width, v.Height, v.Fullscreen, v.Floating = t.view.frame.W, t.view.frame.H, true, false |
| 113 | } | 113 | } | ||
| 114 | if t.realTiled { | 114 | if t.realTiled { | ||
| 115 | v.Width, v.Height = t.client.W, t.client.H | 115 | v.Width, v.Height = t.client.W, t.client.H | ||
| 116 | } | 116 | } | ||
| 117 | - | v.Activated, v.Visible, v.Output, v.Captured = t.focused, onScreen(p, t.area), t.output, false | 117 | + | v.Activated, v.Visible, v.Output, v.Captured = t.focused, t.view.shows(p), t.output, false |
| 118 | default: | 118 | default: | ||
| 119 | - | v = ports.ConfigureWindow{ID: p.ID, Width: t.client.W, Height: t.client.H, Fullscreen: p.Fullscreen, Activated: t.focused, Floating: p.Floating && !p.Fullscreen, Output: t.output, Visible: onScreen(p, t.area)} | 119 | + | v = ports.ConfigureWindow{ID: p.ID, Width: t.client.W, Height: t.client.H, Fullscreen: p.Fullscreen, Activated: t.focused, Floating: p.Floating && !p.Fullscreen, Output: t.output, Visible: t.view.shows(p)} |
| 120 | if v.Floating && !t.imposed { | 120 | if v.Floating && !t.imposed { | ||
| 121 | v.Width, v.Height = 0, 0 | 121 | v.Width, v.Height = 0, 0 | ||
| 122 | } | 122 | } | ||
M internal/core/configures_test.go +6 -6
| @@ -9,7 +9,7 @@ import ( | |||
| 9 | 9 | func TestConfiguresNext(t *testing.T) { | |
| 10 | 10 | area := Rect{W: 1000, H: 800} | |
| 11 | 11 | tiled := Placement{ID: 1, Rect: Rect{W: 500, H: 800}} | |
| 12 | - | target := configureTarget{output: "A", area: area, focused: true, client: Rect{W: 496, H: 796}} | |
| 12 | + | target := configureTarget{output: "A", view: whole(area), focused: true, client: Rect{W: 496, H: 796}} | |
| 13 | 13 | tests := []struct { | |
| 14 | 14 | name string | |
| 15 | 15 | first *Placement // sent before p, with target | |
| @@ -22,14 +22,14 @@ func TestConfiguresNext(t *testing.T) { | |||
| 22 | 22 | {name: "unchanged", first: &tiled, p: tiled, t: target, want: ports.ConfigureWindow{ID: 1, Width: 496, Height: 796, Activated: true, Output: "A", Visible: true}}, | |
| 23 | 23 | {name: "scrolled off", p: Placement{ID: 1, Rect: Rect{X: 1000, W: 500, H: 800}}, t: target, want: ports.ConfigureWindow{ID: 1, Width: 496, Height: 796, Activated: true, Output: "A"}, send: true}, | |
| 24 | 24 | {name: "native float", p: Placement{ID: 1, Rect: Rect{W: 300, H: 200}, Floating: true}, t: target, want: ports.ConfigureWindow{ID: 1, Activated: true, Floating: true, Output: "A", Visible: true}, send: true}, | |
| 25 | - | {name: "imposed float", p: Placement{ID: 1, Rect: Rect{W: 300, H: 200}, Floating: true}, t: configureTarget{output: "A", area: area, client: Rect{W: 300, H: 200}, imposed: true}, want: ports.ConfigureWindow{ID: 1, Width: 300, Height: 200, Floating: true, Output: "A", Visible: true}, send: true}, | |
| 26 | - | {name: "fullscreen float", p: Placement{ID: 1, Rect: area, Floating: true, Fullscreen: true}, t: configureTarget{output: "A", area: area, client: area}, want: ports.ConfigureWindow{ID: 1, Width: 1000, Height: 800, Fullscreen: true, Output: "A", Visible: true}, send: true}, | |
| 25 | + | {name: "imposed float", p: Placement{ID: 1, Rect: Rect{W: 300, H: 200}, Floating: true}, t: configureTarget{output: "A", view: whole(area), client: Rect{W: 300, H: 200}, imposed: true}, want: ports.ConfigureWindow{ID: 1, Width: 300, Height: 200, Floating: true, Output: "A", Visible: true}, send: true}, | |
| 26 | + | {name: "fullscreen float", p: Placement{ID: 1, Rect: area, Floating: true, Fullscreen: true}, t: configureTarget{output: "A", view: whole(area), client: area}, want: ports.ConfigureWindow{ID: 1, Width: 1000, Height: 800, Fullscreen: true, Output: "A", Visible: true}, send: true}, | |
| 27 | 27 | {name: "hidden new", p: Placement{ID: 1, Hidden: true, Floating: true}, t: target, want: ports.ConfigureWindow{ID: 1, Floating: true, Output: "A"}, send: true}, | |
| 28 | 28 | {name: "hidden keeps size", first: &tiled, p: Placement{ID: 1, Hidden: true}, t: configureTarget{output: "B"}, want: ports.ConfigureWindow{ID: 1, Width: 496, Height: 796, Output: "B"}, send: true}, | |
| 29 | 29 | {name: "preview new", p: Placement{ID: 1, Rect: Rect{W: 250, H: 200}, Preview: 0.5}, t: target, want: ports.ConfigureWindow{ID: 1, Width: 500, Height: 400, Activated: true, Output: "A", Visible: true}, send: true}, | |
| 30 | - | {name: "fullscreen preview keeps size and state", first: &Placement{ID: 1, Rect: area, Floating: true, Fullscreen: true}, p: Placement{ID: 1, Rect: Rect{W: 100, H: 80}, Preview: 0.1}, t: configureTarget{output: "A", area: area}, want: ports.ConfigureWindow{ID: 1, Width: 496, Height: 796, Fullscreen: true, Output: "A", Visible: true}, send: true}, | |
| 31 | - | {name: "hidden float made fullscreen previews fullscreen", first: &Placement{ID: 1, Hidden: true, Floating: true}, p: Placement{ID: 1, Rect: Rect{W: 100, H: 80}, Preview: 0.1, Fullscreen: true}, t: configureTarget{output: "A", area: area}, want: ports.ConfigureWindow{ID: 1, Width: 1000, Height: 800, Fullscreen: true, Output: "A", Visible: true}, send: true}, | |
| 32 | - | {name: "preview keeps size", first: &tiled, p: Placement{ID: 1, Rect: Rect{W: 100, H: 100}, Preview: 0.5}, t: configureTarget{output: "A", area: area}, want: ports.ConfigureWindow{ID: 1, Width: 496, Height: 796, Output: "A", Visible: true}, send: true}, | |
| 30 | + | {name: "fullscreen preview keeps size and state", first: &Placement{ID: 1, Rect: area, Floating: true, Fullscreen: true}, p: Placement{ID: 1, Rect: Rect{W: 100, H: 80}, Preview: 0.1}, t: configureTarget{output: "A", view: whole(area)}, want: ports.ConfigureWindow{ID: 1, Width: 496, Height: 796, Fullscreen: true, Output: "A", Visible: true}, send: true}, | |
| 31 | + | {name: "hidden float made fullscreen previews fullscreen", first: &Placement{ID: 1, Hidden: true, Floating: true}, p: Placement{ID: 1, Rect: Rect{W: 100, H: 80}, Preview: 0.1, Fullscreen: true}, t: configureTarget{output: "A", view: whole(area)}, want: ports.ConfigureWindow{ID: 1, Width: 1000, Height: 800, Fullscreen: true, Output: "A", Visible: true}, send: true}, | |
| 32 | + | {name: "preview keeps size", first: &tiled, p: Placement{ID: 1, Rect: Rect{W: 100, H: 100}, Preview: 0.5}, t: configureTarget{output: "A", view: whole(area)}, want: ports.ConfigureWindow{ID: 1, Width: 496, Height: 796, Output: "A", Visible: true}, send: true}, | |
| 33 | 33 | {name: "new float preview stays floating", p: Placement{ID: 1, Rect: Rect{W: 600, H: 480}, Preview: 0.6, Floating: true}, t: target, want: ports.ConfigureWindow{ID: 1, Floating: true, Activated: true, Output: "A", Visible: true}, send: true}, | |
| 34 | 34 | {name: "covering float preview keeps native size", first: &Placement{ID: 1, Rect: area, Floating: true}, p: Placement{ID: 1, Rect: Rect{W: 600, H: 480}, Preview: 0.6, Floating: true}, t: target, want: ports.ConfigureWindow{ID: 1, Floating: true, Activated: true, Output: "A", Visible: true}}, | |
| 35 | 35 | {name: "covering float peek keeps native size", first: &Placement{ID: 1, Rect: area, Floating: true}, p: Placement{ID: 1, Rect: Rect{X: 20, Y: 20, W: 600, H: 480}, Preview: 0.6, Floating: true, Peek: true}, t: target, want: ports.ConfigureWindow{ID: 1, Floating: true, Activated: true, Output: "A", Visible: true}}, | |
| @@ -9,7 +9,7 @@ import ( | |||||
| 9 | func TestConfiguresNext(t *testing.T) { | 9 | func TestConfiguresNext(t *testing.T) { | ||
| 10 | area := Rect{W: 1000, H: 800} | 10 | area := Rect{W: 1000, H: 800} | ||
| 11 | tiled := Placement{ID: 1, Rect: Rect{W: 500, H: 800}} | 11 | tiled := Placement{ID: 1, Rect: Rect{W: 500, H: 800}} | ||
| 12 | - | target := configureTarget{output: "A", area: area, focused: true, client: Rect{W: 496, H: 796}} | 12 | + | target := configureTarget{output: "A", view: whole(area), focused: true, client: Rect{W: 496, H: 796}} |
| 13 | tests := []struct { | 13 | tests := []struct { | ||
| 14 | name string | 14 | name string | ||
| 15 | first *Placement // sent before p, with target | 15 | first *Placement // sent before p, with target | ||
| @@ -22,14 +22,14 @@ func TestConfiguresNext(t *testing.T) { | |||||
| 22 | {name: "unchanged", first: &tiled, p: tiled, t: target, want: ports.ConfigureWindow{ID: 1, Width: 496, Height: 796, Activated: true, Output: "A", Visible: true}}, | 22 | {name: "unchanged", first: &tiled, p: tiled, t: target, want: ports.ConfigureWindow{ID: 1, Width: 496, Height: 796, Activated: true, Output: "A", Visible: true}}, | ||
| 23 | {name: "scrolled off", p: Placement{ID: 1, Rect: Rect{X: 1000, W: 500, H: 800}}, t: target, want: ports.ConfigureWindow{ID: 1, Width: 496, Height: 796, Activated: true, Output: "A"}, send: true}, | 23 | {name: "scrolled off", p: Placement{ID: 1, Rect: Rect{X: 1000, W: 500, H: 800}}, t: target, want: ports.ConfigureWindow{ID: 1, Width: 496, Height: 796, Activated: true, Output: "A"}, send: true}, | ||
| 24 | {name: "native float", p: Placement{ID: 1, Rect: Rect{W: 300, H: 200}, Floating: true}, t: target, want: ports.ConfigureWindow{ID: 1, Activated: true, Floating: true, Output: "A", Visible: true}, send: true}, | 24 | {name: "native float", p: Placement{ID: 1, Rect: Rect{W: 300, H: 200}, Floating: true}, t: target, want: ports.ConfigureWindow{ID: 1, Activated: true, Floating: true, Output: "A", Visible: true}, send: true}, | ||
| 25 | - | {name: "imposed float", p: Placement{ID: 1, Rect: Rect{W: 300, H: 200}, Floating: true}, t: configureTarget{output: "A", area: area, client: Rect{W: 300, H: 200}, imposed: true}, want: ports.ConfigureWindow{ID: 1, Width: 300, Height: 200, Floating: true, Output: "A", Visible: true}, send: true}, | 25 | + | {name: "imposed float", p: Placement{ID: 1, Rect: Rect{W: 300, H: 200}, Floating: true}, t: configureTarget{output: "A", view: whole(area), client: Rect{W: 300, H: 200}, imposed: true}, want: ports.ConfigureWindow{ID: 1, Width: 300, Height: 200, Floating: true, Output: "A", Visible: true}, send: true}, |
| 26 | - | {name: "fullscreen float", p: Placement{ID: 1, Rect: area, Floating: true, Fullscreen: true}, t: configureTarget{output: "A", area: area, client: area}, want: ports.ConfigureWindow{ID: 1, Width: 1000, Height: 800, Fullscreen: true, Output: "A", Visible: true}, send: true}, | 26 | + | {name: "fullscreen float", p: Placement{ID: 1, Rect: area, Floating: true, Fullscreen: true}, t: configureTarget{output: "A", view: whole(area), client: area}, want: ports.ConfigureWindow{ID: 1, Width: 1000, Height: 800, Fullscreen: true, Output: "A", Visible: true}, send: true}, |
| 27 | {name: "hidden new", p: Placement{ID: 1, Hidden: true, Floating: true}, t: target, want: ports.ConfigureWindow{ID: 1, Floating: true, Output: "A"}, send: true}, | 27 | {name: "hidden new", p: Placement{ID: 1, Hidden: true, Floating: true}, t: target, want: ports.ConfigureWindow{ID: 1, Floating: true, Output: "A"}, send: true}, | ||
| 28 | {name: "hidden keeps size", first: &tiled, p: Placement{ID: 1, Hidden: true}, t: configureTarget{output: "B"}, want: ports.ConfigureWindow{ID: 1, Width: 496, Height: 796, Output: "B"}, send: true}, | 28 | {name: "hidden keeps size", first: &tiled, p: Placement{ID: 1, Hidden: true}, t: configureTarget{output: "B"}, want: ports.ConfigureWindow{ID: 1, Width: 496, Height: 796, Output: "B"}, send: true}, | ||
| 29 | {name: "preview new", p: Placement{ID: 1, Rect: Rect{W: 250, H: 200}, Preview: 0.5}, t: target, want: ports.ConfigureWindow{ID: 1, Width: 500, Height: 400, Activated: true, Output: "A", Visible: true}, send: true}, | 29 | {name: "preview new", p: Placement{ID: 1, Rect: Rect{W: 250, H: 200}, Preview: 0.5}, t: target, want: ports.ConfigureWindow{ID: 1, Width: 500, Height: 400, Activated: true, Output: "A", Visible: true}, send: true}, | ||
| 30 | - | {name: "fullscreen preview keeps size and state", first: &Placement{ID: 1, Rect: area, Floating: true, Fullscreen: true}, p: Placement{ID: 1, Rect: Rect{W: 100, H: 80}, Preview: 0.1}, t: configureTarget{output: "A", area: area}, want: ports.ConfigureWindow{ID: 1, Width: 496, Height: 796, Fullscreen: true, Output: "A", Visible: true}, send: true}, | 30 | + | {name: "fullscreen preview keeps size and state", first: &Placement{ID: 1, Rect: area, Floating: true, Fullscreen: true}, p: Placement{ID: 1, Rect: Rect{W: 100, H: 80}, Preview: 0.1}, t: configureTarget{output: "A", view: whole(area)}, want: ports.ConfigureWindow{ID: 1, Width: 496, Height: 796, Fullscreen: true, Output: "A", Visible: true}, send: true}, |
| 31 | - | {name: "hidden float made fullscreen previews fullscreen", first: &Placement{ID: 1, Hidden: true, Floating: true}, p: Placement{ID: 1, Rect: Rect{W: 100, H: 80}, Preview: 0.1, Fullscreen: true}, t: configureTarget{output: "A", area: area}, want: ports.ConfigureWindow{ID: 1, Width: 1000, Height: 800, Fullscreen: true, Output: "A", Visible: true}, send: true}, | 31 | + | {name: "hidden float made fullscreen previews fullscreen", first: &Placement{ID: 1, Hidden: true, Floating: true}, p: Placement{ID: 1, Rect: Rect{W: 100, H: 80}, Preview: 0.1, Fullscreen: true}, t: configureTarget{output: "A", view: whole(area)}, want: ports.ConfigureWindow{ID: 1, Width: 1000, Height: 800, Fullscreen: true, Output: "A", Visible: true}, send: true}, |
| 32 | - | {name: "preview keeps size", first: &tiled, p: Placement{ID: 1, Rect: Rect{W: 100, H: 100}, Preview: 0.5}, t: configureTarget{output: "A", area: area}, want: ports.ConfigureWindow{ID: 1, Width: 496, Height: 796, Output: "A", Visible: true}, send: true}, | 32 | + | {name: "preview keeps size", first: &tiled, p: Placement{ID: 1, Rect: Rect{W: 100, H: 100}, Preview: 0.5}, t: configureTarget{output: "A", view: whole(area)}, want: ports.ConfigureWindow{ID: 1, Width: 496, Height: 796, Output: "A", Visible: true}, send: true}, |
| 33 | {name: "new float preview stays floating", p: Placement{ID: 1, Rect: Rect{W: 600, H: 480}, Preview: 0.6, Floating: true}, t: target, want: ports.ConfigureWindow{ID: 1, Floating: true, Activated: true, Output: "A", Visible: true}, send: true}, | 33 | {name: "new float preview stays floating", p: Placement{ID: 1, Rect: Rect{W: 600, H: 480}, Preview: 0.6, Floating: true}, t: target, want: ports.ConfigureWindow{ID: 1, Floating: true, Activated: true, Output: "A", Visible: true}, send: true}, | ||
| 34 | {name: "covering float preview keeps native size", first: &Placement{ID: 1, Rect: area, Floating: true}, p: Placement{ID: 1, Rect: Rect{W: 600, H: 480}, Preview: 0.6, Floating: true}, t: target, want: ports.ConfigureWindow{ID: 1, Floating: true, Activated: true, Output: "A", Visible: true}}, | 34 | {name: "covering float preview keeps native size", first: &Placement{ID: 1, Rect: area, Floating: true}, p: Placement{ID: 1, Rect: Rect{W: 600, H: 480}, Preview: 0.6, Floating: true}, t: target, want: ports.ConfigureWindow{ID: 1, Floating: true, Activated: true, Output: "A", Visible: true}}, | ||
| 35 | {name: "covering float peek keeps native size", first: &Placement{ID: 1, Rect: area, Floating: true}, p: Placement{ID: 1, Rect: Rect{X: 20, Y: 20, W: 600, H: 480}, Preview: 0.6, Floating: true, Peek: true}, t: target, want: ports.ConfigureWindow{ID: 1, Floating: true, Activated: true, Output: "A", Visible: true}}, | 35 | {name: "covering float peek keeps native size", first: &Placement{ID: 1, Rect: area, Floating: true}, p: Placement{ID: 1, Rect: Rect{X: 20, Y: 20, W: 600, H: 480}, Preview: 0.6, Floating: true, Peek: true}, t: target, want: ports.ConfigureWindow{ID: 1, Floating: true, Activated: true, Output: "A", Visible: true}}, | ||
M internal/core/core.go +36 -8
| @@ -458,10 +458,36 @@ func (c *Core) clientRect(p Placement) Rect { | |||
| 458 | 458 | return p.Rect.Inset(p.Inset, c.cfg.Border.Width) | |
| 459 | 459 | } | |
| 460 | 460 | ||
| 461 | - | // onScreen reports whether a placement is drawn on its output o: not | |
| 462 | - | // hidden and not scrolled off. | |
| 463 | - | func onScreen(p Placement, o Rect) bool { | |
| 464 | - | return !p.Hidden && p.Rect.Overlaps(o) | |
| 461 | + | // viewport is where a workspace is seen: frame is the whole viewport, | |
| 462 | + | // usable the part exclusive layers (panels) leave to the tiles. | |
| 463 | + | type viewport struct{ frame, usable Rect } | |
| 464 | + | ||
| 465 | + | // whole is a viewport with no panel: all of r is usable. | |
| 466 | + | func whole(r Rect) viewport { return viewport{frame: r, usable: r} } | |
| 467 | + | ||
| 468 | + | // 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.TileInset), 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 { | |
| 473 | + | if p.Floating || p.Fullscreen || p.Preview > 0 { | |
| 474 | + | return v.frame | |
| 475 | + | } | |
| 476 | + | return v.usable | |
| 477 | + | } | |
| 478 | + | ||
| 479 | + | // hasRoom reports whether p's area can show anything: panels can leave | |
| 480 | + | // tiles no room at all. Unlike shows, it ignores where p is, so a window | |
| 481 | + | // still sliding in counts. | |
| 482 | + | func hasRoom(v viewport, p Placement) bool { | |
| 483 | + | a := v.area(p) | |
| 484 | + | return a.W > 0 && a.H > 0 | |
| 485 | + | } | |
| 486 | + | ||
| 487 | + | // shows reports whether the user sees p: not hidden, and overlapping its | |
| 488 | + | // area. | |
| 489 | + | func (v viewport) shows(p Placement) bool { | |
| 490 | + | return !p.Hidden && p.Rect.Overlaps(v.area(p)) | |
| 465 | 491 | } | |
| 466 | 492 | ||
| 467 | 493 | // floatDim is the veil opacity of a layout: dim only when a float is | |
| @@ -470,9 +496,9 @@ func onScreen(p Placement, o Rect) bool { | |||
| 470 | 496 | // drops the veil at once: a veil that fades changes Scene.Dim every frame, | |
| 471 | 497 | // which redraws the whole output for the length of the fade, where the | |
| 472 | 498 | // float's own fade redraws only its rect. | |
| 473 | - | func floatDim(layout []Placement, o Rect, dim float64) float64 { | |
| 499 | + | func floatDim(layout []Placement, v viewport, dim float64) float64 { | |
| 474 | 500 | for _, p := range layout { | |
| 475 | - | if p.Floating && !p.Below && p.Preview == 0 && onScreen(p, o) { | |
| 501 | + | if p.Floating && !p.Below && p.Preview == 0 && v.shows(p) { | |
| 476 | 502 | if p.Fullscreen { | |
| 477 | 503 | return 0 | |
| 478 | 504 | } | |
| @@ -809,8 +835,7 @@ func (c *Core) shownClient(id WindowID) (full, visible Rect, ok bool) { | |||
| 809 | 835 | } | |
| 810 | 836 | r := c.clientRect(p) | |
| 811 | 837 | full = Rect{X: r.X + s.x, Y: r.Y + s.y, W: r.W, H: r.H} | |
| 812 | - | f := s.mon.Frame() | |
| 813 | - | visible = intersect(r, f) | |
| 838 | + | visible = intersect(r, s.mon.viewport().area(p)) | |
| 814 | 839 | visible.X, visible.Y = visible.X+s.x, visible.Y+s.y | |
| 815 | 840 | if visible.W <= 0 || visible.H <= 0 { | |
| 816 | 841 | return Rect{}, Rect{}, false | |
| @@ -956,6 +981,7 @@ func (c *Core) hit(x, y float64) (WindowID, float64, float64) { | |||
| 956 | 981 | } | |
| 957 | 982 | var id WindowID | |
| 958 | 983 | var sx, sy float64 | |
| 984 | + | view := sc.mon.viewport() | |
| 959 | 985 | for _, p := range sc.shownLayout() { | |
| 960 | 986 | r := c.clientRect(p) | |
| 961 | 987 | // A peek is clickable wherever it shows, border included: it may | |
| @@ -964,6 +990,8 @@ func (c *Core) hit(x, y float64) (WindowID, float64, float64) { | |||
| 964 | 990 | if p.Peek { | |
| 965 | 991 | box = p.Rect | |
| 966 | 992 | } | |
| 993 | + | // Only what is drawn takes the pointer: a tile not over a panel. | |
| 994 | + | box = intersect(box, view.area(p)) | |
| 967 | 995 | 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) { | |
| 968 | 996 | if p.Peek { | |
| 969 | 997 | 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)) | |
| @@ -458,10 +458,36 @@ func (c *Core) clientRect(p Placement) Rect { | |||||
| 458 | return p.Rect.Inset(p.Inset, c.cfg.Border.Width) | 458 | return p.Rect.Inset(p.Inset, c.cfg.Border.Width) | ||
| 459 | } | 459 | } | ||
| 460 | 460 | ||||
| 461 | - | // onScreen reports whether a placement is drawn on its output o: not | 461 | + | // viewport is where a workspace is seen: frame is the whole viewport, |
| 462 | - | // hidden and not scrolled off. | 462 | + | // usable the part exclusive layers (panels) leave to the tiles. |
| 463 | - | func onScreen(p Placement, o Rect) bool { | 463 | + | type viewport struct{ frame, usable Rect } |
| 464 | - | return !p.Hidden && p.Rect.Overlaps(o) | 464 | + | |
| 465 | + | // whole is a viewport with no panel: all of r is usable. | |||
| 466 | + | func whole(r Rect) viewport { return viewport{frame: r, usable: r} } | |||
| 467 | + | ||||
| 468 | + | // 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.TileInset), 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 { | |||
| 473 | + | if p.Floating || p.Fullscreen || p.Preview > 0 { | |||
| 474 | + | return v.frame | |||
| 475 | + | } | |||
| 476 | + | return v.usable | |||
| 477 | + | } | |||
| 478 | + | ||||
| 479 | + | // hasRoom reports whether p's area can show anything: panels can leave | |||
| 480 | + | // tiles no room at all. Unlike shows, it ignores where p is, so a window | |||
| 481 | + | // still sliding in counts. | |||
| 482 | + | func hasRoom(v viewport, p Placement) bool { | |||
| 483 | + | a := v.area(p) | |||
| 484 | + | return a.W > 0 && a.H > 0 | |||
| 485 | + | } | |||
| 486 | + | ||||
| 487 | + | // shows reports whether the user sees p: not hidden, and overlapping its | |||
| 488 | + | // area. | |||
| 489 | + | func (v viewport) shows(p Placement) bool { | |||
| 490 | + | return !p.Hidden && p.Rect.Overlaps(v.area(p)) | |||
| 465 | } | 491 | } | ||
| 466 | 492 | ||||
| 467 | // floatDim is the veil opacity of a layout: dim only when a float is | 493 | // floatDim is the veil opacity of a layout: dim only when a float is | ||
| @@ -470,9 +496,9 @@ func onScreen(p Placement, o Rect) bool { | |||||
| 470 | // drops the veil at once: a veil that fades changes Scene.Dim every frame, | 496 | // drops the veil at once: a veil that fades changes Scene.Dim every frame, | ||
| 471 | // which redraws the whole output for the length of the fade, where the | 497 | // which redraws the whole output for the length of the fade, where the | ||
| 472 | // float's own fade redraws only its rect. | 498 | // float's own fade redraws only its rect. | ||
| 473 | - | func floatDim(layout []Placement, o Rect, dim float64) float64 { | 499 | + | func floatDim(layout []Placement, v viewport, dim float64) float64 { |
| 474 | for _, p := range layout { | 500 | for _, p := range layout { | ||
| 475 | - | if p.Floating && !p.Below && p.Preview == 0 && onScreen(p, o) { | 501 | + | if p.Floating && !p.Below && p.Preview == 0 && v.shows(p) { |
| 476 | if p.Fullscreen { | 502 | if p.Fullscreen { | ||
| 477 | return 0 | 503 | return 0 | ||
| 478 | } | 504 | } | ||
| @@ -809,8 +835,7 @@ func (c *Core) shownClient(id WindowID) (full, visible Rect, ok bool) { | |||||
| 809 | } | 835 | } | ||
| 810 | r := c.clientRect(p) | 836 | r := c.clientRect(p) | ||
| 811 | full = Rect{X: r.X + s.x, Y: r.Y + s.y, W: r.W, H: r.H} | 837 | full = Rect{X: r.X + s.x, Y: r.Y + s.y, W: r.W, H: r.H} | ||
| 812 | - | f := s.mon.Frame() | 838 | + | visible = intersect(r, s.mon.viewport().area(p)) |
| 813 | - | visible = intersect(r, f) | |||
| 814 | visible.X, visible.Y = visible.X+s.x, visible.Y+s.y | 839 | visible.X, visible.Y = visible.X+s.x, visible.Y+s.y | ||
| 815 | if visible.W <= 0 || visible.H <= 0 { | 840 | if visible.W <= 0 || visible.H <= 0 { | ||
| 816 | return Rect{}, Rect{}, false | 841 | return Rect{}, Rect{}, false | ||
| @@ -956,6 +981,7 @@ func (c *Core) hit(x, y float64) (WindowID, float64, float64) { | |||||
| 956 | } | 981 | } | ||
| 957 | var id WindowID | 982 | var id WindowID | ||
| 958 | var sx, sy float64 | 983 | var sx, sy float64 | ||
| 984 | + | view := sc.mon.viewport() | |||
| 959 | for _, p := range sc.shownLayout() { | 985 | for _, p := range sc.shownLayout() { | ||
| 960 | r := c.clientRect(p) | 986 | r := c.clientRect(p) | ||
| 961 | // A peek is clickable wherever it shows, border included: it may | 987 | // A peek is clickable wherever it shows, border included: it may | ||
| @@ -964,6 +990,8 @@ func (c *Core) hit(x, y float64) (WindowID, float64, float64) { | |||||
| 964 | if p.Peek { | 990 | if p.Peek { | ||
| 965 | box = p.Rect | 991 | box = p.Rect | ||
| 966 | } | 992 | } | ||
| 993 | + | // Only what is drawn takes the pointer: a tile not over a panel. | |||
| 994 | + | box = intersect(box, view.area(p)) | |||
| 967 | 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) { | 995 | 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) { | ||
| 968 | if p.Peek { | 996 | if p.Peek { | ||
| 969 | 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)) | 997 | 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/monitor.go +6 -0
| @@ -451,6 +451,12 @@ func (m *Monitor) Frame() Rect { | |||
| 451 | 451 | return m.Current().Output | |
| 452 | 452 | } | |
| 453 | 453 | ||
| 454 | + | // viewport is where the workspace on screen is seen: Frame, and the | |
| 455 | + | // usable area its tiles live in. | |
| 456 | + | func (m *Monitor) viewport() viewport { | |
| 457 | + | return viewport{frame: m.Frame(), usable: m.Current().Usable} | |
| 458 | + | } | |
| 459 | + | ||
| 454 | 460 | // frameHas reports whether the output-local point is inside the frame. | |
| 455 | 461 | func (m *Monitor) frameHas(x, y float64) bool { | |
| 456 | 462 | f := m.Frame() | |
| @@ -451,6 +451,12 @@ func (m *Monitor) Frame() Rect { | |||||
| 451 | return m.Current().Output | 451 | return m.Current().Output | ||
| 452 | } | 452 | } | ||
| 453 | 453 | ||||
| 454 | + | // viewport is where the workspace on screen is seen: Frame, and the | |||
| 455 | + | // usable area its tiles live in. | |||
| 456 | + | func (m *Monitor) viewport() viewport { | |||
| 457 | + | return viewport{frame: m.Frame(), usable: m.Current().Usable} | |||
| 458 | + | } | |||
| 459 | + | ||||
| 454 | // frameHas reports whether the output-local point is inside the frame. | 460 | // frameHas reports whether the output-local point is inside the frame. | ||
| 455 | func (m *Monitor) frameHas(x, y float64) bool { | 461 | func (m *Monitor) frameHas(x, y float64) bool { | ||
| 456 | f := m.Frame() | 462 | f := m.Frame() | ||
M internal/core/overview_maximized_test.go +2 -2
| @@ -273,7 +273,7 @@ func TestOverviewMaximizedPreviewsKeepConfigures(t *testing.T) { | |||
| 273 | 273 | r := w.columnRectsFor(true)[int(p.ID)-1] | |
| 274 | 274 | p.Rect, p.Hidden = r, false | |
| 275 | 275 | } | |
| 276 | - | v, _ := s.next(p, configureTarget{output: "test", client: p.Rect, area: w.Usable}) | |
| 276 | + | v, _ := s.next(p, configureTarget{output: "test", client: p.Rect, view: whole(w.Usable)}) | |
| 277 | 277 | s.mark(v) | |
| 278 | 278 | before[p.ID] = v | |
| 279 | 279 | } | |
| @@ -283,7 +283,7 @@ func TestOverviewMaximizedPreviewsKeepConfigures(t *testing.T) { | |||
| 283 | 283 | if p.ID < 1 || p.ID > 4 { | |
| 284 | 284 | continue | |
| 285 | 285 | } | |
| 286 | - | v, _ := s.next(p, configureTarget{output: "test", area: w.Usable}) | |
| 286 | + | v, _ := s.next(p, configureTarget{output: "test", view: whole(w.Usable)}) | |
| 287 | 287 | if v.Width != before[p.ID].Width || v.Height != before[p.ID].Height { | |
| 288 | 288 | t.Fatalf("%d resized from %+v to %+v", p.ID, before[p.ID], v) | |
| 289 | 289 | } | |
| @@ -273,7 +273,7 @@ func TestOverviewMaximizedPreviewsKeepConfigures(t *testing.T) { | |||||
| 273 | r := w.columnRectsFor(true)[int(p.ID)-1] | 273 | r := w.columnRectsFor(true)[int(p.ID)-1] | ||
| 274 | p.Rect, p.Hidden = r, false | 274 | p.Rect, p.Hidden = r, false | ||
| 275 | } | 275 | } | ||
| 276 | - | v, _ := s.next(p, configureTarget{output: "test", client: p.Rect, area: w.Usable}) | 276 | + | v, _ := s.next(p, configureTarget{output: "test", client: p.Rect, view: whole(w.Usable)}) |
| 277 | s.mark(v) | 277 | s.mark(v) | ||
| 278 | before[p.ID] = v | 278 | before[p.ID] = v | ||
| 279 | } | 279 | } | ||
| @@ -283,7 +283,7 @@ func TestOverviewMaximizedPreviewsKeepConfigures(t *testing.T) { | |||||
| 283 | if p.ID < 1 || p.ID > 4 { | 283 | if p.ID < 1 || p.ID > 4 { | ||
| 284 | continue | 284 | continue | ||
| 285 | } | 285 | } | ||
| 286 | - | v, _ := s.next(p, configureTarget{output: "test", area: w.Usable}) | 286 | + | v, _ := s.next(p, configureTarget{output: "test", view: whole(w.Usable)}) |
| 287 | if v.Width != before[p.ID].Width || v.Height != before[p.ID].Height { | 287 | if v.Width != before[p.ID].Width || v.Height != before[p.ID].Height { | ||
| 288 | t.Fatalf("%d resized from %+v to %+v", p.ID, before[p.ID], v) | 288 | t.Fatalf("%d resized from %+v to %+v", p.ID, before[p.ID], v) | ||
| 289 | } | 289 | } | ||
M internal/core/overview_stack_test.go +1 -1
| @@ -273,7 +273,7 @@ func TestOverviewStashRightBringsColumnsFront(t *testing.T) { | |||
| 273 | 273 | func TestOverviewStackNoFloatVeil(t *testing.T) { | |
| 274 | 274 | m := stackMonitor() | |
| 275 | 275 | m.ToggleOverview() | |
| 276 | - | if d := floatDim(m.Layout(), Rect{W: 300, H: 200}, 0.3); d != 0 { | |
| 276 | + | if d := floatDim(m.Layout(), whole(Rect{W: 300, H: 200}), 0.3); d != 0 { | |
| 277 | 277 | t.Fatalf("veil %v in the overview", d) | |
| 278 | 278 | } | |
| 279 | 279 | } | |
| @@ -273,7 +273,7 @@ func TestOverviewStashRightBringsColumnsFront(t *testing.T) { | |||||
| 273 | func TestOverviewStackNoFloatVeil(t *testing.T) { | 273 | func TestOverviewStackNoFloatVeil(t *testing.T) { | ||
| 274 | m := stackMonitor() | 274 | m := stackMonitor() | ||
| 275 | m.ToggleOverview() | 275 | m.ToggleOverview() | ||
| 276 | - | if d := floatDim(m.Layout(), Rect{W: 300, H: 200}, 0.3); d != 0 { | 276 | + | if d := floatDim(m.Layout(), whole(Rect{W: 300, H: 200}), 0.3); d != 0 { |
| 277 | t.Fatalf("veil %v in the overview", d) | 277 | t.Fatalf("veil %v in the overview", d) | ||
| 278 | } | 278 | } | ||
| 279 | } | 279 | } | ||
M internal/core/popup.go +4 -3
| @@ -146,9 +146,10 @@ func (c *Core) windowRect(id WindowID) (*screen, Rect, bool) { | |||
| 146 | 146 | for _, sc := range c.screens { | |
| 147 | 147 | for _, pl := range sc.shownLayout() { | |
| 148 | 148 | if pl.ID == id { | |
| 149 | - | // Hidden or scrolled off: not on screen, like its popups. | |
| 150 | - | // A preview takes no input and shows no popups. | |
| 151 | - | if !onScreen(pl, sc.mon.Frame()) || pl.Preview > 0 { | |
| 149 | + | // Hidden, scrolled off or under a panel: not on screen, | |
| 150 | + | // like its popups. A preview takes no input and shows no | |
| 151 | + | // popups. | |
| 152 | + | if !sc.mon.viewport().shows(pl) || pl.Preview > 0 { | |
| 152 | 153 | return nil, Rect{}, false | |
| 153 | 154 | } | |
| 154 | 155 | return sc, c.clientRect(pl), true | |
| @@ -146,9 +146,10 @@ func (c *Core) windowRect(id WindowID) (*screen, Rect, bool) { | |||||
| 146 | for _, sc := range c.screens { | 146 | for _, sc := range c.screens { | ||
| 147 | for _, pl := range sc.shownLayout() { | 147 | for _, pl := range sc.shownLayout() { | ||
| 148 | if pl.ID == id { | 148 | if pl.ID == id { | ||
| 149 | - | // Hidden or scrolled off: not on screen, like its popups. | 149 | + | // Hidden, scrolled off or under a panel: not on screen, |
| 150 | - | // A preview takes no input and shows no popups. | 150 | + | // like its popups. A preview takes no input and shows no |
| 151 | - | if !onScreen(pl, sc.mon.Frame()) || pl.Preview > 0 { | 151 | + | // popups. |
| 152 | + | if !sc.mon.viewport().shows(pl) || pl.Preview > 0 { | |||
| 152 | return nil, Rect{}, false | 153 | return nil, Rect{}, false | ||
| 153 | } | 154 | } | ||
| 154 | return sc, c.clientRect(pl), true | 155 | return sc, c.clientRect(pl), true | ||
M internal/core/pulse.go +3 -3
| @@ -151,11 +151,11 @@ func (m *Monitor) settled() bool { | |||
| 151 | 151 | return true | |
| 152 | 152 | } | |
| 153 | 153 | ||
| 154 | - | // visibleCount is the number of windows of layout shown in area. | |
| 155 | - | func visibleCount(layout []Placement, area Rect) int { | |
| 154 | + | // visibleCount is the number of windows of layout seen in v. | |
| 155 | + | func visibleCount(layout []Placement, v viewport) int { | |
| 156 | 156 | n := 0 | |
| 157 | 157 | for _, p := range layout { | |
| 158 | - | if p.Preview == 0 && onScreen(p, area) { | |
| 158 | + | if p.Preview == 0 && v.shows(p) { | |
| 159 | 159 | n++ | |
| 160 | 160 | } | |
| 161 | 161 | } | |
| @@ -151,11 +151,11 @@ func (m *Monitor) settled() bool { | |||||
| 151 | return true | 151 | return true | ||
| 152 | } | 152 | } | ||
| 153 | 153 | ||||
| 154 | - | // visibleCount is the number of windows of layout shown in area. | 154 | + | // visibleCount is the number of windows of layout seen in v. |
| 155 | - | func visibleCount(layout []Placement, area Rect) int { | 155 | + | func visibleCount(layout []Placement, v viewport) int { |
| 156 | n := 0 | 156 | n := 0 | ||
| 157 | for _, p := range layout { | 157 | for _, p := range layout { | ||
| 158 | - | if p.Preview == 0 && onScreen(p, area) { | 158 | + | if p.Preview == 0 && v.shows(p) { |
| 159 | n++ | 159 | n++ | ||
| 160 | } | 160 | } | ||
| 161 | } | 161 | } | ||
M internal/core/scene.go +16 -11
| @@ -11,17 +11,23 @@ import ( | |||
| 11 | 11 | // drawable reports that it can show the focus pulse on the focused window. | |
| 12 | 12 | func (c *Core) sceneFor(ctx context.Context, i int, sc *screen, only *screen, capture *capView, pulse float64) (scene ports.Scene, drawable bool, err error) { | |
| 13 | 13 | o := sc.mon.Output() | |
| 14 | - | // frame is the viewport of the workspace on screen: the whole output | |
| 15 | - | // unless it has a size override (never in the overview). | |
| 16 | - | frame := sc.mon.Frame() | |
| 14 | + | // view.frame is the viewport of the workspace on screen: the whole | |
| 15 | + | // output unless it has a size override (never in the overview). | |
| 16 | + | view := sc.mon.viewport() | |
| 17 | 17 | var clip Rect | |
| 18 | - | if frame != (Rect{W: o.W, H: o.H}) { | |
| 19 | - | clip = frame | |
| 18 | + | if view.frame != (Rect{W: o.W, H: o.H}) { | |
| 19 | + | clip = view.frame | |
| 20 | 20 | } | |
| 21 | 21 | // layout is what is drawn; settled (same indexes) is where the | |
| 22 | 22 | // windows are going, and alone sizes the configures. | |
| 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 | + | 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} | |
| 30 | + | } | |
| 25 | 31 | var real map[WindowID]Placement | |
| 26 | 32 | if sc.mon.ov.open { | |
| 27 | 33 | // Local to this screen's build: reused, cleared each time. | |
| @@ -37,7 +43,7 @@ func (c *Core) sceneFor(ctx context.Context, i int, sc *screen, only *screen, ca | |||
| 37 | 43 | } | |
| 38 | 44 | } | |
| 39 | 45 | } | |
| 40 | - | scene.Dim = floatDim(layout, frame, c.cfg.Floating.Dim) | |
| 46 | + | scene.Dim = floatDim(layout, view, c.cfg.Floating.Dim) | |
| 41 | 47 | if sc.mon.ov.open { | |
| 42 | 48 | // Darken the wallpaper around the previews. | |
| 43 | 49 | scene.Dim = c.cfg.Floating.Dim | |
| @@ -52,11 +58,10 @@ func (c *Core) sceneFor(ctx context.Context, i int, sc *screen, only *screen, ca | |||
| 52 | 58 | scene.Separators = append(scene.Separators, overviewOutline(layout, max(c.cfg.Border.Width, 2))...) | |
| 53 | 59 | } else { | |
| 54 | 60 | // Only the focused output lights the focused window's lines. | |
| 55 | - | scene.Separators = separators(layout, c.cfg.Border.Width, sc.mon.Current().gap(), frame, i == c.focusScreen) | |
| 61 | + | scene.Separators = separators(layout, c.cfg.Border.Width, sc.mon.Current().gap(), view, i == c.focusScreen) | |
| 56 | 62 | } | |
| 57 | 63 | // A window alone on screen needs no pulse to show it has the focus. | |
| 58 | - | // A tile under a panel (the next cascade band) does not count. | |
| 59 | - | alone := i == c.focusScreen && c.pulse.target != 0 && visibleCount(layout, sc.mon.Current().Usable) == 1 | |
| 64 | + | alone := i == c.focusScreen && c.pulse.target != 0 && visibleCount(layout, view) == 1 | |
| 60 | 65 | for k, p := range layout { | |
| 61 | 66 | ps := settled[k] | |
| 62 | 67 | // Only the focused output has an activated window. | |
| @@ -84,14 +89,14 @@ func (c *Core) sceneFor(ctx context.Context, i int, sc *screen, only *screen, ca | |||
| 84 | 89 | } | |
| 85 | 90 | continue | |
| 86 | 91 | } | |
| 87 | - | if focused && p.ID == c.pulse.target && !alone && !p.Fullscreen && !p.Hidden && p.Preview == 0 && !sc.mon.ov.open { | |
| 92 | + | if focused && p.ID == c.pulse.target && !alone && !p.Fullscreen && !p.Hidden && p.Preview == 0 && !sc.mon.ov.open && hasRoom(view, p) { | |
| 88 | 93 | drawable = true | |
| 89 | 94 | if p.ID == c.pulse.id { | |
| 90 | 95 | sw.FocusEffect = pulse | |
| 91 | 96 | } | |
| 92 | 97 | } | |
| 93 | 98 | scene.Windows = append(scene.Windows, sw) | |
| 94 | - | t := configureTarget{output: sc.name(), area: frame, focused: focused, captured: capture != nil && capture.window == p.ID} | |
| 99 | + | t := configureTarget{output: sc.name(), view: view, focused: focused, captured: capture != nil && capture.window == p.ID} | |
| 95 | 100 | if !p.Hidden && p.Preview == 0 { | |
| 96 | 101 | // Only a sized configure needs the client size. | |
| 97 | 102 | t.client, t.imposed = c.clientRect(ps), sc.mon.Current().imposedFloat(p.ID) | |
| @@ -11,17 +11,23 @@ import ( | |||||
| 11 | // drawable reports that it can show the focus pulse on the focused window. | 11 | // drawable reports that it can show the focus pulse on the focused window. | ||
| 12 | func (c *Core) sceneFor(ctx context.Context, i int, sc *screen, only *screen, capture *capView, pulse float64) (scene ports.Scene, drawable bool, err error) { | 12 | func (c *Core) sceneFor(ctx context.Context, i int, sc *screen, only *screen, capture *capView, pulse float64) (scene ports.Scene, drawable bool, err error) { | ||
| 13 | o := sc.mon.Output() | 13 | o := sc.mon.Output() | ||
| 14 | - | // frame is the viewport of the workspace on screen: the whole output | 14 | + | // view.frame is the viewport of the workspace on screen: the whole |
| 15 | - | // unless it has a size override (never in the overview). | 15 | + | // output unless it has a size override (never in the overview). |
| 16 | - | frame := sc.mon.Frame() | 16 | + | view := sc.mon.viewport() |
| 17 | var clip Rect | 17 | var clip Rect | ||
| 18 | - | if frame != (Rect{W: o.W, H: o.H}) { | 18 | + | if view.frame != (Rect{W: o.W, H: o.H}) { |
| 19 | - | clip = frame | 19 | + | clip = view.frame |
| 20 | } | 20 | } | ||
| 21 | // layout is what is drawn; settled (same indexes) is where the | 21 | // layout is what is drawn; settled (same indexes) is where the | ||
| 22 | // windows are going, and alone sizes the configures. | 22 | // windows are going, and alone sizes the configures. | ||
| 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 | + | 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} | |||
| 30 | + | } | |||
| 25 | var real map[WindowID]Placement | 31 | var real map[WindowID]Placement | ||
| 26 | if sc.mon.ov.open { | 32 | if sc.mon.ov.open { | ||
| 27 | // Local to this screen's build: reused, cleared each time. | 33 | // Local to this screen's build: reused, cleared each time. | ||
| @@ -37,7 +43,7 @@ func (c *Core) sceneFor(ctx context.Context, i int, sc *screen, only *screen, ca | |||||
| 37 | } | 43 | } | ||
| 38 | } | 44 | } | ||
| 39 | } | 45 | } | ||
| 40 | - | scene.Dim = floatDim(layout, frame, c.cfg.Floating.Dim) | 46 | + | scene.Dim = floatDim(layout, view, c.cfg.Floating.Dim) |
| 41 | if sc.mon.ov.open { | 47 | if sc.mon.ov.open { | ||
| 42 | // Darken the wallpaper around the previews. | 48 | // Darken the wallpaper around the previews. | ||
| 43 | scene.Dim = c.cfg.Floating.Dim | 49 | scene.Dim = c.cfg.Floating.Dim | ||
| @@ -52,11 +58,10 @@ func (c *Core) sceneFor(ctx context.Context, i int, sc *screen, only *screen, ca | |||||
| 52 | scene.Separators = append(scene.Separators, overviewOutline(layout, max(c.cfg.Border.Width, 2))...) | 58 | scene.Separators = append(scene.Separators, overviewOutline(layout, max(c.cfg.Border.Width, 2))...) | ||
| 53 | } else { | 59 | } else { | ||
| 54 | // Only the focused output lights the focused window's lines. | 60 | // Only the focused output lights the focused window's lines. | ||
| 55 | - | scene.Separators = separators(layout, c.cfg.Border.Width, sc.mon.Current().gap(), frame, i == c.focusScreen) | 61 | + | scene.Separators = separators(layout, c.cfg.Border.Width, sc.mon.Current().gap(), view, i == c.focusScreen) |
| 56 | } | 62 | } | ||
| 57 | // A window alone on screen needs no pulse to show it has the focus. | 63 | // A window alone on screen needs no pulse to show it has the focus. | ||
| 58 | - | // A tile under a panel (the next cascade band) does not count. | 64 | + | alone := i == c.focusScreen && c.pulse.target != 0 && visibleCount(layout, view) == 1 |
| 59 | - | alone := i == c.focusScreen && c.pulse.target != 0 && visibleCount(layout, sc.mon.Current().Usable) == 1 | |||
| 60 | for k, p := range layout { | 65 | for k, p := range layout { | ||
| 61 | ps := settled[k] | 66 | ps := settled[k] | ||
| 62 | // Only the focused output has an activated window. | 67 | // Only the focused output has an activated window. | ||
| @@ -84,14 +89,14 @@ func (c *Core) sceneFor(ctx context.Context, i int, sc *screen, only *screen, ca | |||||
| 84 | } | 89 | } | ||
| 85 | continue | 90 | continue | ||
| 86 | } | 91 | } | ||
| 87 | - | if focused && p.ID == c.pulse.target && !alone && !p.Fullscreen && !p.Hidden && p.Preview == 0 && !sc.mon.ov.open { | 92 | + | if focused && p.ID == c.pulse.target && !alone && !p.Fullscreen && !p.Hidden && p.Preview == 0 && !sc.mon.ov.open && hasRoom(view, p) { |
| 88 | drawable = true | 93 | drawable = true | ||
| 89 | if p.ID == c.pulse.id { | 94 | if p.ID == c.pulse.id { | ||
| 90 | sw.FocusEffect = pulse | 95 | sw.FocusEffect = pulse | ||
| 91 | } | 96 | } | ||
| 92 | } | 97 | } | ||
| 93 | scene.Windows = append(scene.Windows, sw) | 98 | scene.Windows = append(scene.Windows, sw) | ||
| 94 | - | t := configureTarget{output: sc.name(), area: frame, focused: focused, captured: capture != nil && capture.window == p.ID} | 99 | + | t := configureTarget{output: sc.name(), view: view, focused: focused, captured: capture != nil && capture.window == p.ID} |
| 95 | if !p.Hidden && p.Preview == 0 { | 100 | if !p.Hidden && p.Preview == 0 { | ||
| 96 | // Only a sized configure needs the client size. | 101 | // Only a sized configure needs the client size. | ||
| 97 | t.client, t.imposed = c.clientRect(ps), sc.mon.Current().imposedFloat(p.ID) | 102 | t.client, t.imposed = c.clientRect(ps), sc.mon.Current().imposedFloat(p.ID) | ||
M internal/core/separators.go +12 -9
| @@ -18,19 +18,22 @@ import "github.com/bnema/neferwl/internal/ports" | |||
| 18 | 18 | ||
| 19 | 19 | var sides = [...]ports.Sides{ports.SideLeft, ports.SideRight, ports.SideTop, ports.SideBottom} | |
| 20 | 20 | ||
| 21 | - | // separators are the lines of a layout on output o, inactive ones first | |
| 22 | - | // so active ones draw over them. lit says whether the focused tile shows | |
| 23 | - | // focus (only on the focused output). | |
| 24 | - | func separators(ps []Placement, width, gap int, o Rect, lit bool) []ports.Separator { | |
| 21 | + | // separators are the lines of the windows seen in v, 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). | |
| 24 | + | func separators(ps []Placement, width, gap int, v viewport, lit bool) []ports.Separator { | |
| 25 | 25 | if width <= 0 { | |
| 26 | 26 | return nil | |
| 27 | 27 | } | |
| 28 | + | o := v.frame | |
| 28 | 29 | var out []ports.Separator | |
| 29 | 30 | add := func(p *Placement, r Rect, active bool) { | |
| 30 | - | // A line cannot belong to a scrolled-off window or bleed onto | |
| 31 | - | // another output, even when a neighboring tile remains visible. | |
| 32 | - | x0, y0 := max(r.X, o.X), max(r.Y, o.Y) | |
| 33 | - | x1, y1 := min(r.X+r.W, o.X+o.W), min(r.Y+r.H, o.Y+o.H) | |
| 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 | + | a := v.area(*p) | |
| 35 | + | x0, y0 := max(r.X, a.X), max(r.Y, a.Y) | |
| 36 | + | x1, y1 := min(r.X+r.W, a.X+a.W), min(r.Y+r.H, a.Y+a.H) | |
| 34 | 37 | if x0 >= x1 || y0 >= y1 { | |
| 35 | 38 | return | |
| 36 | 39 | } | |
| @@ -52,7 +55,7 @@ func separators(ps []Placement, width, gap int, o Rect, lit bool) []ports.Separa | |||
| 52 | 55 | p := &ps[i] | |
| 53 | 56 | // A leaving float keeps its own lines while it fades; a leaving | |
| 54 | 57 | // tile's were shared with its neighbours, which re-flowed. | |
| 55 | - | if p.Fullscreen || !(onScreen(*p, o) || p.Leaving && p.Floating && p.Rect.Overlaps(o)) { | |
| 58 | + | if p.Fullscreen || !(v.shows(*p) || p.Leaving && p.Floating && p.Rect.Overlaps(o)) { | |
| 56 | 59 | continue | |
| 57 | 60 | } | |
| 58 | 61 | // Tiles sharing a line: Neighbors only counts tiles visible in | |
| @@ -18,19 +18,22 @@ import "github.com/bnema/neferwl/internal/ports" | |||||
| 18 | 18 | ||||
| 19 | var sides = [...]ports.Sides{ports.SideLeft, ports.SideRight, ports.SideTop, ports.SideBottom} | 19 | var sides = [...]ports.Sides{ports.SideLeft, ports.SideRight, ports.SideTop, ports.SideBottom} | ||
| 20 | 20 | ||||
| 21 | - | // separators are the lines of a layout on output o, inactive ones first | 21 | + | // separators are the lines of the windows seen in v, each clipped to its |
| 22 | - | // so active ones draw over them. lit says whether the focused tile shows | 22 | + | // window's area, inactive ones first so active ones draw over them. lit |
| 23 | - | // 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, o Rect, lit bool) []ports.Separator { | 24 | + | func separators(ps []Placement, width, gap int, v viewport, lit bool) []ports.Separator { |
| 25 | if width <= 0 { | 25 | if width <= 0 { | ||
| 26 | return nil | 26 | return nil | ||
| 27 | } | 27 | } | ||
| 28 | + | o := v.frame | |||
| 28 | var out []ports.Separator | 29 | var out []ports.Separator | ||
| 29 | add := func(p *Placement, r Rect, active bool) { | 30 | add := func(p *Placement, r Rect, active bool) { | ||
| 30 | - | // A line cannot belong to a scrolled-off window or bleed onto | 31 | + | // A line cannot belong to a scrolled-off window, bleed onto |
| 31 | - | // another output, even when a neighboring tile remains visible. | 32 | + | // another output or, for a tile, over a panel, even when a |
| 32 | - | x0, y0 := max(r.X, o.X), max(r.Y, o.Y) | 33 | + | // neighboring tile remains visible. |
| 33 | - | x1, y1 := min(r.X+r.W, o.X+o.W), min(r.Y+r.H, o.Y+o.H) | 34 | + | a := v.area(*p) |
| 35 | + | x0, y0 := max(r.X, a.X), max(r.Y, a.Y) | |||
| 36 | + | x1, y1 := min(r.X+r.W, a.X+a.W), min(r.Y+r.H, a.Y+a.H) | |||
| 34 | if x0 >= x1 || y0 >= y1 { | 37 | if x0 >= x1 || y0 >= y1 { | ||
| 35 | return | 38 | return | ||
| 36 | } | 39 | } | ||
| @@ -52,7 +55,7 @@ func separators(ps []Placement, width, gap int, o Rect, lit bool) []ports.Separa | |||||
| 52 | p := &ps[i] | 55 | p := &ps[i] | ||
| 53 | // A leaving float keeps its own lines while it fades; a leaving | 56 | // A leaving float keeps its own lines while it fades; a leaving | ||
| 54 | // tile's were shared with its neighbours, which re-flowed. | 57 | // tile's were shared with its neighbours, which re-flowed. | ||
| 55 | - | if p.Fullscreen || !(onScreen(*p, o) || p.Leaving && p.Floating && p.Rect.Overlaps(o)) { | 58 | + | if p.Fullscreen || !(v.shows(*p) || p.Leaving && p.Floating && p.Rect.Overlaps(o)) { |
| 56 | continue | 59 | continue | ||
| 57 | } | 60 | } | ||
| 58 | // Tiles sharing a line: Neighbors only counts tiles visible in | 61 | // Tiles sharing a line: Neighbors only counts tiles visible in | ||
M internal/core/separators_test.go +13 -8
| @@ -66,7 +66,7 @@ func TestSeparators(t *testing.T) { | |||
| 66 | 66 | {"left column: whole line, split column untouched", tiles(0, grid...), true, []px{{14, 3, "lit"}, {14, 30, "lit"}, {20, 14, "gray"}}}, | |
| 67 | 67 | } { | |
| 68 | 68 | t.Run(tc.name, func(t *testing.T) { | |
| 69 | - | seps := separators(tc.ps, 2, 0, bigOutput, tc.lit) | |
| 69 | + | seps := separators(tc.ps, 2, 0, whole(bigOutput), tc.lit) | |
| 70 | 70 | for _, c := range tc.check { | |
| 71 | 71 | if got := colorAt(seps, c.x, c.y); got != c.want { | |
| 72 | 72 | t.Errorf("(%d,%d) = %q, want %q; %v", c.x, c.y, got, c.want, seps) | |
| @@ -77,22 +77,22 @@ func TestSeparators(t *testing.T) { | |||
| 77 | 77 | } | |
| 78 | 78 | ||
| 79 | 79 | func TestSeparatorsSkip(t *testing.T) { | |
| 80 | - | if s := separators(tiles(0, Rect{W: 32, H: 16}), 2, 0, bigOutput, true); len(s) != 0 { | |
| 80 | + | if s := separators(tiles(0, Rect{W: 32, H: 16}), 2, 0, whole(bigOutput), true); len(s) != 0 { | |
| 81 | 81 | t.Fatal("lone window:", s) | |
| 82 | 82 | } | |
| 83 | - | if s := separators(tiles(0, Rect{W: 16, H: 16}, Rect{X: 16, W: 16, H: 16}), 0, 0, bigOutput, true); len(s) != 0 { | |
| 83 | + | if s := separators(tiles(0, Rect{W: 16, H: 16}, Rect{X: 16, W: 16, H: 16}), 0, 0, whole(bigOutput), true); len(s) != 0 { | |
| 84 | 84 | t.Fatal("width 0:", s) | |
| 85 | 85 | } | |
| 86 | 86 | full := tiles(0, Rect{W: 16, H: 16}, Rect{X: 16, W: 16, H: 16}) | |
| 87 | 87 | full[0].Fullscreen, full[1].Hidden = true, true | |
| 88 | - | if s := separators(full, 2, 0, bigOutput, true); len(s) != 0 { | |
| 88 | + | if s := separators(full, 2, 0, whole(bigOutput), true); len(s) != 0 { | |
| 89 | 89 | t.Fatal("fullscreen:", s) | |
| 90 | 90 | } | |
| 91 | 91 | } | |
| 92 | 92 | ||
| 93 | 93 | func TestSeparatorsFloat(t *testing.T) { | |
| 94 | 94 | float := Placement{ID: 1, Rect: Rect{X: 10, Y: 10, W: 20, H: 20}, Floating: true, Focused: true, Inset: ports.SideAll} | |
| 95 | - | seps := separators([]Placement{float}, 2, 0, bigOutput, true) | |
| 95 | + | seps := separators([]Placement{float}, 2, 0, whole(bigOutput), true) | |
| 96 | 96 | if seps[0].Window != 1 { | |
| 97 | 97 | t.Fatal("float border not tied to its window:", seps) | |
| 98 | 98 | } | |
| @@ -134,7 +134,7 @@ func TestSeparatorsScroll(t *testing.T) { | |||
| 134 | 134 | t.Fatalf("expected visible borders to change with the view: %v then %v", before, after) | |
| 135 | 135 | } | |
| 136 | 136 | out := Rect{W: 100, H: 80} | |
| 137 | - | seps := separators(w.Layout(), 2, 0, out, true) | |
| 137 | + | seps := separators(w.Layout(), 2, 0, whole(out), true) | |
| 138 | 138 | if got := colorAt(seps, 99, 40); got != "" { | |
| 139 | 139 | t.Fatalf("line at the output edge: %v", seps) | |
| 140 | 140 | } | |
| @@ -183,10 +183,15 @@ func TestSeparatorsCascadeBottomPanel(t *testing.T) { | |||
| 183 | 183 | if p := previewOf(t, ps, 1); p.Neighbors != ports.SideRight { | |
| 184 | 184 | t.Fatalf("neighbors %04b, want right only", p.Neighbors) | |
| 185 | 185 | } | |
| 186 | - | seps := separators(ps, 2, 0, w.Output, true) | |
| 186 | + | seps := separators(ps, 2, 0, viewport{frame: w.Output, usable: w.Usable}, true) | |
| 187 | 187 | if got := colorAt(seps, 200, 578); got != "" { | |
| 188 | 188 | t.Fatalf("line along the panel: %v", seps) | |
| 189 | 189 | } | |
| 190 | + | for _, s := range seps { | |
| 191 | + | if s.Window == 3 { | |
| 192 | + | t.Fatalf("line of the tile under the panel: %v", seps) | |
| 193 | + | } | |
| 194 | + | } | |
| 190 | 195 | // Two tiles on screen: each lights its half of the shared line. | |
| 191 | 196 | if got := colorAt(seps, 449, 100); got != "lit" { | |
| 192 | 197 | t.Fatalf("shared line (449,100) = %q; %v", got, seps) | |
| @@ -200,7 +205,7 @@ func TestSeparatorsCascadeBottomPanel(t *testing.T) { | |||
| 200 | 205 | func TestSeparatorsGaps(t *testing.T) { | |
| 201 | 206 | ps := []Placement{{ID: 1, Rect: Rect{X: 0, W: 16, H: 16}, Focused: true}, {ID: 2, Rect: Rect{X: 20, W: 16, H: 16}}} | |
| 202 | 207 | setVisibleNeighbors(ps, 4, bigOutput) | |
| 203 | - | seps := separators(ps, 2, 4, bigOutput, true) | |
| 208 | + | seps := separators(ps, 2, 4, whole(bigOutput), true) | |
| 204 | 209 | for _, c := range []px{{14, 2, "lit"}, {14, 13, "lit"}, {20, 8, "gray"}, {17, 8, ""}} { | |
| 205 | 210 | if got := colorAt(seps, c.x, c.y); got != c.want { | |
| 206 | 211 | t.Errorf("(%d,%d) = %q, want %q", c.x, c.y, got, c.want) | |
| @@ -66,7 +66,7 @@ func TestSeparators(t *testing.T) { | |||||
| 66 | {"left column: whole line, split column untouched", tiles(0, grid...), true, []px{{14, 3, "lit"}, {14, 30, "lit"}, {20, 14, "gray"}}}, | 66 | {"left column: whole line, split column untouched", tiles(0, grid...), true, []px{{14, 3, "lit"}, {14, 30, "lit"}, {20, 14, "gray"}}}, | ||
| 67 | } { | 67 | } { | ||
| 68 | t.Run(tc.name, func(t *testing.T) { | 68 | t.Run(tc.name, func(t *testing.T) { | ||
| 69 | - | seps := separators(tc.ps, 2, 0, bigOutput, tc.lit) | 69 | + | seps := separators(tc.ps, 2, 0, whole(bigOutput), tc.lit) |
| 70 | for _, c := range tc.check { | 70 | for _, c := range tc.check { | ||
| 71 | if got := colorAt(seps, c.x, c.y); got != c.want { | 71 | if got := colorAt(seps, c.x, c.y); got != c.want { | ||
| 72 | t.Errorf("(%d,%d) = %q, want %q; %v", c.x, c.y, got, c.want, seps) | 72 | t.Errorf("(%d,%d) = %q, want %q; %v", c.x, c.y, got, c.want, seps) | ||
| @@ -77,22 +77,22 @@ func TestSeparators(t *testing.T) { | |||||
| 77 | } | 77 | } | ||
| 78 | 78 | ||||
| 79 | func TestSeparatorsSkip(t *testing.T) { | 79 | func TestSeparatorsSkip(t *testing.T) { | ||
| 80 | - | if s := separators(tiles(0, Rect{W: 32, H: 16}), 2, 0, bigOutput, true); len(s) != 0 { | 80 | + | if s := separators(tiles(0, Rect{W: 32, H: 16}), 2, 0, whole(bigOutput), true); len(s) != 0 { |
| 81 | t.Fatal("lone window:", s) | 81 | t.Fatal("lone window:", s) | ||
| 82 | } | 82 | } | ||
| 83 | - | if s := separators(tiles(0, Rect{W: 16, H: 16}, Rect{X: 16, W: 16, H: 16}), 0, 0, bigOutput, true); len(s) != 0 { | 83 | + | if s := separators(tiles(0, Rect{W: 16, H: 16}, Rect{X: 16, W: 16, H: 16}), 0, 0, whole(bigOutput), true); len(s) != 0 { |
| 84 | t.Fatal("width 0:", s) | 84 | t.Fatal("width 0:", s) | ||
| 85 | } | 85 | } | ||
| 86 | full := tiles(0, Rect{W: 16, H: 16}, Rect{X: 16, W: 16, H: 16}) | 86 | full := tiles(0, Rect{W: 16, H: 16}, Rect{X: 16, W: 16, H: 16}) | ||
| 87 | full[0].Fullscreen, full[1].Hidden = true, true | 87 | full[0].Fullscreen, full[1].Hidden = true, true | ||
| 88 | - | if s := separators(full, 2, 0, bigOutput, true); len(s) != 0 { | 88 | + | if s := separators(full, 2, 0, whole(bigOutput), true); len(s) != 0 { |
| 89 | t.Fatal("fullscreen:", s) | 89 | t.Fatal("fullscreen:", s) | ||
| 90 | } | 90 | } | ||
| 91 | } | 91 | } | ||
| 92 | 92 | ||||
| 93 | func TestSeparatorsFloat(t *testing.T) { | 93 | func TestSeparatorsFloat(t *testing.T) { | ||
| 94 | float := Placement{ID: 1, Rect: Rect{X: 10, Y: 10, W: 20, H: 20}, Floating: true, Focused: true, Inset: ports.SideAll} | 94 | float := Placement{ID: 1, Rect: Rect{X: 10, Y: 10, W: 20, H: 20}, Floating: true, Focused: true, Inset: ports.SideAll} | ||
| 95 | - | seps := separators([]Placement{float}, 2, 0, bigOutput, true) | 95 | + | seps := separators([]Placement{float}, 2, 0, whole(bigOutput), true) |
| 96 | if seps[0].Window != 1 { | 96 | if seps[0].Window != 1 { | ||
| 97 | t.Fatal("float border not tied to its window:", seps) | 97 | t.Fatal("float border not tied to its window:", seps) | ||
| 98 | } | 98 | } | ||
| @@ -134,7 +134,7 @@ func TestSeparatorsScroll(t *testing.T) { | |||||
| 134 | t.Fatalf("expected visible borders to change with the view: %v then %v", before, after) | 134 | t.Fatalf("expected visible borders to change with the view: %v then %v", before, after) | ||
| 135 | } | 135 | } | ||
| 136 | out := Rect{W: 100, H: 80} | 136 | out := Rect{W: 100, H: 80} | ||
| 137 | - | seps := separators(w.Layout(), 2, 0, out, true) | 137 | + | seps := separators(w.Layout(), 2, 0, whole(out), true) |
| 138 | if got := colorAt(seps, 99, 40); got != "" { | 138 | if got := colorAt(seps, 99, 40); got != "" { | ||
| 139 | t.Fatalf("line at the output edge: %v", seps) | 139 | t.Fatalf("line at the output edge: %v", seps) | ||
| 140 | } | 140 | } | ||
| @@ -183,10 +183,15 @@ func TestSeparatorsCascadeBottomPanel(t *testing.T) { | |||||
| 183 | if p := previewOf(t, ps, 1); p.Neighbors != ports.SideRight { | 183 | if p := previewOf(t, ps, 1); p.Neighbors != ports.SideRight { | ||
| 184 | t.Fatalf("neighbors %04b, want right only", p.Neighbors) | 184 | t.Fatalf("neighbors %04b, want right only", p.Neighbors) | ||
| 185 | } | 185 | } | ||
| 186 | - | seps := separators(ps, 2, 0, w.Output, true) | 186 | + | seps := separators(ps, 2, 0, viewport{frame: w.Output, usable: w.Usable}, true) |
| 187 | if got := colorAt(seps, 200, 578); got != "" { | 187 | if got := colorAt(seps, 200, 578); got != "" { | ||
| 188 | t.Fatalf("line along the panel: %v", seps) | 188 | t.Fatalf("line along the panel: %v", seps) | ||
| 189 | } | 189 | } | ||
| 190 | + | for _, s := range seps { | |||
| 191 | + | if s.Window == 3 { | |||
| 192 | + | t.Fatalf("line of the tile under the panel: %v", seps) | |||
| 193 | + | } | |||
| 194 | + | } | |||
| 190 | // Two tiles on screen: each lights its half of the shared line. | 195 | // Two tiles on screen: each lights its half of the shared line. | ||
| 191 | if got := colorAt(seps, 449, 100); got != "lit" { | 196 | if got := colorAt(seps, 449, 100); got != "lit" { | ||
| 192 | t.Fatalf("shared line (449,100) = %q; %v", got, seps) | 197 | t.Fatalf("shared line (449,100) = %q; %v", got, seps) | ||
| @@ -200,7 +205,7 @@ func TestSeparatorsCascadeBottomPanel(t *testing.T) { | |||||
| 200 | func TestSeparatorsGaps(t *testing.T) { | 205 | func TestSeparatorsGaps(t *testing.T) { | ||
| 201 | ps := []Placement{{ID: 1, Rect: Rect{X: 0, W: 16, H: 16}, Focused: true}, {ID: 2, Rect: Rect{X: 20, W: 16, H: 16}}} | 206 | ps := []Placement{{ID: 1, Rect: Rect{X: 0, W: 16, H: 16}, Focused: true}, {ID: 2, Rect: Rect{X: 20, W: 16, H: 16}}} | ||
| 202 | setVisibleNeighbors(ps, 4, bigOutput) | 207 | setVisibleNeighbors(ps, 4, bigOutput) | ||
| 203 | - | seps := separators(ps, 2, 4, bigOutput, true) | 208 | + | seps := separators(ps, 2, 4, whole(bigOutput), true) |
| 204 | for _, c := range []px{{14, 2, "lit"}, {14, 13, "lit"}, {20, 8, "gray"}, {17, 8, ""}} { | 209 | for _, c := range []px{{14, 2, "lit"}, {14, 13, "lit"}, {20, 8, "gray"}, {17, 8, ""}} { | ||
| 205 | if got := colorAt(seps, c.x, c.y); got != c.want { | 210 | if got := colorAt(seps, c.x, c.y); got != c.want { | ||
| 206 | t.Errorf("(%d,%d) = %q, want %q", c.x, c.y, got, c.want) | 211 | t.Errorf("(%d,%d) = %q, want %q", c.x, c.y, got, c.want) | ||
M internal/core/state.go +2 -1
| @@ -54,8 +54,9 @@ func (c *Core) stateInto(st ports.State) ports.State { | |||
| 54 | 54 | // Read at once, into publish's scratch: the snapshot holds no | |
| 55 | 55 | // placement. | |
| 56 | 56 | c.realBuf = m.layoutInto(c.realBuf) | |
| 57 | + | view := m.viewport() | |
| 57 | 58 | for _, p := range c.realBuf { | |
| 58 | - | shown[p.ID] = onScreen(p, m.Frame()) | |
| 59 | + | shown[p.ID] = view.shows(p) | |
| 59 | 60 | } | |
| 60 | 61 | for w := range m.all() { | |
| 61 | 62 | n := indexOf(m.Workspaces, w) + 1 | |
| @@ -54,8 +54,9 @@ func (c *Core) stateInto(st ports.State) ports.State { | |||||
| 54 | // Read at once, into publish's scratch: the snapshot holds no | 54 | // Read at once, into publish's scratch: the snapshot holds no | ||
| 55 | // placement. | 55 | // placement. | ||
| 56 | c.realBuf = m.layoutInto(c.realBuf) | 56 | c.realBuf = m.layoutInto(c.realBuf) | ||
| 57 | + | view := m.viewport() | |||
| 57 | for _, p := range c.realBuf { | 58 | for _, p := range c.realBuf { | ||
| 58 | - | shown[p.ID] = onScreen(p, m.Frame()) | 59 | + | shown[p.ID] = view.shows(p) |
| 59 | } | 60 | } | ||
| 60 | for w := range m.all() { | 61 | for w := range m.all() { | ||
| 61 | n := indexOf(m.Workspaces, w) + 1 | 62 | n := indexOf(m.Workspaces, w) + 1 | ||
A internal/core/viewport_test.go +235 -0
| @@ -0,0 +1,235 @@ | |||
| 1 | + | package core | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "slices" | |
| 6 | + | "testing" | |
| 7 | + | ||
| 8 | + | "github.com/bnema/neferwl/internal/ports" | |
| 9 | + | ) | |
| 10 | + | ||
| 11 | + | // A tile is seen in the usable area; a float, a fullscreen window and a | |
| 12 | + | // preview are placed against the whole frame. | |
| 13 | + | func TestViewportShows(t *testing.T) { | |
| 14 | + | v := viewport{frame: Rect{W: 100, H: 80}, usable: Rect{W: 100, H: 60}} | |
| 15 | + | under := Rect{Y: 60, W: 50, H: 60} // the next cascade band, under a bottom panel | |
| 16 | + | for _, tc := range []struct { | |
| 17 | + | name string | |
| 18 | + | v viewport | |
| 19 | + | p Placement | |
| 20 | + | want bool | |
| 21 | + | }{ | |
| 22 | + | {"tile in usable", v, Placement{Rect: Rect{W: 50, H: 60}}, true}, | |
| 23 | + | {"tile under panel", v, Placement{Rect: under}, false}, | |
| 24 | + | {"tile across the panel line", v, Placement{Rect: Rect{Y: 30, W: 50, H: 60}}, true}, | |
| 25 | + | {"tile off frame", v, Placement{Rect: Rect{Y: 80, W: 50, H: 60}}, false}, | |
| 26 | + | {"hidden tile", v, Placement{Rect: Rect{W: 50, H: 60}, Hidden: true}, false}, | |
| 27 | + | {"float under panel", v, Placement{Rect: under, Floating: true}, true}, | |
| 28 | + | {"hidden float", v, Placement{Rect: Rect{W: 50, H: 60}, Floating: true, Hidden: true}, false}, | |
| 29 | + | {"fullscreen tile", v, Placement{Rect: Rect{W: 100, H: 80}, Fullscreen: true}, true}, | |
| 30 | + | {"preview under panel", v, Placement{Rect: under, Preview: 0.5}, true}, | |
| 31 | + | {"tile with no usable area", viewport{frame: v.frame}, Placement{Rect: Rect{W: 50, H: 60}}, false}, | |
| 32 | + | {"float with no usable area", viewport{frame: v.frame}, Placement{Rect: Rect{W: 50, H: 60}, Floating: true}, true}, | |
| 33 | + | } { | |
| 34 | + | if got := tc.v.shows(tc.p); got != tc.want { | |
| 35 | + | t.Errorf("%s: shows %v, want %v", tc.name, got, tc.want) | |
| 36 | + | } | |
| 37 | + | } | |
| 38 | + | } | |
| 39 | + | ||
| 40 | + | // viewportCore is a core with one 100x80 output whose usable area is | |
| 41 | + | // usable, publishing into the returned check: it asserts what each | |
| 42 | + | // window's configure and the state snapshot say about its visibility. | |
| 43 | + | func viewportCore(t *testing.T, layout func(*ports.Config), usable Rect) (*Core, func(map[WindowID]bool) ports.Scene) { | |
| 44 | + | t.Helper() | |
| 45 | + | cfg := ports.Config{} | |
| 46 | + | cfg.Keyboard.CmdKey = "super" | |
| 47 | + | layout(&cfg) | |
| 48 | + | commands := make(chan ports.ClientCommand, 256) | |
| 49 | + | scenes := make(chan []ports.Scene, 1) | |
| 50 | + | c, err := New(cfg, Channels{Commands: commands, Scenes: scenes}, Options{}) | |
| 51 | + | if err != nil { | |
| 52 | + | t.Fatal(err) | |
| 53 | + | } | |
| 54 | + | c.addScreen(ports.OutputInfo{Name: "OUT-1", Width: 100, Height: 80}) | |
| 55 | + | c.cur().mon.SetUsable(usable) | |
| 56 | + | sent := map[WindowID]ports.ConfigureWindow{} | |
| 57 | + | check := func(want map[WindowID]bool) ports.Scene { | |
| 58 | + | t.Helper() | |
| 59 | + | if err := c.publish(context.Background()); err != nil { | |
| 60 | + | t.Fatal(err) | |
| 61 | + | } | |
| 62 | + | scene := (<-scenes)[0] | |
| 63 | + | for len(commands) > 0 { | |
| 64 | + | if v, ok := (<-commands).(ports.ConfigureWindow); ok { | |
| 65 | + | sent[v.ID] = v | |
| 66 | + | } | |
| 67 | + | } | |
| 68 | + | state := map[WindowID]bool{} | |
| 69 | + | for _, s := range c.state().Windows { | |
| 70 | + | state[s.ID] = s.Visible | |
| 71 | + | } | |
| 72 | + | for id, vis := range want { | |
| 73 | + | if v := sent[id]; v.Visible != vis { | |
| 74 | + | t.Fatalf("window %d: configure %+v, want visible %v", id, v, vis) | |
| 75 | + | } | |
| 76 | + | if state[id] != vis { | |
| 77 | + | t.Fatalf("window %d: state visible %v, want %v", id, state[id], vis) | |
| 78 | + | } | |
| 79 | + | } | |
| 80 | + | return scene | |
| 81 | + | } | |
| 82 | + | return c, check | |
| 83 | + | } | |
| 84 | + | ||
| 85 | + | // The next cascade band starts right under a bottom panel: its windows are | |
| 86 | + | // not seen, so they are told they are invisible (and throttled), while the | |
| 87 | + | // scene still carries them. | |
| 88 | + | func TestConfigureVisibleCascadeUnderPanel(t *testing.T) { | |
| 89 | + | c, check := viewportCore(t, func(cfg *ports.Config) { | |
| 90 | + | cfg.Layout.Overflow = "cascade" | |
| 91 | + | cfg.Layout.MaxColumns = 2 | |
| 92 | + | }, Rect{W: 100, H: 60}) | |
| 93 | + | m := c.cur().mon | |
| 94 | + | w := m.Current() | |
| 95 | + | for id := WindowID(1); id <= 3; id++ { | |
| 96 | + | m.AddWindow(id) | |
| 97 | + | } | |
| 98 | + | w.FocusID(1) | |
| 99 | + | scene := check(map[WindowID]bool{1: true, 2: true, 3: false}) | |
| 100 | + | found := false | |
| 101 | + | for _, sw := range scene.Windows { | |
| 102 | + | found = found || sw.ID == 3 | |
| 103 | + | } | |
| 104 | + | if !found { | |
| 105 | + | t.Fatal("window under the panel left the scene") | |
| 106 | + | } | |
| 107 | + | // It is cut at the panel and takes no pointer there. | |
| 108 | + | if scene.TileInset != (ports.Insets{Bottom: 20}) { | |
| 109 | + | t.Fatalf("tile inset %+v, want the panel's", scene.TileInset) | |
| 110 | + | } | |
| 111 | + | if id, _, _ := c.hit(10, 70); id != 0 { | |
| 112 | + | t.Fatalf("pointer over the panel went to window %d", id) | |
| 113 | + | } | |
| 114 | + | if id, _, _ := c.hit(10, 30); id != 1 { | |
| 115 | + | t.Fatalf("pointer over window 1 went to %d", id) | |
| 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 | + | delete(c.popups, 10) | |
| 126 | + | c.popupOrder = slices.DeleteFunc(c.popupOrder, func(id WindowID) bool { return id == 10 }) | |
| 127 | + | // The overview lays previews over the whole output: no inset. | |
| 128 | + | m.ToggleOverview() | |
| 129 | + | if s := check(nil); s.TileInset != (ports.Insets{}) { | |
| 130 | + | t.Fatalf("tile inset %+v in the overview", s.TileInset) | |
| 131 | + | } | |
| 132 | + | m.ToggleOverview() | |
| 133 | + | // Its band scrolled in, the first band is under nothing but off screen. | |
| 134 | + | w.FocusID(3) | |
| 135 | + | check(map[WindowID]bool{1: false, 2: false, 3: true}) | |
| 136 | + | // Scrolled back, its band is under the panel again. | |
| 137 | + | w.FocusID(1) | |
| 138 | + | check(map[WindowID]bool{1: true, 3: false}) | |
| 139 | + | // Fullscreen covers the panel: always seen. | |
| 140 | + | w.FocusID(3) | |
| 141 | + | w.ToggleFullscreen() | |
| 142 | + | check(map[WindowID]bool{3: true}) | |
| 143 | + | } | |
| 144 | + | ||
| 145 | + | // A tile half under a bottom panel: its lines, its pointer bounds (warp, | |
| 146 | + | // constraints) and its draw all stop at the panel, and the renderer's tile | |
| 147 | + | // rule matches core's. | |
| 148 | + | func TestTileStopsAtPanel(t *testing.T) { | |
| 149 | + | c, check := viewportCore(t, func(cfg *ports.Config) { | |
| 150 | + | cfg.Layout.MaxColumns = 2 | |
| 151 | + | cfg.Border.Width = 2 | |
| 152 | + | }, Rect{W: 100, H: 60}) | |
| 153 | + | m := c.cur().mon | |
| 154 | + | m.AddWindow(1) | |
| 155 | + | m.AddWindow(3) | |
| 156 | + | m.Current().ToggleWindowStash() // 3 floats | |
| 157 | + | m.AddWindow(2) | |
| 158 | + | m.Current().FocusID(1) | |
| 159 | + | scene := check(map[WindowID]bool{1: true, 2: true, 3: true}) | |
| 160 | + | // Move tile 1 across the panel line, as a slide would. | |
| 161 | + | sc := c.cur() | |
| 162 | + | sc.shown = slices.Clone(sc.shownLayout()) | |
| 163 | + | for i := range sc.shown { | |
| 164 | + | if sc.shown[i].ID == 1 { | |
| 165 | + | sc.shown[i].Rect.Y = 30 | |
| 166 | + | } | |
| 167 | + | } | |
| 168 | + | if r, ok := c.shownClientRect(1); !ok || r.Y+r.H > 60 { | |
| 169 | + | t.Fatalf("pointer bounds %+v %v reach over the panel", r, ok) | |
| 170 | + | } | |
| 171 | + | for _, s := range separators(sc.shown, 2, 0, m.viewport(), true) { | |
| 172 | + | if s.Window == 0 && s.Rect.Y+s.Rect.H > 60 { | |
| 173 | + | t.Fatalf("tile line over the panel: %+v", s) | |
| 174 | + | } | |
| 175 | + | } | |
| 176 | + | view := m.viewport() | |
| 177 | + | for k, sw := range scene.Windows { | |
| 178 | + | if sw.Popup { | |
| 179 | + | continue | |
| 180 | + | } | |
| 181 | + | if p := sc.settledLayout[k]; sw.Tile() != (view.area(p) == view.usable) { | |
| 182 | + | t.Fatalf("window %d: renderer tile %v, core area %+v", sw.ID, sw.Tile(), view.area(p)) | |
| 183 | + | } | |
| 184 | + | } | |
| 185 | + | } | |
| 186 | + | ||
| 187 | + | // Panels that leave no room draw no tile, a leaving one included, and the | |
| 188 | + | // focus pulse has nothing to show. | |
| 189 | + | func TestNoUsableAreaDrawsNoTile(t *testing.T) { | |
| 190 | + | c, check := viewportCore(t, func(cfg *ports.Config) { cfg.Layout.MaxColumns = 1 }, Rect{}) | |
| 191 | + | c.cur().mon.AddWindow(1) | |
| 192 | + | c.pulse.target = 1 | |
| 193 | + | scene := check(map[WindowID]bool{1: false}) | |
| 194 | + | if scene.Draws(scene.Windows[0]) || c.pulse.drawable { | |
| 195 | + | t.Fatalf("tile drawn %v, pulse drawable %v with no usable area", scene.Draws(scene.Windows[0]), c.pulse.drawable) | |
| 196 | + | } | |
| 197 | + | sc := c.cur() | |
| 198 | + | sc.shown = slices.Clone(sc.shownLayout()) | |
| 199 | + | sc.shown[0].Leaving = true | |
| 200 | + | scene, _, err := c.sceneFor(context.Background(), 0, sc, nil, nil, 0) | |
| 201 | + | if err != nil { | |
| 202 | + | t.Fatal(err) | |
| 203 | + | } | |
| 204 | + | if scene.Draws(scene.Windows[0]) { | |
| 205 | + | t.Fatalf("leaving tile drawn with no usable area: %+v", scene.Windows[0]) | |
| 206 | + | } | |
| 207 | + | } | |
| 208 | + | ||
| 209 | + | // A sized workspace with a panel: the scene's tile area is the workspace's | |
| 210 | + | // usable area, inside its viewport. | |
| 211 | + | func TestTileAreaSizedWorkspace(t *testing.T) { | |
| 212 | + | c, check := viewportCore(t, func(cfg *ports.Config) { cfg.Layout.MaxColumns = 1 }, Rect{W: 100, H: 70}) | |
| 213 | + | m := c.cur().mon | |
| 214 | + | m.Current().SetSize(60, 80) | |
| 215 | + | m.AddWindow(1) | |
| 216 | + | scene := check(map[WindowID]bool{1: true}) | |
| 217 | + | a, ok := scene.TileArea() | |
| 218 | + | if u := m.Current().Usable; !ok || a != u || u == m.Current().Output { | |
| 219 | + | t.Fatalf("tile area %+v %v, want usable %+v inside frame %+v", a, ok, u, m.Current().Output) | |
| 220 | + | } | |
| 221 | + | } | |
| 222 | + | ||
| 223 | + | // A column scrolled under a left panel is not seen. | |
| 224 | + | func TestConfigureVisibleScrollUnderSidePanel(t *testing.T) { | |
| 225 | + | c, check := viewportCore(t, func(cfg *ports.Config) { | |
| 226 | + | cfg.Layout.MaxColumns = 1 | |
| 227 | + | }, Rect{X: 20, W: 80, H: 80}) | |
| 228 | + | m := c.cur().mon | |
| 229 | + | m.AddWindow(1) | |
| 230 | + | m.AddWindow(2) | |
| 231 | + | if r := m.Layout()[0].Rect; r.X >= 20 || r.X+r.W <= 0 { | |
| 232 | + | t.Fatalf("column 1 at %+v, want partly under the panel", r) | |
| 233 | + | } | |
| 234 | + | check(map[WindowID]bool{1: false, 2: true}) | |
| 235 | + | } | |
| @@ -0,0 +1,235 @@ | |||||
| 1 | + | package core | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "slices" | |||
| 6 | + | "testing" | |||
| 7 | + | ||||
| 8 | + | "github.com/bnema/neferwl/internal/ports" | |||
| 9 | + | ) | |||
| 10 | + | ||||
| 11 | + | // A tile is seen in the usable area; a float, a fullscreen window and a | |||
| 12 | + | // preview are placed against the whole frame. | |||
| 13 | + | func TestViewportShows(t *testing.T) { | |||
| 14 | + | v := viewport{frame: Rect{W: 100, H: 80}, usable: Rect{W: 100, H: 60}} | |||
| 15 | + | under := Rect{Y: 60, W: 50, H: 60} // the next cascade band, under a bottom panel | |||
| 16 | + | for _, tc := range []struct { | |||
| 17 | + | name string | |||
| 18 | + | v viewport | |||
| 19 | + | p Placement | |||
| 20 | + | want bool | |||
| 21 | + | }{ | |||
| 22 | + | {"tile in usable", v, Placement{Rect: Rect{W: 50, H: 60}}, true}, | |||
| 23 | + | {"tile under panel", v, Placement{Rect: under}, false}, | |||
| 24 | + | {"tile across the panel line", v, Placement{Rect: Rect{Y: 30, W: 50, H: 60}}, true}, | |||
| 25 | + | {"tile off frame", v, Placement{Rect: Rect{Y: 80, W: 50, H: 60}}, false}, | |||
| 26 | + | {"hidden tile", v, Placement{Rect: Rect{W: 50, H: 60}, Hidden: true}, false}, | |||
| 27 | + | {"float under panel", v, Placement{Rect: under, Floating: true}, true}, | |||
| 28 | + | {"hidden float", v, Placement{Rect: Rect{W: 50, H: 60}, Floating: true, Hidden: true}, false}, | |||
| 29 | + | {"fullscreen tile", v, Placement{Rect: Rect{W: 100, H: 80}, Fullscreen: true}, true}, | |||
| 30 | + | {"preview under panel", v, Placement{Rect: under, Preview: 0.5}, true}, | |||
| 31 | + | {"tile with no usable area", viewport{frame: v.frame}, Placement{Rect: Rect{W: 50, H: 60}}, false}, | |||
| 32 | + | {"float with no usable area", viewport{frame: v.frame}, Placement{Rect: Rect{W: 50, H: 60}, Floating: true}, true}, | |||
| 33 | + | } { | |||
| 34 | + | if got := tc.v.shows(tc.p); got != tc.want { | |||
| 35 | + | t.Errorf("%s: shows %v, want %v", tc.name, got, tc.want) | |||
| 36 | + | } | |||
| 37 | + | } | |||
| 38 | + | } | |||
| 39 | + | ||||
| 40 | + | // viewportCore is a core with one 100x80 output whose usable area is | |||
| 41 | + | // usable, publishing into the returned check: it asserts what each | |||
| 42 | + | // window's configure and the state snapshot say about its visibility. | |||
| 43 | + | func viewportCore(t *testing.T, layout func(*ports.Config), usable Rect) (*Core, func(map[WindowID]bool) ports.Scene) { | |||
| 44 | + | t.Helper() | |||
| 45 | + | cfg := ports.Config{} | |||
| 46 | + | cfg.Keyboard.CmdKey = "super" | |||
| 47 | + | layout(&cfg) | |||
| 48 | + | commands := make(chan ports.ClientCommand, 256) | |||
| 49 | + | scenes := make(chan []ports.Scene, 1) | |||
| 50 | + | c, err := New(cfg, Channels{Commands: commands, Scenes: scenes}, Options{}) | |||
| 51 | + | if err != nil { | |||
| 52 | + | t.Fatal(err) | |||
| 53 | + | } | |||
| 54 | + | c.addScreen(ports.OutputInfo{Name: "OUT-1", Width: 100, Height: 80}) | |||
| 55 | + | c.cur().mon.SetUsable(usable) | |||
| 56 | + | sent := map[WindowID]ports.ConfigureWindow{} | |||
| 57 | + | check := func(want map[WindowID]bool) ports.Scene { | |||
| 58 | + | t.Helper() | |||
| 59 | + | if err := c.publish(context.Background()); err != nil { | |||
| 60 | + | t.Fatal(err) | |||
| 61 | + | } | |||
| 62 | + | scene := (<-scenes)[0] | |||
| 63 | + | for len(commands) > 0 { | |||
| 64 | + | if v, ok := (<-commands).(ports.ConfigureWindow); ok { | |||
| 65 | + | sent[v.ID] = v | |||
| 66 | + | } | |||
| 67 | + | } | |||
| 68 | + | state := map[WindowID]bool{} | |||
| 69 | + | for _, s := range c.state().Windows { | |||
| 70 | + | state[s.ID] = s.Visible | |||
| 71 | + | } | |||
| 72 | + | for id, vis := range want { | |||
| 73 | + | if v := sent[id]; v.Visible != vis { | |||
| 74 | + | t.Fatalf("window %d: configure %+v, want visible %v", id, v, vis) | |||
| 75 | + | } | |||
| 76 | + | if state[id] != vis { | |||
| 77 | + | t.Fatalf("window %d: state visible %v, want %v", id, state[id], vis) | |||
| 78 | + | } | |||
| 79 | + | } | |||
| 80 | + | return scene | |||
| 81 | + | } | |||
| 82 | + | return c, check | |||
| 83 | + | } | |||
| 84 | + | ||||
| 85 | + | // The next cascade band starts right under a bottom panel: its windows are | |||
| 86 | + | // not seen, so they are told they are invisible (and throttled), while the | |||
| 87 | + | // scene still carries them. | |||
| 88 | + | func TestConfigureVisibleCascadeUnderPanel(t *testing.T) { | |||
| 89 | + | c, check := viewportCore(t, func(cfg *ports.Config) { | |||
| 90 | + | cfg.Layout.Overflow = "cascade" | |||
| 91 | + | cfg.Layout.MaxColumns = 2 | |||
| 92 | + | }, Rect{W: 100, H: 60}) | |||
| 93 | + | m := c.cur().mon | |||
| 94 | + | w := m.Current() | |||
| 95 | + | for id := WindowID(1); id <= 3; id++ { | |||
| 96 | + | m.AddWindow(id) | |||
| 97 | + | } | |||
| 98 | + | w.FocusID(1) | |||
| 99 | + | scene := check(map[WindowID]bool{1: true, 2: true, 3: false}) | |||
| 100 | + | found := false | |||
| 101 | + | for _, sw := range scene.Windows { | |||
| 102 | + | found = found || sw.ID == 3 | |||
| 103 | + | } | |||
| 104 | + | if !found { | |||
| 105 | + | t.Fatal("window under the panel left the scene") | |||
| 106 | + | } | |||
| 107 | + | // It is cut at the panel and takes no pointer there. | |||
| 108 | + | if scene.TileInset != (ports.Insets{Bottom: 20}) { | |||
| 109 | + | t.Fatalf("tile inset %+v, want the panel's", scene.TileInset) | |||
| 110 | + | } | |||
| 111 | + | if id, _, _ := c.hit(10, 70); id != 0 { | |||
| 112 | + | t.Fatalf("pointer over the panel went to window %d", id) | |||
| 113 | + | } | |||
| 114 | + | if id, _, _ := c.hit(10, 30); id != 1 { | |||
| 115 | + | t.Fatalf("pointer over window 1 went to %d", id) | |||
| 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 | + | delete(c.popups, 10) | |||
| 126 | + | c.popupOrder = slices.DeleteFunc(c.popupOrder, func(id WindowID) bool { return id == 10 }) | |||
| 127 | + | // The overview lays previews over the whole output: no inset. | |||
| 128 | + | m.ToggleOverview() | |||
| 129 | + | if s := check(nil); s.TileInset != (ports.Insets{}) { | |||
| 130 | + | t.Fatalf("tile inset %+v in the overview", s.TileInset) | |||
| 131 | + | } | |||
| 132 | + | m.ToggleOverview() | |||
| 133 | + | // Its band scrolled in, the first band is under nothing but off screen. | |||
| 134 | + | w.FocusID(3) | |||
| 135 | + | check(map[WindowID]bool{1: false, 2: false, 3: true}) | |||
| 136 | + | // Scrolled back, its band is under the panel again. | |||
| 137 | + | w.FocusID(1) | |||
| 138 | + | check(map[WindowID]bool{1: true, 3: false}) | |||
| 139 | + | // Fullscreen covers the panel: always seen. | |||
| 140 | + | w.FocusID(3) | |||
| 141 | + | w.ToggleFullscreen() | |||
| 142 | + | check(map[WindowID]bool{3: true}) | |||
| 143 | + | } | |||
| 144 | + | ||||
| 145 | + | // A tile half under a bottom panel: its lines, its pointer bounds (warp, | |||
| 146 | + | // constraints) and its draw all stop at the panel, and the renderer's tile | |||
| 147 | + | // rule matches core's. | |||
| 148 | + | func TestTileStopsAtPanel(t *testing.T) { | |||
| 149 | + | c, check := viewportCore(t, func(cfg *ports.Config) { | |||
| 150 | + | cfg.Layout.MaxColumns = 2 | |||
| 151 | + | cfg.Border.Width = 2 | |||
| 152 | + | }, Rect{W: 100, H: 60}) | |||
| 153 | + | m := c.cur().mon | |||
| 154 | + | m.AddWindow(1) | |||
| 155 | + | m.AddWindow(3) | |||
| 156 | + | m.Current().ToggleWindowStash() // 3 floats | |||
| 157 | + | m.AddWindow(2) | |||
| 158 | + | m.Current().FocusID(1) | |||
| 159 | + | scene := check(map[WindowID]bool{1: true, 2: true, 3: true}) | |||
| 160 | + | // Move tile 1 across the panel line, as a slide would. | |||
| 161 | + | sc := c.cur() | |||
| 162 | + | sc.shown = slices.Clone(sc.shownLayout()) | |||
| 163 | + | for i := range sc.shown { | |||
| 164 | + | if sc.shown[i].ID == 1 { | |||
| 165 | + | sc.shown[i].Rect.Y = 30 | |||
| 166 | + | } | |||
| 167 | + | } | |||
| 168 | + | if r, ok := c.shownClientRect(1); !ok || r.Y+r.H > 60 { | |||
| 169 | + | t.Fatalf("pointer bounds %+v %v reach over the panel", r, ok) | |||
| 170 | + | } | |||
| 171 | + | for _, s := range separators(sc.shown, 2, 0, m.viewport(), true) { | |||
| 172 | + | if s.Window == 0 && s.Rect.Y+s.Rect.H > 60 { | |||
| 173 | + | t.Fatalf("tile line over the panel: %+v", s) | |||
| 174 | + | } | |||
| 175 | + | } | |||
| 176 | + | view := m.viewport() | |||
| 177 | + | for k, sw := range scene.Windows { | |||
| 178 | + | if sw.Popup { | |||
| 179 | + | continue | |||
| 180 | + | } | |||
| 181 | + | if p := sc.settledLayout[k]; sw.Tile() != (view.area(p) == view.usable) { | |||
| 182 | + | t.Fatalf("window %d: renderer tile %v, core area %+v", sw.ID, sw.Tile(), view.area(p)) | |||
| 183 | + | } | |||
| 184 | + | } | |||
| 185 | + | } | |||
| 186 | + | ||||
| 187 | + | // Panels that leave no room draw no tile, a leaving one included, and the | |||
| 188 | + | // focus pulse has nothing to show. | |||
| 189 | + | func TestNoUsableAreaDrawsNoTile(t *testing.T) { | |||
| 190 | + | c, check := viewportCore(t, func(cfg *ports.Config) { cfg.Layout.MaxColumns = 1 }, Rect{}) | |||
| 191 | + | c.cur().mon.AddWindow(1) | |||
| 192 | + | c.pulse.target = 1 | |||
| 193 | + | scene := check(map[WindowID]bool{1: false}) | |||
| 194 | + | if scene.Draws(scene.Windows[0]) || c.pulse.drawable { | |||
| 195 | + | t.Fatalf("tile drawn %v, pulse drawable %v with no usable area", scene.Draws(scene.Windows[0]), c.pulse.drawable) | |||
| 196 | + | } | |||
| 197 | + | sc := c.cur() | |||
| 198 | + | sc.shown = slices.Clone(sc.shownLayout()) | |||
| 199 | + | sc.shown[0].Leaving = true | |||
| 200 | + | scene, _, err := c.sceneFor(context.Background(), 0, sc, nil, nil, 0) | |||
| 201 | + | if err != nil { | |||
| 202 | + | t.Fatal(err) | |||
| 203 | + | } | |||
| 204 | + | if scene.Draws(scene.Windows[0]) { | |||
| 205 | + | t.Fatalf("leaving tile drawn with no usable area: %+v", scene.Windows[0]) | |||
| 206 | + | } | |||
| 207 | + | } | |||
| 208 | + | ||||
| 209 | + | // A sized workspace with a panel: the scene's tile area is the workspace's | |||
| 210 | + | // usable area, inside its viewport. | |||
| 211 | + | func TestTileAreaSizedWorkspace(t *testing.T) { | |||
| 212 | + | c, check := viewportCore(t, func(cfg *ports.Config) { cfg.Layout.MaxColumns = 1 }, Rect{W: 100, H: 70}) | |||
| 213 | + | m := c.cur().mon | |||
| 214 | + | m.Current().SetSize(60, 80) | |||
| 215 | + | m.AddWindow(1) | |||
| 216 | + | scene := check(map[WindowID]bool{1: true}) | |||
| 217 | + | a, ok := scene.TileArea() | |||
| 218 | + | if u := m.Current().Usable; !ok || a != u || u == m.Current().Output { | |||
| 219 | + | t.Fatalf("tile area %+v %v, want usable %+v inside frame %+v", a, ok, u, m.Current().Output) | |||
| 220 | + | } | |||
| 221 | + | } | |||
| 222 | + | ||||
| 223 | + | // A column scrolled under a left panel is not seen. | |||
| 224 | + | func TestConfigureVisibleScrollUnderSidePanel(t *testing.T) { | |||
| 225 | + | c, check := viewportCore(t, func(cfg *ports.Config) { | |||
| 226 | + | cfg.Layout.MaxColumns = 1 | |||
| 227 | + | }, Rect{X: 20, W: 80, H: 80}) | |||
| 228 | + | m := c.cur().mon | |||
| 229 | + | m.AddWindow(1) | |||
| 230 | + | m.AddWindow(2) | |||
| 231 | + | if r := m.Layout()[0].Rect; r.X >= 20 || r.X+r.W <= 0 { | |||
| 232 | + | t.Fatalf("column 1 at %+v, want partly under the panel", r) | |||
| 233 | + | } | |||
| 234 | + | check(map[WindowID]bool{1: false, 2: true}) | |||
| 235 | + | } | |||
M internal/ports/scene.go +44 -5
| @@ -27,6 +27,13 @@ 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 | + | // TileInset is what exclusive layers (panels) and a workspace size | |
| 31 | + | // override take from the output's edges: tiles (SceneWindow.Tile) and | |
| 32 | + | // their lines are drawn only in what is left, the usable area | |
| 33 | + | // (TileArea). A tile is never drawn over a panel, whatever its layer; | |
| 34 | + | // insets covering the output leave no tile drawn. Zero: the usable | |
| 35 | + | // area is the whole output (and the overview, capture scenes). | |
| 36 | + | TileInset Insets | |
| 30 | 37 | // Dim darkens the background, bottom layers and tiles (tile lines | |
| 31 | 38 | // included) inside WorkspaceClip when set, with black at this opacity, | |
| 32 | 39 | // 0 to 1, under the first | |
| @@ -74,7 +81,7 @@ func (s Scene) SameAs(o Scene) bool { | |||
| 74 | 81 | s.OutputWidth != o.OutputWidth || s.OutputHeight != o.OutputHeight || | |
| 75 | 82 | s.Scale != o.Scale || s.Transform != o.Transform || s.Off != o.Off || | |
| 76 | 83 | s.Background != o.Background || s.Border != o.Border || | |
| 77 | - | s.WorkspaceClip != o.WorkspaceClip || s.Dim != o.Dim || s.DimBehind != o.DimBehind { | |
| 84 | + | s.WorkspaceClip != o.WorkspaceClip || s.TileInset != o.TileInset || s.Dim != o.Dim || s.DimBehind != o.DimBehind { | |
| 78 | 85 | return false | |
| 79 | 86 | } | |
| 80 | 87 | if s.CaptureScene != nil || o.CaptureScene != nil { | |
| @@ -97,12 +104,10 @@ func (s Scene) SameAs(o Scene) bool { | |||
| 97 | 104 | ||
| 98 | 105 | // Shows reports whether the scene draws the surface of id: only its | |
| 99 | 106 | // 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. | |
| 107 | + | // not drawn (Draws). | |
| 102 | 108 | func (s Scene) Shows(id WindowID) bool { | |
| 103 | 109 | for _, w := range s.Windows { | |
| 104 | - | 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 | + | if w.ID == id && s.Draws(w) { | |
| 106 | 111 | return true | |
| 107 | 112 | } | |
| 108 | 113 | } | |
| @@ -114,6 +119,33 @@ func (s Scene) Shows(id WindowID) bool { | |||
| 114 | 119 | return false | |
| 115 | 120 | } | |
| 116 | 121 | ||
| 122 | + | // Draws reports whether any of w is drawn: not hidden, on the output, | |
| 123 | + | // inside WorkspaceClip (OverLayers popups remain output-wide) and, for a | |
| 124 | + | // tile, inside TileArea. | |
| 125 | + | func (s Scene) Draws(w SceneWindow) bool { | |
| 126 | + | if w.Hidden || !w.Rect.Overlaps(Rect{W: s.OutputWidth, H: s.OutputHeight}) { | |
| 127 | + | return false | |
| 128 | + | } | |
| 129 | + | if !w.OverLayers && s.WorkspaceClip != (Rect{}) && !w.Rect.Overlaps(s.WorkspaceClip) { | |
| 130 | + | return false | |
| 131 | + | } | |
| 132 | + | if a, ok := s.TileArea(); ok && w.Tile() { | |
| 133 | + | return w.Rect.Overlaps(a) | |
| 134 | + | } | |
| 135 | + | return true | |
| 136 | + | } | |
| 137 | + | ||
| 138 | + | // TileArea is where tiles are drawn: the output minus TileInset, empty | |
| 139 | + | // when the panels cover it. ok is false when TileInset is zero: tiles are | |
| 140 | + | // not clipped beyond the output and WorkspaceClip. | |
| 141 | + | func (s Scene) TileArea() (r Rect, ok bool) { | |
| 142 | + | in := s.TileInset | |
| 143 | + | if in == (Insets{}) { | |
| 144 | + | return Rect{}, false | |
| 145 | + | } | |
| 146 | + | 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 | |
| 147 | + | } | |
| 148 | + | ||
| 117 | 149 | // Border carries the window border style; colors are #rrggbb, "" skips | |
| 118 | 150 | // drawing. Width is in logical pixels. | |
| 119 | 151 | type Border struct { | |
| @@ -180,3 +212,10 @@ type SceneWindow struct { | |||
| 180 | 212 | // overlay layers. Window popups stay with the windows, under them. | |
| 181 | 213 | OverLayers bool | |
| 182 | 214 | } | |
| 215 | + | ||
| 216 | + | // Tile reports whether w is a tile: laid out in the usable area and | |
| 217 | + | // clipped to Scene.TileArea. Floats, fullscreen windows, overview previews | |
| 218 | + | // and popups are placed against the whole viewport. | |
| 219 | + | func (w SceneWindow) Tile() bool { | |
| 220 | + | return !w.Floating && !w.Fullscreen && !w.Popup && w.Preview == 0 | |
| 221 | + | } | |
| @@ -27,6 +27,13 @@ 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 | + | // TileInset is what exclusive layers (panels) and a workspace size | |||
| 31 | + | // override take from the output's edges: tiles (SceneWindow.Tile) and | |||
| 32 | + | // their lines are drawn only in what is left, the usable area | |||
| 33 | + | // (TileArea). A tile is never drawn over a panel, whatever its layer; | |||
| 34 | + | // insets covering the output leave no tile drawn. Zero: the usable | |||
| 35 | + | // area is the whole output (and the overview, capture scenes). | |||
| 36 | + | TileInset Insets | |||
| 30 | // Dim darkens the background, bottom layers and tiles (tile lines | 37 | // Dim darkens the background, bottom layers and tiles (tile lines | ||
| 31 | // included) inside WorkspaceClip when set, with black at this opacity, | 38 | // included) inside WorkspaceClip when set, with black at this opacity, | ||
| 32 | // 0 to 1, under the first | 39 | // 0 to 1, under the first | ||
| @@ -74,7 +81,7 @@ func (s Scene) SameAs(o Scene) bool { | |||||
| 74 | s.OutputWidth != o.OutputWidth || s.OutputHeight != o.OutputHeight || | 81 | s.OutputWidth != o.OutputWidth || s.OutputHeight != o.OutputHeight || | ||
| 75 | s.Scale != o.Scale || s.Transform != o.Transform || s.Off != o.Off || | 82 | s.Scale != o.Scale || s.Transform != o.Transform || s.Off != o.Off || | ||
| 76 | s.Background != o.Background || s.Border != o.Border || | 83 | s.Background != o.Background || s.Border != o.Border || | ||
| 77 | - | s.WorkspaceClip != o.WorkspaceClip || s.Dim != o.Dim || s.DimBehind != o.DimBehind { | 84 | + | s.WorkspaceClip != o.WorkspaceClip || s.TileInset != o.TileInset || s.Dim != o.Dim || s.DimBehind != o.DimBehind { |
| 78 | return false | 85 | return false | ||
| 79 | } | 86 | } | ||
| 80 | if s.CaptureScene != nil || o.CaptureScene != nil { | 87 | if s.CaptureScene != nil || o.CaptureScene != nil { | ||
| @@ -97,12 +104,10 @@ func (s Scene) SameAs(o Scene) bool { | |||||
| 97 | 104 | ||||
| 98 | // Shows reports whether the scene draws the surface of id: only its | 105 | // 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 is | 106 | // content changes need a new frame. A window scrolled off the output is | ||
| 100 | - | // not drawn. WorkspaceClip also excludes off-viewport windows; OverLayers | 107 | + | // not drawn (Draws). |
| 101 | - | // popups remain output-wide. | |||
| 102 | func (s Scene) Shows(id WindowID) bool { | 108 | func (s Scene) Shows(id WindowID) bool { | ||
| 103 | for _, w := range s.Windows { | 109 | for _, w := range s.Windows { | ||
| 104 | - | if w.ID == id && !w.Hidden && w.Rect.Overlaps(Rect{W: s.OutputWidth, H: s.OutputHeight}) && | 110 | + | if w.ID == id && s.Draws(w) { |
| 105 | - | (w.OverLayers || s.WorkspaceClip == (Rect{}) || w.Rect.Overlaps(s.WorkspaceClip)) { | |||
| 106 | return true | 111 | return true | ||
| 107 | } | 112 | } | ||
| 108 | } | 113 | } | ||
| @@ -114,6 +119,33 @@ func (s Scene) Shows(id WindowID) bool { | |||||
| 114 | return false | 119 | return false | ||
| 115 | } | 120 | } | ||
| 116 | 121 | ||||
| 122 | + | // Draws reports whether any of w is drawn: not hidden, on the output, | |||
| 123 | + | // inside WorkspaceClip (OverLayers popups remain output-wide) and, for a | |||
| 124 | + | // tile, inside TileArea. | |||
| 125 | + | func (s Scene) Draws(w SceneWindow) bool { | |||
| 126 | + | if w.Hidden || !w.Rect.Overlaps(Rect{W: s.OutputWidth, H: s.OutputHeight}) { | |||
| 127 | + | return false | |||
| 128 | + | } | |||
| 129 | + | if !w.OverLayers && s.WorkspaceClip != (Rect{}) && !w.Rect.Overlaps(s.WorkspaceClip) { | |||
| 130 | + | return false | |||
| 131 | + | } | |||
| 132 | + | if a, ok := s.TileArea(); ok && w.Tile() { | |||
| 133 | + | return w.Rect.Overlaps(a) | |||
| 134 | + | } | |||
| 135 | + | return true | |||
| 136 | + | } | |||
| 137 | + | ||||
| 138 | + | // TileArea is where tiles are drawn: the output minus TileInset, empty | |||
| 139 | + | // when the panels cover it. ok is false when TileInset is zero: tiles are | |||
| 140 | + | // not clipped beyond the output and WorkspaceClip. | |||
| 141 | + | func (s Scene) TileArea() (r Rect, ok bool) { | |||
| 142 | + | in := s.TileInset | |||
| 143 | + | if in == (Insets{}) { | |||
| 144 | + | return Rect{}, false | |||
| 145 | + | } | |||
| 146 | + | 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 | |||
| 147 | + | } | |||
| 148 | + | ||||
| 117 | // Border carries the window border style; colors are #rrggbb, "" skips | 149 | // Border carries the window border style; colors are #rrggbb, "" skips | ||
| 118 | // drawing. Width is in logical pixels. | 150 | // drawing. Width is in logical pixels. | ||
| 119 | type Border struct { | 151 | type Border struct { | ||
| @@ -180,3 +212,10 @@ type SceneWindow struct { | |||||
| 180 | // overlay layers. Window popups stay with the windows, under them. | 212 | // overlay layers. Window popups stay with the windows, under them. | ||
| 181 | OverLayers bool | 213 | OverLayers bool | ||
| 182 | } | 214 | } | ||
| 215 | + | ||||
| 216 | + | // Tile reports whether w is a tile: laid out in the usable area and | |||
| 217 | + | // clipped to Scene.TileArea. Floats, fullscreen windows, overview previews | |||
| 218 | + | // and popups are placed against the whole viewport. | |||
| 219 | + | func (w SceneWindow) Tile() bool { | |||
| 220 | + | return !w.Floating && !w.Fullscreen && !w.Popup && w.Preview == 0 | |||
| 221 | + | } | |||
M internal/ports/scene_same_test.go +2 -1
| @@ -48,6 +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 inset": func(s *Scene) { s.TileInset.Bottom = 1 }, | |
| 51 | 52 | "dim": func(s *Scene) { s.Dim = 0.5 }, | |
| 52 | 53 | "dim behind": func(s *Scene) { s.DimBehind = true }, | |
| 53 | 54 | "separator": func(s *Scene) { s.Separators[0].Active = true }, | |
| @@ -82,7 +83,7 @@ func TestSceneSameAs(t *testing.T) { | |||
| 82 | 83 | // TestSceneSameAsCoversEveryField fails when Scene gains or loses a field: | |
| 83 | 84 | // update Scene.SameAs, then this count. | |
| 84 | 85 | func TestSceneSameAsCoversEveryField(t *testing.T) { | |
| 85 | - | const fields = 20 | |
| 86 | + | const fields = 21 | |
| 86 | 87 | assert.Equal(t, fields, len(reflect.VisibleFields(reflect.TypeOf(Scene{}))), | |
| 87 | 88 | "Scene fields changed: update Scene.SameAs (and TestSceneSameAs), then this count") | |
| 88 | 89 | } | |
| @@ -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 inset": func(s *Scene) { s.TileInset.Bottom = 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 = 20 | 86 | + | 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/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 +37 -0
| @@ -16,3 +16,40 @@ func TestSceneShowsWorkspaceClip(t *testing.T) { | |||
| 16 | 16 | t.Fatal("inherited workspace excluded surface") | |
| 17 | 17 | } | |
| 18 | 18 | } | |
| 19 | + | ||
| 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) { | |
| 24 | + | under := Rect{Y: 80, W: 50, H: 80} | |
| 25 | + | s := Scene{OutputWidth: 100, OutputHeight: 100, TileInset: Insets{Bottom: 20}, Windows: []SceneWindow{ | |
| 26 | + | {ID: 1, Rect: Rect{W: 50, H: 80}}, | |
| 27 | + | {ID: 2, Rect: under}, | |
| 28 | + | {ID: 3, Rect: under, Floating: true}, | |
| 29 | + | {ID: 4, Rect: under, Fullscreen: true}, | |
| 30 | + | {ID: 5, Rect: under, Preview: 0.5}, | |
| 31 | + | {ID: 6, Rect: under, Popup: true}, | |
| 32 | + | }} | |
| 33 | + | for id, want := range map[WindowID]bool{1: true, 2: false, 3: true, 4: true, 5: true, 6: true} { | |
| 34 | + | if got := s.Shows(id); got != want { | |
| 35 | + | t.Errorf("window %d shown %v, want %v", id, got, want) | |
| 36 | + | } | |
| 37 | + | } | |
| 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{} | |
| 43 | + | if !s.Shows(2) { | |
| 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 | + | } | |
| 54 | + | } | |
| 55 | + | } | |
| @@ -16,3 +16,40 @@ 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 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) { | |||
| 24 | + | under := Rect{Y: 80, W: 50, H: 80} | |||
| 25 | + | s := Scene{OutputWidth: 100, OutputHeight: 100, TileInset: Insets{Bottom: 20}, Windows: []SceneWindow{ | |||
| 26 | + | {ID: 1, Rect: Rect{W: 50, H: 80}}, | |||
| 27 | + | {ID: 2, Rect: under}, | |||
| 28 | + | {ID: 3, Rect: under, Floating: true}, | |||
| 29 | + | {ID: 4, Rect: under, Fullscreen: true}, | |||
| 30 | + | {ID: 5, Rect: under, Preview: 0.5}, | |||
| 31 | + | {ID: 6, Rect: under, Popup: true}, | |||
| 32 | + | }} | |||
| 33 | + | for id, want := range map[WindowID]bool{1: true, 2: false, 3: true, 4: true, 5: true, 6: true} { | |||
| 34 | + | if got := s.Shows(id); got != want { | |||
| 35 | + | t.Errorf("window %d shown %v, want %v", id, got, want) | |||
| 36 | + | } | |||
| 37 | + | } | |||
| 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{} | |||
| 43 | + | if !s.Shows(2) { | |||
| 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 | + | } | |||
| 54 | + | } | |||
| 55 | + | } | |||