Merge pull request #161 from storytold/fix/issue-82
Fix #82: the window fits the screen and comes back where it was
This commit is contained in:
commit
52d6b9a2ba
|
|
@ -4225,6 +4225,7 @@ dependencies = [
|
||||||
"vectorcraft-engine",
|
"vectorcraft-engine",
|
||||||
"vectorcraft-ui-egui",
|
"vectorcraft-ui-egui",
|
||||||
"webbrowser",
|
"webbrowser",
|
||||||
|
"winit",
|
||||||
"winresource",
|
"winresource",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,8 @@ rfd = { workspace = true }
|
||||||
arboard = { workspace = true, features = ["image-data"] }
|
arboard = { workspace = true, features = ["image-data"] }
|
||||||
image = { workspace = true }
|
image = { workspace = true }
|
||||||
webbrowser = { workspace = true }
|
webbrowser = { workspace = true }
|
||||||
|
# The window's monitors, size and position (the same winit eframe runs on).
|
||||||
|
winit = { version = "0.30", default-features = false }
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies]
|
[target.'cfg(windows)'.dependencies]
|
||||||
clipboard-win = { workspace = true }
|
clipboard-win = { workspace = true }
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ mod control_server;
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
mod native_menu;
|
mod native_menu;
|
||||||
mod printing;
|
mod printing;
|
||||||
|
mod window;
|
||||||
|
|
||||||
use vectorcraft_engine::Session;
|
use vectorcraft_engine::Session;
|
||||||
use vectorcraft_engine::cmd::fileio;
|
use vectorcraft_engine::cmd::fileio;
|
||||||
|
|
@ -30,6 +31,7 @@ impl eframe::App for App {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.0.logic(ctx);
|
self.0.logic(ctx);
|
||||||
|
window::track(ctx, &mut self.0.ui.window);
|
||||||
if self.0.ui.status == "quit" {
|
if self.0.ui.status == "quit" {
|
||||||
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
|
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
|
||||||
}
|
}
|
||||||
|
|
@ -71,21 +73,32 @@ fn prefs_path_for(name: &str, lower: &str) -> Option<std::path::PathBuf> {
|
||||||
base.map(|b| b.join("ui.json"))
|
base.map(|b| b.join("ui.json"))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_prefs(app: &mut VectorcraftApp) {
|
/// Runs without preferences (`VECTORCRAFT_NO_PREFS`, agents' test runs) neither read nor write them.
|
||||||
if std::env::var_os("VECTORCRAFT_NO_PREFS").is_some() {
|
fn prefs_enabled() -> bool {
|
||||||
|
std::env::var_os("VECTORCRAFT_NO_PREFS").is_none()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The saved UI preferences, read before the window opens (they hold its size and position).
|
||||||
|
fn read_prefs() -> Option<vectorcraft_ui_egui::UiState> {
|
||||||
|
if !prefs_enabled() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let bytes = prefs_path().and_then(|p| std::fs::read(p).ok()).or_else(|| legacy_prefs_path().and_then(|p| std::fs::read(p).ok()))?;
|
||||||
|
serde_json::from_slice(&bytes).ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_prefs(app: &mut VectorcraftApp, saved: Option<vectorcraft_ui_egui::UiState>) {
|
||||||
|
if !prefs_enabled() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let bytes = prefs_path().and_then(|p| std::fs::read(p).ok()).or_else(|| legacy_prefs_path().and_then(|p| std::fs::read(p).ok()));
|
if let Some(ui) = saved {
|
||||||
if let Some(bytes) = bytes
|
|
||||||
&& let Ok(ui) = serde_json::from_slice::<vectorcraft_ui_egui::UiState>(&bytes)
|
|
||||||
{
|
|
||||||
app.ui = ui.sanitized();
|
app.ui = ui.sanitized();
|
||||||
}
|
}
|
||||||
vectorcraft_ui_egui::prefs_dialog::restore(app);
|
vectorcraft_ui_egui::prefs_dialog::restore(app);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn save_prefs(app: &VectorcraftApp) {
|
fn save_prefs(app: &VectorcraftApp) {
|
||||||
if std::env::var_os("VECTORCRAFT_NO_PREFS").is_some() {
|
if !prefs_enabled() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if let Some(p) = prefs_path() {
|
if let Some(p) = prefs_path() {
|
||||||
|
|
@ -230,11 +243,13 @@ fn main() -> eframe::Result {
|
||||||
_ => files.push(a),
|
_ => files.push(a),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
let saved = read_prefs();
|
||||||
|
let saved_window = saved.as_ref().and_then(|ui| ui.window);
|
||||||
let options = eframe::NativeOptions {
|
let options = eframe::NativeOptions {
|
||||||
viewport: egui::ViewportBuilder::default()
|
viewport: egui::ViewportBuilder::default()
|
||||||
.with_title("VectorCraft")
|
.with_title("VectorCraft")
|
||||||
.with_inner_size([1440.0, 900.0])
|
.with_inner_size(window::DEFAULT_SIZE)
|
||||||
.with_min_inner_size([800.0, 500.0])
|
.with_min_inner_size(window::MIN_SIZE)
|
||||||
.with_drag_and_drop(true)
|
.with_drag_and_drop(true)
|
||||||
.with_decorations(!CUSTOM_TITLEBAR)
|
.with_decorations(!CUSTOM_TITLEBAR)
|
||||||
.with_fullsize_content_view(true)
|
.with_fullsize_content_view(true)
|
||||||
|
|
@ -249,7 +264,11 @@ fn main() -> eframe::Result {
|
||||||
options,
|
options,
|
||||||
Box::new(move |cc| {
|
Box::new(move |cc| {
|
||||||
let mut app = VectorcraftApp::new(Session::new(), services());
|
let mut app = VectorcraftApp::new(Session::new(), services());
|
||||||
load_prefs(&mut app);
|
load_prefs(&mut app, saved);
|
||||||
|
// Fit the window to its monitor, or put it back where it was (still hidden).
|
||||||
|
if let Some(w) = cc.winit_window() {
|
||||||
|
app.ui.window = Some(window::restore(w, saved_window));
|
||||||
|
}
|
||||||
// User Defined swatch and graphic style libraries live next to the preferences.
|
// User Defined swatch and graphic style libraries live next to the preferences.
|
||||||
let swatches = prefs_path().and_then(|p| Some(p.parent()?.join("Swatches").to_string_lossy().to_string()));
|
let swatches = prefs_path().and_then(|p| Some(p.parent()?.join("Swatches").to_string_lossy().to_string()));
|
||||||
app.session.swatch_libraries.set_user_dir(swatches);
|
app.session.swatch_libraries.set_user_dir(swatches);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,256 @@
|
||||||
|
//! The main window's size and position. The first launch fits the window to its monitor, opening
|
||||||
|
//! it maximized where the default size leaves no room for a taskbar or Dock. Later launches restore
|
||||||
|
//! the size, position and maximized state saved with the UI preferences (`UiState::window`), moved
|
||||||
|
//! onto a monitor that is still connected.
|
||||||
|
|
||||||
|
use vectorcraft_ui_egui::state::WindowGeometry;
|
||||||
|
use winit::dpi::{LogicalSize, PhysicalPosition, PhysicalSize};
|
||||||
|
|
||||||
|
/// The first window's size, in points.
|
||||||
|
pub const DEFAULT_SIZE: [f32; 2] = [1440.0, 900.0];
|
||||||
|
/// The smallest window, in points.
|
||||||
|
pub const MIN_SIZE: [f32; 2] = [800.0, 500.0];
|
||||||
|
/// Room the first window leaves around itself for a taskbar, Dock or menu bar (the system doesn't
|
||||||
|
/// say how much of the screen they take): a monitor with less room opens it maximized.
|
||||||
|
const SCREEN_ROOM: [f32; 2] = [64.0, 128.0];
|
||||||
|
|
||||||
|
/// A monitor: its top-left corner and size in physical pixels, and its scale factor.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub struct Monitor {
|
||||||
|
pos: [i64; 2],
|
||||||
|
size: [i64; 2],
|
||||||
|
scale: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Monitor {
|
||||||
|
pub fn new(pos: PhysicalPosition<i32>, size: PhysicalSize<u32>, scale: f64) -> Self {
|
||||||
|
let scale = scale as f32;
|
||||||
|
let scale = if scale.is_finite() && scale > 0.0 { scale } else { 1.0 };
|
||||||
|
Self { pos: [pos.x.into(), pos.y.into()], size: [size.width.into(), size.height.into()], scale }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Its size in points.
|
||||||
|
fn points(&self) -> [f32; 2] {
|
||||||
|
self.size.map(|s| s as f32 / self.scale)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Its size in points less room for a taskbar, Dock or menu bar.
|
||||||
|
fn usable(&self) -> [f32; 2] {
|
||||||
|
let points = self.points();
|
||||||
|
std::array::from_fn(|i| points[i] - SCREEN_ROOM[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A window of `size` points on this monitor, in physical pixels.
|
||||||
|
fn pixels(&self, size: [f32; 2]) -> [i64; 2] {
|
||||||
|
size.map(|s| (s * self.scale) as i64)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// How much of a window at `pos` of `size` points this monitor shows, in square pixels.
|
||||||
|
fn overlap(&self, pos: [i32; 2], size: [f32; 2]) -> i64 {
|
||||||
|
let px = self.pixels(size);
|
||||||
|
let span = |i: usize| {
|
||||||
|
let start = i64::from(pos[i]).max(self.pos[i]);
|
||||||
|
let end = i64::from(pos[i]).saturating_add(px[i]).min(self.pos[i] + self.size[i]);
|
||||||
|
(end - start).max(0)
|
||||||
|
};
|
||||||
|
span(0).saturating_mul(span(1))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The top-left corner nearest to `pos` (centred when none) that keeps a window of `size`
|
||||||
|
/// points on this monitor.
|
||||||
|
fn keep_on(&self, pos: Option<[i32; 2]>, size: [f32; 2]) -> [i32; 2] {
|
||||||
|
let px = self.pixels(size);
|
||||||
|
std::array::from_fn(|i| {
|
||||||
|
let lo = self.pos[i];
|
||||||
|
let hi = lo + self.size[i].saturating_sub(px[i]).max(0);
|
||||||
|
let want = pos.map_or(lo + (hi - lo) / 2, |p| i64::from(p[i]));
|
||||||
|
i32::try_from(want.max(lo).min(hi)).unwrap_or_default()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Where the window opens: `saved` (the last session's geometry) kept on a connected monitor, else
|
||||||
|
/// the default size on `home` (the monitor the system opens it on). Without monitors (the system
|
||||||
|
/// doesn't list them, so nothing to check a saved geometry against) the default size.
|
||||||
|
pub fn place(saved: Option<WindowGeometry>, monitors: &[Monitor], home: Option<usize>) -> WindowGeometry {
|
||||||
|
let default = WindowGeometry { pos: None, size: DEFAULT_SIZE, maximized: false };
|
||||||
|
let Some(home) = home.and_then(|i| monitors.get(i)).or(monitors.first()) else {
|
||||||
|
return default;
|
||||||
|
};
|
||||||
|
let Some(saved) = saved else {
|
||||||
|
let usable = home.usable();
|
||||||
|
if (0..2).all(|i| DEFAULT_SIZE[i] <= usable[i]) {
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
// A small screen: maximized, with a size that fits once un-maximized.
|
||||||
|
let size = fit(DEFAULT_SIZE, usable);
|
||||||
|
return WindowGeometry { pos: Some(home.keep_on(None, size)), size, maximized: true };
|
||||||
|
};
|
||||||
|
// The monitor showing most of the window; none when it was on a monitor that is gone.
|
||||||
|
let shown = saved.pos.and_then(|p| {
|
||||||
|
let (area, m) = monitors.iter().map(|m| (m.overlap(p, saved.size), m)).max_by_key(|(area, _)| *area)?;
|
||||||
|
(area > 0).then_some(m)
|
||||||
|
});
|
||||||
|
let m = shown.unwrap_or(home);
|
||||||
|
// Bigger than its monitor (a smaller screen than last time): maximized, like the first window.
|
||||||
|
let room = m.points();
|
||||||
|
let too_big = (0..2).any(|i| saved.size[i] > room[i]);
|
||||||
|
let size = fit(saved.size, if too_big { m.usable() } else { room });
|
||||||
|
WindowGeometry {
|
||||||
|
// Moved fully onto its monitor, or centred on the home monitor.
|
||||||
|
pos: saved.pos.map(|p| m.keep_on(shown.map(|_| p), size)),
|
||||||
|
size,
|
||||||
|
maximized: saved.maximized || too_big,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `size` no bigger than `room`, and no smaller than the smallest window unless `room` is.
|
||||||
|
fn fit(size: [f32; 2], room: [f32; 2]) -> [f32; 2] {
|
||||||
|
std::array::from_fn(|i| size[i].min(room[i]).max(MIN_SIZE[i].min(room[i])).max(1.0))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Size and place the new window (eframe shows it after drawing the first frame) and return its
|
||||||
|
/// geometry.
|
||||||
|
pub fn restore(window: &winit::window::Window, saved: Option<WindowGeometry>) -> WindowGeometry {
|
||||||
|
let handles: Vec<_> = window.available_monitors().collect();
|
||||||
|
let monitors: Vec<Monitor> = handles.iter().map(|m| Monitor::new(m.position(), m.size(), m.scale_factor())).collect();
|
||||||
|
let home = window.current_monitor().or_else(|| window.primary_monitor()).and_then(|c| handles.iter().position(|m| *m == c));
|
||||||
|
let g = place(saved, &monitors, home);
|
||||||
|
if let Some([x, y]) = g.pos {
|
||||||
|
window.set_outer_position(PhysicalPosition::new(x, y));
|
||||||
|
}
|
||||||
|
// Returns the new size when it applies at once; either way the window gets it.
|
||||||
|
let _ = window.request_inner_size(LogicalSize::new(g.size[0], g.size[1]));
|
||||||
|
if g.maximized {
|
||||||
|
window.set_maximized(true);
|
||||||
|
}
|
||||||
|
g
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Per frame: remember the window's geometry for the next launch. Minimized or full screen keeps
|
||||||
|
/// the last one; maximized only sets its flag, so un-maximizing next time restores the last size.
|
||||||
|
pub fn track(ctx: &egui::Context, geometry: &mut Option<WindowGeometry>) {
|
||||||
|
let zoom = ctx.zoom_factor();
|
||||||
|
ctx.input(|i| {
|
||||||
|
let v = i.viewport();
|
||||||
|
if v.minimized == Some(true) || v.fullscreen == Some(true) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if v.maximized == Some(true) {
|
||||||
|
if let Some(g) = geometry {
|
||||||
|
g.maximized = true;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let Some(inner) = v.inner_rect else { return };
|
||||||
|
let pos = v.outer_rect.map(|r| [r.min.x, r.min.y].map(|c| (c * i.pixels_per_point).round() as i32));
|
||||||
|
*geometry = Some(WindowGeometry { pos, size: [inner.width() * zoom, inner.height() * zoom], maximized: false });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn monitor(x: i32, y: i32, w: u32, h: u32, scale: f64) -> Monitor {
|
||||||
|
Monitor::new(PhysicalPosition::new(x, y), PhysicalSize::new(w, h), scale)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn saved(pos: Option<[i32; 2]>, size: [f32; 2], maximized: bool) -> Option<WindowGeometry> {
|
||||||
|
Some(WindowGeometry { pos, size, maximized })
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_first_window_fits_its_screen() {
|
||||||
|
// 1920 × 1080: the default size, where the system puts it.
|
||||||
|
let big = [monitor(0, 0, 1920, 1080, 1.0)];
|
||||||
|
assert_eq!(place(None, &big, Some(0)), WindowGeometry { pos: None, size: DEFAULT_SIZE, maximized: false });
|
||||||
|
// 1600 × 900, 1366 × 768 and 1920 × 1080 at 150% (1280 × 720 points): maximized, and a
|
||||||
|
// size that fits the screen with room for the taskbar once un-maximized.
|
||||||
|
for (m, size) in [
|
||||||
|
(monitor(0, 0, 1600, 900, 1.0), [1440.0, 772.0]),
|
||||||
|
(monitor(0, 0, 1366, 768, 1.0), [1302.0, 640.0]),
|
||||||
|
(monitor(0, 0, 1920, 1080, 1.5), [1216.0, 592.0]),
|
||||||
|
] {
|
||||||
|
let g = place(None, &[m], Some(0));
|
||||||
|
assert!(g.maximized, "{m:?}");
|
||||||
|
assert_eq!(g.size, size, "{m:?}");
|
||||||
|
let [x, y] = g.pos.unwrap();
|
||||||
|
let px = m.pixels(g.size);
|
||||||
|
assert!(x >= 0 && y >= 0 && i64::from(x) + px[0] <= m.size[0] && i64::from(y) + px[1] <= m.size[1], "{m:?}: {g:?}");
|
||||||
|
}
|
||||||
|
// No monitor information: the default size, whatever was saved.
|
||||||
|
assert_eq!(place(None, &[], None), WindowGeometry { pos: None, size: DEFAULT_SIZE, maximized: false });
|
||||||
|
assert_eq!(place(saved(Some([9000, 9000]), [1e9, 1e9], true), &[], None).size, DEFAULT_SIZE);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_saved_window_comes_back_where_it_was() {
|
||||||
|
let two = [monitor(0, 0, 2560, 1440, 1.0), monitor(2560, 0, 1920, 1080, 1.0)];
|
||||||
|
for g in [saved(Some([100, 80]), [1200.0, 800.0], false), saved(Some([2700, 50]), [1000.0, 700.0], true), saved(None, [1000.0, 700.0], false)]
|
||||||
|
{
|
||||||
|
assert_eq!(Some(place(g, &two, Some(0))), g);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_saved_window_stays_on_a_connected_monitor() {
|
||||||
|
let laptop = [monitor(0, 0, 1600, 900, 1.0)];
|
||||||
|
// Last time on a second monitor that is gone: centred on the remaining one.
|
||||||
|
let g = place(saved(Some([2700, 50]), [1000.0, 700.0], false), &laptop, Some(0));
|
||||||
|
assert_eq!(g, WindowGeometry { pos: Some([300, 100]), size: [1000.0, 700.0], maximized: false });
|
||||||
|
// Half off the monitor's right and bottom edges: moved fully onto it.
|
||||||
|
let g = place(saved(Some([1200, 600]), [1000.0, 700.0], false), &laptop, Some(0));
|
||||||
|
assert_eq!(g.pos, Some([600, 200]));
|
||||||
|
// Bigger than the screen now (it was on a larger one): it fits, and opens maximized.
|
||||||
|
let g = place(saved(Some([0, 0]), [2400.0, 1300.0], false), &laptop, Some(0));
|
||||||
|
assert_eq!(g, WindowGeometry { pos: Some([0, 0]), size: [1536.0, 772.0], maximized: true });
|
||||||
|
// A monitor left of the primary one (negative coordinates) still counts.
|
||||||
|
let left = [monitor(0, 0, 1920, 1080, 1.0), monitor(-1920, 0, 1920, 1080, 1.0)];
|
||||||
|
assert_eq!(place(saved(Some([-1800, 40]), [1000.0, 700.0], false), &left, Some(0)).pos, Some([-1800, 40]));
|
||||||
|
// Nonsense sizes from a damaged preferences file are kept within the screen.
|
||||||
|
let g = place(saved(Some([i32::MAX, i32::MIN]), [-5.0, f32::MAX], false), &laptop, Some(0));
|
||||||
|
assert_eq!(g, WindowGeometry { pos: Some([400, 64]), size: [800.0, 772.0], maximized: true });
|
||||||
|
let g = place(saved(Some([5, 5]), [f32::NEG_INFINITY, f32::INFINITY], false), &laptop, Some(0));
|
||||||
|
assert_eq!(g, WindowGeometry { pos: Some([400, 64]), size: [800.0, 772.0], maximized: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_window_geometry_is_tracked_for_the_next_launch() {
|
||||||
|
let ctx = egui::Context::default();
|
||||||
|
let mut geometry = None;
|
||||||
|
let mut frame = |info: egui::ViewportInfo| {
|
||||||
|
let mut input = egui::RawInput::default();
|
||||||
|
input.viewports.insert(egui::ViewportId::ROOT, info);
|
||||||
|
ctx.run_ui(input, |ui| track(ui.ctx(), &mut geometry)).textures_delta.clear();
|
||||||
|
geometry
|
||||||
|
};
|
||||||
|
let rect = egui::Rect::from_min_size(egui::pos2(20.0, 30.0), egui::vec2(1000.0, 600.0));
|
||||||
|
let normal = egui::ViewportInfo {
|
||||||
|
native_pixels_per_point: Some(1.5),
|
||||||
|
inner_rect: Some(rect),
|
||||||
|
outer_rect: Some(rect),
|
||||||
|
maximized: Some(false),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
// Position in physical pixels, size in points.
|
||||||
|
let g = WindowGeometry { pos: Some([30, 45]), size: [1000.0, 600.0], maximized: false };
|
||||||
|
assert_eq!(frame(normal.clone()), Some(g));
|
||||||
|
// Maximized or minimized: the size to restore stays.
|
||||||
|
let big = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(1280.0, 700.0));
|
||||||
|
let maximized = egui::ViewportInfo { inner_rect: Some(big), outer_rect: Some(big), maximized: Some(true), ..normal.clone() };
|
||||||
|
assert_eq!(frame(maximized), Some(WindowGeometry { maximized: true, ..g }));
|
||||||
|
let minimized = egui::ViewportInfo { inner_rect: None, outer_rect: None, minimized: Some(true), ..normal.clone() };
|
||||||
|
assert_eq!(frame(minimized), Some(WindowGeometry { maximized: true, ..g }));
|
||||||
|
assert_eq!(frame(normal), Some(g));
|
||||||
|
// Saved with the preferences; older preference files have no window.
|
||||||
|
let mut ui = vectorcraft_ui_egui::UiState { window: Some(g), ..Default::default() };
|
||||||
|
let back: vectorcraft_ui_egui::UiState = serde_json::from_slice(&serde_json::to_vec(&ui).unwrap()).unwrap();
|
||||||
|
assert_eq!(back.window, Some(g));
|
||||||
|
ui.window = None;
|
||||||
|
let json = serde_json::to_value(&ui).unwrap();
|
||||||
|
assert!(json.get("window").is_none());
|
||||||
|
let back: vectorcraft_ui_egui::UiState = serde_json::from_value(json).unwrap();
|
||||||
|
assert_eq!(back.window, None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -303,6 +303,24 @@ pub struct UiState {
|
||||||
/// becoming active, leaves it.
|
/// becoming active, leaves it.
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
pub home: Option<(Option<u64>, usize)>,
|
pub home: Option<(Option<u64>, usize)>,
|
||||||
|
/// The desktop window's size, position and maximized state, saved when the app quits and
|
||||||
|
/// restored at the next launch (the desktop host reads and writes it; none on the web).
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub window: Option<WindowGeometry>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The desktop window's geometry, kept across launches.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct WindowGeometry {
|
||||||
|
/// Top-left corner of the window frame, in physical pixels on the desktop (none where the
|
||||||
|
/// system doesn't tell windows where they are).
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub pos: Option<[i32; 2]>,
|
||||||
|
/// Size of the window's contents in logical pixels (points at 100% UI scaling).
|
||||||
|
pub size: [f32; 2],
|
||||||
|
/// The window was maximized; `pos` and `size` are where un-maximizing puts it.
|
||||||
|
#[serde(default)]
|
||||||
|
pub maximized: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UiState {
|
impl UiState {
|
||||||
|
|
@ -365,6 +383,7 @@ impl Default for UiState {
|
||||||
eps_options: Value::Null,
|
eps_options: Value::Null,
|
||||||
dxf_import: Value::Null,
|
dxf_import: Value::Null,
|
||||||
home: None,
|
home: None,
|
||||||
|
window: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue