mirror of
https://github.com/gui-cs/Terminal.Gui.git
synced 2025-12-26 15:57:56 +01:00
Merge pull request #3586 from tznind/gradients
Gradients - From Terminal Text Effects
This commit is contained in:
41
Terminal.Gui/Drawing/FillPair.cs
Normal file
41
Terminal.Gui/Drawing/FillPair.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
namespace Terminal.Gui;
|
||||
|
||||
/// <summary>
|
||||
/// Describes a pair of <see cref="IFill"/> which cooperate in creating
|
||||
/// <see cref="Attribute"/>. One gives foreground color while other gives background.
|
||||
/// </summary>
|
||||
public class FillPair
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new instance using the provided fills for foreground and background
|
||||
/// color when assembling <see cref="Attribute"/>.
|
||||
/// </summary>
|
||||
/// <param name="fore"></param>
|
||||
/// <param name="back"></param>
|
||||
public FillPair (IFill fore, IFill back)
|
||||
{
|
||||
Foreground = fore;
|
||||
Background = back;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The fill which provides point based foreground color.
|
||||
/// </summary>
|
||||
public IFill Foreground { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The fill which provides point based background color.
|
||||
/// </summary>
|
||||
public IFill Background { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Returns the color pair (foreground+background) to use when rendering
|
||||
/// a rune at the given <paramref name="point"/>.
|
||||
/// </summary>
|
||||
/// <param name="point"></param>
|
||||
/// <returns></returns>
|
||||
public Attribute GetAttribute (Point point)
|
||||
{
|
||||
return new (Foreground.GetColor (point), Background.GetColor (point));
|
||||
}
|
||||
}
|
||||
255
Terminal.Gui/Drawing/Gradient.cs
Normal file
255
Terminal.Gui/Drawing/Gradient.cs
Normal file
@@ -0,0 +1,255 @@
|
||||
// This code is a C# port from python library Terminal Text Effects https://github.com/ChrisBuilds/terminaltexteffects/
|
||||
|
||||
namespace Terminal.Gui;
|
||||
|
||||
/// <summary>
|
||||
/// Describes the pattern that a <see cref="Gradient"/> results in e.g. <see cref="Vertical"/>,
|
||||
/// <see cref="Horizontal"/> etc
|
||||
/// </summary>
|
||||
public enum GradientDirection
|
||||
{
|
||||
/// <summary>
|
||||
/// Color varies along Y axis but is constant on X axis.
|
||||
/// </summary>
|
||||
Vertical,
|
||||
|
||||
/// <summary>
|
||||
/// Color varies along X axis but is constant on Y axis.
|
||||
/// </summary>
|
||||
Horizontal,
|
||||
|
||||
/// <summary>
|
||||
/// Color varies by distance from center (i.e. in circular ripples)
|
||||
/// </summary>
|
||||
Radial,
|
||||
|
||||
/// <summary>
|
||||
/// Color varies by X and Y axis (i.e. a slanted gradient)
|
||||
/// </summary>
|
||||
Diagonal
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Describes a <see cref="Spectrum"/> of colors that can be combined
|
||||
/// to make a color gradient. Use <see cref="BuildCoordinateColorMapping"/>
|
||||
/// to create into gradient fill area maps.
|
||||
/// </summary>
|
||||
public class Gradient
|
||||
{
|
||||
/// <summary>
|
||||
/// The discrete colors that will make up the <see cref="Gradient"/>.
|
||||
/// </summary>
|
||||
public List<Color> Spectrum { get; }
|
||||
|
||||
private readonly bool _loop;
|
||||
private readonly List<Color> _stops;
|
||||
private readonly List<int> _steps;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of the <see cref="Gradient"/> class which hosts a <see cref="Spectrum"/>
|
||||
/// of colors including all <paramref name="stops"/> and <paramref name="steps"/> interpolated colors
|
||||
/// between each corresponding pair.
|
||||
/// </summary>
|
||||
/// <param name="stops">The colors to use in the spectrum (N)</param>
|
||||
/// <param name="steps">
|
||||
/// 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.
|
||||
/// </param>
|
||||
/// <param name="loop">True to duplicate the first stop and step so that the gradient repeats itself</param>
|
||||
/// <exception cref="ArgumentException"></exception>
|
||||
public Gradient (IEnumerable<Color> stops, IEnumerable<int> 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the color to use at the given part of the spectrum
|
||||
/// </summary>
|
||||
/// <param name="fraction">
|
||||
/// Proportion of the way through the spectrum, must be between
|
||||
/// 0 and 1 (inclusive). Returns the last color if <paramref name="fraction"/> is
|
||||
/// <see cref="double.NaN"/>.
|
||||
/// </param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="ArgumentOutOfRangeException"></exception>
|
||||
public Color GetColorAtFraction (double fraction)
|
||||
{
|
||||
if (double.IsNaN (fraction))
|
||||
{
|
||||
return Spectrum.Last ();
|
||||
}
|
||||
|
||||
if (fraction is < 0 or > 1)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException (nameof (fraction), @"Fraction must be between 0 and 1.");
|
||||
}
|
||||
|
||||
var index = (int)(fraction * (Spectrum.Count - 1));
|
||||
|
||||
return Spectrum [index];
|
||||
}
|
||||
|
||||
private List<Color> GenerateGradient (IEnumerable<int> steps)
|
||||
{
|
||||
List<Color> gradient = new ();
|
||||
|
||||
if (_stops.Count == 1)
|
||||
{
|
||||
for (var i = 0; i < steps.Sum (); i++)
|
||||
{
|
||||
gradient.Add (_stops [0]);
|
||||
}
|
||||
|
||||
return gradient;
|
||||
}
|
||||
|
||||
List<Color> stopsToUse = _stops.ToList ();
|
||||
List<int> stepsToUse = _steps.ToList ();
|
||||
|
||||
if (_loop)
|
||||
{
|
||||
stopsToUse.Add (_stops [0]);
|
||||
stepsToUse.Add (_steps.First ());
|
||||
}
|
||||
|
||||
var colorPairs = stopsToUse.Zip (stopsToUse.Skip (1), (start, end) => new { start, end });
|
||||
List<int> stepsList = stepsToUse;
|
||||
|
||||
foreach ((var colorPair, int thesteps) in colorPairs.Zip (stepsList, (pair, step) => (pair, step)))
|
||||
{
|
||||
gradient.AddRange (InterpolateColors (colorPair.start, colorPair.end, thesteps));
|
||||
}
|
||||
|
||||
return gradient;
|
||||
}
|
||||
|
||||
private static IEnumerable<Color> InterpolateColors (Color start, Color end, int steps)
|
||||
{
|
||||
for (var step = 0; step < steps; step++)
|
||||
{
|
||||
double fraction = (double)step / steps;
|
||||
var r = (int)(start.R + fraction * (end.R - start.R));
|
||||
var g = (int)(start.G + fraction * (end.G - start.G));
|
||||
var b = (int)(start.B + fraction * (end.B - start.B));
|
||||
|
||||
yield return new (r, g, b);
|
||||
}
|
||||
|
||||
yield return end; // Ensure the last color is included
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <para>
|
||||
/// Creates a mapping starting at 0,0 and going to <paramref name="maxRow"/> and <paramref name="maxColumn"/>
|
||||
/// (inclusively) using the supplied <paramref name="direction"/>.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Note that this method is inclusive i.e. passing 1/1 results in 4 mapped coordinates.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <param name="maxRow"></param>
|
||||
/// <param name="maxColumn"></param>
|
||||
/// <param name="direction"></param>
|
||||
/// <returns></returns>
|
||||
public Dictionary<Point, Color> BuildCoordinateColorMapping (int maxRow, int maxColumn, GradientDirection direction)
|
||||
{
|
||||
Dictionary<Point, Color> gradientMapping = new ();
|
||||
|
||||
switch (direction)
|
||||
{
|
||||
case GradientDirection.Vertical:
|
||||
for (var row = 0; row <= maxRow; row++)
|
||||
{
|
||||
double fraction = maxRow == 0 ? 1.0 : (double)row / maxRow;
|
||||
Color color = GetColorAtFraction (fraction);
|
||||
|
||||
for (var col = 0; col <= maxColumn; col++)
|
||||
{
|
||||
gradientMapping [new (col, row)] = color;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case GradientDirection.Horizontal:
|
||||
for (var col = 0; col <= maxColumn; col++)
|
||||
{
|
||||
double fraction = maxColumn == 0 ? 1.0 : (double)col / maxColumn;
|
||||
Color color = GetColorAtFraction (fraction);
|
||||
|
||||
for (var row = 0; row <= maxRow; row++)
|
||||
{
|
||||
gradientMapping [new (col, row)] = color;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case GradientDirection.Radial:
|
||||
for (var row = 0; row <= maxRow; row++)
|
||||
{
|
||||
for (var col = 0; col <= maxColumn; col++)
|
||||
{
|
||||
double distanceFromCenter = FindNormalizedDistanceFromCenter (maxRow, maxColumn, new (col, row));
|
||||
Color color = GetColorAtFraction (distanceFromCenter);
|
||||
gradientMapping [new (col, row)] = color;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case GradientDirection.Diagonal:
|
||||
for (var row = 0; row <= maxRow; row++)
|
||||
{
|
||||
for (var col = 0; col <= maxColumn; col++)
|
||||
{
|
||||
double fraction = ((double)row * 2 + col) / (maxRow * 2 + maxColumn);
|
||||
Color color = GetColorAtFraction (fraction);
|
||||
gradientMapping [new (col, row)] = color;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
return gradientMapping;
|
||||
}
|
||||
|
||||
private static double FindNormalizedDistanceFromCenter (int maxRow, int maxColumn, Point coord)
|
||||
{
|
||||
double centerX = maxColumn / 2.0;
|
||||
double centerY = maxRow / 2.0;
|
||||
double dx = coord.X - centerX;
|
||||
double dy = coord.Y - centerY;
|
||||
double distance = Math.Sqrt (dx * dx + dy * dy);
|
||||
double maxDistance = Math.Sqrt (centerX * centerX + centerY * centerY);
|
||||
|
||||
return distance / maxDistance;
|
||||
}
|
||||
}
|
||||
42
Terminal.Gui/Drawing/GradientFill.cs
Normal file
42
Terminal.Gui/Drawing/GradientFill.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
namespace Terminal.Gui;
|
||||
|
||||
/// <summary>
|
||||
/// Implementation of <see cref="IFill"/> that uses a color gradient (including
|
||||
/// radial, diagonal etc.).
|
||||
/// </summary>
|
||||
public class GradientFill : IFill
|
||||
{
|
||||
private readonly Dictionary<Point, Color> _map;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of the <see cref="GradientFill"/> class that can return
|
||||
/// color for any point in the given <paramref name="area"/> using the provided
|
||||
/// <paramref name="gradient"/> and <paramref name="direction"/>.
|
||||
/// </summary>
|
||||
/// <param name="area"></param>
|
||||
/// <param name="gradient"></param>
|
||||
/// <param name="direction"></param>
|
||||
public GradientFill (Rectangle area, Gradient gradient, GradientDirection direction)
|
||||
{
|
||||
_map = gradient.BuildCoordinateColorMapping (area.Height - 1, area.Width - 1, direction)
|
||||
.ToDictionary (
|
||||
kvp => new Point (kvp.Key.X + area.X, kvp.Key.Y + area.Y),
|
||||
kvp => kvp.Value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the color to use for the given <paramref name="point"/> or Black if it
|
||||
/// lies outside the prepared gradient area (see constructor).
|
||||
/// </summary>
|
||||
/// <param name="point"></param>
|
||||
/// <returns></returns>
|
||||
public Color GetColor (Point point)
|
||||
{
|
||||
if (_map.TryGetValue (point, out Color color))
|
||||
{
|
||||
return color;
|
||||
}
|
||||
|
||||
return new (0, 0); // Default to black if point not found
|
||||
}
|
||||
}
|
||||
14
Terminal.Gui/Drawing/IFill.cs
Normal file
14
Terminal.Gui/Drawing/IFill.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
namespace Terminal.Gui;
|
||||
|
||||
/// <summary>
|
||||
/// Describes an area fill (e.g. solid color or gradient).
|
||||
/// </summary>
|
||||
public interface IFill
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns the color that should be used at the given point
|
||||
/// </summary>
|
||||
/// <param name="point"></param>
|
||||
/// <returns></returns>
|
||||
Color GetColor (Point point);
|
||||
}
|
||||
@@ -4,6 +4,13 @@ namespace Terminal.Gui;
|
||||
/// <summary>Facilitates box drawing and line intersection detection and rendering. Does not support diagonal lines.</summary>
|
||||
public class LineCanvas : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Optional <see cref="FillPair"/> which when present overrides the <see cref="StraightLine.Attribute"/>
|
||||
/// (colors) of lines in the canvas. This can be used e.g. to apply a global <see cref="GradientFill"/>
|
||||
/// across all lines.
|
||||
/// </summary>
|
||||
public FillPair? Fill { get; set; }
|
||||
|
||||
private readonly List<StraightLine> _lines = [];
|
||||
|
||||
private readonly Dictionary<IntersectionRuneType, IntersectionRuneResolver> _runeResolvers = new ()
|
||||
@@ -85,7 +92,7 @@ public class LineCanvas : IDisposable
|
||||
viewport = Rectangle.Union (viewport, _lines [i].Viewport);
|
||||
}
|
||||
|
||||
if (viewport is {Width: 0} or {Height: 0})
|
||||
if (viewport is { Width: 0 } or { Height: 0 })
|
||||
{
|
||||
viewport = viewport with
|
||||
{
|
||||
@@ -135,7 +142,7 @@ public class LineCanvas : IDisposable
|
||||
)
|
||||
{
|
||||
_cachedViewport = Rectangle.Empty;
|
||||
_lines.Add (new StraightLine (start, length, orientation, style, attribute));
|
||||
_lines.Add (new (start, length, orientation, style, attribute));
|
||||
}
|
||||
|
||||
/// <summary>Adds a new line to the canvas</summary>
|
||||
@@ -183,7 +190,7 @@ public class LineCanvas : IDisposable
|
||||
|
||||
if (cell is { })
|
||||
{
|
||||
map.Add (new Point (x, y), cell);
|
||||
map.Add (new (x, y), cell);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -218,7 +225,7 @@ public class LineCanvas : IDisposable
|
||||
|
||||
if (rune is { })
|
||||
{
|
||||
map.Add (new Point (x, y), rune.Value);
|
||||
map.Add (new (x, y), rune.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -324,7 +331,10 @@ public class LineCanvas : IDisposable
|
||||
/// <returns></returns>
|
||||
private bool Exactly (HashSet<IntersectionType> intersects, params IntersectionType [] types) { return intersects.SetEquals (types); }
|
||||
|
||||
private Attribute? GetAttributeForIntersects (IntersectionDefinition? [] intersects) { return intersects [0]!.Line.Attribute; }
|
||||
private Attribute? GetAttributeForIntersects (IntersectionDefinition? [] intersects)
|
||||
{
|
||||
return Fill != null ? Fill.GetAttribute (intersects [0]!.Point) : intersects [0]!.Line.Attribute;
|
||||
}
|
||||
|
||||
private Cell? GetCellForIntersects (ConsoleDriver driver, IntersectionDefinition? [] intersects)
|
||||
{
|
||||
@@ -428,12 +438,12 @@ public class LineCanvas : IDisposable
|
||||
useThickDotted ? Glyphs.VLineHvDa4 : Glyphs.VLine;
|
||||
|
||||
default:
|
||||
throw new Exception (
|
||||
"Could not find resolver or switch case for "
|
||||
+ nameof (runeType)
|
||||
+ ":"
|
||||
+ runeType
|
||||
);
|
||||
throw new (
|
||||
"Could not find resolver or switch case for "
|
||||
+ nameof (runeType)
|
||||
+ ":"
|
||||
+ runeType
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -843,4 +853,4 @@ public class LineCanvas : IDisposable
|
||||
_normal = Glyphs.URCorner;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
24
Terminal.Gui/Drawing/SolidFill.cs
Normal file
24
Terminal.Gui/Drawing/SolidFill.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
namespace Terminal.Gui;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="IFill"/> implementation that uses a solid color for all points
|
||||
/// </summary>
|
||||
public class SolidFill : IFill
|
||||
{
|
||||
private readonly Color _color;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of the <see cref="SolidFill"/> class which will return
|
||||
/// the provided <paramref name="color"/> regardless of which point is requested.
|
||||
/// </summary>
|
||||
/// <param name="color"></param>
|
||||
public SolidFill (Color color) { _color = color; }
|
||||
|
||||
/// <summary>
|
||||
/// Returns the color this instance was constructed with regardless of
|
||||
/// which <paramref name="point"/> is being colored.
|
||||
/// </summary>
|
||||
/// <param name="point"></param>
|
||||
/// <returns></returns>
|
||||
public Color GetColor (Point point) { return _color; }
|
||||
}
|
||||
@@ -45,6 +45,7 @@ public class StraightLine
|
||||
/// Gets the rectangle that describes the bounds of the canvas. Location is the coordinates of the line that is
|
||||
/// furthest left/top and Size is defined by the line that extends the furthest right/bottom.
|
||||
/// </summary>
|
||||
|
||||
// PERF: Probably better to store the rectangle rather than make a new one on every single access to Viewport.
|
||||
internal Rectangle Viewport
|
||||
{
|
||||
@@ -111,26 +112,28 @@ public class StraightLine
|
||||
return null;
|
||||
}
|
||||
|
||||
var p = new Point (x, y);
|
||||
|
||||
if (StartsAt (x, y))
|
||||
{
|
||||
return new IntersectionDefinition (
|
||||
Start,
|
||||
GetTypeByLength (
|
||||
IntersectionType.StartLeft,
|
||||
IntersectionType.PassOverHorizontal,
|
||||
IntersectionType.StartRight
|
||||
),
|
||||
this
|
||||
);
|
||||
return new (
|
||||
p,
|
||||
GetTypeByLength (
|
||||
IntersectionType.StartLeft,
|
||||
IntersectionType.PassOverHorizontal,
|
||||
IntersectionType.StartRight
|
||||
),
|
||||
this
|
||||
);
|
||||
}
|
||||
|
||||
if (EndsAt (x, y))
|
||||
{
|
||||
return new IntersectionDefinition (
|
||||
Start,
|
||||
Length < 0 ? IntersectionType.StartRight : IntersectionType.StartLeft,
|
||||
this
|
||||
);
|
||||
return new (
|
||||
p,
|
||||
Length < 0 ? IntersectionType.StartRight : IntersectionType.StartLeft,
|
||||
this
|
||||
);
|
||||
}
|
||||
|
||||
int xmin = Math.Min (Start.X, Start.X + Length);
|
||||
@@ -138,11 +141,11 @@ public class StraightLine
|
||||
|
||||
if (xmin < x && xmax > x)
|
||||
{
|
||||
return new IntersectionDefinition (
|
||||
new Point (x, y),
|
||||
IntersectionType.PassOverHorizontal,
|
||||
this
|
||||
);
|
||||
return new (
|
||||
p,
|
||||
IntersectionType.PassOverHorizontal,
|
||||
this
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -155,26 +158,28 @@ public class StraightLine
|
||||
return null;
|
||||
}
|
||||
|
||||
var p = new Point (x, y);
|
||||
|
||||
if (StartsAt (x, y))
|
||||
{
|
||||
return new IntersectionDefinition (
|
||||
Start,
|
||||
GetTypeByLength (
|
||||
IntersectionType.StartUp,
|
||||
IntersectionType.PassOverVertical,
|
||||
IntersectionType.StartDown
|
||||
),
|
||||
this
|
||||
);
|
||||
return new (
|
||||
p,
|
||||
GetTypeByLength (
|
||||
IntersectionType.StartUp,
|
||||
IntersectionType.PassOverVertical,
|
||||
IntersectionType.StartDown
|
||||
),
|
||||
this
|
||||
);
|
||||
}
|
||||
|
||||
if (EndsAt (x, y))
|
||||
{
|
||||
return new IntersectionDefinition (
|
||||
Start,
|
||||
Length < 0 ? IntersectionType.StartDown : IntersectionType.StartUp,
|
||||
this
|
||||
);
|
||||
return new (
|
||||
p,
|
||||
Length < 0 ? IntersectionType.StartDown : IntersectionType.StartUp,
|
||||
this
|
||||
);
|
||||
}
|
||||
|
||||
int ymin = Math.Min (Start.Y, Start.Y + Length);
|
||||
@@ -182,11 +187,11 @@ public class StraightLine
|
||||
|
||||
if (ymin < y && ymax > y)
|
||||
{
|
||||
return new IntersectionDefinition (
|
||||
new Point (x, y),
|
||||
IntersectionType.PassOverVertical,
|
||||
this
|
||||
);
|
||||
return new (
|
||||
p,
|
||||
IntersectionType.PassOverVertical,
|
||||
this
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -78,7 +78,7 @@ public class Border : Adornment
|
||||
if ((Parent?.Arrangement & ViewArrangement.Movable) != 0)
|
||||
{
|
||||
HighlightStyle |= HighlightStyle.Hover;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
base.BeginInit ();
|
||||
@@ -149,31 +149,32 @@ public class Border : Adornment
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle GetBorderRectangle (Rectangle screenRect)
|
||||
private Rectangle GetBorderRectangle (Rectangle screenRect)
|
||||
{
|
||||
return new (
|
||||
screenRect.X + Math.Max (0, Thickness.Left - 1),
|
||||
screenRect.Y + Math.Max (0, Thickness.Top - 1),
|
||||
Math.Max (
|
||||
0,
|
||||
screenRect.Width
|
||||
- Math.Max (
|
||||
0,
|
||||
Math.Max (0, Thickness.Left - 1)
|
||||
+ Math.Max (0, Thickness.Right - 1)
|
||||
)
|
||||
),
|
||||
Math.Max (
|
||||
0,
|
||||
screenRect.Height
|
||||
- Math.Max (
|
||||
0,
|
||||
Math.Max (0, Thickness.Top - 1)
|
||||
+ Math.Max (0, Thickness.Bottom - 1)
|
||||
)
|
||||
)
|
||||
);
|
||||
screenRect.X + Math.Max (0, Thickness.Left - 1),
|
||||
screenRect.Y + Math.Max (0, Thickness.Top - 1),
|
||||
Math.Max (
|
||||
0,
|
||||
screenRect.Width
|
||||
- Math.Max (
|
||||
0,
|
||||
Math.Max (0, Thickness.Left - 1)
|
||||
+ Math.Max (0, Thickness.Right - 1)
|
||||
)
|
||||
),
|
||||
Math.Max (
|
||||
0,
|
||||
screenRect.Height
|
||||
- Math.Max (
|
||||
0,
|
||||
Math.Max (0, Thickness.Top - 1)
|
||||
+ Math.Max (0, Thickness.Bottom - 1)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the style of the border by changing the <see cref="Thickness"/>. This is a helper API for setting the
|
||||
/// <see cref="Thickness"/> to <c>(1,1,1,1)</c> and setting the line style of the views that comprise the border. If
|
||||
@@ -196,21 +197,22 @@ public class Border : Adornment
|
||||
set => _lineStyle = value;
|
||||
}
|
||||
|
||||
private bool _showTitle = true;
|
||||
private BorderSettings _settings = BorderSettings.Title;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether the title should be shown. The default is <see langword="true"/>.
|
||||
/// Gets or sets the settings for the border.
|
||||
/// </summary>
|
||||
public bool ShowTitle
|
||||
public BorderSettings Settings
|
||||
{
|
||||
get => _showTitle;
|
||||
get => _settings;
|
||||
set
|
||||
{
|
||||
if (value == _showTitle)
|
||||
if (value == _settings)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_showTitle = value;
|
||||
|
||||
_settings = value;
|
||||
|
||||
Parent?.SetNeedsDisplay ();
|
||||
}
|
||||
@@ -225,6 +227,7 @@ public class Border : Adornment
|
||||
if (!Parent.Arrangement.HasFlag (ViewArrangement.Movable))
|
||||
{
|
||||
e.Cancel = true;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -235,9 +238,9 @@ public class Border : Adornment
|
||||
_savedForeColor = ColorScheme.Normal.Foreground;
|
||||
}
|
||||
|
||||
ColorScheme cs = new ColorScheme (ColorScheme)
|
||||
var cs = new ColorScheme (ColorScheme)
|
||||
{
|
||||
Normal = new Attribute (ColorScheme.Normal.Foreground.GetHighlightColor (), ColorScheme.Normal.Background)
|
||||
Normal = new (ColorScheme.Normal.Foreground.GetHighlightColor (), ColorScheme.Normal.Background)
|
||||
};
|
||||
ColorScheme = cs;
|
||||
}
|
||||
@@ -254,12 +257,13 @@ public class Border : Adornment
|
||||
|
||||
if (e.NewValue == HighlightStyle.None && _savedForeColor.HasValue)
|
||||
{
|
||||
ColorScheme cs = new ColorScheme (ColorScheme)
|
||||
var cs = new ColorScheme (ColorScheme)
|
||||
{
|
||||
Normal = new Attribute (_savedForeColor.Value, ColorScheme.Normal.Background)
|
||||
Normal = new (_savedForeColor.Value, ColorScheme.Normal.Background)
|
||||
};
|
||||
ColorScheme = cs;
|
||||
}
|
||||
|
||||
Parent?.SetNeedsDisplay ();
|
||||
e.Cancel = true;
|
||||
}
|
||||
@@ -267,7 +271,7 @@ public class Border : Adornment
|
||||
private Point? _dragPosition;
|
||||
private Point _startGrabPoint;
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <inheritdoc/>
|
||||
protected internal override bool OnMouseEvent (MouseEvent mouseEvent)
|
||||
{
|
||||
if (base.OnMouseEvent (mouseEvent))
|
||||
@@ -322,16 +326,17 @@ public class Border : Adornment
|
||||
|
||||
_dragPosition = mouseEvent.Position;
|
||||
|
||||
Point parentLoc = Parent.SuperView?.ScreenToViewport (new (mouseEvent.ScreenPosition.X, mouseEvent.ScreenPosition.Y)) ?? mouseEvent.ScreenPosition;
|
||||
Point parentLoc = Parent.SuperView?.ScreenToViewport (new (mouseEvent.ScreenPosition.X, mouseEvent.ScreenPosition.Y))
|
||||
?? mouseEvent.ScreenPosition;
|
||||
|
||||
GetLocationEnsuringFullVisibility (
|
||||
Parent,
|
||||
parentLoc.X - _startGrabPoint.X,
|
||||
parentLoc.Y - _startGrabPoint.Y,
|
||||
out int nx,
|
||||
out int ny,
|
||||
out _
|
||||
);
|
||||
Parent,
|
||||
parentLoc.X - _startGrabPoint.X,
|
||||
parentLoc.Y - _startGrabPoint.Y,
|
||||
out int nx,
|
||||
out int ny,
|
||||
out _
|
||||
);
|
||||
|
||||
Parent.X = nx;
|
||||
Parent.Y = ny;
|
||||
@@ -352,7 +357,6 @@ public class Border : Adornment
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void Dispose (bool disposing)
|
||||
{
|
||||
@@ -403,7 +407,7 @@ public class Border : Adornment
|
||||
// ...thickness extends outward (border/title is always as far in as possible)
|
||||
// PERF: How about a call to Rectangle.Offset?
|
||||
|
||||
var borderBounds = GetBorderRectangle (screenBounds);
|
||||
Rectangle borderBounds = GetBorderRectangle (screenBounds);
|
||||
int topTitleLineY = borderBounds.Y;
|
||||
int titleY = borderBounds.Y;
|
||||
var titleBarsLength = 0; // the little vertical thingies
|
||||
@@ -421,7 +425,7 @@ public class Border : Adornment
|
||||
int sideLineLength = borderBounds.Height;
|
||||
bool canDrawBorder = borderBounds is { Width: > 0, Height: > 0 };
|
||||
|
||||
if (ShowTitle)
|
||||
if (Settings.FastHasFlags (BorderSettings.Title))
|
||||
{
|
||||
if (Thickness.Top == 2)
|
||||
{
|
||||
@@ -453,9 +457,10 @@ public class Border : Adornment
|
||||
}
|
||||
}
|
||||
|
||||
if (canDrawBorder && Thickness.Top > 0 && maxTitleWidth > 0 && ShowTitle && !string.IsNullOrEmpty (Parent?.Title))
|
||||
if (canDrawBorder && Thickness.Top > 0 && maxTitleWidth > 0 && Settings.FastHasFlags (BorderSettings.Title) && !string.IsNullOrEmpty (Parent?.Title))
|
||||
{
|
||||
var focus = Parent.GetNormalColor ();
|
||||
Attribute focus = Parent.GetNormalColor ();
|
||||
|
||||
if (Parent.SuperView is { } && Parent.SuperView?.Subviews!.Count (s => s.CanFocus) > 1)
|
||||
{
|
||||
// Only use focus color if there are multiple focusable views
|
||||
@@ -492,7 +497,7 @@ public class Border : Adornment
|
||||
{
|
||||
// ╔╡Title╞═════╗
|
||||
// ╔╡╞═════╗
|
||||
if (borderBounds.Width < 4 || !ShowTitle || string.IsNullOrEmpty (Parent?.Title))
|
||||
if (borderBounds.Width < 4 || !Settings.FastHasFlags (BorderSettings.Title) || string.IsNullOrEmpty (Parent?.Title))
|
||||
{
|
||||
// ╔╡╞╗ should be ╔══╗
|
||||
lc.AddLine (
|
||||
@@ -631,7 +636,7 @@ public class Border : Adornment
|
||||
Driver.SetAttribute (prevAttr);
|
||||
|
||||
// TODO: This should be moved to LineCanvas as a new BorderStyle.Ruler
|
||||
if (View.Diagnostics.HasFlag (ViewDiagnosticFlags.Ruler))
|
||||
if (Diagnostics.HasFlag (ViewDiagnosticFlags.Ruler))
|
||||
{
|
||||
// Top
|
||||
var hruler = new Ruler { Length = screenBounds.Width, Orientation = Orientation.Horizontal };
|
||||
@@ -642,7 +647,7 @@ public class Border : Adornment
|
||||
}
|
||||
|
||||
// Redraw title
|
||||
if (drawTop && maxTitleWidth > 0 && ShowTitle)
|
||||
if (drawTop && maxTitleWidth > 0 && Settings.FastHasFlags (BorderSettings.Title))
|
||||
{
|
||||
Parent.TitleTextFormatter.Draw (
|
||||
new (borderBounds.X + 2, titleY, maxTitleWidth, 1),
|
||||
@@ -670,6 +675,45 @@ public class Border : Adornment
|
||||
vruler.Draw (new (screenBounds.X + screenBounds.Width - 1, screenBounds.Y + 1), 1);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: This should not be done on each draw?
|
||||
if (Settings.FastHasFlags (BorderSettings.Gradient))
|
||||
{
|
||||
SetupGradientLineCanvas (lc, screenBounds);
|
||||
}
|
||||
else
|
||||
{
|
||||
lc.Fill = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void SetupGradientLineCanvas (LineCanvas lc, Rectangle rect)
|
||||
{
|
||||
GetAppealingGradientColors (out List<Color> stops, out List<int> steps);
|
||||
|
||||
var g = new Gradient (stops, steps);
|
||||
|
||||
var fore = new GradientFill (rect, g, GradientDirection.Diagonal);
|
||||
var back = new SolidFill (GetNormalColor ().Background);
|
||||
|
||||
lc.Fill = new (fore, back);
|
||||
}
|
||||
|
||||
private static void GetAppealingGradientColors (out List<Color> stops, out List<int> steps)
|
||||
{
|
||||
// Define the colors of the gradient stops with more appealing colors
|
||||
stops = new()
|
||||
{
|
||||
new (0, 128, 255), // Bright Blue
|
||||
new (0, 255, 128), // Bright Green
|
||||
new (255, 255), // Bright Yellow
|
||||
new (255, 128), // Bright Orange
|
||||
new (255, 0, 128) // Bright Pink
|
||||
};
|
||||
|
||||
// Define the number of steps between each color for smoother transitions
|
||||
// If we pass only a single value then it will assume equal steps between all pairs
|
||||
steps = new() { 15 };
|
||||
}
|
||||
}
|
||||
|
||||
26
Terminal.Gui/View/Adornment/BorderSettings.cs
Normal file
26
Terminal.Gui/View/Adornment/BorderSettings.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
using Terminal.Gui.Analyzers.Internal.Attributes;
|
||||
|
||||
namespace Terminal.Gui;
|
||||
|
||||
/// <summary>
|
||||
/// Determines the settings for <see cref="Border"/>.
|
||||
/// </summary>
|
||||
[Flags]
|
||||
[GenerateEnumExtensionMethods (FastHasFlags = true)]
|
||||
public enum BorderSettings
|
||||
{
|
||||
/// <summary>
|
||||
/// No settings.
|
||||
/// </summary>
|
||||
None = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Show the title.
|
||||
/// </summary>
|
||||
Title = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Use <see cref="GradientFill"/> to draw the border.
|
||||
/// </summary>
|
||||
Gradient = 2,
|
||||
}
|
||||
@@ -223,7 +223,7 @@ public class Margin : Adornment
|
||||
if (ShadowStyle != ShadowStyle.None && _rightShadow is { } && _bottomShadow is { })
|
||||
{
|
||||
_rightShadow.Y = Parent.Border.Thickness.Top > 0
|
||||
? Parent.Border.Thickness.Top - (Parent.Border.Thickness.Top > 2 && Parent.Border.ShowTitle ? 1 : 0)
|
||||
? Parent.Border.Thickness.Top - (Parent.Border.Thickness.Top > 2 && Parent.Border.Settings.FastHasFlags (BorderSettings.Title) ? 1 : 0)
|
||||
: 1;
|
||||
_bottomShadow.X = Parent.Border.Thickness.Left > 0 ? Parent.Border.Thickness.Left : 1;
|
||||
}
|
||||
|
||||
@@ -104,7 +104,7 @@ public class Shortcut : View
|
||||
void OnInitialized (object sender, EventArgs e)
|
||||
{
|
||||
SuperViewRendersLineCanvas = true;
|
||||
Border.ShowTitle = false;
|
||||
Border.Settings &= ~BorderSettings.Title;
|
||||
|
||||
ShowHide ();
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ namespace UICatalog.Scenarios;
|
||||
[ScenarioMetadata ("Animation", "Demonstration of how to render animated images with threading.")]
|
||||
[ScenarioCategory ("Threading")]
|
||||
[ScenarioCategory ("Drawing")]
|
||||
public class Animation : Scenario
|
||||
public class AnimationScenario : Scenario
|
||||
{
|
||||
private bool _isDisposed;
|
||||
|
||||
@@ -9,29 +9,30 @@ public class BorderEditor : AdornmentEditor
|
||||
{
|
||||
private CheckBox _ckbTitle;
|
||||
private RadioGroup _rbBorderStyle;
|
||||
private CheckBox _ckbGradient;
|
||||
|
||||
public BorderEditor ()
|
||||
{
|
||||
Title = "_Border";
|
||||
Initialized += BorderEditor_Initialized;
|
||||
AdornmentChanged += BorderEditor_AdornmentChanged;
|
||||
|
||||
}
|
||||
|
||||
private void BorderEditor_AdornmentChanged (object sender, EventArgs e)
|
||||
{
|
||||
_ckbTitle.State = ((Border)AdornmentToEdit).ShowTitle ? CheckState.Checked : CheckState.UnChecked;
|
||||
_ckbTitle.State = ((Border)AdornmentToEdit).Settings.FastHasFlags (BorderSettings.Title) ? CheckState.Checked : CheckState.UnChecked;
|
||||
_rbBorderStyle.SelectedItem = (int)((Border)AdornmentToEdit).LineStyle;
|
||||
_ckbGradient.State = ((Border)AdornmentToEdit).Settings.FastHasFlags (BorderSettings.Gradient) ? CheckState.Checked : CheckState.UnChecked;
|
||||
}
|
||||
|
||||
private void BorderEditor_Initialized (object sender, EventArgs e)
|
||||
{
|
||||
|
||||
List<LineStyle> borderStyleEnum = Enum.GetValues (typeof (LineStyle)).Cast<LineStyle> ().ToList ();
|
||||
|
||||
_rbBorderStyle = new RadioGroup
|
||||
_rbBorderStyle = new()
|
||||
{
|
||||
X = 0,
|
||||
|
||||
// BUGBUG: Hack until dimauto is working properly
|
||||
Y = Pos.Bottom (Subviews [^1]),
|
||||
Width = Dim.Width (Subviews [^2]) + Dim.Width (Subviews [^1]) - 1,
|
||||
@@ -46,21 +47,34 @@ public class BorderEditor : AdornmentEditor
|
||||
|
||||
_rbBorderStyle.SelectedItemChanged += OnRbBorderStyleOnSelectedItemChanged;
|
||||
|
||||
_ckbTitle = new CheckBox
|
||||
_ckbTitle = new()
|
||||
{
|
||||
X = 0,
|
||||
Y = Pos.Bottom (_rbBorderStyle),
|
||||
|
||||
State = CheckState.Checked,
|
||||
SuperViewRendersLineCanvas = true,
|
||||
Text = "Show Title",
|
||||
Text = "Title",
|
||||
Enabled = AdornmentToEdit is { }
|
||||
};
|
||||
|
||||
|
||||
_ckbTitle.Toggle += OnCkbTitleOnToggle;
|
||||
Add (_ckbTitle);
|
||||
|
||||
_ckbGradient = new ()
|
||||
{
|
||||
X = 0,
|
||||
Y = Pos.Bottom (_ckbTitle),
|
||||
|
||||
State = CheckState.Checked,
|
||||
SuperViewRendersLineCanvas = true,
|
||||
Text = "Gradient",
|
||||
Enabled = AdornmentToEdit is { }
|
||||
};
|
||||
|
||||
_ckbGradient.Toggle += OnCkbGradientOnToggle;
|
||||
Add (_ckbGradient);
|
||||
|
||||
return;
|
||||
|
||||
void OnRbBorderStyleOnSelectedItemChanged (object s, SelectedItemChangedArgs e)
|
||||
@@ -81,6 +95,32 @@ public class BorderEditor : AdornmentEditor
|
||||
LayoutSubviews ();
|
||||
}
|
||||
|
||||
void OnCkbTitleOnToggle (object sender, CancelEventArgs<CheckState> args) { ((Border)AdornmentToEdit).ShowTitle = args.NewValue == CheckState.Checked; }
|
||||
void OnCkbTitleOnToggle (object sender, CancelEventArgs<CheckState> args)
|
||||
{
|
||||
if (args.NewValue == CheckState.Checked)
|
||||
|
||||
{
|
||||
((Border)AdornmentToEdit).Settings |= BorderSettings.Title;
|
||||
}
|
||||
else
|
||||
|
||||
{
|
||||
((Border)AdornmentToEdit).Settings &= ~BorderSettings.Title;
|
||||
}
|
||||
}
|
||||
|
||||
void OnCkbGradientOnToggle (object sender, CancelEventArgs<CheckState> args)
|
||||
{
|
||||
if (args.NewValue == CheckState.Checked)
|
||||
|
||||
{
|
||||
((Border)AdornmentToEdit).Settings |= BorderSettings.Gradient;
|
||||
}
|
||||
else
|
||||
|
||||
{
|
||||
((Border)AdornmentToEdit).Settings &= ~BorderSettings.Gradient;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
264
UICatalog/Scenarios/TextEffectsScenario.cs
Normal file
264
UICatalog/Scenarios/TextEffectsScenario.cs
Normal file
@@ -0,0 +1,264 @@
|
||||
using System.Collections.Generic;
|
||||
using Terminal.Gui;
|
||||
|
||||
namespace UICatalog.Scenarios;
|
||||
|
||||
[ScenarioMetadata ("Text Effects", "Text Effects.")]
|
||||
[ScenarioCategory ("Colors")]
|
||||
[ScenarioCategory ("Text and Formatting")]
|
||||
public class TextEffectsScenario : Scenario
|
||||
{
|
||||
private TabView _tabView;
|
||||
|
||||
/// <summary>
|
||||
/// Enable or disable looping of the gradient colors.
|
||||
/// </summary>
|
||||
public static bool LoopingGradient;
|
||||
|
||||
public override void Main ()
|
||||
{
|
||||
Application.Init ();
|
||||
|
||||
var w = new Window
|
||||
{
|
||||
Width = Dim.Fill (),
|
||||
Height = Dim.Fill (),
|
||||
Title = "Text Effects Scenario"
|
||||
};
|
||||
|
||||
w.Loaded += (s, e) => { SetupGradientLineCanvas (w, w.Frame.Size); };
|
||||
|
||||
w.SizeChanging += (s, e) =>
|
||||
{
|
||||
if (e.Size.HasValue)
|
||||
{
|
||||
SetupGradientLineCanvas (w, e.Size.Value);
|
||||
}
|
||||
};
|
||||
|
||||
w.ColorScheme = new ()
|
||||
{
|
||||
Normal = new (ColorName.White, ColorName.Black),
|
||||
Focus = new (ColorName.Black, ColorName.White),
|
||||
HotNormal = new (ColorName.White, ColorName.Black),
|
||||
HotFocus = new (ColorName.White, ColorName.Black),
|
||||
Disabled = new (ColorName.Gray, ColorName.Black)
|
||||
};
|
||||
|
||||
// Creates a window that occupies the entire terminal with a title.
|
||||
_tabView = new ()
|
||||
{
|
||||
Width = Dim.Fill (),
|
||||
Height = Dim.Fill ()
|
||||
};
|
||||
|
||||
var gradientsView = new GradientsView
|
||||
{
|
||||
Width = Dim.Fill (),
|
||||
Height = Dim.Fill ()
|
||||
};
|
||||
|
||||
var t1 = new Tab
|
||||
{
|
||||
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);
|
||||
|
||||
Application.Run (w);
|
||||
w.Dispose ();
|
||||
|
||||
Application.Shutdown ();
|
||||
Dispose ();
|
||||
}
|
||||
|
||||
private static void SetupGradientLineCanvas (View w, Size size)
|
||||
{
|
||||
GetAppealingGradientColors (out List<Color> stops, out List<int> steps);
|
||||
|
||||
var g = new Gradient (stops, steps, LoopingGradient);
|
||||
|
||||
var fore = new GradientFill (
|
||||
new (0, 0, size.Width, size.Height),
|
||||
g,
|
||||
GradientDirection.Diagonal);
|
||||
var back = new SolidFill (new (ColorName.Black));
|
||||
|
||||
w.LineCanvas.Fill = new (
|
||||
fore,
|
||||
back);
|
||||
}
|
||||
|
||||
public static void GetAppealingGradientColors (out List<Color> stops, out List<int> steps)
|
||||
{
|
||||
// Define the colors of the gradient stops with more appealing colors
|
||||
stops =
|
||||
[
|
||||
new (0, 128, 255), // Bright Blue
|
||||
new (0, 255, 128), // Bright Green
|
||||
new (255, 255), // Bright Yellow
|
||||
new (255, 128), // Bright Orange
|
||||
new (255, 0, 128)
|
||||
];
|
||||
|
||||
// Define the number of steps between each color for smoother transitions
|
||||
// If we pass only a single value then it will assume equal steps between all pairs
|
||||
steps = [15];
|
||||
}
|
||||
}
|
||||
|
||||
internal class GradientsView : View
|
||||
{
|
||||
private const int GRADIENT_WIDTH = 30;
|
||||
private const int GRADIENT_HEIGHT = 15;
|
||||
private const int LABEL_HEIGHT = 1;
|
||||
private const int GRADIENT_WITH_LABEL_HEIGHT = GRADIENT_HEIGHT + LABEL_HEIGHT + 1; // +1 for spacing
|
||||
|
||||
public override void OnDrawContent (Rectangle viewport)
|
||||
{
|
||||
base.OnDrawContent (viewport);
|
||||
|
||||
DrawTopLineGradient (viewport);
|
||||
|
||||
var x = 2;
|
||||
var y = 3;
|
||||
|
||||
List<(string Label, GradientDirection Direction)> gradients = new ()
|
||||
{
|
||||
("Horizontal", GradientDirection.Horizontal),
|
||||
("Vertical", GradientDirection.Vertical),
|
||||
("Radial", GradientDirection.Radial),
|
||||
("Diagonal", GradientDirection.Diagonal)
|
||||
};
|
||||
|
||||
foreach ((string label, GradientDirection direction) in gradients)
|
||||
{
|
||||
if (x + GRADIENT_WIDTH > viewport.Width)
|
||||
{
|
||||
x = 2; // Reset to left margin
|
||||
y += GRADIENT_WITH_LABEL_HEIGHT; // Move down to next row
|
||||
}
|
||||
|
||||
DrawLabeledGradientArea (label, direction, x, y);
|
||||
x += GRADIENT_WIDTH + 2; // Move right for next gradient, +2 for spacing
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawLabeledGradientArea (string label, GradientDirection direction, int xOffset, int yOffset)
|
||||
{
|
||||
DrawGradientArea (direction, xOffset, yOffset);
|
||||
CenterText (label, xOffset, yOffset + GRADIENT_HEIGHT); // Adjusted for text below the gradient
|
||||
}
|
||||
|
||||
private void CenterText (string text, int xOffset, int yOffset)
|
||||
{
|
||||
if (yOffset + 1 >= Viewport.Height)
|
||||
{
|
||||
// Not enough space for label
|
||||
return;
|
||||
}
|
||||
|
||||
int width = text.Length;
|
||||
int x = xOffset + (GRADIENT_WIDTH - width) / 2; // Center the text within the gradient area width
|
||||
Driver.SetAttribute (GetNormalColor ());
|
||||
Move (x, yOffset + 1);
|
||||
Driver.AddStr (text);
|
||||
}
|
||||
|
||||
private void DrawGradientArea (GradientDirection direction, int xOffset, int yOffset)
|
||||
{
|
||||
// Define the colors of the gradient stops
|
||||
List<Color> stops =
|
||||
[
|
||||
new (255, 0), // Red
|
||||
new (0, 255), // Green
|
||||
new (238, 130, 238)
|
||||
];
|
||||
|
||||
// Define the number of steps between each color
|
||||
List<int> steps = [10, 10]; // 10 steps between Red -> Green, and Green -> Blue
|
||||
|
||||
// Create the gradient
|
||||
var radialGradient = new Gradient (stops, steps, TextEffectsScenario.LoopingGradient);
|
||||
|
||||
// Define the size of the rectangle
|
||||
int maxRow = GRADIENT_HEIGHT; // Adjusted to keep aspect ratio
|
||||
int maxColumn = GRADIENT_WIDTH;
|
||||
|
||||
// Build the coordinate-color mapping for a radial gradient
|
||||
Dictionary<Point, Color> gradientMapping = radialGradient.BuildCoordinateColorMapping (maxRow, maxColumn, direction);
|
||||
|
||||
// Print the gradient
|
||||
for (var row = 0; row <= maxRow; row++)
|
||||
{
|
||||
for (var col = 0; col <= maxColumn; col++)
|
||||
{
|
||||
var coord = new Point (col, row);
|
||||
Color color = gradientMapping [coord];
|
||||
|
||||
SetColor (color);
|
||||
|
||||
AddRune (col + xOffset, row + yOffset, new ('█'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawTopLineGradient (Rectangle viewport)
|
||||
{
|
||||
// Define the colors of the rainbow
|
||||
List<Color> stops =
|
||||
[
|
||||
new (255, 0), // Red
|
||||
new (255, 165), // Orange
|
||||
new (255, 255), // Yellow
|
||||
new (0, 128), // Green
|
||||
new (0, 0, 255), // Blue
|
||||
new (75, 0, 130), // Indigo
|
||||
new (238, 130, 238)
|
||||
];
|
||||
|
||||
// Define the number of steps between each color
|
||||
List<int> steps =
|
||||
[
|
||||
20, // between Red and Orange
|
||||
20, // between Orange and Yellow
|
||||
20, // between Yellow and Green
|
||||
20, // between Green and Blue
|
||||
20, // between Blue and Indigo
|
||||
20
|
||||
];
|
||||
|
||||
// Create the gradient
|
||||
var rainbowGradient = new Gradient (stops, steps, TextEffectsScenario.LoopingGradient);
|
||||
|
||||
for (var x = 0; x < viewport.Width; x++)
|
||||
{
|
||||
double fraction = (double)x / (viewport.Width - 1);
|
||||
Color color = rainbowGradient.GetColorAtFraction (fraction);
|
||||
|
||||
SetColor (color);
|
||||
|
||||
AddRune (x, 0, new ('█'));
|
||||
}
|
||||
}
|
||||
|
||||
private static void SetColor (Color color) { Application.Driver.SetAttribute (new (color, color)); }
|
||||
}
|
||||
29
UnitTests/Drawing/FillPairTests.cs
Normal file
29
UnitTests/Drawing/FillPairTests.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Terminal.Gui.DrawingTests;
|
||||
|
||||
public class FillPairTests
|
||||
{
|
||||
[Fact]
|
||||
public void GetAttribute_ReturnsCorrectColors ()
|
||||
{
|
||||
// Arrange
|
||||
var foregroundColor = new Color (100, 150, 200);
|
||||
var backgroundColor = new Color (50, 75, 100);
|
||||
var foregroundFill = new SolidFill (foregroundColor);
|
||||
var backgroundFill = new SolidFill (backgroundColor);
|
||||
|
||||
var fillPair = new FillPair (foregroundFill, backgroundFill);
|
||||
|
||||
// Act
|
||||
Attribute resultAttribute = fillPair.GetAttribute (new (0, 0));
|
||||
|
||||
// Assert
|
||||
Assert.Equal (foregroundColor, resultAttribute.Foreground);
|
||||
Assert.Equal (backgroundColor, resultAttribute.Background);
|
||||
}
|
||||
}
|
||||
118
UnitTests/Drawing/GradientFillTests.cs
Normal file
118
UnitTests/Drawing/GradientFillTests.cs
Normal file
@@ -0,0 +1,118 @@
|
||||
namespace Terminal.Gui.DrawingTests;
|
||||
|
||||
public class GradientFillTests
|
||||
{
|
||||
private readonly Gradient _gradient;
|
||||
|
||||
public GradientFillTests ()
|
||||
{
|
||||
// Define the colors of the gradient stops
|
||||
List<Color> stops = new List<Color>
|
||||
{
|
||||
new (255, 0), // Red
|
||||
new (0, 0, 255) // Blue
|
||||
};
|
||||
|
||||
// Define the number of steps between each color
|
||||
List<int> steps = new() { 10 }; // 10 steps between Red -> Blue
|
||||
|
||||
_gradient = new (stops, steps);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestGradientFillCorners_AtOrigin ()
|
||||
{
|
||||
var area = new Rectangle (0, 0, 10, 10);
|
||||
var gradientFill = new GradientFill (area, _gradient, GradientDirection.Diagonal);
|
||||
|
||||
// Test the corners
|
||||
var topLeft = new Point (0, 0);
|
||||
var topRight = new Point (area.Width - 1, 0);
|
||||
var bottomLeft = new Point (0, area.Height - 1);
|
||||
var bottomRight = new Point (area.Width - 1, area.Height - 1);
|
||||
|
||||
Color topLeftColor = gradientFill.GetColor (topLeft);
|
||||
Color topRightColor = gradientFill.GetColor (topRight);
|
||||
Color bottomLeftColor = gradientFill.GetColor (bottomLeft);
|
||||
Color bottomRightColor = gradientFill.GetColor (bottomRight);
|
||||
|
||||
// Expected colors
|
||||
var expectedTopLeftColor = new Color (255, 0); // Red
|
||||
var expectedBottomRightColor = new Color (0, 0, 255); // Blue
|
||||
|
||||
Assert.Equal (expectedTopLeftColor, topLeftColor);
|
||||
Assert.Equal (expectedBottomRightColor, bottomRightColor);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestGradientFillCorners_NotAtOrigin ()
|
||||
{
|
||||
var area = new Rectangle (5, 5, 10, 10);
|
||||
var gradientFill = new GradientFill (area, _gradient, GradientDirection.Diagonal);
|
||||
|
||||
// Test the corners
|
||||
var topLeft = new Point (5, 5);
|
||||
var topRight = new Point (area.Right - 1, 5);
|
||||
var bottomLeft = new Point (5, area.Bottom - 1);
|
||||
var bottomRight = new Point (area.Right - 1, area.Bottom - 1);
|
||||
|
||||
Color topLeftColor = gradientFill.GetColor (topLeft);
|
||||
Color topRightColor = gradientFill.GetColor (topRight);
|
||||
Color bottomLeftColor = gradientFill.GetColor (bottomLeft);
|
||||
Color bottomRightColor = gradientFill.GetColor (bottomRight);
|
||||
|
||||
// Expected colors
|
||||
var expectedTopLeftColor = new Color (255, 0); // Red
|
||||
var expectedBottomRightColor = new Color (0, 0, 255); // Blue
|
||||
|
||||
Assert.Equal (expectedTopLeftColor, topLeftColor);
|
||||
Assert.Equal (expectedBottomRightColor, bottomRightColor);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestGradientFillColorTransition ()
|
||||
{
|
||||
var area = new Rectangle (0, 0, 10, 10);
|
||||
var gradientFill = new GradientFill (area, _gradient, GradientDirection.Diagonal);
|
||||
|
||||
for (var row = 0; row < area.Height; row++)
|
||||
{
|
||||
var previousRed = 255;
|
||||
var previousBlue = 0;
|
||||
|
||||
for (var col = 0; col < area.Width; col++)
|
||||
{
|
||||
var point = new Point (col, row);
|
||||
Color color = gradientFill.GetColor (point);
|
||||
|
||||
// Check if the current color is 'more blue' and 'less red' as it goes right and down
|
||||
Assert.True (color.R <= previousRed, $"Failed at ({col}, {row}): {color.R} > {previousRed}");
|
||||
Assert.True (color.B >= previousBlue, $"Failed at ({col}, {row}): {color.B} < {previousBlue}");
|
||||
|
||||
// Update the previous color values for the next iteration
|
||||
previousRed = color.R;
|
||||
previousBlue = color.B;
|
||||
}
|
||||
}
|
||||
|
||||
for (var col = 0; col < area.Width; col++)
|
||||
{
|
||||
var previousRed = 255;
|
||||
var previousBlue = 0;
|
||||
|
||||
for (var row = 0; row < area.Height; row++)
|
||||
{
|
||||
var point = new Point (col, row);
|
||||
Color color = gradientFill.GetColor (point);
|
||||
|
||||
// Check if the current color is 'more blue' and 'less red' as it goes right and down
|
||||
Assert.True (color.R <= previousRed, $"Failed at ({col}, {row}): {color.R} > {previousRed}");
|
||||
Assert.True (color.B >= previousBlue, $"Failed at ({col}, {row}): {color.B} < {previousBlue}");
|
||||
|
||||
// Update the previous color values for the next iteration
|
||||
previousRed = color.R;
|
||||
previousBlue = color.B;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
173
UnitTests/Drawing/GradientTests.cs
Normal file
173
UnitTests/Drawing/GradientTests.cs
Normal file
@@ -0,0 +1,173 @@
|
||||
namespace Terminal.Gui.DrawingTests;
|
||||
|
||||
public class GradientTests
|
||||
{
|
||||
// Static method to provide all enum values
|
||||
public static IEnumerable<object []> GradientDirectionValues ()
|
||||
{
|
||||
return typeof (GradientDirection).GetEnumValues ()
|
||||
.Cast<GradientDirection> ()
|
||||
.Select (direction => new object [] { direction });
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData (nameof (GradientDirectionValues))]
|
||||
public void GradientIsInclusive_2_by_2 (GradientDirection direction)
|
||||
{
|
||||
// Define the colors of the gradient stops
|
||||
List<Color> stops = new()
|
||||
{
|
||||
new (255, 0), // Red
|
||||
new (0, 0, 255) // Blue
|
||||
};
|
||||
|
||||
// Define the number of steps between each color
|
||||
List<int> steps = new() { 10 }; // 10 steps between Red -> Blue
|
||||
|
||||
var g = new Gradient (stops, steps);
|
||||
Assert.Equal (4, g.BuildCoordinateColorMapping (1, 1, direction).Count);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData (nameof (GradientDirectionValues))]
|
||||
public void GradientIsInclusive_1_by_1 (GradientDirection direction)
|
||||
{
|
||||
// Define the colors of the gradient stops
|
||||
List<Color> stops = new()
|
||||
{
|
||||
new (255, 0), // Red
|
||||
new (0, 0, 255) // Blue
|
||||
};
|
||||
|
||||
// Define the number of steps between each color
|
||||
List<int> steps = new() { 10 }; // 10 steps between Red -> Blue
|
||||
|
||||
var g = new Gradient (stops, steps);
|
||||
|
||||
// Note that maxRow and maxCol are inclusive so this results in 1x1 area i.e. a single cell.
|
||||
KeyValuePair<Point, Color> c = Assert.Single (g.BuildCoordinateColorMapping (0, 0, direction));
|
||||
Assert.Equal (c.Key, new (0, 0));
|
||||
Assert.Equal (c.Value, new (0, 0, 255));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SingleColorStop ()
|
||||
{
|
||||
List<Color> stops = new() { new (255, 0) }; // Red
|
||||
List<int> steps = new ();
|
||||
|
||||
var g = new Gradient (stops, steps);
|
||||
Assert.All (g.Spectrum, color => Assert.Equal (new (255, 0), color));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LoopingGradient_CorrectColors ()
|
||||
{
|
||||
List<Color> stops = new()
|
||||
{
|
||||
new (255, 0), // Red
|
||||
new (0, 0, 255) // Blue
|
||||
};
|
||||
|
||||
List<int> steps = new() { 10 };
|
||||
|
||||
var g = new Gradient (stops, steps, true);
|
||||
Assert.Equal (new (255, 0), g.Spectrum.First ());
|
||||
Assert.Equal (new (255, 0), g.Spectrum.Last ());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DifferentStepSizes ()
|
||||
{
|
||||
List<Color> stops = new List<Color>
|
||||
{
|
||||
new (255, 0), // Red
|
||||
new (0, 255), // Green
|
||||
new (0, 0, 255) // Blue
|
||||
};
|
||||
|
||||
List<int> steps = new() { 5, 15 }; // Different steps
|
||||
|
||||
var g = new Gradient (stops, steps);
|
||||
Assert.Equal (22, g.Spectrum.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FractionOutOfRange_ThrowsException ()
|
||||
{
|
||||
List<Color> stops = new()
|
||||
{
|
||||
new (255, 0), // Red
|
||||
new (0, 0, 255) // Blue
|
||||
};
|
||||
|
||||
List<int> steps = new() { 10 };
|
||||
|
||||
var g = new Gradient (stops, steps);
|
||||
|
||||
Assert.Throws<ArgumentOutOfRangeException> (() => g.GetColorAtFraction (-0.1));
|
||||
Assert.Throws<ArgumentOutOfRangeException> (() => g.GetColorAtFraction (1.1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NaNFraction_ReturnsLastColor ()
|
||||
{
|
||||
List<Color> stops = new()
|
||||
{
|
||||
new (255, 0), // Red
|
||||
new (0, 0, 255) // Blue
|
||||
};
|
||||
|
||||
List<int> steps = new() { 10 };
|
||||
|
||||
var g = new Gradient (stops, steps);
|
||||
Assert.Equal (new (0, 0, 255), g.GetColorAtFraction (double.NaN));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_SingleStepProvided_ReplicatesForAllPairs ()
|
||||
{
|
||||
List<Color> stops = new List<Color>
|
||||
{
|
||||
new (255, 0), // Red
|
||||
new (0, 255), // Green
|
||||
new (0, 0, 255) // Blue
|
||||
};
|
||||
|
||||
List<int> singleStep = new() { 5 }; // Single step provided
|
||||
var gradient = new Gradient (stops, singleStep);
|
||||
|
||||
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 ()
|
||||
{
|
||||
List<Color> stops = new()
|
||||
{
|
||||
new (255, 0), // Red
|
||||
new (0, 0, 255) // Blue
|
||||
};
|
||||
|
||||
List<int> invalidSteps = new() { 5, 5 }; // Invalid length (N-1 expected)
|
||||
Assert.Throws<ArgumentException> (() => new Gradient (stops, invalidSteps));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_ValidStepsLength_DoesNotThrow ()
|
||||
{
|
||||
List<Color> stops = new List<Color>
|
||||
{
|
||||
new (255, 0), // Red
|
||||
new (0, 255), // Green
|
||||
new (0, 0, 255) // Blue
|
||||
};
|
||||
|
||||
List<int> validSteps = new() { 5, 5 }; // Valid length (N-1)
|
||||
var gradient = new Gradient (stops, validSteps);
|
||||
|
||||
Assert.NotNull (gradient.Spectrum);
|
||||
Assert.Equal (12, gradient.Spectrum.Count); // 5 steps Red -> Green + 5 steps Green -> Blue + 2 end colors
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ using Xunit.Abstractions;
|
||||
|
||||
namespace Terminal.Gui.DrawingTests;
|
||||
|
||||
public class LineCanvasTests (ITestOutputHelper output)
|
||||
public class LineCanvasTests (ITestOutputHelper _output)
|
||||
{
|
||||
[Theory]
|
||||
|
||||
@@ -294,7 +294,7 @@ public class LineCanvasTests (ITestOutputHelper output)
|
||||
lc.AddLine (new (x1, y1), len1, o1, s1);
|
||||
lc.AddLine (new (x2, y2), len2, o2, s2);
|
||||
|
||||
TestHelpers.AssertEqual (output, expected, lc.ToString ());
|
||||
TestHelpers.AssertEqual (_output, expected, lc.ToString ());
|
||||
v.Dispose ();
|
||||
}
|
||||
|
||||
@@ -503,7 +503,7 @@ public class LineCanvasTests (ITestOutputHelper output)
|
||||
Assert.Equal (new (x, y, 4, 2), lc.Viewport);
|
||||
|
||||
TestHelpers.AssertEqual (
|
||||
output,
|
||||
_output,
|
||||
@"
|
||||
╔╡╞╗
|
||||
║ ║",
|
||||
@@ -553,7 +553,7 @@ public class LineCanvasTests (ITestOutputHelper output)
|
||||
Assert.Equal (new (x, y, 4, 2), lc.Viewport);
|
||||
|
||||
TestHelpers.AssertEqual (
|
||||
output,
|
||||
_output,
|
||||
@"
|
||||
╔╡╞╗
|
||||
║ ║",
|
||||
@@ -596,7 +596,7 @@ public class LineCanvasTests (ITestOutputHelper output)
|
||||
|
||||
// Add a line at 5, 5 that's has length of 1
|
||||
canvas.AddLine (new (x, y), 1, orientation, LineStyle.Single);
|
||||
TestHelpers.AssertEqual (output, $"{expected}", $"{canvas}");
|
||||
TestHelpers.AssertEqual (_output, $"{expected}", $"{canvas}");
|
||||
}
|
||||
|
||||
// X is offset by 2
|
||||
@@ -653,7 +653,7 @@ public class LineCanvasTests (ITestOutputHelper output)
|
||||
canvas.AddLine (new (x, y), length, orientation, LineStyle.Single);
|
||||
|
||||
var result = canvas.ToString ();
|
||||
TestHelpers.AssertEqual (output, expected, result);
|
||||
TestHelpers.AssertEqual (_output, expected, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -680,7 +680,7 @@ public class LineCanvasTests (ITestOutputHelper output)
|
||||
|
||||
// Add a line at 0, 0 that's has length of 0
|
||||
lc.AddLine (Point.Empty, 0, orientation, LineStyle.Single);
|
||||
TestHelpers.AssertEqual (output, expected, $"{lc}");
|
||||
TestHelpers.AssertEqual (_output, expected, $"{lc}");
|
||||
}
|
||||
|
||||
[InlineData (Orientation.Horizontal, "┼")]
|
||||
@@ -701,7 +701,7 @@ public class LineCanvasTests (ITestOutputHelper output)
|
||||
|
||||
// Add a line at 0, 0 that's has length of 0
|
||||
lc.AddLine (Point.Empty, 0, orientation, LineStyle.Single);
|
||||
TestHelpers.AssertEqual (output, expected, $"{lc}");
|
||||
TestHelpers.AssertEqual (_output, expected, $"{lc}");
|
||||
}
|
||||
|
||||
[InlineData (Orientation.Horizontal, "╥")]
|
||||
@@ -724,7 +724,7 @@ public class LineCanvasTests (ITestOutputHelper output)
|
||||
|
||||
// Add a line at 0, 0 that's has length of 0
|
||||
lc.AddLine (Point.Empty, 0, orientation, LineStyle.Single);
|
||||
TestHelpers.AssertEqual (output, expected, $"{lc}");
|
||||
TestHelpers.AssertEqual (_output, expected, $"{lc}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -740,7 +740,7 @@ public class LineCanvasTests (ITestOutputHelper output)
|
||||
@"
|
||||
┌─
|
||||
│ ";
|
||||
TestHelpers.AssertEqual (output, looksLike, $"{Environment.NewLine}{canvas}");
|
||||
TestHelpers.AssertEqual (_output, looksLike, $"{Environment.NewLine}{canvas}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -767,7 +767,7 @@ public class LineCanvasTests (ITestOutputHelper output)
|
||||
┣━━━━╋━━━┫
|
||||
┃ ┃ ┃
|
||||
┗━━━━┻━━━┛";
|
||||
TestHelpers.AssertDriverContentsAre (looksLike, output);
|
||||
TestHelpers.AssertDriverContentsAre (looksLike, _output);
|
||||
v.Dispose ();
|
||||
}
|
||||
|
||||
@@ -798,7 +798,7 @@ public class LineCanvasTests (ITestOutputHelper output)
|
||||
│ │ │
|
||||
┕━━━━┷━━━┙
|
||||
";
|
||||
TestHelpers.AssertDriverContentsAre (looksLike, output);
|
||||
TestHelpers.AssertDriverContentsAre (looksLike, _output);
|
||||
v.Dispose ();
|
||||
}
|
||||
|
||||
@@ -830,7 +830,7 @@ public class LineCanvasTests (ITestOutputHelper output)
|
||||
┖────┸───┚
|
||||
|
||||
";
|
||||
TestHelpers.AssertDriverContentsAre (looksLike, output);
|
||||
TestHelpers.AssertDriverContentsAre (looksLike, _output);
|
||||
v.Dispose ();
|
||||
}
|
||||
|
||||
@@ -848,7 +848,7 @@ public class LineCanvasTests (ITestOutputHelper output)
|
||||
@"
|
||||
┌─
|
||||
│ ";
|
||||
TestHelpers.AssertEqual (output, looksLike, $"{Environment.NewLine}{canvas}");
|
||||
TestHelpers.AssertEqual (_output, looksLike, $"{Environment.NewLine}{canvas}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -878,7 +878,7 @@ public class LineCanvasTests (ITestOutputHelper output)
|
||||
Assert.Equal (2, map.Count);
|
||||
|
||||
TestHelpers.AssertEqual (
|
||||
output,
|
||||
_output,
|
||||
@"
|
||||
─
|
||||
─",
|
||||
@@ -891,7 +891,7 @@ public class LineCanvasTests (ITestOutputHelper output)
|
||||
public void ToString_Empty ()
|
||||
{
|
||||
var lc = new LineCanvas ();
|
||||
TestHelpers.AssertEqual (output, string.Empty, lc.ToString ());
|
||||
TestHelpers.AssertEqual (_output, string.Empty, lc.ToString ());
|
||||
}
|
||||
|
||||
// 012
|
||||
@@ -910,7 +910,7 @@ public class LineCanvasTests (ITestOutputHelper output)
|
||||
{
|
||||
var lc = new LineCanvas ();
|
||||
lc.AddLine (new (x, y), 3, Orientation.Horizontal, LineStyle.Double);
|
||||
TestHelpers.AssertEqual (output, expected, $"{lc}");
|
||||
TestHelpers.AssertEqual (_output, expected, $"{lc}");
|
||||
}
|
||||
|
||||
[InlineData (0, 0, 0, 0, "═══")]
|
||||
@@ -935,7 +935,7 @@ public class LineCanvasTests (ITestOutputHelper output)
|
||||
lc.AddLine (new (x1, y1), 3, Orientation.Horizontal, LineStyle.Double);
|
||||
lc.AddLine (new (x2, y2), 3, Orientation.Horizontal, LineStyle.Double);
|
||||
|
||||
TestHelpers.AssertEqual (output, expected, $"{lc}");
|
||||
TestHelpers.AssertEqual (_output, expected, $"{lc}");
|
||||
}
|
||||
|
||||
// [Fact, SetupFakeDriver]
|
||||
@@ -995,7 +995,7 @@ public class LineCanvasTests (ITestOutputHelper output)
|
||||
|
||||
v.Draw ();
|
||||
|
||||
TestHelpers.AssertDriverContentsAre (expected, output);
|
||||
TestHelpers.AssertDriverContentsAre (expected, _output);
|
||||
v.Dispose ();
|
||||
}
|
||||
|
||||
@@ -1014,7 +1014,7 @@ public class LineCanvasTests (ITestOutputHelper output)
|
||||
@"
|
||||
┌─
|
||||
│";
|
||||
TestHelpers.AssertDriverContentsAre (looksLike, output);
|
||||
TestHelpers.AssertDriverContentsAre (looksLike, _output);
|
||||
v.Dispose ();
|
||||
}
|
||||
|
||||
@@ -1037,7 +1037,7 @@ public class LineCanvasTests (ITestOutputHelper output)
|
||||
──
|
||||
│
|
||||
│";
|
||||
TestHelpers.AssertDriverContentsAre (looksLike, output);
|
||||
TestHelpers.AssertDriverContentsAre (looksLike, _output);
|
||||
v.Dispose ();
|
||||
}
|
||||
|
||||
@@ -1055,7 +1055,7 @@ public class LineCanvasTests (ITestOutputHelper output)
|
||||
var looksLike =
|
||||
@"
|
||||
──";
|
||||
TestHelpers.AssertDriverContentsAre (looksLike, output);
|
||||
TestHelpers.AssertDriverContentsAre (looksLike, _output);
|
||||
v.Dispose ();
|
||||
}
|
||||
|
||||
@@ -1071,7 +1071,7 @@ public class LineCanvasTests (ITestOutputHelper output)
|
||||
var looksLike =
|
||||
@"
|
||||
══";
|
||||
TestHelpers.AssertDriverContentsAre (looksLike, output);
|
||||
TestHelpers.AssertDriverContentsAre (looksLike, _output);
|
||||
v.Dispose ();
|
||||
}
|
||||
|
||||
@@ -1090,7 +1090,7 @@ public class LineCanvasTests (ITestOutputHelper output)
|
||||
@"
|
||||
│
|
||||
│";
|
||||
TestHelpers.AssertDriverContentsAre (looksLike, output);
|
||||
TestHelpers.AssertDriverContentsAre (looksLike, _output);
|
||||
v.Dispose ();
|
||||
}
|
||||
|
||||
@@ -1107,7 +1107,7 @@ public class LineCanvasTests (ITestOutputHelper output)
|
||||
@"
|
||||
║
|
||||
║";
|
||||
TestHelpers.AssertDriverContentsAre (looksLike, output);
|
||||
TestHelpers.AssertDriverContentsAre (looksLike, _output);
|
||||
v.Dispose ();
|
||||
}
|
||||
|
||||
@@ -1135,7 +1135,7 @@ public class LineCanvasTests (ITestOutputHelper output)
|
||||
╠════╬═══╣
|
||||
║ ║ ║
|
||||
╚════╩═══╝";
|
||||
TestHelpers.AssertDriverContentsAre (looksLike, output);
|
||||
TestHelpers.AssertDriverContentsAre (looksLike, _output);
|
||||
v.Dispose ();
|
||||
}
|
||||
|
||||
@@ -1166,7 +1166,7 @@ public class LineCanvasTests (ITestOutputHelper output)
|
||||
│ │ │
|
||||
╘════╧═══╛
|
||||
";
|
||||
TestHelpers.AssertDriverContentsAre (looksLike, output);
|
||||
TestHelpers.AssertDriverContentsAre (looksLike, _output);
|
||||
v.Dispose ();
|
||||
}
|
||||
|
||||
@@ -1203,7 +1203,7 @@ public class LineCanvasTests (ITestOutputHelper output)
|
||||
├────┼───┤
|
||||
│ │ │
|
||||
╰────┴───╯";
|
||||
TestHelpers.AssertDriverContentsAre (looksLike, output);
|
||||
TestHelpers.AssertDriverContentsAre (looksLike, _output);
|
||||
v.Dispose ();
|
||||
}
|
||||
|
||||
@@ -1235,7 +1235,7 @@ public class LineCanvasTests (ITestOutputHelper output)
|
||||
╙────╨───╜
|
||||
|
||||
";
|
||||
TestHelpers.AssertDriverContentsAre (looksLike, output);
|
||||
TestHelpers.AssertDriverContentsAre (looksLike, _output);
|
||||
v.Dispose ();
|
||||
}
|
||||
|
||||
@@ -1262,7 +1262,7 @@ public class LineCanvasTests (ITestOutputHelper output)
|
||||
├────┼───┤
|
||||
│ │ │
|
||||
└────┴───┘";
|
||||
TestHelpers.AssertEqual (output, looksLike, $"{Environment.NewLine}{canvas}");
|
||||
TestHelpers.AssertEqual (_output, looksLike, $"{Environment.NewLine}{canvas}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -1300,7 +1300,93 @@ public class LineCanvasTests (ITestOutputHelper output)
|
||||
var looksLike = @"
|
||||
╔╡╞══╗
|
||||
║ ║";
|
||||
TestHelpers.AssertEqual (output, looksLike, $"{Environment.NewLine}{lc}");
|
||||
TestHelpers.AssertEqual (_output, looksLike, $"{Environment.NewLine}{lc}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LineCanvas_UsesFillCorrectly ()
|
||||
{
|
||||
// Arrange
|
||||
var foregroundColor = new Color (255, 0); // Red
|
||||
var backgroundColor = new Color (0, 0); // Black
|
||||
var foregroundFill = new SolidFill (foregroundColor);
|
||||
var backgroundFill = new SolidFill (backgroundColor);
|
||||
var fillPair = new FillPair (foregroundFill, backgroundFill);
|
||||
|
||||
var lineCanvas = new LineCanvas
|
||||
{
|
||||
Fill = fillPair
|
||||
};
|
||||
|
||||
// Act
|
||||
lineCanvas.AddLine (new (0, 0), 5, Orientation.Horizontal, LineStyle.Single);
|
||||
Dictionary<Point, Cell?> cellMap = lineCanvas.GetCellMap ();
|
||||
|
||||
// Assert
|
||||
foreach (Cell? cell in cellMap.Values)
|
||||
{
|
||||
Assert.NotNull (cell);
|
||||
Assert.Equal (foregroundColor, cell.Value.Attribute.Value.Foreground);
|
||||
Assert.Equal (backgroundColor, cell.Value.Attribute.Value.Background);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LineCanvas_LineColorIgnoredBecauseOfFill ()
|
||||
{
|
||||
// Arrange
|
||||
var foregroundColor = new Color (255, 0); // Red
|
||||
var backgroundColor = new Color (0, 0); // Black
|
||||
var lineColor = new Attribute (new Color (0, 255), new Color (255, 255, 255)); // Green on White
|
||||
var foregroundFill = new SolidFill (foregroundColor);
|
||||
var backgroundFill = new SolidFill (backgroundColor);
|
||||
var fillPair = new FillPair (foregroundFill, backgroundFill);
|
||||
|
||||
var lineCanvas = new LineCanvas
|
||||
{
|
||||
Fill = fillPair
|
||||
};
|
||||
|
||||
// Act
|
||||
lineCanvas.AddLine (new (0, 0), 5, Orientation.Horizontal, LineStyle.Single, lineColor);
|
||||
Dictionary<Point, Cell?> cellMap = lineCanvas.GetCellMap ();
|
||||
|
||||
// Assert
|
||||
foreach (Cell? cell in cellMap.Values)
|
||||
{
|
||||
Assert.NotNull (cell);
|
||||
Assert.Equal (foregroundColor, cell.Value.Attribute.Value.Foreground);
|
||||
Assert.Equal (backgroundColor, cell.Value.Attribute.Value.Background);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LineCanvas_IntersectingLinesUseFillCorrectly ()
|
||||
{
|
||||
// Arrange
|
||||
var foregroundColor = new Color (255, 0); // Red
|
||||
var backgroundColor = new Color (0, 0); // Black
|
||||
var foregroundFill = new SolidFill (foregroundColor);
|
||||
var backgroundFill = new SolidFill (backgroundColor);
|
||||
var fillPair = new FillPair (foregroundFill, backgroundFill);
|
||||
|
||||
var lineCanvas = new LineCanvas
|
||||
{
|
||||
Fill = fillPair
|
||||
};
|
||||
|
||||
// Act
|
||||
lineCanvas.AddLine (new (0, 0), 5, Orientation.Horizontal, LineStyle.Single);
|
||||
lineCanvas.AddLine (new (2, -2), 5, Orientation.Vertical, LineStyle.Single);
|
||||
Dictionary<Point, Cell?> cellMap = lineCanvas.GetCellMap ();
|
||||
|
||||
// Assert
|
||||
foreach (Cell? cell in cellMap.Values)
|
||||
{
|
||||
Assert.NotNull (cell);
|
||||
Assert.Equal (foregroundColor, cell.Value.Attribute.Value.Foreground);
|
||||
Assert.Equal (backgroundColor, cell.Value.Attribute.Value.Background);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Remove this and make all LineCanvas tests independent of View
|
||||
|
||||
37
UnitTests/Drawing/SolidFillTests.cs
Normal file
37
UnitTests/Drawing/SolidFillTests.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
namespace Terminal.Gui.DrawingTests;
|
||||
|
||||
public class SolidFillTests
|
||||
{
|
||||
[Fact]
|
||||
public void GetColor_ReturnsCorrectColor ()
|
||||
{
|
||||
// Arrange
|
||||
var expectedColor = new Color (100, 150, 200);
|
||||
var solidFill = new SolidFill (expectedColor);
|
||||
|
||||
// Act
|
||||
Color resultColor = solidFill.GetColor (new (0, 0));
|
||||
|
||||
// Assert
|
||||
Assert.Equal (expectedColor, resultColor);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData (0, 0)]
|
||||
[InlineData (1, 1)]
|
||||
[InlineData (-1, -1)]
|
||||
[InlineData (100, 100)]
|
||||
[InlineData (-100, -100)]
|
||||
public void GetColor_ReturnsSameColorForDifferentPoints (int x, int y)
|
||||
{
|
||||
// Arrange
|
||||
var expectedColor = new Color (50, 100, 150);
|
||||
var solidFill = new SolidFill (expectedColor);
|
||||
|
||||
// Act
|
||||
Color resultColor = solidFill.GetColor (new (x, y));
|
||||
|
||||
// Assert
|
||||
Assert.Equal (expectedColor, resultColor);
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,8 @@
|
||||
|
||||
namespace Terminal.Gui.DrawingTests;
|
||||
|
||||
public class StraightLineExtensionsTests
|
||||
public class StraightLineExtensionsTests (ITestOutputHelper output)
|
||||
{
|
||||
private readonly ITestOutputHelper _output;
|
||||
public StraightLineExtensionsTests (ITestOutputHelper output) { _output = output; }
|
||||
|
||||
[Fact]
|
||||
[AutoInitShutdown]
|
||||
public void LineCanvasIntegrationTest ()
|
||||
@@ -18,7 +15,7 @@ public class StraightLineExtensionsTests
|
||||
lc.AddLine (new Point (0, 4), -5, Orientation.Vertical, LineStyle.Single);
|
||||
|
||||
TestHelpers.AssertEqual (
|
||||
_output,
|
||||
output,
|
||||
@"
|
||||
┌────────┐
|
||||
│ │
|
||||
@@ -32,7 +29,7 @@ public class StraightLineExtensionsTests
|
||||
lc = new LineCanvas (origLines.Exclude (Point.Empty, 10, Orientation.Horizontal));
|
||||
|
||||
TestHelpers.AssertEqual (
|
||||
_output,
|
||||
output,
|
||||
@"
|
||||
│ │
|
||||
│ │
|
||||
@@ -44,7 +41,7 @@ public class StraightLineExtensionsTests
|
||||
lc = new LineCanvas (origLines.Exclude (new Point (0, 1), 10, Orientation.Horizontal));
|
||||
|
||||
TestHelpers.AssertEqual (
|
||||
_output,
|
||||
output,
|
||||
@"
|
||||
┌────────┐
|
||||
|
||||
@@ -57,7 +54,7 @@ public class StraightLineExtensionsTests
|
||||
lc = new LineCanvas (origLines.Exclude (new Point (0, 2), 10, Orientation.Horizontal));
|
||||
|
||||
TestHelpers.AssertEqual (
|
||||
_output,
|
||||
output,
|
||||
@"
|
||||
┌────────┐
|
||||
│ │
|
||||
@@ -70,7 +67,7 @@ public class StraightLineExtensionsTests
|
||||
lc = new LineCanvas (origLines.Exclude (new Point (0, 3), 10, Orientation.Horizontal));
|
||||
|
||||
TestHelpers.AssertEqual (
|
||||
_output,
|
||||
output,
|
||||
@"
|
||||
┌────────┐
|
||||
│ │
|
||||
@@ -83,7 +80,7 @@ public class StraightLineExtensionsTests
|
||||
lc = new LineCanvas (origLines.Exclude (new Point (0, 4), 10, Orientation.Horizontal));
|
||||
|
||||
TestHelpers.AssertEqual (
|
||||
_output,
|
||||
output,
|
||||
@"
|
||||
┌────────┐
|
||||
│ │
|
||||
@@ -95,7 +92,7 @@ public class StraightLineExtensionsTests
|
||||
lc = new LineCanvas (origLines.Exclude (Point.Empty, 10, Orientation.Vertical));
|
||||
|
||||
TestHelpers.AssertEqual (
|
||||
_output,
|
||||
output,
|
||||
@"
|
||||
────────┐
|
||||
│
|
||||
@@ -108,7 +105,7 @@ public class StraightLineExtensionsTests
|
||||
lc = new LineCanvas (origLines.Exclude (new Point (1, 0), 10, Orientation.Vertical));
|
||||
|
||||
TestHelpers.AssertEqual (
|
||||
_output,
|
||||
output,
|
||||
@"
|
||||
┌ ───────┐
|
||||
│ │
|
||||
@@ -121,7 +118,7 @@ public class StraightLineExtensionsTests
|
||||
lc = new LineCanvas (origLines.Exclude (new Point (8, 0), 10, Orientation.Vertical));
|
||||
|
||||
TestHelpers.AssertEqual (
|
||||
_output,
|
||||
output,
|
||||
@"
|
||||
┌─────── ┐
|
||||
│ │
|
||||
@@ -134,7 +131,7 @@ public class StraightLineExtensionsTests
|
||||
lc = new LineCanvas (origLines.Exclude (new Point (9, 0), 10, Orientation.Vertical));
|
||||
|
||||
TestHelpers.AssertEqual (
|
||||
_output,
|
||||
output,
|
||||
@"
|
||||
┌────────
|
||||
│
|
||||
|
||||
@@ -2,10 +2,9 @@
|
||||
|
||||
namespace Terminal.Gui.DrawingTests;
|
||||
|
||||
public class StraightLineTests
|
||||
public class StraightLineTests (ITestOutputHelper output)
|
||||
{
|
||||
private readonly ITestOutputHelper output;
|
||||
public StraightLineTests (ITestOutputHelper output) { this.output = output; }
|
||||
private readonly ITestOutputHelper _output = output;
|
||||
|
||||
[InlineData (
|
||||
Orientation.Horizontal,
|
||||
@@ -320,8 +319,8 @@ public class StraightLineTests
|
||||
int expectedHeight
|
||||
)
|
||||
{
|
||||
var sl = new StraightLine (new Point (x, y), length, orientation, LineStyle.Single);
|
||||
var sl = new StraightLine (new (x, y), length, orientation, LineStyle.Single);
|
||||
|
||||
Assert.Equal (new Rectangle (expectedX, expectedY, expectedWidth, expectedHeight), sl.Viewport);
|
||||
Assert.Equal (new (expectedX, expectedY, expectedWidth, expectedHeight), sl.Viewport);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user