fix(ui): remember window size across launches
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 <noreply@anthropic.com>
This commit is contained in:
+32
-11
@@ -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;
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
Reference in New Issue
Block a user