Compare commits

...
Author SHA1 Message Date
l0ng-aiandClaude 89f68aa404 fix(window): closing a project tab group returns to the welcome screen
The system close button closes a native tab group as a whole, so every member asks
to close at once. Each member's close guard saw the other members as remaining
windows, let itself close, and the app quit instead of showing the welcome screen.

Members closing together now count as one window closing: the first of them is the
last window left and returns to the welcome screen, and the others close.

Co-Authored-By: Claude <[email protected]>
2026-11-08 18:22:38 +08:00
5 changed files with 41 additions and 5 deletions

No files matched your search

+13 -3
View File
@@ -9,7 +9,7 @@ use specta::Type;
use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, Ordering};
use tauri::ipc::Channel;
use tauri::{AppHandle, State, WebviewWindow};
use tauri::{AppHandle, State, WebviewWindow, Window};
/// Opens `path` in a new window (`new_window`) or tells the caller to open it
/// itself (`current`). A project already open in some window is focused instead.
@@ -211,8 +211,18 @@ pub struct CloseContext {
#[tauri::command]
#[specta::specta]
pub fn window_close_context(app: AppHandle, windows: State<'_, WindowsState>) -> CloseContext {
CloseContext { quitting: windows.quitting(), windows: windows::project_window_labels(&app).len() as u32 }
pub fn window_close_context(app: AppHandle, window: Window, windows: State<'_, WindowsState>) -> CloseContext {
let all = windows::project_window_labels(&app);
let pending: Vec<String> = windows::sorted_labels(
windows.pending_closes().iter().map(String::as_str).filter(|l| all.iter().any(|a| a == l)),
);
// Members of a tab group asking to close together are one window closing: the first of them
// is the last window left (it returns to the welcome screen); the others just close.
let count = match pending.first() {
Some(keeper) if pending.len() > 1 && keeper == window.label() => all.len() - pending.len() + 1,
_ => all.len(),
};
CloseContext { quitting: windows.quitting(), windows: count as u32 }
}
/// Quits: closes every window, each of which runs its own unsaved-changes check.
+7 -2
View File
@@ -205,8 +205,13 @@ pub fn run() {
Ok(())
})
.on_window_event(|window, event| match event {
#[cfg(target_os = "linux")]
tauri::WindowEvent::CloseRequested { .. } => linux_window_state::save_before_close(window.app_handle()),
tauri::WindowEvent::CloseRequested { .. } => {
#[cfg(target_os = "linux")]
linux_window_state::save_before_close(window.app_handle());
if let Some(w) = window.try_state::<WindowsState>() {
w.close_requested(window.label());
}
}
tauri::WindowEvent::Focused(true) => {
application_focus::changed(window.app_handle(), true);
if let Some(w) = window.try_state::<WindowsState>() {
+1
View File
@@ -214,6 +214,7 @@ pub fn close_tabs(app: &AppHandle, caller: &str, keep: &str, labels: Vec<String>
/// The closing tab's project vetoed (its unsaved-changes prompt was cancelled): leave the rest open.
pub fn cancel_close(app: &AppHandle, caller: &str) {
let state = app.state::<WindowsState>();
state.close_settled(caller);
let mut batch = state.tab_closes.lock();
if batch.as_ref().is_some_and(|b| b.pending.front().is_some_and(|l| l == caller)) {
*batch = None;
+19
View File
@@ -140,6 +140,9 @@ pub struct WindowsState {
last_focused: Mutex<Option<String>>,
/// Set by Quit while windows close one by one.
quitting: AtomicBool,
/// Windows asked to close whose guards have not finished. AppKit closes a whole tab group at
/// once, so its members ask together.
close_requests: Mutex<Vec<String>>,
/// Project tabs closing one by one (Close Other Tabs and friends).
pub tab_closes: Mutex<Option<crate::window_groups::TabCloses>>,
/// Relaunch once the quit completes (after installing an update).
@@ -149,6 +152,21 @@ pub struct WindowsState {
}
impl WindowsState {
pub fn close_requested(&self, label: &str) {
let mut requests = self.close_requests.lock();
if !requests.iter().any(|l| l == label) {
requests.push(label.to_owned());
}
}
pub fn close_settled(&self, label: &str) {
self.close_requests.lock().retain(|l| l != label);
}
pub fn pending_closes(&self) -> Vec<String> {
self.close_requests.lock().clone()
}
pub fn set_launch(&self, label: &str, launch: Launch) {
self.launches.lock().insert(label.to_owned(), launch);
}
@@ -187,6 +205,7 @@ impl WindowsState {
}
pub fn forget(&self, label: &str) {
self.close_settled(label);
self.launches.lock().remove(label);
let mut last = self.last_focused.lock();
if last.as_deref() == Some(label) {
+1
View File
@@ -189,6 +189,7 @@ async function onCloseRequested(deps: CloseDeps): Promise<boolean> {
// screen (to open another one); closing the welcome screen, or quitting, closes the window.
if (!ctx.quitting && ctx.windows <= 1 && (s.project || s.lightEdit)) {
if (await confirmLeave(deps)) await closeProject();
await commands.windowGroupCancelClose();
return false;
}
const confirmTerminate = workflowSettings().run.confirmTerminate;