33using Avalonia ;
44using Avalonia . Controls ;
55using Avalonia . Controls . ApplicationLifetimes ;
6+ using Avalonia . Data ;
67using Avalonia . Input ;
78using Avalonia . Interactivity ;
89using Avalonia . Layout ;
@@ -39,19 +40,24 @@ public enum PageType
3940
4041public partial class MainWindow : Window
4142{
42- private const int SW_RESTORE = 9 ;
43+ // Workaround for Avalonia 12 issue #21160 / #21212: BorderOnly + ExtendClientArea
44+ // strips WS_CAPTION / WS_THICKFRAME, which makes DWM disable Aero Snap drag-to-top,
45+ // Win+Up, and the maximize/minimize/restore animations. Re-add those bits on every
46+ // style change. WM_GETMINMAXINFO is also overridden because Avalonia's default values
47+ // on the primary monitor make Aero Snap maximize to the current window size (no-op).
48+ // Targeted upstream fix in Avalonia 12.1.
49+ private const uint WM_STYLECHANGING = 0x007C ;
50+ private const uint WM_GETMINMAXINFO = 0x0024 ;
51+ private const int GWL_STYLE = - 16 ;
52+ private const uint WS_CAPTION = 0x00C00000 ;
53+ private const uint WS_THICKFRAME = 0x00040000 ;
54+ private const uint WS_MINIMIZEBOX = 0x00020000 ;
55+ private const uint WS_MAXIMIZEBOX = 0x00010000 ;
4356 private const uint MONITOR_DEFAULTTONEAREST = 2 ;
44- private const int SM_CXSIZEFRAME = 32 ;
45- private const int SM_CYSIZEFRAME = 33 ;
46- private const int SM_CXPADDEDBORDER = 92 ;
47- private const uint SWP_NOZORDER = 0x0004 ;
48- private const uint SWP_NOACTIVATE = 0x0010 ;
49- private const uint SWP_FRAMECHANGED = 0x0020 ;
5057
5158 private bool _focusSidebarSelectionOnNextPageChange ;
5259 private TrayService ? _trayService ;
5360 private bool _allowClose ;
54- private NativeMethods . RECT ? _windowsRestoreBoundsBeforeManualMaximize ;
5561
5662 public enum RuntimeNotificationLevel
5763 {
@@ -79,6 +85,27 @@ public MainWindow()
7985 _trayService . UpdateStatus ( ) ;
8086 }
8187
88+ protected override void OnOpened ( EventArgs e )
89+ {
90+ base . OnOpened ( e ) ;
91+ if ( ! OperatingSystem . IsWindows ( ) )
92+ return ;
93+
94+ // Install the hook so future style-change attempts by Avalonia can't re-strip our bits.
95+ Win32Properties . AddWndProcHookCallback ( this , OnWindowsWndProc ) ;
96+
97+ // The initial strip already happened during Show() (before this hook could catch it),
98+ // so manually OR our bits back into the current style. DWM picks them up immediately
99+ // and starts honouring Aero Snap / Win+Up / native maximize animations again.
100+ if ( TryGetPlatformHandle ( ) ? . Handle is { } handle && handle != 0 )
101+ {
102+ nint current = NativeMethods . GetWindowLongPtr ( handle , GWL_STYLE ) ;
103+ nint updated = ( nint ) ( ( nuint ) current | WS_CAPTION | WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX ) ;
104+ if ( updated != current )
105+ NativeMethods . SetWindowLongPtr ( handle , GWL_STYLE , updated ) ;
106+ }
107+ }
108+
82109 protected override void OnClosing ( WindowClosingEventArgs e )
83110 {
84111 if ( ! _allowClose && ! Settings . Get ( Settings . K . DisableSystemTray ) )
@@ -169,6 +196,34 @@ private void SetupTitleBar()
169196 // Traffic lights sit on the left → keep the 65 px HamburgerPanel margin.
170197 ExtendClientAreaToDecorationsHint = true ;
171198 ExtendClientAreaTitleBarHeightHint = - 1 ;
199+
200+ // In fullscreen the native title bar is hidden and WindowDecorationMargin
201+ // collapses to 0, which would clip the search box and hamburger. Use a fixed
202+ // title bar height in that state, and drop the traffic-light reservation
203+ // since the traffic lights aren't shown either.
204+ this . GetObservable ( WindowStateProperty ) . Subscribe ( state =>
205+ {
206+ if ( state == WindowState . FullScreen )
207+ {
208+ TitleBarGrid . ClearValue ( HeightProperty ) ;
209+ TitleBarGrid . Height = 44 ;
210+ MainContentGrid . ClearValue ( MarginProperty ) ;
211+ MainContentGrid . Margin = new Thickness ( 0 , 44 , 0 , 0 ) ;
212+ HamburgerPanel . Margin = new Thickness ( 10 , 0 , 8 , 0 ) ;
213+ }
214+ else
215+ {
216+ TitleBarGrid . Bind ( HeightProperty , new Binding ( "WindowDecorationMargin.Top" )
217+ {
218+ RelativeSource = new RelativeSource ( RelativeSourceMode . FindAncestor ) { AncestorType = typeof ( Window ) } ,
219+ } ) ;
220+ MainContentGrid . Bind ( MarginProperty , new Binding ( "WindowDecorationMargin" )
221+ {
222+ RelativeSource = new RelativeSource ( RelativeSourceMode . FindAncestor ) { AncestorType = typeof ( Window ) } ,
223+ } ) ;
224+ HamburgerPanel . Margin = new Thickness ( 65 , 0 , 8 , 0 ) ;
225+ }
226+ } ) ;
172227 }
173228 else if ( OperatingSystem . IsWindows ( ) )
174229 {
@@ -182,7 +237,7 @@ private void SetupTitleBar()
182237 MainContentGrid . Margin = new Thickness ( 0 , 44 , 0 , 0 ) ;
183238 this . GetObservable ( WindowStateProperty ) . Subscribe ( state =>
184239 {
185- UpdateMaximizeButtonState ( state == WindowState . Maximized || _windowsRestoreBoundsBeforeManualMaximize is not null ) ;
240+ UpdateMaximizeButtonState ( state == WindowState . Maximized ) ;
186241 } ) ;
187242 }
188243 else if ( OperatingSystem . IsLinux ( ) )
@@ -301,12 +356,6 @@ private void MinimizeButton_Click(object? sender, RoutedEventArgs e)
301356
302357 private void MaximizeButton_Click ( object ? sender , RoutedEventArgs e )
303358 {
304- if ( OperatingSystem . IsWindows ( ) && TryGetNativeWindowHandle ( ) is { } handle )
305- {
306- ToggleWindowsManualMaximize ( handle ) ;
307- return ;
308- }
309-
310359 WindowState = WindowState == WindowState . Maximized
311360 ? WindowState . Normal
312361 : WindowState . Maximized ;
@@ -323,121 +372,64 @@ private void UpdateMaximizeButtonState(bool isMaximized)
323372 CoreTools . Translate ( isMaximized ? "Restore" : "Maximize" ) ) ;
324373 }
325374
326- private nint ? TryGetNativeWindowHandle ( )
375+ private static nint OnWindowsWndProc ( nint hWnd , uint msg , nint wParam , nint lParam , ref bool handled )
327376 {
328- var handle = TryGetPlatformHandle ( ) ? . Handle ?? 0 ;
329- return handle == 0 ? null : handle ;
330- }
331-
332- private void ToggleWindowsManualMaximize ( nint handle )
333- {
334- if ( _windowsRestoreBoundsBeforeManualMaximize is { } restoreBounds )
335- {
336- if ( SetWindowsWindowBounds ( handle , restoreBounds ) )
377+ // Intercept SetWindowLong(GWL_STYLE, ...) attempts and OR our required bits back into
378+ // the new style before Windows accepts the change. lParam points to a STYLESTRUCT
379+ // whose styleNew member is the proposed new style. We modify it in place and let the
380+ // chain continue (no handled=true) so Avalonia / DefWindowProc still process the
381+ // (now-corrected) message.
382+ if ( msg == WM_STYLECHANGING && wParam . ToInt64 ( ) == GWL_STYLE )
383+ {
384+ var ss = Marshal . PtrToStructure < NativeMethods . STYLESTRUCT > ( lParam ) ;
385+ uint preserved = ss . styleNew | WS_CAPTION | WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX ;
386+ if ( preserved != ss . styleNew )
337387 {
338- _windowsRestoreBoundsBeforeManualMaximize = null ;
339- UpdateMaximizeButtonState ( false ) ;
388+ ss . styleNew = preserved ;
389+ Marshal . StructureToPtr ( ss , lParam , false ) ;
340390 }
341- return ;
342- }
343-
344- if ( NativeMethods . IsZoomed ( handle ) )
345- {
346- _ = NativeMethods . ShowWindow ( handle , SW_RESTORE ) ;
347- UpdateMaximizeButtonState ( false ) ;
348- return ;
349- }
350-
351- if ( ! NativeMethods . GetWindowRect ( handle , out NativeMethods . RECT currentBounds ) )
352- {
353- Logger . Warn ( "Could not get the window bounds before maximizing." ) ;
354- return ;
355- }
356-
357- var monitor = NativeMethods . MonitorFromWindow ( handle , MONITOR_DEFAULTTONEAREST ) ;
358- if ( monitor == 0 )
359- {
360- Logger . Warn ( "Could not find a monitor for the window before maximizing." ) ;
361- return ;
362391 }
363392
364- var monitorInfo = new NativeMethods . MONITORINFO
365- {
366- cbSize = Marshal . SizeOf < NativeMethods . MONITORINFO > ( ) ,
367- } ;
368- if ( ! NativeMethods . GetMonitorInfo ( monitor , ref monitorInfo ) )
369- {
370- Logger . Warn ( "Could not get monitor bounds before maximizing." ) ;
371- return ;
372- }
373-
374- if ( SetWindowsWindowBounds ( handle , GetMaximizedWindowBounds ( handle , monitorInfo . rcWork ) ) )
375- {
376- _windowsRestoreBoundsBeforeManualMaximize = currentBounds ;
377- UpdateMaximizeButtonState ( true ) ;
393+ // Override the max-size / max-position Avalonia would otherwise provide. On the
394+ // primary monitor (where the taskbar lives) Avalonia's defaults can leave ptMaxSize
395+ // equal to the current window size, so Aero Snap drag-to-top "maximizes" to the same
396+ // bounds and the window appears not to resize. We always report the current monitor's
397+ // work area, which is what Windows actually uses for native maximize.
398+ // handled = true so Avalonia's own WM_GETMINMAXINFO handler can't run after us and
399+ // overwrite the values we just set.
400+ if ( msg == WM_GETMINMAXINFO )
401+ {
402+ nint monitor = NativeMethods . MonitorFromWindow ( hWnd , MONITOR_DEFAULTTONEAREST ) ;
403+ if ( monitor != 0 )
404+ {
405+ var mi = new NativeMethods . MONITORINFO { cbSize = Marshal . SizeOf < NativeMethods . MONITORINFO > ( ) } ;
406+ if ( NativeMethods . GetMonitorInfo ( monitor , ref mi ) )
407+ {
408+ var mmi = Marshal . PtrToStructure < NativeMethods . MINMAXINFO > ( lParam ) ;
409+ mmi . ptMaxPosition . X = mi . rcWork . Left - mi . rcMonitor . Left ;
410+ mmi . ptMaxPosition . Y = mi . rcWork . Top - mi . rcMonitor . Top ;
411+ mmi . ptMaxSize . X = mi . rcWork . Right - mi . rcWork . Left ;
412+ mmi . ptMaxSize . Y = mi . rcWork . Bottom - mi . rcWork . Top ;
413+ if ( mmi . ptMaxTrackSize . X < mmi . ptMaxSize . X ) mmi . ptMaxTrackSize . X = mmi . ptMaxSize . X ;
414+ if ( mmi . ptMaxTrackSize . Y < mmi . ptMaxSize . Y ) mmi . ptMaxTrackSize . Y = mmi . ptMaxSize . Y ;
415+ Marshal . StructureToPtr ( mmi , lParam , false ) ;
416+ handled = true ;
417+ return 0 ;
418+ }
419+ }
378420 }
421+ return 0 ;
379422 }
380423
381- private static NativeMethods . RECT GetMaximizedWindowBounds ( nint handle , NativeMethods . RECT workArea )
382- {
383- uint dpi = NativeMethods . GetDpiForWindow ( handle ) ;
384- if ( dpi == 0 )
385- dpi = NativeMethods . GetDpiForSystem ( ) ;
386-
387- int frameX = NativeMethods . GetSystemMetricsForDpi ( SM_CXSIZEFRAME , dpi )
388- + NativeMethods . GetSystemMetricsForDpi ( SM_CXPADDEDBORDER , dpi ) ;
389- int frameY = NativeMethods . GetSystemMetricsForDpi ( SM_CYSIZEFRAME , dpi )
390- + NativeMethods . GetSystemMetricsForDpi ( SM_CXPADDEDBORDER , dpi ) ;
391-
392- frameX = Math . Max ( frameX , 8 ) ;
393- frameY = Math . Max ( frameY , 8 ) ;
394-
395- return new NativeMethods . RECT
396- {
397- Left = workArea . Left - frameX ,
398- Top = workArea . Top - frameY ,
399- Right = workArea . Right + frameX ,
400- Bottom = workArea . Bottom + frameY ,
401- } ;
402- }
403-
404- private static bool SetWindowsWindowBounds ( nint handle , NativeMethods . RECT bounds )
405- {
406- bool result = NativeMethods . SetWindowPos (
407- handle ,
408- 0 ,
409- bounds . Left ,
410- bounds . Top ,
411- bounds . Right - bounds . Left ,
412- bounds . Bottom - bounds . Top ,
413- SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED ) ;
414- if ( ! result )
415- Logger . Warn ( $ "Could not set window bounds. Win32 error: { Marshal . GetLastWin32Error ( ) } ") ;
416- return result ;
417- }
418-
424+ // P/Invokes compile on any platform; they are only called from code paths guarded by
425+ // OperatingSystem.IsWindows(), so non-Windows targets never invoke user32.dll at runtime.
419426 private static class NativeMethods
420427 {
421- [ DllImport ( "user32.dll" ) ]
422- [ return : MarshalAs ( UnmanagedType . Bool ) ]
423- public static extern bool IsZoomed ( nint hWnd ) ;
428+ [ DllImport ( "user32.dll" , EntryPoint = "GetWindowLongPtrW" , SetLastError = true ) ]
429+ public static extern nint GetWindowLongPtr ( nint hWnd , int nIndex ) ;
424430
425- [ DllImport ( "user32.dll" ) ]
426- [ return : MarshalAs ( UnmanagedType . Bool ) ]
427- public static extern bool ShowWindow ( nint hWnd , int nCmdShow ) ;
428-
429- [ DllImport ( "user32.dll" ) ]
430- public static extern uint GetDpiForWindow ( nint hwnd ) ;
431-
432- [ DllImport ( "user32.dll" ) ]
433- public static extern uint GetDpiForSystem ( ) ;
434-
435- [ DllImport ( "user32.dll" ) ]
436- public static extern int GetSystemMetricsForDpi ( int nIndex , uint dpi ) ;
437-
438- [ DllImport ( "user32.dll" , SetLastError = true ) ]
439- [ return : MarshalAs ( UnmanagedType . Bool ) ]
440- public static extern bool GetWindowRect ( nint hWnd , out RECT lpRect ) ;
431+ [ DllImport ( "user32.dll" , EntryPoint = "SetWindowLongPtrW" , SetLastError = true ) ]
432+ public static extern nint SetWindowLongPtr ( nint hWnd , int nIndex , nint dwNewLong ) ;
441433
442434 [ DllImport ( "user32.dll" ) ]
443435 public static extern nint MonitorFromWindow ( nint hwnd , uint dwFlags ) ;
@@ -446,16 +438,29 @@ private static class NativeMethods
446438 [ return : MarshalAs ( UnmanagedType . Bool ) ]
447439 public static extern bool GetMonitorInfo ( nint hMonitor , ref MONITORINFO lpmi ) ;
448440
449- [ DllImport ( "user32.dll" , SetLastError = true ) ]
450- [ return : MarshalAs ( UnmanagedType . Bool ) ]
451- public static extern bool SetWindowPos (
452- nint hWnd ,
453- nint hWndInsertAfter ,
454- int X ,
455- int Y ,
456- int cx ,
457- int cy ,
458- uint uFlags ) ;
441+ [ StructLayout ( LayoutKind . Sequential ) ]
442+ public struct STYLESTRUCT
443+ {
444+ public uint styleOld ;
445+ public uint styleNew ;
446+ }
447+
448+ [ StructLayout ( LayoutKind . Sequential ) ]
449+ public struct POINT
450+ {
451+ public int X ;
452+ public int Y ;
453+ }
454+
455+ [ StructLayout ( LayoutKind . Sequential ) ]
456+ public struct MINMAXINFO
457+ {
458+ public POINT ptReserved ;
459+ public POINT ptMaxSize ;
460+ public POINT ptMaxPosition ;
461+ public POINT ptMinTrackSize ;
462+ public POINT ptMaxTrackSize ;
463+ }
459464
460465 [ StructLayout ( LayoutKind . Sequential ) ]
461466 public struct RECT
0 commit comments