feat(window): restore window position on X11
X11 sessions now persist and restore the window position (window_x/y) in addition to size. Gated to X11: Wayland's xdg-shell gives clients no way to self-position, so we center there (and iced never emits Moved on Wayland, so window_x/y stay None). No drift across save/restore — iced's Moved event and Position::Specific both use the window's outer position. Also confirmed peerspeak already runs on X11 out of the box (winit compiles both backends and auto-selects via WAYLAND_DISPLAY/DISPLAY) and documented X11/Wayland support in FEATURES.md. - config: window_x/window_y: Option<i32> (serde-default None). - app: is_wayland() + pure initial_window_position() helper; a Moved handler records position; the close path persists it. - tests: +3 initial_window_position (X11 restore / Wayland centers / partial-or-missing centers), +2 config (round-trip incl. negative coords; backward-compat load without the new fields). 130 -> 135 lib. Verified live on X11/XWayland: saved an off-center position, the window reopened there (not centered). clippy clean incl. --all-targets. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+42
-1
@@ -158,11 +158,19 @@ pub struct AppConfig {
|
||||
#[serde(default)]
|
||||
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.)
|
||||
/// Saved on close.
|
||||
#[serde(default = "default_window_width")]
|
||||
pub window_width: f32,
|
||||
#[serde(default = "default_window_height")]
|
||||
pub window_height: f32,
|
||||
/// Last window position (px). Saved on close, restored on next launch — but
|
||||
/// only on **X11**: Wayland's xdg-shell gives clients no way to place their
|
||||
/// own window, so we center there and let the compositor decide. `None` =
|
||||
/// never saved a position yet (e.g. always run under Wayland) → center.
|
||||
#[serde(default)]
|
||||
pub window_x: Option<i32>,
|
||||
#[serde(default)]
|
||||
pub window_y: Option<i32>,
|
||||
}
|
||||
|
||||
impl Default for AppConfig {
|
||||
@@ -193,6 +201,8 @@ impl Default for AppConfig {
|
||||
pixelpass_path: None,
|
||||
window_width: default_window_width(),
|
||||
window_height: default_window_height(),
|
||||
window_x: None,
|
||||
window_y: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -290,6 +300,37 @@ mod tests {
|
||||
assert_eq!(back.window_height, 720.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_window_position_fields() {
|
||||
// Position is unset by default (only ever saved on X11).
|
||||
let def = AppConfig::default();
|
||||
assert_eq!(def.window_x, None);
|
||||
assert_eq!(def.window_y, None);
|
||||
// A saved position (incl. negative coords) round-trips.
|
||||
let cfg = AppConfig {
|
||||
window_x: Some(200),
|
||||
window_y: Some(-50),
|
||||
..AppConfig::default()
|
||||
};
|
||||
let json = serde_json::to_string(&cfg).unwrap();
|
||||
let back: AppConfig = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(back.window_x, Some(200));
|
||||
assert_eq!(back.window_y, Some(-50));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn old_config_without_position_loads() {
|
||||
// A config written before window_x/window_y existed still deserializes
|
||||
// (serde default → None), so upgrades don't wipe a user's settings.
|
||||
// (The three device/gate fields have no serde default, so any valid
|
||||
// config must carry them — mirrors test_backward_compat_default_fill.)
|
||||
let json = r#"{"input_device":"","output_device":"","noise_gate_threshold":0.01,"window_width":1024.0,"window_height":768.0}"#;
|
||||
let cfg: AppConfig = serde_json::from_str(json).unwrap();
|
||||
assert_eq!(cfg.window_x, None);
|
||||
assert_eq!(cfg.window_y, None);
|
||||
assert_eq!(cfg.window_width, 1024.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_input_output_volume_fields() {
|
||||
// Default impl is unity gain.
|
||||
|
||||
Reference in New Issue
Block a user