diff --git a/Terminal.Gui/Drawing/Gradient.cs b/Terminal.Gui/Drawing/Gradient.cs index 7aea37b90..01d7d2490 100644 --- a/Terminal.Gui/Drawing/Gradient.cs +++ b/Terminal.Gui/Drawing/Gradient.cs @@ -44,15 +44,42 @@ public class Gradient private readonly List _steps; + /// + /// Creates a new instance of the class which hosts a + /// of colors including all and interpolated colors + /// between each corresponding pair. + /// + /// The colors to use in the spectrum (N) + /// The number of colors to generate between each pair (must be N-1 numbers). + /// If only one step is passed then it is assumed to be the same distance for all pairs. + /// True to duplicate the first stop and step so that the gradient repeats itself + /// public Gradient (IEnumerable stops, IEnumerable steps, bool loop = false) { _stops = stops.ToList (); + if (_stops.Count < 1) + { throw new ArgumentException ("At least one color stop must be provided."); + } _steps = steps.ToList (); + + // If multiple colors and only 1 step assume same distance applies to all steps + if (_stops.Count > 2 && _steps.Count == 1) + { + _steps = Enumerable.Repeat (_steps.Single (),_stops.Count() - 1).ToList(); + } + if (_steps.Any (step => step < 1)) + { throw new ArgumentException ("Steps must be greater than 0."); + } + + if (_steps.Count != _stops.Count - 1) + { + throw new ArgumentException ("Number of steps must be N-1"); + } _loop = loop; Spectrum = GenerateGradient (_steps); @@ -85,6 +112,7 @@ public class Gradient private List GenerateGradient (IEnumerable steps) { List gradient = new List (); + if (_stops.Count == 1) { for (int i = 0; i < steps.Sum (); i++) @@ -94,13 +122,17 @@ public class Gradient return gradient; } + var stopsToUse = _stops.ToList (); + var stepsToUse = _steps.ToList (); + if (_loop) { - _stops.Add (_stops [0]); + stopsToUse.Add (_stops [0]); + stepsToUse.Add (_steps.First ()); } - var colorPairs = _stops.Zip (_stops.Skip (1), (start, end) => new { start, end }); - var stepsList = _steps.ToList (); + var colorPairs = stopsToUse.Zip (stopsToUse.Skip (1), (start, end) => new { start, end }); + var stepsList = stepsToUse; foreach (var (colorPair, thesteps) in colorPairs.Zip (stepsList, (pair, step) => (pair, step))) { @@ -112,7 +144,7 @@ public class Gradient private IEnumerable InterpolateColors (Color start, Color end, int steps) { - for (int step = 0; step <= steps; step++) + for (int step = 0; step < steps; step++) { double fraction = (double)step / steps; int r = (int)(start.R + fraction * (end.R - start.R)); @@ -120,8 +152,10 @@ public class Gradient int b = (int)(start.B + fraction * (end.B - start.B)); yield return new Color (r, g, b); } + yield return end; // Ensure the last color is included } + /// /// /// Creates a mapping starting at 0,0 and going to and diff --git a/UICatalog/Scenarios/TextEffectsScenario.cs b/UICatalog/Scenarios/TextEffectsScenario.cs index 63bdc8b00..14ce7c329 100644 --- a/UICatalog/Scenarios/TextEffectsScenario.cs +++ b/UICatalog/Scenarios/TextEffectsScenario.cs @@ -14,6 +14,8 @@ public class TextEffectsScenario : Scenario { private TabView tabView; + public static bool LoopingGradient = false; + public override void Main () { Application.Init (); @@ -27,8 +29,6 @@ public class TextEffectsScenario : Scenario w.Loaded += (s, e) => { SetupGradientLineCanvas (w, w.Frame.Size); - // TODO: Does not work - // SetupGradientLineCanvas (tabView, tabView.Frame.Size); }; w.SizeChanging += (s, e) => { @@ -36,9 +36,6 @@ public class TextEffectsScenario : Scenario { SetupGradientLineCanvas (w, e.Size.Value); } - - // TODO: Does not work - //SetupGradientLineCanvas (tabView, tabView.Frame.Size); }; w.ColorScheme = new ColorScheme @@ -57,16 +54,32 @@ public class TextEffectsScenario : Scenario Height = Dim.Fill (), }; + var gradientsView = new GradientsView () + { + Width = Dim.Fill (), + Height = Dim.Fill (), + }; var t1 = new Tab () { - View = new GradientsView () - { - Width = Dim.Fill (), - Height = Dim.Fill (), - }, + View = gradientsView, DisplayText = "Gradients" }; + + var cbLooping = new CheckBox () + { + Text = "Looping", + Y = Pos.AnchorEnd (1) + }; + cbLooping.Toggle += (s, e) => + { + LoopingGradient = e.NewValue == CheckState.Checked; + SetupGradientLineCanvas (w, w.Frame.Size); + tabView.SetNeedsDisplay (); + }; + + gradientsView.Add (cbLooping); + tabView.AddTab (t1, false); w.Add (tabView); @@ -83,7 +96,7 @@ public class TextEffectsScenario : Scenario { GetAppealingGradientColors (out var stops, out var steps); - var g = new Gradient (stops, steps); + var g = new Gradient (stops, steps, LoopingGradient); var fore = new GradientFill ( new Rectangle (0, 0, size.Width, size.Height), g, GradientDirection.Diagonal); @@ -107,7 +120,8 @@ public class TextEffectsScenario : Scenario }; // Define the number of steps between each color for smoother transitions - steps = new List { 15, 15, 15, 15 }; + // If we pass only a single value then it will assume equal steps between all pairs + steps = new List { 15 }; } } @@ -157,7 +171,7 @@ internal class GradientsView : View var steps = new List { 10, 10 }; // 10 steps between Red -> Green, and Green -> Blue // Create the gradient - var radialGradient = new Gradient (stops, steps, loop: false); + var radialGradient = new Gradient (stops, steps, loop: TextEffectsScenario.LoopingGradient); // Define the size of the rectangle int maxRow = 15; // Adjusted to keep aspect ratio @@ -207,7 +221,7 @@ internal class GradientsView : View }; // Create the gradient - var rainbowGradient = new Gradient (stops, steps, loop: true); + var rainbowGradient = new Gradient (stops, steps, TextEffectsScenario.LoopingGradient); for (int x = 0; x < viewport.Width; x++) { diff --git a/UnitTests/Drawing/GradientTests.cs b/UnitTests/Drawing/GradientTests.cs index e9e0194ea..0ff4c4a68 100644 --- a/UnitTests/Drawing/GradientTests.cs +++ b/UnitTests/Drawing/GradientTests.cs @@ -50,4 +50,126 @@ public class GradientTests Assert.Equal (c.Key, new Point(0,0)); Assert.Equal (c.Value, new Color (0, 0, 255)); } -} + + [Fact] + public void SingleColorStop () + { + var stops = new List { new Color (255, 0, 0) }; // Red + var steps = new List { }; + + var g = new Gradient (stops, steps, loop: false); + Assert.All (g.Spectrum, color => Assert.Equal (new Color (255, 0, 0), color)); + } + + [Fact] + public void LoopingGradient_CorrectColors () + { + var stops = new List + { + new Color(255, 0, 0), // Red + new Color(0, 0, 255) // Blue + }; + + var steps = new List { 10 }; + + var g = new Gradient (stops, steps, loop: true); + Assert.Equal (new Color (255, 0, 0), g.Spectrum.First ()); + Assert.Equal (new Color (255, 0, 0), g.Spectrum.Last ()); + } + + [Fact] + public void DifferentStepSizes () + { + var stops = new List + { + new Color(255, 0, 0), // Red + new Color(0, 255, 0), // Green + new Color(0, 0, 255) // Blue + }; + + var steps = new List { 5, 15 }; // Different steps + + var g = new Gradient (stops, steps, loop: false); + Assert.Equal (22, g.Spectrum.Count); + } + + [Fact] + public void FractionOutOfRange_ThrowsException () + { + var stops = new List + { + new Color(255, 0, 0), // Red + new Color(0, 0, 255) // Blue + }; + + var steps = new List { 10 }; + + var g = new Gradient (stops, steps, loop: false); + + Assert.Throws (() => g.GetColorAtFraction (-0.1)); + Assert.Throws (() => g.GetColorAtFraction (1.1)); + } + + [Fact] + public void NaNFraction_ReturnsLastColor () + { + var stops = new List + { + new Color(255, 0, 0), // Red + new Color(0, 0, 255) // Blue + }; + + var steps = new List { 10 }; + + var g = new Gradient (stops, steps, loop: false); + Assert.Equal (new Color (0, 0, 255), g.GetColorAtFraction (double.NaN)); + } + + [Fact] + public void Constructor_SingleStepProvided_ReplicatesForAllPairs () + { + var stops = new List + { + new Color(255, 0, 0), // Red + new Color(0, 255, 0), // Green + new Color(0, 0, 255) // Blue + }; + + var singleStep = new List { 5 }; // Single step provided + var gradient = new Gradient (stops, singleStep, loop: false); + + Assert.NotNull (gradient.Spectrum); + Assert.Equal (12, gradient.Spectrum.Count); // 5 steps Red -> Green + 5 steps Green -> Blue + 2 end colors + } + + [Fact] + public void Constructor_InvalidStepsLength_ThrowsArgumentException () + { + var stops = new List + { + new Color(255, 0, 0), // Red + new Color(0, 0, 255) // Blue + }; + + var invalidSteps = new List { 5, 5 }; // Invalid length (N-1 expected) + Assert.Throws (() => new Gradient (stops, invalidSteps, loop: false)); + } + + [Fact] + public void Constructor_ValidStepsLength_DoesNotThrow () + { + var stops = new List + { + new Color(255, 0, 0), // Red + new Color(0, 255, 0), // Green + new Color(0, 0, 255) // Blue + }; + + var validSteps = new List { 5, 5 }; // Valid length (N-1) + var gradient = new Gradient (stops, validSteps, loop: false); + + Assert.NotNull (gradient.Spectrum); + Assert.Equal (12, gradient.Spectrum.Count); // 5 steps Red -> Green + 5 steps Green -> Blue + 2 end colors + } + +} \ No newline at end of file