diff --git a/Terminal.Gui/Drawing/FillPair.cs b/Terminal.Gui/Drawing/FillPair.cs new file mode 100644 index 0000000000..d0ea126080 --- /dev/null +++ b/Terminal.Gui/Drawing/FillPair.cs @@ -0,0 +1,41 @@ +namespace Terminal.Gui; + +/// +/// Describes a pair of which cooperate in creating +/// . One gives foreground color while other gives background. +/// +public class FillPair +{ + /// + /// Creates a new instance using the provided fills for foreground and background + /// color when assembling . + /// + /// + /// + public FillPair (IFill fore, IFill back) + { + Foreground = fore; + Background = back; + } + + /// + /// The fill which provides point based foreground color. + /// + public IFill Foreground { get; init; } + + /// + /// The fill which provides point based background color. + /// + public IFill Background { get; init; } + + /// + /// Returns the color pair (foreground+background) to use when rendering + /// a rune at the given . + /// + /// + /// + public Attribute GetAttribute (Point point) + { + return new (Foreground.GetColor (point), Background.GetColor (point)); + } +} diff --git a/Terminal.Gui/Drawing/Gradient.cs b/Terminal.Gui/Drawing/Gradient.cs new file mode 100644 index 0000000000..3b41e3e493 --- /dev/null +++ b/Terminal.Gui/Drawing/Gradient.cs @@ -0,0 +1,255 @@ +// This code is a C# port from python library Terminal Text Effects https://github.com/ChrisBuilds/terminaltexteffects/ + +namespace Terminal.Gui; + +/// +/// Describes the pattern that a results in e.g. , +/// etc +/// +public enum GradientDirection +{ + /// + /// Color varies along Y axis but is constant on X axis. + /// + Vertical, + + /// + /// Color varies along X axis but is constant on Y axis. + /// + Horizontal, + + /// + /// Color varies by distance from center (i.e. in circular ripples) + /// + Radial, + + /// + /// Color varies by X and Y axis (i.e. a slanted gradient) + /// + Diagonal +} + +/// +/// Describes a of colors that can be combined +/// to make a color gradient. Use +/// to create into gradient fill area maps. +/// +public class Gradient +{ + /// + /// The discrete colors that will make up the . + /// + public List Spectrum { get; } + + private readonly bool _loop; + private readonly List _stops; + 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); + } + + /// + /// Returns the color to use at the given part of the spectrum + /// + /// + /// Proportion of the way through the spectrum, must be between + /// 0 and 1 (inclusive). Returns the last color if is + /// . + /// + /// + /// + 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 GenerateGradient (IEnumerable steps) + { + List gradient = new (); + + if (_stops.Count == 1) + { + for (var i = 0; i < steps.Sum (); i++) + { + gradient.Add (_stops [0]); + } + + return gradient; + } + + List stopsToUse = _stops.ToList (); + List 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 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 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 + } + + /// + /// + /// Creates a mapping starting at 0,0 and going to and + /// (inclusively) using the supplied . + /// + /// + /// Note that this method is inclusive i.e. passing 1/1 results in 4 mapped coordinates. + /// + /// + /// + /// + /// + /// + public Dictionary BuildCoordinateColorMapping (int maxRow, int maxColumn, GradientDirection direction) + { + Dictionary 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; + } +} diff --git a/Terminal.Gui/Drawing/GradientFill.cs b/Terminal.Gui/Drawing/GradientFill.cs new file mode 100644 index 0000000000..6518d2dabb --- /dev/null +++ b/Terminal.Gui/Drawing/GradientFill.cs @@ -0,0 +1,42 @@ +namespace Terminal.Gui; + +/// +/// Implementation of that uses a color gradient (including +/// radial, diagonal etc.). +/// +public class GradientFill : IFill +{ + private readonly Dictionary _map; + + /// + /// Creates a new instance of the class that can return + /// color for any point in the given using the provided + /// and . + /// + /// + /// + /// + 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); + } + + /// + /// Returns the color to use for the given or Black if it + /// lies outside the prepared gradient area (see constructor). + /// + /// + /// + 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 + } +} diff --git a/Terminal.Gui/Drawing/IFill.cs b/Terminal.Gui/Drawing/IFill.cs new file mode 100644 index 0000000000..7d1d19a68f --- /dev/null +++ b/Terminal.Gui/Drawing/IFill.cs @@ -0,0 +1,14 @@ +namespace Terminal.Gui; + +/// +/// Describes an area fill (e.g. solid color or gradient). +/// +public interface IFill +{ + /// + /// Returns the color that should be used at the given point + /// + /// + /// + Color GetColor (Point point); +} diff --git a/Terminal.Gui/Drawing/LineCanvas.cs b/Terminal.Gui/Drawing/LineCanvas.cs index b1e0ced13b..2bda9e5dd4 100644 --- a/Terminal.Gui/Drawing/LineCanvas.cs +++ b/Terminal.Gui/Drawing/LineCanvas.cs @@ -4,6 +4,13 @@ namespace Terminal.Gui; /// Facilitates box drawing and line intersection detection and rendering. Does not support diagonal lines. public class LineCanvas : IDisposable { + /// + /// Optional which when present overrides the + /// (colors) of lines in the canvas. This can be used e.g. to apply a global + /// across all lines. + /// + public FillPair? Fill { get; set; } + private readonly List _lines = []; private readonly Dictionary _runeResolvers = new () @@ -85,7 +92,7 @@ public Rectangle Viewport 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 void AddLine ( ) { _cachedViewport = Rectangle.Empty; - _lines.Add (new StraightLine (start, length, orientation, style, attribute)); + _lines.Add (new (start, length, orientation, style, attribute)); } /// Adds a new line to the canvas @@ -183,7 +190,7 @@ public void Clear () if (cell is { }) { - map.Add (new Point (x, y), cell); + map.Add (new (x, y), cell); } } } @@ -218,7 +225,7 @@ public Dictionary GetMap (Rectangle inArea) if (rune is { }) { - map.Add (new Point (x, y), rune.Value); + map.Add (new (x, y), rune.Value); } } } @@ -324,7 +331,10 @@ private void ConfigurationManager_Applied (object? sender, ConfigurationManagerE /// private bool Exactly (HashSet 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 @@ private void ConfigurationManager_Applied (object? sender, ConfigurationManagerE 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 override void SetGlyphs () _normal = Glyphs.URCorner; } } -} \ No newline at end of file +} diff --git a/Terminal.Gui/Drawing/SolidFill.cs b/Terminal.Gui/Drawing/SolidFill.cs new file mode 100644 index 0000000000..2619f67eaf --- /dev/null +++ b/Terminal.Gui/Drawing/SolidFill.cs @@ -0,0 +1,24 @@ +namespace Terminal.Gui; + +/// +/// implementation that uses a solid color for all points +/// +public class SolidFill : IFill +{ + private readonly Color _color; + + /// + /// Creates a new instance of the class which will return + /// the provided regardless of which point is requested. + /// + /// + public SolidFill (Color color) { _color = color; } + + /// + /// Returns the color this instance was constructed with regardless of + /// which is being colored. + /// + /// + /// + public Color GetColor (Point point) { return _color; } +} diff --git a/Terminal.Gui/Drawing/StraightLine.cs b/Terminal.Gui/Drawing/StraightLine.cs index 9a2785f0f4..2f36995df6 100644 --- a/Terminal.Gui/Drawing/StraightLine.cs +++ b/Terminal.Gui/Drawing/StraightLine.cs @@ -45,6 +45,7 @@ public 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. /// + // 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 @@ IntersectionType typeWhenPositive 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 @@ IntersectionType typeWhenPositive 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 @@ IntersectionType typeWhenPositive 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 @@ IntersectionType typeWhenPositive if (ymin < y && ymax > y) { - return new IntersectionDefinition ( - new Point (x, y), - IntersectionType.PassOverVertical, - this - ); + return new ( + p, + IntersectionType.PassOverVertical, + this + ); } return null; diff --git a/Terminal.Gui/View/Adornment/Border.cs b/Terminal.Gui/View/Adornment/Border.cs index 39d98f635f..2931cd9ad2 100644 --- a/Terminal.Gui/View/Adornment/Border.cs +++ b/Terminal.Gui/View/Adornment/Border.cs @@ -78,7 +78,7 @@ public override void BeginInit () if ((Parent?.Arrangement & ViewArrangement.Movable) != 0) { HighlightStyle |= HighlightStyle.Hover; - } + } #endif base.BeginInit (); @@ -149,31 +149,32 @@ public override ColorScheme ColorScheme } } - 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) + ) + ) + ); } + /// /// Sets the style of the border by changing the . This is a helper API for setting the /// to (1,1,1,1) and setting the line style of the views that comprise the border. If @@ -196,21 +197,22 @@ public LineStyle LineStyle set => _lineStyle = value; } - private bool _showTitle = true; + private BorderSettings _settings = BorderSettings.Title; /// - /// Gets or sets whether the title should be shown. The default is . + /// Gets or sets the settings for the border. /// - 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 @@ private void Border_Highlight (object sender, CancelEventArgs e) if (!Parent.Arrangement.HasFlag (ViewArrangement.Movable)) { e.Cancel = true; + return; } @@ -235,9 +238,9 @@ private void Border_Highlight (object sender, CancelEventArgs e) _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 @@ private void Border_Highlight (object sender, CancelEventArgs e) 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 @@ private void Border_Highlight (object sender, CancelEventArgs e) private Point? _dragPosition; private Point _startGrabPoint; - /// + /// protected internal override bool OnMouseEvent (MouseEvent mouseEvent) { if (base.OnMouseEvent (mouseEvent)) @@ -322,16 +326,17 @@ protected internal override bool OnMouseEvent (MouseEvent mouseEvent) _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 @@ out _ return false; } - /// protected override void Dispose (bool disposing) { @@ -403,7 +407,7 @@ public override void OnDrawContent (Rectangle viewport) // ...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 override void OnDrawContent (Rectangle viewport) 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 override void OnDrawContent (Rectangle viewport) } } - 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 override void OnDrawContent (Rectangle viewport) { // ╔╡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 override void OnDrawContent (Rectangle viewport) 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 override void OnDrawContent (Rectangle viewport) } // 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 override void OnDrawContent (Rectangle viewport) 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 stops, out List 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 stops, out List 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 }; + } } diff --git a/Terminal.Gui/View/Adornment/BorderSettings.cs b/Terminal.Gui/View/Adornment/BorderSettings.cs new file mode 100644 index 0000000000..5829d1ed67 --- /dev/null +++ b/Terminal.Gui/View/Adornment/BorderSettings.cs @@ -0,0 +1,26 @@ +using Terminal.Gui.Analyzers.Internal.Attributes; + +namespace Terminal.Gui; + +/// +/// Determines the settings for . +/// +[Flags] +[GenerateEnumExtensionMethods (FastHasFlags = true)] +public enum BorderSettings +{ + /// + /// No settings. + /// + None = 0, + + /// + /// Show the title. + /// + Title = 1, + + /// + /// Use to draw the border. + /// + Gradient = 2, +} diff --git a/Terminal.Gui/View/Adornment/Margin.cs b/Terminal.Gui/View/Adornment/Margin.cs index 2e1ea57602..046965e321 100644 --- a/Terminal.Gui/View/Adornment/Margin.cs +++ b/Terminal.Gui/View/Adornment/Margin.cs @@ -223,7 +223,7 @@ private void Margin_LayoutStarted (object? sender, LayoutEventArgs e) 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; } diff --git a/Terminal.Gui/Views/Shortcut.cs b/Terminal.Gui/Views/Shortcut.cs index 802631d521..7ddbe7c5a0 100644 --- a/Terminal.Gui/Views/Shortcut.cs +++ b/Terminal.Gui/Views/Shortcut.cs @@ -104,7 +104,7 @@ public Shortcut (Key key, string commandText, Action action, string helpText = n void OnInitialized (object sender, EventArgs e) { SuperViewRendersLineCanvas = true; - Border.ShowTitle = false; + Border.Settings &= ~BorderSettings.Title; ShowHide (); diff --git a/UICatalog/Scenarios/Animation.cs b/UICatalog/Scenarios/AnimationScenario.cs similarity index 99% rename from UICatalog/Scenarios/Animation.cs rename to UICatalog/Scenarios/AnimationScenario.cs index 42ad540c1f..f924f8f08d 100644 --- a/UICatalog/Scenarios/Animation.cs +++ b/UICatalog/Scenarios/AnimationScenario.cs @@ -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; diff --git a/UICatalog/Scenarios/BorderEditor.cs b/UICatalog/Scenarios/BorderEditor.cs index a5ccae2129..5e0a6e77a6 100644 --- a/UICatalog/Scenarios/BorderEditor.cs +++ b/UICatalog/Scenarios/BorderEditor.cs @@ -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 borderStyleEnum = Enum.GetValues (typeof (LineStyle)).Cast ().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 @@ private void BorderEditor_Initialized (object sender, EventArgs e) _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 @@ void OnRbBorderStyleOnSelectedItemChanged (object s, SelectedItemChangedArgs e) LayoutSubviews (); } - void OnCkbTitleOnToggle (object sender, CancelEventArgs args) { ((Border)AdornmentToEdit).ShowTitle = args.NewValue == CheckState.Checked; } + void OnCkbTitleOnToggle (object sender, CancelEventArgs args) + { + if (args.NewValue == CheckState.Checked) + + { + ((Border)AdornmentToEdit).Settings |= BorderSettings.Title; + } + else + + { + ((Border)AdornmentToEdit).Settings &= ~BorderSettings.Title; + } + } + + void OnCkbGradientOnToggle (object sender, CancelEventArgs args) + { + if (args.NewValue == CheckState.Checked) + + { + ((Border)AdornmentToEdit).Settings |= BorderSettings.Gradient; + } + else + + { + ((Border)AdornmentToEdit).Settings &= ~BorderSettings.Gradient; + } + } } -} \ No newline at end of file +} diff --git a/UICatalog/Scenarios/TextEffectsScenario.cs b/UICatalog/Scenarios/TextEffectsScenario.cs new file mode 100644 index 0000000000..17f6a6e5c1 --- /dev/null +++ b/UICatalog/Scenarios/TextEffectsScenario.cs @@ -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; + + /// + /// Enable or disable looping of the gradient colors. + /// + 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 stops, out List 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 stops, out List 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 stops = + [ + new (255, 0), // Red + new (0, 255), // Green + new (238, 130, 238) + ]; + + // Define the number of steps between each color + List 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 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 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 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)); } +} diff --git a/UnitTests/Drawing/FillPairTests.cs b/UnitTests/Drawing/FillPairTests.cs new file mode 100644 index 0000000000..cfe8d192d6 --- /dev/null +++ b/UnitTests/Drawing/FillPairTests.cs @@ -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); + } +} diff --git a/UnitTests/Drawing/GradientFillTests.cs b/UnitTests/Drawing/GradientFillTests.cs new file mode 100644 index 0000000000..75bc65bbc8 --- /dev/null +++ b/UnitTests/Drawing/GradientFillTests.cs @@ -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 stops = new List + { + new (255, 0), // Red + new (0, 0, 255) // Blue + }; + + // Define the number of steps between each color + List 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; + } + } + } +} diff --git a/UnitTests/Drawing/GradientTests.cs b/UnitTests/Drawing/GradientTests.cs new file mode 100644 index 0000000000..0174a7ff9a --- /dev/null +++ b/UnitTests/Drawing/GradientTests.cs @@ -0,0 +1,173 @@ +namespace Terminal.Gui.DrawingTests; + +public class GradientTests +{ + // Static method to provide all enum values + public static IEnumerable GradientDirectionValues () + { + return typeof (GradientDirection).GetEnumValues () + .Cast () + .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 stops = new() + { + new (255, 0), // Red + new (0, 0, 255) // Blue + }; + + // Define the number of steps between each color + List 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 stops = new() + { + new (255, 0), // Red + new (0, 0, 255) // Blue + }; + + // Define the number of steps between each color + List 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 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 stops = new() { new (255, 0) }; // Red + List 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 stops = new() + { + new (255, 0), // Red + new (0, 0, 255) // Blue + }; + + List 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 stops = new List + { + new (255, 0), // Red + new (0, 255), // Green + new (0, 0, 255) // Blue + }; + + List steps = new() { 5, 15 }; // Different steps + + var g = new Gradient (stops, steps); + Assert.Equal (22, g.Spectrum.Count); + } + + [Fact] + public void FractionOutOfRange_ThrowsException () + { + List stops = new() + { + new (255, 0), // Red + new (0, 0, 255) // Blue + }; + + List steps = new() { 10 }; + + var g = new Gradient (stops, steps); + + Assert.Throws (() => g.GetColorAtFraction (-0.1)); + Assert.Throws (() => g.GetColorAtFraction (1.1)); + } + + [Fact] + public void NaNFraction_ReturnsLastColor () + { + List stops = new() + { + new (255, 0), // Red + new (0, 0, 255) // Blue + }; + + List 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 stops = new List + { + new (255, 0), // Red + new (0, 255), // Green + new (0, 0, 255) // Blue + }; + + List 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 stops = new() + { + new (255, 0), // Red + new (0, 0, 255) // Blue + }; + + List invalidSteps = new() { 5, 5 }; // Invalid length (N-1 expected) + Assert.Throws (() => new Gradient (stops, invalidSteps)); + } + + [Fact] + public void Constructor_ValidStepsLength_DoesNotThrow () + { + List stops = new List + { + new (255, 0), // Red + new (0, 255), // Green + new (0, 0, 255) // Blue + }; + + List 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 + } +} diff --git a/UnitTests/Drawing/LineCanvasTests.cs b/UnitTests/Drawing/LineCanvasTests.cs index 8426d39525..90a2f4123f 100644 --- a/UnitTests/Drawing/LineCanvasTests.cs +++ b/UnitTests/Drawing/LineCanvasTests.cs @@ -3,7 +3,7 @@ namespace Terminal.Gui.DrawingTests; -public class LineCanvasTests (ITestOutputHelper output) +public class LineCanvasTests (ITestOutputHelper _output) { [Theory] @@ -294,7 +294,7 @@ string expected 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 void Viewport_Specific () Assert.Equal (new (x, y, 4, 2), lc.Viewport); TestHelpers.AssertEqual ( - output, + _output, @" ╔╡╞╗ ║ ║", @@ -553,7 +553,7 @@ public void Viewport_Specific_With_Ustring () Assert.Equal (new (x, y, 4, 2), lc.Viewport); TestHelpers.AssertEqual ( - output, + _output, @" ╔╡╞╗ ║ ║", @@ -596,7 +596,7 @@ public void Length_0_Is_1_Long (int x, int y, Orientation orientation, string ex // 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 void Length_n_Is_n_Long (int x, int y, int length, Orientation orientatio 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 void Length_Zero_Alone_Is_Line (Orientation orientation, string expected) // 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 void Length_Zero_Cross_Is_Cross (Orientation orientation, string expected // 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 void Length_Zero_NextTo_Opposite_Is_T (Orientation orientation, string ex // 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 void TestLineCanvas_LeaveMargin_Top1_Left1 () @" ┌─ │ "; - TestHelpers.AssertEqual (output, looksLike, $"{Environment.NewLine}{canvas}"); + TestHelpers.AssertEqual (_output, looksLike, $"{Environment.NewLine}{canvas}"); } [Fact] @@ -767,7 +767,7 @@ public void TestLineCanvas_Window_Heavy () ┣━━━━╋━━━┫ ┃ ┃ ┃ ┗━━━━┻━━━┛"; - TestHelpers.AssertDriverContentsAre (looksLike, output); + TestHelpers.AssertDriverContentsAre (looksLike, _output); v.Dispose (); } @@ -798,7 +798,7 @@ public void TestLineCanvas_Window_HeavyTop_ThinSides (LineStyle thinStyle) │ │ │ ┕━━━━┷━━━┙ "; - TestHelpers.AssertDriverContentsAre (looksLike, output); + TestHelpers.AssertDriverContentsAre (looksLike, _output); v.Dispose (); } @@ -830,7 +830,7 @@ public void TestLineCanvas_Window_ThinTop_HeavySides (LineStyle thinStyle) ┖────┸───┚ "; - TestHelpers.AssertDriverContentsAre (looksLike, output); + TestHelpers.AssertDriverContentsAre (looksLike, _output); v.Dispose (); } @@ -848,7 +848,7 @@ public void Top_Left_From_TopRight_LeftUp () @" ┌─ │ "; - TestHelpers.AssertEqual (output, looksLike, $"{Environment.NewLine}{canvas}"); + TestHelpers.AssertEqual (_output, looksLike, $"{Environment.NewLine}{canvas}"); } [Fact] @@ -878,7 +878,7 @@ public void Top_With_1Down () Assert.Equal (2, map.Count); TestHelpers.AssertEqual ( - output, + _output, @" ─ ─", @@ -891,7 +891,7 @@ public void Top_With_1Down () 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 void ToString_Positive_Horizontal_1Line_Offset (int x, int y, string expe { 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 void ToString_Positive_Horizontal_2Line_Offset (int x1, int y1, int x2, i 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 @@ string expected v.Draw (); - TestHelpers.AssertDriverContentsAre (expected, output); + TestHelpers.AssertDriverContentsAre (expected, _output); v.Dispose (); } @@ -1014,7 +1014,7 @@ public void View_Draws_Corner_Correct () @" ┌─ │"; - TestHelpers.AssertDriverContentsAre (looksLike, output); + TestHelpers.AssertDriverContentsAre (looksLike, _output); v.Dispose (); } @@ -1037,7 +1037,7 @@ public void View_Draws_Corner_NoOverlap () ── │ │"; - TestHelpers.AssertDriverContentsAre (looksLike, output); + TestHelpers.AssertDriverContentsAre (looksLike, _output); v.Dispose (); } @@ -1055,7 +1055,7 @@ public void View_Draws_Horizontal (LineStyle style) var looksLike = @" ──"; - TestHelpers.AssertDriverContentsAre (looksLike, output); + TestHelpers.AssertDriverContentsAre (looksLike, _output); v.Dispose (); } @@ -1071,7 +1071,7 @@ public void View_Draws_Horizontal_Double () var looksLike = @" ══"; - TestHelpers.AssertDriverContentsAre (looksLike, output); + TestHelpers.AssertDriverContentsAre (looksLike, _output); v.Dispose (); } @@ -1090,7 +1090,7 @@ public void View_Draws_Vertical (LineStyle style) @" │ │"; - TestHelpers.AssertDriverContentsAre (looksLike, output); + TestHelpers.AssertDriverContentsAre (looksLike, _output); v.Dispose (); } @@ -1107,7 +1107,7 @@ public void View_Draws_Vertical_Double () @" ║ ║"; - TestHelpers.AssertDriverContentsAre (looksLike, output); + TestHelpers.AssertDriverContentsAre (looksLike, _output); v.Dispose (); } @@ -1135,7 +1135,7 @@ public void View_Draws_Window_Double () ╠════╬═══╣ ║ ║ ║ ╚════╩═══╝"; - TestHelpers.AssertDriverContentsAre (looksLike, output); + TestHelpers.AssertDriverContentsAre (looksLike, _output); v.Dispose (); } @@ -1166,7 +1166,7 @@ public void View_Draws_Window_DoubleTop_SingleSides (LineStyle thinStyle) │ │ │ ╘════╧═══╛ "; - TestHelpers.AssertDriverContentsAre (looksLike, output); + TestHelpers.AssertDriverContentsAre (looksLike, _output); v.Dispose (); } @@ -1203,7 +1203,7 @@ public void View_Draws_Window_Rounded () ├────┼───┤ │ │ │ ╰────┴───╯"; - TestHelpers.AssertDriverContentsAre (looksLike, output); + TestHelpers.AssertDriverContentsAre (looksLike, _output); v.Dispose (); } @@ -1235,7 +1235,7 @@ public void View_Draws_Window_SingleTop_DoubleSides (LineStyle thinStyle) ╙────╨───╜ "; - TestHelpers.AssertDriverContentsAre (looksLike, output); + TestHelpers.AssertDriverContentsAre (looksLike, _output); v.Dispose (); } @@ -1262,7 +1262,7 @@ public void Window () ├────┼───┤ │ │ │ └────┴───┘"; - TestHelpers.AssertEqual (output, looksLike, $"{Environment.NewLine}{canvas}"); + TestHelpers.AssertEqual (_output, looksLike, $"{Environment.NewLine}{canvas}"); } [Fact] @@ -1300,7 +1300,93 @@ public void Zero_Length_Intersections () 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 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 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 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 diff --git a/UnitTests/Drawing/SolidFillTests.cs b/UnitTests/Drawing/SolidFillTests.cs new file mode 100644 index 0000000000..749a87c49b --- /dev/null +++ b/UnitTests/Drawing/SolidFillTests.cs @@ -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); + } +} diff --git a/UnitTests/Drawing/StraightLineExtensionsTests.cs b/UnitTests/Drawing/StraightLineExtensionsTests.cs index b7a5f36b10..865ae805ac 100644 --- a/UnitTests/Drawing/StraightLineExtensionsTests.cs +++ b/UnitTests/Drawing/StraightLineExtensionsTests.cs @@ -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 void LineCanvasIntegrationTest () lc.AddLine (new Point (0, 4), -5, Orientation.Vertical, LineStyle.Single); TestHelpers.AssertEqual ( - _output, + output, @" ┌────────┐ │ │ @@ -32,7 +29,7 @@ public void LineCanvasIntegrationTest () lc = new LineCanvas (origLines.Exclude (Point.Empty, 10, Orientation.Horizontal)); TestHelpers.AssertEqual ( - _output, + output, @" │ │ │ │ @@ -44,7 +41,7 @@ public void LineCanvasIntegrationTest () lc = new LineCanvas (origLines.Exclude (new Point (0, 1), 10, Orientation.Horizontal)); TestHelpers.AssertEqual ( - _output, + output, @" ┌────────┐ @@ -57,7 +54,7 @@ public void LineCanvasIntegrationTest () lc = new LineCanvas (origLines.Exclude (new Point (0, 2), 10, Orientation.Horizontal)); TestHelpers.AssertEqual ( - _output, + output, @" ┌────────┐ │ │ @@ -70,7 +67,7 @@ public void LineCanvasIntegrationTest () lc = new LineCanvas (origLines.Exclude (new Point (0, 3), 10, Orientation.Horizontal)); TestHelpers.AssertEqual ( - _output, + output, @" ┌────────┐ │ │ @@ -83,7 +80,7 @@ public void LineCanvasIntegrationTest () lc = new LineCanvas (origLines.Exclude (new Point (0, 4), 10, Orientation.Horizontal)); TestHelpers.AssertEqual ( - _output, + output, @" ┌────────┐ │ │ @@ -95,7 +92,7 @@ public void LineCanvasIntegrationTest () lc = new LineCanvas (origLines.Exclude (Point.Empty, 10, Orientation.Vertical)); TestHelpers.AssertEqual ( - _output, + output, @" ────────┐ │ @@ -108,7 +105,7 @@ public void LineCanvasIntegrationTest () lc = new LineCanvas (origLines.Exclude (new Point (1, 0), 10, Orientation.Vertical)); TestHelpers.AssertEqual ( - _output, + output, @" ┌ ───────┐ │ │ @@ -121,7 +118,7 @@ public void LineCanvasIntegrationTest () lc = new LineCanvas (origLines.Exclude (new Point (8, 0), 10, Orientation.Vertical)); TestHelpers.AssertEqual ( - _output, + output, @" ┌─────── ┐ │ │ @@ -134,7 +131,7 @@ public void LineCanvasIntegrationTest () lc = new LineCanvas (origLines.Exclude (new Point (9, 0), 10, Orientation.Vertical)); TestHelpers.AssertEqual ( - _output, + output, @" ┌──────── │ diff --git a/UnitTests/Drawing/StraightLineTests.cs b/UnitTests/Drawing/StraightLineTests.cs index bb68708214..4395ea0c17 100644 --- a/UnitTests/Drawing/StraightLineTests.cs +++ b/UnitTests/Drawing/StraightLineTests.cs @@ -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 void Viewport ( 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); } }