using System.Numerics; 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. /// /// /// Thickness uses intenrally. As a result, there is a potential precision loss for very /// large numbers. This is typically not an issue for UI dimensions but could be relevant in other contexts. /// /// public record struct Thickness { /// Initializes a new instance of the class with all widths set to 0. public Thickness () { _sides = Vector4.Zero; } /// 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; } private Vector4 _sides; /// /// Adds the thickness widths of another to the current , returning a /// new . /// /// /// public readonly Thickness Add (Thickness other) { return new (Left + other.Left, Top + other.Top, Right + other.Right, Bottom + other.Bottom); } /// Gets or sets the width of the lower side of the rectangle. [JsonInclude] public int Bottom { readonly get => (int)_sides.W; set => _sides.W = value; } /// /// Gets whether the specified coordinates lie within the thickness (inside the bounding rectangle but outside /// the rectangle described by . /// /// Describes the location and size of the rectangle that contains the thickness. /// The coordinate to check. /// if the specified coordinate is within the thickness; otherwise. public bool Contains (in Rectangle outside, in Point location) { Rectangle inside = GetInside (outside); return outside.Contains (location) && !inside.Contains (location); } /// Draws the rectangle with an optional diagnostics label. /// /// If is set to /// then 'T', 'L', 'R', and 'B' glyphs will be used instead of /// space. If is set to /// then a ruler will be drawn on the outer edge of the /// Thickness. /// /// 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, ViewDiagnosticFlags diagnosticFlags = ViewDiagnosticFlags.Off, 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 (diagnosticFlags.HasFlag (ViewDiagnosticFlags.Thickness)) { 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 // 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 (diagnosticFlags.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 (diagnosticFlags.HasFlag (ViewDiagnosticFlags.Thickness)) { // Draw the diagnostics label on the bottom string text = label is null ? string.Empty : $"{label} {this}"; var tf = new TextFormatter { Text = text, Alignment = Alignment.Center, VerticalAlignment = Alignment.End, ConstrainToWidth = text.GetColumns (), ConstrainToHeight = 1 }; if (Application.Driver?.CurrentAttribute is { }) { tf.Draw (rect, Application.Driver!.CurrentAttribute, Application.Driver!.CurrentAttribute, rect); } } return GetInside (rect); } /// Gets an empty thickness. public static Thickness Empty => new (0); /// /// 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); } /// /// 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 or sets the width of the left side of the rectangle. [JsonInclude] public int Left { readonly get => (int)_sides.X; set => _sides.X = value; } /// /// Adds the thickness widths of another to another . /// /// /// /// public static Thickness operator + (Thickness a, Thickness b) { return a.Add (b); } /// Gets or sets the width of the right side of the rectangle. [JsonInclude] public int Right { readonly get => (int)_sides.Z; set => _sides.Z = value; } /// Gets or sets the width of the upper side of the rectangle. [JsonInclude] public int Top { readonly get => (int)_sides.Y; set => _sides.Y = value; } /// 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})"; } /// /// 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; } }