mirror of
https://github.com/Artemis-RGB/Artemis
synced 2026-11-09 18:36:27 +00:00
Core - Fix color gradient and utility bugs found by the tests (#914)
* Profile adaption - Fix arrow keys section including Page Down - The arrow keys keyboard section now starts at Arrow Up instead of the key before it (Page Down) - Updated the keyboard section test to expect only the four arrow keys * Color quantizer - Let fully saturated colors be vibrant and grays be muted - The saturation limits of the color types are now inclusive, so 100% saturated colors count as vibrant and 0% saturated colors count as muted - The lightness limits stay exclusive so pure black and white are still left out - Updated the quantizer tests to match * Color gradients - Fix stop placement, seamless toggling and list events - Stops on an occupied position now move down 0.001 instead of 0.002 (rounding error in the distance check) - Flip, rotate, spread and interpolate move all stops together, so a stop can take a position another stop is leaving - Spreading or randomizing a single stop puts it at 0 instead of NaN - ToggleSeamless does nothing on an empty gradient and caps stretched stops at 1 instead of 100 - RemoveAt reports the removed stop and its index - Insert links the stop like Add does and raises the add before the move - Remove, RemoveAt, Clear and replacing a stop through the indexer unlink and stop listening to the actual removed stop - Copying from a list of stops sorts them by position - Obsolete GetPositionsArray no longer skips the seamless positions, obsolete GetColor picks the right colors after a repeat - Updated the gradient tests to match * Core - Fix small color, enum, string and type utility bugs - Darken keeps the alpha of the color - GetAllValuesAndDescriptions(Type) uses the [Description] attribute like the generic version - UrlFriendly cuts titles at 80 characters instead of 81 - RemapInternationalCharToAscii knows both cases of r, l, d, thorn, sharp s, h and j with accents - IsGenericType returns false instead of throwing when the base type is not generic - IsCastableFrom also finds conversion operators declared on the target type - GetDisplayName keeps the brackets of array types when humanizing - Updated the utility tests to match
This commit is contained in:
1 parent
6df81b19b1
commit
63186ee06d
15 files changed
+274
-201
No files matched your search
@@ -121,15 +121,17 @@ public class ColorQuantizerTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void FindColorVariation_FullySaturatedColorsAreNeverVibrant()
|
public void FindColorVariation_FullySaturatedColorsAreVibrantAndGraysAreMuted()
|
||||||
{
|
{
|
||||||
// Pinned as-is: a color only counts as vibrant when its saturation is below the maximum of 100%, so pure colors like
|
ColorQuantizer.FindColorVariation([SKColors.Red], ColorType.Vibrant).ShouldBe(SKColors.Red);
|
||||||
// red are skipped. The same goes for grays, a color only counts as muted when it has more than 0% saturation
|
ColorQuantizer.FindColorVariation([SKColors.Gray], ColorType.Muted).ShouldBe(SKColors.Gray);
|
||||||
ColorQuantizer.FindColorVariation([SKColors.Red], ColorType.Vibrant).ShouldBe(SKColor.Empty);
|
}
|
||||||
ColorQuantizer.FindColorVariation([SKColors.Gray], ColorType.Muted).ShouldBe(SKColor.Empty);
|
|
||||||
|
|
||||||
ColorQuantizer.FindColorVariation([SKColors.Red], ColorType.Vibrant, true).ShouldBe(SKColors.Red);
|
[Fact]
|
||||||
ColorQuantizer.FindColorVariation([SKColors.Gray], ColorType.Muted, true).ShouldBe(SKColors.Gray);
|
public void FindColorVariation_PureBlackAndWhiteAreNeverMuted()
|
||||||
|
{
|
||||||
|
ColorQuantizer.FindColorVariation([SKColors.Black], ColorType.DarkMuted).ShouldBe(SKColor.Empty);
|
||||||
|
ColorQuantizer.FindColorVariation([SKColors.White], ColorType.LightMuted).ShouldBe(SKColor.Empty);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
@@ -159,9 +159,7 @@ public class ColorGradientTests
|
|||||||
|
|
||||||
gradient.Add(stop);
|
gradient.Add(stop);
|
||||||
|
|
||||||
// Pinned as-is: stops step down 0.001 at a time until they are at least 0.001 away from other stops, but 0.5 - 0.499
|
stop.Position.ShouldBe(0.499f, 0.0001f);
|
||||||
// comes out a hair under 0.001 in floating point, so the stop takes a second step and ends up 0.002 lower
|
|
||||||
stop.Position.ShouldBe(0.498f, 0.0001f);
|
|
||||||
gradient.IndexOf(stop).ShouldBe(1);
|
gradient.IndexOf(stop).ShouldBe(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,7 +182,7 @@ public class ColorGradientTests
|
|||||||
|
|
||||||
gradient.Add(stop);
|
gradient.Add(stop);
|
||||||
|
|
||||||
stop.Position.ShouldBe(0.496f, 0.0001f);
|
stop.Position.ShouldBe(0.498f, 0.0001f);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -199,33 +197,32 @@ public class ColorGradientTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Insert_RaisesTheMoveBeforeTheAdd()
|
public void Insert_RaisesAnAddAndThenAMoveIntoPlace()
|
||||||
{
|
{
|
||||||
ColorGradient gradient = [new ColorGradientStop(SKColors.Red, 0), new ColorGradientStop(SKColors.Blue, 1)];
|
ColorGradient gradient = [new ColorGradientStop(SKColors.Red, 0), new ColorGradientStop(SKColors.Blue, 1)];
|
||||||
List<NotifyCollectionChangedEventArgs> events = RecordCollectionChanges(gradient);
|
List<NotifyCollectionChangedEventArgs> events = RecordCollectionChanges(gradient);
|
||||||
|
ColorGradientStop middle = new(SKColors.Lime, 0.5f);
|
||||||
|
|
||||||
gradient.Insert(0, new ColorGradientStop(SKColors.Lime, 0.5f));
|
gradient.Insert(0, middle);
|
||||||
|
|
||||||
// Pinned as-is: Insert reorders before reporting the add, so listeners hear about a move of a stop they
|
events.Select(e => e.Action).ShouldBe([NotifyCollectionChangedAction.Add, NotifyCollectionChangedAction.Move]);
|
||||||
// were never told about, and the add then reports the index the stop ended up at. Add does it the other way around
|
events[0].NewStartingIndex.ShouldBe(0);
|
||||||
events.Select(e => e.Action).ShouldBe([NotifyCollectionChangedAction.Move, NotifyCollectionChangedAction.Add]);
|
events[0].NewItems![0].ShouldBeSameAs(middle);
|
||||||
events[1].NewStartingIndex.ShouldBe(1);
|
gradient.IndexOf(middle).ShouldBe(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Insert_DoesNotMoveAStopOffAnOccupiedPosition()
|
public void Insert_MovesAStopOnAnOccupiedPositionSlightlyDown()
|
||||||
{
|
{
|
||||||
ColorGradient gradient = [new ColorGradientStop(SKColors.Red, 0), new ColorGradientStop(SKColors.Blue, 0.5f)];
|
ColorGradient gradient = [new ColorGradientStop(SKColors.Red, 0), new ColorGradientStop(SKColors.Blue, 0.5f)];
|
||||||
ColorGradientStop stop = new(SKColors.Lime, 0.5f);
|
ColorGradientStop stop = new(SKColors.Lime, 0.5f);
|
||||||
|
|
||||||
gradient.Insert(0, stop);
|
gradient.Insert(0, stop);
|
||||||
|
|
||||||
// Pinned as-is: unlike Add, Insert doesn't link the stop to the gradient, so two stops can share a position.
|
// Like Add, the stop also keeps avoiding other stops when it is moved later
|
||||||
// Moving the stop later doesn't avoid other stops either
|
stop.Position.ShouldBe(0.499f, 0.0001f);
|
||||||
stop.Position.ShouldBe(0.5f);
|
|
||||||
stop.Position = 0f;
|
stop.Position = 0f;
|
||||||
stop.Position.ShouldBe(0f);
|
stop.Position.ShouldBe(0.001f, 0.0001f);
|
||||||
gradient.Count(s => s.Position == 0f).ShouldBe(2);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -270,6 +267,21 @@ public class ColorGradientTests
|
|||||||
changes.ShouldBe(1);
|
changes.ShouldBe(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Indexer_LinksTheNewStopAndUnlinksTheReplacedStop()
|
||||||
|
{
|
||||||
|
ColorGradientStop red = new(SKColors.Red, 0);
|
||||||
|
ColorGradient gradient = [red, new ColorGradientStop(SKColors.Blue, 1)];
|
||||||
|
ColorGradientStop lime = new(SKColors.Lime, 1);
|
||||||
|
|
||||||
|
gradient[0] = lime;
|
||||||
|
red.Position = 1;
|
||||||
|
|
||||||
|
// The new stop avoids the other stops, the replaced stop no longer does
|
||||||
|
lime.Position.ShouldBe(0.999f, 0.0001f);
|
||||||
|
red.Position.ShouldBe(1f);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Remove_RemovesTheStopAndRaisesARemove()
|
public void Remove_RemovesTheStopAndRaisesARemove()
|
||||||
{
|
{
|
||||||
@@ -298,20 +310,21 @@ public class ColorGradientTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Remove_OfAnEqualStopRemovesTheGradientsStopButKeepsListeningToIt()
|
public void Remove_OfAnEqualStopRemovesTheGradientsStopAndStopsListeningToIt()
|
||||||
{
|
{
|
||||||
ColorGradientStop red = new(SKColors.Red, 0);
|
ColorGradientStop red = new(SKColors.Red, 0);
|
||||||
ColorGradient gradient = [red, new ColorGradientStop(SKColors.Blue, 1)];
|
ColorGradient gradient = [red, new ColorGradientStop(SKColors.Blue, 1)];
|
||||||
|
List<NotifyCollectionChangedEventArgs> events = RecordCollectionChanges(gradient);
|
||||||
int changes = 0;
|
int changes = 0;
|
||||||
gradient.StopChanged += (_, _) => changes++;
|
gradient.StopChanged += (_, _) => changes++;
|
||||||
|
|
||||||
|
// Stops are equal when their color and position match, so removing a look-alike removes the gradient's own stop
|
||||||
gradient.Remove(new ColorGradientStop(SKColors.Red, 0)).ShouldBeTrue();
|
gradient.Remove(new ColorGradientStop(SKColors.Red, 0)).ShouldBeTrue();
|
||||||
red.Color = SKColors.White;
|
red.Color = SKColors.White;
|
||||||
|
|
||||||
// Pinned as-is: stops are equal when their color and position match, so removing a look-alike removes the
|
|
||||||
// gradient's own stop. Only the look-alike is unsubscribed though, the gradient keeps reacting to the removed stop
|
|
||||||
gradient.Count.ShouldBe(1);
|
gradient.Count.ShouldBe(1);
|
||||||
changes.ShouldBe(1);
|
events.Single().OldItems![0].ShouldBeSameAs(red);
|
||||||
|
changes.ShouldBe(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -329,7 +342,7 @@ public class ColorGradientTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Remove_LeavesTheStopLinkedToTheGradient()
|
public void Remove_UnlinksTheStopFromTheGradient()
|
||||||
{
|
{
|
||||||
ColorGradientStop red = new(SKColors.Red, 0);
|
ColorGradientStop red = new(SKColors.Red, 0);
|
||||||
ColorGradientStop blue = new(SKColors.Blue, 1);
|
ColorGradientStop blue = new(SKColors.Blue, 1);
|
||||||
@@ -338,8 +351,8 @@ public class ColorGradientTests
|
|||||||
gradient.Remove(blue);
|
gradient.Remove(blue);
|
||||||
blue.Position = 0f;
|
blue.Position = 0f;
|
||||||
|
|
||||||
// Pinned as-is: a removed stop still avoids the positions of the stops in the gradient it was removed from
|
// A removed stop no longer avoids the positions of the stops in the gradient it was removed from
|
||||||
blue.Position.ShouldBe(0.001f, 0.0001f);
|
blue.Position.ShouldBe(0f);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -358,18 +371,17 @@ public class ColorGradientTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void RemoveAt_ReportsTheIndexAsTheRemovedStop()
|
public void RemoveAt_ReportsTheRemovedStopAndItsIndex()
|
||||||
{
|
{
|
||||||
ColorGradient gradient = [new ColorGradientStop(SKColors.Red, 0), new ColorGradientStop(SKColors.Blue, 1)];
|
ColorGradientStop blue = new(SKColors.Blue, 1);
|
||||||
|
ColorGradient gradient = [new ColorGradientStop(SKColors.Red, 0), blue];
|
||||||
List<NotifyCollectionChangedEventArgs> events = RecordCollectionChanges(gradient);
|
List<NotifyCollectionChangedEventArgs> events = RecordCollectionChanges(gradient);
|
||||||
|
|
||||||
gradient.RemoveAt(1);
|
gradient.RemoveAt(1);
|
||||||
|
|
||||||
// Pinned as-is: the index is passed where the event expects the removed item, so listeners get the number 1 as
|
|
||||||
// the removed stop and no index. Remove reports the stop and its index properly
|
|
||||||
events.Single().Action.ShouldBe(NotifyCollectionChangedAction.Remove);
|
events.Single().Action.ShouldBe(NotifyCollectionChangedAction.Remove);
|
||||||
events.Single().OldItems![0].ShouldBe(1);
|
events.Single().OldItems![0].ShouldBeSameAs(blue);
|
||||||
events.Single().OldStartingIndex.ShouldBe(-1);
|
events.Single().OldStartingIndex.ShouldBe(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -547,17 +559,20 @@ public class ColorGradientTests
|
|||||||
|
|
||||||
gradient.ToggleSeamless();
|
gradient.ToggleSeamless();
|
||||||
|
|
||||||
// Pinned as-is: the remaining stops are stretched as if they were squeezed by an earlier toggle. Blue would land
|
// The remaining stops are stretched as if they were squeezed by an earlier toggle, Blue would land past the end
|
||||||
// past the end (1.35) so it stays where it was, the cap of 100 in ToggleSeamless looks like it was meant to be 1
|
// (1.35) so it stops at the end
|
||||||
gradient.Select(s => s.Color).ShouldBe([SKColors.Red, SKColors.Lime, SKColors.Blue]);
|
gradient.Select(s => s.Color).ShouldBe([SKColors.Red, SKColors.Lime, SKColors.Blue]);
|
||||||
gradient.Select(s => s.Position).ShouldBe([0f, 0.75f, 0.9f]);
|
gradient.Select(s => s.Position).ShouldBe([0f, 0.75f, 1f]);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ToggleSeamless_OnAnEmptyGradientThrows()
|
public void ToggleSeamless_OnAnEmptyGradientDoesNothing()
|
||||||
{
|
{
|
||||||
// Pinned as-is: an empty gradient counts as seamless, so toggling tries to remove a last stop that isn't there
|
ColorGradient gradient = [];
|
||||||
Should.Throw<InvalidOperationException>(() => new ColorGradient().ToggleSeamless());
|
|
||||||
|
gradient.ToggleSeamless();
|
||||||
|
|
||||||
|
gradient.ShouldBeEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
@@ -582,7 +597,7 @@ public class ColorGradientTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void SpreadStops_AvoidsAStopThatIsStillInTheWay()
|
public void SpreadStops_CanMoveAStopToWhereAnotherStopWas()
|
||||||
{
|
{
|
||||||
ColorGradient gradient =
|
ColorGradient gradient =
|
||||||
[
|
[
|
||||||
@@ -594,19 +609,18 @@ public class ColorGradientTests
|
|||||||
|
|
||||||
gradient.SpreadStops();
|
gradient.SpreadStops();
|
||||||
|
|
||||||
// Pinned as-is: stops are moved one by one, Lime can't take 0.333 while Blue still sits there and moves down instead
|
// Lime takes 0.333 even though Blue is still there at the moment Lime moves
|
||||||
gradient.Select(s => s.Position).ShouldBe([0f, 0.331f, 0.667f, 1f], 0.0001f);
|
gradient.Select(s => s.Position).ShouldBe([0f, 0.333f, 0.667f, 1f], 0.0001f);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void SpreadStops_WithASingleStopGivesItNoPosition()
|
public void SpreadStops_WithASingleStopMovesItToTheStart()
|
||||||
{
|
{
|
||||||
ColorGradient gradient = [new ColorGradientStop(SKColors.Red, 0.5f)];
|
ColorGradient gradient = [new ColorGradientStop(SKColors.Red, 0.5f)];
|
||||||
|
|
||||||
gradient.SpreadStops();
|
gradient.SpreadStops();
|
||||||
|
|
||||||
// Pinned as-is: spreading divides by the number of gaps, which is zero for a single stop
|
gradient[0].Position.ShouldBe(0f);
|
||||||
float.IsNaN(gradient[0].Position).ShouldBeTrue();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -637,11 +651,9 @@ public class ColorGradientTests
|
|||||||
|
|
||||||
gradient.FlipStops();
|
gradient.FlipStops();
|
||||||
|
|
||||||
// Pinned as-is: stops are moved one by one, Red can't take 1 while Blue still sits there so it moves down (by
|
// Red and Blue swap places, even though the other one is still there at the moment the first of them moves
|
||||||
// 0.002, see Add_MovesAStopOnAnOccupiedPositionSlightlyDown).
|
|
||||||
// The same happens to any pair of stops on mirrored positions, like 0.1 and 0.9
|
|
||||||
gradient.Select(s => s.Color).ShouldBe([SKColors.Blue, SKColors.Lime, SKColors.Red]);
|
gradient.Select(s => s.Color).ShouldBe([SKColors.Blue, SKColors.Lime, SKColors.Red]);
|
||||||
gradient.Select(s => s.Position).ShouldBe([0f, 0.75f, 0.998f], 0.0001f);
|
gradient.Select(s => s.Position).ShouldBe([0f, 0.75f, 1f], 0.0001f);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -656,10 +668,9 @@ public class ColorGradientTests
|
|||||||
|
|
||||||
gradient.RotateStops(false);
|
gradient.RotateStops(false);
|
||||||
|
|
||||||
// Pinned as-is: stops are moved one by one, starting with the last stop which goes to the position of the first.
|
// The last stop wraps around to the position of the first
|
||||||
// The first stop is still there at that moment, so the last stop lands just below it
|
|
||||||
gradient.Select(s => s.Color).ShouldBe([SKColors.Blue, SKColors.Red, SKColors.Lime]);
|
gradient.Select(s => s.Color).ShouldBe([SKColors.Blue, SKColors.Red, SKColors.Lime]);
|
||||||
gradient.Select(s => s.Position).ShouldBe([0.099f, 0.5f, 0.9f], 0.0001f);
|
gradient.Select(s => s.Position).ShouldBe([0.1f, 0.5f, 0.9f], 0.0001f);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -674,10 +685,9 @@ public class ColorGradientTests
|
|||||||
|
|
||||||
gradient.RotateStops(true);
|
gradient.RotateStops(true);
|
||||||
|
|
||||||
// Pinned as-is: like rotating forwards, the first stop to move lands just below the stop that still occupies its
|
// The first stop wraps around to the position of the last
|
||||||
// new position
|
|
||||||
gradient.Select(s => s.Color).ShouldBe([SKColors.Lime, SKColors.Blue, SKColors.Red]);
|
gradient.Select(s => s.Color).ShouldBe([SKColors.Lime, SKColors.Blue, SKColors.Red]);
|
||||||
gradient.Select(s => s.Position).ShouldBe([0.1f, 0.5f, 0.898f], 0.0001f);
|
gradient.Select(s => s.Position).ShouldBe([0.1f, 0.5f, 0.9f], 0.0001f);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -689,11 +699,10 @@ public class ColorGradientTests
|
|||||||
forward.RotateStops(false);
|
forward.RotateStops(false);
|
||||||
backward.RotateStops(true);
|
backward.RotateStops(true);
|
||||||
|
|
||||||
// Pinned as-is: see above, at the start of the gradient there is no room below so the stop lands just above 0
|
|
||||||
forward.Select(s => s.Color).ShouldBe([SKColors.Blue, SKColors.Red, SKColors.Lime]);
|
forward.Select(s => s.Color).ShouldBe([SKColors.Blue, SKColors.Red, SKColors.Lime]);
|
||||||
forward.Select(s => s.Position).ShouldBe([0.001f, 0.5f, 1f], 0.0001f);
|
forward.Select(s => s.Position).ShouldBe([0f, 0.5f, 1f], 0.0001f);
|
||||||
backward.Select(s => s.Color).ShouldBe([SKColors.Lime, SKColors.Blue, SKColors.Red]);
|
backward.Select(s => s.Color).ShouldBe([SKColors.Lime, SKColors.Blue, SKColors.Red]);
|
||||||
backward.Select(s => s.Position).ShouldBe([0f, 0.5f, 0.998f], 0.0001f);
|
backward.Select(s => s.Position).ShouldBe([0f, 0.5f, 1f], 0.0001f);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -732,14 +741,13 @@ public class ColorGradientTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Randomize_WithASingleStopGivesItNoPosition()
|
public void Randomize_WithASingleStopPutsItAtTheStart()
|
||||||
{
|
{
|
||||||
ColorGradient gradient = [];
|
ColorGradient gradient = [];
|
||||||
|
|
||||||
gradient.Randomize(1);
|
gradient.Randomize(1);
|
||||||
|
|
||||||
// Pinned as-is: like SpreadStops, the spacing divides by the number of gaps, which is zero for a single stop
|
gradient.Single().Position.ShouldBe(0f);
|
||||||
float.IsNaN(gradient.Single().Position).ShouldBeTrue();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -832,11 +840,10 @@ public class ColorGradientTests
|
|||||||
ColorGradient end = from.Interpolate(to, 1f);
|
ColorGradient end = from.Interpolate(to, 1f);
|
||||||
|
|
||||||
// The first two stops follow the target's stops, the third has no counterpart and takes the target's color at its
|
// The first two stops follow the target's stops, the third has no counterpart and takes the target's color at its
|
||||||
// own position. Pinned as-is: the second stop goes to the target's last stop at 1, which is still taken by the
|
// own position. The second stop goes all the way to the target's last stop at 1, where the third stop also is
|
||||||
// third stop, so it lands just below it
|
|
||||||
end.Count.ShouldBe(3);
|
end.Count.ShouldBe(3);
|
||||||
end.Colors.ShouldBe([SKColors.Black, SKColors.White, SKColors.White]);
|
end.Colors.ShouldBe([SKColors.Black, SKColors.White, SKColors.White]);
|
||||||
end.Positions.ShouldBe([0f, 0.998f, 1f], 0.0001f);
|
end.Positions.ShouldBe([0f, 1f, 1f], 0.0001f);
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
@@ -874,7 +881,7 @@ public class ColorGradientTests
|
|||||||
|
|
||||||
copyChanges.ShouldBe(2);
|
copyChanges.ShouldBe(2);
|
||||||
originalChanges.ShouldBe(0);
|
originalChanges.ShouldBe(0);
|
||||||
copiedBlue.Position.ShouldBe(0.498f, 0.0001f, "the copied stops avoid each other's position");
|
copiedBlue.Position.ShouldBe(0.499f, 0.0001f,"the copied stops avoid each other's position");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -902,16 +909,14 @@ public class ColorGradientTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Copy_FromAListKeepsTheOrderOfTheList()
|
public void Copy_FromAListSortsTheStops()
|
||||||
{
|
{
|
||||||
List<ColorGradientStop> stops = [new ColorGradientStop(SKColors.Blue, 1), new ColorGradientStop(SKColors.Red, 0)];
|
List<ColorGradientStop> stops = [new ColorGradientStop(SKColors.Blue, 1), new ColorGradientStop(SKColors.Red, 0)];
|
||||||
|
|
||||||
ColorGradient gradient = new(stops);
|
ColorGradient gradient = new(stops);
|
||||||
|
|
||||||
// Pinned as-is: the copy constructors don't sort, so stops out of order stay out of order until one of them changes,
|
gradient.Colors.ShouldBe([SKColors.Red, SKColors.Blue]);
|
||||||
// and GetColor relies on stops being in order
|
gradient.GetColor(0.5f).ShouldBe(new SKColor(127, 0, 127));
|
||||||
gradient.Colors.ShouldBe([SKColors.Blue, SKColors.Red]);
|
|
||||||
gradient.GetColor(0.5f).ShouldBe(SKColors.Blue);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -967,13 +972,12 @@ public class ColorGradientTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ObsoletePositionsArray_IgnoresSeamlessWithoutRepeats()
|
public void ObsoletePositionsArray_ClosesTheLoopWithoutRepeats()
|
||||||
{
|
{
|
||||||
ColorGradient gradient = [new ColorGradientStop(SKColors.Red, 0), new ColorGradientStop(SKColors.Blue, 1)];
|
ColorGradient gradient = [new ColorGradientStop(SKColors.Red, 0), new ColorGradientStop(SKColors.Blue, 1)];
|
||||||
|
|
||||||
// Pinned as-is: without repeats the seamless positions are skipped (the check looks inverted), so there is one
|
// A position for each of the three colors
|
||||||
// position less than there are colors
|
GetPositionsArray(gradient, 0, true).ShouldBe([0f, 0.5f, 1f]);
|
||||||
GetPositionsArray(gradient, 0, true).ShouldBe([0f, 1f]);
|
|
||||||
GetColorsArray(gradient, 0, true).Length.ShouldBe(3);
|
GetColorsArray(gradient, 0, true).Length.ShouldBe(3);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -996,24 +1000,21 @@ public class ColorGradientTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ObsoleteGetColor_PicksTheWrongColorAfterARepeat()
|
public void ObsoleteGetColor_FadesAgainAfterARepeat()
|
||||||
{
|
{
|
||||||
ColorGradient gradient = [new ColorGradientStop(SKColors.Red, 0), new ColorGradientStop(SKColors.Blue, 1)];
|
ColorGradient gradient = [new ColorGradientStop(SKColors.Red, 0), new ColorGradientStop(SKColors.Blue, 1)];
|
||||||
|
|
||||||
// Pinned as-is: the second repeat starts at 0.5, where the first repeat also ends. Colors are looked up by
|
// The second repeat starts at 0.5, where the first repeat ends, and fades from red to blue again
|
||||||
// position, which finds the end of the first repeat (blue) instead of the start of the second (red), so the
|
GetColor(gradient, 0.75f, 1, false).ShouldBe(new SKColor(127, 0, 127));
|
||||||
// second half is solid blue instead of fading from red to blue again
|
|
||||||
GetColor(gradient, 0.75f, 1, false).ShouldBe(SKColors.Blue);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ObsoleteGetColor_IgnoresSeamlessWithoutRepeats()
|
public void ObsoleteGetColor_SeamlessWithoutRepeatsTurnsBackTowardsTheFirstColor()
|
||||||
{
|
{
|
||||||
ColorGradient gradient = [new ColorGradientStop(SKColors.Red, 0), new ColorGradientStop(SKColors.Blue, 1)];
|
ColorGradient gradient = [new ColorGradientStop(SKColors.Red, 0), new ColorGradientStop(SKColors.Blue, 1)];
|
||||||
|
|
||||||
// Pinned as-is: because of the missing seamless positions (see ObsoletePositionsArray_IgnoresSeamlessWithoutRepeats)
|
// Red at the start, blue in the middle and red again at the end
|
||||||
// this fades from red to blue as if seamless was off, instead of turning back towards red past the middle
|
GetColor(gradient, 0.75f, 0, true).ShouldBe(new SKColor(127, 0, 127));
|
||||||
GetColor(gradient, 0.75f, 0, true).ShouldBe(new SKColor(63, 0, 191));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
@@ -148,8 +148,7 @@ public class AdaptionHintTests : AdaptionTestBase
|
|||||||
LedId.Keyboard_Num0, LedId.Keyboard_NumPeriodAndDelete
|
LedId.Keyboard_Num0, LedId.Keyboard_NumPeriodAndDelete
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
// Pinned as-is: the arrow keys section starts one key early and includes Page Down
|
{KeyboardSection.ArrowKeys, [LedId.Keyboard_ArrowUp, LedId.Keyboard_ArrowLeft, LedId.Keyboard_ArrowDown, LedId.Keyboard_ArrowRight]},
|
||||||
{KeyboardSection.ArrowKeys, [LedId.Keyboard_PageDown, LedId.Keyboard_ArrowUp, LedId.Keyboard_ArrowLeft, LedId.Keyboard_ArrowDown, LedId.Keyboard_ArrowRight]},
|
|
||||||
{
|
{
|
||||||
KeyboardSection.MediaKeys, [
|
KeyboardSection.MediaKeys, [
|
||||||
LedId.Keyboard_MediaMute, LedId.Keyboard_MediaStop, LedId.Keyboard_MediaPreviousTrack,
|
LedId.Keyboard_MediaMute, LedId.Keyboard_MediaStop, LedId.Keyboard_MediaPreviousTrack,
|
||||||
|
|||||||
@@ -60,10 +60,9 @@ public class ColorExtensionsTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Darken_MakesTheColorOpaque()
|
public void Darken_KeepsTheAlpha()
|
||||||
{
|
{
|
||||||
// Pinned as-is: the color is rebuilt from its hue, saturation and lightness, which drops the alpha
|
SKColors.Red.WithAlpha(100).Darken(0.5f).Alpha.ShouldBe((byte) 100);
|
||||||
SKColors.Red.WithAlpha(100).Darken(0.5f).Alpha.ShouldBe((byte) 255);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
|
|||||||
@@ -21,14 +21,13 @@ public class EnumUtilitiesTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void GetAllValuesAndDescriptions_UsesDescriptionsOnlyWhenGivenTheTypeAsGenericArgument()
|
public void GetAllValuesAndDescriptions_UsesDescriptionsBothWhenGivenTheTypeAsGenericArgumentAndAsType()
|
||||||
{
|
{
|
||||||
(KeyboardSection, string) generic = EnumUtilities.GetAllValuesAndDescriptions<KeyboardSection>().Single(v => v.Item1 == KeyboardSection.LedStrips);
|
(KeyboardSection, string) generic = EnumUtilities.GetAllValuesAndDescriptions<KeyboardSection>().Single(v => v.Item1 == KeyboardSection.LedStrips);
|
||||||
(Enum, string) byType = EnumUtilities.GetAllValuesAndDescriptions(typeof(KeyboardSection)).Single(v => v.Item1.Equals(KeyboardSection.LedStrips));
|
(Enum, string) byType = EnumUtilities.GetAllValuesAndDescriptions(typeof(KeyboardSection)).Single(v => v.Item1.Equals(KeyboardSection.LedStrips));
|
||||||
|
|
||||||
// Pinned as-is: the generic version uses the [Description] attribute, the version taking a type ignores it
|
|
||||||
generic.Item2.ShouldBe("LED Strips");
|
generic.Item2.ShouldBe("LED Strips");
|
||||||
byType.Item2.ShouldBe("Led strips");
|
byType.Item2.ShouldBe("LED Strips");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
@@ -27,8 +27,7 @@ public class StringUtilitiesTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void UrlFriendly_CutsLongTitlesShort()
|
public void UrlFriendly_CutsLongTitlesShort()
|
||||||
{
|
{
|
||||||
// Pinned as-is: the limit is meant to be 80 characters, but the character at position 80 is still included
|
StringUtilities.UrlFriendly(new string('a', 100)).Length.ShouldBe(80);
|
||||||
StringUtilities.UrlFriendly(new string('a', 100)).Length.ShouldBe(81);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -64,14 +63,22 @@ public class StringUtilitiesTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData('Ř')]
|
[InlineData('ř', "r")]
|
||||||
[InlineData('Ł')]
|
[InlineData('Ř', "r")]
|
||||||
[InlineData('Đ')]
|
[InlineData('ł', "l")]
|
||||||
[InlineData('þ')]
|
[InlineData('Ł', "l")]
|
||||||
public void RemapInternationalCharToAscii_OnlyKnowsOneCaseOfSomeLetters(char c)
|
[InlineData('đ', "d")]
|
||||||
|
[InlineData('Đ', "d")]
|
||||||
|
[InlineData('þ', "th")]
|
||||||
|
[InlineData('Þ', "th")]
|
||||||
|
[InlineData('ĥ', "h")]
|
||||||
|
[InlineData('Ĥ', "h")]
|
||||||
|
[InlineData('ĵ', "j")]
|
||||||
|
[InlineData('Ĵ', "j")]
|
||||||
|
[InlineData('ß', "ss")]
|
||||||
|
[InlineData('ẞ', "ss")]
|
||||||
|
public void RemapInternationalCharToAscii_KnowsBothCasesOfEveryLetter(char c, string expected)
|
||||||
{
|
{
|
||||||
// Pinned as-is: most letters are lowercased before they are looked up, but ř, ł, đ, þ, ß, ĥ and ĵ are compared
|
StringUtilities.RemapInternationalCharToAscii(c).ShouldBe(expected);
|
||||||
// as typed, so their other case isn't recognized. For the thorn only the uppercase Þ is known
|
|
||||||
StringUtilities.RemapInternationalCharToAscii(c).ShouldBe("");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -34,12 +34,11 @@ public class TypeExtensionsTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void IsGenericType_ThrowsWhenTheBaseTypeIsNotGeneric()
|
public void IsGenericType_IsFalseWhenTheBaseTypeIsNotGeneric()
|
||||||
{
|
{
|
||||||
// Pinned as-is: asking for the generic definition of a base type that isn't generic throws instead of returning false.
|
// Only the direct base type is looked at, so this includes types further down the line
|
||||||
// This includes types further down the line, only the direct base type is looked at
|
typeof(string).IsGenericType(typeof(GenericBase<>)).ShouldBeFalse();
|
||||||
Should.Throw<InvalidOperationException>(() => typeof(string).IsGenericType(typeof(GenericBase<>)));
|
typeof(DerivedTwice).IsGenericType(typeof(GenericBase<>)).ShouldBeFalse();
|
||||||
Should.Throw<InvalidOperationException>(() => typeof(DerivedTwice).IsGenericType(typeof(GenericBase<>)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
@@ -114,12 +113,14 @@ public class TypeExtensionsTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void IsCastableFrom_OnlyFindsOperatorsDeclaredOnTheSourceType()
|
public void IsCastableFrom_FindsOperatorsDeclaredOnEitherType()
|
||||||
{
|
{
|
||||||
|
// Celsius declares the conversion to Fahrenheit, Fahrenheit declares the conversion from Kelvin
|
||||||
typeof(Fahrenheit).IsCastableFrom(typeof(Celsius)).ShouldBeTrue();
|
typeof(Fahrenheit).IsCastableFrom(typeof(Celsius)).ShouldBeTrue();
|
||||||
|
typeof(Fahrenheit).IsCastableFrom(typeof(Kelvin)).ShouldBeTrue();
|
||||||
|
|
||||||
// Pinned as-is: Fahrenheit declares a conversion from Kelvin, but only the operators of Kelvin are looked at
|
// The operator on Fahrenheit only converts from Kelvin
|
||||||
typeof(Fahrenheit).IsCastableFrom(typeof(Kelvin)).ShouldBeFalse();
|
typeof(Fahrenheit).IsCastableFrom(typeof(string)).ShouldBeFalse();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
@@ -221,12 +222,11 @@ public class TypeExtensionsTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void GetDisplayName_OfAnArrayLosesItsBracketsWhenHumanized()
|
public void GetDisplayName_OfAnArrayKeepsItsBrackets()
|
||||||
{
|
{
|
||||||
typeof(ColorGradient[]).GetDisplayName().ShouldBe("ColorGradient[]");
|
typeof(ColorGradient[]).GetDisplayName().ShouldBe("ColorGradient[]");
|
||||||
|
typeof(ColorGradient[]).GetDisplayName(true).ShouldBe("Color gradient[]");
|
||||||
// Pinned as-is: humanizing drops the brackets, so a list of gradients reads the same as a single gradient
|
typeof(int[,]).GetDisplayName().ShouldBe("int[,]");
|
||||||
typeof(ColorGradient[]).GetDisplayName(true).ShouldBe("Color gradient");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|||||||
@@ -182,10 +182,12 @@ public static class ColorQuantizer
|
|||||||
saturation /= 100f;
|
saturation /= 100f;
|
||||||
luma /= 100f;
|
luma /= 100f;
|
||||||
|
|
||||||
if (!ignoreLimits && ((saturation <= GetMinSaturation(type)) || (saturation >= GetMaxSaturation(type)) || (luma <= GetMinLuma(type)) || (luma >= GetMaxLuma(type))))
|
if (!ignoreLimits && ((saturation < GetMinSaturation(type)) || (saturation > GetMaxSaturation(type)) || (luma <= GetMinLuma(type)) || (luma >= GetMaxLuma(type))))
|
||||||
{
|
{
|
||||||
//if either saturation or luma falls outside the min-max, return the
|
//if either saturation or luma falls outside the min-max, return the
|
||||||
//lowest score possible unless we're ignoring these limits.
|
//lowest score possible unless we're ignoring these limits.
|
||||||
|
//the saturation limits are inclusive so fully saturated colors can be vibrant and grays can be muted,
|
||||||
|
//the luma limits are not so pure black and white are left out.
|
||||||
return float.MinValue;
|
return float.MinValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ public static class SKColorExtensions
|
|||||||
{
|
{
|
||||||
c.ToHsl(out float h, out float s, out float l);
|
c.ToHsl(out float h, out float s, out float l);
|
||||||
l *= 1f - amount;
|
l *= 1f - amount;
|
||||||
return SKColor.FromHsl(h, s, l);
|
return SKColor.FromHsl(h, s, l, c.Alpha);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static byte ClampToByte(float value)
|
private static byte ClampToByte(float value)
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ public static class TypeExtensions
|
|||||||
if (type == null)
|
if (type == null)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
return type.BaseType?.GetGenericTypeDefinition() == genericType;
|
return type.BaseType is {IsGenericType: true} baseType && baseType.GetGenericTypeDefinition() == genericType;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -111,9 +111,7 @@ public static class TypeExtensions
|
|||||||
return true;
|
return true;
|
||||||
if (PrimitiveTypeConversions.ContainsKey(to) && PrimitiveTypeConversions[to].Contains(from))
|
if (PrimitiveTypeConversions.ContainsKey(to) && PrimitiveTypeConversions[to].Contains(from))
|
||||||
return true;
|
return true;
|
||||||
bool castable = from.GetMethods(BindingFlags.Public | BindingFlags.Static)
|
return HasConversionOperator(to, from);
|
||||||
.Any(m => m.ReturnType == to && (m.Name == "op_Implicit" || m.Name == "op_Explicit"));
|
|
||||||
return castable;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -141,6 +139,15 @@ public static class TypeExtensions
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool HasConversionOperator(Type to, Type from)
|
||||||
|
{
|
||||||
|
// A conversion operator can be declared on either of the two types
|
||||||
|
return from.GetMethods(BindingFlags.Public | BindingFlags.Static)
|
||||||
|
.Any(m => m.ReturnType == to && (m.Name == "op_Implicit" || m.Name == "op_Explicit")) ||
|
||||||
|
to.GetMethods(BindingFlags.Public | BindingFlags.Static)
|
||||||
|
.Any(m => m.ReturnType == to && (m.Name == "op_Implicit" || m.Name == "op_Explicit") && m.GetParameters()[0].ParameterType.IsAssignableFrom(from));
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns the default value of the given type
|
/// Returns the default value of the given type
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -200,6 +207,10 @@ public static class TypeExtensions
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public static string GetDisplayName(this Type type, bool humanize = false)
|
public static string GetDisplayName(this Type type, bool humanize = false)
|
||||||
{
|
{
|
||||||
|
// Name the element type separately so humanizing doesn't drop the brackets
|
||||||
|
if (type.IsArray)
|
||||||
|
return $"{type.GetElementType()!.GetDisplayName(humanize)}[{new string(',', type.GetArrayRank() - 1)}]";
|
||||||
|
|
||||||
if (!type.IsGenericType)
|
if (!type.IsGenericType)
|
||||||
{
|
{
|
||||||
string displayValue = TypeKeywords.TryGetValue(type, out string? keyword) ? keyword! : type.Name;
|
string displayValue = TypeKeywords.TryGetValue(type, out string? keyword) ? keyword! : type.Name;
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ public class KeyboardSectionAdaptionHint : CorePropertyChanged, IAdaptionHint
|
|||||||
{KeyboardSection.FunctionKeys, Enum.GetValues<LedId>().Where(l => l >= LedId.Keyboard_F1 && l <= LedId.Keyboard_F12).ToList()},
|
{KeyboardSection.FunctionKeys, Enum.GetValues<LedId>().Where(l => l >= LedId.Keyboard_F1 && l <= LedId.Keyboard_F12).ToList()},
|
||||||
{KeyboardSection.NumberKeys, Enum.GetValues<LedId>().Where(l => l >= LedId.Keyboard_1 && l <= LedId.Keyboard_0).ToList()},
|
{KeyboardSection.NumberKeys, Enum.GetValues<LedId>().Where(l => l >= LedId.Keyboard_1 && l <= LedId.Keyboard_0).ToList()},
|
||||||
{KeyboardSection.NumPad, Enum.GetValues<LedId>().Where(l => l >= LedId.Keyboard_NumLock && l <= LedId.Keyboard_NumPeriodAndDelete).ToList()},
|
{KeyboardSection.NumPad, Enum.GetValues<LedId>().Where(l => l >= LedId.Keyboard_NumLock && l <= LedId.Keyboard_NumPeriodAndDelete).ToList()},
|
||||||
{KeyboardSection.ArrowKeys, Enum.GetValues<LedId>().Where(l => l >= LedId.Keyboard_PageDown && l <= LedId.Keyboard_ArrowRight).ToList()},
|
{KeyboardSection.ArrowKeys, Enum.GetValues<LedId>().Where(l => l >= LedId.Keyboard_ArrowUp && l <= LedId.Keyboard_ArrowRight).ToList()},
|
||||||
{KeyboardSection.MediaKeys, Enum.GetValues<LedId>().Where(l => l >= LedId.Keyboard_MediaMute && l <= LedId.Keyboard_MediaNextTrack).ToList()},
|
{KeyboardSection.MediaKeys, Enum.GetValues<LedId>().Where(l => l >= LedId.Keyboard_MediaMute && l <= LedId.Keyboard_MediaNextTrack).ToList()},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -87,6 +87,9 @@ public class ColorGradient : IList<ColorGradientStop>, IList, INotifyCollectionC
|
|||||||
stop.ColorGradient = this;
|
stop.ColorGradient = this;
|
||||||
_stops.Add(stop);
|
_stops.Add(stop);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The list may not be in order, the rest of the gradient expects the stops to be sorted by position
|
||||||
|
Update();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -129,7 +132,7 @@ public class ColorGradient : IList<ColorGradientStop>, IList, INotifyCollectionC
|
|||||||
[Obsolete("Use the Positions property instead", true)]
|
[Obsolete("Use the Positions property instead", true)]
|
||||||
public float[] GetPositionsArray(int timesToRepeat = 0, bool seamless = false)
|
public float[] GetPositionsArray(int timesToRepeat = 0, bool seamless = false)
|
||||||
{
|
{
|
||||||
if (timesToRepeat == 0 && seamless)
|
if (timesToRepeat == 0 && !seamless)
|
||||||
return Positions;
|
return Positions;
|
||||||
|
|
||||||
List<float> result = [];
|
List<float> result = [];
|
||||||
@@ -190,31 +193,34 @@ public class ColorGradient : IList<ColorGradientStop>, IList, INotifyCollectionC
|
|||||||
|
|
||||||
// Walk through the stops until we find the one at or after the requested position, that becomes the right stop
|
// Walk through the stops until we find the one at or after the requested position, that becomes the right stop
|
||||||
// The left stop is the previous stop before the right one was found.
|
// The left stop is the previous stop before the right one was found.
|
||||||
float left = stops[0];
|
// Keep track of indexes rather than positions, repeats share positions where one ends and the next begins
|
||||||
float? right = null;
|
int leftIndex = 0;
|
||||||
foreach (float stop in stops)
|
int rightIndex = -1;
|
||||||
|
for (int i = 0; i < stops.Length; i++)
|
||||||
{
|
{
|
||||||
if (stop >= position)
|
if (stops[i] >= position)
|
||||||
{
|
{
|
||||||
right = stop;
|
rightIndex = i;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
left = stop;
|
leftIndex = i;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the left stop's color
|
// Get the left stop's color
|
||||||
SKColor leftColor = colors[Array.IndexOf(stops, left)];
|
float left = stops[leftIndex];
|
||||||
|
SKColor leftColor = colors[leftIndex];
|
||||||
|
|
||||||
// If no right stop was found or the left and right stops are on the same spot, return the left stop's color
|
// If no right stop was found or the left and right stops are on the same spot, return the left stop's color
|
||||||
if (right == null || left == right)
|
if (rightIndex == -1 || left == stops[rightIndex])
|
||||||
return leftColor;
|
return leftColor;
|
||||||
|
|
||||||
// Get the right stop's color
|
// Get the right stop's color
|
||||||
SKColor rightColor = colors[Array.IndexOf(stops, right)];
|
float right = stops[rightIndex];
|
||||||
|
SKColor rightColor = colors[rightIndex];
|
||||||
|
|
||||||
// Interpolate the position between the left and right color
|
// Interpolate the position between the left and right color
|
||||||
position = MathF.Round((position - left) / (right.Value - left), 2);
|
position = MathF.Round((position - left) / (right - left), 2);
|
||||||
byte a = (byte) ((rightColor.Alpha - leftColor.Alpha) * position + leftColor.Alpha);
|
byte a = (byte) ((rightColor.Alpha - leftColor.Alpha) * position + leftColor.Alpha);
|
||||||
byte r = (byte) ((rightColor.Red - leftColor.Red) * position + leftColor.Red);
|
byte r = (byte) ((rightColor.Red - leftColor.Red) * position + leftColor.Red);
|
||||||
byte g = (byte) ((rightColor.Green - leftColor.Green) * position + leftColor.Green);
|
byte g = (byte) ((rightColor.Green - leftColor.Green) * position + leftColor.Green);
|
||||||
@@ -284,17 +290,12 @@ public class ColorGradient : IList<ColorGradientStop>, IList, INotifyCollectionC
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public void SpreadStops()
|
public void SpreadStops()
|
||||||
{
|
{
|
||||||
try
|
MoveStops(() =>
|
||||||
{
|
{
|
||||||
_updating = true;
|
// A single stop has nothing to spread across and stays at the start
|
||||||
for (int i = 0; i < Count; i++)
|
for (int i = 0; i < Count; i++)
|
||||||
this[i].Position = MathF.Round(i / ((float) Count - 1), 3, MidpointRounding.AwayFromZero);
|
this[i].Position = Count > 1 ? MathF.Round(i / ((float) Count - 1), 3, MidpointRounding.AwayFromZero) : 0;
|
||||||
}
|
});
|
||||||
finally
|
|
||||||
{
|
|
||||||
_updating = false;
|
|
||||||
Update();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -307,6 +308,10 @@ public class ColorGradient : IList<ColorGradientStop>, IList, INotifyCollectionC
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public void ToggleSeamless()
|
public void ToggleSeamless()
|
||||||
{
|
{
|
||||||
|
// An empty gradient has no stops to repeat or remove
|
||||||
|
if (Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_updating = true;
|
_updating = true;
|
||||||
@@ -321,7 +326,7 @@ public class ColorGradient : IList<ColorGradientStop>, IList, INotifyCollectionC
|
|||||||
{
|
{
|
||||||
float multiplier = Count / (Count - 1f);
|
float multiplier = Count / (Count - 1f);
|
||||||
foreach (ColorGradientStop stop in this)
|
foreach (ColorGradientStop stop in this)
|
||||||
stop.Position = MathF.Round(Math.Min(stop.Position * multiplier, 100f), 3, MidpointRounding.AwayFromZero);
|
stop.Position = MathF.Round(Math.Min(stop.Position * multiplier, 1f), 3, MidpointRounding.AwayFromZero);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -348,17 +353,11 @@ public class ColorGradient : IList<ColorGradientStop>, IList, INotifyCollectionC
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public void FlipStops()
|
public void FlipStops()
|
||||||
{
|
{
|
||||||
try
|
MoveStops(() =>
|
||||||
{
|
{
|
||||||
_updating = true;
|
|
||||||
foreach (ColorGradientStop stop in this)
|
foreach (ColorGradientStop stop in this)
|
||||||
stop.Position = 1 - stop.Position;
|
stop.Position = 1 - stop.Position;
|
||||||
}
|
});
|
||||||
finally
|
|
||||||
{
|
|
||||||
_updating = false;
|
|
||||||
Update();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -368,9 +367,8 @@ public class ColorGradient : IList<ColorGradientStop>, IList, INotifyCollectionC
|
|||||||
/// <param name="inverse">A boolean indicating whether or not the invert the rotation.</param>
|
/// <param name="inverse">A boolean indicating whether or not the invert the rotation.</param>
|
||||||
public void RotateStops(bool inverse)
|
public void RotateStops(bool inverse)
|
||||||
{
|
{
|
||||||
try
|
MoveStops(() =>
|
||||||
{
|
{
|
||||||
_updating = true;
|
|
||||||
List<ColorGradientStop> stops = inverse
|
List<ColorGradientStop> stops = inverse
|
||||||
? this.OrderBy(s => s.Position).ToList()
|
? this.OrderBy(s => s.Position).ToList()
|
||||||
: this.OrderByDescending(s => s.Position).ToList();
|
: this.OrderByDescending(s => s.Position).ToList();
|
||||||
@@ -378,12 +376,7 @@ public class ColorGradient : IList<ColorGradientStop>, IList, INotifyCollectionC
|
|||||||
float lastStopPosition = stops.Last().Position;
|
float lastStopPosition = stops.Last().Position;
|
||||||
foreach (ColorGradientStop stop in stops)
|
foreach (ColorGradientStop stop in stops)
|
||||||
(stop.Position, lastStopPosition) = (lastStopPosition, stop.Position);
|
(stop.Position, lastStopPosition) = (lastStopPosition, stop.Position);
|
||||||
}
|
});
|
||||||
finally
|
|
||||||
{
|
|
||||||
_updating = false;
|
|
||||||
Update();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -401,7 +394,7 @@ public class ColorGradient : IList<ColorGradientStop>, IList, INotifyCollectionC
|
|||||||
for (int index = 0; index < stops; index++)
|
for (int index = 0; index < stops; index++)
|
||||||
{
|
{
|
||||||
SKColor skColor = SKColor.FromHsv(random.NextSingle() * 360, 100, 100);
|
SKColor skColor = SKColor.FromHsv(random.NextSingle() * 360, 100, 100);
|
||||||
float position = 1f / (stops - 1f) * index;
|
float position = stops > 1 ? 1f / (stops - 1f) * index : 0;
|
||||||
Add(new ColorGradientStop(skColor, position));
|
Add(new ColorGradientStop(skColor, position));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -431,22 +424,25 @@ public class ColorGradient : IList<ColorGradientStop>, IList, INotifyCollectionC
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Interpolate stops
|
// Interpolate stops
|
||||||
int index = 0;
|
interpolated.MoveStops(() =>
|
||||||
foreach (ColorGradientStop stop in interpolated.ToList())
|
|
||||||
{
|
{
|
||||||
if (index < targetValue.Count)
|
int index = 0;
|
||||||
|
foreach (ColorGradientStop stop in interpolated)
|
||||||
{
|
{
|
||||||
ColorGradientStop targetStop = targetValue[index];
|
if (index < targetValue.Count)
|
||||||
stop.Interpolate(targetStop, progress);
|
{
|
||||||
}
|
ColorGradientStop targetStop = targetValue[index];
|
||||||
// Interpolate stops not on the target gradient
|
stop.Interpolate(targetStop, progress);
|
||||||
else
|
}
|
||||||
{
|
// Interpolate stops not on the target gradient
|
||||||
stop.Color = stop.Color.Interpolate(targetValue.GetColor(stop.Position), progress);
|
else
|
||||||
}
|
{
|
||||||
|
stop.Color = stop.Color.Interpolate(targetValue.GetColor(stop.Position), progress);
|
||||||
|
}
|
||||||
|
|
||||||
index++;
|
index++;
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return interpolated;
|
return interpolated;
|
||||||
}
|
}
|
||||||
@@ -494,6 +490,28 @@ public class ColorGradient : IList<ColorGradientStop>, IList, INotifyCollectionC
|
|||||||
StopChanged?.Invoke(this, EventArgs.Empty);
|
StopChanged?.Invoke(this, EventArgs.Empty);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void MoveStops(Action move)
|
||||||
|
{
|
||||||
|
// Unlink the stops while they move so a stop can take a position another stop is about to leave,
|
||||||
|
// otherwise it would avoid that stop and land next to it
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_updating = true;
|
||||||
|
foreach (ColorGradientStop stop in _stops)
|
||||||
|
stop.ColorGradient = null;
|
||||||
|
|
||||||
|
move();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
foreach (ColorGradientStop stop in _stops)
|
||||||
|
stop.ColorGradient = this;
|
||||||
|
|
||||||
|
_updating = false;
|
||||||
|
Update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void Update()
|
private void Update()
|
||||||
{
|
{
|
||||||
_dirty = true;
|
_dirty = true;
|
||||||
@@ -627,7 +645,11 @@ public class ColorGradient : IList<ColorGradientStop>, IList, INotifyCollectionC
|
|||||||
public void Clear()
|
public void Clear()
|
||||||
{
|
{
|
||||||
foreach (ColorGradientStop item in _stops)
|
foreach (ColorGradientStop item in _stops)
|
||||||
|
{
|
||||||
item.PropertyChanged -= ItemOnPropertyChanged;
|
item.PropertyChanged -= ItemOnPropertyChanged;
|
||||||
|
item.ColorGradient = null;
|
||||||
|
}
|
||||||
|
|
||||||
_stops.Clear();
|
_stops.Clear();
|
||||||
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
|
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
|
||||||
}
|
}
|
||||||
@@ -673,13 +695,13 @@ public class ColorGradient : IList<ColorGradientStop>, IList, INotifyCollectionC
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public bool Remove(ColorGradientStop item)
|
public bool Remove(ColorGradientStop item)
|
||||||
{
|
{
|
||||||
item.PropertyChanged -= ItemOnPropertyChanged;
|
// Stops are compared by color and position, so this may find a different stop that looks the same
|
||||||
int index = _stops.IndexOf(item);
|
int index = _stops.IndexOf(item);
|
||||||
bool removed = _stops.Remove(item);
|
if (index == -1)
|
||||||
if (removed)
|
return false;
|
||||||
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, item, index));
|
|
||||||
|
|
||||||
return removed;
|
RemoveAt(index);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -720,17 +742,23 @@ public class ColorGradient : IList<ColorGradientStop>, IList, INotifyCollectionC
|
|||||||
public void Insert(int index, ColorGradientStop item)
|
public void Insert(int index, ColorGradientStop item)
|
||||||
{
|
{
|
||||||
_stops.Insert(index, item);
|
_stops.Insert(index, item);
|
||||||
|
item.ColorGradient = this;
|
||||||
|
// Update the position, reapplying the overlap-check in Position's setter
|
||||||
|
item.Position = item.Position;
|
||||||
item.PropertyChanged += ItemOnPropertyChanged;
|
item.PropertyChanged += ItemOnPropertyChanged;
|
||||||
|
|
||||||
|
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, index));
|
||||||
Update();
|
Update();
|
||||||
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, _stops.IndexOf(item)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc cref="IList{T}.RemoveAt" />
|
/// <inheritdoc cref="IList{T}.RemoveAt" />
|
||||||
public void RemoveAt(int index)
|
public void RemoveAt(int index)
|
||||||
{
|
{
|
||||||
_stops[index].PropertyChanged -= ItemOnPropertyChanged;
|
ColorGradientStop item = _stops[index];
|
||||||
|
item.PropertyChanged -= ItemOnPropertyChanged;
|
||||||
|
item.ColorGradient = null;
|
||||||
_stops.RemoveAt(index);
|
_stops.RemoveAt(index);
|
||||||
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, index));
|
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, item, index));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -744,8 +772,12 @@ public class ColorGradient : IList<ColorGradientStop>, IList, INotifyCollectionC
|
|||||||
{
|
{
|
||||||
ColorGradientStop? oldValue = _stops[index];
|
ColorGradientStop? oldValue = _stops[index];
|
||||||
oldValue.PropertyChanged -= ItemOnPropertyChanged;
|
oldValue.PropertyChanged -= ItemOnPropertyChanged;
|
||||||
|
oldValue.ColorGradient = null;
|
||||||
_stops[index] = value;
|
_stops[index] = value;
|
||||||
_stops[index].PropertyChanged += ItemOnPropertyChanged;
|
value.ColorGradient = this;
|
||||||
|
// Update the position, reapplying the overlap-check in Position's setter
|
||||||
|
value.Position = value.Position;
|
||||||
|
value.PropertyChanged += ItemOnPropertyChanged;
|
||||||
Update();
|
Update();
|
||||||
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, value, oldValue));
|
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, value, oldValue));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ namespace Artemis.Core;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class ColorGradientStop : CorePropertyChanged
|
public class ColorGradientStop : CorePropertyChanged
|
||||||
{
|
{
|
||||||
|
// The smallest distance kept between two stops in the same gradient
|
||||||
|
private const float MINIMUM_DISTANCE = 0.001f;
|
||||||
|
|
||||||
private SKColor _color;
|
private SKColor _color;
|
||||||
private float _position;
|
private float _position;
|
||||||
|
|
||||||
@@ -75,23 +78,31 @@ public class ColorGradientStop : CorePropertyChanged
|
|||||||
{
|
{
|
||||||
if (ColorGradient == null)
|
if (ColorGradient == null)
|
||||||
return stopPosition;
|
return stopPosition;
|
||||||
|
|
||||||
// Find the first available spot going down
|
// Find the first available spot going down
|
||||||
while (ColorGradient.Any(s => !ReferenceEquals(s, this) && Math.Abs(s.Position - stopPosition) < 0.001f))
|
float position = stopPosition;
|
||||||
stopPosition -= 0.001f;
|
for (int step = 1; IsOccupied(position); step++)
|
||||||
|
position = stopPosition - step * MINIMUM_DISTANCE;
|
||||||
|
|
||||||
// If we ran out of space, try going up
|
// If we ran out of space, try going up
|
||||||
if (stopPosition < 0)
|
if (position < 0)
|
||||||
{
|
{
|
||||||
stopPosition = 0;
|
position = 0;
|
||||||
while (ColorGradient.Any(s => !ReferenceEquals(s, this) && Math.Abs(s.Position - stopPosition) < 0.001f))
|
for (int step = 1; IsOccupied(position); step++)
|
||||||
stopPosition += 0.001f;
|
position = step * MINIMUM_DISTANCE;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we ran out of space there too, movement isn't possible
|
// If we ran out of space there too, movement isn't possible
|
||||||
if (stopPosition > 1)
|
if (position > 1)
|
||||||
stopPosition = Position;
|
position = Position;
|
||||||
|
|
||||||
return stopPosition;
|
return position;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsOccupied(float position)
|
||||||
|
{
|
||||||
|
// Allow for a little rounding error, 0.5 - 0.499 comes out a hair under 0.001 in floating point
|
||||||
|
return ColorGradient != null && ColorGradient.Any(s => !ReferenceEquals(s, this) && Math.Abs(s.Position - position) < MINIMUM_DISTANCE - 0.00001f);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
using Humanizer;
|
using Humanizer;
|
||||||
|
|
||||||
namespace Artemis.Core;
|
namespace Artemis.Core;
|
||||||
@@ -30,7 +32,15 @@ public static class EnumUtilities
|
|||||||
if (!t.IsEnum)
|
if (!t.IsEnum)
|
||||||
throw new ArgumentException($"{t} must be an enum type");
|
throw new ArgumentException($"{t} must be an enum type");
|
||||||
|
|
||||||
return Enum.GetValues(t).Cast<Enum>().Select(e => (e, HumanizeValue(e))).ToList();
|
return Enum.GetValues(t).Cast<Enum>().Select(e => (e, GetDescription(e))).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetDescription(Enum value)
|
||||||
|
{
|
||||||
|
// Use the [Description] attribute like the generic version does, Humanizer can only read it when the type is known
|
||||||
|
string name = value.ToString();
|
||||||
|
DescriptionAttribute? description = value.GetType().GetField(name)?.GetCustomAttribute<DescriptionAttribute>();
|
||||||
|
return description?.Description ?? HumanizeValue(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ public static class StringUtilities
|
|||||||
if (prevlen != sb.Length) prevdash = false;
|
if (prevlen != sb.Length) prevdash = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (i == maxlen) break;
|
if (i == maxlen - 1) break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (prevdash)
|
if (prevdash)
|
||||||
@@ -114,31 +114,31 @@ public static class StringUtilities
|
|||||||
{
|
{
|
||||||
return "g";
|
return "g";
|
||||||
}
|
}
|
||||||
else if (c == 'ř')
|
else if (s == "ř")
|
||||||
{
|
{
|
||||||
return "r";
|
return "r";
|
||||||
}
|
}
|
||||||
else if (c == 'ł')
|
else if (s == "ł")
|
||||||
{
|
{
|
||||||
return "l";
|
return "l";
|
||||||
}
|
}
|
||||||
else if (c == 'đ')
|
else if (s == "đ")
|
||||||
{
|
{
|
||||||
return "d";
|
return "d";
|
||||||
}
|
}
|
||||||
else if (c == 'ß')
|
else if (s == "ß")
|
||||||
{
|
{
|
||||||
return "ss";
|
return "ss";
|
||||||
}
|
}
|
||||||
else if (c == 'Þ')
|
else if (s == "þ")
|
||||||
{
|
{
|
||||||
return "th";
|
return "th";
|
||||||
}
|
}
|
||||||
else if (c == 'ĥ')
|
else if (s == "ĥ")
|
||||||
{
|
{
|
||||||
return "h";
|
return "h";
|
||||||
}
|
}
|
||||||
else if (c == 'ĵ')
|
else if (s == "ĵ")
|
||||||
{
|
{
|
||||||
return "j";
|
return "j";
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user