Thickness.cs 9.2 KB

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