Merge branch 'main' into docs/deb-install
4 files · +170 -17
M internal/adapters/drm/overlay.go +29 -10
| @@ -57,8 +57,10 @@ type overlayWin struct { | |||
| 57 | 57 | } | |
| 58 | 58 | ||
| 59 | 59 | // overlayCandidate finds the one window the overlay can show: an opaque | |
| 60 | - | // dmabuf at integer physical coordinates, drawn 1:1, with no other window | |
| 61 | - | // or layer above it, and the plane colour mode it needs. reason is why none. | |
| 60 | + | // dmabuf at integer physical coordinates, drawn 1:1, with no other window, | |
| 61 | + | // popup, border line or layer drawn over it, and the plane colour mode it | |
| 62 | + | // needs. What is above it elsewhere on screen stays on the primary plane, | |
| 63 | + | // which the overlay covers only over the window. reason is why none. | |
| 62 | 64 | // | |
| 63 | 65 | // kept is scanoutCandidate's: a window drawn from a withdrawn content is | |
| 64 | 66 | // composed, never put on the plane. | |
| @@ -78,8 +80,9 @@ func overlayCandidate(s ports.Scene, surfaces map[ports.WindowID]ports.SurfaceCo | |||
| 78 | 80 | continue | |
| 79 | 81 | } | |
| 80 | 82 | if w.Popup { | |
| 81 | - | // Popups draw after every window, wherever they are listed. | |
| 82 | - | return ports.SceneWindow{}, ports.SurfaceContent{}, colorBypass, "window_above" | |
| 83 | + | // Popups draw after every window, wherever they are listed: | |
| 84 | + | // checked once the candidate is known. | |
| 85 | + | continue | |
| 83 | 86 | } | |
| 84 | 87 | c := surfaces[w.ID] | |
| 85 | 88 | // A viewport crop is composed: the plane would show the whole | |
| @@ -94,11 +97,18 @@ func overlayCandidate(s ports.Scene, surfaces map[ports.WindowID]ports.SurfaceCo | |||
| 94 | 97 | continue | |
| 95 | 98 | } | |
| 96 | 99 | } | |
| 97 | - | if pick != nil { | |
| 98 | - | // Something is drawn above the candidate. | |
| 100 | + | if pick != nil && w.Rect.Overlaps(pick.Rect) { | |
| 101 | + | // Something is drawn over the candidate. | |
| 99 | 102 | return ports.SceneWindow{}, ports.SurfaceContent{}, colorBypass, "window_above" | |
| 100 | 103 | } | |
| 101 | 104 | } | |
| 105 | + | if pick != nil { | |
| 106 | + | for i := range s.Windows { | |
| 107 | + | if w := &s.Windows[i]; w.Popup && !w.Hidden && w.Rect.Overlaps(pick.Rect) { | |
| 108 | + | return ports.SceneWindow{}, ports.SurfaceContent{}, colorBypass, "window_above" | |
| 109 | + | } | |
| 110 | + | } | |
| 111 | + | } | |
| 102 | 112 | if pick == nil { | |
| 103 | 113 | if kept != nil && len(s.Windows) > 0 { | |
| 104 | 114 | // The one window that could have gone on the plane fades out | |
| @@ -121,13 +131,22 @@ func overlayCandidate(s ports.Scene, surfaces map[ports.WindowID]ports.SurfaceCo | |||
| 121 | 131 | return ports.SceneWindow{}, ports.SurfaceContent{}, colorBypass, "drop_hint" | |
| 122 | 132 | } | |
| 123 | 133 | for _, l := range s.Layers { | |
| 124 | - | if l.Layer >= ports.LayerTop && l.Rect.W > 0 && l.Rect.H > 0 { | |
| 134 | + | if l.Layer >= ports.LayerTop && l.Rect.Overlaps(pick.Rect) { | |
| 125 | 135 | return ports.SceneWindow{}, ports.SurfaceContent{}, colorBypass, "layer_above" | |
| 126 | 136 | } | |
| 127 | 137 | } | |
| 128 | - | if !pick.Fullscreen && len(s.Separators) > 0 { | |
| 129 | - | // The overlay shows the buffer alone; the lines would go with it. | |
| 130 | - | return ports.SceneWindow{}, ports.SurfaceContent{}, colorBypass, "border" | |
| 138 | + | if !pick.Fullscreen { | |
| 139 | + | if pick.Inset != 0 && s.Border.Width > 0 { | |
| 140 | + | // The content sits inside the lines, not at the window's rect. | |
| 141 | + | return ports.SceneWindow{}, ports.SurfaceContent{}, colorBypass, "border" | |
| 142 | + | } | |
| 143 | + | for _, sep := range s.Separators { | |
| 144 | + | if sep.Rect.Overlaps(pick.Rect) { | |
| 145 | + | // The overlay shows the buffer alone; the lines would go | |
| 146 | + | // under it. | |
| 147 | + | return ports.SceneWindow{}, ports.SurfaceContent{}, colorBypass, "border" | |
| 148 | + | } | |
| 149 | + | } | |
| 131 | 150 | } | |
| 132 | 151 | if clip := s.WorkspaceClip; clip != (ports.Rect{}) && | |
| 133 | 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) { | |
| @@ -57,8 +57,10 @@ type overlayWin struct { | |||||
| 57 | } | 57 | } | ||
| 58 | 58 | ||||
| 59 | // overlayCandidate finds the one window the overlay can show: an opaque | 59 | // overlayCandidate finds the one window the overlay can show: an opaque | ||
| 60 | - | // dmabuf at integer physical coordinates, drawn 1:1, with no other window | 60 | + | // dmabuf at integer physical coordinates, drawn 1:1, with no other window, |
| 61 | - | // or layer above it, and the plane colour mode it needs. reason is why none. | 61 | + | // popup, border line or layer drawn over it, and the plane colour mode it |
| 62 | + | // needs. What is above it elsewhere on screen stays on the primary plane, | |||
| 63 | + | // which the overlay covers only over the window. reason is why none. | |||
| 62 | // | 64 | // | ||
| 63 | // kept is scanoutCandidate's: a window drawn from a withdrawn content is | 65 | // kept is scanoutCandidate's: a window drawn from a withdrawn content is | ||
| 64 | // composed, never put on the plane. | 66 | // composed, never put on the plane. | ||
| @@ -78,8 +80,9 @@ func overlayCandidate(s ports.Scene, surfaces map[ports.WindowID]ports.SurfaceCo | |||||
| 78 | continue | 80 | continue | ||
| 79 | } | 81 | } | ||
| 80 | if w.Popup { | 82 | if w.Popup { | ||
| 81 | - | // Popups draw after every window, wherever they are listed. | 83 | + | // Popups draw after every window, wherever they are listed: |
| 82 | - | return ports.SceneWindow{}, ports.SurfaceContent{}, colorBypass, "window_above" | 84 | + | // checked once the candidate is known. |
| 85 | + | continue | |||
| 83 | } | 86 | } | ||
| 84 | c := surfaces[w.ID] | 87 | c := surfaces[w.ID] | ||
| 85 | // A viewport crop is composed: the plane would show the whole | 88 | // A viewport crop is composed: the plane would show the whole | ||
| @@ -94,11 +97,18 @@ func overlayCandidate(s ports.Scene, surfaces map[ports.WindowID]ports.SurfaceCo | |||||
| 94 | continue | 97 | continue | ||
| 95 | } | 98 | } | ||
| 96 | } | 99 | } | ||
| 97 | - | if pick != nil { | 100 | + | if pick != nil && w.Rect.Overlaps(pick.Rect) { |
| 98 | - | // Something is drawn above the candidate. | 101 | + | // Something is drawn over the candidate. |
| 99 | return ports.SceneWindow{}, ports.SurfaceContent{}, colorBypass, "window_above" | 102 | return ports.SceneWindow{}, ports.SurfaceContent{}, colorBypass, "window_above" | ||
| 100 | } | 103 | } | ||
| 101 | } | 104 | } | ||
| 105 | + | if pick != nil { | |||
| 106 | + | for i := range s.Windows { | |||
| 107 | + | if w := &s.Windows[i]; w.Popup && !w.Hidden && w.Rect.Overlaps(pick.Rect) { | |||
| 108 | + | return ports.SceneWindow{}, ports.SurfaceContent{}, colorBypass, "window_above" | |||
| 109 | + | } | |||
| 110 | + | } | |||
| 111 | + | } | |||
| 102 | if pick == nil { | 112 | if pick == nil { | ||
| 103 | if kept != nil && len(s.Windows) > 0 { | 113 | if kept != nil && len(s.Windows) > 0 { | ||
| 104 | // The one window that could have gone on the plane fades out | 114 | // The one window that could have gone on the plane fades out | ||
| @@ -121,13 +131,22 @@ func overlayCandidate(s ports.Scene, surfaces map[ports.WindowID]ports.SurfaceCo | |||||
| 121 | return ports.SceneWindow{}, ports.SurfaceContent{}, colorBypass, "drop_hint" | 131 | return ports.SceneWindow{}, ports.SurfaceContent{}, colorBypass, "drop_hint" | ||
| 122 | } | 132 | } | ||
| 123 | for _, l := range s.Layers { | 133 | for _, l := range s.Layers { | ||
| 124 | - | if l.Layer >= ports.LayerTop && l.Rect.W > 0 && l.Rect.H > 0 { | 134 | + | if l.Layer >= ports.LayerTop && l.Rect.Overlaps(pick.Rect) { |
| 125 | return ports.SceneWindow{}, ports.SurfaceContent{}, colorBypass, "layer_above" | 135 | return ports.SceneWindow{}, ports.SurfaceContent{}, colorBypass, "layer_above" | ||
| 126 | } | 136 | } | ||
| 127 | } | 137 | } | ||
| 128 | - | if !pick.Fullscreen && len(s.Separators) > 0 { | 138 | + | if !pick.Fullscreen { |
| 129 | - | // The overlay shows the buffer alone; the lines would go with it. | 139 | + | if pick.Inset != 0 && s.Border.Width > 0 { |
| 130 | - | return ports.SceneWindow{}, ports.SurfaceContent{}, colorBypass, "border" | 140 | + | // The content sits inside the lines, not at the window's rect. |
| 141 | + | return ports.SceneWindow{}, ports.SurfaceContent{}, colorBypass, "border" | |||
| 142 | + | } | |||
| 143 | + | for _, sep := range s.Separators { | |||
| 144 | + | if sep.Rect.Overlaps(pick.Rect) { | |||
| 145 | + | // The overlay shows the buffer alone; the lines would go | |||
| 146 | + | // under it. | |||
| 147 | + | return ports.SceneWindow{}, ports.SurfaceContent{}, colorBypass, "border" | |||
| 148 | + | } | |||
| 149 | + | } | |||
| 131 | } | 150 | } | ||
| 132 | if clip := s.WorkspaceClip; clip != (ports.Rect{}) && | 151 | if clip := s.WorkspaceClip; clip != (ports.Rect{}) && | ||
| 133 | (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) { | 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) { | ||
M internal/adapters/drm/overlay_test.go +63 -4
| @@ -87,7 +87,7 @@ func TestOverlayCandidate(t *testing.T) { | |||
| 87 | 87 | t.Fatalf("transformed overlay reason %q", reason) | |
| 88 | 88 | } | |
| 89 | 89 | _, c = overlayScene() | |
| 90 | - | s.Windows = append(s.Windows, ports.SceneWindow{ID: 3, Rect: ports.Rect{W: 10, H: 10}}) | |
| 90 | + | s.Windows = append(s.Windows, ports.SceneWindow{ID: 3, Rect: ports.Rect{X: 150, W: 10, H: 10}}) | |
| 91 | 91 | if _, _, _, reason := overlayCandidate(s, c, false, nil, nil); reason != "window_above" { | |
| 92 | 92 | t.Fatalf("reason %q", reason) | |
| 93 | 93 | } | |
| @@ -97,7 +97,7 @@ func TestOverlayCandidate(t *testing.T) { | |||
| 97 | 97 | t.Fatalf("reason %q", reason) | |
| 98 | 98 | } | |
| 99 | 99 | s, _ = overlayScene() | |
| 100 | - | s.Layers = []ports.SceneLayer{{ID: 5, Layer: ports.LayerOverlay, Rect: ports.Rect{W: 5, H: 5}}} | |
| 100 | + | s.Layers = []ports.SceneLayer{{ID: 5, Layer: ports.LayerOverlay, Rect: ports.Rect{X: 150, W: 5, H: 5}}} | |
| 101 | 101 | if _, _, _, reason := overlayCandidate(s, c, false, nil, nil); reason != "layer_above" { | |
| 102 | 102 | t.Fatalf("reason %q", reason) | |
| 103 | 103 | } | |
| @@ -223,15 +223,74 @@ func TestOverlayTestRefusedFallsBack(t *testing.T) { | |||
| 223 | 223 | // overlay; a bordered window does too. | |
| 224 | 224 | func TestOverlayPopupAndBorder(t *testing.T) { | |
| 225 | 225 | s, c := overlayScene() | |
| 226 | - | s.Windows = append([]ports.SceneWindow{{ID: 7, Popup: true, Rect: ports.Rect{W: 5, H: 5}}}, s.Windows...) | |
| 226 | + | s.Windows = append([]ports.SceneWindow{{ID: 7, Popup: true, Rect: ports.Rect{X: 120, W: 5, H: 5}}}, s.Windows...) | |
| 227 | 227 | if _, _, _, reason := overlayCandidate(s, c, false, nil, nil); reason != "window_above" { | |
| 228 | 228 | t.Fatalf("popup: %q", reason) | |
| 229 | 229 | } | |
| 230 | 230 | s, _ = overlayScene() | |
| 231 | - | s.Separators = []ports.Separator{{Rect: ports.Rect{W: 2, H: 2}}} | |
| 231 | + | s.Separators = []ports.Separator{{Rect: ports.Rect{X: 100, W: 2, H: 100}}} | |
| 232 | 232 | if _, _, _, reason := overlayCandidate(s, c, false, nil, nil); reason != "border" { | |
| 233 | 233 | t.Fatalf("border: %q", reason) | |
| 234 | 234 | } | |
| 235 | + | s, _ = overlayScene() | |
| 236 | + | s.Border.Width = 2 | |
| 237 | + | s.Windows[1].Inset = ports.SideLeft | |
| 238 | + | if _, _, _, reason := overlayCandidate(s, c, false, nil, nil); reason != "border" { | |
| 239 | + | t.Fatalf("inset content: %q", reason) | |
| 240 | + | } | |
| 241 | + | } | |
| 242 | + | ||
| 243 | + | // What is drawn elsewhere on screen stays composed on the primary plane: a | |
| 244 | + | // bar, a popup, another window's lines or a window that do not touch the | |
| 245 | + | // candidate leave it on the overlay. | |
| 246 | + | func TestOverlayIgnoresWhatDoesNotCoverIt(t *testing.T) { | |
| 247 | + | s, c := overlayScene() | |
| 248 | + | s.Layers = []ports.SceneLayer{{ID: 5, Layer: ports.LayerTop, Rect: ports.Rect{W: 100, H: 10}}} | |
| 249 | + | s.Windows = append(s.Windows, | |
| 250 | + | ports.SceneWindow{ID: 7, Popup: true, Rect: ports.Rect{X: 10, Y: 10, W: 20, H: 20}}, | |
| 251 | + | ports.SceneWindow{ID: 8, Floating: true, Rect: ports.Rect{X: 40, Y: 40, W: 20, H: 20}}) | |
| 252 | + | s.Separators = []ports.Separator{{Rect: ports.Rect{X: 98, W: 2, H: 100}, Window: 1}} | |
| 253 | + | if w, _, _, reason := overlayCandidate(s, c, false, nil, nil); reason != "" || w.ID != 2 { | |
| 254 | + | t.Fatalf("candidate %v %q", w.ID, reason) | |
| 255 | + | } | |
| 256 | + | // Touching edges do not cover it. | |
| 257 | + | s.Layers[0].Rect = ports.Rect{W: 100, H: 100} | |
| 258 | + | if _, _, _, reason := overlayCandidate(s, c, false, nil, nil); reason != "" { | |
| 259 | + | t.Fatalf("adjacent layer: %q", reason) | |
| 260 | + | } | |
| 261 | + | s.Layers[0].Rect = ports.Rect{W: 101, H: 10} | |
| 262 | + | if _, _, _, reason := overlayCandidate(s, c, false, nil, nil); reason != "layer_above" { | |
| 263 | + | t.Fatalf("overlapping layer: %q", reason) | |
| 264 | + | } | |
| 265 | + | } | |
| 266 | + | ||
| 267 | + | // Draw order decides: a window listed before the candidate is under it, one | |
| 268 | + | // after is over it; a popup is over it wherever it is listed, unless hidden. | |
| 269 | + | func TestOverlayStackingOrder(t *testing.T) { | |
| 270 | + | under := ports.SceneWindow{ID: 8, Floating: true, Below: true, Rect: ports.Rect{X: 120, W: 20, H: 20}} | |
| 271 | + | over := under | |
| 272 | + | over.Below = false | |
| 273 | + | popup := ports.SceneWindow{ID: 7, Popup: true, Rect: ports.Rect{X: 120, W: 20, H: 20}} | |
| 274 | + | for name, tc := range map[string]struct { | |
| 275 | + | edit func(*ports.Scene) | |
| 276 | + | want string | |
| 277 | + | }{ | |
| 278 | + | "window under": {func(s *ports.Scene) { s.Windows = append([]ports.SceneWindow{under}, s.Windows...) }, ""}, | |
| 279 | + | "window over": {func(s *ports.Scene) { s.Windows = append(s.Windows, over) }, "window_above"}, | |
| 280 | + | "popup after": {func(s *ports.Scene) { s.Windows = append(s.Windows, popup) }, "window_above"}, | |
| 281 | + | "hidden popup": {func(s *ports.Scene) { p := popup; p.Hidden = true; s.Windows = append(s.Windows, p) }, ""}, | |
| 282 | + | "popup touching": {func(s *ports.Scene) { | |
| 283 | + | p := popup | |
| 284 | + | p.Rect.X = 80 | |
| 285 | + | s.Windows = append(s.Windows, p) | |
| 286 | + | }, ""}, | |
| 287 | + | } { | |
| 288 | + | s, c := overlayScene() | |
| 289 | + | tc.edit(&s) | |
| 290 | + | if _, _, _, reason := overlayCandidate(s, c, false, nil, nil); reason != tc.want { | |
| 291 | + | t.Errorf("%s: reason %q, want %q", name, reason, tc.want) | |
| 292 | + | } | |
| 293 | + | } | |
| 235 | 294 | } | |
| 236 | 295 | ||
| 237 | 296 | // A cursor commit refused with the overlay on drops the overlay: the | |
| @@ -87,7 +87,7 @@ func TestOverlayCandidate(t *testing.T) { | |||||
| 87 | t.Fatalf("transformed overlay reason %q", reason) | 87 | t.Fatalf("transformed overlay reason %q", reason) | ||
| 88 | } | 88 | } | ||
| 89 | _, c = overlayScene() | 89 | _, c = overlayScene() | ||
| 90 | - | s.Windows = append(s.Windows, ports.SceneWindow{ID: 3, Rect: ports.Rect{W: 10, H: 10}}) | 90 | + | s.Windows = append(s.Windows, ports.SceneWindow{ID: 3, Rect: ports.Rect{X: 150, W: 10, H: 10}}) |
| 91 | if _, _, _, reason := overlayCandidate(s, c, false, nil, nil); reason != "window_above" { | 91 | if _, _, _, reason := overlayCandidate(s, c, false, nil, nil); reason != "window_above" { | ||
| 92 | t.Fatalf("reason %q", reason) | 92 | t.Fatalf("reason %q", reason) | ||
| 93 | } | 93 | } | ||
| @@ -97,7 +97,7 @@ func TestOverlayCandidate(t *testing.T) { | |||||
| 97 | t.Fatalf("reason %q", reason) | 97 | t.Fatalf("reason %q", reason) | ||
| 98 | } | 98 | } | ||
| 99 | s, _ = overlayScene() | 99 | s, _ = overlayScene() | ||
| 100 | - | s.Layers = []ports.SceneLayer{{ID: 5, Layer: ports.LayerOverlay, Rect: ports.Rect{W: 5, H: 5}}} | 100 | + | s.Layers = []ports.SceneLayer{{ID: 5, Layer: ports.LayerOverlay, Rect: ports.Rect{X: 150, W: 5, H: 5}}} |
| 101 | if _, _, _, reason := overlayCandidate(s, c, false, nil, nil); reason != "layer_above" { | 101 | if _, _, _, reason := overlayCandidate(s, c, false, nil, nil); reason != "layer_above" { | ||
| 102 | t.Fatalf("reason %q", reason) | 102 | t.Fatalf("reason %q", reason) | ||
| 103 | } | 103 | } | ||
| @@ -223,15 +223,74 @@ func TestOverlayTestRefusedFallsBack(t *testing.T) { | |||||
| 223 | // overlay; a bordered window does too. | 223 | // overlay; a bordered window does too. | ||
| 224 | func TestOverlayPopupAndBorder(t *testing.T) { | 224 | func TestOverlayPopupAndBorder(t *testing.T) { | ||
| 225 | s, c := overlayScene() | 225 | s, c := overlayScene() | ||
| 226 | - | s.Windows = append([]ports.SceneWindow{{ID: 7, Popup: true, Rect: ports.Rect{W: 5, H: 5}}}, s.Windows...) | 226 | + | s.Windows = append([]ports.SceneWindow{{ID: 7, Popup: true, Rect: ports.Rect{X: 120, W: 5, H: 5}}}, s.Windows...) |
| 227 | if _, _, _, reason := overlayCandidate(s, c, false, nil, nil); reason != "window_above" { | 227 | if _, _, _, reason := overlayCandidate(s, c, false, nil, nil); reason != "window_above" { | ||
| 228 | t.Fatalf("popup: %q", reason) | 228 | t.Fatalf("popup: %q", reason) | ||
| 229 | } | 229 | } | ||
| 230 | s, _ = overlayScene() | 230 | s, _ = overlayScene() | ||
| 231 | - | s.Separators = []ports.Separator{{Rect: ports.Rect{W: 2, H: 2}}} | 231 | + | s.Separators = []ports.Separator{{Rect: ports.Rect{X: 100, W: 2, H: 100}}} |
| 232 | if _, _, _, reason := overlayCandidate(s, c, false, nil, nil); reason != "border" { | 232 | if _, _, _, reason := overlayCandidate(s, c, false, nil, nil); reason != "border" { | ||
| 233 | t.Fatalf("border: %q", reason) | 233 | t.Fatalf("border: %q", reason) | ||
| 234 | } | 234 | } | ||
| 235 | + | s, _ = overlayScene() | |||
| 236 | + | s.Border.Width = 2 | |||
| 237 | + | s.Windows[1].Inset = ports.SideLeft | |||
| 238 | + | if _, _, _, reason := overlayCandidate(s, c, false, nil, nil); reason != "border" { | |||
| 239 | + | t.Fatalf("inset content: %q", reason) | |||
| 240 | + | } | |||
| 241 | + | } | |||
| 242 | + | ||||
| 243 | + | // What is drawn elsewhere on screen stays composed on the primary plane: a | |||
| 244 | + | // bar, a popup, another window's lines or a window that do not touch the | |||
| 245 | + | // candidate leave it on the overlay. | |||
| 246 | + | func TestOverlayIgnoresWhatDoesNotCoverIt(t *testing.T) { | |||
| 247 | + | s, c := overlayScene() | |||
| 248 | + | s.Layers = []ports.SceneLayer{{ID: 5, Layer: ports.LayerTop, Rect: ports.Rect{W: 100, H: 10}}} | |||
| 249 | + | s.Windows = append(s.Windows, | |||
| 250 | + | ports.SceneWindow{ID: 7, Popup: true, Rect: ports.Rect{X: 10, Y: 10, W: 20, H: 20}}, | |||
| 251 | + | ports.SceneWindow{ID: 8, Floating: true, Rect: ports.Rect{X: 40, Y: 40, W: 20, H: 20}}) | |||
| 252 | + | s.Separators = []ports.Separator{{Rect: ports.Rect{X: 98, W: 2, H: 100}, Window: 1}} | |||
| 253 | + | if w, _, _, reason := overlayCandidate(s, c, false, nil, nil); reason != "" || w.ID != 2 { | |||
| 254 | + | t.Fatalf("candidate %v %q", w.ID, reason) | |||
| 255 | + | } | |||
| 256 | + | // Touching edges do not cover it. | |||
| 257 | + | s.Layers[0].Rect = ports.Rect{W: 100, H: 100} | |||
| 258 | + | if _, _, _, reason := overlayCandidate(s, c, false, nil, nil); reason != "" { | |||
| 259 | + | t.Fatalf("adjacent layer: %q", reason) | |||
| 260 | + | } | |||
| 261 | + | s.Layers[0].Rect = ports.Rect{W: 101, H: 10} | |||
| 262 | + | if _, _, _, reason := overlayCandidate(s, c, false, nil, nil); reason != "layer_above" { | |||
| 263 | + | t.Fatalf("overlapping layer: %q", reason) | |||
| 264 | + | } | |||
| 265 | + | } | |||
| 266 | + | ||||
| 267 | + | // Draw order decides: a window listed before the candidate is under it, one | |||
| 268 | + | // after is over it; a popup is over it wherever it is listed, unless hidden. | |||
| 269 | + | func TestOverlayStackingOrder(t *testing.T) { | |||
| 270 | + | under := ports.SceneWindow{ID: 8, Floating: true, Below: true, Rect: ports.Rect{X: 120, W: 20, H: 20}} | |||
| 271 | + | over := under | |||
| 272 | + | over.Below = false | |||
| 273 | + | popup := ports.SceneWindow{ID: 7, Popup: true, Rect: ports.Rect{X: 120, W: 20, H: 20}} | |||
| 274 | + | for name, tc := range map[string]struct { | |||
| 275 | + | edit func(*ports.Scene) | |||
| 276 | + | want string | |||
| 277 | + | }{ | |||
| 278 | + | "window under": {func(s *ports.Scene) { s.Windows = append([]ports.SceneWindow{under}, s.Windows...) }, ""}, | |||
| 279 | + | "window over": {func(s *ports.Scene) { s.Windows = append(s.Windows, over) }, "window_above"}, | |||
| 280 | + | "popup after": {func(s *ports.Scene) { s.Windows = append(s.Windows, popup) }, "window_above"}, | |||
| 281 | + | "hidden popup": {func(s *ports.Scene) { p := popup; p.Hidden = true; s.Windows = append(s.Windows, p) }, ""}, | |||
| 282 | + | "popup touching": {func(s *ports.Scene) { | |||
| 283 | + | p := popup | |||
| 284 | + | p.Rect.X = 80 | |||
| 285 | + | s.Windows = append(s.Windows, p) | |||
| 286 | + | }, ""}, | |||
| 287 | + | } { | |||
| 288 | + | s, c := overlayScene() | |||
| 289 | + | tc.edit(&s) | |||
| 290 | + | if _, _, _, reason := overlayCandidate(s, c, false, nil, nil); reason != tc.want { | |||
| 291 | + | t.Errorf("%s: reason %q, want %q", name, reason, tc.want) | |||
| 292 | + | } | |||
| 293 | + | } | |||
| 235 | } | 294 | } | ||
| 236 | 295 | ||||
| 237 | // A cursor commit refused with the overlay on drops the overlay: the | 296 | // A cursor commit refused with the overlay on drops the overlay: the | ||
M internal/adapters/wayland/damage_test.go +68 -0
| @@ -106,4 +106,72 @@ func TestDamageHistory(t *testing.T) { | |||
| 106 | 106 | if _, ok := third.DamageSince(second.Seq); ok { | |
| 107 | 107 | t.Fatal("undamaged new buffer taken as unchanged") | |
| 108 | 108 | } | |
| 109 | + | // A subsurface with no buffer draws nothing: once its layout is | |
| 110 | + | // applied (one full change), the root keeps its damage. | |
| 111 | + | subc := bindProtocol(t, c, "wl_subcompositor") | |
| 112 | + | child, sub := c.AllocateID(), c.AllocateID() | |
| 113 | + | requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, child) | |
| 114 | + | registerProtocol(t, c, child) | |
| 115 | + | registerProtocol(t, c, sub) | |
| 116 | + | requestProtocol(t, c, subc, wayland.SubcompositorRequestGetSubsurface, sub, child, surf) | |
| 117 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, bufs[1], int32(0), int32(0)) | |
| 118 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |
| 119 | + | third = next() | |
| 120 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, bufs[0], int32(0), int32(0)) | |
| 121 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestDamageBuffer, int32(1), int32(1), int32(2), int32(2)) | |
| 122 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |
| 123 | + | fourth := next() | |
| 124 | + | if got, ok := fourth.DamageSince(third.Seq); !ok || len(got) != 1 || got[0] != (ports.Rect{X: 1, Y: 1, W: 2, H: 2}) { | |
| 125 | + | t.Fatalf("empty subsurface: damage %v %v", got, ok) | |
| 126 | + | } | |
| 127 | + | // A synced child drawn, then hidden by a null attach the parent's | |
| 128 | + | // commit applies: its old area must be redrawn, both times. | |
| 129 | + | rootCommit := func(what string, wantChildren int) { | |
| 130 | + | t.Helper() | |
| 131 | + | prev := fourth | |
| 132 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |
| 133 | + | fourth = next() | |
| 134 | + | if len(fourth.Children) != wantChildren { | |
| 135 | + | t.Fatalf("%s: %d children, want %d", what, len(fourth.Children), wantChildren) | |
| 136 | + | } | |
| 137 | + | if got, ok := fourth.DamageSince(prev.Seq); ok { | |
| 138 | + | t.Fatalf("%s kept partial damage %v", what, got) | |
| 139 | + | } | |
| 140 | + | } | |
| 141 | + | requestProtocol(t, c, child, wayland.SurfaceRequestAttach, bufs[1], int32(0), int32(0)) | |
| 142 | + | requestProtocol(t, c, child, wayland.SurfaceRequestCommit) | |
| 143 | + | rootCommit("child appearing", 1) | |
| 144 | + | requestProtocol(t, c, child, wayland.SurfaceRequestAttach, uint32(0), int32(0), int32(0)) | |
| 145 | + | requestProtocol(t, c, child, wayland.SurfaceRequestCommit) | |
| 146 | + | rootCommit("child hidden", 0) | |
| 147 | + | // Destroying a child that draws nothing changes nothing on screen. | |
| 148 | + | prev := fourth | |
| 149 | + | requestProtocol(t, c, sub, wayland.SubsurfaceRequestDestroy) | |
| 150 | + | fourth = next() | |
| 151 | + | if got, ok := fourth.DamageSince(prev.Seq); !ok || len(got) != 0 { | |
| 152 | + | t.Fatalf("empty child destroyed: damage %v %v", got, ok) | |
| 153 | + | } | |
| 154 | + | prev = fourth | |
| 155 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, bufs[0], int32(0), int32(0)) | |
| 156 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestDamageBuffer, int32(1), int32(1), int32(2), int32(2)) | |
| 157 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |
| 158 | + | fourth = next() | |
| 159 | + | if got, ok := fourth.DamageSince(prev.Seq); !ok || len(got) != 1 { | |
| 160 | + | t.Fatalf("after empty child: damage %v %v", got, ok) | |
| 161 | + | } | |
| 162 | + | // Destroying a drawn child redraws its area. | |
| 163 | + | child2, sub2 := c.AllocateID(), c.AllocateID() | |
| 164 | + | requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, child2) | |
| 165 | + | registerProtocol(t, c, child2) | |
| 166 | + | registerProtocol(t, c, sub2) | |
| 167 | + | requestProtocol(t, c, subc, wayland.SubcompositorRequestGetSubsurface, sub2, child2, surf) | |
| 168 | + | requestProtocol(t, c, child2, wayland.SurfaceRequestAttach, bufs[1], int32(0), int32(0)) | |
| 169 | + | requestProtocol(t, c, child2, wayland.SurfaceRequestCommit) | |
| 170 | + | rootCommit("second child appearing", 1) | |
| 171 | + | prev = fourth | |
| 172 | + | requestProtocol(t, c, sub2, wayland.SubsurfaceRequestDestroy) | |
| 173 | + | fourth = next() | |
| 174 | + | if got, ok := fourth.DamageSince(prev.Seq); ok || len(fourth.Children) != 0 { | |
| 175 | + | t.Fatalf("drawn child destroyed: damage %v %v, %d children", got, ok, len(fourth.Children)) | |
| 176 | + | } | |
| 109 | 177 | } | |
| @@ -106,4 +106,72 @@ func TestDamageHistory(t *testing.T) { | |||||
| 106 | if _, ok := third.DamageSince(second.Seq); ok { | 106 | if _, ok := third.DamageSince(second.Seq); ok { | ||
| 107 | t.Fatal("undamaged new buffer taken as unchanged") | 107 | t.Fatal("undamaged new buffer taken as unchanged") | ||
| 108 | } | 108 | } | ||
| 109 | + | // A subsurface with no buffer draws nothing: once its layout is | |||
| 110 | + | // applied (one full change), the root keeps its damage. | |||
| 111 | + | subc := bindProtocol(t, c, "wl_subcompositor") | |||
| 112 | + | child, sub := c.AllocateID(), c.AllocateID() | |||
| 113 | + | requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, child) | |||
| 114 | + | registerProtocol(t, c, child) | |||
| 115 | + | registerProtocol(t, c, sub) | |||
| 116 | + | requestProtocol(t, c, subc, wayland.SubcompositorRequestGetSubsurface, sub, child, surf) | |||
| 117 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, bufs[1], int32(0), int32(0)) | |||
| 118 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |||
| 119 | + | third = next() | |||
| 120 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, bufs[0], int32(0), int32(0)) | |||
| 121 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestDamageBuffer, int32(1), int32(1), int32(2), int32(2)) | |||
| 122 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |||
| 123 | + | fourth := next() | |||
| 124 | + | if got, ok := fourth.DamageSince(third.Seq); !ok || len(got) != 1 || got[0] != (ports.Rect{X: 1, Y: 1, W: 2, H: 2}) { | |||
| 125 | + | t.Fatalf("empty subsurface: damage %v %v", got, ok) | |||
| 126 | + | } | |||
| 127 | + | // A synced child drawn, then hidden by a null attach the parent's | |||
| 128 | + | // commit applies: its old area must be redrawn, both times. | |||
| 129 | + | rootCommit := func(what string, wantChildren int) { | |||
| 130 | + | t.Helper() | |||
| 131 | + | prev := fourth | |||
| 132 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |||
| 133 | + | fourth = next() | |||
| 134 | + | if len(fourth.Children) != wantChildren { | |||
| 135 | + | t.Fatalf("%s: %d children, want %d", what, len(fourth.Children), wantChildren) | |||
| 136 | + | } | |||
| 137 | + | if got, ok := fourth.DamageSince(prev.Seq); ok { | |||
| 138 | + | t.Fatalf("%s kept partial damage %v", what, got) | |||
| 139 | + | } | |||
| 140 | + | } | |||
| 141 | + | requestProtocol(t, c, child, wayland.SurfaceRequestAttach, bufs[1], int32(0), int32(0)) | |||
| 142 | + | requestProtocol(t, c, child, wayland.SurfaceRequestCommit) | |||
| 143 | + | rootCommit("child appearing", 1) | |||
| 144 | + | requestProtocol(t, c, child, wayland.SurfaceRequestAttach, uint32(0), int32(0), int32(0)) | |||
| 145 | + | requestProtocol(t, c, child, wayland.SurfaceRequestCommit) | |||
| 146 | + | rootCommit("child hidden", 0) | |||
| 147 | + | // Destroying a child that draws nothing changes nothing on screen. | |||
| 148 | + | prev := fourth | |||
| 149 | + | requestProtocol(t, c, sub, wayland.SubsurfaceRequestDestroy) | |||
| 150 | + | fourth = next() | |||
| 151 | + | if got, ok := fourth.DamageSince(prev.Seq); !ok || len(got) != 0 { | |||
| 152 | + | t.Fatalf("empty child destroyed: damage %v %v", got, ok) | |||
| 153 | + | } | |||
| 154 | + | prev = fourth | |||
| 155 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, bufs[0], int32(0), int32(0)) | |||
| 156 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestDamageBuffer, int32(1), int32(1), int32(2), int32(2)) | |||
| 157 | + | requestProtocol(t, c, surf, wayland.SurfaceRequestCommit) | |||
| 158 | + | fourth = next() | |||
| 159 | + | if got, ok := fourth.DamageSince(prev.Seq); !ok || len(got) != 1 { | |||
| 160 | + | t.Fatalf("after empty child: damage %v %v", got, ok) | |||
| 161 | + | } | |||
| 162 | + | // Destroying a drawn child redraws its area. | |||
| 163 | + | child2, sub2 := c.AllocateID(), c.AllocateID() | |||
| 164 | + | requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, child2) | |||
| 165 | + | registerProtocol(t, c, child2) | |||
| 166 | + | registerProtocol(t, c, sub2) | |||
| 167 | + | requestProtocol(t, c, subc, wayland.SubcompositorRequestGetSubsurface, sub2, child2, surf) | |||
| 168 | + | requestProtocol(t, c, child2, wayland.SurfaceRequestAttach, bufs[1], int32(0), int32(0)) | |||
| 169 | + | requestProtocol(t, c, child2, wayland.SurfaceRequestCommit) | |||
| 170 | + | rootCommit("second child appearing", 1) | |||
| 171 | + | prev = fourth | |||
| 172 | + | requestProtocol(t, c, sub2, wayland.SubsurfaceRequestDestroy) | |||
| 173 | + | fourth = next() | |||
| 174 | + | if got, ok := fourth.DamageSince(prev.Seq); ok || len(fourth.Children) != 0 { | |||
| 175 | + | t.Fatalf("drawn child destroyed: damage %v %v, %d children", got, ok, len(fourth.Children)) | |||
| 176 | + | } | |||
| 109 | } | 177 | } | ||
M internal/adapters/wayland/surface.go +10 -3
| @@ -98,6 +98,9 @@ type surface struct { | |||
| 98 | 98 | // committed is what the last commit changed, in buffer pixels (full: | |
| 99 | 99 | // everything), read by the window's damage history. | |
| 100 | 100 | committed damage | |
| 101 | + | // drewChildren is whether the root's last published tree drew a | |
| 102 | + | // subsurface: one that stops drawing leaves its area to redraw. | |
| 103 | + | drewChildren bool | |
| 101 | 104 | // Explicit sync (syncobj.go): the surface's syncobj object and the | |
| 102 | 105 | // current buffer's hold. | |
| 103 | 106 | sync *syncState | |
| @@ -252,15 +255,19 @@ func (s *surface) appendTree(out *[]ports.Subsurface, x, y int, below bool) { | |||
| 252 | 255 | } | |
| 253 | 256 | ||
| 254 | 257 | // redraw sends the tree of a mapped root to the outputs. Only a commit | |
| 255 | - | // of the root's own buffer with no other change keeps partial damage. | |
| 258 | + | // of the root's own buffer, with no subsurface drawn before or now, keeps | |
| 259 | + | // partial damage: subsurfaces without a buffer (clients often keep one | |
| 260 | + | // for later) are not drawn, so they do not make it full. | |
| 256 | 261 | func (s *surface) redraw() { | |
| 257 | 262 | r := s.root() | |
| 258 | 263 | if id := r.windowID(); id != 0 && s.server.channels.Contents != nil { | |
| 264 | + | c := r.tree(id) | |
| 259 | 265 | d := damage{full: true} | |
| 260 | - | if s == r && len(r.sub.children) == 0 { | |
| 266 | + | if s == r && len(c.Children) == 0 && !r.drewChildren { | |
| 261 | 267 | d = r.committed | |
| 262 | 268 | } | |
| 263 | - | s.server.emitContent(r.tree(id), d) | |
| 269 | + | r.drewChildren = len(c.Children) > 0 | |
| 270 | + | s.server.emitContent(c, d) | |
| 264 | 271 | } | |
| 265 | 272 | } | |
| 266 | 273 | ||
| @@ -98,6 +98,9 @@ type surface struct { | |||||
| 98 | // committed is what the last commit changed, in buffer pixels (full: | 98 | // committed is what the last commit changed, in buffer pixels (full: | ||
| 99 | // everything), read by the window's damage history. | 99 | // everything), read by the window's damage history. | ||
| 100 | committed damage | 100 | committed damage | ||
| 101 | + | // drewChildren is whether the root's last published tree drew a | |||
| 102 | + | // subsurface: one that stops drawing leaves its area to redraw. | |||
| 103 | + | drewChildren bool | |||
| 101 | // Explicit sync (syncobj.go): the surface's syncobj object and the | 104 | // Explicit sync (syncobj.go): the surface's syncobj object and the | ||
| 102 | // current buffer's hold. | 105 | // current buffer's hold. | ||
| 103 | sync *syncState | 106 | sync *syncState | ||
| @@ -252,15 +255,19 @@ func (s *surface) appendTree(out *[]ports.Subsurface, x, y int, below bool) { | |||||
| 252 | } | 255 | } | ||
| 253 | 256 | ||||
| 254 | // redraw sends the tree of a mapped root to the outputs. Only a commit | 257 | // redraw sends the tree of a mapped root to the outputs. Only a commit | ||
| 255 | - | // of the root's own buffer with no other change keeps partial damage. | 258 | + | // of the root's own buffer, with no subsurface drawn before or now, keeps |
| 259 | + | // partial damage: subsurfaces without a buffer (clients often keep one | |||
| 260 | + | // for later) are not drawn, so they do not make it full. | |||
| 256 | func (s *surface) redraw() { | 261 | func (s *surface) redraw() { | ||
| 257 | r := s.root() | 262 | r := s.root() | ||
| 258 | if id := r.windowID(); id != 0 && s.server.channels.Contents != nil { | 263 | if id := r.windowID(); id != 0 && s.server.channels.Contents != nil { | ||
| 264 | + | c := r.tree(id) | |||
| 259 | d := damage{full: true} | 265 | d := damage{full: true} | ||
| 260 | - | if s == r && len(r.sub.children) == 0 { | 266 | + | if s == r && len(c.Children) == 0 && !r.drewChildren { |
| 261 | d = r.committed | 267 | d = r.committed | ||
| 262 | } | 268 | } | ||
| 263 | - | s.server.emitContent(r.tree(id), d) | 269 | + | r.drewChildren = len(c.Children) > 0 |
| 270 | + | s.server.emitContent(c, d) | |||
| 264 | } | 271 | } | ||
| 265 | } | 272 | } | ||
| 266 | 273 | ||||