Skip to content

Commit 5c24a77

Browse files
authored
Avalonia: remember size and position (#4805)
1 parent 9d4a429 commit 5c24a77

1 file changed

Lines changed: 188 additions & 3 deletions

File tree

src/UniGetUI.Avalonia/Views/MainWindow.axaml.cs

Lines changed: 188 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,15 @@ public MainWindow()
8080
InitializeComponent();
8181
SetupTitleBar();
8282

83+
RestoreGeometry();
84+
8385
KeyDown += Window_KeyDown;
8486
ViewModel.CurrentPageChanged += OnCurrentPageChanged;
8587

88+
Resized += (_, _) => _ = SaveGeometryAsync();
89+
PositionChanged += (_, _) => _ = SaveGeometryAsync();
90+
this.GetObservable(WindowStateProperty).Subscribe(state => { _ = SaveGeometryAsync(); });
91+
8692
_trayService = new TrayService(this);
8793
_trayService.UpdateStatus();
8894
}
@@ -117,6 +123,7 @@ protected override void OnClosing(WindowClosingEventArgs e)
117123
return;
118124
}
119125

126+
SaveGeometryNow();
120127
AvaloniaAutoUpdater.ReleaseLockForAutoupdate_Window = true;
121128
_trayService?.Dispose();
122129
_trayService = null;
@@ -266,7 +273,6 @@ private void SetupTitleBar()
266273
{
267274
CreateResizeGrips();
268275
}
269-
270276
}
271277
}
272278

@@ -441,7 +447,143 @@ static Border MakeGrip(MainWindow window, double width, double height,
441447
};
442448
return grip;
443449
}
450+
}
451+
452+
private async Task SaveGeometryAsync()
453+
{
454+
try
455+
{
456+
int oldWidth = (int)Width;
457+
int oldHeight = (int)Height;
458+
PixelPoint oldPosition = Position;
459+
WindowState oldState = WindowState;
460+
await Task.Delay(100);
461+
462+
if (oldWidth != (int)Width || oldHeight != (int)Height
463+
|| oldPosition != Position || oldState != WindowState)
464+
return;
465+
466+
SaveGeometryNow();
467+
}
468+
catch (Exception ex)
469+
{
470+
Logger.Error(ex);
471+
}
472+
}
473+
474+
private void SaveGeometryNow()
475+
{
476+
try
477+
{
478+
int state = WindowState == WindowState.Maximized ? 1 : 0;
479+
string geometry = $"v2,{Position.X},{Position.Y},{(int)Width},{(int)Height},{state}";
480+
Settings.SetValue(Settings.K.WindowGeometry, geometry);
481+
}
482+
catch (Exception ex)
483+
{
484+
Logger.Error(ex);
485+
}
486+
}
487+
488+
private void RestoreGeometry()
489+
{
490+
string geometry = Settings.GetValue(Settings.K.WindowGeometry);
491+
if (string.IsNullOrEmpty(geometry))
492+
return;
493+
494+
string[] items = geometry.Split(',');
495+
if (items.Length is not (5 or 6))
496+
{
497+
Logger.Warn($"The restored geometry did not have a supported item count (found length was {items.Length})");
498+
return;
499+
}
500+
501+
int x, y, width, height, state;
502+
try
503+
{
504+
if (items.Length == 6 && items[0] == "v2")
505+
{
506+
x = int.Parse(items[1]);
507+
y = int.Parse(items[2]);
508+
width = int.Parse(items[3]);
509+
height = int.Parse(items[4]);
510+
state = int.Parse(items[5]);
511+
}
512+
else
513+
{
514+
x = int.Parse(items[0]);
515+
y = int.Parse(items[1]);
516+
width = int.Parse(items[2]);
517+
height = int.Parse(items[3]);
518+
state = int.Parse(items[4]);
519+
}
520+
}
521+
catch (Exception ex)
522+
{
523+
Logger.Error("Could not parse window geometry integers");
524+
Logger.Error(ex);
525+
return;
526+
}
527+
528+
WindowStartupLocation = WindowStartupLocation.Manual;
529+
530+
if (state == 1)
531+
{
532+
// Mirror WinUI behaviour: don't reapply the saved (maximized) bounds, just
533+
// maximize. The OS / Avalonia picks a sensible un-maximize restore size.
534+
WindowState = WindowState.Maximized;
535+
}
536+
else if (IsRectangleFullyVisible(x, y, width, height))
537+
{
538+
Width = width;
539+
Height = height;
540+
Position = new PixelPoint(x, y);
541+
}
542+
else
543+
{
544+
Logger.Warn("Restored geometry was outside of desktop bounds");
545+
}
546+
}
547+
548+
private bool IsRectangleFullyVisible(int x, int y, int width, int height)
549+
{
550+
// Position is in screen pixels, Width/Height are DIPs. Scale width/height
551+
// by the DPI of the screen that contains the saved position before comparing
552+
// against the union of all monitor bounds (which Avalonia reports in pixels).
553+
var screens = Screens?.All;
554+
if (screens is null || screens.Count == 0)
555+
return true;
556+
557+
int minX = int.MaxValue, minY = int.MaxValue;
558+
int maxX = int.MinValue, maxY = int.MinValue;
559+
double hostScaling = 1.0;
560+
bool foundHost = false;
561+
562+
foreach (var screen in screens)
563+
{
564+
var bounds = screen.Bounds;
565+
if (bounds.X < minX) minX = bounds.X;
566+
if (bounds.Y < minY) minY = bounds.Y;
567+
if (bounds.X + bounds.Width > maxX) maxX = bounds.X + bounds.Width;
568+
if (bounds.Y + bounds.Height > maxY) maxY = bounds.Y + bounds.Height;
569+
570+
if (!foundHost && bounds.Contains(new PixelPoint(x, y)))
571+
{
572+
hostScaling = screen.Scaling;
573+
foundHost = true;
574+
}
575+
}
576+
577+
if (!foundHost)
578+
hostScaling = Screens?.Primary?.Scaling ?? 1.0;
579+
580+
int widthPx = (int)(width * hostScaling);
581+
int heightPx = (int)(height * hostScaling);
444582

583+
if (x + 10 < minX || x + widthPx - 10 > maxX || y + 10 < minY || y + heightPx - 10 > maxY)
584+
return false;
585+
586+
return true;
445587
}
446588

447589
private void MinimizeButton_Click(object? sender, RoutedEventArgs e)
@@ -505,6 +647,32 @@ private static nint OnWindowsWndProc(nint hWnd, uint msg, nint wParam, nint lPar
505647
mmi.ptMaxSize.Y = mi.rcWork.Bottom - mi.rcWork.Top;
506648
if (mmi.ptMaxTrackSize.X < mmi.ptMaxSize.X) mmi.ptMaxTrackSize.X = mmi.ptMaxSize.X;
507649
if (mmi.ptMaxTrackSize.Y < mmi.ptMaxSize.Y) mmi.ptMaxTrackSize.Y = mmi.ptMaxSize.Y;
650+
// Set ptMinTrackSize to MinWidth/MinHeight in DIPs plus the real
651+
// WS_THICKFRAME inset. Avalonia's own handler would omit the inset for
652+
// BorderOnly (BorderThickness returns 0), letting the outer window shrink
653+
// below the client minimum — Avalonia then grows it back via SetWindowPos
654+
// pinning x, pushing the right edge → the window slides past MinWidth.
655+
if (Instance is { } w)
656+
{
657+
uint dpi = NativeMethods.GetDpiForWindow(hWnd);
658+
if (dpi == 0) dpi = 96;
659+
double scale = dpi / 96.0;
660+
uint style = (uint)NativeMethods.GetWindowLongPtr(hWnd, GWL_STYLE).ToInt64();
661+
662+
var frame = default(NativeMethods.RECT);
663+
int frameW = 0, frameH = 0;
664+
if (NativeMethods.AdjustWindowRectExForDpi(ref frame, style, false, 0, dpi))
665+
{
666+
frameW = (-frame.Left) + frame.Right;
667+
frameH = (-frame.Top) + frame.Bottom;
668+
}
669+
670+
int minX = (int)Math.Ceiling(w.MinWidth * scale) + frameW;
671+
int minY = (int)Math.Ceiling(w.MinHeight * scale) + frameH;
672+
if (mmi.ptMinTrackSize.X < minX) mmi.ptMinTrackSize.X = minX;
673+
if (mmi.ptMinTrackSize.Y < minY) mmi.ptMinTrackSize.Y = minY;
674+
}
675+
508676
Marshal.StructureToPtr(mmi, lParam, false);
509677
handled = true;
510678
return 0;
@@ -524,6 +692,13 @@ private static class NativeMethods
524692
[DllImport("user32.dll", EntryPoint = "SetWindowLongPtrW", SetLastError = true)]
525693
public static extern nint SetWindowLongPtr(nint hWnd, int nIndex, nint dwNewLong);
526694

695+
[DllImport("user32.dll")]
696+
public static extern uint GetDpiForWindow(nint hWnd);
697+
698+
[DllImport("user32.dll", SetLastError = true)]
699+
[return: MarshalAs(UnmanagedType.Bool)]
700+
public static extern bool AdjustWindowRectExForDpi(ref RECT lpRect, uint dwStyle, [MarshalAs(UnmanagedType.Bool)] bool bMenu, uint dwExStyle, uint dpi);
701+
527702
[DllImport("user32.dll")]
528703
public static extern nint MonitorFromWindow(nint hwnd, uint dwFlags);
529704

@@ -579,8 +754,18 @@ private void CloseButton_Click(object? sender, RoutedEventArgs e)
579754

580755
private void TitleBar_PointerPressed(object? sender, PointerPressedEventArgs e)
581756
{
582-
if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
583-
BeginMoveDrag(e);
757+
if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
758+
return;
759+
760+
if (e.ClickCount == 2)
761+
{
762+
WindowState = WindowState == WindowState.Maximized
763+
? WindowState.Normal
764+
: WindowState.Maximized;
765+
return;
766+
}
767+
768+
BeginMoveDrag(e);
584769
}
585770

586771
private void SearchBox_KeyDown(object? sender, KeyEventArgs e)

0 commit comments

Comments
 (0)