Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/apps/desktop/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ windows = { workspace = true, features = [
"Win32_Graphics_Dxgi",
"Win32_Graphics_Dxgi_Common",
"Win32_Graphics_Gdi",
"Win32_Storage_FileSystem",
"Win32_Storage_Xps",
"Win32_System_Com",
"Win32_System_Ole",
Expand Down
6 changes: 3 additions & 3 deletions src/apps/desktop/src/api/system_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -401,7 +401,7 @@ pub struct RestartAppRequest {}
pub async fn restart_app(app: AppHandle, request: RestartAppRequest) -> Result<(), String> {
let _ = request;
crate::crash_diagnostics::mark_clean_shutdown("restart_app");
crate::save_main_window_state(&app);
crate::save_main_window_state(&app, "restart_app");
crate::perform_process_exit_cleanup();
app.restart();
Ok(())
Expand Down Expand Up @@ -660,7 +660,7 @@ pub async fn set_main_window_transient_geometry(
pub async fn quit_app(app: tauri::AppHandle) -> Result<(), String> {
log::info!("Quit requested via quit_app command");
crate::crash_diagnostics::mark_clean_shutdown("quit_app_command");
crate::save_main_window_state(&app);
crate::save_main_window_state(&app, "quit_app_command");
crate::perform_process_exit_cleanup();
app.exit(0);
Ok(())
Expand Down Expand Up @@ -732,7 +732,7 @@ pub async fn startup_window_control(
if behavior == "quit" {
log::info!("Quit requested from startup window control");
crate::crash_diagnostics::mark_clean_shutdown("startup_window_control");
crate::save_main_window_state(&app);
crate::save_main_window_state(&app, "startup_window_control_quit");
crate::perform_process_exit_cleanup();
app.exit(0);
} else {
Expand Down
33 changes: 31 additions & 2 deletions src/apps/desktop/src/appearance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -646,7 +646,7 @@ pub fn create_main_window(
let build_started_at = Instant::now();
match builder.build() {
Ok(window) => {
crate::restore_main_window_state(&window);
let reapply_maximized = crate::restore_main_window_state(&window);
crate::webview_recovery::install(&window);
startup_trace.record_elapsed_step("native_window", "webview_build", build_started_at);
debug!(
Expand All @@ -665,7 +665,12 @@ pub fn create_main_window(
}
}

show_main_window_for_startup(&window, total_started_at, startup_trace);
show_main_window_for_startup(
&window,
total_started_at,
startup_trace,
reapply_maximized,
);
}
Err(e) => {
error!(
Expand All @@ -681,6 +686,7 @@ fn show_main_window_for_startup(
window: &tauri::WebviewWindow,
total_started_at: Instant,
startup_trace: &DesktopStartupTrace,
reapply_maximized: bool,
) {
let show_started_at = Instant::now();
if let Err(error) = window.show() {
Expand All @@ -705,6 +711,29 @@ fn show_main_window_for_startup(
focus_started_at.elapsed().as_millis(),
total_started_at.elapsed().as_millis()
);

// Maximize only after the window is visible: maximizing a hidden
// undecorated window on Windows is dropped on show and leaves a bogus
// normal-placement rect behind (see `main_window_restore_flags`).
if reapply_maximized {
match window.is_maximized() {
Ok(true) => {}
Ok(false) => {
if let Err(error) = window.maximize() {
log::warn!(
"Failed to re-apply persisted maximized state after main window show: {}",
error
);
}
}
Err(error) => {
log::warn!(
"Failed to query main window maximized state after show: {}",
error
)
}
}
}
}

fn app_url(path: &str) -> WebviewUrl {
Expand Down
120 changes: 104 additions & 16 deletions src/apps/desktop/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ pub mod sleep_prevention;
pub mod startup_trace;
pub mod tray;
mod webview_recovery;
mod window_state_support;

use bitfun_agent_runtime::sdk::{attach_session_event_cursor, SessionEventJournal};
use bitfun_core::agentic::tools::computer_use_capability::set_computer_use_desktop_available;
Expand Down Expand Up @@ -315,22 +316,75 @@ fn handle_secondary_launch(app: &tauri::AppHandle) {
}

fn main_window_state_flags() -> StateFlags {
StateFlags::SIZE | StateFlags::POSITION | StateFlags::MAXIMIZED | StateFlags::FULLSCREEN
main_window_geometry_state_flags() | StateFlags::MAXIMIZED
}

fn persist_main_window_state(app: &tauri::AppHandle) -> Result<(), String> {
app.save_window_state(main_window_state_flags())
.map_err(|error| error.to_string())
fn main_window_geometry_state_flags() -> StateFlags {
StateFlags::SIZE | StateFlags::POSITION | StateFlags::FULLSCREEN
}

pub(crate) fn save_main_window_state(app: &tauri::AppHandle) {
/// Restore deliberately excludes `MAXIMIZED` on Windows: maximizing a hidden
/// undecorated window does not survive `show()` and leaves Windows tracking a
/// bogus normal-placement rect. Other platforms use the plugin's complete
/// restore behavior.
#[cfg(target_os = "windows")]
fn main_window_restore_flags() -> StateFlags {
main_window_geometry_state_flags()
}

#[cfg(not(target_os = "windows"))]
fn main_window_restore_flags() -> StateFlags {
main_window_state_flags()
}

fn persist_main_window_state(app: &tauri::AppHandle, reason: &str) -> Result<(), String> {
persist_main_window_state_with_flags(app, reason, main_window_state_flags())
}

fn persist_main_window_geometry_state(app: &tauri::AppHandle, reason: &str) -> Result<(), String> {
persist_main_window_state_with_flags(app, reason, main_window_geometry_state_flags())
}

fn persist_main_window_state_with_flags(
app: &tauri::AppHandle,
reason: &str,
flags: StateFlags,
) -> Result<(), String> {
let result = app
.save_window_state(flags)
.map_err(|error| error.to_string());
if let Err(error) = &result {
log::warn!(
"Failed to save main window state: reason={}, error={}",
reason,
error
);
return result;
}

#[cfg(target_os = "windows")]
if flags.contains(StateFlags::MAXIMIZED) {
window_state_support::correct_saved_main_window_state(app);
}

Ok(())
}

pub(crate) fn save_main_window_state(app: &tauri::AppHandle, reason: &str) {
if MAIN_WINDOW_USES_TRANSIENT_GEOMETRY.load(Ordering::SeqCst) {
log::debug!("Skipped saving transient main window geometry");
log::debug!(
"Skipped saving transient main window geometry: reason={}",
reason
);
return;
}

if let Err(error) = persist_main_window_state(app) {
log::warn!("Failed to save main window state: {}", error);
if let Err(error) = persist_main_window_state(app, reason) {
log::warn!(
"Failed to save main window state: reason={}, error={}",
reason,
error
);
}
}

Expand All @@ -345,7 +399,7 @@ pub(crate) fn set_main_window_transient_geometry(

// Capture the latest normal bounds before toolbar mode starts resizing
// the shared native window.
persist_main_window_state(app).map_err(|error| {
persist_main_window_state(app, "transient_geometry_enter_capture").map_err(|error| {
format!(
"Failed to save main window state before transient geometry: {}",
error
Expand All @@ -356,7 +410,7 @@ pub(crate) fn set_main_window_transient_geometry(
}

MAIN_WINDOW_USES_TRANSIENT_GEOMETRY.store(false, Ordering::SeqCst);
persist_main_window_state(app).map_err(|error| {
persist_main_window_state(app, "transient_geometry_exit_persist").map_err(|error| {
format!(
"Failed to save restored main window state after transient geometry: {}",
error
Expand All @@ -368,11 +422,18 @@ fn has_standard_main_window_size(width: f64, height: f64) -> bool {
width >= MAIN_WINDOW_MIN_WIDTH && height >= MAIN_WINDOW_MIN_HEIGHT
}

pub(crate) fn restore_main_window_state(window: &tauri::WebviewWindow) {
if let Err(error) = window.restore_state(main_window_state_flags()) {
pub(crate) fn restore_main_window_state(window: &tauri::WebviewWindow) -> bool {
if let Err(error) = window.restore_state(main_window_restore_flags()) {
log::warn!("Failed to restore main window state: {}", error);
}

#[cfg(target_os = "windows")]
let reapply_maximized =
window_state_support::read_persisted_main_maximized(window.app_handle()).unwrap_or(false);

#[cfg(not(target_os = "windows"))]
let reapply_maximized = false;

let is_maximized = window.is_maximized().unwrap_or(false);
let is_fullscreen = window.is_fullscreen().unwrap_or(false);
if !is_maximized && !is_fullscreen {
Expand Down Expand Up @@ -402,7 +463,10 @@ pub(crate) fn restore_main_window_state(window: &tauri::WebviewWindow) {
log::warn!("Failed to center reset main window: {}", error);
}
if resize_succeeded {
if let Err(error) = persist_main_window_state(window.app_handle()) {
if let Err(error) = persist_main_window_geometry_state(
window.app_handle(),
"startup_geometry_repair",
) {
log::warn!("Failed to persist repaired main window state: {}", error);
}
}
Expand All @@ -423,11 +487,17 @@ pub(crate) fn restore_main_window_state(window: &tauri::WebviewWindow) {
))) {
log::warn!("Failed to set main window minimum size: {}", error);
}

reapply_maximized
}

#[cfg(test)]
mod main_window_geometry_tests {
use super::has_standard_main_window_size;
use super::{
has_standard_main_window_size, main_window_geometry_state_flags, main_window_restore_flags,
main_window_state_flags,
};
use tauri_plugin_window_state::StateFlags;

#[test]
fn floating_toolbar_sizes_are_not_valid_main_window_sizes() {
Expand All @@ -439,6 +509,24 @@ mod main_window_geometry_tests {
fn default_client_size_is_a_valid_main_window_size() {
assert!(has_standard_main_window_size(1200.0, 800.0));
}

#[test]
fn geometry_saves_do_not_overwrite_maximized_state() {
assert!(!main_window_geometry_state_flags().contains(StateFlags::MAXIMIZED));
assert!(main_window_state_flags().contains(StateFlags::MAXIMIZED));
}

#[cfg(target_os = "windows")]
#[test]
fn windows_restore_defers_maximized_state_until_after_show() {
assert!(!main_window_restore_flags().contains(StateFlags::MAXIMIZED));
}

#[cfg(not(target_os = "windows"))]
#[test]
fn non_windows_restore_keeps_plugin_maximized_behavior() {
assert!(main_window_restore_flags().contains(StateFlags::MAXIMIZED));
}
}

#[tauri::command]
Expand Down Expand Up @@ -1177,7 +1265,7 @@ pub async fn run() {
if window.label() == "main"
&& matches!(event, tauri::WindowEvent::CloseRequested { .. })
{
save_main_window_state(window.app_handle());
save_main_window_state(window.app_handle(), "close_requested");
}

if let tauri::WindowEvent::CloseRequested { api: _api, .. } = event {
Expand Down Expand Up @@ -1907,7 +1995,7 @@ pub async fn run() {
app.run(|_app_handle, event| match event {
tauri::RunEvent::ExitRequested { .. } | tauri::RunEvent::Exit => {
crash_diagnostics::mark_clean_shutdown("tauri_run_exit");
save_main_window_state(_app_handle);
save_main_window_state(_app_handle, "tauri_run_exit");
perform_process_exit_cleanup();
}
#[cfg(target_os = "macos")]
Expand Down
2 changes: 1 addition & 1 deletion src/apps/desktop/src/tray.rs
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ pub fn setup_tray(
} else if id == "quit" {
log::info!("Quit requested from tray menu");
crate::crash_diagnostics::mark_clean_shutdown("tray_quit");
crate::save_main_window_state(app);
crate::save_main_window_state(app, "tray_quit");
crate::perform_process_exit_cleanup();
app.exit(0);
} else if id == "toggle_desktop_pet" {
Expand Down
6 changes: 3 additions & 3 deletions src/apps/desktop/src/webview_recovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ mod windows {
fn request_automatic_restart(app: &tauri::AppHandle) {
log::warn!("Requesting controlled application restart for WebView2 recovery");
crate::crash_diagnostics::mark_clean_shutdown("webview_recovery_restart");
crate::save_main_window_state(app);
crate::save_main_window_state(app, "webview_recovery_restart");
crate::perform_process_exit_cleanup();
app.request_restart();
}
Expand All @@ -266,7 +266,7 @@ mod windows {
}
_ => {
crate::crash_diagnostics::mark_clean_shutdown("webview_recovery_exit");
crate::save_main_window_state(&app);
crate::save_main_window_state(&app, "webview_recovery_exit_dialog");
crate::perform_process_exit_cleanup();
app.exit(1);
}
Expand All @@ -275,7 +275,7 @@ mod windows {

fn request_user_restart(app: &tauri::AppHandle) {
crate::crash_diagnostics::mark_clean_shutdown("webview_recovery_user_restart");
crate::save_main_window_state(app);
crate::save_main_window_state(app, "webview_recovery_user_restart");
crate::perform_process_exit_cleanup();
app.request_restart();
}
Expand Down
Loading
Loading