Label.cs 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353
  1. //
  2. // Label.cs: Label control
  3. //
  4. // Authors:
  5. // Miguel de Icaza ([email protected])
  6. //
  7. using System;
  8. using System.Collections.Generic;
  9. using System.Linq;
  10. using System.Text.RegularExpressions;
  11. using NStack;
  12. namespace Terminal.Gui {
  13. /// <summary>
  14. /// The Label <see cref="View"/> displays a string at a given position and supports multiple lines separted by newline characters. Multi-line Labels support word wrap.
  15. /// </summary>
  16. public class Label : View {
  17. List<ustring> lines = new List<ustring> ();
  18. bool recalcPending = true;
  19. ustring text;
  20. TextAlignment textAlignment;
  21. static Rect CalcRect (int x, int y, ustring s)
  22. {
  23. int mw = 0;
  24. int ml = 1;
  25. int cols = 0;
  26. foreach (var rune in s) {
  27. if (rune == '\n') {
  28. ml++;
  29. if (cols > mw)
  30. mw = cols;
  31. cols = 0;
  32. } else
  33. cols++;
  34. }
  35. if (cols > mw)
  36. mw = cols;
  37. return new Rect (x, y, mw, ml);
  38. }
  39. /// <summary>
  40. /// Initializes a new instance of <see cref="Label"/> using <see cref="LayoutStyle.Absolute"/> layout.
  41. /// </summary>
  42. /// <remarks>
  43. /// <para>
  44. /// The <see cref="Label"/> will be created at the given
  45. /// coordinates with the given string. The size (<see cref="View.Frame"/> will be
  46. /// adjusted to fit the contents of <see cref="Text"/>, including newlines ('\n') for multiple lines.
  47. /// </para>
  48. /// <para>
  49. /// No line wrapping is provided.
  50. /// </para>
  51. /// </remarks>
  52. /// <param name="x">column to locate the Label.</param>
  53. /// <param name="y">row to locate the Label.</param>
  54. /// <param name="text">text to initialize the <see cref="Text"/> property with.</param>
  55. public Label (int x, int y, ustring text) : this (CalcRect (x, y, text), text)
  56. {
  57. }
  58. /// <summary>
  59. /// Initializes a new instance of <see cref="Label"/> using <see cref="LayoutStyle.Absolute"/> layout.
  60. /// </summary>
  61. /// <remarks>
  62. /// <para>
  63. /// The <see cref="Label"/> will be created at the given
  64. /// coordinates with the given string. The initial size (<see cref="View.Frame"/> will be
  65. /// adjusted to fit the contents of <see cref="Text"/>, including newlines ('\n') for multiple lines.
  66. /// </para>
  67. /// <para>
  68. /// If <c>rect.Height</c> is greater than one, word wrapping is provided.
  69. /// </para>
  70. /// </remarks>
  71. /// <param name="rect">Location.</param>
  72. /// <param name="text">text to initialize the <see cref="Text"/> property with.</param>
  73. public Label (Rect rect, ustring text) : base (rect)
  74. {
  75. this.text = text;
  76. }
  77. /// <summary>
  78. /// Initializes a new instance of <see cref="Label"/> using <see cref="LayoutStyle.Computed"/> layout.
  79. /// </summary>
  80. /// <remarks>
  81. /// <para>
  82. /// The <see cref="Label"/> will be created using <see cref="LayoutStyle.Computed"/>
  83. /// coordinates with the given string. The initial size (<see cref="View.Frame"/> will be
  84. /// adjusted to fit the contents of <see cref="Text"/>, including newlines ('\n') for multiple lines.
  85. /// </para>
  86. /// <para>
  87. /// If <c>Height</c> is greater than one, word wrapping is provided.
  88. /// </para>
  89. /// </remarks>
  90. /// <param name="text">text to initialize the <see cref="Text"/> property with.</param>
  91. public Label (ustring text) : base ()
  92. {
  93. this.text = text;
  94. var r = CalcRect (0, 0, text);
  95. Width = r.Width;
  96. Height = r.Height;
  97. }
  98. /// <summary>
  99. /// Initializes a new instance of <see cref="Label"/> using <see cref="LayoutStyle.Computed"/> layout.
  100. /// </summary>
  101. /// <remarks>
  102. /// <para>
  103. /// The <see cref="Label"/> will be created using <see cref="LayoutStyle.Computed"/>
  104. /// coordinates. The initial size (<see cref="View.Frame"/> will be
  105. /// adjusted to fit the contents of <see cref="Text"/>, including newlines ('\n') for multiple lines.
  106. /// </para>
  107. /// <para>
  108. /// If <c>Height</c> is greater than one, word wrapping is provided.
  109. /// </para>
  110. /// </remarks>
  111. public Label () : this (text: string.Empty) { }
  112. static char [] whitespace = new char [] { ' ', '\t' };
  113. static ustring ClipAndJustify (ustring str, int width, TextAlignment talign)
  114. {
  115. // Get rid of any '\r' added by Windows
  116. str = str.Replace ("\r", ustring.Empty);
  117. int slen = str.RuneCount;
  118. if (slen > width) {
  119. var uints = str.ToRunes (width);
  120. var runes = new Rune [uints.Length];
  121. for (int i = 0; i < uints.Length; i++)
  122. runes [i] = uints [i];
  123. return ustring.Make (runes);
  124. } else {
  125. if (talign == TextAlignment.Justified) {
  126. // TODO: ustring needs this
  127. var words = str.ToString ().Split (whitespace, StringSplitOptions.RemoveEmptyEntries);
  128. int textCount = words.Sum (arg => arg.Length);
  129. var spaces = words.Length > 1 ? (width - textCount) / (words.Length - 1) : 0;
  130. var extras = words.Length > 1 ? (width - textCount) % words.Length : 0;
  131. var s = new System.Text.StringBuilder ();
  132. //s.Append ($"tc={textCount} sp={spaces},x={extras} - ");
  133. for (int w = 0; w < words.Length; w++) {
  134. var x = words [w];
  135. s.Append (x);
  136. if (w + 1 < words.Length)
  137. for (int i = 0; i < spaces; i++)
  138. s.Append (' ');
  139. if (extras > 0) {
  140. //s.Append ('_');
  141. extras--;
  142. }
  143. }
  144. return ustring.Make (s.ToString ());
  145. }
  146. return str;
  147. }
  148. }
  149. void Recalc ()
  150. {
  151. recalcPending = false;
  152. Recalc (text, lines, Frame.Width, textAlignment, Bounds.Height > 1);
  153. }
  154. static List<ustring> WordWrap (ustring text, int margin)
  155. {
  156. int start = 0, end;
  157. var lines = new List<ustring> ();
  158. text = text.Replace ("\f", " ")
  159. .Replace ("\n", " ")
  160. .Replace ("\r", " ")
  161. .Replace ("\t", " ")
  162. .Replace ("\v", " ")
  163. .TrimSpace ();
  164. while ((end = start + margin) < text.Length) {
  165. while (text [end] != ' ' && end > start)
  166. end -= 1;
  167. if (end == start)
  168. end = start + margin;
  169. lines.Add (text [start, end]);
  170. start = end + 1;
  171. }
  172. if (start < text.Length)
  173. lines.Add (text.Substring (start));
  174. return lines;
  175. }
  176. static void Recalc (ustring textStr, List<ustring> lineResult, int width, TextAlignment talign, bool wordWrap)
  177. {
  178. lineResult.Clear ();
  179. if (wordWrap == false) {
  180. textStr = textStr.Replace ("\f", " ")
  181. .Replace ("\n", " ")
  182. .Replace ("\r", " ")
  183. .Replace ("\t", " ")
  184. .Replace ("\v", " ")
  185. .TrimSpace ();
  186. lineResult.Add (ClipAndJustify (textStr, width, talign));
  187. return;
  188. }
  189. int textLen = textStr.Length;
  190. int lp = 0;
  191. for (int i = 0; i < textLen; i++) {
  192. Rune c = textStr [i];
  193. if (c == '\n') {
  194. var wrappedLines = WordWrap (textStr [lp, i], width);
  195. foreach (var line in wrappedLines) {
  196. lineResult.Add (ClipAndJustify (line, width, talign));
  197. }
  198. if (wrappedLines.Count == 0) {
  199. lineResult.Add (ustring.Empty);
  200. }
  201. lp = i + 1;
  202. }
  203. }
  204. foreach (var line in WordWrap (textStr [lp, textLen], width)) {
  205. lineResult.Add (ClipAndJustify (line, width, talign));
  206. }
  207. }
  208. ///<inheritdoc/>
  209. public override void LayoutSubviews ()
  210. {
  211. recalcPending = true;
  212. }
  213. ///<inheritdoc/>
  214. public override void Redraw (Rect bounds)
  215. {
  216. if (recalcPending)
  217. Recalc ();
  218. if (TextColor != -1)
  219. Driver.SetAttribute (TextColor);
  220. else
  221. Driver.SetAttribute (ColorScheme.Normal);
  222. Clear ();
  223. for (int line = 0; line < lines.Count; line++) {
  224. if (line < bounds.Top || line >= bounds.Bottom)
  225. continue;
  226. var str = lines [line];
  227. int x;
  228. switch (textAlignment) {
  229. case TextAlignment.Left:
  230. x = 0;
  231. break;
  232. case TextAlignment.Justified:
  233. x = Bounds.Left;
  234. break;
  235. case TextAlignment.Right:
  236. x = Bounds.Right - str.Length;
  237. break;
  238. case TextAlignment.Centered:
  239. x = Bounds.Left + (Bounds.Width - str.Length) / 2;
  240. break;
  241. default:
  242. throw new ArgumentOutOfRangeException ();
  243. }
  244. Move (x, line);
  245. Driver.AddStr (str);
  246. }
  247. }
  248. /// <summary>
  249. /// Computes the number of lines needed to render the specified text by the <see cref="Label"/> view
  250. /// </summary>
  251. /// <returns>Number of lines.</returns>
  252. /// <param name="text">Text, may contain newlines.</param>
  253. /// <param name="width">The width for the text.</param>
  254. public static int MeasureLines (ustring text, int width)
  255. {
  256. var result = new List<ustring> ();
  257. Recalc (text, result, width, TextAlignment.Left, true);
  258. return result.Count;
  259. }
  260. /// <summary>
  261. /// Computes the max width of a line or multilines needed to render by the Label control
  262. /// </summary>
  263. /// <returns>Max width of lines.</returns>
  264. /// <param name="text">Text, may contain newlines.</param>
  265. /// <param name="width">The width for the text.</param>
  266. public static int MaxWidth (ustring text, int width)
  267. {
  268. var result = new List<ustring> ();
  269. Recalc (text, result, width, TextAlignment.Left, true);
  270. return result.Max (s => s.RuneCount);
  271. }
  272. /// <summary>
  273. /// Computes the max height of a line or multilines needed to render by the Label control
  274. /// </summary>
  275. /// <returns>Max height of lines.</returns>
  276. /// <param name="text">Text, may contain newlines.</param>
  277. /// <param name="width">The width for the text.</param>
  278. public static int MaxHeight (ustring text, int width)
  279. {
  280. var result = new List<ustring> ();
  281. Recalc (text, result, width, TextAlignment.Left, true);
  282. return result.Count;
  283. }
  284. /// <summary>
  285. /// The text displayed by the <see cref="Label"/>.
  286. /// </summary>
  287. public virtual ustring Text {
  288. get => text;
  289. set {
  290. text = value;
  291. recalcPending = true;
  292. SetNeedsDisplay ();
  293. }
  294. }
  295. /// <summary>
  296. /// Controls the text-alignment property of the label, changing it will redisplay the <see cref="Label"/>.
  297. /// </summary>
  298. /// <value>The text alignment.</value>
  299. public TextAlignment TextAlignment {
  300. get => textAlignment;
  301. set {
  302. textAlignment = value;
  303. SetNeedsDisplay ();
  304. }
  305. }
  306. Attribute textColor = -1;
  307. /// <summary>
  308. /// The color used for the <see cref="Label"/>.
  309. /// </summary>
  310. public Attribute TextColor {
  311. get => textColor;
  312. set {
  313. textColor = value;
  314. SetNeedsDisplay ();
  315. }
  316. }
  317. }
  318. }