Files
pdfcraft/crates/ui-egui/src/window_state.rs
T
mtlcl410andClaude Opus 5.5 30c174be9e M3: closing PdfCraft while minimized no longer saves a 0 x 0 window
eframe stores the window's size and place just before App::save. A
minimized window has none: Windows reports 0 x 0 at (-32000, -32000),
and the next start opened at egui's 64-point floor in the screen's
corner (seen on a 6K display at 225%: a 144 x 206 px window). While the
window is minimized the app now puts back the last settings it was
shown with (or none), and a window that opens at that floor from
settings saved before this fix is given a usable size.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
2026-10-16 19:20:47 +08:00

138 lines
6.2 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! The window's saved size and place across starts. eframe stores them just before
//! `App::save`, from the window as it is at that moment. A minimized window has neither: Windows
//! reports it as 0 × 0 at (-32000, -32000), and the next start then opened at egui's 64-point
//! floor in the screen's corner. So while the window is minimized the last settings it was shown
//! with are put back, and a window that opens at that floor is given a usable size.
/// eframe's storage key for the window settings.
const WINDOW_KEY: &str = "window";
/// The size egui opens a stored size below (its floor for "ridiculously small" windows), in points.
const EGUI_FLOOR: f32 = 64.0;
/// The size a window that opened at [`EGUI_FLOOR`] is given (apps/pdfcraft opens a first start at
/// this size too), at most nine tenths of the monitor.
const USABLE: egui::Vec2 = egui::vec2(1440.0, 920.0);
#[derive(Debug, Default)]
pub struct WindowState {
/// The settings eframe last stored for a shown (not minimized) window.
shown: Option<String>,
/// The size at start has been checked ([`Self::repair`]).
checked: bool,
}
impl WindowState {
/// The settings the window started with: what to put back if it is minimized before eframe
/// stores a shown one.
pub fn started_with(storage: Option<&dyn eframe::Storage>) -> Self {
Self { shown: storage.and_then(|s| s.get_string(WINDOW_KEY)), checked: false }
}
/// After eframe has stored the window: remember it while the window is shown, and while it
/// is minimized put the last shown settings back (or none, so the next start opens at the
/// default size).
pub fn keep(&mut self, storage: &mut dyn eframe::Storage, minimized: bool) {
if !minimized {
self.shown = storage.get_string(WINDOW_KEY);
} else if let Some(shown) = &self.shown {
storage.set_string(WINDOW_KEY, shown.clone());
} else {
storage.remove_string(WINDOW_KEY);
}
}
/// Once the window's size is known: a shown window at egui's floor in both directions came
/// from settings stored while it was minimized (before this fix), so give it a usable size.
pub fn repair(&mut self, ctx: &egui::Context) {
if self.checked {
return;
}
let (rect, minimized, monitor) = ctx.input(|i| (i.viewport().inner_rect, i.viewport().minimized, i.viewport().monitor_size));
let Some(rect) = rect else { return };
if minimized == Some(true) {
return;
}
self.checked = true;
if rect.width() <= EGUI_FLOOR + 0.5 && rect.height() <= EGUI_FLOOR + 0.5 {
// The stored settings are the broken ones: never put them back.
self.shown = None;
let size = monitor.map_or(USABLE, |m| USABLE.min(m * 0.9));
ctx.send_viewport_cmd(egui::ViewportCommand::InnerSize(size));
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use eframe::Storage as _;
#[derive(Default)]
struct Store(std::collections::HashMap<String, String>);
impl eframe::Storage for Store {
fn get_string(&self, key: &str) -> Option<String> {
self.0.get(key).cloned()
}
fn set_string(&mut self, key: &str, value: String) {
self.0.insert(key.to_string(), value);
}
fn remove_string(&mut self, key: &str) {
self.0.remove(key);
}
fn flush(&mut self) {}
}
const SHOWN: &str = "(outer_position_pixels:Some((x:200.0,y:200.0)),inner_size_points:Some((x:1320.0,y:940.0)))";
const MINIMIZED: &str = "(outer_position_pixels:Some((x:-32000.0,y:-32000.0)),inner_size_points:Some((x:0.0,y:0.0)))";
/// Closing PdfCraft while it was minimized stored a 0 × 0 window off screen.
#[test]
fn a_minimized_window_keeps_the_last_shown_settings() {
let mut store = Store::default();
store.set_string(WINDOW_KEY, SHOWN.into());
let mut state = WindowState::started_with(Some(&store));
// eframe stores the minimized window, then the app saves.
store.set_string(WINDOW_KEY, MINIMIZED.into());
state.keep(&mut store, true);
assert_eq!(store.get_string(WINDOW_KEY).as_deref(), Some(SHOWN));
// Shown again and resized: that is what is kept from then on.
let moved = SHOWN.replace("200.0", "300.0");
store.set_string(WINDOW_KEY, moved.clone());
state.keep(&mut store, false);
store.set_string(WINDOW_KEY, MINIMIZED.into());
state.keep(&mut store, true);
assert_eq!(store.get_string(WINDOW_KEY), Some(moved));
}
#[test]
fn with_nothing_shown_yet_a_minimized_save_stores_no_window() {
let mut store = Store::default();
let mut state = WindowState::started_with(Some(&store));
store.set_string(WINDOW_KEY, MINIMIZED.into());
state.keep(&mut store, true);
assert_eq!(store.get_string(WINDOW_KEY), None, "the next start opens at the default size");
}
fn repaired(size: egui::Vec2, minimized: Option<bool>) -> Vec<egui::ViewportCommand> {
let ctx = egui::Context::default();
let mut input = egui::RawInput::default();
let info = input.viewports.entry(egui::ViewportId::ROOT).or_default();
info.inner_rect = Some(egui::Rect::from_min_size(egui::Pos2::ZERO, size));
info.minimized = minimized;
info.monitor_size = Some(egui::vec2(2674.0, 1504.0));
let mut state = WindowState { shown: Some(MINIMIZED.into()), checked: false };
let out = ctx.run_logic(&input, |ctx| state.repair(ctx));
let commands = out.viewport_commands.get(&egui::ViewportId::ROOT).cloned().unwrap_or_default();
// Repaired: the settings it started with are not put back by a minimized close.
assert_eq!(state.shown.is_none(), !commands.is_empty());
commands
}
#[test]
fn a_window_at_eguis_floor_gets_a_usable_size() {
assert_eq!(repaired(egui::vec2(64.0, 64.0), Some(false)), [egui::ViewportCommand::InnerSize(USABLE)]);
assert!(repaired(egui::vec2(820.0, 520.0), Some(false)).is_empty(), "a small window someone chose stays");
assert!(repaired(egui::vec2(64.0, 64.0), Some(true)).is_empty(), "not while minimized");
}
}