feat(gui): hand-rolled winit loop for true window-hide on Wayland
Replace eframe::run_native with a winit ApplicationHandler + glutin + egui_glow loop so "keep running in the tray" can genuinely hide the window. winit's set_visible(false) is a deliberate no-op on Wayland (xdg-shell has no unmap-but-keep-alive request), so the only way to hide a toplevel is to destroy its surface: hide-to-tray now drops the Window + GL surface (parking the GL context as not-current) and a tray click recreates them and makes the context current again. The GL context, glutin display/config, egui_glow painter (uploaded textures), and egui-winit state (clipboard) all persist across the cycle — only the OS window and its surface churn. Wakeups route through winit's EventLoopProxy (the new Waker, and the tray) instead of egui's repaint callback, so a child event or tray click wakes the loop even while the window is dropped and no frame is running — keeping viewer join/leave notifications and the tray tooltip live while hidden. Removes the old Wayland minimize-to-tray fallback (window stayed in the taskbar); hide is now uniform on Wayland and X11. Deps: winit/glutin/glutin-winit/egui_glow promoted to direct (gui-gated, optional) — all already transitive via eframe, so no new crates. winit's default features minus wayland-csd-adwaita, so sctk-adwaita/tiny-skia/ ttf-parser aren't pulled for a CSD fallback titlebar (KWin draws server-side decorations, and eframe never had CSD either). Verified end-to-end on KWin Wayland: launch->render; close->window AND taskbar entry gone (true hide, process stays alive); tray activate-> window + GL surface recreated and renders; tray quit->clean exit; stderr clean throughout. cargo test --features gui: 15 pass; clippy clean; headless dependency tree unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+15
-5
@@ -12,11 +12,12 @@ use std::sync::mpsc::Receiver;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
|
||||
use eframe::egui;
|
||||
use nix::sys::signal::{Signal, kill};
|
||||
use nix::unistd::Pid;
|
||||
use serde::Deserialize;
|
||||
|
||||
use super::Waker;
|
||||
|
||||
/// One parsed event from the child's stdout. Owned mirror of
|
||||
/// [`crate::common::output::Event`] (which borrows for emit); kept separate so
|
||||
/// the wire format and the parser can evolve independently.
|
||||
@@ -77,9 +78,13 @@ pub struct ChildProc {
|
||||
}
|
||||
|
||||
impl ChildProc {
|
||||
/// Spawn `pixelpass <args>` as a child, wiring up the event reader. `ctx`
|
||||
/// is repainted whenever an event arrives so the UI updates live.
|
||||
pub fn spawn(args: &[String], ctx: egui::Context) -> std::io::Result<Self> {
|
||||
/// Spawn `pixelpass <args>` as a child, wiring up the event reader. The
|
||||
/// `waker` is pinged whenever an event arrives so the UI thread wakes to
|
||||
/// drain it — this wakes the winit event loop directly (via an
|
||||
/// `EventLoopProxy`), so it works even when the window is hidden to the tray
|
||||
/// and no frames are running (egui's own repaint callback would not fire
|
||||
/// repeatedly in that idle state — see [`super::Waker`]).
|
||||
pub fn spawn(args: &[String], waker: Waker) -> std::io::Result<Self> {
|
||||
let exe = std::env::current_exe()?;
|
||||
let mut child = Command::new(exe)
|
||||
.args(args)
|
||||
@@ -104,9 +109,14 @@ impl ChildProc {
|
||||
if tx.send(ev).is_err() {
|
||||
break; // app gone
|
||||
}
|
||||
ctx.request_repaint();
|
||||
waker.wake();
|
||||
}
|
||||
}
|
||||
// stdout closed → the child has exited (player closed, connection
|
||||
// ended, or a failed launch). Wake once more so the UI reaps it and
|
||||
// clears the "running" view, even if no final event was emitted and
|
||||
// the window is hidden to the tray.
|
||||
waker.wake();
|
||||
});
|
||||
|
||||
let stderr_tail = Arc::new(Mutex::new(Vec::<String>::new()));
|
||||
|
||||
Reference in New Issue
Block a user