Browse Source

feat: implement unified IFormattable on Color, HslColor, and HsvColor (#20919)

* fix: correct HslColor.ToString() prefix from hsva to hsla

Pre-existing copy-paste bug: HslColor.ToString() was outputting
"hsva(" instead of "hsla(" for its default format.

* feat: implement unified IFormattable on Color, HslColor, and HsvColor

All three color types now support all format specifiers via
auto-conversion, following the DateTime analogy where the same
format codes work regardless of source type.

Format specifiers:
  Hex:  X (#AARRGGBB), x (#RRGGBB), H (#RRGGBBAA)
  RGB:  R/r (absolute), R%/r% (percent)
  HSL:  L/l (CSS standard), L%/l% (all percent)
  HSV:  V/v (CSS standard), V%/v% (all percent)

Convention: uppercase = include alpha (rgba/hsla/hsva prefix),
lowercase = exclude alpha (rgb/hsl/hsv prefix).

Each type handles its native model natively and delegates
cross-model formats via ToRgb()/ToHsl()/ToHsv().

Breaking changes from prior PR iteration:
- "h" dropped (identical to "x" without alpha)
- "C"/"c" removed, reserved for future complex format strings
- "P"/"p" replaced by "R%"/"r%"
- "R" now outputs rgba() not rgb() (CSS convention)
- "L" now outputs hsla() not hsl() (CSS convention)
- "V" now outputs hsva() not hsv() (CSS convention)

* refactor(media): clarify color CSS formatters

Color formatting now exposes the same IFormattable implementation in the BuildTask compile path as in normal builds. Keeping the interface conditional made the public contract harder to reason about even though the formatting members are System-only APIs.

Remove the BuildTask guard from the formatting members, combine alpha and no-alpha CSS helpers behind includeAlpha parameters, and rename the helpers so future changes preserve the CSS-compliant output contract.

* Remove regions

---------

Co-authored-by: Jumar Macato <16554748+jmacato@users.noreply.github.com>
Co-authored-by: Nathan Nguyen <HoangAnhVu.Nguyen-1@student.uts.edu.au>
Co-authored-by: Julien Lebosquain <julien@lebosquain.net>
pull/22024/head
Nathan Nguyen 1 month ago
committed by GitHub
parent
commit
79162b5da8
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 93
      src/Avalonia.Base/Media/Color.cs
  2. 97
      src/Avalonia.Base/Media/HslColor.cs
  3. 95
      src/Avalonia.Base/Media/HsvColor.cs
  4. 411
      tests/Avalonia.Base.UnitTests/Media/ColorTests.cs

93
src/Avalonia.Base/Media/Color.cs

@ -21,7 +21,7 @@ namespace Avalonia.Media
#if !BUILDTASK
public
#endif
readonly struct Color : IEquatable<Color>
readonly struct Color : IEquatable<Color>, IFormattable
{
private const double byteToDouble = 1.0 / 255;
@ -461,6 +461,97 @@ namespace Avalonia.Media
}
}
/// <summary>
/// Returns a formatted string representation of the color.
/// </summary>
/// <param name="format">
/// The format specifier. All color types support all specifiers via auto-conversion.
/// Uppercase includes alpha, lowercase excludes it. <c>%</c> suffix = percent mode:
/// <list type="bullet">
/// <item><c>null</c> or <c>""</c> — Default (known name or <c>#aarrggbb</c>)</item>
/// <item><c>"X"</c> — XAML hex with alpha: <c>#AARRGGBB</c></item>
/// <item><c>"x"</c> — Hex without alpha: <c>#RRGGBB</c></item>
/// <item><c>"H"</c> — HTML hex with alpha: <c>#RRGGBBAA</c></item>
/// <item><c>"R"</c> — CSS rgba absolute: <c>rgba(r, g, b, a)</c></item>
/// <item><c>"r"</c> — CSS rgb absolute: <c>rgb(r, g, b)</c></item>
/// <item><c>"R%"</c> — CSS rgba percent: <c>rgba(r%, g%, b%, a%)</c></item>
/// <item><c>"r%"</c> — CSS rgb percent: <c>rgb(r%, g%, b%)</c></item>
/// <item><c>"L"/"l"/"L%"/"l%"</c> — CSS hsl/hsla (auto-converts to HSL)</item>
/// <item><c>"V"/"v"/"V%"/"v%"</c> — CSS hsv/hsva (auto-converts to HSV)</item>
/// </list>
/// <para><c>"C"</c> and <c>"A"</c> are reserved for future complex format strings.</para>
/// </param>
/// <param name="formatProvider">Ignored. Color formatting is culture-invariant.</param>
/// <returns>The formatted string representation.</returns>
/// <exception cref="FormatException">Thrown when <paramref name="format"/> is not recognized.</exception>
public string ToString(string? format, IFormatProvider? formatProvider)
{
if (string.IsNullOrEmpty(format))
{
return ToString();
}
return format switch
{
"X" => $"#{A:X2}{R:X2}{G:X2}{B:X2}",
"x" => $"#{R:X2}{G:X2}{B:X2}",
"H" => $"#{R:X2}{G:X2}{B:X2}{A:X2}",
"R" => FormatRgbCss(),
"r" => FormatRgbCss(includeAlpha: false),
"R%" => FormatRgbPercentCss(),
"r%" => FormatRgbPercentCss(includeAlpha: false),
"L" or "l" or "L%" or "l%" => ToHsl().ToString(format, formatProvider),
"V" or "v" or "V%" or "v%" => ToHsv().ToString(format, formatProvider),
_ => throw new FormatException($"Format string '{format}' is not supported.")
};
}
/// <summary>
/// Formats the color as a CSS rgb() or rgba() string using absolute RGB components and fractional alpha.
/// </summary>
private string FormatRgbCss(bool includeAlpha = true)
{
if (includeAlpha)
{
return string.Format(
CultureInfo.InvariantCulture,
"rgba({0}, {1}, {2}, {3:F2})",
R,
G,
B,
A * byteToDouble);
}
return string.Format(CultureInfo.InvariantCulture, "rgb({0}, {1}, {2})", R, G, B);
}
/// <summary>
/// Formats the color as a CSS rgb() or rgba() string with all components as percentages.
/// </summary>
private string FormatRgbPercentCss(bool includeAlpha = true)
{
int rPct = (int)Math.Round(R * byteToDouble * 100.0);
int gPct = (int)Math.Round(G * byteToDouble * 100.0);
int bPct = (int)Math.Round(B * byteToDouble * 100.0);
if (includeAlpha)
{
int aPct = (int)Math.Round(A * byteToDouble * 100.0);
return string.Format(
CultureInfo.InvariantCulture,
"rgba({0}%, {1}%, {2}%, {3}%)",
rPct,
gPct,
bPct,
aPct);
}
return string.Format(CultureInfo.InvariantCulture, "rgb({0}%, {1}%, {2}%)", rPct, gPct, bPct);
}
/// <summary>
/// Returns the integer representation of the color.
/// </summary>

97
src/Avalonia.Base/Media/HslColor.cs

@ -17,7 +17,7 @@ namespace Avalonia.Media
#if !BUILDTASK
public
#endif
readonly struct HslColor : IEquatable<HslColor>
readonly struct HslColor : IEquatable<HslColor>, IFormattable
{
/// <summary>
/// Initializes a new instance of the <see cref="HslColor"/> struct.
@ -177,6 +177,99 @@ namespace Avalonia.Media
return HslColor.ToHsv(H, S, L, A);
}
/// <summary>
/// Returns a formatted string representation of the HSL color.
/// </summary>
/// <param name="format">
/// The format specifier. All color types support all specifiers via auto-conversion.
/// Uppercase includes alpha, lowercase excludes it. <c>%</c> suffix = percent mode.
/// <list type="bullet">
/// <item><c>null</c> or <c>""</c> — Default (full-precision <c>hsla(h, s, l, a)</c>)</item>
/// <item><c>"X"</c> — XAML hex: <c>#AARRGGBB</c></item>
/// <item><c>"x"</c> — Hex: <c>#RRGGBB</c></item>
/// <item><c>"H"</c> — HTML hex: <c>#RRGGBBAA</c></item>
/// <item><c>"R"/"r"/"R%"/"r%"</c> — CSS rgb/rgba (auto-converts to RGB)</item>
/// <item><c>"L"</c> — CSS hsla: <c>hsla(h, s%, l%, a)</c></item>
/// <item><c>"l"</c> — CSS hsl: <c>hsl(h, s%, l%)</c></item>
/// <item><c>"L%"</c> — All percent: <c>hsla(h%, s%, l%, a%)</c></item>
/// <item><c>"l%"</c> — All percent: <c>hsl(h%, s%, l%)</c></item>
/// <item><c>"V"/"v"/"V%"/"v%"</c> — CSS hsv/hsva (auto-converts to HSV)</item>
/// </list>
/// <para><c>"C"</c> and <c>"A"</c> are reserved for future complex format strings.</para>
/// </param>
/// <param name="formatProvider">Ignored. Color formatting is culture-invariant.</param>
/// <returns>The formatted string representation.</returns>
/// <exception cref="FormatException">Thrown when <paramref name="format"/> is not recognized.</exception>
public string ToString(string? format, IFormatProvider? formatProvider)
{
if (string.IsNullOrEmpty(format))
{
return ToString();
}
return format switch
{
"X" or "x" or "H" or "R" or "r" or "R%" or "r%" => ToRgb().ToString(format, formatProvider),
"L" => FormatHslCss(),
"l" => FormatHslCss(includeAlpha: false),
"L%" => FormatHslPercentCss(),
"l%" => FormatHslPercentCss(includeAlpha: false),
"V" or "v" or "V%" or "v%" => ToHsv().ToString(format, formatProvider),
_ => throw new FormatException($"Format string '{format}' is not supported.")
};
}
/// <summary>
/// Formats the color as a CSS hsl() or hsla() string using degrees, percent saturation/lightness,
/// and fractional alpha.
/// </summary>
private string FormatHslCss(bool includeAlpha = true)
{
int hDeg = (int)Math.Round(H);
int sPct = (int)Math.Round(S * 100.0);
int lPct = (int)Math.Round(L * 100.0);
if (includeAlpha)
{
return string.Format(
CultureInfo.InvariantCulture,
"hsla({0}, {1}%, {2}%, {3:F2})",
hDeg,
sPct,
lPct,
A);
}
return string.Format(CultureInfo.InvariantCulture, "hsl({0}, {1}%, {2}%)", hDeg, sPct, lPct);
}
/// <summary>
/// Formats the color as a CSS hsl() or hsla() string with all components as percentages.
/// </summary>
private string FormatHslPercentCss(bool includeAlpha = true)
{
int hPct = (int)Math.Round(H / 360.0 * 100.0);
int sPct = (int)Math.Round(S * 100.0);
int lPct = (int)Math.Round(L * 100.0);
if (includeAlpha)
{
int aPct = (int)Math.Round(A * 100.0);
return string.Format(
CultureInfo.InvariantCulture,
"hsla({0}%, {1}%, {2}%, {3}%)",
hPct,
sPct,
lPct,
aPct);
}
return string.Format(CultureInfo.InvariantCulture, "hsl({0}%, {1}%, {2}%)", hPct, sPct, lPct);
}
/// <inheritdoc/>
public override string ToString()
{
@ -193,7 +286,7 @@ namespace Avalonia.Media
// hsla(230, 1.0, 0.5, 1.0)
//
sb.Append("hsva(");
sb.Append("hsla(");
sb.Append(H.ToString(CultureInfo.InvariantCulture));
sb.Append(", ");
sb.Append(S.ToString(CultureInfo.InvariantCulture));

95
src/Avalonia.Base/Media/HsvColor.cs

@ -17,7 +17,7 @@ namespace Avalonia.Media
#if !BUILDTASK
public
#endif
readonly struct HsvColor : IEquatable<HsvColor>
readonly struct HsvColor : IEquatable<HsvColor>, IFormattable
{
/// <summary>
/// Initializes a new instance of the <see cref="HsvColor"/> struct.
@ -207,6 +207,99 @@ namespace Avalonia.Media
return HsvColor.ToHsl(H, S, V, A);
}
/// <summary>
/// Returns a formatted string representation of the HSV color.
/// </summary>
/// <param name="format">
/// The format specifier. All color types support all specifiers via auto-conversion.
/// Uppercase includes alpha, lowercase excludes it. <c>%</c> suffix = percent mode.
/// <list type="bullet">
/// <item><c>null</c> or <c>""</c> — Default (full-precision <c>hsva(h, s, v, a)</c>)</item>
/// <item><c>"X"</c> — XAML hex: <c>#AARRGGBB</c></item>
/// <item><c>"x"</c> — Hex: <c>#RRGGBB</c></item>
/// <item><c>"H"</c> — HTML hex: <c>#RRGGBBAA</c></item>
/// <item><c>"R"/"r"/"R%"/"r%"</c> — CSS rgb/rgba (auto-converts to RGB)</item>
/// <item><c>"L"/"l"/"L%"/"l%"</c> — CSS hsl/hsla (auto-converts to HSL)</item>
/// <item><c>"V"</c> — CSS hsva: <c>hsva(h, s%, v%, a)</c></item>
/// <item><c>"v"</c> — CSS hsv: <c>hsv(h, s%, v%)</c></item>
/// <item><c>"V%"</c> — All percent: <c>hsva(h%, s%, v%, a%)</c></item>
/// <item><c>"v%"</c> — All percent: <c>hsv(h%, s%, v%)</c></item>
/// </list>
/// <para><c>"C"</c> and <c>"A"</c> are reserved for future complex format strings.</para>
/// </param>
/// <param name="formatProvider">Ignored. Color formatting is culture-invariant.</param>
/// <returns>The formatted string representation.</returns>
/// <exception cref="FormatException">Thrown when <paramref name="format"/> is not recognized.</exception>
public string ToString(string? format, IFormatProvider? formatProvider)
{
if (string.IsNullOrEmpty(format))
{
return ToString();
}
return format switch
{
"X" or "x" or "H" or "R" or "r" or "R%" or "r%" => ToRgb().ToString(format, formatProvider),
"L" or "l" or "L%" or "l%" => ToHsl().ToString(format, formatProvider),
"V" => FormatHsvCss(),
"v" => FormatHsvCss(includeAlpha: false),
"V%" => FormatHsvPercentCss(),
"v%" => FormatHsvPercentCss(includeAlpha: false),
_ => throw new FormatException($"Format string '{format}' is not supported.")
};
}
/// <summary>
/// Formats the color as a CSS hsv() or hsva() string using degrees, percent saturation/value,
/// and fractional alpha.
/// </summary>
private string FormatHsvCss(bool includeAlpha = true)
{
int hDeg = (int)Math.Round(H);
int sPct = (int)Math.Round(S * 100.0);
int vPct = (int)Math.Round(V * 100.0);
if (includeAlpha)
{
return string.Format(
CultureInfo.InvariantCulture,
"hsva({0}, {1}%, {2}%, {3:F2})",
hDeg,
sPct,
vPct,
A);
}
return string.Format(CultureInfo.InvariantCulture, "hsv({0}, {1}%, {2}%)", hDeg, sPct, vPct);
}
/// <summary>
/// Formats the color as a CSS hsv() or hsva() string with all components as percentages.
/// </summary>
private string FormatHsvPercentCss(bool includeAlpha = true)
{
int hPct = (int)Math.Round(H / 360.0 * 100.0);
int sPct = (int)Math.Round(S * 100.0);
int vPct = (int)Math.Round(V * 100.0);
if (includeAlpha)
{
int aPct = (int)Math.Round(A * 100.0);
return string.Format(
CultureInfo.InvariantCulture,
"hsva({0}%, {1}%, {2}%, {3}%)",
hPct,
sPct,
vPct,
aPct);
}
return string.Format(CultureInfo.InvariantCulture, "hsv({0}%, {1}%, {2}%)", hPct, sPct, vPct);
}
/// <inheritdoc/>
public override string ToString()
{

411
tests/Avalonia.Base.UnitTests/Media/ColorTests.cs

@ -1,4 +1,5 @@
using System;
using System.Globalization;
using Avalonia.Media;
using Xunit;
@ -364,5 +365,415 @@ namespace Avalonia.Base.UnitTests.Media
Assert.Equal(convertedHsl, dataPoint.Item2);
}
}
// =====================================================================
// IFormattable unified format specifier tests
//
// All three color types (Color, HslColor, HsvColor) support ALL
// format specifiers. Cross-model formats auto-convert.
//
// Convention:
// Uppercase = include alpha, a-suffixed prefix (rgba, hsla, hsva)
// Lowercase = exclude alpha, plain prefix (rgb, hsl, hsv)
// "%" suffix = percent mode
// =====================================================================
[Fact]
public void Color_ToString_Default_Returns_KnownName()
{
var red = new Color(0xFF, 0xFF, 0x00, 0x00);
Assert.Equal("Red", red.ToString());
}
[Fact]
public void Color_ToString_Default_Returns_Hex_For_Unknown()
{
var color = new Color(0x40, 0xFF, 0x88, 0x44);
Assert.Equal("#40ff8844", color.ToString());
}
[Fact]
public void Color_ToString_Null_Format_Matches_Default()
{
var color = new Color(0x40, 0xFF, 0x88, 0x44);
Assert.Equal(color.ToString(), color.ToString(null, null));
}
[Fact]
public void Color_ToString_Empty_Format_Matches_Default()
{
var color = new Color(0x40, 0xFF, 0x88, 0x44);
Assert.Equal(color.ToString(), color.ToString("", null));
}
[Fact]
public void HslColor_ToString_Null_Format_Matches_Default()
{
var color = new HslColor(0.8, 200, 0.6, 0.4);
Assert.Equal(color.ToString(), color.ToString(null, null));
}
[Fact]
public void HsvColor_ToString_Null_Format_Matches_Default()
{
var color = new HsvColor(0.8, 200, 0.6, 0.4);
Assert.Equal(color.ToString(), color.ToString(null, null));
}
[Theory]
[InlineData(0xFF, 0xFF, 0x88, 0x44, "#FFFF8844")]
[InlineData(0x40, 0xFF, 0x88, 0x44, "#40FF8844")]
[InlineData(0xFF, 0x00, 0x00, 0x00, "#FF000000")]
[InlineData(0x00, 0x00, 0x00, 0x00, "#00000000")]
public void Color_ToString_X_Returns_Xaml_Hex_With_Alpha(byte a, byte r, byte g, byte b, string expected)
{
var color = new Color(a, r, g, b);
Assert.Equal(expected, color.ToString("X", CultureInfo.InvariantCulture));
}
[Theory]
[InlineData(0xFF, 0xFF, 0x88, 0x44, "#FF8844")]
[InlineData(0x40, 0xFF, 0x88, 0x44, "#FF8844")]
[InlineData(0x00, 0x00, 0x00, 0x00, "#000000")]
public void Color_ToString_x_Returns_Hex_Without_Alpha(byte a, byte r, byte g, byte b, string expected)
{
var color = new Color(a, r, g, b);
Assert.Equal(expected, color.ToString("x", CultureInfo.InvariantCulture));
}
[Theory]
[InlineData(0xFF, 0xFF, 0x88, 0x44, "#FF8844FF")]
[InlineData(0x40, 0xFF, 0x88, 0x44, "#FF884440")]
[InlineData(0x00, 0x00, 0x00, 0x00, "#00000000")]
public void Color_ToString_H_Returns_Html_Hex_With_Alpha(byte a, byte r, byte g, byte b, string expected)
{
var color = new Color(a, r, g, b);
Assert.Equal(expected, color.ToString("H", CultureInfo.InvariantCulture));
}
[Fact]
public void HslColor_ToString_X_Converts_To_Rgb_Hex()
{
// Pure red: HSL(0, 1, 0.5) = RGB(255, 0, 0)
var hsl = new HslColor(1.0, 0, 1.0, 0.5);
Assert.Equal("#FFFF0000", hsl.ToString("X", CultureInfo.InvariantCulture));
}
[Fact]
public void HslColor_ToString_x_Converts_To_Rgb_Hex()
{
var hsl = new HslColor(1.0, 0, 1.0, 0.5);
Assert.Equal("#FF0000", hsl.ToString("x", CultureInfo.InvariantCulture));
}
[Fact]
public void HsvColor_ToString_X_Converts_To_Rgb_Hex()
{
// Pure red: HSV(0, 1, 1) = RGB(255, 0, 0)
var hsv = new HsvColor(1.0, 0, 1.0, 1.0);
Assert.Equal("#FFFF0000", hsv.ToString("X", CultureInfo.InvariantCulture));
}
[Fact]
public void HsvColor_ToString_x_Converts_To_Rgb_Hex()
{
var hsv = new HsvColor(1.0, 0, 1.0, 1.0);
Assert.Equal("#FF0000", hsv.ToString("x", CultureInfo.InvariantCulture));
}
[Theory]
[InlineData(0xFF, 0xFF, 0x88, 0x44, "rgba(255, 136, 68, 1.00)")]
[InlineData(0x80, 0xFF, 0x88, 0x44, "rgba(255, 136, 68, 0.50)")]
[InlineData(0x00, 0x00, 0x00, 0x00, "rgba(0, 0, 0, 0.00)")]
public void Color_ToString_R_Returns_Rgba_With_Alpha(byte a, byte r, byte g, byte b, string expected)
{
var color = new Color(a, r, g, b);
Assert.Equal(expected, color.ToString("R", CultureInfo.InvariantCulture));
}
[Theory]
[InlineData(0xFF, 0xFF, 0x88, 0x44, "rgb(255, 136, 68)")]
[InlineData(0x80, 0xFF, 0x88, 0x44, "rgb(255, 136, 68)")]
[InlineData(0xFF, 0x00, 0x00, 0x00, "rgb(0, 0, 0)")]
public void Color_ToString_r_Returns_Rgb_Without_Alpha(byte a, byte r, byte g, byte b, string expected)
{
var color = new Color(a, r, g, b);
Assert.Equal(expected, color.ToString("r", CultureInfo.InvariantCulture));
}
[Fact]
public void HslColor_ToString_R_Converts_To_Rgba()
{
// Pure blue: HSL(240, 1, 0.5) = RGB(0, 0, 255)
var hsl = new HslColor(1.0, 240, 1.0, 0.5);
Assert.Equal("rgba(0, 0, 255, 1.00)", hsl.ToString("R", CultureInfo.InvariantCulture));
}
[Fact]
public void HsvColor_ToString_r_Converts_To_Rgb()
{
// Pure red: HSV(0, 1, 1) = RGB(255, 0, 0)
var hsv = new HsvColor(1.0, 0, 1.0, 1.0);
Assert.Equal("rgb(255, 0, 0)", hsv.ToString("r", CultureInfo.InvariantCulture));
}
[Theory]
[InlineData(0xFF, 0xFF, 0x80, 0x00, "rgba(100%, 50%, 0%, 100%)")]
[InlineData(0x80, 0xFF, 0x80, 0x00, "rgba(100%, 50%, 0%, 50%)")]
[InlineData(0x00, 0x00, 0x00, 0x00, "rgba(0%, 0%, 0%, 0%)")]
public void Color_ToString_RPct_Returns_Rgba_Percent(byte a, byte r, byte g, byte b, string expected)
{
var color = new Color(a, r, g, b);
Assert.Equal(expected, color.ToString("R%", CultureInfo.InvariantCulture));
}
[Theory]
[InlineData(0xFF, 0xFF, 0x80, 0x00, "rgb(100%, 50%, 0%)")]
[InlineData(0x80, 0xFF, 0x80, 0x00, "rgb(100%, 50%, 0%)")]
[InlineData(0xFF, 0x00, 0x00, 0x00, "rgb(0%, 0%, 0%)")]
public void Color_ToString_rPct_Returns_Rgb_Percent(byte a, byte r, byte g, byte b, string expected)
{
var color = new Color(a, r, g, b);
Assert.Equal(expected, color.ToString("r%", CultureInfo.InvariantCulture));
}
[Fact]
public void HslColor_ToString_RPct_Converts_To_Rgba_Percent()
{
// Pure red: HSL(0, 1, 0.5) = RGB(255, 0, 0)
var hsl = new HslColor(1.0, 0, 1.0, 0.5);
Assert.Equal("rgba(100%, 0%, 0%, 100%)", hsl.ToString("R%", CultureInfo.InvariantCulture));
}
[Theory]
[InlineData(1.0, 180, 0.5, 0.5, "hsla(180, 50%, 50%, 1.00)")]
[InlineData(0.5, 240, 0.8, 0.2, "hsla(240, 80%, 20%, 0.50)")]
[InlineData(0.0, 0, 0.0, 0.0, "hsla(0, 0%, 0%, 0.00)")]
public void HslColor_ToString_L_Returns_Hsla_With_Alpha(double a, double h, double s, double l, string expected)
{
var color = new HslColor(a, h, s, l);
Assert.Equal(expected, color.ToString("L", CultureInfo.InvariantCulture));
}
[Theory]
[InlineData(1.0, 180, 0.5, 0.5, "hsl(180, 50%, 50%)")]
[InlineData(0.5, 240, 0.8, 0.2, "hsl(240, 80%, 20%)")]
[InlineData(0.0, 0, 0.0, 0.0, "hsl(0, 0%, 0%)")]
public void HslColor_ToString_l_Returns_Hsl_Without_Alpha(double a, double h, double s, double l, string expected)
{
var color = new HslColor(a, h, s, l);
Assert.Equal(expected, color.ToString("l", CultureInfo.InvariantCulture));
}
[Fact]
public void Color_ToString_L_Converts_To_Hsla()
{
// Pure red: RGB(255, 0, 0) = HSL(0, 100%, 50%)
var color = new Color(0xFF, 0xFF, 0x00, 0x00);
Assert.Equal("hsla(0, 100%, 50%, 1.00)", color.ToString("L", CultureInfo.InvariantCulture));
}
[Fact]
public void Color_ToString_l_Converts_To_Hsl()
{
var color = new Color(0xFF, 0xFF, 0x00, 0x00);
Assert.Equal("hsl(0, 100%, 50%)", color.ToString("l", CultureInfo.InvariantCulture));
}
[Fact]
public void HsvColor_ToString_L_Converts_To_Hsla()
{
// Pure red: HSV(0, 1, 1) = HSL(0, 100%, 50%)
var hsv = new HsvColor(1.0, 0, 1.0, 1.0);
Assert.Equal("hsla(0, 100%, 50%, 1.00)", hsv.ToString("L", CultureInfo.InvariantCulture));
}
[Theory]
[InlineData(1.0, 180, 0.5, 0.5, "hsla(50%, 50%, 50%, 100%)")]
[InlineData(0.5, 90, 1.0, 1.0, "hsla(25%, 100%, 100%, 50%)")]
[InlineData(1.0, 0, 0.0, 0.0, "hsla(0%, 0%, 0%, 100%)")]
public void HslColor_ToString_LPct_Returns_Hsla_All_Percent(double a, double h, double s, double l, string expected)
{
var color = new HslColor(a, h, s, l);
Assert.Equal(expected, color.ToString("L%", CultureInfo.InvariantCulture));
}
[Theory]
[InlineData(1.0, 180, 0.5, 0.5, "hsl(50%, 50%, 50%)")]
[InlineData(0.5, 90, 1.0, 1.0, "hsl(25%, 100%, 100%)")]
[InlineData(1.0, 0, 0.0, 0.0, "hsl(0%, 0%, 0%)")]
public void HslColor_ToString_lPct_Returns_Hsl_All_Percent(double a, double h, double s, double l, string expected)
{
var color = new HslColor(a, h, s, l);
Assert.Equal(expected, color.ToString("l%", CultureInfo.InvariantCulture));
}
[Theory]
[InlineData(1.0, 180, 0.5, 0.5, "hsva(180, 50%, 50%, 1.00)")]
[InlineData(0.5, 240, 0.8, 0.2, "hsva(240, 80%, 20%, 0.50)")]
[InlineData(0.0, 0, 0.0, 0.0, "hsva(0, 0%, 0%, 0.00)")]
public void HsvColor_ToString_V_Returns_Hsva_With_Alpha(double a, double h, double s, double v, string expected)
{
var color = new HsvColor(a, h, s, v);
Assert.Equal(expected, color.ToString("V", CultureInfo.InvariantCulture));
}
[Theory]
[InlineData(1.0, 180, 0.5, 0.5, "hsv(180, 50%, 50%)")]
[InlineData(0.5, 240, 0.8, 0.2, "hsv(240, 80%, 20%)")]
[InlineData(0.0, 0, 0.0, 0.0, "hsv(0, 0%, 0%)")]
public void HsvColor_ToString_v_Returns_Hsv_Without_Alpha(double a, double h, double s, double v, string expected)
{
var color = new HsvColor(a, h, s, v);
Assert.Equal(expected, color.ToString("v", CultureInfo.InvariantCulture));
}
[Fact]
public void Color_ToString_V_Converts_To_Hsva()
{
// Pure red: RGB(255, 0, 0) = HSV(0, 100%, 100%)
var color = new Color(0xFF, 0xFF, 0x00, 0x00);
Assert.Equal("hsva(0, 100%, 100%, 1.00)", color.ToString("V", CultureInfo.InvariantCulture));
}
[Fact]
public void HslColor_ToString_V_Converts_To_Hsva()
{
// Pure red: HSL(0, 1, 0.5) = HSV(0, 100%, 100%)
var hsl = new HslColor(1.0, 0, 1.0, 0.5);
Assert.Equal("hsva(0, 100%, 100%, 1.00)", hsl.ToString("V", CultureInfo.InvariantCulture));
}
[Theory]
[InlineData(1.0, 180, 0.5, 0.5, "hsva(50%, 50%, 50%, 100%)")]
[InlineData(0.5, 90, 1.0, 1.0, "hsva(25%, 100%, 100%, 50%)")]
[InlineData(1.0, 0, 0.0, 0.0, "hsva(0%, 0%, 0%, 100%)")]
public void HsvColor_ToString_VPct_Returns_Hsva_All_Percent(double a, double h, double s, double v, string expected)
{
var color = new HsvColor(a, h, s, v);
Assert.Equal(expected, color.ToString("V%", CultureInfo.InvariantCulture));
}
[Theory]
[InlineData(1.0, 180, 0.5, 0.5, "hsv(50%, 50%, 50%)")]
[InlineData(0.5, 90, 1.0, 1.0, "hsv(25%, 100%, 100%)")]
[InlineData(1.0, 0, 0.0, 0.0, "hsv(0%, 0%, 0%)")]
public void HsvColor_ToString_vPct_Returns_Hsv_All_Percent(double a, double h, double s, double v, string expected)
{
var color = new HsvColor(a, h, s, v);
Assert.Equal(expected, color.ToString("v%", CultureInfo.InvariantCulture));
}
[Fact]
public void Color_ToString_Invalid_Format_Throws()
{
var color = new Color(0xFF, 0xFF, 0x00, 0x00);
Assert.Throws<FormatException>(() => color.ToString("Z", null));
}
[Fact]
public void HslColor_ToString_Invalid_Format_Throws()
{
var color = new HslColor(1.0, 0, 0, 0);
Assert.Throws<FormatException>(() => color.ToString("Z", null));
}
[Fact]
public void HsvColor_ToString_Invalid_Format_Throws()
{
var color = new HsvColor(1.0, 0, 0, 0);
Assert.Throws<FormatException>(() => color.ToString("Z", null));
}
[Theory]
[InlineData("C")]
[InlineData("c")]
[InlineData("A")]
[InlineData("a")]
[InlineData("P")]
[InlineData("h")]
public void Color_ToString_Reserved_And_Removed_Specifiers_Throw(string format)
{
var color = new Color(0xFF, 0xFF, 0x00, 0x00);
Assert.Throws<FormatException>(() => color.ToString(format, null));
}
[Theory]
[InlineData("C")]
[InlineData("c")]
public void HslColor_ToString_Reserved_C_Throws(string format)
{
var color = new HslColor(1.0, 0, 1.0, 0.5);
Assert.Throws<FormatException>(() => color.ToString(format, null));
}
[Theory]
[InlineData("C")]
[InlineData("c")]
public void HsvColor_ToString_Reserved_C_Throws(string format)
{
var color = new HsvColor(1.0, 0, 1.0, 1.0);
Assert.Throws<FormatException>(() => color.ToString(format, null));
}
[Fact]
public void Color_ToString_IFormatProvider_Is_Ignored()
{
var color = new Color(0x80, 0xFF, 0x88, 0x44);
var french = CultureInfo.GetCultureInfo("fr-FR");
Assert.Equal(
color.ToString("R", CultureInfo.InvariantCulture),
color.ToString("R", french));
}
[Fact]
public void HslColor_ToString_IFormatProvider_Is_Ignored()
{
var color = new HslColor(0.5, 180, 0.5, 0.5);
var french = CultureInfo.GetCultureInfo("fr-FR");
Assert.Equal(
color.ToString("L", CultureInfo.InvariantCulture),
color.ToString("L", french));
}
}
}

Loading…
Cancel
Save