diff --git a/package-lock.json b/package-lock.json index 175ca61..55b1a97 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,7 +23,6 @@ "@tauri-apps/plugin-fs": "^2.4.5", "@tauri-apps/plugin-opener": "^2", "@tauri-apps/plugin-store": "^2.4.2", - "@tauri-apps/plugin-window-state": "^2.4.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", @@ -4648,15 +4647,6 @@ "@tauri-apps/api": "^2.8.0" } }, - "node_modules/@tauri-apps/plugin-window-state": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-window-state/-/plugin-window-state-2.4.1.tgz", - "integrity": "sha512-OuvdrzyY8Q5Dbzpj+GcrnV1iCeoZbcFdzMjanZMMcAEUNy/6PH5pxZPXpaZLOR7whlzXiuzx0L9EKZbH7zpdRw==", - "license": "MIT OR Apache-2.0", - "dependencies": { - "@tauri-apps/api": "^2.8.0" - } - }, "node_modules/@tiptap/core": { "version": "3.20.4", "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.20.4.tgz", diff --git a/package.json b/package.json index f23220b..7eefe33 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,6 @@ "@tauri-apps/plugin-fs": "^2.4.5", "@tauri-apps/plugin-opener": "^2", "@tauri-apps/plugin-store": "^2.4.2", - "@tauri-apps/plugin-window-state": "^2.4.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 501a57a..1ba8b87 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -3445,7 +3445,6 @@ dependencies = [ "tauri-plugin-fs", "tauri-plugin-opener", "tauri-plugin-store", - "tauri-plugin-window-state", "uuid", ] @@ -4207,21 +4206,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "tauri-plugin-window-state" -version = "2.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73736611e14142408d15353e21e3cca2f12a3cfb523ad0ce85999b6d2ef1a704" -dependencies = [ - "bitflags 2.11.0", - "log", - "serde", - "serde_json", - "tauri", - "tauri-plugin", - "thiserror 2.0.18", -] - [[package]] name = "tauri-runtime" version = "2.10.1" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index dc0439a..a0ab8ce 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -25,4 +25,3 @@ chrono = { version = "0.4", features = ["serde"] } reqwest = { version = "0.12", features = ["rustls-tls"], default-features = false } scraper = "0.23" tauri-plugin-fs = "2" -tauri-plugin-window-state = "2" diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index c2f29cf..0fb4d95 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -11,7 +11,6 @@ "core:window:allow-set-maximizable", "store:default", "dialog:default", - "window-state:default", "fs:default", { "identifier": "fs:allow-write-file", diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 58915d9..7da0049 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -2,9 +2,9 @@ mod db; mod file_io; mod groups; mod link_preview; +mod window; use tauri::Manager; -use tauri_plugin_window_state::StateFlags; /// Initializes and runs the Tauri application. /// @@ -14,14 +14,16 @@ use tauri_plugin_window_state::StateFlags; /// - **store** — Persistent key-value config storage. /// - **dialog** — Native file/message dialogs. /// - **fs** — Scoped filesystem access. -/// - **window-state** — Saves and optionally restores window position and -/// size across launches (initial restore is skipped for the `"main"` -/// window so the frontend can decide via a user setting). /// -/// During setup the SQLite database is initialized and a +/// During setup the SQLite database is initialized, a /// [`LinkPreviewCache`](link_preview::LinkPreviewCache) is registered as -/// managed state. All note CRUD and utility commands are then registered -/// via the invoke handler. +/// managed state, and the main window is created via +/// [`window::create_main_window`]. Window position and size are restored +/// from `config.json` when the user has enabled the setting; otherwise +/// the window opens at the default 1200×800 dimensions. +/// +/// On window close the current geometry is persisted to `config.json` +/// so it can be restored on the next launch. /// /// # Panics /// @@ -37,19 +39,19 @@ pub fn run() { .plugin(tauri_plugin_store::Builder::default().build()) .plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_fs::init()) - .plugin( - tauri_plugin_window_state::Builder::new() - .with_state_flags(StateFlags::POSITION | StateFlags::SIZE) - .skip_initial_state("main") - .build(), - ) .setup(|app| { db::init_db(app.handle())?; app.manage(link_preview::LinkPreviewCache(std::sync::Mutex::new( std::collections::HashMap::new(), ))); + window::create_main_window(app.handle())?; Ok(()) }) + .on_window_event(|window, event| { + if let tauri::WindowEvent::CloseRequested { .. } = event { + window::save_window_geometry(window.app_handle()); + } + }) .invoke_handler(tauri::generate_handler![ db::get_note, db::list_notes, diff --git a/src-tauri/src/window.rs b/src-tauri/src/window.rs new file mode 100644 index 0000000..46fe023 --- /dev/null +++ b/src-tauri/src/window.rs @@ -0,0 +1,168 @@ +//! Window creation and geometry persistence. +//! +//! This module is responsible for creating the main application window +//! and saving/restoring its position and size across launches. The +//! geometry is stored in `config.json` via the Tauri store plugin and +//! only restored when the user has enabled the +//! `windowStateRestoreEnabled` setting. + +use serde::{Deserialize, Serialize}; +use tauri::{AppHandle, Manager, WebviewUrl, WebviewWindowBuilder}; +use tauri_plugin_store::StoreExt; + +/// Default window width in logical pixels when no saved geometry is restored. +const DEFAULT_WIDTH: f64 = 1200.0; + +/// Default window height in logical pixels when no saved geometry is restored. +const DEFAULT_HEIGHT: f64 = 800.0; + +/// Title displayed in the window title bar at creation time. +const WINDOW_TITLE: &str = "Scripta"; + +/// Store key under which the serialized [`WindowGeometry`] is persisted in `config.json`. +const GEOMETRY_STORE_KEY: &str = "windowGeometry"; + +/// Store key for the boolean flag that controls whether saved geometry +/// is applied on startup. Mirrors the frontend constant in +/// `windowStateConfig.ts`. +const RESTORE_ENABLED_KEY: &str = "windowStateRestoreEnabled"; + +/// Saved window geometry in logical (DPI-independent) coordinates. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +struct WindowGeometry { + x: f64, + y: f64, + width: f64, + height: f64, +} + +/// Creates the main application window. +/// +/// Reads `config.json` to determine whether to restore the previous +/// window position and size. When restoration is enabled and valid +/// saved geometry exists within the visible monitor area, the window +/// opens at the saved coordinates. Otherwise it opens at the default +/// 1200×800 size, centred by the OS. +/// +/// The window is created with `resizable(false)` and +/// `maximizable(false)` so the splash screen cannot be resized. +/// The frontend unlocks these constraints once the splash has faded +/// out. +pub fn create_main_window(app: &AppHandle) -> Result<(), Box> { + let geometry = read_saved_geometry(app); + + let mut builder = WebviewWindowBuilder::new(app, "main", WebviewUrl::default()) + .title(WINDOW_TITLE) + .resizable(false) + .maximizable(false); + + match geometry { + Some(geo) => { + builder = builder + .inner_size(geo.width, geo.height) + .position(geo.x, geo.y); + } + None => { + builder = builder.inner_size(DEFAULT_WIDTH, DEFAULT_HEIGHT); + // Position is omitted so the OS centres the window. + } + } + + builder.build()?; + Ok(()) +} + +/// Saves the current window geometry (logical coordinates) to `config.json`. +/// +/// Called from the `CloseRequested` window event handler so the +/// position and size are persisted for the next launch. +pub fn save_window_geometry(app: &AppHandle) { + let Some(window) = app.get_webview_window("main") else { + return; + }; + + let scale = match window.scale_factor() { + Ok(s) => s, + Err(_) => return, + }; + let Ok(pos) = window.outer_position() else { + return; + }; + let Ok(size) = window.inner_size() else { + return; + }; + + let geo = WindowGeometry { + x: pos.x as f64 / scale, + y: pos.y as f64 / scale, + width: size.width as f64 / scale, + height: size.height as f64 / scale, + }; + + if let Ok(store) = app.store("config.json") { + store.set( + GEOMETRY_STORE_KEY, + serde_json::to_value(&geo).unwrap_or_default(), + ); + } +} + +/// Reads the saved geometry from `config.json` if restoration is +/// enabled and the saved position is within a visible monitor. +/// +/// Returns `None` when: +/// - The `windowStateRestoreEnabled` setting is `false`. +/// - No saved geometry exists. +/// - The saved position is outside all connected monitors (e.g. an +/// external monitor was disconnected). +fn read_saved_geometry(app: &AppHandle) -> Option { + let store = app.store("config.json").ok()?; + + let restore_enabled = store + .get(RESTORE_ENABLED_KEY) + .and_then(|v| v.as_bool()) + .unwrap_or(true); + + if !restore_enabled { + return None; + } + + let raw = store.get(GEOMETRY_STORE_KEY)?; + let geo: WindowGeometry = serde_json::from_value(raw).ok()?; + + if geo.width <= 0.0 || geo.height <= 0.0 { + return None; + } + + if is_position_on_screen(app, geo.x, geo.y) { + Some(geo) + } else { + None + } +} + +/// Returns `true` when the given logical position falls within at +/// least one connected monitor. +fn is_position_on_screen(app: &AppHandle, x: f64, y: f64) -> bool { + let monitors = match app.available_monitors() { + Ok(m) => m, + Err(_) => return true, // assume on-screen when detection fails + }; + + if monitors.is_empty() { + return true; + } + + monitors.iter().any(|m| { + let pos = m.position(); + let size = m.size(); + let scale = m.scale_factor(); + let mx = pos.x as f64 / scale; + let my = pos.y as f64 / scale; + let mw = size.width as f64 / scale; + let mh = size.height as f64 / scale; + + x >= mx && x < mx + mw && y >= my && y < my + mh + }) +} diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 699f61d..f5bc026 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -10,16 +10,7 @@ "frontendDist": "../dist" }, "app": { - "windows": [ - { - "title": "Scripta", - "width": 1200, - "height": 800, - "resizable": false, - "maximizable": false, - "dragDropEnabled": false - } - ], + "windows": [], "security": { "csp": "default-src 'self'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; script-src 'self'; img-src 'self' data: asset: https://asset.localhost", "assetProtocol": { diff --git a/src/app/providers/store-provider.tsx b/src/app/providers/store-provider.tsx index e3dc764..193a8f9 100644 --- a/src/app/providers/store-provider.tsx +++ b/src/app/providers/store-provider.tsx @@ -1,8 +1,4 @@ import { LazyStore } from '@tauri-apps/plugin-store' -import { - restoreStateCurrent, - StateFlags, -} from '@tauri-apps/plugin-window-state' import { createContext, type ReactNode, use, useContext } from 'react' import type { Theme } from '@/app/providers/theme-provider' import { @@ -90,13 +86,6 @@ export const storeInitPromise = Promise.all([ if (storedTitlePrefix != null) { configDefaults.windowTitlePrefixEnabled = storedTitlePrefix } - if (restoreEnabled) { - try { - await restoreStateCurrent(StateFlags.POSITION | StateFlags.SIZE) - } catch (err) { - console.error('Failed to restore window state:', err) - } - } }) /** diff --git a/src/features/editor/index.ts b/src/features/editor/index.ts index 67e7f95..a8c5d23 100644 --- a/src/features/editor/index.ts +++ b/src/features/editor/index.ts @@ -26,6 +26,7 @@ export { useCursorCentering } from './hooks/useCursorCentering' export { useEditorFontSize } from './hooks/useEditorFontSize' export type { UseSearchReplaceReturn } from './hooks/useSearchReplace' export { useSearchReplace } from './hooks/useSearchReplace' +export { checklistSplitFixExtension } from './lib/checklistSplitFix' export { DEFAULT_BLOCKS, DEFAULT_CONTENT, extractTitle } from './lib/constants' export { cursorCenteringExtension } from './lib/cursorCentering' export { cursorVimKeysExtension } from './lib/cursorVimKeys' @@ -45,7 +46,6 @@ export { exportToMarkdown, fixBlockNoteTableExport, } from './lib/markdown-export' -export { checklistSplitFixExtension } from './lib/checklistSplitFix' export { rangeCheckToggleExtension } from './lib/rangeCheckToggle' export { searchExtension } from './lib/searchExtension' export { slashMenuEmacsKeysExtension } from './lib/slashMenuEmacsKeys' diff --git a/src/features/settings/lib/windowStateConfig.ts b/src/features/settings/lib/windowStateConfig.ts index ae4eb48..1048ff7 100644 --- a/src/features/settings/lib/windowStateConfig.ts +++ b/src/features/settings/lib/windowStateConfig.ts @@ -4,13 +4,13 @@ export const DEFAULT_WINDOW_STATE_RESTORE = true /** * Store key name for persistence via `configStore`. * - * When `true`, the app restores the last-saved window position and size - * on startup. When `false`, the window opens at the default 1200×800 - * dimensions (as specified in `tauri.conf.json`). + * When `true`, the Rust backend restores the last-saved window position + * and size before creating the main window frame. When `false`, the + * window opens at the default 1200×800 dimensions. * * @remarks - * The window-state plugin always saves position/size on close regardless - * of this setting. This key only controls whether restore is performed - * at startup. + * Window geometry is always saved to `config.json` on close regardless + * of this setting. This key only controls whether the saved geometry is + * applied when the window is created at startup. */ export const WINDOW_STATE_STORE_KEY = 'windowStateRestoreEnabled' as const