From d108c78a97fea6030ce1a1e5cbc67b7bc788c19d Mon Sep 17 00:00:00 2001 From: GabrielDuf Date: Fri, 29 May 2026 11:44:56 -0400 Subject: [PATCH 01/10] Add Windows 11 Mica backdrop and native styling to Avalonia app --- src/UniGetUI.Avalonia/App.axaml.cs | 16 ++ .../Assets/Styles/Styles.Common.axaml | 4 + .../Assets/Styles/Styles.WindowsMica.axaml | 89 ++++++++++ .../Infrastructure/MicaWindowHelper.cs | 154 ++++++++++++++++++ .../UniGetUI.Avalonia.csproj | 7 +- .../Views/DialogPages/AboutWindow.axaml.cs | 1 + .../BundleSecurityReportDialog.axaml.cs | 1 + .../DialogPages/CrashReportWindow.axaml.cs | 1 + .../DiscardBundleChangesDialog.axaml.cs | 1 + .../DialogPages/InstallOptionsWindow.axaml.cs | 1 + .../IntegrityViolationDialog.axaml.cs | 1 + .../ManageDesktopShortcutsWindow.axaml.cs | 1 + .../ManageIgnoredUpdatesWindow.axaml.cs | 1 + .../MissingDependencyDialog.axaml.cs | 1 + .../OperationFailedDialog.axaml.cs | 1 + .../OperationOutputWindow.axaml.cs | 1 + .../DialogPages/PackageDetailsWindow.axaml.cs | 1 + .../DialogPages/SimpleErrorDialog.axaml.cs | 1 + .../DialogPages/TelemetryDialog.axaml.cs | 1 + src/UniGetUI.Avalonia/Views/MainWindow.axaml | 4 +- .../Views/MainWindow.axaml.cs | 53 ++++++ .../SettingsPages/PackageManagerPage.axaml | 16 ++ .../SoftwarePages/AbstractPackagesPage.axaml | 37 +++-- 23 files changed, 374 insertions(+), 20 deletions(-) create mode 100644 src/UniGetUI.Avalonia/Assets/Styles/Styles.WindowsMica.axaml create mode 100644 src/UniGetUI.Avalonia/Infrastructure/MicaWindowHelper.cs diff --git a/src/UniGetUI.Avalonia/App.axaml.cs b/src/UniGetUI.Avalonia/App.axaml.cs index 67ec82f6f1..34639f9757 100644 --- a/src/UniGetUI.Avalonia/App.axaml.cs +++ b/src/UniGetUI.Avalonia/App.axaml.cs @@ -1,8 +1,10 @@ +using System; using System.Diagnostics; using System.IO; using Avalonia; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Markup.Xaml; +using Avalonia.Markup.Xaml.Styling; using Avalonia.Platform; using Avalonia.Styling; using Avalonia.Threading; @@ -24,6 +26,20 @@ public partial class App : Application public override void Initialize() { AvaloniaXamlLoader.Load(this); + + // Windows 11 Mica look is opt-in per environment: only merge the translucent + // surface overrides when Mica is actually usable (Win11 + transparency on). + // macOS, Linux, Windows 10, and transparency-off all keep the solid Styles.Common look. + if (MicaWindowHelper.IsMicaEnabled()) + { + Resources.MergedDictionaries.Add(new ResourceInclude((Uri?)null) + { + Source = new Uri("avares://UniGetUI.Avalonia/Assets/Styles/Styles.WindowsMica.axaml") + }); + // Give flyouts/menus/tooltips a native acrylic backdrop (DWM) so they blur + tint + // from behind and adapt to the theme. + MicaWindowHelper.EnableAcrylicPopups(); + } #if AVALONIA_DIAGNOSTICS_ENABLED this.AttachDeveloperTools(); #endif diff --git a/src/UniGetUI.Avalonia/Assets/Styles/Styles.Common.axaml b/src/UniGetUI.Avalonia/Assets/Styles/Styles.Common.axaml index 807dbea918..c3973aeb46 100644 --- a/src/UniGetUI.Avalonia/Assets/Styles/Styles.Common.axaml +++ b/src/UniGetUI.Avalonia/Assets/Styles/Styles.Common.axaml @@ -36,6 +36,8 @@ + + @@ -97,6 +99,8 @@ + + diff --git a/src/UniGetUI.Avalonia/Assets/Styles/Styles.WindowsMica.axaml b/src/UniGetUI.Avalonia/Assets/Styles/Styles.WindowsMica.axaml new file mode 100644 index 0000000000..6d44525f10 --- /dev/null +++ b/src/UniGetUI.Avalonia/Assets/Styles/Styles.WindowsMica.axaml @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/UniGetUI.Avalonia/Infrastructure/MicaWindowHelper.cs b/src/UniGetUI.Avalonia/Infrastructure/MicaWindowHelper.cs new file mode 100644 index 0000000000..0f85fd8ebe --- /dev/null +++ b/src/UniGetUI.Avalonia/Infrastructure/MicaWindowHelper.cs @@ -0,0 +1,154 @@ +using System; +using System.Runtime.InteropServices; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Media; +using Avalonia.Platform; + +namespace UniGetUI.Avalonia.Infrastructure; + +/// +/// Gives a standard Avalonia the native Windows 11 look: +/// the Mica backdrop (whole-window), rounded corners, and an accent-colored border +/// that follows focus. Windows-only and gated on Windows 11; a no-op elsewhere, so +/// the window keeps its opaque look when Mica isn't available. +/// Used by the secondary windows/dialogs; MainWindow has its own (custom-frame) variant. +/// +internal static class MicaWindowHelper +{ + private const int DWMWA_WINDOW_CORNER_PREFERENCE = 33; + private const int DWMWA_BORDER_COLOR = 34; + private const int DWMWA_SYSTEMBACKDROP_TYPE = 38; + private const int DWMWCP_ROUND = 2; + private const int DWMSBT_MAINWINDOW = 2; // Mica — same backdrop as the rest of the app, so menus match + private const int DWMWA_COLOR_DEFAULT = unchecked((int)0xFFFFFFFF); + + private static bool _acrylicPopupsHooked; + + public static void Apply(Window window) + { + if (!IsMicaEnabled()) + return; + + // Avalonia paints the Mica backdrop from this hint; the transparent window lets it + // show. The dialog's content panels keep their (translucent) surfaces from the + // merged Styles.WindowsMica dictionary. + window.TransparencyLevelHint = new[] { WindowTransparencyLevel.Mica }; + + window.Opened += (_, _) => + { + if (window.TryGetPlatformHandle()?.Handle is { } handle && handle != 0) + { + int corner = DWMWCP_ROUND; + NativeMethods.DwmSetWindowAttribute(handle, DWMWA_WINDOW_CORNER_PREFERENCE, ref corner, sizeof(int)); + } + window.Background = Brushes.Transparent; + ApplyBorderAccent(window, window.IsActive); + }; + + window.GetObservable(WindowBase.IsActiveProperty).Subscribe(active => ApplyBorderAccent(window, active)); + + if (Application.Current?.PlatformSettings is { } settings) + { + void Handler(object? s, PlatformColorValues e) + { + if (window.IsActive) ApplyBorderAccent(window, true); + } + settings.ColorValuesChanged += Handler; + window.Closed += (_, _) => settings.ColorValuesChanged -= Handler; + } + } + + // Gives flyouts / menus / combo dropdowns / tooltips a native Win11 acrylic backdrop: + // the popup window is made transparent and DWM paints the (blurred, theme-adaptive) + // acrylic material behind it. Registered once at startup when Mica is enabled. + public static void EnableAcrylicPopups() + { + if (!IsMicaEnabled() || _acrylicPopupsHooked) + return; + _acrylicPopupsHooked = true; + + // Every flyout/menu/tooltip/combo popup is hosted in a PopupRoot; style it as it loads. + Control.LoadedEvent.AddClassHandler((root, _) => ApplyAcrylicToPopup(root)); + } + + private static void ApplyAcrylicToPopup(PopupRoot root) + { + // Transparent surface so the DWM acrylic shows; the presenter backgrounds are also + // transparent (Styles.WindowsMica) so only the acrylic + menu items are painted. + root.TransparencyLevelHint = new[] { WindowTransparencyLevel.Transparent }; + root.Background = Brushes.Transparent; + + if (root.TryGetPlatformHandle()?.Handle is not { } handle || handle == 0) + return; + + int corner = DWMWCP_ROUND; + NativeMethods.DwmSetWindowAttribute(handle, DWMWA_WINDOW_CORNER_PREFERENCE, ref corner, sizeof(int)); + int backdrop = DWMSBT_MAINWINDOW; + NativeMethods.DwmSetWindowAttribute(handle, DWMWA_SYSTEMBACKDROP_TYPE, ref backdrop, sizeof(int)); + } + + // Accent-coloured window border that follows focus (kept on the dialogs). + private static void ApplyBorderAccent(Window window, bool active) + { + if (window.TryGetPlatformHandle()?.Handle is not { } handle || handle == 0) + return; + + int colorRef = DWMWA_COLOR_DEFAULT; + if (active) + { + Color accent = Application.Current?.PlatformSettings?.GetColorValues().AccentColor1 + ?? Colors.Transparent; + colorRef = accent.R | (accent.G << 8) | (accent.B << 16); // Color -> COLORREF (0x00BBGGRR) + } + NativeMethods.DwmSetWindowAttribute(handle, DWMWA_BORDER_COLOR, ref colorRef, sizeof(int)); + } + + /// + /// True when the native Mica look should be used: Windows 11+ AND the user has + /// "Transparency effects" enabled. When transparency is off we keep the solid look, + /// since the Mica backdrop otherwise lingers (DWM keeps painting it). + /// + public static bool IsMicaEnabled() + => OperatingSystem.IsWindows() + && Environment.OSVersion.Version.Build >= 22000 + && IsOsTransparencyEnabled(); + + private static bool IsOsTransparencyEnabled() + { + if (!OperatingSystem.IsWindows()) + return false; + + try + { + var data = new byte[4]; + int size = data.Length; + int result = NativeMethods.RegGetValueW( + NativeMethods.HKEY_CURRENT_USER, + @"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize", + "EnableTransparency", + NativeMethods.RRF_RT_REG_DWORD, + out _, data, ref size); + if (result == 0) // ERROR_SUCCESS + return BitConverter.ToInt32(data, 0) != 0; + } + catch { /* fall through to the default */ } + + return true; // value missing/unreadable -> assume transparency is on + } + + private static class NativeMethods + { + // winreg.h: HKEY_CURRENT_USER = (HKEY)(ULONG_PTR)((LONG)0x80000001) — sign-extended on x64. + public static readonly nint HKEY_CURRENT_USER = new(unchecked((int)0x80000001)); + public const int RRF_RT_REG_DWORD = 0x00000010; + + [DllImport("dwmapi.dll")] + public static extern int DwmSetWindowAttribute(nint hwnd, int attr, ref int value, int size); + + [DllImport("advapi32.dll", CharSet = CharSet.Unicode)] + public static extern int RegGetValueW(nint hkey, string lpSubKey, string lpValue, int dwFlags, + out int pdwType, byte[] pvData, ref int pcbData); + } +} diff --git a/src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj b/src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj index e5a47fc98e..a9a3c93d47 100644 --- a/src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj +++ b/src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj @@ -82,10 +82,10 @@ - + - - + + @@ -163,6 +163,7 @@ + diff --git a/src/UniGetUI.Avalonia/Views/DialogPages/AboutWindow.axaml.cs b/src/UniGetUI.Avalonia/Views/DialogPages/AboutWindow.axaml.cs index a2b8b8b2e9..267b4388e5 100644 --- a/src/UniGetUI.Avalonia/Views/DialogPages/AboutWindow.axaml.cs +++ b/src/UniGetUI.Avalonia/Views/DialogPages/AboutWindow.axaml.cs @@ -8,6 +8,7 @@ public partial class AboutWindow : Window public AboutWindow() { InitializeComponent(); + UniGetUI.Avalonia.Infrastructure.MicaWindowHelper.Apply(this); } protected override void OnOpened(EventArgs e) diff --git a/src/UniGetUI.Avalonia/Views/DialogPages/BundleSecurityReportDialog.axaml.cs b/src/UniGetUI.Avalonia/Views/DialogPages/BundleSecurityReportDialog.axaml.cs index 77c9a86bc4..f04a91f32c 100644 --- a/src/UniGetUI.Avalonia/Views/DialogPages/BundleSecurityReportDialog.axaml.cs +++ b/src/UniGetUI.Avalonia/Views/DialogPages/BundleSecurityReportDialog.axaml.cs @@ -9,6 +9,7 @@ public partial class BundleSecurityReportDialog : Window public BundleSecurityReportDialog(BundleReport report) { InitializeComponent(); + UniGetUI.Avalonia.Infrastructure.MicaWindowHelper.Apply(this); var sb = new System.Text.StringBuilder(); foreach (var (pkgId, entries) in report.Contents) diff --git a/src/UniGetUI.Avalonia/Views/DialogPages/CrashReportWindow.axaml.cs b/src/UniGetUI.Avalonia/Views/DialogPages/CrashReportWindow.axaml.cs index 53c1d06331..0be7336d52 100644 --- a/src/UniGetUI.Avalonia/Views/DialogPages/CrashReportWindow.axaml.cs +++ b/src/UniGetUI.Avalonia/Views/DialogPages/CrashReportWindow.axaml.cs @@ -16,6 +16,7 @@ public CrashReportWindow(string crashReport) { _crashReport = crashReport; InitializeComponent(); + UniGetUI.Avalonia.Infrastructure.MicaWindowHelper.Apply(this); CrashReportText.Text = crashReport; DontSendButton.Content = CoreTools.Translate("Don't Send"); SendButton.Content = CoreTools.Translate("Send Report"); diff --git a/src/UniGetUI.Avalonia/Views/DialogPages/DiscardBundleChangesDialog.axaml.cs b/src/UniGetUI.Avalonia/Views/DialogPages/DiscardBundleChangesDialog.axaml.cs index 6e5dbc0146..d156fdca58 100644 --- a/src/UniGetUI.Avalonia/Views/DialogPages/DiscardBundleChangesDialog.axaml.cs +++ b/src/UniGetUI.Avalonia/Views/DialogPages/DiscardBundleChangesDialog.axaml.cs @@ -10,6 +10,7 @@ public partial class DiscardBundleChangesDialog : Window public DiscardBundleChangesDialog() { InitializeComponent(); + UniGetUI.Avalonia.Infrastructure.MicaWindowHelper.Apply(this); CancelButton.Click += (_, _) => Close(); DiscardButton.Click += (_, _) => { Confirmed = true; Close(); }; } diff --git a/src/UniGetUI.Avalonia/Views/DialogPages/InstallOptionsWindow.axaml.cs b/src/UniGetUI.Avalonia/Views/DialogPages/InstallOptionsWindow.axaml.cs index c7f2407ac6..fb55d63a76 100644 --- a/src/UniGetUI.Avalonia/Views/DialogPages/InstallOptionsWindow.axaml.cs +++ b/src/UniGetUI.Avalonia/Views/DialogPages/InstallOptionsWindow.axaml.cs @@ -17,6 +17,7 @@ public InstallOptionsWindow(IPackage package, OperationType operation, InstallOp var vm = new InstallOptionsViewModel(package, operation, options); DataContext = vm; InitializeComponent(); + UniGetUI.Avalonia.Infrastructure.MicaWindowHelper.Apply(this); vm.CloseRequested += (_, _) => Close(); } diff --git a/src/UniGetUI.Avalonia/Views/DialogPages/IntegrityViolationDialog.axaml.cs b/src/UniGetUI.Avalonia/Views/DialogPages/IntegrityViolationDialog.axaml.cs index 1517137ea5..2a7c309af3 100644 --- a/src/UniGetUI.Avalonia/Views/DialogPages/IntegrityViolationDialog.axaml.cs +++ b/src/UniGetUI.Avalonia/Views/DialogPages/IntegrityViolationDialog.axaml.cs @@ -8,6 +8,7 @@ public partial class IntegrityViolationDialog : Window public IntegrityViolationDialog() { InitializeComponent(); + UniGetUI.Avalonia.Infrastructure.MicaWindowHelper.Apply(this); CloseButton.Click += (_, _) => Close(); } diff --git a/src/UniGetUI.Avalonia/Views/DialogPages/ManageDesktopShortcutsWindow.axaml.cs b/src/UniGetUI.Avalonia/Views/DialogPages/ManageDesktopShortcutsWindow.axaml.cs index a347181cfd..c8b7a68b22 100644 --- a/src/UniGetUI.Avalonia/Views/DialogPages/ManageDesktopShortcutsWindow.axaml.cs +++ b/src/UniGetUI.Avalonia/Views/DialogPages/ManageDesktopShortcutsWindow.axaml.cs @@ -12,6 +12,7 @@ public ManageDesktopShortcutsWindow(System.Collections.Generic.IReadOnlyList Close(); } diff --git a/src/UniGetUI.Avalonia/Views/DialogPages/ManageIgnoredUpdatesWindow.axaml.cs b/src/UniGetUI.Avalonia/Views/DialogPages/ManageIgnoredUpdatesWindow.axaml.cs index dffda0ed6e..3897c61742 100644 --- a/src/UniGetUI.Avalonia/Views/DialogPages/ManageIgnoredUpdatesWindow.axaml.cs +++ b/src/UniGetUI.Avalonia/Views/DialogPages/ManageIgnoredUpdatesWindow.axaml.cs @@ -12,6 +12,7 @@ public ManageIgnoredUpdatesWindow() var vm = new ManageIgnoredUpdatesViewModel(); DataContext = vm; InitializeComponent(); + UniGetUI.Avalonia.Infrastructure.MicaWindowHelper.Apply(this); vm.CloseRequested += (_, _) => Close(); } diff --git a/src/UniGetUI.Avalonia/Views/DialogPages/MissingDependencyDialog.axaml.cs b/src/UniGetUI.Avalonia/Views/DialogPages/MissingDependencyDialog.axaml.cs index dfdd76ea12..0e36090543 100644 --- a/src/UniGetUI.Avalonia/Views/DialogPages/MissingDependencyDialog.axaml.cs +++ b/src/UniGetUI.Avalonia/Views/DialogPages/MissingDependencyDialog.axaml.cs @@ -26,6 +26,7 @@ public MissingDependencyDialog(ManagerDependency dep, int current, int total) _total = total; InitializeComponent(); + UniGetUI.Avalonia.Infrastructure.MicaWindowHelper.Apply(this); bool notFirstTime = Settings.GetDictionaryItem(Settings.K.DependencyManagement, dep.Name) diff --git a/src/UniGetUI.Avalonia/Views/DialogPages/OperationFailedDialog.axaml.cs b/src/UniGetUI.Avalonia/Views/DialogPages/OperationFailedDialog.axaml.cs index 7ba469a438..3c9620e15c 100644 --- a/src/UniGetUI.Avalonia/Views/DialogPages/OperationFailedDialog.axaml.cs +++ b/src/UniGetUI.Avalonia/Views/DialogPages/OperationFailedDialog.axaml.cs @@ -16,6 +16,7 @@ public partial class OperationFailedDialog : Window public OperationFailedDialog(AbstractOperation operation) { InitializeComponent(); + UniGetUI.Avalonia.Infrastructure.MicaWindowHelper.Apply(this); Title = operation.Metadata.FailureMessage; HeaderContent.Text = diff --git a/src/UniGetUI.Avalonia/Views/DialogPages/OperationOutputWindow.axaml.cs b/src/UniGetUI.Avalonia/Views/DialogPages/OperationOutputWindow.axaml.cs index 007c437320..92751bbcdd 100644 --- a/src/UniGetUI.Avalonia/Views/DialogPages/OperationOutputWindow.axaml.cs +++ b/src/UniGetUI.Avalonia/Views/DialogPages/OperationOutputWindow.axaml.cs @@ -15,6 +15,7 @@ public OperationOutputWindow(AbstractOperation operation) var vm = new OperationOutputViewModel(operation); DataContext = vm; InitializeComponent(); + UniGetUI.Avalonia.Infrastructure.MicaWindowHelper.Apply(this); foreach (var line in vm.OutputLines) AppendLine(line); diff --git a/src/UniGetUI.Avalonia/Views/DialogPages/PackageDetailsWindow.axaml.cs b/src/UniGetUI.Avalonia/Views/DialogPages/PackageDetailsWindow.axaml.cs index 353a8fabe1..417949454f 100644 --- a/src/UniGetUI.Avalonia/Views/DialogPages/PackageDetailsWindow.axaml.cs +++ b/src/UniGetUI.Avalonia/Views/DialogPages/PackageDetailsWindow.axaml.cs @@ -43,6 +43,7 @@ public PackageDetailsWindow(IPackage package, OperationType operation) _vm = new PackageDetailsViewModel(package, operation); DataContext = _vm; InitializeComponent(); + UniGetUI.Avalonia.Infrastructure.MicaWindowHelper.Apply(this); _vm.CloseRequested += (_, _) => Close(); _vm.DetailsLoaded += (_, _) => diff --git a/src/UniGetUI.Avalonia/Views/DialogPages/SimpleErrorDialog.axaml.cs b/src/UniGetUI.Avalonia/Views/DialogPages/SimpleErrorDialog.axaml.cs index 6b977cb29a..9ff1e79adc 100644 --- a/src/UniGetUI.Avalonia/Views/DialogPages/SimpleErrorDialog.axaml.cs +++ b/src/UniGetUI.Avalonia/Views/DialogPages/SimpleErrorDialog.axaml.cs @@ -8,6 +8,7 @@ public partial class SimpleErrorDialog : Window public SimpleErrorDialog(string title, string message) { InitializeComponent(); + UniGetUI.Avalonia.Infrastructure.MicaWindowHelper.Apply(this); Title = title; TitleBlock.Text = title; MessageBlock.Text = message; diff --git a/src/UniGetUI.Avalonia/Views/DialogPages/TelemetryDialog.axaml.cs b/src/UniGetUI.Avalonia/Views/DialogPages/TelemetryDialog.axaml.cs index 64f838d882..0aa027331e 100644 --- a/src/UniGetUI.Avalonia/Views/DialogPages/TelemetryDialog.axaml.cs +++ b/src/UniGetUI.Avalonia/Views/DialogPages/TelemetryDialog.axaml.cs @@ -11,6 +11,7 @@ public partial class TelemetryDialog : Window public TelemetryDialog() { InitializeComponent(); + UniGetUI.Avalonia.Infrastructure.MicaWindowHelper.Apply(this); Body2.Text = CoreTools.Translate("No personal information is collected nor sent, and the collected data is anonimized, so it can't be back-tracked to you."); diff --git a/src/UniGetUI.Avalonia/Views/MainWindow.axaml b/src/UniGetUI.Avalonia/Views/MainWindow.axaml index 1c30bde762..db5fd4ad72 100644 --- a/src/UniGetUI.Avalonia/Views/MainWindow.axaml +++ b/src/UniGetUI.Avalonia/Views/MainWindow.axaml @@ -20,7 +20,7 @@ @@ -298,7 +298,7 @@ diff --git a/src/UniGetUI.Avalonia/Views/MainWindow.axaml.cs b/src/UniGetUI.Avalonia/Views/MainWindow.axaml.cs index 80d8068bb4..7a9cf2dc1c 100644 --- a/src/UniGetUI.Avalonia/Views/MainWindow.axaml.cs +++ b/src/UniGetUI.Avalonia/Views/MainWindow.axaml.cs @@ -63,6 +63,13 @@ public partial class MainWindow : Window private const uint SWP_NOACTIVATE = 0x0010; private const uint SWP_FRAMECHANGED = 0x0020; + // DWM attributes for the native Windows 11 Mica look: rounded corners and an + // accent-colored window border that tracks focus. + private const int DWMWA_WINDOW_CORNER_PREFERENCE = 33; + private const int DWMWA_BORDER_COLOR = 34; + private const int DWMWCP_ROUND = 2; + private const int DWMWA_COLOR_NONE = unchecked((int)0xFFFFFFFE); + private bool _focusSidebarSelectionOnNextPageChange; private TrayService? _trayService; private bool _allowClose; @@ -123,6 +130,8 @@ protected override void OnOpened(EventArgs e) SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED); } } + + SetupMicaAndAccentBorder(); } protected override void OnClosing(WindowClosingEventArgs e) @@ -250,6 +259,12 @@ private void SetupTitleBar() WindowDecorations = WindowDecorations.BorderOnly; ExtendClientAreaToDecorationsHint = true; ExtendClientAreaTitleBarHeightHint = -1; + // Request the Win11 Mica backdrop only when it should actually be used + // (Windows 11 + "Transparency effects" enabled). Otherwise leave the default + // so the window stays solid. The transparent-background switch happens in + // SetupMicaAndAccentBorder() once the native handle exists. + if (MicaWindowHelper.IsMicaEnabled()) + TransparencyLevelHint = new[] { WindowTransparencyLevel.Mica }; TitleBarGrid.ClearValue(HeightProperty); TitleBarGrid.Height = 44; HamburgerPanel.Margin = new Thickness(10, 0, 8, 0); @@ -618,6 +633,41 @@ private void UpdateMaximizeButtonState(bool isMaximized) CoreTools.Translate(isMaximized ? "Restore" : "Maximize")); } + // Applies the Windows 11 Mica look when it's actually usable (Win11 + transparency on): + // a transparent window so the backdrop shows, native rounded corners, and no accent + // border (it reads as out of place on the large main window). Otherwise the window keeps + // its solid background. Must run after the native handle exists (OnOpened); Windows-only. + private void SetupMicaAndAccentBorder() + { + if (!OperatingSystem.IsWindows()) + return; + + if (TryGetPlatformHandle()?.Handle is not { } handle || handle == 0) + return; + + if (!MicaWindowHelper.IsMicaEnabled()) + { + // No Mica (Windows 10, transparency off, etc.): keep the solid window background. + // Styles.WindowsMica is not merged in this case, so the surfaces stay opaque too. + if (this.TryFindResource("AppWindowBackground", ActualThemeVariant, out var bg) && bg is IBrush brush) + Background = brush; + return; + } + + // The custom NCCALCSIZE frame keeps WS_THICKFRAME, so DWM still has a frame to round. + int corner = DWMWCP_ROUND; + NativeMethods.DwmSetWindowAttribute(handle, DWMWA_WINDOW_CORNER_PREFERENCE, ref corner, sizeof(int)); + + // Transparent window + transparent MicaPageBackground (from Styles.WindowsMica) let + // the backdrop show through the chrome and page area. + Background = Brushes.Transparent; + + // Suppress the window border colour so the main window doesn't get the accent edge + // (the dialogs keep it via MicaWindowHelper). + int noBorder = DWMWA_COLOR_NONE; + NativeMethods.DwmSetWindowAttribute(handle, DWMWA_BORDER_COLOR, ref noBorder, sizeof(int)); + } + private static nint OnWindowsWndProc(nint hWnd, uint msg, nint wParam, nint lParam, ref bool handled) { // Force client = full window rect. Avalonia's ExtendClientArea handler only overrides @@ -729,6 +779,9 @@ private static class NativeMethods [return: MarshalAs(UnmanagedType.Bool)] public static extern bool GetMonitorInfo(nint hMonitor, ref MONITORINFO lpmi); + [DllImport("dwmapi.dll")] + public static extern int DwmSetWindowAttribute(nint hwnd, int attr, ref int value, int size); + [StructLayout(LayoutKind.Sequential)] public struct STYLESTRUCT { diff --git a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/PackageManagerPage.axaml b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/PackageManagerPage.axaml index ce4ba782c3..a9f59d572c 100644 --- a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/PackageManagerPage.axaml +++ b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/PackageManagerPage.axaml @@ -26,6 +26,19 @@ + + + + @@ -55,7 +68,10 @@ Opacity="0.85" TextWrapping="Wrap"/> diff --git a/src/UniGetUI.Avalonia/Views/SoftwarePages/AbstractPackagesPage.axaml.cs b/src/UniGetUI.Avalonia/Views/SoftwarePages/AbstractPackagesPage.axaml.cs index b490a9e409..160250e653 100644 --- a/src/UniGetUI.Avalonia/Views/SoftwarePages/AbstractPackagesPage.axaml.cs +++ b/src/UniGetUI.Avalonia/Views/SoftwarePages/AbstractPackagesPage.axaml.cs @@ -373,11 +373,12 @@ private void UpdateFilterPaneColumn(bool open) // ─── Card overflow button (Grid / Icons view) ───────────────────────────── private void CardOverflowButton_Click(object? sender, RoutedEventArgs e) { - if (sender is not Button { DataContext: PackageWrapper wrapper }) return; + if (sender is not Button { DataContext: PackageWrapper wrapper } button) return; PackageList.SelectedItem = wrapper; if (_contextMenu is null) return; WhenShowingContextMenu(wrapper.Package); - _contextMenu.Open(sender as Control); + _contextMenu.PlacementTarget = button; + _contextMenu.Open(); e.Handled = true; } From 54df07bb8f05966dbdfa12730a433ceed288d77c Mon Sep 17 00:00:00 2001 From: Gabriel Dufresne Date: Mon, 1 Jun 2026 09:26:39 -0400 Subject: [PATCH 04/10] Add monochrome tray icon style and unify tray icon selection --- .../Infrastructure/TrayService.cs | 41 ++++++++++++++---- .../UniGetUI.Avalonia.csproj | 15 ++++++- .../SettingsPages/Interface_PViewModel.cs | 6 +++ .../Pages/SettingsPages/Interface_P.axaml | 6 +-- .../Pages/SettingsPages/Interface_P.axaml.cs | 14 ++++++ .../SettingsEngine_Names.cs | 4 +- .../Images/tray_monochrome_blue_dark.ico | Bin 0 -> 11800 bytes .../Images/tray_monochrome_blue_light.ico | Bin 0 -> 11555 bytes .../Images/tray_monochrome_empty_dark.ico | Bin 0 -> 10656 bytes .../Images/tray_monochrome_empty_light.ico | Bin 0 -> 9482 bytes .../Images/tray_monochrome_green_dark.ico | Bin 0 -> 12301 bytes .../Images/tray_monochrome_green_light.ico | Bin 0 -> 11938 bytes .../Images/tray_monochrome_orange_dark.ico | Bin 0 -> 11768 bytes .../Images/tray_monochrome_orange_light.ico | Bin 0 -> 11541 bytes .../Images/tray_monochrome_turquoise_dark.ico | Bin 0 -> 11976 bytes .../tray_monochrome_turquoise_light.ico | Bin 0 -> 11750 bytes src/UniGetUI/MainWindow.xaml.cs | 19 +++++--- .../GeneralPages/Interface_P.xaml | 10 ++--- .../GeneralPages/Interface_P.xaml.cs | 12 ++++- 19 files changed, 99 insertions(+), 28 deletions(-) create mode 100644 src/UniGetUI/Assets/Images/tray_monochrome_blue_dark.ico create mode 100644 src/UniGetUI/Assets/Images/tray_monochrome_blue_light.ico create mode 100644 src/UniGetUI/Assets/Images/tray_monochrome_empty_dark.ico create mode 100644 src/UniGetUI/Assets/Images/tray_monochrome_empty_light.ico create mode 100644 src/UniGetUI/Assets/Images/tray_monochrome_green_dark.ico create mode 100644 src/UniGetUI/Assets/Images/tray_monochrome_green_light.ico create mode 100644 src/UniGetUI/Assets/Images/tray_monochrome_orange_dark.ico create mode 100644 src/UniGetUI/Assets/Images/tray_monochrome_orange_light.ico create mode 100644 src/UniGetUI/Assets/Images/tray_monochrome_turquoise_dark.ico create mode 100644 src/UniGetUI/Assets/Images/tray_monochrome_turquoise_light.ico diff --git a/src/UniGetUI.Avalonia/Infrastructure/TrayService.cs b/src/UniGetUI.Avalonia/Infrastructure/TrayService.cs index 0bca843e66..8b13c993e7 100644 --- a/src/UniGetUI.Avalonia/Infrastructure/TrayService.cs +++ b/src/UniGetUI.Avalonia/Infrastructure/TrayService.cs @@ -40,7 +40,7 @@ public void UpdateStatus() { _trayIcon.IsVisible = !Settings.Get(Settings.K.DisableSystemTray); - string modifier; + string status; string tooltip; bool anyRunning = AvaloniaOperationRegistry.Operations.Any( @@ -50,38 +50,47 @@ public void UpdateStatus() if (anyRunning) { - modifier = "_blue"; + status = "blue"; tooltip = CoreTools.Translate("Operation in progress"); } else if (AvaloniaOperationRegistry.ErrorsOccurred > 0) { - modifier = "_orange"; + status = "orange"; tooltip = CoreTools.Translate("Attention required"); } else if (AvaloniaOperationRegistry.RestartRequired) { - modifier = "_turquoise"; + status = "turquoise"; tooltip = CoreTools.Translate("Restart required"); } else if (updatesCount > 0) { - modifier = "_green"; + status = "green"; tooltip = updatesCount == 1 ? CoreTools.Translate("1 update is available") : CoreTools.Translate("{0} updates are available", updatesCount); } else { - modifier = "_empty"; + status = "empty"; tooltip = CoreTools.Translate("Everything is up to date"); } _trayIcon.ToolTipText = tooltip + " - UniGetUI"; - modifier += IsTaskbarLight() ? "_black" : "_white"; - string suffix = Settings.Get(Settings.K.UseLegacyTrayIcon) ? "_legacy" : ""; + bool light = IsTaskbarLight(); + string tone = light ? "_black" : "_white"; + + // monochrome icons can't be tinted by parts, so for those we ship pre-composited + // assets: a monochrome base glyph matching the taskbar appearance with the + // colour-coded status dot kept intact (drawn non-template on macOS). + string uri = ResolveStyle() switch + { + "monochrome" => $"avares://UniGetUI.Avalonia/Assets/tray_monochrome_{status}_{(light ? "light" : "dark")}.ico", + "legacy" => $"avares://UniGetUI.Avalonia/Assets/tray_{status}{tone}_legacy.ico", + _ => $"avares://UniGetUI.Avalonia/Assets/tray_{status}{tone}.ico", + }; - string uri = $"avares://UniGetUI.Avalonia/Assets/tray{modifier}{suffix}.ico"; if (_lastIconUri == uri) return; _lastIconUri = uri; @@ -95,6 +104,20 @@ public void UpdateStatus() } } + // Tray icon style chosen by the user (Interface preferences). macOS menu-bar icons are always + // monochrome (HIG), so the style is fixed there and the selector is hidden; on Windows/Linux it + // is user-selectable and defaults to monochrome. + private static string ResolveStyle() + { + if (OperatingSystem.IsMacOS()) + return "monochrome"; + + string style = Settings.GetValue(Settings.K.TrayIconStyle); + if (style.Length == 0) + style = "monochrome"; + return style; + } + private static bool IsTaskbarLight() { #if WINDOWS diff --git a/src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj b/src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj index aa0962b4d5..874790b52d 100644 --- a/src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj +++ b/src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj @@ -136,7 +136,7 @@ - + @@ -147,6 +147,19 @@ + + + + + + + + + + + diff --git a/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/Interface_PViewModel.cs b/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/Interface_PViewModel.cs index 242ba5c62c..e9364f7fb4 100644 --- a/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/Interface_PViewModel.cs +++ b/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/Interface_PViewModel.cs @@ -15,6 +15,12 @@ public partial class Interface_PViewModel : ViewModelBase { public bool IsWindows { get; } = OperatingSystem.IsWindows(); + /// + /// The tray icon style is user-selectable on Windows and Linux only; macOS menu-bar icons are + /// always monochrome, so the selector is hidden there. + /// + public bool ShowTrayIconStyleSelector { get; } = !OperatingSystem.IsMacOS(); + /// /// True when the user is enrolled in the beta program. In that case the modern UI is forced /// and the classic-mode toggle should be disabled. diff --git a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Interface_P.axaml b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Interface_P.axaml index c28e3b3ff9..077553c4a0 100644 --- a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Interface_P.axaml +++ b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Interface_P.axaml @@ -47,9 +47,9 @@ StateChangedCommand="{Binding RefreshSystemTrayCommand}" CornerRadius="8,8,0,0"/> -