Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 87 additions & 15 deletions src/UniGetUI.Avalonia/Assets/Styles/Styles.Common.axaml

Large diffs are not rendered by default.

29 changes: 17 additions & 12 deletions src/UniGetUI.Avalonia/Assets/Styles/Styles.WindowsMica.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,25 @@
<SolidColorBrush x:Key="MicaPageBackground" Color="Transparent"/>
<!-- Cards / panels: translucent white layers over the (light) Mica -->
<SolidColorBrush x:Key="SettingsCardBackground" Color="#B3FFFFFF"/>
<SolidColorBrush x:Key="SettingsCardBorderBrush" Color="#14000000"/>
<SolidColorBrush x:Key="SettingsCardBorderBrush" Color="#0F000000"/>
<!-- Package list: near-transparent so Mica shows through, matching WinUI's SystemFillColorNeutralBackground -->
<SolidColorBrush x:Key="PackageListBackground" Color="#06000000"/>
<SolidColorBrush x:Key="SettingsCardHoverBackground" Color="#99FFFFFF"/>
<SolidColorBrush x:Key="AppSplitterBackground" Color="#18000000"/>
<SolidColorBrush x:Key="AppOperationsPanelBackground" Color="#80FFFFFF"/>
<SolidColorBrush x:Key="AppOperationsPanelBackground" Color="Transparent"/>
Comment thread
Meteoni marked this conversation as resolved.
<SolidColorBrush x:Key="AppBorderBrush" Color="#18000000"/>
<SolidColorBrush x:Key="AppBadgeBackground" Color="#99FFFFFF"/>
<SolidColorBrush x:Key="AppDialogPanelBackground" Color="#80FFFFFF"/>
<SolidColorBrush x:Key="AppDialogSubtleBackground" Color="#80FFFFFF"/>
<SolidColorBrush x:Key="AppDialogDarkBackground" Color="#80FFFFFF"/>
<SolidColorBrush x:Key="IgnoredUpdatesListBackground" Color="#F3F3F3"/>
<!-- Flyouts / menus / dropdowns / tooltips: DWM acrylic backdrop with the card tint over it -->
<SolidColorBrush x:Key="FlyoutPresenterBackground" Color="#80FFFFFF"/>
<SolidColorBrush x:Key="MenuFlyoutPresenterBackground" Color="#FFFFFFFF"/>
<!-- WinUI MenuFlyoutPresenter is transparent over a DesktopAcrylic backdrop.
These overrides are only merged when Windows transparency is usable;
Styles.Common remains the normal opaque fallback otherwise. -->
<SolidColorBrush x:Key="FlyoutPresenterBackground" Color="#D9FCFCFC"/>
<SolidColorBrush x:Key="MenuSurfaceBrush" Color="#D9FCFCFC"/>
<SolidColorBrush x:Key="MenuSurfaceFallbackBrush" Color="#FFFFFFFF"/>
<SolidColorBrush x:Key="MenuFlyoutPresenterBackground" Color="#D9FCFCFC"/>
<!-- Keep light ComboBox flyouts opaque. A translucent white popup composites
against the dimmed owner window and reads as a disabled grey surface. -->
<SolidColorBrush x:Key="ComboBoxDropDownBackground" Color="#FFFFFFFF"/>
Expand Down Expand Up @@ -63,19 +67,20 @@
<SolidColorBrush x:Key="PackageListBackground" Color="#08FFFFFF"/>
<SolidColorBrush x:Key="SettingsCardHoverBackground" Color="#15FFFFFF"/>
<SolidColorBrush x:Key="AppSplitterBackground" Color="#66000000"/>
<SolidColorBrush x:Key="AppOperationsPanelBackground" Color="#66202020"/>
<SolidColorBrush x:Key="AppOperationsPanelBackground" Color="Transparent"/>
<SolidColorBrush x:Key="AppBorderBrush" Color="#15FFFFFF"/>
<SolidColorBrush x:Key="AppBadgeBackground" Color="#66383838"/>
<SolidColorBrush x:Key="AppDialogPanelBackground" Color="#662C2C2C"/>
<SolidColorBrush x:Key="AppDialogSubtleBackground" Color="#66393939"/>
<SolidColorBrush x:Key="AppDialogDarkBackground" Color="#66181818"/>
<SolidColorBrush x:Key="IgnoredUpdatesListBackground" Color="#66181818"/>
<!-- Flyouts / menus / dropdowns / tooltips: MicaWindowHelper gives the popup window a DWM
acrylic backdrop (transient surfaces can't use Mica). Kept mostly opaque so the surface
stays readably dark even when the popup floats over bright content (e.g. the release-notes
WebView) — a lighter tint let the white page wash the menu out (#5007) -->
<SolidColorBrush x:Key="FlyoutPresenterBackground" Color="#BF2D2D2D"/>
<SolidColorBrush x:Key="MenuFlyoutPresenterBackground" Color="#FF2C2C2C"/>
<!-- WinUI MenuFlyoutPresenter is transparent over a DesktopAcrylic backdrop.
These overrides are only merged when Windows transparency is usable;
Styles.Common remains the normal opaque fallback otherwise. -->
<SolidColorBrush x:Key="FlyoutPresenterBackground" Color="#F52C2C2C"/>
<SolidColorBrush x:Key="MenuSurfaceBrush" Color="#F52C2C2C"/>
<SolidColorBrush x:Key="MenuSurfaceFallbackBrush" Color="#FF2C2C2C"/>
<SolidColorBrush x:Key="MenuFlyoutPresenterBackground" Color="#F52C2C2C"/>
<SolidColorBrush x:Key="ComboBoxDropDownBackground" Color="#BF2D2D2D"/>
<SolidColorBrush x:Key="ToolTipBackground" Color="#BF2D2D2D"/>
<!-- Expander chrome -->
Expand Down
14 changes: 11 additions & 3 deletions src/UniGetUI.Avalonia/Infrastructure/AvaloniaOperationRegistry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,16 +84,17 @@ public static void Add(AbstractOperation op)
Dispatcher.UIThread.Post(UpdateTrayStatus);
};

// Cancellation drives Status = Canceled from several code paths, so StatusChanged(Canceled)
// can fire more than once for a single operation. Handle the terminal cancel exactly once.
// Keep the canceled card visible, matching the removed WinUI control, but retain the
// old short IPC lifetime so canceled operations do not stay tracked indefinitely.
int cancelHandled = 0;
op.StatusChanged += (_, status) =>
{
if (status is OperationStatus.Canceled && Interlocked.Exchange(ref cancelHandled, 1) == 0)
{
WindowsAppNotificationBridge.RemoveProgress(op);
_ = RemoveAfterDelayAsync(op, milliseconds: 2500);
_ = ForgetTrackingAfterDelayAsync(op, milliseconds: 2500);
}

Dispatcher.UIThread.Post(UpdateTrayStatus);
};

Expand Down Expand Up @@ -165,6 +166,13 @@ public static void Remove(OperationViewModel vm)
}
}

private static async Task ForgetTrackingAfterDelayAsync(AbstractOperation op, int milliseconds)
{
await Task.Delay(milliseconds);
if (op.Status is not (OperationStatus.InQueue or OperationStatus.Running))
IpcOperationApi.ForgetTracking(op.Metadata.Identifier);
}

private static async Task RemoveAfterDelayAsync(AbstractOperation op, int milliseconds)
{
await Task.Delay(milliseconds);
Expand Down
73 changes: 43 additions & 30 deletions src/UniGetUI.Avalonia/Infrastructure/MicaWindowHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,7 @@ namespace UniGetUI.Avalonia.Infrastructure;
internal static class MicaWindowHelper
{
private const int DWMWA_WINDOW_CORNER_PREFERENCE = 33;
private const int DWMWA_SYSTEMBACKDROP_TYPE = 38;
private const int DWMWCP_ROUND = 2;
private const int DWMSBT_TRANSIENTWINDOW = 3; // Acrylic — for transient surfaces (menus/flyouts); Mica won't paint on these

private static bool _acrylicPopupsHooked;
private static bool _micaConfirmedUnavailable;
Expand All @@ -49,9 +47,10 @@ public static void Apply(Window window)
};
}

// Applies the Windows 11 transient-surface treatment to popup hosts. Menus use an
// opaque WinUI surface; combo dropdowns, tooltips, and ordinary flyouts use acrylic.
// Registered once at startup when Mica is enabled.
// Applies the Windows 11 transient-surface treatment to popup hosts. WinUI menu
// flyouts/context menus use desktop acrylic just like the other transient surfaces.
// The native tray popup is intentionally excluded. Registered once at startup when
// Mica is enabled; otherwise the existing opaque surfaces remain as the fallback.
public static void EnableAcrylicPopups()
{
if (!IsMicaEnabled() || _acrylicPopupsHooked)
Expand All @@ -61,10 +60,9 @@ public static void EnableAcrylicPopups()
// In-app menus, flyouts, tooltips, and combo popups are hosted in a PopupRoot.
Control.LoadedEvent.AddClassHandler<PopupRoot>((root, _) => ApplyAcrylicToPopup(root));

// The system-tray context menu is NOT a PopupRoot — Avalonia hosts it in its own Window
// (Avalonia.Win32.TrayIconImpl.TrayPopupRoot), so it misses the handler above and would
// render with no surface over the desktop. Catch it by type name and apply the opaque
// menu treatment. The other Windows (MainWindow/dialogs) are handled via Apply().
// The system-tray context menu is not an app flyout and must keep its existing
// opaque/native-looking surface. Catch Avalonia's dedicated tray popup host here.
// The other Windows (MainWindow/dialogs) are handled via Apply().
Control.LoadedEvent.AddClassHandler<Window>((win, _) =>
{
if (win.GetType().Name == "TrayPopupRoot")
Expand All @@ -77,39 +75,54 @@ private static void ApplyAcrylicToPopup(TopLevel root)
if (!IsMicaEnabled())
return;

// Menus use WinUI's opaque flyout surface. Other transient controls (combo boxes,
// tooltips and ordinary flyouts) retain acrylic.
bool isMenu = root.GetType().Name == "TrayPopupRoot"
|| root.GetVisualDescendants()
.Any(control => control is MenuFlyoutPresenter or ContextMenu);
if (isMenu)
{
root.TransparencyLevelHint = new[] { WindowTransparencyLevel.None };
if (root.TryFindResource("MenuSurfaceBrush", root.ActualThemeVariant, out object? resource)
&& resource is IBrush brush)
{
root.Background = brush;
}
bool isTrayMenu = root.GetType().Name == "TrayPopupRoot";
bool isAppMenu = !isTrayMenu
&& root.GetVisualDescendants()
.Any(control => control is MenuFlyoutPresenter or ContextMenu);

ApplyRoundedCorners(root);
if (isTrayMenu)
{
ApplyOpaqueMenuFallback(root);
return;
}

// Request acrylic (not Transparent): the Transparent level makes a layered window, and DWM
// system backdrops never paint on those — so the popup ended up fully see-through with only
// the presenter tint and make its contents unreadable. AcrylicBlur gives a composited window
// DWM can actually fill. This path only runs when Mica is enabled
// (Win11 + transparency effects), so acrylic is always available here.
// Avalonia's AcrylicBlur is already a real Win32 composition acrylic path:
// it enables the host-backdrop brush and installs Avalonia's backdrop blur/saturation
// effect. Keep one acrylic path for menus, flyouts, tooltips and combo popups instead
// of stacking a second DWM transient backdrop on ordinary Flyout surfaces.
root.TransparencyLevelHint = new[] { WindowTransparencyLevel.AcrylicBlur, WindowTransparencyLevel.Blur };
root.Background = Brushes.Transparent;

if (root.TryGetPlatformHandle()?.Handle is not { } handle || handle == 0)
{
if (isAppMenu)
ApplyOpaqueMenuFallback(root);
return;
}

int corner = DWMWCP_ROUND;
NativeMethods.DwmSetWindowAttribute(handle, DWMWA_WINDOW_CORNER_PREFERENCE, ref corner, sizeof(int));
int backdrop = DWMSBT_TRANSIENTWINDOW;
NativeMethods.DwmSetWindowAttribute(handle, DWMWA_SYSTEMBACKDROP_TYPE, ref backdrop, sizeof(int));
}

private static void ApplyOpaqueMenuFallback(TopLevel root)
{
root.TransparencyLevelHint = new[] { WindowTransparencyLevel.None };

if (root.TryFindResource("MenuSurfaceFallbackBrush", root.ActualThemeVariant, out object? resource)
&& resource is IBrush brush)
{
// A local resource wins over the Windows-Mica transparent menu resource, so the
// presenter and root both return to the current solid menu appearance.
root.Resources["MenuSurfaceBrush"] = brush;
root.Background = brush;
}
else if (root.TryFindResource("MenuSurfaceBrush", root.ActualThemeVariant, out resource)
&& resource is IBrush existingBrush)
{
root.Background = existingBrush;
}

ApplyRoundedCorners(root);
}

private static void ApplyRoundedCorners(TopLevel root)
Expand Down
34 changes: 19 additions & 15 deletions src/UniGetUI.Avalonia/ViewModels/DialogPages/OperationViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@ public sealed record OperationBadgeVm(
string IconPath,
string Primary,
string Secondary
);
)
{
public bool HasSecondary => !string.IsNullOrEmpty(Secondary);
}

public sealed partial class OperationViewModel : ViewModelBase
{
Expand All @@ -50,10 +53,15 @@ public sealed partial class OperationViewModel : ViewModelBase
[ObservableProperty] private string _buttonText;
[ObservableProperty] private bool _progressIndeterminate;
[ObservableProperty] private double _progressValue;
[ObservableProperty] private IBrush _progressBrush;
[ObservableProperty] private IBrush _backgroundBrush;
[ObservableProperty] private IImage? _packageIcon;

private OperationStatus _visualStatus;
public bool IsInQueue => _visualStatus is OperationStatus.InQueue;
public bool IsRunning => _visualStatus is OperationStatus.Running;
public bool IsSucceeded => _visualStatus is OperationStatus.Succeeded;
public bool IsFailed => _visualStatus is OperationStatus.Failed;
public bool IsCanceled => _visualStatus is OperationStatus.Canceled;

private static readonly Uri _fallbackIconUri =
new("avares://UniGetUI/Assets/package_color.png");

Expand All @@ -75,8 +83,7 @@ public OperationViewModel(AbstractOperation operation)
? operation.GetOutput()[^1].Item1
: CoreTools.Translate("Please wait...");
_buttonText = CoreTools.Translate("Cancel");
_progressBrush = new SolidColorBrush(Color.Parse("#888888"));
_backgroundBrush = Brushes.Transparent;
_visualStatus = operation.Status;

_ = LoadIconAsync();

Expand Down Expand Up @@ -198,35 +205,32 @@ private void ApplyStatus(OperationStatus status)

private void ApplyStatusVisuals(OperationStatus status)
{
_visualStatus = status;
OnPropertyChanged(nameof(IsInQueue));
OnPropertyChanged(nameof(IsRunning));
OnPropertyChanged(nameof(IsSucceeded));
OnPropertyChanged(nameof(IsFailed));
OnPropertyChanged(nameof(IsCanceled));

switch (status)
{
case OperationStatus.InQueue:
ProgressBrush = new SolidColorBrush(Color.Parse("#888888"));
BackgroundBrush = Brushes.Transparent;
ButtonText = CoreTools.Translate("Cancel");
break;

case OperationStatus.Running:
ProgressBrush = new SolidColorBrush(Color.Parse("#F0A500"));
BackgroundBrush = new SolidColorBrush(Color.FromArgb(30, 240, 165, 0));
ButtonText = CoreTools.Translate("Cancel");
break;

case OperationStatus.Succeeded:
ProgressBrush = new SolidColorBrush(Color.Parse("#0F7B0F"));
BackgroundBrush = new SolidColorBrush(Color.FromArgb(30, 15, 123, 15));
ButtonText = CoreTools.Translate("Close");
break;

case OperationStatus.Failed:
ProgressBrush = new SolidColorBrush(Color.Parse("#BC0000"));
BackgroundBrush = new SolidColorBrush(Color.FromArgb(40, 188, 0, 0));
ButtonText = CoreTools.Translate("Close");
break;

case OperationStatus.Canceled:
ProgressBrush = new SolidColorBrush(Color.Parse("#9D5D00"));
BackgroundBrush = Brushes.Transparent;
ButtonText = CoreTools.Translate("Close");
break;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,15 @@ public SettingsCard BuildCredentialsCard()
MinWidth = 200,
Margin = new Thickness(0, 0, 0, 4),
};
_usernameBox.Classes.Add("search-field-standalone");

_passwordBox = new TextBox
{
PlaceholderText = CoreTools.Translate("Password"),
MinWidth = 200,
PasswordChar = '●',
};
_passwordBox.Classes.Add("search-field-standalone");

var creds = CoreSettings.GetProxyCredentials();
if (creds is not null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ public CheckboxButtonCard()
IS_INVERTED = false;
AutomationProperties.SetAccessibilityView(Button, AccessibilityView.Control);

RightAlignWrappedContent = true;
Content = new StackPanel
{
Orientation = Orientation.Horizontal,
Expand Down
11 changes: 3 additions & 8 deletions src/UniGetUI.Avalonia/Views/Controls/Settings/CheckboxCard.cs
Original file line number Diff line number Diff line change
Expand Up @@ -142,14 +142,9 @@ public CheckboxCard()
_checkbox.IsCheckedChanged += _checkbox_Toggled;
ApplyAutomationMetadata(_checkbox, _textblock.Text);

// The SettingsCard measures the Header with infinite width, so TextWrapping
// alone won't constrain the warning block. Fix it by updating MaxWidth after
// every layout pass, leaving room for the Content (toggle) area.
SizeChanged += (_, e) =>
{
var contentWidth = (Content as Control)?.Bounds.Width ?? 0;
_warningBlock.MaxWidth = Math.Max(100, e.NewSize.Width - contentWidth - 48);
};
// Header width is constrained by SettingsCard's responsive grid, so both the title
// and warning text wrap naturally around the right-side toggle.
RightAlignWrappedContent = true;
}

protected void UpdateStateLabel()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,14 +126,8 @@ public SecureCheckboxCard()
this.GetObservable(IsEnabledProperty)
.SubscribeValue(enabled => _warningBlock.Opacity = enabled ? 1 : 0.2);

// The Devolutions SettingsCard measures the Header with infinite width, so
// TextWrapping alone won't constrain the warning block. We fix it by updating
// MaxWidth after every layout pass, leaving room for the Content (toggle) area.
SizeChanged += (_, e) =>
{
var contentWidth = (Content as Control)?.Bounds.Width ?? 0;
_warningBlock.MaxWidth = Math.Max(100, e.NewSize.Width - contentWidth - 48);
};
// Keep the toggle group on the right when the SettingsCard enters its wrapped layout.
RightAlignWrappedContent = true;
}

protected virtual async Task _checkbox_Toggled()
Expand Down
Loading
Loading