Label.cs 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354
  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
  159. .Replace ("\f", "\u21a1") // U+21A1 ↡ DOWNWARDS TWO HEADED ARROW
  160. .Replace ("\n", "\u240a") // U+240A (SYMBOL FOR LINE FEED, ␊)
  161. .Replace ("\r", "\u240d") // U+240D (SYMBOL FOR CARRIAGE RETURN, ␍)
  162. .Replace ("\t", "\u2409") // U+2409 ␉ SYMBOL FOR HORIZONTAL TABULATION
  163. .Replace ("\v", "\u240b") // U+240B ␋ SYMBOL FOR VERTICAL TABULATION
  164. .TrimSpace ();
  165. while ((end = start + margin) < text.Length) {
  166. while (text [end] != ' ' && end > start)
  167. end -= 1;
  168. if (end == start)
  169. end = start + margin;
  170. lines.Add (text [start, end]);
  171. start = end + 1;
  172. }
  173. if (start < text.Length)
  174. lines.Add (text.Substring (start));
  175. return lines;
  176. }
  177. static void Recalc (ustring textStr, List<ustring> lineResult, int width, TextAlignment talign, bool wordWrap)
  178. {
  179. lineResult.Clear ();
  180. if (wordWrap == false) {
  181. textStr = textStr.Replace ("\f", " ")
  182. .Replace ("\n", " ")
  183. .Replace ("\r", " ")
  184. .Replace ("\t", " ")
  185. .Replace ("\v", " ")
  186. .TrimSpace ();
  187. lineResult.Add (ClipAndJustify (textStr, width, talign));
  188. return;
  189. }
  190. int textLen = textStr.Length;
  191. int lp = 0;
  192. for (int i = 0; i < textLen; i++) {
  193. Rune c = textStr [i];
  194. if (c == '\n') {
  195. var wrappedLines = WordWrap (textStr [lp, i], width);
  196. foreach (var line in wrappedLines) {
  197. lineResult.Add (ClipAndJustify (line, width, talign));
  198. }
  199. if (wrappedLines.Count == 0) {
  200. lineResult.Add (ustring.Empty);
  201. }
  202. lp = i + 1;
  203. }
  204. }
  205. foreach (var line in WordWrap (textStr [lp, textLen], width)) {
  206. lineResult.Add (ClipAndJustify (line, width, talign));
  207. }
  208. }
  209. ///<inheritdoc/>
  210. public override void LayoutSubviews ()
  211. {
  212. recalcPending = true;
  213. }
  214. ///<inheritdoc/>
  215. public override void Redraw (Rect bounds)
  216. {
  217. if (recalcPending)
  218. Recalc ();
  219. if (TextColor != -1)
  220. Driver.SetAttribute (TextColor);
  221. else
  222. Driver.SetAttribute (ColorScheme.Normal);
  223. Clear ();
  224. for (int line = 0; line < lines.Count; line++) {
  225. if (line < bounds.Top || line >= bounds.Bottom)
  226. continue;
  227. var str = lines [line];
  228. int x;
  229. switch (textAlignment) {
  230. case TextAlignment.Left:
  231. x = 0;
  232. break;
  233. case TextAlignment.Justified:
  234. x = Bounds.Left;
  235. break;
  236. case TextAlignment.Right:
  237. x = Bounds.Right - str.Length;
  238. break;
  239. case TextAlignment.Centered:
  240. x = Bounds.Left + (Bounds.Width - str.Length) / 2;
  241. break;
  242. default:
  243. throw new ArgumentOutOfRangeException ();
  244. }
  245. Move (x, line);
  246. Driver.AddStr (str);
  247. }
  248. }
  249. /// <summary>
  250. /// Computes the number of lines needed to render the specified text by the <see cref="Label"/> view
  251. /// </summary>
  252. /// <returns>Number of lines.</returns>
  253. /// <param name="text">Text, may contain newlines.</param>
  254. /// <param name="width">The width for the text.</param>
  255. public static int MeasureLines (ustring text, int width)
  256. {
  257. var result = new List<ustring> ();
  258. Recalc (text, result, width, TextAlignment.Left, true);
  259. return result.Count;
  260. }
  261. /// <summary>
  262. /// Computes the max width of a line or multilines needed to render by the Label control
  263. /// </summary>
  264. /// <returns>Max width of lines.</returns>
  265. /// <param name="text">Text, may contain newlines.</param>
  266. /// <param name="width">The width for the text.</param>
  267. public static int MaxWidth (ustring text, int width)
  268. {
  269. var result = new List<ustring> ();
  270. Recalc (text, result, width, TextAlignment.Left, true);
  271. return result.Max (s => s.RuneCount);
  272. }
  273. /// <summary>
  274. /// Computes the max height of a line or multilines needed to render by the Label control
  275. /// </summary>
  276. /// <returns>Max height of lines.</returns>
  277. /// <param name="text">Text, may contain newlines.</param>
  278. /// <param name="width">The width for the text.</param>
  279. public static int MaxHeight (ustring text, int width)
  280. {
  281. var result = new List<ustring> ();
  282. Recalc (text, result, width, TextAlignment.Left, true);
  283. return result.Count;
  284. }
  285. /// <summary>
  286. /// The text displayed by the <see cref="Label"/>.
  287. /// </summary>
  288. public virtual ustring Text {
  289. get => text;
  290. set {
  291. text = value;
  292. recalcPending = true;
  293. SetNeedsDisplay ();
  294. }
  295. }
  296. /// <summary>
  297. /// Controls the text-alignment property of the label, changing it will redisplay the <see cref="Label"/>.
  298. /// </summary>
  299. /// <value>The text alignment.</value>
  300. public TextAlignment TextAlignment {
  301. get => textAlignment;
  302. set {
  303. textAlignment = value;
  304. SetNeedsDisplay ();
  305. }
  306. }
  307. Attribute textColor = -1;
  308. /// <summary>
  309. /// The color used for the <see cref="Label"/>.
  310. /// </summary>
  311. public Attribute TextColor {
  312. get => textColor;
  313. set {
  314. textColor = value;
  315. SetNeedsDisplay ();
  316. }
  317. }
  318. }
  319. }