detached 42b53c5

refactor(drm): decide drawn windows from Scene.Draws

The overlay, scanout and VRR checks use Scene.Draws instead of their
own Hidden and size tests, so a tile the scene does not draw neither
takes nor blocks a plane. TileInset's doc covers size overrides. Tests
cover a sized workspace's tile area, scanout past an undrawn tile and
the cascade scroll back under the panel.
commit
42b53c5b18cc01af71341b1f324d6018be8664c2
author
brice · · 2026-10-08 14:59 UTC
parent
5077157
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
8 files · +52 -18
M internal/adapters/drm/overlay.go +4 -7
@@ -76,12 +76,9 @@ func overlayCandidate(s ports.Scene, surfaces map[ports.WindowID]ports.SurfaceCo
7676 var mode colorMode
7777 for i := range s.Windows {
7878 w := &s.Windows[i]
79- if w.Hidden || w.Rect.W <= 0 || w.Rect.H <= 0 {
80- continue
81- }
82- if a, ok := s.TileArea(); ok && w.Tile() && !w.Rect.Overlaps(a) {
83- // A tile wholly under a panel is not drawn: neither a
84- // candidate nor over one.
79+ if !s.Draws(*w) {
80+ // Not drawn (a tile wholly under a panel included): neither
81+ // a candidate nor over one.
8582 continue
8683 }
8784 if w.Popup {
@@ -109,7 +106,7 @@ func overlayCandidate(s ports.Scene, surfaces map[ports.WindowID]ports.SurfaceCo
109106 }
110107 if pick != nil {
111108 for i := range s.Windows {
112- 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) {
113110 return ports.SceneWindow{}, ports.SurfaceContent{}, colorBypass, "window_above"
114111 }
115112 }
@@ -76,12 +76,9 @@ func overlayCandidate(s ports.Scene, surfaces map[ports.WindowID]ports.SurfaceCo
76 var mode colorMode76 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- continue80+ // Not drawn (a tile wholly under a panel included): neither
81- }81+ // a candidate nor over one.
82- if a, ok := s.TileArea(); ok && w.Tile() && !w.Rect.Overlaps(a) {
83- // A tile wholly under a panel is not drawn: neither a
84- // candidate nor over one.
85 continue82 continue
86 }83 }
87 if w.Popup {84 if w.Popup {
@@ -109,7 +106,7 @@ func overlayCandidate(s ports.Scene, surfaces map[ports.WindowID]ports.SurfaceCo
109 }106 }
110 if pick != nil {107 if pick != nil {
111 for i := range s.Windows {108 for i := range s.Windows {
112- 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) {
113 return ports.SceneWindow{}, ports.SurfaceContent{}, colorBypass, "window_above"110 return ports.SceneWindow{}, ports.SurfaceContent{}, colorBypass, "window_above"
114 }111 }
115 }112 }
M internal/adapters/drm/overlay_test.go +11 -0
@@ -83,6 +83,17 @@ func TestOverlayTileInset(t *testing.T) {
8383 }
8484 }
8585
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+
8697 func TestOverlayCandidate(t *testing.T) {
8798 s, c := overlayScene()
8899 if w, _, _, reason := overlayCandidate(s, c, false, nil, nil); reason != "" || w.ID != 2 {
@@ -83,6 +83,17 @@ func TestOverlayTileInset(t *testing.T) {
83 }83 }
84 }84 }
85 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+
86 func TestOverlayCandidate(t *testing.T) {97 func TestOverlayCandidate(t *testing.T) {
87 s, c := overlayScene()98 s, c := overlayScene()
88 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
100100 var full *ports.SceneWindow
101101 for i := range s.Windows {
102102 win := &s.Windows[i]
103- if win.Hidden || win.Rect.W <= 0 || win.Rect.H <= 0 {
103+ if !s.Draws(*win) {
104104 continue
105105 }
106106 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.SceneWindow100 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 continue104 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
6868 var full *ports.SceneWindow
6969 for i := range s.Windows {
7070 w := &s.Windows[i]
71- if w.Hidden || w.Rect.W <= 0 || w.Rect.H <= 0 {
71+ if !s.Draws(*w) {
7272 continue
7373 }
7474 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.SceneWindow68 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 continue72 continue
73 }73 }
74 if !w.Fullscreen || !covers(&s, w) || full != nil {74 if !w.Fullscreen || !covers(&s, w) || full != nil {
M internal/core/core.go +8 -0
@@ -476,6 +476,14 @@ func (v viewport) area(p Placement) Rect {
476476 return v.usable
477477 }
478478
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+
479487 // shows reports whether the user sees p: not hidden, and overlapping its
480488 // area.
481489 func (v viewport) shows(p Placement) bool {
@@ -476,6 +476,14 @@ func (v viewport) area(p Placement) Rect {
476 return v.usable476 return v.usable
477 }477 }
478 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+
479 // shows reports whether the user sees p: not hidden, and overlapping its487 // shows reports whether the user sees p: not hidden, and overlapping its
480 // area.488 // area.
481 func (v viewport) shows(p Placement) bool {489 func (v viewport) shows(p Placement) bool {
M internal/core/scene.go +1 -4
@@ -89,10 +89,7 @@ func (c *Core) sceneFor(ctx context.Context, i int, sc *screen, only *screen, ca
8989 }
9090 continue
9191 }
92- // A window sliding in still waits for its pulse; one with no room
93- // to be drawn (panels leave no usable area) never gets it.
94- room := view.area(p)
95- if focused && p.ID == c.pulse.target && !alone && !p.Fullscreen && !p.Hidden && room.W > 0 && room.H > 0 && p.Preview == 0 && !sc.mon.ov.open {
92+ if focused && p.ID == c.pulse.target && !alone && !p.Fullscreen && !p.Hidden && p.Preview == 0 && !sc.mon.ov.open && hasRoom(view, p) {
9693 drawable = true
9794 if p.ID == c.pulse.id {
9895 sw.FocusEffect = pulse
@@ -89,10 +89,7 @@ func (c *Core) sceneFor(ctx context.Context, i int, sc *screen, only *screen, ca
89 }89 }
90 continue90 continue
91 }91 }
92- // A window sliding in still waits for its pulse; one with no room92+ if focused && p.ID == c.pulse.target && !alone && !p.Fullscreen && !p.Hidden && p.Preview == 0 && !sc.mon.ov.open && hasRoom(view, p) {
93- // to be drawn (panels leave no usable area) never gets it.
94- room := view.area(p)
95- if focused && p.ID == c.pulse.target && !alone && !p.Fullscreen && !p.Hidden && room.W > 0 && room.H > 0 && p.Preview == 0 && !sc.mon.ov.open {
96 drawable = true93 drawable = true
97 if p.ID == c.pulse.id {94 if p.ID == c.pulse.id {
98 sw.FocusEffect = pulse95 sw.FocusEffect = pulse
M internal/core/viewport_test.go +20 -0
@@ -122,6 +122,8 @@ func TestConfigureVisibleCascadeUnderPanel(t *testing.T) {
122122 if c.visible(3) || c.visible(10) || c.pointer != 0 {
123123 t.Fatalf("tile under the panel: visible %v, popup %v, pointer %d", c.visible(3), c.visible(10), c.pointer)
124124 }
125+ delete(c.popups, 10)
126+ c.popupOrder = slices.DeleteFunc(c.popupOrder, func(id WindowID) bool { return id == 10 })
125127 // The overview lays previews over the whole output: no inset.
126128 m.ToggleOverview()
127129 if s := check(nil); s.TileInset != (ports.Insets{}) {
@@ -131,7 +133,11 @@ func TestConfigureVisibleCascadeUnderPanel(t *testing.T) {
131133 // Its band scrolled in, the first band is under nothing but off screen.
132134 w.FocusID(3)
133135 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})
134139 // Fullscreen covers the panel: always seen.
140+ w.FocusID(3)
135141 w.ToggleFullscreen()
136142 check(map[WindowID]bool{3: true})
137143 }
@@ -200,6 +206,20 @@ func TestNoUsableAreaDrawsNoTile(t *testing.T) {
200206 }
201207 }
202208
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+
203223 // A column scrolled under a left panel is not seen.
204224 func TestConfigureVisibleScrollUnderSidePanel(t *testing.T) {
205225 c, check := viewportCore(t, func(cfg *ports.Config) {
@@ -122,6 +122,8 @@ func TestConfigureVisibleCascadeUnderPanel(t *testing.T) {
122 if c.visible(3) || c.visible(10) || c.pointer != 0 {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)123 t.Fatalf("tile under the panel: visible %v, popup %v, pointer %d", c.visible(3), c.visible(10), c.pointer)
124 }124 }
125+ delete(c.popups, 10)
126+ c.popupOrder = slices.DeleteFunc(c.popupOrder, func(id WindowID) bool { return id == 10 })
125 // The overview lays previews over the whole output: no inset.127 // The overview lays previews over the whole output: no inset.
126 m.ToggleOverview()128 m.ToggleOverview()
127 if s := check(nil); s.TileInset != (ports.Insets{}) {129 if s := check(nil); s.TileInset != (ports.Insets{}) {
@@ -131,7 +133,11 @@ func TestConfigureVisibleCascadeUnderPanel(t *testing.T) {
131 // Its band scrolled in, the first band is under nothing but off screen.133 // Its band scrolled in, the first band is under nothing but off screen.
132 w.FocusID(3)134 w.FocusID(3)
133 check(map[WindowID]bool{1: false, 2: false, 3: true})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})
134 // Fullscreen covers the panel: always seen.139 // Fullscreen covers the panel: always seen.
140+ w.FocusID(3)
135 w.ToggleFullscreen()141 w.ToggleFullscreen()
136 check(map[WindowID]bool{3: true})142 check(map[WindowID]bool{3: true})
137 }143 }
@@ -200,6 +206,20 @@ func TestNoUsableAreaDrawsNoTile(t *testing.T) {
200 }206 }
201 }207 }
202 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+
203 // A column scrolled under a left panel is not seen.223 // A column scrolled under a left panel is not seen.
204 func TestConfigureVisibleScrollUnderSidePanel(t *testing.T) {224 func TestConfigureVisibleScrollUnderSidePanel(t *testing.T) {
205 c, check := viewportCore(t, func(cfg *ports.Config) {225 c, check := viewportCore(t, func(cfg *ports.Config) {
M internal/ports/scene.go +6 -5
@@ -27,11 +27,12 @@ type Scene struct {
2727 // in logical output coordinates. Zero inherits the full output. Layers
2828 // remain output-wide; overview previews do not use this clip.
2929 WorkspaceClip Rect
30- // TileInset is what exclusive layers (panels) take from the output's
31- // edges. Tiles (SceneWindow.Tile) and their lines are drawn only in
32- // what is left (TileArea), so a tile is never drawn over a panel,
33- // whatever its layer; insets covering the output leave no tile drawn.
34- // Zero: no panel (and the overview, capture scenes).
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).
3536 TileInset Insets
3637 // Dim darkens the background, bottom layers and tiles (tile lines
3738 // included) inside WorkspaceClip when set, with black at this opacity,
@@ -27,11 +27,12 @@ type Scene struct {
27 // in logical output coordinates. Zero inherits the full output. Layers27 // in logical output coordinates. Zero inherits the full output. Layers
28 // remain output-wide; overview previews do not use this clip.28 // remain output-wide; overview previews do not use this clip.
29 WorkspaceClip Rect29 WorkspaceClip Rect
30- // TileInset is what exclusive layers (panels) take from the output's30+ // TileInset is what exclusive layers (panels) and a workspace size
31- // edges. Tiles (SceneWindow.Tile) and their lines are drawn only in31+ // override take from the output's edges: tiles (SceneWindow.Tile) and
32- // what is left (TileArea), so a tile is never drawn over a panel,32+ // their lines are drawn only in what is left, the usable area
33- // whatever its layer; insets covering the output leave no tile drawn.33+ // (TileArea). A tile is never drawn over a panel, whatever its layer;
34- // Zero: no panel (and the overview, capture scenes).34+ // insets covering the output leave no tile drawn. Zero: the usable
35+ // area is the whole output (and the overview, capture scenes).
35 TileInset Insets36 TileInset Insets
36 // Dim darkens the background, bottom layers and tiles (tile lines37 // Dim darkens the background, bottom layers and tiles (tile lines
37 // included) inside WorkspaceClip when set, with black at this opacity,38 // included) inside WorkspaceClip when set, with black at this opacity,

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 25

Tags 10

↑↓ moveenter openesc close