From 88905e5173c9d78156a52a6540db1b04a78f2c31 Mon Sep 17 00:00:00 2001 From: Mollusk Date: Sat, 6 Jun 2026 17:42:59 -0400 Subject: [PATCH] fix(ui): remember window size across launches MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The window always opened at the hardcoded 900x760 because the size was never persisted: run_gui hardcoded it, AppConfig had no size fields, and the Resized handler only kept the size in memory (for divider clamping) while exit_on_close_request:true quit before anything could save. - AppConfig gains window_width/window_height (serde-default 900/760). - run_gui restores them as the initial window size. - The Resized handler mirrors the live size into config (guarded against bogus tiny sizes); divider positions on load now clamp against the restored size rather than a hardcoded default. - exit_on_close_request:false + a CloseRequested handler writes the final size once, then iced::exit() — no per-resize disk thrash. Verified empirically that KWin/Wayland honors a client-requested initial size (requested 1150x680 -> window reported 1150x680). Window *position* is not restored: xdg-shell gives Wayland clients no way to set their own position. +1 config test (default + round-trip). Co-Authored-By: Claude Opus 4.8 --- src/app/mod.rs | 43 ++++++++++++++++++++++++++++++++----------- src/config.rs | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 11 deletions(-) diff --git a/src/app/mod.rs b/src/app/mod.rs index 2496e5e..d55c044 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -250,13 +250,15 @@ impl Default for AppState { let _ = UI_RX.set(Mutex::new(Some(ui_rx))); let mut config = AppConfig::load(); - // A persisted divider size from a differently-sized window could be out of - // range for the default window — clamp it before first render. + // The window opens at the restored size (see `run_gui`); clamp the + // persisted divider positions against THAT size, not a hardcoded default, + // so they stay valid for the window we're actually about to show. + let (ww, wh) = (config.window_width, config.window_height); config.participants_width = - clamp_participants_width(config.participants_width, 900.0); - config.chat_height = clamp_chat_height(config.chat_height, 760.0); - config.controls_width = clamp_controls_width(config.controls_width, 900.0); - config.chat_drawer_width = clamp_chat_drawer_width(config.chat_drawer_width, 900.0); + clamp_participants_width(config.participants_width, ww); + config.chat_height = clamp_chat_height(config.chat_height, wh); + config.controls_width = clamp_controls_width(config.controls_width, ww); + config.chat_drawer_width = clamp_chat_drawer_width(config.chat_drawer_width, ww); notify::set_enabled(config.notifications_enabled); let _ = controller.send(CoreCommand::SetNoiseGateThreshold(config.noise_gate_threshold)); let _ = controller.send(CoreCommand::SetInputVolume(config.input_volume)); @@ -300,7 +302,7 @@ impl Default for AppState { recording_started: None, chat_messages: Vec::new(), chat_input: String::new(), - window_size: Size::new(900.0, 760.0), + window_size: Size::new(ww, wh), layout_picker_open: false, drawer_chat_open: false, mic_level: 0.0, @@ -320,16 +322,22 @@ fn theme(_state: &AppState) -> Theme { } pub fn run_gui() -> iced::Result { + // Restore the last window size (saved on close). Position can't be restored on + // Wayland — xdg-shell gives clients no way to set their own position — so we + // only persist size and leave placement to the compositor. + let saved = AppConfig::load(); + let init_size = iced::Size::new(saved.window_width, saved.window_height); iced::application(AppState::default, update, view) .title("PeerSpeak P2P Voice Chat") .theme(theme) .subscription(subscription) .window(iced::window::Settings { - // Taller default so the bottom chat dock doesn't squeeze the controls - // column. Layout is responsive (Fill), so resizing still works. - size: iced::Size::new(900.0, 760.0), + // Restored from config (defaults 900×760: taller so the bottom chat + // dock doesn't squeeze the controls column). Layout is responsive. + size: init_size, position: iced::window::Position::Centered, - exit_on_close_request: true, + // We save the final size ourselves on CloseRequested, then exit. + exit_on_close_request: false, ..Default::default() }) .run() @@ -734,6 +742,12 @@ fn update(state: &mut AppState, message: AppMessage) -> Task { } AppMessage::EventOccurred(Event::Window(iced::window::Event::Resized(size))) => { state.window_size = size; + // Remember the size in-memory; it's written to disk once on close. + // Guard against bogus tiny/zero sizes some compositors emit transiently. + if size.width >= 200.0 && size.height >= 200.0 { + state.config.window_width = size.width; + state.config.window_height = size.height; + } // Keep divider positions valid for the new window dimensions. (Saved // with the next drag-release or other config write; not worth a disk // write on every resize tick.) @@ -746,6 +760,13 @@ fn update(state: &mut AppState, message: AppMessage) -> Task { state.config.chat_drawer_width = clamp_chat_drawer_width(state.config.chat_drawer_width, size.width); } + AppMessage::EventOccurred(Event::Window(iced::window::Event::CloseRequested)) => { + // We took over the close path (exit_on_close_request:false) so we can + // persist the final window size before quitting. The latest size is + // already mirrored into config by the Resized handler above. + state.config.save(); + return iced::exit(); + } AppMessage::EventOccurred(_) => {} AppMessage::NavigateToSettings => { state.current_screen = Screen::Settings; diff --git a/src/config.rs b/src/config.rs index bc86f2a..b5096cd 100644 --- a/src/config.rs +++ b/src/config.rs @@ -92,6 +92,14 @@ fn default_chat_drawer_width() -> f32 { 320.0 } +fn default_window_width() -> f32 { + 900.0 +} + +fn default_window_height() -> f32 { + 760.0 +} + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct AppConfig { /// Last nickname used to join/create a room; pre-filled on the launch screen. @@ -149,6 +157,12 @@ pub struct AppConfig { /// Empty / unset = look it up on `$PATH`. Hand-editable; no Settings UI yet. #[serde(default)] pub pixelpass_path: Option, + /// Last window size (px), restored as the initial size on next launch. + /// Saved on close. (Window *position* can't be restored on Wayland.) + #[serde(default = "default_window_width")] + pub window_width: f32, + #[serde(default = "default_window_height")] + pub window_height: f32, } impl Default for AppConfig { @@ -177,6 +191,8 @@ impl Default for AppConfig { custom_sound_mic_toggle: None, custom_sound_reconnect_failed: None, pixelpass_path: None, + window_width: default_window_width(), + window_height: default_window_height(), } } } @@ -251,6 +267,27 @@ mod tests { assert!(deserialized.custom_sound_self_leave.is_none()); assert!(deserialized.custom_sound_mic_toggle.is_none()); assert!(deserialized.custom_sound_reconnect_failed.is_none()); + // Configs predating the remembered window size load the default size. + assert_eq!(deserialized.window_width, 900.0); + assert_eq!(deserialized.window_height, 760.0); + } + + #[test] + fn test_window_size_fields() { + // Default impl is the standard launch size. + let def = AppConfig::default(); + assert_eq!(def.window_width, 900.0); + assert_eq!(def.window_height, 760.0); + // A saved size round-trips. + let cfg = AppConfig { + window_width: 1280.0, + window_height: 720.0, + ..AppConfig::default() + }; + let json = serde_json::to_string(&cfg).unwrap(); + let back: AppConfig = serde_json::from_str(&json).unwrap(); + assert_eq!(back.window_width, 1280.0); + assert_eq!(back.window_height, 720.0); } #[test]