Skip to content
Open
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
16 changes: 16 additions & 0 deletions src-tauri/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,15 @@ pub fn settings_set(
settings::save_setting(&conn, "min_to_tray", "false").map_err(|e| e.to_string())?;
settings::save_setting(&conn, "min_to_tray_on_close", "false").map_err(|e| e.to_string())?;
}
// When remember_window_state is turned off, clear the stored coordinates
// immediately so the window reverts to OS-default position on next launch.
if key == "remember_window_state" && value == "false" {
conn.execute(
"DELETE FROM settings WHERE key IN ('window_x','window_y','window_width','window_height')",
[],
).map_err(|e| e.to_string())?;
log::debug!("[settings] window coordinates cleared (remember_window_state disabled)");
}
settings::load(&conn).map_err(|e| {
log::error!("[settings] failed to reload after save: {e}");
e.to_string()
Expand Down Expand Up @@ -247,6 +256,13 @@ pub fn settings_reset_defaults(
conn.execute("DELETE FROM settings", [])
.map_err(|e| e.to_string())?;
settings::seed_defaults(&conn).map_err(|e| e.to_string())?;
// Also remove any stored window coordinates so the window returns to
// OS-default position after a full reset (seed_defaults does not seed
// these keys since they have no meaningful default).
conn.execute(
"DELETE FROM settings WHERE key IN ('window_x','window_y','window_width','window_height')",
[],
).map_err(|e| e.to_string())?;
settings::load(&conn).map_err(|e| e.to_string())?
};

Expand Down
16 changes: 15 additions & 1 deletion src-tauri/src/db/migrations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,14 @@ const MIGRATION_6: &str = "
INSERT INTO schema_version VALUES (6);
";

/// Seeds the `remember_window_state` setting for users upgrading from a version
/// that did not have this feature. Defaults to 'false' so the feature is opt-in
/// and existing users are not surprised by a position change on first upgrade.
const MIGRATION_7: &str = "
INSERT OR IGNORE INTO settings (key, value) VALUES ('remember_window_state', 'false');
INSERT INTO schema_version VALUES (7);
";

/// Apply any pending migrations. Each migration is wrapped in a transaction
/// so a partial failure leaves the database unchanged.
pub fn run(conn: &Connection) -> Result<()> {
Expand Down Expand Up @@ -131,6 +139,12 @@ pub fn run(conn: &Connection) -> Result<()> {
log::info!("[db/migrations] MIGRATION_6 complete");
}

if version < 7 {
log::info!("[db/migrations] applying MIGRATION_7: seed remember_window_state setting");
conn.execute_batch(&format!("BEGIN; {MIGRATION_7} COMMIT;"))?;
log::info!("[db/migrations] MIGRATION_7 complete");
}

Ok(())
}

Expand Down Expand Up @@ -166,7 +180,7 @@ mod tests {
let v: i64 = conn
.query_row("SELECT MAX(version) FROM schema_version", [], |r| r.get(0))
.unwrap();
assert_eq!(v, 6);
assert_eq!(v, 7);
}

#[test]
Expand Down
120 changes: 81 additions & 39 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ use log::LevelFilter;
use tauri::Manager;
use tauri_plugin_log::{Builder as LogBuilder, RotationStrategy, Target, TargetKind};

use std::sync::atomic::{AtomicBool, Ordering};

use commands::{
accessibility_trusted,
tray_supported,
Expand Down Expand Up @@ -283,44 +285,59 @@ pub fn run() {
let _ = main_window.set_always_on_top(true);
}

// Restore saved window position/size if all four values are present and
// the saved rectangle still intersects at least one connected monitor.
if let (Some(wx), Some(wy), Some(ww), Some(wh)) = (
initial_settings.window_x,
initial_settings.window_y,
initial_settings.window_width,
initial_settings.window_height,
) {
let on_screen = app
.available_monitors()
.unwrap_or_default()
.into_iter()
.any(|m| {
let mp = m.position();
let ms = m.size();
let mx0 = mp.x as i64;
let my0 = mp.y as i64;
let mx1 = mx0 + ms.width as i64;
let my1 = my0 + ms.height as i64;
let wx0 = wx as i64;
let wy0 = wy as i64;
let wx1 = wx0 + ww as i64;
let wy1 = wy0 + wh as i64;
wx0 < mx1 && wx1 > mx0 && wy0 < my1 && wy1 > my0
});
if on_screen {
let _ = main_window.set_position(tauri::PhysicalPosition::new(wx, wy));
let _ = main_window.set_size(tauri::PhysicalSize::new(ww, wh));
// Startup-suppression flag: blocks the Moved/Resized window event
// handlers from writing to the DB until after the restore phase
// completes. On Windows and Linux the OS fires those events when the
// window is first shown, which would otherwise overwrite any
// previously-saved position with the tauri.conf.json defaults.
let window_ready = Arc::new(AtomicBool::new(false));

// Restore saved window position/size if all four values are present,
// remember_window_state is enabled, and the saved rectangle still
// intersects at least one connected monitor.
if initial_settings.remember_window_state {
if let (Some(wx), Some(wy), Some(ww), Some(wh)) = (
initial_settings.window_x,
initial_settings.window_y,
initial_settings.window_width,
initial_settings.window_height,
) {
let on_screen = app
.available_monitors()
.unwrap_or_default()
.into_iter()
.any(|m| {
let mp = m.position();
let ms = m.size();
let mx0 = mp.x as i64;
let my0 = mp.y as i64;
let mx1 = mx0 + ms.width as i64;
let my1 = my0 + ms.height as i64;
let wx0 = wx as i64;
let wy0 = wy as i64;
let wx1 = wx0 + ww as i64;
let wy1 = wy0 + wh as i64;
wx0 < mx1 && wx1 > mx0 && wy0 < my1 && wy1 > my0
});
if on_screen {
let _ = main_window.set_position(tauri::PhysicalPosition::new(wx, wy));
let _ = main_window.set_size(tauri::PhysicalSize::new(ww, wh));
}
}
}

// Allow the window event handlers to start persisting geometry now
// that the restore phase is complete.
window_ready.store(true, Ordering::SeqCst);

// Persist window position/size on move and resize, and close child windows
// when the main window is truly closed (not hidden to tray).
let db_for_close = db.clone();
let win_for_close = main_window.clone();
let app_for_close = app.handle().clone();
let db_for_pos = db.clone();
let win_for_pos = main_window.clone();
let window_ready_for_event = Arc::clone(&window_ready);
main_window.on_window_event(move |event| {
match event {
tauri::WindowEvent::CloseRequested { api, .. } => {
Expand All @@ -343,23 +360,48 @@ pub fn run() {
}
}
tauri::WindowEvent::Moved(pos) => {
if let Ok(conn) = db_for_pos.lock() {
let _ = settings::save_setting(&conn, "window_x", &pos.x.to_string());
let _ = settings::save_setting(&conn, "window_y", &pos.y.to_string());
// Ignore events fired during the startup/restore phase to
// prevent the OS default position from overwriting saved values.
if !window_ready_for_event.load(Ordering::SeqCst) {
return;
}
}
tauri::WindowEvent::Resized(size) => {
if let Ok(conn) = db_for_pos.lock() {
let _ = settings::save_setting(&conn, "window_width", &size.width.to_string());
let _ = settings::save_setting(&conn, "window_height", &size.height.to_string());
// Also capture position, since some window managers shift the
// window origin when resizing.
if let Ok(pos) = win_for_pos.outer_position() {
let should_save = db_for_pos
.lock()
.ok()
.and_then(|conn| settings::load(&conn).ok())
.map(|s| s.remember_window_state)
.unwrap_or(false);
if should_save {
if let Ok(conn) = db_for_pos.lock() {
let _ = settings::save_setting(&conn, "window_x", &pos.x.to_string());
let _ = settings::save_setting(&conn, "window_y", &pos.y.to_string());
}
}
}
tauri::WindowEvent::Resized(size) => {
// Ignore events fired during the startup/restore phase.
if !window_ready_for_event.load(Ordering::SeqCst) {
return;
}
let should_save = db_for_pos
.lock()
.ok()
.and_then(|conn| settings::load(&conn).ok())
.map(|s| s.remember_window_state)
.unwrap_or(false);
if should_save {
if let Ok(conn) = db_for_pos.lock() {
let _ = settings::save_setting(&conn, "window_width", &size.width.to_string());
let _ = settings::save_setting(&conn, "window_height", &size.height.to_string());
// Also capture position, since some window managers shift the
// window origin when resizing.
if let Ok(pos) = win_for_pos.outer_position() {
let _ = settings::save_setting(&conn, "window_x", &pos.x.to_string());
let _ = settings::save_setting(&conn, "window_y", &pos.y.to_string());
}
}
}
}
_ => {}
}
});
Expand Down
1 change: 1 addition & 0 deletions src-tauri/src/settings/defaults.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,5 @@ pub const DEFAULTS: &[(&str, &str)] = &[
("local_shortcut_volume_up", "ArrowUp"),
("local_shortcut_mute", "m"),
("local_shortcut_fullscreen", "F11"),
("remember_window_state", "false"),
];
5 changes: 5 additions & 0 deletions src-tauri/src/settings/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ pub struct Settings {
pub window_width: Option<u32>,
/// Last known window height (physical pixels). `None` = use OS default.
pub window_height: Option<u32>,
/// When true, the window's size and position are saved on move/resize and
/// restored on the next launch.
pub remember_window_state: bool,
}

impl Default for Settings {
Expand Down Expand Up @@ -124,6 +127,7 @@ impl Default for Settings {
window_y: None,
window_width: None,
window_height: None,
remember_window_state: false,
}
}
}
Expand Down Expand Up @@ -249,6 +253,7 @@ pub fn load(conn: &Connection) -> Result<Settings> {
window_y: parse_opt_i32(&map, "window_y"),
window_width: parse_opt_u32(&map, "window_width"),
window_height: parse_opt_u32(&map, "window_height"),
remember_window_state: parse_bool(&map, "remember_window_state", d.remember_window_state),
})
}

Expand Down
6 changes: 6 additions & 0 deletions src/lib/components/settings/sections/SystemSection.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,12 @@
onclick={() => toggle('break_always_on_top', $settings.break_always_on_top)}
/>
{/if}
<SettingsToggle
label={m.system_toggle_remember_window()}
description={m.system_toggle_remember_window_desc()}
checked={$settings.remember_window_state}
onclick={() => toggle('remember_window_state', $settings.remember_window_state)}
/>

<div class="group-heading">{m.system_group_data()}</div>

Expand Down
5 changes: 5 additions & 0 deletions src/lib/stores/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ const defaults: Settings = {
local_shortcut_volume_up: 'ArrowUp',
local_shortcut_mute: 'm',
local_shortcut_fullscreen: 'F11',
window_x: null,
window_y: null,
window_width: null,
window_height: null,
remember_window_state: false,
};

export const settings = writable<Settings>(defaults);
5 changes: 5 additions & 0 deletions src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ export interface Settings {
local_shortcut_volume_up: string;
local_shortcut_mute: string;
local_shortcut_fullscreen: string;
window_x: number | null;
window_y: number | null;
window_width: number | null;
window_height: number | null;
remember_window_state: boolean;
}

/** Returned by `check_update` — describes an available update. */
Expand Down
2 changes: 2 additions & 0 deletions src/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@
"system_toggle_aot_desc": "Keep the timer window above all other windows.",
"system_toggle_break_aot": "Lower Priority During Breaks",
"system_toggle_break_aot_desc": "Disable always-on-top while a break is running.",
"system_toggle_remember_window": "Remember Size and Position",
"system_toggle_remember_window_desc": "Restore the window to its last size and position when the app launches.",
"system_group_tray": "System Tray",
"system_toggle_show_tray": "Show in System Tray",
"system_toggle_show_tray_desc": "Display a persistent icon in the system tray.",
Expand Down