diff --git a/src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj b/src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj
index bc185e5bd..d884f5147 100644
--- a/src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj
+++ b/src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj
@@ -70,17 +70,17 @@
-
-
-
-
+
+
+
+
-
+
diff --git a/src/UniGetUI.Avalonia/Views/MainWindow.axaml.cs b/src/UniGetUI.Avalonia/Views/MainWindow.axaml.cs
index 67d513d5d..5af8a614e 100644
--- a/src/UniGetUI.Avalonia/Views/MainWindow.axaml.cs
+++ b/src/UniGetUI.Avalonia/Views/MainWindow.axaml.cs
@@ -3,6 +3,7 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
+using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Layout;
@@ -39,19 +40,24 @@ public enum PageType
public partial class MainWindow : Window
{
- private const int SW_RESTORE = 9;
+ // Workaround for Avalonia 12 issue #21160 / #21212: BorderOnly + ExtendClientArea
+ // strips WS_CAPTION / WS_THICKFRAME, which makes DWM disable Aero Snap drag-to-top,
+ // Win+Up, and the maximize/minimize/restore animations. Re-add those bits on every
+ // style change. WM_GETMINMAXINFO is also overridden because Avalonia's default values
+ // on the primary monitor make Aero Snap maximize to the current window size (no-op).
+ // Targeted upstream fix in Avalonia 12.1.
+ private const uint WM_STYLECHANGING = 0x007C;
+ private const uint WM_GETMINMAXINFO = 0x0024;
+ private const int GWL_STYLE = -16;
+ private const uint WS_CAPTION = 0x00C00000;
+ private const uint WS_THICKFRAME = 0x00040000;
+ private const uint WS_MINIMIZEBOX = 0x00020000;
+ private const uint WS_MAXIMIZEBOX = 0x00010000;
private const uint MONITOR_DEFAULTTONEAREST = 2;
- private const int SM_CXSIZEFRAME = 32;
- private const int SM_CYSIZEFRAME = 33;
- private const int SM_CXPADDEDBORDER = 92;
- private const uint SWP_NOZORDER = 0x0004;
- private const uint SWP_NOACTIVATE = 0x0010;
- private const uint SWP_FRAMECHANGED = 0x0020;
private bool _focusSidebarSelectionOnNextPageChange;
private TrayService? _trayService;
private bool _allowClose;
- private NativeMethods.RECT? _windowsRestoreBoundsBeforeManualMaximize;
public enum RuntimeNotificationLevel
{
@@ -79,6 +85,27 @@ public MainWindow()
_trayService.UpdateStatus();
}
+ protected override void OnOpened(EventArgs e)
+ {
+ base.OnOpened(e);
+ if (!OperatingSystem.IsWindows())
+ return;
+
+ // Install the hook so future style-change attempts by Avalonia can't re-strip our bits.
+ Win32Properties.AddWndProcHookCallback(this, OnWindowsWndProc);
+
+ // The initial strip already happened during Show() (before this hook could catch it),
+ // so manually OR our bits back into the current style. DWM picks them up immediately
+ // and starts honouring Aero Snap / Win+Up / native maximize animations again.
+ if (TryGetPlatformHandle()?.Handle is { } handle && handle != 0)
+ {
+ nint current = NativeMethods.GetWindowLongPtr(handle, GWL_STYLE);
+ nint updated = (nint)((nuint)current | WS_CAPTION | WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX);
+ if (updated != current)
+ NativeMethods.SetWindowLongPtr(handle, GWL_STYLE, updated);
+ }
+ }
+
protected override void OnClosing(WindowClosingEventArgs e)
{
if (!_allowClose && !Settings.Get(Settings.K.DisableSystemTray))
@@ -169,6 +196,34 @@ private void SetupTitleBar()
// Traffic lights sit on the left → keep the 65 px HamburgerPanel margin.
ExtendClientAreaToDecorationsHint = true;
ExtendClientAreaTitleBarHeightHint = -1;
+
+ // In fullscreen the native title bar is hidden and WindowDecorationMargin
+ // collapses to 0, which would clip the search box and hamburger. Use a fixed
+ // title bar height in that state, and drop the traffic-light reservation
+ // since the traffic lights aren't shown either.
+ this.GetObservable(WindowStateProperty).Subscribe(state =>
+ {
+ if (state == WindowState.FullScreen)
+ {
+ TitleBarGrid.ClearValue(HeightProperty);
+ TitleBarGrid.Height = 44;
+ MainContentGrid.ClearValue(MarginProperty);
+ MainContentGrid.Margin = new Thickness(0, 44, 0, 0);
+ HamburgerPanel.Margin = new Thickness(10, 0, 8, 0);
+ }
+ else
+ {
+ TitleBarGrid.Bind(HeightProperty, new Binding("WindowDecorationMargin.Top")
+ {
+ RelativeSource = new RelativeSource(RelativeSourceMode.FindAncestor) { AncestorType = typeof(Window) },
+ });
+ MainContentGrid.Bind(MarginProperty, new Binding("WindowDecorationMargin")
+ {
+ RelativeSource = new RelativeSource(RelativeSourceMode.FindAncestor) { AncestorType = typeof(Window) },
+ });
+ HamburgerPanel.Margin = new Thickness(65, 0, 8, 0);
+ }
+ });
}
else if (OperatingSystem.IsWindows())
{
@@ -182,7 +237,7 @@ private void SetupTitleBar()
MainContentGrid.Margin = new Thickness(0, 44, 0, 0);
this.GetObservable(WindowStateProperty).Subscribe(state =>
{
- UpdateMaximizeButtonState(state == WindowState.Maximized || _windowsRestoreBoundsBeforeManualMaximize is not null);
+ UpdateMaximizeButtonState(state == WindowState.Maximized);
});
}
else if (OperatingSystem.IsLinux())
@@ -301,12 +356,6 @@ private void MinimizeButton_Click(object? sender, RoutedEventArgs e)
private void MaximizeButton_Click(object? sender, RoutedEventArgs e)
{
- if (OperatingSystem.IsWindows() && TryGetNativeWindowHandle() is { } handle)
- {
- ToggleWindowsManualMaximize(handle);
- return;
- }
-
WindowState = WindowState == WindowState.Maximized
? WindowState.Normal
: WindowState.Maximized;
@@ -323,121 +372,64 @@ private void UpdateMaximizeButtonState(bool isMaximized)
CoreTools.Translate(isMaximized ? "Restore" : "Maximize"));
}
- private nint? TryGetNativeWindowHandle()
+ private static nint OnWindowsWndProc(nint hWnd, uint msg, nint wParam, nint lParam, ref bool handled)
{
- var handle = TryGetPlatformHandle()?.Handle ?? 0;
- return handle == 0 ? null : handle;
- }
-
- private void ToggleWindowsManualMaximize(nint handle)
- {
- if (_windowsRestoreBoundsBeforeManualMaximize is { } restoreBounds)
- {
- if (SetWindowsWindowBounds(handle, restoreBounds))
+ // Intercept SetWindowLong(GWL_STYLE, ...) attempts and OR our required bits back into
+ // the new style before Windows accepts the change. lParam points to a STYLESTRUCT
+ // whose styleNew member is the proposed new style. We modify it in place and let the
+ // chain continue (no handled=true) so Avalonia / DefWindowProc still process the
+ // (now-corrected) message.
+ if (msg == WM_STYLECHANGING && wParam.ToInt64() == GWL_STYLE)
+ {
+ var ss = Marshal.PtrToStructure(lParam);
+ uint preserved = ss.styleNew | WS_CAPTION | WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX;
+ if (preserved != ss.styleNew)
{
- _windowsRestoreBoundsBeforeManualMaximize = null;
- UpdateMaximizeButtonState(false);
+ ss.styleNew = preserved;
+ Marshal.StructureToPtr(ss, lParam, false);
}
- return;
- }
-
- if (NativeMethods.IsZoomed(handle))
- {
- _ = NativeMethods.ShowWindow(handle, SW_RESTORE);
- UpdateMaximizeButtonState(false);
- return;
- }
-
- if (!NativeMethods.GetWindowRect(handle, out NativeMethods.RECT currentBounds))
- {
- Logger.Warn("Could not get the window bounds before maximizing.");
- return;
- }
-
- var monitor = NativeMethods.MonitorFromWindow(handle, MONITOR_DEFAULTTONEAREST);
- if (monitor == 0)
- {
- Logger.Warn("Could not find a monitor for the window before maximizing.");
- return;
}
- var monitorInfo = new NativeMethods.MONITORINFO
- {
- cbSize = Marshal.SizeOf(),
- };
- if (!NativeMethods.GetMonitorInfo(monitor, ref monitorInfo))
- {
- Logger.Warn("Could not get monitor bounds before maximizing.");
- return;
- }
-
- if (SetWindowsWindowBounds(handle, GetMaximizedWindowBounds(handle, monitorInfo.rcWork)))
- {
- _windowsRestoreBoundsBeforeManualMaximize = currentBounds;
- UpdateMaximizeButtonState(true);
+ // Override the max-size / max-position Avalonia would otherwise provide. On the
+ // primary monitor (where the taskbar lives) Avalonia's defaults can leave ptMaxSize
+ // equal to the current window size, so Aero Snap drag-to-top "maximizes" to the same
+ // bounds and the window appears not to resize. We always report the current monitor's
+ // work area, which is what Windows actually uses for native maximize.
+ // handled = true so Avalonia's own WM_GETMINMAXINFO handler can't run after us and
+ // overwrite the values we just set.
+ if (msg == WM_GETMINMAXINFO)
+ {
+ nint monitor = NativeMethods.MonitorFromWindow(hWnd, MONITOR_DEFAULTTONEAREST);
+ if (monitor != 0)
+ {
+ var mi = new NativeMethods.MONITORINFO { cbSize = Marshal.SizeOf() };
+ if (NativeMethods.GetMonitorInfo(monitor, ref mi))
+ {
+ var mmi = Marshal.PtrToStructure(lParam);
+ mmi.ptMaxPosition.X = mi.rcWork.Left - mi.rcMonitor.Left;
+ mmi.ptMaxPosition.Y = mi.rcWork.Top - mi.rcMonitor.Top;
+ mmi.ptMaxSize.X = mi.rcWork.Right - mi.rcWork.Left;
+ mmi.ptMaxSize.Y = mi.rcWork.Bottom - mi.rcWork.Top;
+ if (mmi.ptMaxTrackSize.X < mmi.ptMaxSize.X) mmi.ptMaxTrackSize.X = mmi.ptMaxSize.X;
+ if (mmi.ptMaxTrackSize.Y < mmi.ptMaxSize.Y) mmi.ptMaxTrackSize.Y = mmi.ptMaxSize.Y;
+ Marshal.StructureToPtr(mmi, lParam, false);
+ handled = true;
+ return 0;
+ }
+ }
}
+ return 0;
}
- private static NativeMethods.RECT GetMaximizedWindowBounds(nint handle, NativeMethods.RECT workArea)
- {
- uint dpi = NativeMethods.GetDpiForWindow(handle);
- if (dpi == 0)
- dpi = NativeMethods.GetDpiForSystem();
-
- int frameX = NativeMethods.GetSystemMetricsForDpi(SM_CXSIZEFRAME, dpi)
- + NativeMethods.GetSystemMetricsForDpi(SM_CXPADDEDBORDER, dpi);
- int frameY = NativeMethods.GetSystemMetricsForDpi(SM_CYSIZEFRAME, dpi)
- + NativeMethods.GetSystemMetricsForDpi(SM_CXPADDEDBORDER, dpi);
-
- frameX = Math.Max(frameX, 8);
- frameY = Math.Max(frameY, 8);
-
- return new NativeMethods.RECT
- {
- Left = workArea.Left - frameX,
- Top = workArea.Top - frameY,
- Right = workArea.Right + frameX,
- Bottom = workArea.Bottom + frameY,
- };
- }
-
- private static bool SetWindowsWindowBounds(nint handle, NativeMethods.RECT bounds)
- {
- bool result = NativeMethods.SetWindowPos(
- handle,
- 0,
- bounds.Left,
- bounds.Top,
- bounds.Right - bounds.Left,
- bounds.Bottom - bounds.Top,
- SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED);
- if (!result)
- Logger.Warn($"Could not set window bounds. Win32 error: {Marshal.GetLastWin32Error()}");
- return result;
- }
-
+ // P/Invokes compile on any platform; they are only called from code paths guarded by
+ // OperatingSystem.IsWindows(), so non-Windows targets never invoke user32.dll at runtime.
private static class NativeMethods
{
- [DllImport("user32.dll")]
- [return: MarshalAs(UnmanagedType.Bool)]
- public static extern bool IsZoomed(nint hWnd);
+ [DllImport("user32.dll", EntryPoint = "GetWindowLongPtrW", SetLastError = true)]
+ public static extern nint GetWindowLongPtr(nint hWnd, int nIndex);
- [DllImport("user32.dll")]
- [return: MarshalAs(UnmanagedType.Bool)]
- public static extern bool ShowWindow(nint hWnd, int nCmdShow);
-
- [DllImport("user32.dll")]
- public static extern uint GetDpiForWindow(nint hwnd);
-
- [DllImport("user32.dll")]
- public static extern uint GetDpiForSystem();
-
- [DllImport("user32.dll")]
- public static extern int GetSystemMetricsForDpi(int nIndex, uint dpi);
-
- [DllImport("user32.dll", SetLastError = true)]
- [return: MarshalAs(UnmanagedType.Bool)]
- public static extern bool GetWindowRect(nint hWnd, out RECT lpRect);
+ [DllImport("user32.dll", EntryPoint = "SetWindowLongPtrW", SetLastError = true)]
+ public static extern nint SetWindowLongPtr(nint hWnd, int nIndex, nint dwNewLong);
[DllImport("user32.dll")]
public static extern nint MonitorFromWindow(nint hwnd, uint dwFlags);
@@ -446,16 +438,29 @@ private static class NativeMethods
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool GetMonitorInfo(nint hMonitor, ref MONITORINFO lpmi);
- [DllImport("user32.dll", SetLastError = true)]
- [return: MarshalAs(UnmanagedType.Bool)]
- public static extern bool SetWindowPos(
- nint hWnd,
- nint hWndInsertAfter,
- int X,
- int Y,
- int cx,
- int cy,
- uint uFlags);
+ [StructLayout(LayoutKind.Sequential)]
+ public struct STYLESTRUCT
+ {
+ public uint styleOld;
+ public uint styleNew;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ public struct POINT
+ {
+ public int X;
+ public int Y;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ public struct MINMAXINFO
+ {
+ public POINT ptReserved;
+ public POINT ptMaxSize;
+ public POINT ptMaxPosition;
+ public POINT ptMinTrackSize;
+ public POINT ptMaxTrackSize;
+ }
[StructLayout(LayoutKind.Sequential)]
public struct RECT