fix: cover the whole display while a disc loads

The loading surface was invisible. GtkWindow only renders its own CSS
background when the application is not painting the window itself, and
this window was created app-paintable with no draw handler and no child
— so it drew nothing at all. The black area during loading was never it:
it was VLC's own window, tiled by sway into the usable area, which
excludes the strip the dock reserves. That is why the dock and the
wallpaper stayed on screen underneath.

An earlier headless check missed this because that sway had no wallpaper
configured, so an invisible surface and a black one looked identical.
Rechecked with the real wallpaper and the real dock underneath.

The surface now paints, and carries a spinner and "Loading DVD..." on
the same translucent rounded card as the dock. It moves from the top
layer to the overlay layer, because the dock is a top-layer surface and
DVD mode has to cover it rather than depend on which surface sway
created first. Its exclusive zone of -1 keeps sway from moving it out of
the way of the dock's reserved strip.

It goes up before VLC is started rather than alongside it, so no part of
VLC's window is ever seen against the wallpaper, and comes down only
once VLC reports playback on screen. The deadline that takes it down
regardless is now twenty seconds, since an optical drive can be slow,
and a failure to start VLC at all ends the session instead of leaving a
cover with nothing behind it.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
ekklesia
2026-09-04 22:52:21 -05:00
co-authored by Claude Opus 5
parent 4a0ff43253
commit 68b6347c05
3 changed files with 145 additions and 52 deletions
+19 -2
View File
@@ -45,13 +45,30 @@ disc behind it.
`classroom-dvd-session` runs the whole thing:
```
black backdrop layer-shell TOP, covers wallpaper and dock while the
disc loads, hidden once VLC is on screen
loading surface layer-shell OVERLAY, the whole display including the
dock, taken down once playback is on screen
VLC fullscreen, drawn above the top layer by sway
control bar layer-shell OVERLAY, the one layer sway draws above a
fullscreen window
```
### Loading
Pressing DVD covers the entire display immediately — before VLC is even
started — with a black surface carrying a spinner and *Loading DVD…* on
the same translucent rounded card the dock uses. The wallpaper, the dock
and the clock are all gone from the moment the button is pressed.
It is on the **overlay** layer rather than the top layer, because the
dock is a top-layer surface and DVD mode has to cover it. Its exclusive
zone is `-1`, which tells sway not to move it out of the way of the
strip the dock reserves.
It comes down when VLC reports playing or paused, meaning the video
output exists and has taken the display, plus a moment for the handover.
A twenty second deadline takes it down regardless: a cover with nothing
behind it is the one outcome DVD mode must never produce.
VLC is still the backend because its `dvdnav` module is what decodes
disc menus, and nothing else on Debian does that reliably. But nothing
of VLC's own interface is exposed: no menu bar, no panels, no
+6 -3
View File
@@ -124,9 +124,12 @@ With `DVD_ENABLED=true`:
### DVD mode
- [ ] Pressing DVD goes black immediately — no wallpaper, no dock, no
partial VLC window at any point
- [ ] Video or the disc menu appears inside that black surface
- [ ] Pressing DVD covers the **whole** display immediately, dock and
clock included — no wallpaper visible anywhere, at any point
- [ ] A spinner and "Loading DVD…" show while the disc spins up
- [ ] No partial VLC window is ever visible against the wallpaper
- [ ] Video or the disc menu replaces the loading surface when ready
- [ ] A disc that never opens still gives the desktop back (20s)
- [ ] No VLC chrome anywhere: no menu bar, no fullscreen controller,
no cone, no sway title bar
- [ ] Wallpaper is never visible around VLC
+115 -42
View File
@@ -10,8 +10,9 @@ this program, styled from the same stylesheet as the dock.
Layout, bottom to top:
black backdrop layer-shell TOP, covers wallpaper and dock while
the disc loads, hidden once VLC is up
loading surface layer-shell OVERLAY, covers the whole display
including the dock while the disc loads, removed
once playback is actually on screen
VLC fullscreen, drawn above the top layer by sway
control bar layer-shell OVERLAY, drawn above fullscreen video
"""
@@ -76,26 +77,56 @@ HOVER_STRIP = 14
IDLE_HIDE_SECONDS = 4
LEAVE_HIDE_SECONDS = 1
# The backdrop goes away as soon as VLC is up. The deadline is a safety
# net: whatever happens, DVD mode must never leave a black screen with
# nothing behind it.
# The loading surface goes away as soon as playback is on screen. The
# deadline is a safety net: whatever happens, DVD mode must never leave a
# cover over a screen with nothing behind it. Generous, because an
# optical drive can take a while to spin a disc up.
WINDOW_POLL_MS = 500
BACKDROP_DEADLINE_SECONDS = 10
LOADING_DEADLINE_SECONDS = 20
# Long enough for the cover to be on screen before VLC is even started,
# so no part of VLC's window is ever visible against the wallpaper.
LAUNCH_DELAY_MS = 120
STATUS_POLL_SECONDS = 1
EXTRA_CSS = """
window#classroom-dvd-backdrop {
def extra_css(scale):
"""Styling for the two surfaces this program owns.
The loading card reuses the dock's own .dock class, so it is the same
translucent surface, border and corner radius as everything else in
the room.
"""
return f"""
window#classroom-dvd-loading {{
background-color: #000000;
background-image: none;
}
}}
window#classroom-dvd-bar {
.dvd-loading {{
padding: {scaled(16, scale)}px {scaled(22, scale)}px;
}}
.dvd-loading label {{
color: #ffffff;
font-size: {scaled(17, scale)}px;
font-weight: 600;
}}
.dvd-loading spinner,
.dvd-loading spinner:checked {{
color: #ffffff;
opacity: 1;
min-width: {scaled(28, scale)}px;
min-height: {scaled(28, scale)}px;
}}
window#classroom-dvd-bar {{
background-color: transparent;
background-image: none;
border: none;
box-shadow: none;
}
}}
"""
@@ -161,24 +192,35 @@ class VlcControl:
return found.group(1) if found else None
class Backdrop(Gtk.Window):
"""Full-screen black, above the wallpaper and the dock."""
class LoadingSurface(Gtk.Window):
"""The whole display, while the disc spins up.
def __init__(self):
Entering DVD mode has to look like entering DVD mode, not like an
application opening on the classroom desktop, so this goes up before
VLC is even started and covers everything until there is something
to show.
"""
def __init__(self, scale):
super().__init__()
screen = self.get_screen()
visual = screen.get_rgba_visual()
if visual is not None:
self.set_visual(visual)
self.set_app_paintable(True)
self.set_name("classroom-dvd-backdrop")
self.set_name("classroom-dvd-loading")
self.set_decorated(False)
# Deliberately NOT app-paintable, and deliberately no RGBA visual.
# A GtkWindow only renders its own CSS background when the
# application is not painting the window itself, and this surface
# is nothing but background. Setting app_paintable here is what
# made an earlier version of this window invisible.
GtkLayerShell.init_for_window(self)
GtkLayerShell.set_layer(self, GtkLayerShell.Layer.TOP)
# OVERLAY rather than TOP. The dock is a TOP layer surface, and
# DVD mode has to cover it; OVERLAY is above it without depending
# on which surface sway happens to have created first. It is
# taken down the moment playback is on screen, so it never sits
# over the video.
GtkLayerShell.set_layer(self, GtkLayerShell.Layer.OVERLAY)
for edge in (
GtkLayerShell.Edge.TOP,
@@ -188,19 +230,33 @@ class Backdrop(Gtk.Window):
):
GtkLayerShell.set_anchor(self, edge, True)
# -1 means "ignore everyone else's reserved space": DVD mode is
# the whole display, including the strip the dock reserves.
# -1 means "do not move me out of anyone's way": the strip the
# dock reserves is part of the display like any other.
GtkLayerShell.set_exclusive_zone(self, -1)
GtkLayerShell.set_keyboard_interactivity(self, False)
self.connect("realize", self.ignore_pointer)
card = Gtk.Box(
orientation=Gtk.Orientation.HORIZONTAL,
spacing=scaled(14, scale),
)
card.get_style_context().add_class("dock")
card.get_style_context().add_class("dvd-loading")
def ignore_pointer(self, *_):
# Scenery, never a target.
if cairo is None:
return
self.spinner = Gtk.Spinner()
self.spinner.set_size_request(scaled(28, scale), scaled(28, scale))
card.pack_start(self.spinner, False, False, 0)
self.get_window().input_shape_combine_region(cairo.Region(), 0, 0)
label = Gtk.Label(label="Loading DVD\u2026")
card.pack_start(label, False, False, 0)
centred = Gtk.Alignment(xalign=0.5, yalign=0.5, xscale=0, yscale=0)
centred.add(card)
self.add(centred)
def show_all(self):
super().show_all()
self.spinner.start()
class ControlBar(Gtk.Window):
@@ -404,7 +460,7 @@ class DvdSession:
self.eject_on_exit = False
self.elapsed = 0
self.backdrop = Backdrop()
self.loading = LoadingSurface(self.scale)
self.bar = ControlBar(
self.scale,
[
@@ -473,49 +529,64 @@ class DvdSession:
# -- the player -------------------------------------------------
def start(self):
self.backdrop.show_all()
self.loading.show_all()
self.bar.show_all()
# The cover goes up first. VLC follows once it is actually on
# screen, so no part of its window is ever seen against the
# wallpaper.
GLib.timeout_add(LAUNCH_DELAY_MS, self.launch)
def launch(self):
command = vlc.player_command(self.device, self.port, self.password)
try:
self.pid, _, _, _ = GLib.spawn_async(
argv=command,
flags=GLib.SpawnFlags.SEARCH_PATH
| GLib.SpawnFlags.DO_NOT_REAP_CHILD,
)
except GLib.Error as error:
# A cover with nothing behind it is the one outcome DVD mode
# must never produce.
print(f"classroom-dvd-session: {error}", file=sys.stderr)
Gtk.main_quit()
return GLib.SOURCE_REMOVE
GLib.child_watch_add(GLib.PRIORITY_DEFAULT, self.pid, self.on_exit)
GLib.timeout_add(WINDOW_POLL_MS, self.watch_startup)
GLib.timeout_add_seconds(STATUS_POLL_SECONDS, self.watch_state)
return GLib.SOURCE_REMOVE
def on_exit(self, pid, status, *_):
GLib.spawn_close_pid(pid)
self.pid = None
Gtk.main_quit()
def watch_startup(self):
"""Keep the black surface until VLC is actually covering the screen.
"""Hold the cover until playback is genuinely on screen.
Playing or paused means the video output exists, which is also
when VLC takes the screen. The deadline is the safety net: DVD
mode must never leave a black screen with nothing behind it, even
if the disc never opens.
when VLC takes the display. The deadline is the safety net: DVD
mode must never leave a cover over a screen with nothing behind
it, even if the disc never opens.
"""
self.elapsed += WINDOW_POLL_MS / 1000
ready = self.control.state() in ("playing", "paused")
if ready or self.elapsed >= BACKDROP_DEADLINE_SECONDS:
if ready or self.elapsed >= LOADING_DEADLINE_SECONDS:
# A moment for the video window to actually take the screen,
# so the handover has no frame of wallpaper in it.
GLib.timeout_add(400, self.drop_backdrop)
GLib.timeout_add(400, self.drop_loading)
return GLib.SOURCE_REMOVE
return GLib.SOURCE_CONTINUE
def drop_backdrop(self):
self.backdrop.hide()
def drop_loading(self):
self.loading.hide()
return GLib.SOURCE_REMOVE
def watch_state(self):
@@ -530,7 +601,9 @@ class DvdSession:
def load_css(scale):
try:
provider = Gtk.CssProvider()
provider.load_from_data((style.render(scale) + EXTRA_CSS).encode("utf-8"))
provider.load_from_data(
(style.render(scale) + extra_css(scale)).encode("utf-8")
)
except (OSError, GLib.Error) as error:
print(f"classroom-dvd-session: {error}", file=sys.stderr)
return