fix(window): closing a project tab group returns to the welcome screen (#2130)

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]>
Цей коміт міститься в:
l0ng-aiтаClaude authored and GitHub committed 2026-11-08 18:23:59 +08:00
1 джерело f42d10d86c
коміт 47d547e206
5 files changed
+41 -5

No files matched your search

+13 -3
Переглянути файл
@@ -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
Переглянути файл
@@ -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
Переглянути файл
@@ -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
Переглянути файл
@@ -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
Переглянути файл
@@ -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;