mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-07-05 04:21:50 +02:00
219 lines
6.9 KiB
Rust
219 lines
6.9 KiB
Rust
use log::{debug, warn};
|
|
use serde::{Deserialize, Serialize};
|
|
use std::collections::HashMap;
|
|
use std::fs;
|
|
use std::path::PathBuf;
|
|
use std::sync::atomic::{AtomicU64, Ordering};
|
|
use std::sync::{Arc, Mutex};
|
|
use std::time::Duration;
|
|
use tauri::{AppHandle, Manager, Monitor, Runtime, WebviewWindow, WindowEvent};
|
|
|
|
const WINDOW_STATE_FILE: &str = "window-state.json";
|
|
const SAVE_DEBOUNCE: Duration = Duration::from_millis(1000);
|
|
static WINDOW_STATE_FILE_LOCK: Mutex<()> = Mutex::new(());
|
|
|
|
#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize)]
|
|
struct WindowState {
|
|
width: f64,
|
|
height: f64,
|
|
x: f64,
|
|
y: f64,
|
|
maximized: bool,
|
|
}
|
|
|
|
impl WindowState {
|
|
fn has_size(self) -> bool {
|
|
self.width > 0.0 && self.height > 0.0
|
|
}
|
|
|
|
fn has_position(self) -> bool {
|
|
self.x.is_finite() && self.y.is_finite()
|
|
}
|
|
}
|
|
|
|
pub fn apply_saved_state<R: Runtime>(
|
|
app_handle: &AppHandle<R>,
|
|
state_key: &str,
|
|
inner_size: &mut Option<(f64, f64)>,
|
|
position: &mut Option<(f64, f64)>,
|
|
maximized: &mut bool,
|
|
restore_position: bool,
|
|
) {
|
|
let Some(state) = read_window_state(app_handle, state_key) else {
|
|
debug!("No saved window state for {state_key}");
|
|
return;
|
|
};
|
|
|
|
debug!(
|
|
"Applying saved window state for {state_key}: width={} height={} x={} y={} maximized={} restore_position={restore_position}",
|
|
state.width, state.height, state.x, state.y, state.maximized
|
|
);
|
|
|
|
if state.has_size() {
|
|
*inner_size = Some((state.width, state.height));
|
|
}
|
|
|
|
if restore_position && state.has_position() {
|
|
if is_position_visible(app_handle, state) {
|
|
*position = Some((state.x, state.y));
|
|
} else {
|
|
debug!("Ignoring saved window position for {state_key} because it is off-screen");
|
|
}
|
|
}
|
|
|
|
*maximized = state.maximized;
|
|
}
|
|
|
|
pub fn track_window<R: Runtime>(window: &WebviewWindow<R>, state_key: &str) {
|
|
let state_key = state_key.to_string();
|
|
let save_generation = Arc::new(AtomicU64::new(0));
|
|
let tracked_window = window.clone();
|
|
|
|
window.clone().on_window_event(move |event| match event {
|
|
WindowEvent::Moved(_) | WindowEvent::Resized(_) => {
|
|
schedule_save(tracked_window.clone(), state_key.clone(), save_generation.clone());
|
|
}
|
|
WindowEvent::CloseRequested { .. } => {
|
|
save_generation.fetch_add(1, Ordering::Relaxed);
|
|
if let Err(e) = save_window_state(&tracked_window, &state_key) {
|
|
warn!("Failed to save window state for {state_key}: {e}");
|
|
}
|
|
}
|
|
_ => {}
|
|
});
|
|
}
|
|
|
|
fn schedule_save<R: Runtime>(
|
|
window: WebviewWindow<R>,
|
|
state_key: String,
|
|
save_generation: Arc<AtomicU64>,
|
|
) {
|
|
let generation = save_generation.fetch_add(1, Ordering::Relaxed) + 1;
|
|
let window_for_dispatch = window.clone();
|
|
|
|
std::thread::spawn(move || {
|
|
std::thread::sleep(SAVE_DEBOUNCE);
|
|
|
|
if save_generation.load(Ordering::Relaxed) != generation {
|
|
return;
|
|
}
|
|
|
|
let state_key_for_save = state_key.clone();
|
|
let window_for_save = window.clone();
|
|
if let Err(e) = window_for_dispatch.run_on_main_thread(move || {
|
|
if let Err(e) = save_window_state(&window_for_save, &state_key_for_save) {
|
|
warn!("Failed to save window state for {state_key_for_save}: {e}");
|
|
}
|
|
}) {
|
|
debug!("Failed to dispatch debounced window state save for {state_key}: {e}");
|
|
}
|
|
});
|
|
}
|
|
|
|
fn save_window_state<R: Runtime>(window: &WebviewWindow<R>, state_key: &str) -> tauri::Result<()> {
|
|
let app_handle = window.app_handle();
|
|
let state_path = window_state_path(&app_handle)?;
|
|
let _lock = WINDOW_STATE_FILE_LOCK.lock().unwrap();
|
|
let mut states = read_window_states(&state_path);
|
|
let mut state = states.get(state_key).copied().unwrap_or_default();
|
|
|
|
let maximized = window.is_maximized().unwrap_or(false);
|
|
let minimized = window.is_minimized().unwrap_or(false);
|
|
let scale_factor = window.scale_factor().unwrap_or(1.0);
|
|
|
|
if !minimized && (!maximized || !state.has_size()) {
|
|
let size = window.inner_size()?.to_logical::<f64>(scale_factor);
|
|
if size.width > 0.0 && size.height > 0.0 {
|
|
state.width = size.width;
|
|
state.height = size.height;
|
|
}
|
|
}
|
|
|
|
if !minimized && (!maximized || !state.has_position()) {
|
|
let position = window.outer_position()?.to_logical::<f64>(scale_factor);
|
|
state.x = position.x;
|
|
state.y = position.y;
|
|
}
|
|
|
|
state.maximized = maximized;
|
|
states.insert(state_key.to_string(), state);
|
|
write_window_states(&state_path, &states)?;
|
|
debug!(
|
|
"Saved window state for {state_key} to {}: width={} height={} x={} y={} maximized={} minimized={minimized}",
|
|
state_path.display(),
|
|
state.width,
|
|
state.height,
|
|
state.x,
|
|
state.y,
|
|
state.maximized
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
fn read_window_state<R: Runtime>(
|
|
app_handle: &AppHandle<R>,
|
|
state_key: &str,
|
|
) -> Option<WindowState> {
|
|
let state_path = window_state_path(app_handle).ok()?;
|
|
debug!("Reading window state for {state_key} from {}", state_path.display());
|
|
read_window_states(&state_path).get(state_key).copied()
|
|
}
|
|
|
|
fn window_state_path<R: Runtime>(app_handle: &AppHandle<R>) -> tauri::Result<PathBuf> {
|
|
Ok(app_handle.path().app_config_dir()?.join(WINDOW_STATE_FILE))
|
|
}
|
|
|
|
fn read_window_states(state_path: &PathBuf) -> HashMap<String, WindowState> {
|
|
let Ok(bytes) = fs::read(state_path) else {
|
|
return HashMap::new();
|
|
};
|
|
|
|
match serde_json::from_slice(&bytes) {
|
|
Ok(states) => states,
|
|
Err(e) => {
|
|
warn!("Failed to read window state {}: {e}", state_path.display());
|
|
HashMap::new()
|
|
}
|
|
}
|
|
}
|
|
|
|
fn write_window_states(
|
|
state_path: &PathBuf,
|
|
states: &HashMap<String, WindowState>,
|
|
) -> tauri::Result<()> {
|
|
if let Some(parent) = state_path.parent() {
|
|
fs::create_dir_all(parent)?;
|
|
}
|
|
|
|
fs::write(state_path, serde_json::to_vec_pretty(states)?)?;
|
|
Ok(())
|
|
}
|
|
|
|
fn is_position_visible<R: Runtime>(app_handle: &AppHandle<R>, state: WindowState) -> bool {
|
|
let Ok(monitors) = app_handle.available_monitors() else {
|
|
return true;
|
|
};
|
|
|
|
monitors.into_iter().any(|monitor| monitor_intersects_window(&monitor, state))
|
|
}
|
|
|
|
fn monitor_intersects_window(monitor: &Monitor, state: WindowState) -> bool {
|
|
let scale_factor = monitor.scale_factor();
|
|
let position = monitor.position().to_logical::<f64>(scale_factor);
|
|
let size = monitor.size().to_logical::<f64>(scale_factor);
|
|
|
|
let left = position.x;
|
|
let right = position.x + size.width;
|
|
let top = position.y;
|
|
let bottom = position.y + size.height;
|
|
|
|
[
|
|
(state.x, state.y),
|
|
(state.x + state.width, state.y),
|
|
(state.x, state.y + state.height),
|
|
(state.x + state.width, state.y + state.height),
|
|
]
|
|
.into_iter()
|
|
.any(|(x, y)| x >= left && x < right && y >= top && y < bottom)
|
|
}
|