Thickness.cs 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302
  1. using NStack;
  2. using System;
  3. using System.Collections.Generic;
  4. using System.Text;
  5. using System.Text.Json.Serialization;
  6. using Terminal.Gui.Configuration;
  7. namespace Terminal.Gui {
  8. /// <summary>
  9. /// Describes the thickness of a frame around a rectangle. Four <see cref="int"/> values describe
  10. /// the <see cref="Left"/>, <see cref="Top"/>, <see cref="Right"/>, and <see cref="Bottom"/> sides
  11. /// of the rectangle, respectively.
  12. /// </summary>
  13. /// <remarks>
  14. /// <para>
  15. /// Use the helper API (<see cref="GetInside(Rect)"/> to get the rectangle describing the insides of the frame,
  16. /// with the thickness widths subtracted.
  17. /// </para>
  18. /// <para>
  19. /// Use the helper API (<see cref="Draw(Rect, string)"/> to draw the frame with the specified thickness.
  20. /// </para>
  21. /// </remarks>
  22. public class Thickness : IEquatable<Thickness> {
  23. private int validate (int width)
  24. {
  25. if (width < 0) {
  26. throw new ArgumentException ("Thickness widths cannot be negative.");
  27. }
  28. return width;
  29. }
  30. /// <summary>
  31. /// Gets or sets the width of the left side of the rectangle.
  32. /// </summary>
  33. [JsonInclude]
  34. public int Left;
  35. /// <summary>
  36. /// Gets or sets the width of the upper side of the rectangle.
  37. /// </summary>
  38. [JsonInclude]
  39. public int Top;
  40. /// <summary>
  41. /// Gets or sets the width of the right side of the rectangle.
  42. /// </summary>
  43. [JsonInclude]
  44. public int Right;
  45. /// <summary>
  46. /// Gets or sets the width of the lower side of the rectangle.
  47. /// </summary>
  48. [JsonInclude]
  49. public int Bottom;
  50. private Rect inside;
  51. /// <summary>
  52. /// Initializes a new instance of the <see cref="Thickness"/> class with all widths
  53. /// set to 0.
  54. /// </summary>
  55. public Thickness () { }
  56. /// <summary>
  57. /// Initializes a new instance of the <see cref="Thickness"/> class with a uniform width to each side.
  58. /// </summary>
  59. /// <param name="width"></param>
  60. public Thickness (int width) : this (width, width, width, width) { }
  61. /// <summary>
  62. /// Initializes a new instance of the <see cref="Thickness"/> class that has specific
  63. /// widths applied to each side of the rectangle.
  64. /// </summary>
  65. /// <param name="left"></param>
  66. /// <param name="top"></param>
  67. /// <param name="right"></param>
  68. /// <param name="bottom"></param>
  69. public Thickness (int left, int top, int right, int bottom)
  70. {
  71. Left = left;
  72. Top = top;
  73. Right = right;
  74. Bottom = bottom;
  75. }
  76. /// <summary>
  77. /// Gets the total width of the left and right sides of the rectangle. Sets the height of the left and right sides of the rectangle to half the specified value.
  78. /// </summary>
  79. public int Vertical {
  80. get {
  81. return Top + Bottom;
  82. }
  83. set {
  84. Top = Bottom = value / 2;
  85. }
  86. }
  87. /// <summary>
  88. /// Gets the total width of the top and bottom sides of the rectangle. Sets the width of the top and bottom sides of the rectangle to half the specified value.
  89. /// </summary>
  90. public int Horizontal {
  91. get {
  92. return Left + Right;
  93. }
  94. set {
  95. Left = Right = value / 2;
  96. }
  97. }
  98. /// <summary>
  99. /// Returns a rectangle describing the location and size of the inside area of <paramref name="rect"/>
  100. /// with the thickness widths subtracted. The height and width of the returned rectangle will
  101. /// never be less than 0.
  102. /// </summary>
  103. /// <remarks>If a thickness width is negative, the inside rectangle will be larger than <paramref name="rect"/>. e.g.
  104. /// a <c>Thickness (-1, -1, -1, -1) will result in a rectangle skewed -1 in the X and Y directions and
  105. /// with a Size increased by 1.</c></remarks>
  106. /// <param name="rect">The source rectangle</param>
  107. /// <returns></returns>
  108. public Rect GetInside (Rect rect)
  109. {
  110. var x = rect.X + Left;
  111. var y = rect.Y + Top;
  112. var width = Math.Max (0, rect.Size.Width - Horizontal);
  113. var height = Math.Max (0, rect.Size.Height - Vertical);
  114. return new Rect (new Point (x, y), new Size (width, height));
  115. }
  116. /// <summary>
  117. /// Draws the <see cref="Thickness"/> rectangle with an optional diagnostics label.
  118. /// </summary>
  119. /// <remarks>
  120. /// If <see cref="ConsoleDriver.DiagnosticFlags"/> is set to <see cref="ConsoleDriver.DiagnosticFlags.FramePadding"/> then
  121. /// 'T', 'L', 'R', and 'B' glyphs will be used instead of space. If <see cref="ConsoleDriver.DiagnosticFlags"/>
  122. /// is set to <see cref="ConsoleDriver.DiagnosticFlags.FrameRuler"/> then a ruler will be drawn on the outer edge of the
  123. /// Thickness.
  124. /// </remarks>
  125. /// <param name="rect">The location and size of the rectangle that bounds the thickness rectangle, in
  126. /// screen coordinates.</param>
  127. /// <param name="label">The diagnostics label to draw on the bottom of the <see cref="Bottom"/>.</param>
  128. /// <returns>The inner rectangle remaining to be drawn.</returns>
  129. public Rect Draw (Rect rect, string label = null)
  130. {
  131. if (rect.Size.Width < 1 || rect.Size.Height < 1) {
  132. return Rect.Empty;
  133. }
  134. System.Rune clearChar = ' ';
  135. System.Rune leftChar = clearChar;
  136. System.Rune rightChar = clearChar;
  137. System.Rune topChar = clearChar;
  138. System.Rune bottomChar = clearChar;
  139. if ((ConsoleDriver.Diagnostics & ConsoleDriver.DiagnosticFlags.FramePadding) == ConsoleDriver.DiagnosticFlags.FramePadding) {
  140. leftChar = 'L';
  141. rightChar = 'R';
  142. topChar = 'T';
  143. bottomChar = 'B';
  144. if (!string.IsNullOrEmpty (label)) {
  145. leftChar = rightChar = bottomChar = topChar = label [0];
  146. }
  147. }
  148. ustring hrule = ustring.Empty;
  149. ustring vrule = ustring.Empty;
  150. if ((ConsoleDriver.Diagnostics & ConsoleDriver.DiagnosticFlags.FrameRuler) == ConsoleDriver.DiagnosticFlags.FrameRuler) {
  151. string h = "0123456789";
  152. hrule = h.Repeat ((int)Math.Ceiling ((double)(rect.Width) / (double)h.Length)) [0..(rect.Width)];
  153. string v = "0123456789";
  154. vrule = v.Repeat ((int)Math.Ceiling ((double)(rect.Height * 2) / (double)v.Length)) [0..(rect.Height * 2)];
  155. };
  156. // Draw the Top side
  157. if (Top > 0) {
  158. Application.Driver.FillRect (new Rect (rect.X, rect.Y, rect.Width, Math.Min (rect.Height, Top)), topChar);
  159. }
  160. // Draw the Left side
  161. if (Left > 0) {
  162. Application.Driver.FillRect (new Rect (rect.X, rect.Y, Math.Min (rect.Width, Left), rect.Height), leftChar);
  163. }
  164. // Draw the Right side
  165. if (Right > 0) {
  166. Application.Driver.FillRect (new Rect (Math.Max (0, rect.X + rect.Width - Right), rect.Y, Math.Min (rect.Width, Right), rect.Height), rightChar);
  167. }
  168. // Draw the Bottom side
  169. if (Bottom > 0) {
  170. Application.Driver.FillRect (new Rect (rect.X, rect.Y + Math.Max (0, rect.Height - Bottom), rect.Width, Bottom), bottomChar);
  171. }
  172. // TODO: This should be moved to LineCanvas as a new BorderStyle.Ruler
  173. if ((ConsoleDriver.Diagnostics & ConsoleDriver.DiagnosticFlags.FrameRuler) == ConsoleDriver.DiagnosticFlags.FrameRuler) {
  174. // Top
  175. Application.Driver.Move (rect.X, rect.Y);
  176. Application.Driver.AddStr (hrule);
  177. //Left
  178. for (var r = rect.Y; r < rect.Y + rect.Height; r++) {
  179. Application.Driver.Move (rect.X, r);
  180. Application.Driver.AddRune (vrule [r - rect.Y]);
  181. }
  182. // Bottom
  183. Application.Driver.Move (rect.X, rect.Y + rect.Height - Bottom + 1);
  184. Application.Driver.AddStr (hrule);
  185. // Right
  186. for (var r = rect.Y + 1; r < rect.Y + rect.Height; r++) {
  187. Application.Driver.Move (rect.X + rect.Width - Right + 1, r);
  188. Application.Driver.AddRune (vrule [r - rect.Y]);
  189. }
  190. }
  191. if ((ConsoleDriver.Diagnostics & ConsoleDriver.DiagnosticFlags.FramePadding) == ConsoleDriver.DiagnosticFlags.FramePadding) {
  192. // Draw the diagnostics label on the bottom
  193. var tf = new TextFormatter () {
  194. Text = label == null ? string.Empty : $"{label} {this}",
  195. Alignment = TextAlignment.Centered,
  196. VerticalAlignment = VerticalTextAlignment.Bottom
  197. };
  198. tf.Draw (rect, Application.Driver.CurrentAttribute, Application.Driver.CurrentAttribute, rect, false);
  199. }
  200. return GetInside (rect);
  201. }
  202. // TODO: add operator overloads
  203. /// <summary>
  204. /// Gets an empty thickness.
  205. /// </summary>
  206. public static Thickness Empty => new Thickness (0);
  207. /// <inheritdoc/>
  208. public override bool Equals (object obj)
  209. {
  210. //Check for null and compare run-time types.
  211. if ((obj == null) || !this.GetType ().Equals (obj.GetType ())) {
  212. return false;
  213. } else {
  214. return Equals ((Thickness)obj);
  215. }
  216. }
  217. /// <summary>Returns the thickness widths of the Thickness formatted as a string.</summary>
  218. /// <returns>The thickness widths as a string.</returns>
  219. public override string ToString ()
  220. {
  221. return $"(Left={Left},Top={Top},Right={Right},Bottom={Bottom})";
  222. }
  223. // IEquitable
  224. /// <inheritdoc/>
  225. public bool Equals (Thickness other)
  226. {
  227. return other is not null &&
  228. Left == other.Left &&
  229. Right == other.Right &&
  230. Top == other.Top &&
  231. Bottom == other.Bottom;
  232. }
  233. /// <inheritdoc/>
  234. public override int GetHashCode ()
  235. {
  236. int hashCode = 1380952125;
  237. hashCode = hashCode * -1521134295 + Left.GetHashCode ();
  238. hashCode = hashCode * -1521134295 + Right.GetHashCode ();
  239. hashCode = hashCode * -1521134295 + Top.GetHashCode ();
  240. hashCode = hashCode * -1521134295 + Bottom.GetHashCode ();
  241. return hashCode;
  242. }
  243. /// <inheritdoc/>
  244. public static bool operator == (Thickness left, Thickness right)
  245. {
  246. return EqualityComparer<Thickness>.Default.Equals (left, right);
  247. }
  248. /// <inheritdoc/>
  249. public static bool operator != (Thickness left, Thickness right)
  250. {
  251. return !(left == right);
  252. }
  253. }
  254. internal static class StringExtensions {
  255. public static string Repeat (this string instr, int n)
  256. {
  257. if (n <= 0) {
  258. return null;
  259. }
  260. if (string.IsNullOrEmpty (instr) || n == 1) {
  261. return instr;
  262. }
  263. return new StringBuilder (instr.Length * n)
  264. .Insert (0, instr, n)
  265. .ToString ();
  266. }
  267. }
  268. }