Label.cs 9.9 KB

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