detached 776b1d8

Merge branch 'main' into docs/deb-install

commit
776b1d810c98048e5f80489de555920514b9d9ea
author
bnema · · 2026-10-08 05:16 UTC
merge of
49b88fb + 5fcaae0 · diff against the first parent
signature
unsigned
4 files · +170 -17
M internal/adapters/drm/overlay.go +29 -10
@@ -57,8 +57,10 @@ type overlayWin struct {
5757 }
5858
5959 // 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.
6264 //
6365 // kept is scanoutCandidate's: a window drawn from a withdrawn content is
6466 // composed, never put on the plane.
@@ -78,8 +80,9 @@ func overlayCandidate(s ports.Scene, surfaces map[ports.WindowID]ports.SurfaceCo
7880 continue
7981 }
8082 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
8386 }
8487 c := surfaces[w.ID]
8588 // 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
9497 continue
9598 }
9699 }
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.
99102 return ports.SceneWindow{}, ports.SurfaceContent{}, colorBypass, "window_above"
100103 }
101104 }
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+ }
102112 if pick == nil {
103113 if kept != nil && len(s.Windows) > 0 {
104114 // 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
121131 return ports.SceneWindow{}, ports.SurfaceContent{}, colorBypass, "drop_hint"
122132 }
123133 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) {
125135 return ports.SceneWindow{}, ports.SurfaceContent{}, colorBypass, "layer_above"
126136 }
127137 }
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+ }
131150 }
132151 if clip := s.WorkspaceClip; clip != (ports.Rect{}) &&
133152 (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 opaque59 // overlayCandidate finds the one window the overlay can show: an opaque
60-// dmabuf at integer physical coordinates, drawn 1:1, with no other window60+// 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 is65 // 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 continue80 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 whole88 // 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 continue97 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 out114 // 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) {
8787 t.Fatalf("transformed overlay reason %q", reason)
8888 }
8989 _, 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}})
9191 if _, _, _, reason := overlayCandidate(s, c, false, nil, nil); reason != "window_above" {
9292 t.Fatalf("reason %q", reason)
9393 }
@@ -97,7 +97,7 @@ func TestOverlayCandidate(t *testing.T) {
9797 t.Fatalf("reason %q", reason)
9898 }
9999 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}}}
101101 if _, _, _, reason := overlayCandidate(s, c, false, nil, nil); reason != "layer_above" {
102102 t.Fatalf("reason %q", reason)
103103 }
@@ -223,15 +223,74 @@ func TestOverlayTestRefusedFallsBack(t *testing.T) {
223223 // overlay; a bordered window does too.
224224 func TestOverlayPopupAndBorder(t *testing.T) {
225225 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...)
227227 if _, _, _, reason := overlayCandidate(s, c, false, nil, nil); reason != "window_above" {
228228 t.Fatalf("popup: %q", reason)
229229 }
230230 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}}}
232232 if _, _, _, reason := overlayCandidate(s, c, false, nil, nil); reason != "border" {
233233 t.Fatalf("border: %q", reason)
234234 }
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+ }
235294 }
236295
237296 // 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: the296 // 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) {
106106 if _, ok := third.DamageSince(second.Seq); ok {
107107 t.Fatal("undamaged new buffer taken as unchanged")
108108 }
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+ }
109177 }
@@ -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 {
9898 // committed is what the last commit changed, in buffer pixels (full:
9999 // everything), read by the window's damage history.
100100 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
101104 // Explicit sync (syncobj.go): the surface's syncobj object and the
102105 // current buffer's hold.
103106 sync *syncState
@@ -252,15 +255,19 @@ func (s *surface) appendTree(out *[]ports.Subsurface, x, y int, below bool) {
252255 }
253256
254257 // 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.
256261 func (s *surface) redraw() {
257262 r := s.root()
258263 if id := r.windowID(); id != 0 && s.server.channels.Contents != nil {
264+ c := r.tree(id)
259265 d := damage{full: true}
260- if s == r && len(r.sub.children) == 0 {
266+ if s == r && len(c.Children) == 0 && !r.drewChildren {
261267 d = r.committed
262268 }
263- s.server.emitContent(r.tree(id), d)
269+ r.drewChildren = len(c.Children) > 0
270+ s.server.emitContent(c, d)
264271 }
265272 }
266273
@@ -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 damage100 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 the104 // Explicit sync (syncobj.go): the surface's syncobj object and the
102 // current buffer's hold.105 // current buffer's hold.
103 sync *syncState106 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 commit257 // 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.committed267 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

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 23

Tags 10

↑↓ moveenter openesc close