Merge fix/remember-window-size: persist + restore window size

This commit is contained in:
2026-06-06 17:44:11 -04:00
2 changed files with 69 additions and 11 deletions
+32 -11
View File
@@ -250,13 +250,15 @@ impl Default for AppState {
let _ = UI_RX.set(Mutex::new(Some(ui_rx))); let _ = UI_RX.set(Mutex::new(Some(ui_rx)));
let mut config = AppConfig::load(); let mut config = AppConfig::load();
// A persisted divider size from a differently-sized window could be out of // The window opens at the restored size (see `run_gui`); clamp the
// range for the default window — clamp it before first render. // 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 = config.participants_width =
clamp_participants_width(config.participants_width, 900.0); clamp_participants_width(config.participants_width, ww);
config.chat_height = clamp_chat_height(config.chat_height, 760.0); config.chat_height = clamp_chat_height(config.chat_height, wh);
config.controls_width = clamp_controls_width(config.controls_width, 900.0); config.controls_width = clamp_controls_width(config.controls_width, ww);
config.chat_drawer_width = clamp_chat_drawer_width(config.chat_drawer_width, 900.0); config.chat_drawer_width = clamp_chat_drawer_width(config.chat_drawer_width, ww);
notify::set_enabled(config.notifications_enabled); notify::set_enabled(config.notifications_enabled);
let _ = controller.send(CoreCommand::SetNoiseGateThreshold(config.noise_gate_threshold)); let _ = controller.send(CoreCommand::SetNoiseGateThreshold(config.noise_gate_threshold));
let _ = controller.send(CoreCommand::SetInputVolume(config.input_volume)); let _ = controller.send(CoreCommand::SetInputVolume(config.input_volume));
@@ -300,7 +302,7 @@ impl Default for AppState {
recording_started: None, recording_started: None,
chat_messages: Vec::new(), chat_messages: Vec::new(),
chat_input: String::new(), chat_input: String::new(),
window_size: Size::new(900.0, 760.0), window_size: Size::new(ww, wh),
layout_picker_open: false, layout_picker_open: false,
drawer_chat_open: false, drawer_chat_open: false,
mic_level: 0.0, mic_level: 0.0,
@@ -320,16 +322,22 @@ fn theme(_state: &AppState) -> Theme {
} }
pub fn run_gui() -> iced::Result { 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) iced::application(AppState::default, update, view)
.title("PeerSpeak P2P Voice Chat") .title("PeerSpeak P2P Voice Chat")
.theme(theme) .theme(theme)
.subscription(subscription) .subscription(subscription)
.window(iced::window::Settings { .window(iced::window::Settings {
// Taller default so the bottom chat dock doesn't squeeze the controls // Restored from config (defaults 900×760: taller so the bottom chat
// column. Layout is responsive (Fill), so resizing still works. // dock doesn't squeeze the controls column). Layout is responsive.
size: iced::Size::new(900.0, 760.0), size: init_size,
position: iced::window::Position::Centered, 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() ..Default::default()
}) })
.run() .run()
@@ -734,6 +742,12 @@ fn update(state: &mut AppState, message: AppMessage) -> Task<AppMessage> {
} }
AppMessage::EventOccurred(Event::Window(iced::window::Event::Resized(size))) => { AppMessage::EventOccurred(Event::Window(iced::window::Event::Resized(size))) => {
state.window_size = 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 // Keep divider positions valid for the new window dimensions. (Saved
// with the next drag-release or other config write; not worth a disk // with the next drag-release or other config write; not worth a disk
// write on every resize tick.) // write on every resize tick.)
@@ -746,6 +760,13 @@ fn update(state: &mut AppState, message: AppMessage) -> Task<AppMessage> {
state.config.chat_drawer_width = state.config.chat_drawer_width =
clamp_chat_drawer_width(state.config.chat_drawer_width, size.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::EventOccurred(_) => {}
AppMessage::NavigateToSettings => { AppMessage::NavigateToSettings => {
state.current_screen = Screen::Settings; state.current_screen = Screen::Settings;
+37
View File
@@ -92,6 +92,14 @@ fn default_chat_drawer_width() -> f32 {
320.0 320.0
} }
fn default_window_width() -> f32 {
900.0
}
fn default_window_height() -> f32 {
760.0
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct AppConfig { pub struct AppConfig {
/// Last nickname used to join/create a room; pre-filled on the launch screen. /// 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. /// Empty / unset = look it up on `$PATH`. Hand-editable; no Settings UI yet.
#[serde(default)] #[serde(default)]
pub pixelpass_path: Option<String>, pub pixelpass_path: Option<String>,
/// 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 { impl Default for AppConfig {
@@ -177,6 +191,8 @@ impl Default for AppConfig {
custom_sound_mic_toggle: None, custom_sound_mic_toggle: None,
custom_sound_reconnect_failed: None, custom_sound_reconnect_failed: None,
pixelpass_path: 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_self_leave.is_none());
assert!(deserialized.custom_sound_mic_toggle.is_none()); assert!(deserialized.custom_sound_mic_toggle.is_none());
assert!(deserialized.custom_sound_reconnect_failed.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] #[test]