Master allocated DR-224 and UT-211 while this branch was in flight — the third collision on this work. Everything here moves up by one: DR-224..236 become DR-225..237, UT-211..213 become UT-212..214. UR-079, UR-080 and IR-033 were still free and are unchanged. Mechanical, and matched on each row's own text rather than on its number, so a row cannot be shifted twice or the wrong one caught. Master's DR-224 (the background-audio toggle) and UT-211 are untouched.
233 lines
8.8 KiB
Rust
233 lines
8.8 KiB
Rust
//! The native video surface: a GL area beneath Tauri's own webview.
|
|
//!
|
|
//! This is the desktop counterpart of the Android arrangement — a native
|
|
//! renderer at the bottom of the stack with a transparent webview drawn over it,
|
|
//! so the Svelte controls composite on top of moving video.
|
|
//!
|
|
//! The spike that authorised this built its *own* `GtkOverlay` and proved mpv
|
|
//! renders into it on X11 and Wayland. What it could not prove is the step this
|
|
//! module exists for: taking the overlay Tauri already built and reparenting the
|
|
//! real webview into it. Same widgets, one extra move, and the only place
|
|
//! Tauri-specific behaviour can still bite — which is why it is gate one.
|
|
//!
|
|
//! TRACES: UR-080 | DR-231, IR-033
|
|
|
|
use gtk::prelude::*;
|
|
use log::{info, warn};
|
|
|
|
/// The widgets that make up the video surface, kept together because their
|
|
/// lifetimes are bound: the render context (added next) is created when the GL
|
|
/// area realizes and must be freed before it unrealizes — DR-232.
|
|
pub struct VideoSurface {
|
|
/// The GL area mpv renders into. Main child of the overlay, so it sits
|
|
/// *under* everything else.
|
|
#[allow(dead_code)]
|
|
gl_area: gtk::GLArea,
|
|
/// The overlay holding the GL area and the webview.
|
|
#[allow(dead_code)]
|
|
overlay: gtk::Overlay,
|
|
}
|
|
|
|
impl VideoSurface {
|
|
// Consumed by the render context, which binds to the GL area on `realize`
|
|
// and is freed on `unrealize` (DR-232). Held here from the moment the
|
|
// surface exists so that binding has something to attach to.
|
|
#[allow(dead_code)]
|
|
/// The GL area, for the render context to bind to.
|
|
pub fn gl_area(&self) -> >k::GLArea {
|
|
&self.gl_area
|
|
}
|
|
|
|
#[allow(dead_code)]
|
|
/// The overlay, for teardown.
|
|
pub fn overlay(&self) -> >k::Overlay {
|
|
&self.overlay
|
|
}
|
|
}
|
|
|
|
/// Why a surface could not be attached.
|
|
///
|
|
/// One variant, because there is exactly one way this fails that is not already
|
|
/// reported by Tauri itself: the window exists and has a vbox, but the vbox is
|
|
/// not shaped the way Tauri has always shaped it.
|
|
#[derive(Debug)]
|
|
pub enum SurfaceError {
|
|
/// The vbox held no webview to reparent — Tauri's layout has changed.
|
|
NoWebviewChild,
|
|
}
|
|
|
|
impl std::fmt::Display for SurfaceError {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
match self {
|
|
SurfaceError::NoWebviewChild => write!(
|
|
f,
|
|
"Tauri's default vbox had no child to reparent — its window layout has changed"
|
|
),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl std::error::Error for SurfaceError {}
|
|
|
|
/// Build the overlay and move Tauri's webview on top of it.
|
|
///
|
|
/// Tauri's Linux window is an `ApplicationWindow` holding a single vertical
|
|
/// `gtk::Box` (`default_vbox`), with the webview packed into it. This takes that
|
|
/// webview out, puts a `GtkGLArea` in its place inside a `GtkOverlay`, and adds
|
|
/// the webview back as the *overlay* child so it draws above.
|
|
///
|
|
/// **Must run on the GTK main thread.** Every GTK call here is main-thread-only,
|
|
/// and the caller reaches it via `run_on_main_thread`.
|
|
///
|
|
/// Ordering matters: the GL area is added as the overlay's main child *before*
|
|
/// the webview goes back, because `GtkOverlay` treats its first `add` as the
|
|
/// bottom of the stack. Adding them the other way round yields a webview with
|
|
/// video painted over it — an easy mistake with an obvious symptom.
|
|
///
|
|
/// TRACES: UR-080 | DR-231
|
|
pub fn attach(vbox: >k::Box) -> Result<VideoSurface, SurfaceError> {
|
|
// Tauri packs exactly one child (the webview) into the default vbox. Take it
|
|
// rather than assume its type: wry's widget is an implementation detail, and
|
|
// all this needs is "whatever Tauri put here".
|
|
let children = vbox.children();
|
|
let webview = children
|
|
.into_iter()
|
|
.next()
|
|
.ok_or(SurfaceError::NoWebviewChild)?;
|
|
|
|
let gl_area = gtk::GLArea::new();
|
|
// No depth buffer: mpv draws a flat picture into an FBO and nothing here is
|
|
// 3D. Asking for one costs memory on every resize for nothing.
|
|
gl_area.set_has_depth_buffer(false);
|
|
gl_area.set_has_stencil_buffer(false);
|
|
// Fill the overlay rather than centring at intrinsic size — the same defect
|
|
// `videoFitClass` had to fix on the webview side, where `max-w-full` only
|
|
// ever shrank and a 480p source rendered as a small box on a black screen.
|
|
gl_area.set_hexpand(true);
|
|
gl_area.set_vexpand(true);
|
|
|
|
let overlay = gtk::Overlay::new();
|
|
|
|
// Reparent. `remove` drops the container's reference, so hold one across the
|
|
// move or the widget is destroyed between the two calls.
|
|
let webview_ref = webview.clone();
|
|
vbox.remove(&webview);
|
|
|
|
overlay.add(&gl_area); // main child — the bottom of the stack
|
|
overlay.add_overlay(&webview_ref); // drawn above the video
|
|
|
|
// The webview must keep receiving input: it *is* the UI. `GtkOverlay` passes
|
|
// events to overlay children by default, so pass-through stays off — setting
|
|
// it would send clicks to the GL area, which has no controls on it.
|
|
overlay.set_overlay_pass_through(&webview_ref, false);
|
|
|
|
vbox.pack_start(&overlay, true, true, 0);
|
|
overlay.show_all();
|
|
|
|
info!("[VideoSurface] GL area attached beneath Tauri's webview");
|
|
|
|
Ok(VideoSurface { gl_area, overlay })
|
|
}
|
|
|
|
/// Put Tauri's window back the way it was found.
|
|
///
|
|
/// Not merely tidiness: the webview outlives the video surface, so if the
|
|
/// surface is torn down without returning the webview to the vbox the UI
|
|
/// disappears while the app keeps running. Mirrors [`attach`] exactly.
|
|
///
|
|
/// TRACES: UR-080 | DR-231, DR-232
|
|
// Called by the render-context teardown, which lands with DR-232. Written now,
|
|
// beside `attach`, because a reparent whose inverse is written later is a
|
|
// reparent whose inverse is written wrong.
|
|
#[allow(dead_code)]
|
|
pub fn detach(vbox: >k::Box, surface: &VideoSurface) {
|
|
let children = surface.overlay.children();
|
|
for child in children {
|
|
// Everything except the GL area came from the vbox and goes back to it.
|
|
if child.downcast_ref::<gtk::GLArea>().is_some() {
|
|
continue;
|
|
}
|
|
surface.overlay.remove(&child);
|
|
vbox.pack_start(&child, true, true, 0);
|
|
}
|
|
vbox.remove(&surface.overlay);
|
|
vbox.show_all();
|
|
warn!("[VideoSurface] detached; webview returned to Tauri's vbox");
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
//! These exercise GTK widget wiring, so they need a display and are ignored
|
|
//! by default — CI has no X11 or Wayland session. Run locally with
|
|
//! `cargo test -- --ignored video_surface`.
|
|
|
|
use super::*;
|
|
|
|
/// The stacking order is the whole point, and getting it backwards produces
|
|
/// video painted over the controls rather than under them.
|
|
///
|
|
/// TRACES: UR-080 | DR-231
|
|
#[test]
|
|
#[ignore = "requires a display"]
|
|
fn test_gl_area_is_below_the_reparented_webview() {
|
|
if gtk::init().is_err() {
|
|
return;
|
|
}
|
|
let vbox = gtk::Box::new(gtk::Orientation::Vertical, 0);
|
|
// Stand in for the webview; `attach` deliberately does not care what it is.
|
|
let stand_in = gtk::DrawingArea::new();
|
|
vbox.pack_start(&stand_in, true, true, 0);
|
|
|
|
let surface = attach(&vbox).expect("attaches");
|
|
let children = surface.overlay().children();
|
|
|
|
// GtkOverlay lists its main child first.
|
|
assert!(
|
|
children[0].downcast_ref::<gtk::GLArea>().is_some(),
|
|
"the GL area must be the overlay's main child, i.e. underneath"
|
|
);
|
|
assert!(
|
|
children.len() > 1,
|
|
"the reparented widget must still be present"
|
|
);
|
|
}
|
|
|
|
/// A surface that tears down without returning the webview leaves a running
|
|
/// app with no UI.
|
|
///
|
|
/// TRACES: UR-080 | DR-231, DR-232
|
|
#[test]
|
|
#[ignore = "requires a display"]
|
|
fn test_detach_returns_the_webview_to_the_vbox() {
|
|
if gtk::init().is_err() {
|
|
return;
|
|
}
|
|
let vbox = gtk::Box::new(gtk::Orientation::Vertical, 0);
|
|
let stand_in = gtk::DrawingArea::new();
|
|
vbox.pack_start(&stand_in, true, true, 0);
|
|
|
|
let surface = attach(&vbox).expect("attaches");
|
|
detach(&vbox, &surface);
|
|
|
|
let children = vbox.children();
|
|
assert_eq!(children.len(), 1, "exactly the original child comes back");
|
|
assert!(
|
|
children[0].downcast_ref::<gtk::DrawingArea>().is_some(),
|
|
"and it is the webview stand-in, not the overlay"
|
|
);
|
|
}
|
|
|
|
/// A vbox Tauri has not populated is a changed assumption, not a panic.
|
|
///
|
|
/// TRACES: UR-080 | DR-231
|
|
#[test]
|
|
#[ignore = "requires a display"]
|
|
fn test_an_empty_vbox_is_an_error_not_a_panic() {
|
|
if gtk::init().is_err() {
|
|
return;
|
|
}
|
|
let vbox = gtk::Box::new(gtk::Orientation::Vertical, 0);
|
|
assert!(matches!(attach(&vbox), Err(SurfaceError::NoWebviewChild)));
|
|
}
|
|
}
|