168 lines
5.0 KiB
Rust
168 lines
5.0 KiB
Rust
use anyhow::{anyhow, Result};
|
|
use serde::{Deserialize, Serialize};
|
|
use std::{
|
|
path::PathBuf,
|
|
sync::{Arc, Mutex},
|
|
};
|
|
use tauri::{LogicalPosition, LogicalSize, WebviewWindow, Window};
|
|
use tokio::fs as async_fs;
|
|
|
|
const WINDOW_STATE_FILE: &str = "window-state.json";
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct WindowState {
|
|
pub width: f64,
|
|
pub height: f64,
|
|
pub x: f64,
|
|
pub y: f64,
|
|
pub is_maximized: bool,
|
|
}
|
|
|
|
impl Default for WindowState {
|
|
fn default() -> Self {
|
|
Self {
|
|
width: 1280.0,
|
|
height: 800.0,
|
|
x: 0.0,
|
|
y: 0.0,
|
|
is_maximized: false,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize)]
|
|
struct WindowStateFile {
|
|
#[serde(rename = "windowState")]
|
|
pub window_state: WindowState,
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub struct WindowStateManager {
|
|
inner: Arc<Mutex<WindowState>>,
|
|
}
|
|
|
|
impl WindowStateManager {
|
|
pub fn new(initial: WindowState) -> Self {
|
|
Self {
|
|
inner: Arc::new(Mutex::new(initial)),
|
|
}
|
|
}
|
|
|
|
pub fn snapshot(&self) -> WindowState {
|
|
self.inner.lock().expect("window state poisoned").clone()
|
|
}
|
|
|
|
pub fn update_position(&self, x: f64, y: f64, is_maximized: bool) {
|
|
if is_maximized {
|
|
return;
|
|
}
|
|
if let Ok(mut state) = self.inner.lock() {
|
|
if !state.is_maximized {
|
|
state.x = x;
|
|
state.y = y;
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn update_size(&self, width: f64, height: f64, is_maximized: bool) {
|
|
if let Ok(mut state) = self.inner.lock() {
|
|
if !is_maximized {
|
|
state.width = width;
|
|
state.height = height;
|
|
}
|
|
state.is_maximized = is_maximized;
|
|
}
|
|
}
|
|
}
|
|
|
|
fn state_file_path() -> Result<PathBuf> {
|
|
let mut path = dirs::home_dir().ok_or_else(|| anyhow!("No home directory"))?;
|
|
path.push(".config");
|
|
path.push("openchamber");
|
|
path.push(WINDOW_STATE_FILE);
|
|
Ok(path)
|
|
}
|
|
|
|
pub async fn load_window_state() -> Result<Option<WindowState>> {
|
|
let path = state_file_path()?;
|
|
match async_fs::read(&path).await {
|
|
Ok(bytes) => {
|
|
let file: WindowStateFile = serde_json::from_slice(&bytes)?;
|
|
Ok(Some(file.window_state))
|
|
}
|
|
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None),
|
|
Err(err) => Err(err.into()),
|
|
}
|
|
}
|
|
|
|
pub async fn save_window_state(state: &WindowState) -> Result<()> {
|
|
let path = state_file_path()?;
|
|
if let Some(parent) = path.parent() {
|
|
async_fs::create_dir_all(parent).await?;
|
|
}
|
|
let payload = WindowStateFile {
|
|
window_state: state.clone(),
|
|
};
|
|
let data = serde_json::to_vec_pretty(&payload)?;
|
|
async_fs::write(&path, data).await?;
|
|
Ok(())
|
|
}
|
|
|
|
pub fn apply_window_state(window: &WebviewWindow, state: &WindowState) -> Result<()> {
|
|
let mut normalized = state.clone();
|
|
clamp_to_visible_region(window, &mut normalized);
|
|
|
|
if normalized.width > 0.0 && normalized.height > 0.0 {
|
|
let _ = window.set_size(LogicalSize::new(normalized.width, normalized.height));
|
|
}
|
|
let _ = window.set_position(LogicalPosition::new(normalized.x, normalized.y));
|
|
if state.is_maximized {
|
|
let _ = window.maximize();
|
|
} else {
|
|
let _ = window.unmaximize();
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn persist_window_state(window: &Window, manager: &WindowStateManager) -> Result<()> {
|
|
let mut snapshot = manager.snapshot();
|
|
let is_maximized = window.is_maximized().unwrap_or(snapshot.is_maximized);
|
|
snapshot.is_maximized = is_maximized;
|
|
|
|
if !is_maximized {
|
|
let scale_factor = window.scale_factor().unwrap_or(1.0);
|
|
if let Ok(size) = window.outer_size() {
|
|
let logical: LogicalSize<f64> = size.to_logical(scale_factor);
|
|
snapshot.width = logical.width.max(200.0);
|
|
snapshot.height = logical.height.max(200.0);
|
|
}
|
|
if let Ok(position) = window.outer_position() {
|
|
let logical: LogicalPosition<f64> = position.to_logical(scale_factor);
|
|
snapshot.x = logical.x;
|
|
snapshot.y = logical.y;
|
|
}
|
|
}
|
|
|
|
save_window_state(&snapshot).await
|
|
}
|
|
|
|
fn clamp_to_visible_region(window: &WebviewWindow, state: &mut WindowState) {
|
|
let monitor = match window.current_monitor() {
|
|
Ok(Some(monitor)) => monitor,
|
|
_ => return,
|
|
};
|
|
let scale_factor = monitor.scale_factor();
|
|
let monitor_size: LogicalSize<f64> = monitor.size().to_logical(scale_factor);
|
|
let monitor_position: LogicalPosition<f64> = monitor.position().to_logical(scale_factor);
|
|
|
|
state.width = state.width.clamp(400.0, monitor_size.width);
|
|
state.height = state.height.clamp(300.0, monitor_size.height);
|
|
|
|
let max_x = monitor_position.x + (monitor_size.width - state.width).max(0.0);
|
|
let max_y = monitor_position.y + (monitor_size.height - state.height).max(0.0);
|
|
|
|
state.x = state.x.clamp(monitor_position.x, max_x);
|
|
state.y = state.y.clamp(monitor_position.y, max_y);
|
|
}
|