diff --git a/docs/dvd.md b/docs/dvd.md index cfa4c1a..3440a2e 100644 --- a/docs/dvd.md +++ b/docs/dvd.md @@ -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 diff --git a/docs/testing.md b/docs/testing.md index 8479f16..c7c36d7 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -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 diff --git a/scripts/classroom-dvd-session b/scripts/classroom-dvd-session index 9d34748..3e20868 100755 --- a/scripts/classroom-dvd-session +++ b/scripts/classroom-dvd-session @@ -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) - self.pid, _, _, _ = GLib.spawn_async( - argv=command, - flags=GLib.SpawnFlags.SEARCH_PATH - | GLib.SpawnFlags.DO_NOT_REAP_CHILD, - ) + 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