From 0d7207e58e7bf5853bf0daf571bc94c78fb41eb1 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Mon, 9 Nov 2026 22:05:09 -0500 Subject: [PATCH] feat(avalonia): remember the main window's size, position and state The Avalonia main window restores its last size, position and maximized state from the shared user settings and saves them when it closes, keeping the normal bounds while maximized. On Windows it reads what the WinForms app saved: WinForms stores the outer window and Avalonia sets the client area, so the size differs by the frame between the two apps. --snapshot skips it, so its image never depends on the user's settings. --- OpenNest.Avalonia/App.axaml.cs | 2 + OpenNest.Avalonia/MainWindow.axaml.cs | 9 ++ OpenNest.Avalonia/WindowPlacementBinding.cs | 111 ++++++++++++++++++++ 3 files changed, 122 insertions(+) create mode 100644 OpenNest.Avalonia/WindowPlacementBinding.cs diff --git a/OpenNest.Avalonia/App.axaml.cs b/OpenNest.Avalonia/App.axaml.cs index 5716015f..67322799 100644 --- a/OpenNest.Avalonia/App.axaml.cs +++ b/OpenNest.Avalonia/App.axaml.cs @@ -25,6 +25,8 @@ namespace OpenNest.Avalonia if (snapshot >= 0 && snapshot + 1 < args.Length) window.SaveSnapshotAndClose(args[snapshot + 1], snapshot + 2 < args.Length && int.TryParse(args[snapshot + 2], out var plate) ? plate : 1); + else + window.RememberPlacement(); } base.OnFrameworkInitializationCompleted(); diff --git a/OpenNest.Avalonia/MainWindow.axaml.cs b/OpenNest.Avalonia/MainWindow.axaml.cs index 09de9848..9a32590e 100644 --- a/OpenNest.Avalonia/MainWindow.axaml.cs +++ b/OpenNest.Avalonia/MainWindow.axaml.cs @@ -74,6 +74,15 @@ namespace OpenNest.Avalonia PlateList.SelectedIndex = nest.Plates.Count > 0 ? System.Math.Clamp(selectIndex, 0, nest.Plates.Count - 1) : -1; } + /// + /// Restores the window's size, position and maximized state from the user settings the + /// WinForms app shares, and saves them on close. Not used for --snapshot, whose image + /// must not depend on the user's settings. + /// + public void RememberPlacement() => + WindowPlacementBinding.Remember( + this, WindowPlacement.MainWindow, Data.UserSettingsFile.DefaultPath); + /// Runs Auto Nest once the window opens, for --auto-nest. public void AutoNestOnOpen() => Opened += (_, _) => autoNest = RunAutoNestAsync(); diff --git a/OpenNest.Avalonia/WindowPlacementBinding.cs b/OpenNest.Avalonia/WindowPlacementBinding.cs new file mode 100644 index 00000000..c75b4717 --- /dev/null +++ b/OpenNest.Avalonia/WindowPlacementBinding.cs @@ -0,0 +1,111 @@ +using System; +using System.Linq; +using Avalonia; +using Avalonia.Controls; +using OpenNest.Data; +using OpenNest.Desktop; + +namespace OpenNest.Avalonia +{ + /// + /// Restores a window's size, position and maximized state from the shared user-settings + /// file and saves them when it closes, under the same names the WinForms app uses. + /// + /// + /// Sizes are stored in screen pixels. Avalonia sets the client area, while WinForms stores + /// the outer window, so a size saved by one app opens a frame's width different in the + /// other; each app's own round trip is exact. + /// + internal sealed class WindowPlacementBinding + { + private readonly Window window; + private readonly string name; + private readonly string settingsPath; + private PixelPoint? normalPosition; + private (int Width, int Height)? normalSize; + + private WindowPlacementBinding(Window window, string name, string settingsPath) + { + this.window = window; + this.name = name; + this.settingsPath = settingsPath; + } + + /// Call before the window is shown. + public static void Remember(Window window, string name, string settingsPath) + { + var binding = new WindowPlacementBinding(window, name, settingsPath); + binding.Restore(); + window.Opened += (_, _) => binding.Track(); + window.PositionChanged += (_, _) => binding.Track(); + window.PropertyChanged += (_, e) => + { + if (e.Property == TopLevel.ClientSizeProperty + || e.Property == Window.WindowStateProperty) + binding.Track(); + }; + window.Closing += (_, _) => binding.Save(); + } + + private void Restore() + { + var saved = WindowPlacement.Read(UserSettingsFile.Read(settingsPath), name); + var screens = window.Screens?.All; + if (saved == null || screens == null) + return; + + var areas = screens + .Select(s => s.WorkingArea) + .Select(a => new OpenNest.Desktop.PixelRect(a.X, a.Y, a.Width, a.Height)) + .ToList(); + var placement = saved.FitTo(areas); + var screen = placement.Location is { } at + ? screens.First(s => s.WorkingArea.Contains(new PixelPoint(at.X, at.Y))) + : screens.FirstOrDefault(s => s.IsPrimary) ?? screens.FirstOrDefault(); + + if (placement.Location is { } location) + { + window.WindowStartupLocation = WindowStartupLocation.Manual; + window.Position = new PixelPoint(location.X, location.Y); + } + + if (placement.Size is { } size) + { + var scaling = screen?.Scaling ?? 1; + window.Width = size.Width / scaling; + window.Height = size.Height / scaling; + } + + if (placement.Maximized) + window.WindowState = WindowState.Maximized; + } + + /// Remembers the normal bounds, which maximizing keeps. + private void Track() + { + if (window.WindowState != WindowState.Normal || !window.IsVisible) + return; + + normalPosition = window.Position; + var scaling = window.DesktopScaling; + normalSize = ((int)System.Math.Round(window.ClientSize.Width * scaling), + (int)System.Math.Round(window.ClientSize.Height * scaling)); + } + + private void Save() + { + Track(); + var maximized = window.WindowState == WindowState.Maximized; + UserSettingsFile.Update(settingsPath, values => + { + // A window never shown normal this session keeps the bounds saved before. + var previous = WindowPlacement.Read(values, name); + var placement = new WindowPlacement( + normalPosition is { } p ? (p.X, p.Y) : previous?.Location, + normalSize ?? previous?.Size, + maximized); + placement.WriteTo(values, name); + }); + } + } +}