Add orderly shutdown on window close

This commit is contained in:
2026-06-16 17:37:00 -04:00
parent 44bad7b70b
commit 33e3998e7c
4 changed files with 92 additions and 3 deletions
+53 -3
View File
@@ -82,6 +82,8 @@ const CHAT_MIN_H: f32 = 110.0;
const ABOVE_CHAT_MIN_H: f32 = 300.0;
/// Thickness of a draggable divider (px).
const DIVIDER_THICKNESS: f32 = 8.0;
/// Upper bound for waiting on orderly core shutdown before letting the window exit.
const SHUTDOWN_TIMEOUT_SECS: u64 = 5;
/// Clamp the Participants panel width so neither it nor the Controls panel drops
/// below its minimum, given the current window width.
@@ -221,6 +223,10 @@ pub enum AppMessage {
ToggleScreenShare,
/// Watch a peer's screen share, identified by their pixelpass ticket.
WatchShare(String),
/// Result of asynchronously enqueueing the core shutdown command.
ShutdownCommandSent(bool),
/// Fallback close if the core does not acknowledge shutdown promptly.
ShutdownTimeout,
}
fn core_subscription() -> impl iced::futures::Stream<Item = UiEvent> {
@@ -324,6 +330,8 @@ pub struct AppState {
friend_add_name: String,
/// Inline feedback for the add-friend form (e.g. a bad id), cleared on edit.
friend_add_error: Option<String>,
/// Window close has been requested and the GUI is waiting for core teardown.
closing: bool,
}
impl AppState {
@@ -441,6 +449,7 @@ impl Default for AppState {
friend_add_id: String::new(),
friend_add_name: String::new(),
friend_add_error: None,
closing: false,
}
}
}
@@ -520,6 +529,20 @@ fn subscription(_state: &AppState) -> Subscription<AppMessage> {
Subscription::batch(vec![core_sub, event_sub])
}
fn shutdown_timeout_task() -> Task<AppMessage> {
Task::perform(
async {
let (tx, rx) = iced::futures::channel::oneshot::channel();
std::thread::spawn(move || {
std::thread::sleep(std::time::Duration::from_secs(SHUTDOWN_TIMEOUT_SECS));
let _ = tx.send(());
});
let _ = rx.await;
},
|_| AppMessage::ShutdownTimeout,
)
}
/// Reconnect-chime edge trigger for `UiEvent::PeerConnecting`. Marks the peer as
/// connecting and returns `Some(Sound::ReconnectAttempt)` exactly once per outage:
/// only when the peer had a live link before (a genuine reconnect, not a first
@@ -865,6 +888,11 @@ fn update(state: &mut AppState, message: AppMessage) -> Task<AppMessage> {
state.status_message =
"Discoverable timed out — back to Normal".to_string();
}
UiEvent::ShutdownComplete => {
if state.closing {
return iced::exit();
}
}
UiEvent::Error(err) => {
state.status_message = format!("Error: {}", err);
}
@@ -1318,13 +1346,35 @@ fn update(state: &mut AppState, message: AppMessage) -> Task<AppMessage> {
state.config.window_y = Some(position.y as i32);
}
AppMessage::EventOccurred(Event::Window(iced::window::Event::CloseRequested)) => {
if state.closing {
return Task::none();
}
// We took over the close path (exit_on_close_request:false) so we can
// persist the final window size + position before quitting. Both are
// already mirrored into config by the Resized/Moved handlers above.
// persist the final window size + position and give core a chance to
// leave the room/finalize recordings before quitting.
state.config.save();
return iced::exit();
state.closing = true;
state.status_message = "Shutting down...".to_string();
let tx = state.controller.command_sender();
return Task::batch(vec![
Task::perform(
async move { tx.send(CoreCommand::Shutdown).await.is_ok() },
AppMessage::ShutdownCommandSent,
),
shutdown_timeout_task(),
]);
}
AppMessage::EventOccurred(_) => {}
AppMessage::ShutdownCommandSent(sent) => {
if !sent {
return iced::exit();
}
}
AppMessage::ShutdownTimeout => {
if state.closing {
return iced::exit();
}
}
AppMessage::NavigateToSettings => {
state.current_screen = Screen::Settings;
state.layout_picker_open = false;