Quit from the tray even when the window is minimized and gets no frames (#77)

This commit is contained in:
MarekSpodBiedry 2026-10-11 10:35:06 +02:00 committed by GitHub
parent 5b7d4781f5
commit 714522d577
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 58 additions and 5 deletions

View File

@ -12,6 +12,7 @@ mod tray;
use std::path::PathBuf;
use std::process::ExitCode;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::mpsc::{Receiver, RecvTimeoutError, SyncSender, channel, sync_channel};
use std::sync::{Arc, Mutex, OnceLock, PoisonError};
use std::time::Duration;
@ -45,7 +46,7 @@ fn main() -> ExitCode {
Instance::Primary(rx) => rx,
};
let (tray_loop, woken) = sync_channel(1);
let waker = Arc::new(Waker { window: Mutex::new(None), tray_loop });
let waker = Arc::new(Waker { window: Mutex::new(None), tray_loop, quit_pending: AtomicBool::new(false) });
{
let waker = waker.clone();
let _ = wake.set(Box::new(move || waker.wake()));
@ -173,8 +174,13 @@ fn wgpu_options() -> eframe::WgpuConfiguration {
struct Waker {
window: Mutex<Option<egui::Context>>,
tray_loop: SyncSender<()>,
/// Quit was picked in the tray and no tick has taken it up yet.
quit_pending: AtomicBool,
}
/// How long a tray Quit may wait for the window to take it up before the launcher exits anyway.
const QUIT_GRACE: Duration = Duration::from_secs(2);
impl Waker {
fn wake(&self) {
if let Some(ctx) = self.window.lock().unwrap_or_else(PoisonError::into_inner).as_ref() {
@ -183,6 +189,30 @@ impl Waker {
let _ = self.tray_loop.try_send(());
}
/// Quit was picked in the tray. A minimized window may get no frames at all (Wayland sends none),
/// so the quit would wait until the window is restored: if no tick has taken it up in time, exit.
fn quit_from_tray(self: &Arc<Self>) {
self.quit_pending.store(true, Ordering::SeqCst);
let waker = self.clone();
std::thread::spawn(move || {
std::thread::sleep(QUIT_GRACE);
if waker.quit_unseen() {
log::warn!("the window did not answer a quit from the tray: exiting");
std::process::exit(0);
}
});
}
/// A tick took up the quit.
fn quit_seen(&self) {
self.quit_pending.store(false, Ordering::SeqCst);
}
/// Whether a quit from the tray is still waiting (and no longer is).
fn quit_unseen(&self) -> bool {
self.quit_pending.swap(false, Ordering::SeqCst)
}
fn set_window(&self, ctx: Option<egui::Context>) {
*self.window.lock().unwrap_or_else(PoisonError::into_inner) = ctx;
}
@ -317,6 +347,7 @@ struct Launcher {
closed_to_tray: bool,
/// eframe's settings file, which the launcher writes itself while there is no window.
settings_path: Option<PathBuf>,
waker: Option<Arc<Waker>>,
}
impl Launcher {
@ -336,6 +367,7 @@ impl Launcher {
started: false,
closed_to_tray: false,
settings_path,
waker: None,
}
}
@ -346,8 +378,9 @@ impl Launcher {
if let Some(settings) = settings {
self.ui.settings = settings;
}
let waker = waker.clone();
match tray::Tray::new(move || waker.wake()) {
self.waker = Some(waker.clone());
let (wake, on_quit) = (waker.clone(), waker.clone());
match tray::Tray::new(move || wake.wake(), move || on_quit.quit_from_tray()) {
Ok(t) => self.tray = Some(t),
Err(e) => log::warn!("no tray icon: {e}"),
}
@ -391,7 +424,12 @@ impl Launcher {
log::warn!("couldn't open {url}: {e}");
}
}
tray::TrayAction::Quit => requests.push(Request::Quit),
tray::TrayAction::Quit => {
if let Some(waker) = &self.waker {
waker.quit_seen();
}
requests.push(Request::Quit);
}
}
}
if let Some(t) = self.tray.as_mut() {
@ -768,6 +806,17 @@ mod tests {
assert!(!dir.exists());
}
#[test]
fn a_tray_quit_nobody_took_up_ends_the_launcher() {
let (tray_loop, _woken) = sync_channel(1);
let waker = Waker { window: Mutex::new(None), tray_loop, quit_pending: AtomicBool::new(true) };
assert!(waker.quit_unseen());
assert!(!waker.quit_unseen());
waker.quit_pending.store(true, Ordering::SeqCst);
waker.quit_seen();
assert!(!waker.quit_unseen());
}
#[test]
fn quitting_overrides_a_close_to_the_tray() {
let (mut launcher, _show, _) = launcher();

View File

@ -42,7 +42,8 @@ fn load_icon() -> Result<tray_icon::Icon, String> {
impl Tray {
/// Creates the tray icon. `wake` repaints the UI so it sees the action, even when hidden.
pub fn new(wake: impl Fn() + Send + Sync + Clone + 'static) -> Result<Self, String> {
/// `on_quit` runs when Quit is picked, before the UI can see it: the UI may never get to.
pub fn new(wake: impl Fn() + Send + Sync + Clone + 'static, on_quit: impl Fn() + Send + Sync + 'static) -> Result<Self, String> {
let show = MenuItem::new(i18n::t("Show ArtCraft Launcher"), true, None);
let check = MenuItem::new(i18n::t("Check for Updates"), true, None);
use artcraft_launcher_core::catalog::{DISCORD_URL, GITHUB_URL, WEBSITE_URL};
@ -80,6 +81,9 @@ impl Tray {
} else {
return;
};
if matches!(action, TrayAction::Quit) {
on_quit();
}
let _ = tx.send(action);
wake();
}));