Skip to content

Commit 3c2a7bc

Browse files
MovGP0Johann Dirry
andauthored
Implementing conversions from int to System.Windows.Media.Color for MaterialColorUtilities (#3949)
* adding conversion methods for converting argb colors between System.Int32 and System.Windows.Media.Color representations * implementing unit tests for scheme colors; showing how to create a scheme from a primary color * adding a factory method for creating dynamic color schemes --------- Co-authored-by: Johann Dirry <[email protected]>
1 parent 5ec45a3 commit 3c2a7bc

File tree

7 files changed

+240
-4
lines changed

7 files changed

+240
-4
lines changed

Directory.packages.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,5 +32,6 @@
3232
<PackageVersion Include="System.Memory" Version="4.6.3" />
3333
<PackageVersion Include="System.ValueTuple" Version="4.6.1" />
3434
<PackageVersion Include="Polyfill" Version="8.8.1" />
35+
<PackageVersion Include="Shouldly" Version="4.3.0" />
3536
</ItemGroup>
3637
</Project>

src/MaterialDesign3.MaterialColorUtilities/DynamicColor/DynamicColor.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,9 @@ public int GetArgb(DynamicScheme scheme)
140140
return (argb & 0x00ffffff) | (alpha << 24);
141141
}
142142

143+
public System.Windows.Media.Color GetColor(DynamicScheme scheme)
144+
=> ColorUtils.ColorFromArgb(GetArgb(scheme));
145+
143146
public Hct GetHct(DynamicScheme scheme)
144147
{
145148
if (hctCache.TryGetValue(scheme, out var cached))
@@ -320,4 +323,4 @@ private void ValidateExtendedColor(SpecVersion specVersion, DynamicColor extende
320323
}
321324
}
322325
}
323-
}
326+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
using System.Windows.Media;
2+
3+
namespace MaterialColorUtilities;
4+
5+
/// <summary>
6+
/// Factory for creating a dynamic color scheme.
7+
/// </summary>
8+
public static class DynamicSchemeFactory
9+
{
10+
/// <summary>
11+
/// Factory method for creating a dynamic color scheme.
12+
/// </summary>
13+
/// <remarks>
14+
/// The colors are optional. If any of them are null,
15+
/// the color will be automatically generated based on the source color.
16+
/// </remarks>
17+
public static DynamicScheme Create(
18+
Color sourceColor,
19+
Variant variant,
20+
bool isDark,
21+
double contrastLevel,
22+
Platform platform,
23+
SpecVersion specVersion,
24+
Color? primary,
25+
Color? secondary,
26+
Color? tertiary,
27+
Color? neutral,
28+
Color? neutralVariant,
29+
Color? error)
30+
{
31+
var sourceColorHct = Hct.FromInt(ColorUtils.ArgbFromColor(sourceColor));
32+
33+
TonalPalette primaryPalette = primary == null
34+
? ColorSpecs.Get(specVersion).GetPrimaryPalette(variant, sourceColorHct, isDark, platform, contrastLevel)
35+
: ColorSpecs.Get(specVersion).GetPrimaryPalette(variant, Hct.FromInt(ColorUtils.ArgbFromColor(primary.Value)), isDark, platform, contrastLevel);
36+
37+
TonalPalette secondaryPalette = secondary == null
38+
? ColorSpecs.Get(specVersion).GetSecondaryPalette(variant, sourceColorHct, isDark, platform, contrastLevel)
39+
: ColorSpecs.Get(specVersion).GetSecondaryPalette(variant, Hct.FromInt(ColorUtils.ArgbFromColor(secondary.Value)), isDark, platform, contrastLevel);
40+
41+
TonalPalette tertiaryPalette = tertiary == null
42+
? ColorSpecs.Get(specVersion).GetTertiaryPalette(variant, sourceColorHct, isDark, platform, contrastLevel)
43+
: ColorSpecs.Get(specVersion).GetTertiaryPalette(variant, Hct.FromInt(ColorUtils.ArgbFromColor(tertiary.Value)), isDark, platform, contrastLevel);
44+
45+
TonalPalette neutralPalette = neutral == null
46+
? ColorSpecs.Get(specVersion).GetNeutralPalette(variant, sourceColorHct, isDark, platform, contrastLevel)
47+
: ColorSpecs.Get(specVersion).GetNeutralPalette(variant, Hct.FromInt(ColorUtils.ArgbFromColor(neutral.Value)), isDark, platform, contrastLevel);
48+
49+
TonalPalette neutralVariantPalette = neutralVariant == null
50+
? ColorSpecs.Get(specVersion).GetNeutralVariantPalette(variant, sourceColorHct, isDark, platform, contrastLevel)
51+
: ColorSpecs.Get(specVersion).GetNeutralVariantPalette(variant, Hct.FromInt(ColorUtils.ArgbFromColor(neutralVariant.Value)), isDark, platform, contrastLevel);
52+
53+
TonalPalette? errorPalette = error == null
54+
? ColorSpecs.Get(specVersion).GetErrorPalette(variant, sourceColorHct, isDark, platform, contrastLevel)
55+
: ColorSpecs.Get(specVersion).GetErrorPalette(variant, Hct.FromInt(ColorUtils.ArgbFromColor(error.Value)), isDark, platform, contrastLevel);
56+
57+
return new DynamicScheme(
58+
sourceColorHct,
59+
variant,
60+
isDark,
61+
contrastLevel,
62+
platform,
63+
specVersion,
64+
primaryPalette,
65+
secondaryPalette,
66+
tertiaryPalette,
67+
neutralPalette,
68+
neutralVariantPalette,
69+
errorPalette);
70+
}
71+
}

src/MaterialDesign3.MaterialColorUtilities/Utils/ColorUtils.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,22 @@ public static class ColorUtils
2626
/// </summary>
2727
public static int ArgbFromRgb(int red, int green, int blue) => (255 << 24) | ((red & 255) << 16) | ((green & 255) << 8) | (blue & 255);
2828

29+
/// <summary>
30+
/// Converts a color in ARGB format to a <see cref="System.Windows.Media.Color"/>.
31+
/// </summary>
32+
public static System.Windows.Media.Color ColorFromArgb(int argb) =>
33+
System.Windows.Media.Color.FromArgb(
34+
(byte)AlphaFromArgb(argb),
35+
(byte)RedFromArgb(argb),
36+
(byte)GreenFromArgb(argb),
37+
(byte)BlueFromArgb(argb));
38+
39+
/// <summary>
40+
/// Converts a <see cref="System.Windows.Media.Color"/> to ARGB format.
41+
/// </summary>
42+
public static int ArgbFromColor(System.Windows.Media.Color color) =>
43+
(color.A << 24) | (color.R << 16) | (color.G << 8) | color.B;
44+
2945
/// <summary>
3046
/// Converts a color from linear RGB components to ARGB format.
3147
/// </summary>

tests/MaterialColorUtilities.Tests/ColorUtilsTests.cs

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
namespace MaterialColorUtilities.Tests;
1+
using System.Windows.Media;
2+
3+
namespace MaterialColorUtilities.Tests;
24

35
public sealed class ColorUtilsTests
46
{
@@ -264,4 +266,41 @@ public async Task Linearize_Delinearize_RoundTrip()
264266
await Assert.That(converted).IsEqualTo(c);
265267
}
266268
}
269+
270+
public static IEnumerable<Func<(int argb, Color color)>> TestColors()
271+
{
272+
yield return () => (unchecked((int)0xFFFF0000), Colors.Red);
273+
yield return () => (unchecked((int)0xFF00FF00), Colors.Lime);
274+
yield return () => (unchecked((int)0xFF0000FF), Colors.Blue);
275+
yield return () => (unchecked((int)0xFFFF00FF), Colors.Magenta);
276+
yield return () => (unchecked((int)0xFFFFFF00), Colors.Yellow);
277+
yield return () => (unchecked((int)0xFF00FFFF), Colors.Cyan);
278+
yield return () => (unchecked((int)0xFFFFFFFF), Colors.White);
279+
yield return () => (unchecked((int)0xFF000000), Colors.Black);
280+
yield return () => (unchecked((int)0x00FFFFFF), Colors.Transparent);
281+
}
282+
283+
[Test]
284+
[DisplayName("colorFromArgb returns known Colors")]
285+
[MethodDataSource(nameof(TestColors))]
286+
public async Task ColorFromArgb_KnownColors(int argb, Color color)
287+
{
288+
var converted = ColorUtils.ColorFromArgb(argb);
289+
290+
string result = $"{converted.A:x}{converted.R:x}{converted.G:x}{converted.B:X}";
291+
string expected = $"{color.A:x}{color.R:x}{color.G:x}{color.B:X}";
292+
293+
await Assert.That(result).IsEqualTo(expected);
294+
}
295+
296+
[Test]
297+
[DisplayName("argbFromColor returns known ints")]
298+
[MethodDataSource(nameof(TestColors))]
299+
public async Task ArgbFromColor_KnownColors(int argb, Color color)
300+
{
301+
string result = ColorUtils.ArgbFromColor(color).ToString("X");
302+
string expected = argb.ToString("X");
303+
304+
await Assert.That(result).IsEqualTo(expected);
305+
}
267306
}

tests/MaterialColorUtilities.Tests/DynamicSchemeTests.cs

Lines changed: 104 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using System.Runtime.Serialization;
1+
using System.Windows.Media;
22

33
namespace MaterialColorUtilities.Tests;
44

@@ -65,4 +65,107 @@ public async Task RotationGreaterThan360_Wraps()
6565
// 43 + 480 = 523 -> 163 after sanitize/wrap
6666
await Assert.That(hue).IsEqualTo(163.0).Within(1.0);
6767
}
68+
69+
/// <summary>
70+
/// Shows how te crate a theme from a primary color.
71+
/// </summary>
72+
[Test]
73+
public async Task CreateThemeFromColor()
74+
{
75+
var mdc = new MaterialDynamicColors();
76+
var primaryColorHct = Hct.FromInt(ColorUtils.ArgbFromColor(Color.FromArgb(0xff, 0x6a, 0x9c, 0x59)));
77+
var scheme = new SchemeContent(
78+
primaryColorHct,
79+
isDark: true,
80+
contrastLevel : 0.5,
81+
SpecVersion.Spec2025,
82+
Platform.Phone);
83+
84+
scheme.ShouldSatisfyAllConditions(
85+
// Main Palettes
86+
() => mdc.PrimaryPaletteKeyColor.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0x52, 0x83, 0x43)),
87+
() => mdc.SecondaryPaletteKeyColor.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0x68, 0x7D, 0x5E)),
88+
() => mdc.TertiaryPaletteKeyColor.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0x2B, 0x9F, 0x94)),
89+
() => mdc.NeutralPaletteKeyColor.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0x75, 0x78, 0x71)),
90+
() => mdc.NeutralVariantPaletteKeyColor.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0x72, 0x79, 0x6C)),
91+
() => mdc.ErrorPaletteKeyColor.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0xDE, 0x37, 0x30)),
92+
93+
// Surfaces [S]
94+
() => mdc.Background.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0x11, 0x14, 0x0F)),
95+
() => mdc.OnBackground.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0xE1, 0xE3, 0xDB)),
96+
() => mdc.Surface.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0x11, 0x14, 0x0F)),
97+
() => mdc.SurfaceDim.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0x11, 0x14, 0x0F)),
98+
() => mdc.SurfaceBright.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0x42, 0x45, 0x3F)),
99+
() => mdc.SurfaceContainerLowest.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0x06, 0x08, 0x05)),
100+
() => mdc.SurfaceContainerLow.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0x1B, 0x1E, 0x19)),
101+
() => mdc.SurfaceContainer.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0x26, 0x29, 0x23)),
102+
() => mdc.SurfaceContainerHigh.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0x30, 0x33, 0x2E)),
103+
() => mdc.SurfaceContainerHighest.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0x3C, 0x3F, 0x39)),
104+
() => mdc.OnSurface.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0xFF, 0xFF, 0xFF)),
105+
() => mdc.SurfaceVariant.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0x42, 0x49, 0x3E)),
106+
() => mdc.OnSurfaceVariant.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0xD8, 0xDF, 0xCF)),
107+
() => mdc.Outline.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0xAD, 0xB4, 0xA6)),
108+
() => mdc.OutlineVariant.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0x8B, 0x93, 0x85)),
109+
() => mdc.InverseSurface.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0xE1, 0xE3, 0xDB)),
110+
() => mdc.InverseOnSurface.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0x28, 0x2B, 0x25)),
111+
() => mdc.Shadow.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0x00, 0x00, 0x00)),
112+
() => mdc.Scrim.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0x00, 0x00, 0x00)),
113+
() => mdc.SurfaceTint.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0x9F, 0xD5, 0x8B)),
114+
115+
// Primaries [P]
116+
() => mdc.Primary.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0xB5, 0xEB, 0x9F)),
117+
() => mdc.PrimaryDim?.GetColor(scheme).ShouldBe(Color.FromArgb(0x00, 0x00, 0x00, 0x00)),
118+
() => mdc.OnPrimary.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0x04, 0x2D, 0x00)),
119+
() => mdc.PrimaryContainer.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0x6B, 0x9D, 0x5A)),
120+
() => mdc.OnPrimaryContainer.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0x00, 0x00, 0x00)),
121+
() => mdc.PrimaryFixed.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0xBB, 0xF1, 0xA5)),
122+
() => mdc.PrimaryFixedDim.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0x9F, 0xD5, 0x8B)),
123+
() => mdc.OnPrimaryFixed.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0x01, 0x16, 0x00)),
124+
() => mdc.OnPrimaryFixedVariant.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0x10, 0x3F, 0x06)),
125+
() => mdc.InversePrimary.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0x24, 0x52, 0x18)),
126+
127+
// Secondaries [Q]
128+
() => mdc.Secondary.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0xCC, 0xE3, 0xBF)),
129+
() => mdc.SecondaryDim?.GetColor(scheme).ShouldBe(Color.FromArgb(0x00, 0x00, 0x00, 0x00)),
130+
() => mdc.OnSecondary.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0x18, 0x2A, 0x12)),
131+
() => mdc.SecondaryContainer.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0x81, 0x97, 0x77)),
132+
() => mdc.OnSecondaryContainer.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0x00, 0x00, 0x00)),
133+
134+
// Secondary Fixed [QF]
135+
() => mdc.SecondaryFixed.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0xD2, 0xE9, 0xC5)),
136+
() => mdc.SecondaryFixedDim.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0xB7, 0xCD, 0xAA)),
137+
() => mdc.OnSecondaryFixed.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0x05, 0x15, 0x02)),
138+
() => mdc.OnSecondaryFixedVariant.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0x28, 0x3B, 0x22)),
139+
140+
// Tertiaries [T]
141+
() => mdc.Tertiary.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0x85, 0xEE, 0xE1)),
142+
() => mdc.TertiaryDim?.GetColor(scheme).ShouldBe(Color.FromArgb(0x00, 0x00, 0x00, 0x00)),
143+
() => mdc.OnTertiary.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0x00, 0x2B, 0x27)),
144+
() => mdc.TertiaryContainer.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0x2D, 0xA1, 0x96)),
145+
() => mdc.OnTertiaryContainer.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0x00, 0x00, 0x00)),
146+
147+
// Tertiary Fixed [TF]
148+
() => mdc.TertiaryFixed.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0x8B, 0xF5, 0xE8)),
149+
() => mdc.TertiaryFixedDim.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0x6E, 0xD8, 0xCC)),
150+
() => mdc.OnTertiaryFixed.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0x00, 0x15, 0x12)),
151+
() => mdc.OnTertiaryFixedVariant.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0x00, 0x3E, 0x38)),
152+
153+
// Errors [E]
154+
() => mdc.Error.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0xFF, 0xD2, 0xCC)),
155+
() => mdc.ErrorDim?.GetColor(scheme).ShouldBe(Color.FromArgb(0x00, 0x00, 0x00, 0x00)),
156+
() => mdc.OnError.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0x54, 0x00, 0x03)),
157+
() => mdc.ErrorContainer.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0xFF, 0x54, 0x49)),
158+
() => mdc.OnErrorContainer.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0x00, 0x00, 0x00)),
159+
160+
// Android-only
161+
() => mdc.ControlActivated.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0x23, 0x51, 0x17)),
162+
() => mdc.ControlNormal.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0xC2, 0xC9, 0xBA)),
163+
() => mdc.ControlHighlight.GetColor(scheme).ShouldBe(Color.FromArgb(0x33, 0xFF, 0xFF, 0xFF)),
164+
() => mdc.TextPrimaryInverse.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0x19, 0x1C, 0x17)),
165+
() => mdc.TextSecondaryAndTertiaryInverse.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0x42, 0x49, 0x3E)),
166+
() => mdc.TextPrimaryInverseDisableOnly.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0x19, 0x1C, 0x17)),
167+
() => mdc.TextSecondaryAndTertiaryInverseDisabled.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0x19, 0x1C, 0x17)),
168+
() => mdc.TextHintInverse.GetColor(scheme).ShouldBe(Color.FromArgb(0xFF, 0x19, 0x1C, 0x17)));
169+
await Task.CompletedTask;
170+
}
68171
}

tests/MaterialColorUtilities.Tests/MaterialColorUtilities.Tests.csproj

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<?xml version="1.0" encoding="utf-8"?>
22
<Project Sdk="Microsoft.NET.Sdk">
33
<PropertyGroup>
4-
<TargetFrameworks>net8.0-windows</TargetFrameworks>
4+
<TargetFramework>net8.0-windows</TargetFramework>
55
<AssemblyTitle>MaterialColorUtilities.Tests</AssemblyTitle>
66
<Product>MaterialColorUtilities.Tests</Product>
77
<OutputType>Exe</OutputType>
@@ -13,6 +13,7 @@
1313
<EnableSourceLink>false</EnableSourceLink>
1414
<SuppressImplicitGitSourceLink>true</SuppressImplicitGitSourceLink>
1515
</PropertyGroup>
16+
1617
<ItemGroup>
1718
<ProjectReference Include="..\..\src\MaterialDesign3.MaterialColorUtilities\MaterialColorUtilities.csproj" />
1819
</ItemGroup>
@@ -25,10 +26,12 @@
2526
<PackageReference Include="System.Net.Http" />
2627
<PackageReference Include="System.Text.RegularExpressions" />
2728
<PackageReference Include="TUnit" />
29+
<PackageReference Include="Shouldly" />
2830
</ItemGroup>
2931
<ItemGroup>
3032
<Using Include="TUnit.Assertions.AssertConditions.Throws" />
3133
<Using Include="TUnit.Core.Executors" />
3234
<Using Include="TUnit.Assertions" />
35+
<Using Include="Shouldly" />
3336
</ItemGroup>
3437
</Project>

0 commit comments

Comments
 (0)