diff --git a/ThemeEditor.sln b/ThemeEditor.sln
index ade1b51..2ae0a68 100644
--- a/ThemeEditor.sln
+++ b/ThemeEditor.sln
@@ -41,8 +41,15 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "props", "props", "{A88815F2
build\Newtonsoft.Json.props = build\Newtonsoft.Json.props
build\ReactiveUI.props = build\ReactiveUI.props
build\Rx.props = build\Rx.props
+ build\XUnit.props = build\XUnit.props
EndProjectSection
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{F2D7C825-1AC1-45FF-A221-CEE80E2F2374}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ThemeEditor.Colors", "src\ThemeEditor.Colors\ThemeEditor.Colors.csproj", "{BFDD803E-2B9B-46A2-AC25-C77FD2376893}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ThemeEditor.Colors.UnitTests", "tests\ThemeEditor.Colors.UnitTests\ThemeEditor.Colors.UnitTests.csproj", "{E2079787-7F2F-46F8-BD56-2C226D883904}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -57,6 +64,14 @@ Global
{2B5611EB-1E0D-4EAB-96C9-821A965E496B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2B5611EB-1E0D-4EAB-96C9-821A965E496B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2B5611EB-1E0D-4EAB-96C9-821A965E496B}.Release|Any CPU.Build.0 = Release|Any CPU
+ {BFDD803E-2B9B-46A2-AC25-C77FD2376893}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {BFDD803E-2B9B-46A2-AC25-C77FD2376893}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {BFDD803E-2B9B-46A2-AC25-C77FD2376893}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {BFDD803E-2B9B-46A2-AC25-C77FD2376893}.Release|Any CPU.Build.0 = Release|Any CPU
+ {E2079787-7F2F-46F8-BD56-2C226D883904}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {E2079787-7F2F-46F8-BD56-2C226D883904}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E2079787-7F2F-46F8-BD56-2C226D883904}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {E2079787-7F2F-46F8-BD56-2C226D883904}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -68,6 +83,8 @@ Global
{497FBEFD-7228-4E2F-A947-9F416498E263} = {3353FD4A-FE9B-4670-86C9-09EF0F7CEDB2}
{9A80222C-EDE8-42AF-9D6D-76325FC4673F} = {3353FD4A-FE9B-4670-86C9-09EF0F7CEDB2}
{A88815F2-21C3-4EBA-8DBC-0A461EA7F82C} = {3353FD4A-FE9B-4670-86C9-09EF0F7CEDB2}
+ {BFDD803E-2B9B-46A2-AC25-C77FD2376893} = {219954C4-6A40-4D94-914E-148B7EBB718E}
+ {E2079787-7F2F-46F8-BD56-2C226D883904} = {F2D7C825-1AC1-45FF-A221-CEE80E2F2374}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {CF676549-9409-4598-8229-80E04B51ADC5}
diff --git a/src/ThemeEditor.Colors/HSV.cs b/src/ThemeEditor.Colors/HSV.cs
new file mode 100644
index 0000000..5d31e90
--- /dev/null
+++ b/src/ThemeEditor.Colors/HSV.cs
@@ -0,0 +1,98 @@
+using System;
+
+namespace ThemeEditor.Colors
+{
+ public readonly struct HSV
+ {
+ public double H { get; }
+ public double S { get; }
+ public double V { get; }
+
+ public HSV(double h, double s, double v)
+ {
+ H = h;
+ S = s;
+ V = v;
+ }
+
+ public HSV(HSV hsv)
+ {
+ H = hsv.H;
+ S = hsv.S;
+ V = hsv.V;
+ }
+
+ public HSV(RGB rgb)
+ {
+ HSV hsv = rgb.ToHSV();
+ H = hsv.H;
+ S = hsv.S;
+ V = hsv.V;
+ }
+
+ public RGB ToRGB() => ToRGB(H, S, V);
+
+ public static RGB ToRGB(double h, double s, double v)
+ {
+ double R = default;
+ double G = default;
+ double B = default;
+
+ if (s == 0)
+ {
+ R = G = B = Math.Round(v * 2.55);
+ return new RGB(R, G, B);
+ }
+
+ s = s / 100;
+ v = v / 100;
+ h /= 60;
+
+ var i = Math.Floor(h);
+ var f = h - i;
+ var p = v * (1 - s);
+ var q = v * (1 - s * f);
+ var t = v * (1 - s * (1 - f));
+
+ switch ((int)i)
+ {
+ case 0:
+ R = v;
+ G = t;
+ B = p;
+ break;
+ case 1:
+ R = q;
+ G = v;
+ B = p;
+ break;
+ case 2:
+ R = p;
+ G = v;
+ B = t;
+ break;
+ case 3:
+ R = p;
+ G = q;
+ B = v;
+ break;
+ case 4:
+ R = t;
+ G = p;
+ B = v;
+ break;
+ default:
+ R = v;
+ G = p;
+ B = q;
+ break;
+ }
+
+ R = Math.Round(R * 255);
+ G = Math.Round(G * 255);
+ B = Math.Round(B * 255);
+
+ return new RGB(R, G, B);
+ }
+ }
+}
diff --git a/src/ThemeEditor.Colors/RGB.cs b/src/ThemeEditor.Colors/RGB.cs
new file mode 100644
index 0000000..3c26d20
--- /dev/null
+++ b/src/ThemeEditor.Colors/RGB.cs
@@ -0,0 +1,109 @@
+using System;
+
+namespace ThemeEditor.Colors
+{
+ public readonly struct RGB
+ {
+ public double R { get; }
+ public double G { get; }
+ public double B { get; }
+
+ public RGB(double r, double g, double b)
+ {
+ R = r;
+ G = g;
+ B = b;
+ }
+
+ public RGB(RGB rgb)
+ {
+ R = rgb.R;
+ G = rgb.G;
+ B = rgb.B;
+ }
+
+ public RGB(HSV hsv)
+ {
+ RGB rgb = hsv.ToRGB();
+ R = rgb.R;
+ G = rgb.G;
+ B = rgb.B;
+ }
+
+ public HSV ToHSV() => ToHSV(R, G, B);
+
+ public static HSV ToHSV(double r, double g, double b)
+ {
+ double H = default;
+ double S = default;
+ double V = default;
+
+ var m = r;
+
+ if (g < m)
+ {
+ m = g;
+ }
+
+ if (b < m)
+ {
+ m = b;
+ }
+
+ var v = r;
+
+ if (g > v)
+ {
+ v = g;
+ }
+
+ if (b > v)
+ {
+ v = b;
+ }
+
+ var value = 100 * v / 255;
+ var delta = v - m;
+
+ if (v == 0.0)
+ {
+ S = 0;
+ }
+ else
+ {
+ S = 100 * delta / v;
+ }
+
+ if (S == 0)
+ {
+ H = 0;
+ }
+ else
+ {
+ if (r == v)
+ {
+ H = 60.0 * (g - b) / delta;
+ }
+ else if (g == v)
+ {
+ H = 120.0 + 60.0 * (b - r) / delta;
+ }
+ else if (b == v)
+ {
+ H = 240.0 + 60.0 * (r - g) / delta;
+ }
+
+ if (H < 0.0)
+ {
+ H = H + 360.0;
+ }
+ }
+
+ H = Math.Round(H);
+ S = Math.Round(S);
+ V = Math.Round(value);
+
+ return new HSV(H, S, V);
+ }
+ }
+}
diff --git a/src/ThemeEditor.Colors/ThemeEditor.Colors.csproj b/src/ThemeEditor.Colors/ThemeEditor.Colors.csproj
new file mode 100644
index 0000000..2b71fbd
--- /dev/null
+++ b/src/ThemeEditor.Colors/ThemeEditor.Colors.csproj
@@ -0,0 +1,16 @@
+
+
+
+ netstandard2.0
+ Library
+ False
+ latest
+
+
+
+
+
+
+
+
+
diff --git a/src/ThemeEditor/Controls/ColorPicker.xaml b/src/ThemeEditor/Controls/ColorPicker.xaml
index af5ee57..ed36bc8 100644
--- a/src/ThemeEditor/Controls/ColorPicker.xaml
+++ b/src/ThemeEditor/Controls/ColorPicker.xaml
@@ -1,12 +1,12 @@
-
+