using System.Text.Json.Serialization; namespace Terminal.Gui; /// /// Describes the thickness of a frame around a rectangle. Four values describe the /// , , , and sides of the rectangle, /// respectively. /// /// /// /// Use the helper API ( to get the rectangle describing the insides of the frame, /// with the thickness widths subtracted. /// /// Use the helper API ( to draw the frame with the specified thickness. /// public class Thickness : IEquatable { /// Gets or sets the width of the lower side of the rectangle. [JsonInclude] public int Bottom; /// Gets or sets the width of the left side of the rectangle. [JsonInclude] public int Left; /// Gets or sets the width of the right side of the rectangle. [JsonInclude] public int Right; /// Gets or sets the width of the upper side of the rectangle. [JsonInclude] public int Top; /// Initializes a new instance of the class with all widths set to 0. public Thickness () { } /// Initializes a new instance of the class with a uniform width to each side. /// public Thickness (int width) : this (width, width, width, width) { } /// /// Initializes a new instance of the class that has specific widths applied to each side /// of the rectangle. /// /// /// /// /// public Thickness (int left, int top, int right, int bottom) { Left = left; Top = top; Right = right; Bottom = bottom; } // TODO: add operator overloads /// Gets an empty thickness. public static Thickness Empty => new (0); /// /// Gets the total width of the left and right sides of the rectangle. Sets the width of the left and rigth sides /// of the rectangle to half the specified value. /// public int Horizontal { get => Left + Right; set => Left = Right = value / 2; } /// /// Gets the total height of the top and bottom sides of the rectangle. Sets the height of the top and bottom /// sides of the rectangle to half the specified value. /// public int Vertical { get => Top + Bottom; set => Top = Bottom = value / 2; } // IEquitable /// Indicates whether the current object is equal to another object of the same type. /// /// true if the current object is equal to the other parameter; otherwise, false. public bool Equals (Thickness other) { return other is { } && Left == other.Left && Right == other.Right && Top == other.Top && Bottom == other.Bottom; } /// /// Gets whether the specified coordinates lie within the thickness (inside the bounding rectangle but outside of /// the rectangle described by . /// /// Describes the location and size of the rectangle that contains the thickness. /// The x coord to check. /// The y coord to check. /// if the specified coordinate is within the thickness; otherwise. public bool Contains (Rectangle outside, int x, int y) { Rectangle inside = GetInside (outside); return outside.Contains (x, y) && !inside.Contains (x, y); } /// /// Adds the thickness widths of another to the current , returning a new . /// /// /// public Thickness Add (Thickness other) { return new Thickness (Left + other.Left, Top + other.Top, Right + other.Right, Bottom + other.Bottom); } /// /// Adds the thickness widths of another to another . /// /// /// /// public static Thickness operator + (Thickness a, Thickness b) { return a.Add (b); } /// Draws the rectangle with an optional diagnostics label. /// /// If is set to /// is set to /// The location and size of the rectangle that bounds the thickness rectangle, in screen coordinates. /// The diagnostics label to draw on the bottom of the . /// The inner rectangle remaining to be drawn. public Rectangle Draw (Rectangle rect, string label = null) { if (rect.Size.Width < 1 || rect.Size.Height < 1) { return Rectangle.Empty; } var clearChar = (Rune)' '; Rune leftChar = clearChar; Rune rightChar = clearChar; Rune topChar = clearChar; Rune bottomChar = clearChar; if (View.Diagnostics.HasFlag (ViewDiagnosticFlags.Padding)) { leftChar = (Rune)'L'; rightChar = (Rune)'R'; topChar = (Rune)'T'; bottomChar = (Rune)'B'; if (!string.IsNullOrEmpty (label)) { leftChar = rightChar = bottomChar = topChar = (Rune)label [0]; } } // Draw the Top side if (Top > 0) { Application.Driver.FillRect (rect with { Height = Math.Min (rect.Height, Top) }, topChar); } // Draw the Left side if (Left > 0) { Application.Driver.FillRect (rect with { Width = Math.Min (rect.Width, Left) }, leftChar); } // Draw the Right side if (Right > 0) { Application.Driver.FillRect ( rect with { X = Math.Max (0, rect.X + rect.Width - Right), Width = Math.Min (rect.Width, Right) }, rightChar ); } // Draw the Bottom side if (Bottom > 0) { Application.Driver.FillRect ( rect with { Y = rect.Y + Math.Max (0, rect.Height - Bottom), Height = Bottom }, bottomChar ); } if (View.Diagnostics.HasFlag(ViewDiagnosticFlags.Ruler)) { // PERF: This can almost certainly be simplified down to a single point offset and fewer calls to Draw // Top var hruler = new Ruler { Length = rect.Width, Orientation = Orientation.Horizontal }; if (Top > 0) { hruler.Draw (rect.Location); } //Left var vruler = new Ruler { Length = rect.Height - 2, Orientation = Orientation.Vertical }; if (Left > 0) { vruler.Draw (rect.Location with { Y = rect.Y + 1 }, 1); } // Bottom if (Bottom > 0) { hruler.Draw (rect.Location with { Y = rect.Y + rect.Height - 1 }); } // Right if (Right > 0) { vruler.Draw (new (rect.X + rect.Width - 1, rect.Y + 1), 1); } } if (View.Diagnostics.HasFlag (ViewDiagnosticFlags.Padding)) { // Draw the diagnostics label on the bottom var tf = new TextFormatter { Text = label is null ? string.Empty : $"{label} {this}", Alignment = TextAlignment.Centered, VerticalAlignment = VerticalTextAlignment.Bottom }; tf.Draw (rect, Application.Driver.CurrentAttribute, Application.Driver.CurrentAttribute, rect); } return GetInside (rect); } /// Determines whether the specified object is equal to the current object. /// The object to compare with the current object. /// true if the specified object is equal to the current object; otherwise, false. public override bool Equals (object obj) { //Check for null and compare run-time types. if (obj is null || !GetType ().Equals (obj.GetType ())) { return false; } return Equals ((Thickness)obj); } /// public override int GetHashCode () { var hashCode = 1380952125; hashCode = hashCode * -1521134295 + Left.GetHashCode (); hashCode = hashCode * -1521134295 + Right.GetHashCode (); hashCode = hashCode * -1521134295 + Top.GetHashCode (); hashCode = hashCode * -1521134295 + Bottom.GetHashCode (); return hashCode; } /// /// Returns a rectangle describing the location and size of the inside area of with the /// thickness widths subtracted. The height and width of the returned rectangle will never be less than 0. /// /// /// If a thickness width is negative, the inside rectangle will be larger than . e.g. a /// /// Thickness (-1, -1, -1, -1) will result in a rectangle skewed -1 in the X and Y directions and with a Size /// increased by 1. /// /// /// The source rectangle /// public Rectangle GetInside (Rectangle rect) { int x = rect.X + Left; int y = rect.Y + Top; int width = Math.Max (0, rect.Size.Width - Horizontal); int height = Math.Max (0, rect.Size.Height - Vertical); return new (x, y, width, height); } /// public static bool operator == (Thickness left, Thickness right) { return EqualityComparer.Default.Equals (left, right); } /// public static bool operator != (Thickness left, Thickness right) { return !(left == right); } /// Returns the thickness widths of the Thickness formatted as a string. /// The thickness widths as a string. public override string ToString () { return $"(Left={Left},Top={Top},Right={Right},Bottom={Bottom})"; } private int validate (int width) { if (width < 0) { throw new ArgumentException ("Thickness widths cannot be negative."); } return width; } }