Browse Source

Fluent2: use OS accent shades on Windows, drop WinUI's forced MinWidth

PlatformColorValues gains six optional accent shade slots
(AccentColorDark1-3/Light1-3); the Win32 backend fills them from the
UISettings object it already holds, and SystemAccentColors prefers them
over HSL-computed shades, so the palette matches Windows exactly.

CheckBox/RadioButton no longer force WinUI's 120px MinWidth (v1 never
had it); 8px trailing padding compensates, and the CheckBoxMinWidth/
RadioButtonMinWidth resources stay as override knobs. Both changes
follow FluentAvalonia's (MIT) lead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
pull/21771/head
Mike James 3 months ago
parent
commit
71bbc901a5
  1. 31
      src/Avalonia.Base/Platform/PlatformColorValues.cs
  2. 20
      src/Avalonia.Themes.Fluent2/Accents/SystemAccentColors.cs
  3. 8
      src/Avalonia.Themes.Fluent2/Controls/CheckBox.xaml
  4. 8
      src/Avalonia.Themes.Fluent2/Controls/RadioButton.xaml
  5. 12
      src/Avalonia.Themes.Fluent2/README.md
  6. 10
      src/Windows/Avalonia.Win32/Win32PlatformSettings.cs
  7. 50
      tests/Avalonia.Themes.Fluent2.UnitTests/ControlDefaultsTests.cs
  8. 43
      tests/Avalonia.Themes.Fluent2.UnitTests/PaletteTests.cs

31
src/Avalonia.Base/Platform/PlatformColorValues.cs

@ -61,6 +61,37 @@ public record PlatformColorValues
init => _accentColor3 = value; init => _accentColor3 = value;
} }
/// <summary>
/// Dark shade 1 of <see cref="AccentColor1"/>, when the platform provides one (Windows does).
/// Null when the platform only reports the base accent color; consumers are expected to compute a shade instead.
/// </summary>
public Color? AccentColorDark1 { get; init; }
/// <summary>
/// Dark shade 2 of <see cref="AccentColor1"/>, when the platform provides one. See <see cref="AccentColorDark1"/>.
/// </summary>
public Color? AccentColorDark2 { get; init; }
/// <summary>
/// Dark shade 3 of <see cref="AccentColor1"/>, when the platform provides one. See <see cref="AccentColorDark1"/>.
/// </summary>
public Color? AccentColorDark3 { get; init; }
/// <summary>
/// Light shade 1 of <see cref="AccentColor1"/>, when the platform provides one. See <see cref="AccentColorDark1"/>.
/// </summary>
public Color? AccentColorLight1 { get; init; }
/// <summary>
/// Light shade 2 of <see cref="AccentColor1"/>, when the platform provides one. See <see cref="AccentColorDark1"/>.
/// </summary>
public Color? AccentColorLight2 { get; init; }
/// <summary>
/// Light shade 3 of <see cref="AccentColor1"/>, when the platform provides one. See <see cref="AccentColorDark1"/>.
/// </summary>
public Color? AccentColorLight3 { get; init; }
public PlatformColorValues() public PlatformColorValues()
{ {
AccentColor1 = DefaultAccent; AccentColor1 = DefaultAccent;

20
src/Avalonia.Themes.Fluent2/Accents/SystemAccentColors.cs

@ -20,7 +20,8 @@ internal sealed class SystemAccentColors : ResourceProvider
private static readonly Color s_defaultSystemAccentColor = Color.FromRgb(0, 120, 215); private static readonly Color s_defaultSystemAccentColor = Color.FromRgb(0, 120, 215);
// Static shade values from WinUI (matching the Uno/WinUI SystemResources defaults). Used when // Static shade values from WinUI (matching the Uno/WinUI SystemResources defaults). Used when
// the platform provides no accent color, so the out-of-the-box palette matches WinUI exactly. // the platform provides no accent color, so the out-of-the-box palette matches WinUI exactly.
// OS-provided accent colors get computed shades instead. // When the platform reports the OS-computed shades (Windows does, via UISettings) those are
// used verbatim; otherwise an OS-provided base accent gets HSL-computed shades.
private static readonly Color s_defaultSystemAccentColorDark1 = Color.FromUInt32(0xFF005A9E); private static readonly Color s_defaultSystemAccentColorDark1 = Color.FromUInt32(0xFF005A9E);
private static readonly Color s_defaultSystemAccentColorDark2 = Color.FromUInt32(0xFF004275); private static readonly Color s_defaultSystemAccentColorDark2 = Color.FromUInt32(0xFF004275);
private static readonly Color s_defaultSystemAccentColorDark3 = Color.FromUInt32(0xFF002642); private static readonly Color s_defaultSystemAccentColorDark3 = Color.FromUInt32(0xFF002642);
@ -117,11 +118,22 @@ internal sealed class SystemAccentColors : ResourceProvider
{ {
_invalidateColors = false; _invalidateColors = false;
var platformSettings = GetFromOwner(Owner); var colorValues = GetFromOwner(Owner)?.GetColorValues();
_systemAccentColor = platformSettings?.GetColorValues().AccentColor1 ?? s_defaultSystemAccentColor; _systemAccentColor = colorValues?.AccentColor1 ?? s_defaultSystemAccentColor;
if (_systemAccentColor == s_defaultSystemAccentColor) if (colorValues is
{
AccentColorDark1: { } dark1, AccentColorDark2: { } dark2, AccentColorDark3: { } dark3,
AccentColorLight1: { } light1, AccentColorLight2: { } light2, AccentColorLight3: { } light3
})
{
// The platform reported the OS-computed shades (Windows, via UISettings);
// use them verbatim so the palette matches the OS exactly.
(_systemAccentColorDark1, _systemAccentColorDark2, _systemAccentColorDark3) = (dark1, dark2, dark3);
(_systemAccentColorLight1, _systemAccentColorLight2, _systemAccentColorLight3) = (light1, light2, light3);
}
else if (_systemAccentColor == s_defaultSystemAccentColor)
{ {
// Platforms without a real accent color report the default; use WinUI's static // Platforms without a real accent color report the default; use WinUI's static
// shade values so the out-of-the-box palette matches WinUI exactly. // shade values so the out-of-the-box palette matches WinUI exactly.

8
src/Avalonia.Themes.Fluent2/Controls/CheckBox.xaml

@ -14,11 +14,15 @@
<StreamGeometry x:Key="CheckMarkPathData">M5.5 10.586 1.707 6.793A1 1 0 0 0 .293 8.207l4.5 4.5a1 1 0 0 0 1.414 0l11-11A1 1 0 0 0 15.793.293L5.5 10.586Z</StreamGeometry> <StreamGeometry x:Key="CheckMarkPathData">M5.5 10.586 1.707 6.793A1 1 0 0 0 .293 8.207l4.5 4.5a1 1 0 0 0 1.414 0l11-11A1 1 0 0 0 15.793.293L5.5 10.586Z</StreamGeometry>
<x:Double x:Key="CheckBoxMinHeight">32</x:Double> <x:Double x:Key="CheckBoxMinHeight">32</x:Double>
<x:Double x:Key="CheckBoxMinWidth">120</x:Double> <!-- WinUI forces MinWidth=120, which breaks desktop form layouts (and v1 never had it).
Deliberately 0 here; override this resource to restore the WinUI metric. -->
<x:Double x:Key="CheckBoxMinWidth">0</x:Double>
<!-- WinUI reference: CheckBox_themeresources.xaml (DefaultCheckBoxStyle), tag winui3/release/1.5.2 --> <!-- WinUI reference: CheckBox_themeresources.xaml (DefaultCheckBoxStyle), tag winui3/release/1.5.2 -->
<ControlTheme x:Key="{x:Type CheckBox}" TargetType="CheckBox"> <ControlTheme x:Key="{x:Type CheckBox}" TargetType="CheckBox">
<Setter Property="Padding" Value="8,5,0,0" /> <!-- WinUI pads 8,5,0,0 and relies on MinWidth for trailing space; with MinWidth dropped,
8px right padding keeps a sane hit target (same amendment FluentAvalonia made). -->
<Setter Property="Padding" Value="8,5,8,0" />
<Setter Property="HorizontalAlignment" Value="Left" /> <Setter Property="HorizontalAlignment" Value="Left" />
<Setter Property="VerticalAlignment" Value="Center" /> <Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="HorizontalContentAlignment" Value="Left" /> <Setter Property="HorizontalContentAlignment" Value="Left" />

8
src/Avalonia.Themes.Fluent2/Controls/RadioButton.xaml

@ -12,7 +12,9 @@
</Design.PreviewWith> </Design.PreviewWith>
<x:Double x:Key="RadioButtonMinHeight">32</x:Double> <x:Double x:Key="RadioButtonMinHeight">32</x:Double>
<x:Double x:Key="RadioButtonMinWidth">120</x:Double> <!-- WinUI forces MinWidth=120, which breaks desktop form layouts (and v1 never had it).
Deliberately 0 here; override this resource to restore the WinUI metric. -->
<x:Double x:Key="RadioButtonMinWidth">0</x:Double>
<x:Double x:Key="RadioButtonCheckGlyphSize">12</x:Double> <x:Double x:Key="RadioButtonCheckGlyphSize">12</x:Double>
<x:Double x:Key="RadioButtonCheckGlyphPointerOverSize">14</x:Double> <x:Double x:Key="RadioButtonCheckGlyphPointerOverSize">14</x:Double>
<x:Double x:Key="RadioButtonCheckGlyphPressedOverSize">10</x:Double> <x:Double x:Key="RadioButtonCheckGlyphPressedOverSize">10</x:Double>
@ -28,7 +30,9 @@
<Setter Property="Foreground" Value="{DynamicResource RadioButtonForeground}" /> <Setter Property="Foreground" Value="{DynamicResource RadioButtonForeground}" />
<Setter Property="BorderBrush" Value="{DynamicResource RadioButtonBorderBrush}" /> <Setter Property="BorderBrush" Value="{DynamicResource RadioButtonBorderBrush}" />
<Setter Property="CornerRadius" Value="{DynamicResource ControlCornerRadius}" /> <Setter Property="CornerRadius" Value="{DynamicResource ControlCornerRadius}" />
<Setter Property="Padding" Value="8,6,0,0" /> <!-- WinUI pads 8,6,0,0 and relies on MinWidth for trailing space; with MinWidth dropped,
8px right padding keeps a sane hit target (same amendment FluentAvalonia made). -->
<Setter Property="Padding" Value="8,6,8,0" />
<Setter Property="MinWidth" Value="{DynamicResource RadioButtonMinWidth}" /> <Setter Property="MinWidth" Value="{DynamicResource RadioButtonMinWidth}" />
<Setter Property="HorizontalAlignment" Value="Left" /> <Setter Property="HorizontalAlignment" Value="Left" />
<Setter Property="VerticalAlignment" Value="Center" /> <Setter Property="VerticalAlignment" Value="Center" />

12
src/Avalonia.Themes.Fluent2/README.md

@ -48,6 +48,12 @@ Migrating from `Avalonia.Themes.Fluent`: change the package reference and swap
| Scroll bars | 16 px rail | **12 px rail, 2 px collapsed thumb** | | Scroll bars | 16 px rail | **12 px rail, 2 px collapsed thumb** |
| Window background | AltHigh | **SolidBackgroundFillColorBase** (#F3F3F3/#202020) | | Window background | AltHigh | **SolidBackgroundFillColorBase** (#F3F3F3/#202020) |
| Default accent shades | HSL-computed | **WinUI static values** (when no OS accent) | | Default accent shades | HSL-computed | **WinUI static values** (when no OS accent) |
| OS accent shades (Windows) | HSL-computed | **read from the OS** (`UISettings` Light1–3/Dark1–3) |
One deliberate deviation from WinUI (following FluentAvalonia's lead): CheckBox
and RadioButton do **not** get WinUI's forced 120 px `MinWidth` — they size to
content plus 8 px trailing padding, as in v1. Set the `CheckBoxMinWidth` /
`RadioButtonMinWidth` resources to restore the WinUI metric.
The full WinUI 3 token family (`TextFillColor*`, `ControlFillColor*`, The full WinUI 3 token family (`TextFillColor*`, `ControlFillColor*`,
`SubtleFillColor*`, `ControlStrokeColor*`, `CardBackgroundFillColor*`, `SubtleFillColor*`, `ControlStrokeColor*`, `CardBackgroundFillColor*`,
@ -64,8 +70,10 @@ legacy `SystemControl*` aliases.
- No HighContrast variant yet (same as v1). - No HighContrast variant yet (same as v1).
- AnimatedIcon glyph animations are approximated with static glyphs and simple - AnimatedIcon glyph animations are approximated with static glyphs and simple
transitions. transitions.
- OS-provided accent colors still use HSL-computed shades, which can deviate - On Windows the six accent shades (`SystemAccentColorDark1`…`Light3`) come
slightly from Windows' palette algorithm. straight from the OS; on platforms that report only a base accent color
(macOS, Linux) the shades are HSL-computed and can deviate slightly from
Windows' palette algorithm.
## Compatibility tests ## Compatibility tests

10
src/Windows/Avalonia.Win32/Win32PlatformSettings.cs

@ -73,8 +73,14 @@ internal class Win32PlatformSettings : DefaultPlatformSettings
PlatformThemeVariant.Dark : PlatformThemeVariant.Dark :
PlatformThemeVariant.Light, PlatformThemeVariant.Light,
ContrastPreference = ColorContrastPreference.NoPreference, ContrastPreference = ColorContrastPreference.NoPreference,
AccentColor1 = accent AccentColor1 = accent,
}; AccentColorDark1 = uiSettings.GetColorValue(UIColorType.AccentDark1).ToAvalonia(),
AccentColorDark2 = uiSettings.GetColorValue(UIColorType.AccentDark2).ToAvalonia(),
AccentColorDark3 = uiSettings.GetColorValue(UIColorType.AccentDark3).ToAvalonia(),
AccentColorLight1 = uiSettings.GetColorValue(UIColorType.AccentLight1).ToAvalonia(),
AccentColorLight2 = uiSettings.GetColorValue(UIColorType.AccentLight2).ToAvalonia(),
AccentColorLight3 = uiSettings.GetColorValue(UIColorType.AccentLight3).ToAvalonia()
};
} }
} }

50
tests/Avalonia.Themes.Fluent2.UnitTests/ControlDefaultsTests.cs

@ -0,0 +1,50 @@
using Avalonia.Controls;
using Avalonia.Threading;
namespace Avalonia.Themes.Fluent2.UnitTests;
/// <summary>
/// Guards the deliberate desktop-ergonomic deviations from the WinUI metrics.
/// </summary>
public class ControlDefaultsTests
{
[AvaloniaFact]
public void CheckBox_and_RadioButton_do_not_force_winui_min_width()
{
var theme = new Fluent2Theme();
var app = Application.Current!;
app.Styles.Add(theme);
try
{
var checkBox = new CheckBox { Content = "Ok" };
var radioButton = new RadioButton { Content = "Ok" };
var window = new Window
{
Width = 400,
Height = 300,
Content = new StackPanel { Children = { checkBox, radioButton } },
};
window.Show();
try
{
Dispatcher.UIThread.RunJobs();
// WinUI forces MinWidth=120 on both; v1 never did, and neither does Fluent2.
Assert.Equal(0d, checkBox.MinWidth);
Assert.Equal(0d, radioButton.MinWidth);
Assert.True(checkBox.Bounds.Width < 120,
$"CheckBox width {checkBox.Bounds.Width} should hug its content.");
Assert.True(radioButton.Bounds.Width < 120,
$"RadioButton width {radioButton.Bounds.Width} should hug its content.");
}
finally
{
window.Close();
}
}
finally
{
app.Styles.Remove(theme);
}
}
}

43
tests/Avalonia.Themes.Fluent2.UnitTests/PaletteTests.cs

@ -1,5 +1,6 @@
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.Styling; using Avalonia.Styling;
namespace Avalonia.Themes.Fluent2.UnitTests; namespace Avalonia.Themes.Fluent2.UnitTests;
@ -65,6 +66,48 @@ public class PaletteTests
} }
} }
[AvaloniaFact]
public void Platform_provided_accent_shades_take_precedence_over_computed_ones()
{
var originalSettings = AvaloniaLocator.Current.GetService<IPlatformSettings>();
Assert.NotNull(originalSettings);
AvaloniaLocator.CurrentMutable.Bind<IPlatformSettings>().ToConstant(new ShadeReportingPlatformSettings());
var theme = new Fluent2Theme();
var app = Application.Current!;
app.Styles.Add(theme);
try
{
Assert.True(app.TryGetResource("SystemAccentColor", ThemeVariant.Light, out var accent));
Assert.Equal(Colors.Crimson, Assert.IsType<Color>(accent));
// The OS-reported shades are used verbatim, not HSL-computed from the base color.
Assert.True(app.TryGetResource("SystemAccentColorDark1", ThemeVariant.Light, out var dark1));
Assert.Equal(Colors.DarkRed, Assert.IsType<Color>(dark1));
Assert.True(app.TryGetResource("SystemAccentColorLight3", ThemeVariant.Light, out var light3));
Assert.Equal(Colors.MistyRose, Assert.IsType<Color>(light3));
}
finally
{
app.Styles.Remove(theme);
AvaloniaLocator.CurrentMutable.Bind<IPlatformSettings>().ToConstant(originalSettings);
}
}
private sealed class ShadeReportingPlatformSettings : DefaultPlatformSettings
{
public override PlatformColorValues GetColorValues() => new()
{
AccentColor1 = Colors.Crimson,
AccentColorDark1 = Colors.DarkRed,
AccentColorDark2 = Colors.Maroon,
AccentColorDark3 = Colors.Brown,
AccentColorLight1 = Colors.Salmon,
AccentColorLight2 = Colors.LightSalmon,
AccentColorLight3 = Colors.MistyRose,
};
}
[AvaloniaFact] [AvaloniaFact]
public void Legacy_palette_colors_derive_fluent2_tokens() public void Legacy_palette_colors_derive_fluent2_tokens()
{ {

Loading…
Cancel
Save