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:
+17
-18
@@ -3,9 +3,13 @@
|
||||
//! The tray runs on its **own dedicated thread** with its own current-thread
|
||||
//! tokio runtime, fully decoupled from the winit event loop (which owns the
|
||||
//! main thread) and from the process-wide `#[tokio::main]` runtime. It talks to
|
||||
//! the egui app purely over channels:
|
||||
//! the egui app purely over winit's event channel and a status channel:
|
||||
//!
|
||||
//! * tray → app: [`TrayAction`] (Show / Quit), polled each frame.
|
||||
//! * tray → app: a [`super::UserEvent::Tray`] carrying a [`TrayAction`]
|
||||
//! (Show / Quit), pushed through the [`winit::event_loop::EventLoopProxy`].
|
||||
//! Using the proxy (not egui's repaint) is essential: a tray click must
|
||||
//! wake the winit loop even when the window has been **dropped** (hidden to
|
||||
//! tray), so the loop can recreate it.
|
||||
//! * app → tray: [`TrayStatus`] (idle / hosting / viewing), pushed on change.
|
||||
//!
|
||||
//! Why a separate thread instead of `Handle::current().spawn`: updating the
|
||||
@@ -15,12 +19,14 @@
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::mpsc::{Receiver, Sender};
|
||||
|
||||
use eframe::egui;
|
||||
use ksni::TrayMethods;
|
||||
use winit::event_loop::EventLoopProxy;
|
||||
|
||||
/// What the user picked from the tray icon or its menu (tray thread → app).
|
||||
use super::UserEvent;
|
||||
|
||||
/// What the user picked from the tray icon or its menu (tray thread → app),
|
||||
/// delivered as a [`UserEvent::Tray`].
|
||||
pub enum TrayAction {
|
||||
/// Left-click, or the "Show window" item: bring the window back.
|
||||
Show,
|
||||
@@ -49,8 +55,6 @@ fn status_text(status: TrayStatus) -> String {
|
||||
/// Handle held by the egui app for the lifetime of the window. Dropping it
|
||||
/// closes the app→tray channel, which ends the tray thread and removes the icon.
|
||||
pub struct TrayHandle {
|
||||
/// Menu/icon actions to drain each frame.
|
||||
pub actions: Receiver<TrayAction>,
|
||||
status_tx: tokio::sync::mpsc::UnboundedSender<TrayStatus>,
|
||||
/// Set true once the tray actually registered with a StatusNotifier host.
|
||||
/// The app must not divert the window's close to a tray that never appeared.
|
||||
@@ -80,16 +84,14 @@ struct PixelPassTray {
|
||||
/// ARGB pixmap, so the icon shows even where the themed "pixelpass" name
|
||||
/// can't be resolved (e.g. running the dev binary before `make install`).
|
||||
icon: Vec<ksni::Icon>,
|
||||
actions: Sender<TrayAction>,
|
||||
/// Repaint the (possibly hidden/minimized) window so it wakes to act on a
|
||||
/// tray click — otherwise an idle, hidden window never processes the action.
|
||||
ctx: egui::Context,
|
||||
/// Wakes the winit loop and delivers the action — works even when the
|
||||
/// window has been dropped to the tray (no egui frame is running then).
|
||||
proxy: EventLoopProxy<UserEvent>,
|
||||
}
|
||||
|
||||
impl PixelPassTray {
|
||||
fn notify(&self, action: TrayAction) {
|
||||
let _ = self.actions.send(action);
|
||||
self.ctx.request_repaint();
|
||||
let _ = self.proxy.send_event(UserEvent::Tray(action));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,9 +178,8 @@ fn load_icon() -> Option<Vec<ksni::Icon>> {
|
||||
/// Start the tray on its own thread. Returns a handle for the app to drive it,
|
||||
/// or `None` if the icon couldn't be decoded or the thread couldn't spawn (in
|
||||
/// which case the GUI simply runs without a tray — close behaves as before).
|
||||
pub fn start(ctx: egui::Context) -> Option<TrayHandle> {
|
||||
pub fn start(proxy: EventLoopProxy<UserEvent>) -> Option<TrayHandle> {
|
||||
let icon = load_icon()?;
|
||||
let (action_tx, action_rx) = std::sync::mpsc::channel();
|
||||
let (status_tx, mut status_rx) = tokio::sync::mpsc::unbounded_channel::<TrayStatus>();
|
||||
let registered = Arc::new(AtomicBool::new(false));
|
||||
let registered_thread = registered.clone();
|
||||
@@ -200,8 +201,7 @@ pub fn start(ctx: egui::Context) -> Option<TrayHandle> {
|
||||
let tray = PixelPassTray {
|
||||
status: TrayStatus::Idle,
|
||||
icon,
|
||||
actions: action_tx,
|
||||
ctx,
|
||||
proxy,
|
||||
};
|
||||
let handle = match tray.spawn().await {
|
||||
Ok(handle) => handle,
|
||||
@@ -226,7 +226,6 @@ pub fn start(ctx: egui::Context) -> Option<TrayHandle> {
|
||||
.ok()?;
|
||||
|
||||
Some(TrayHandle {
|
||||
actions: action_rx,
|
||||
status_tx,
|
||||
registered,
|
||||
last_sent: None,
|
||||
|
||||
Reference in New Issue
Block a user