@@ -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