Label.cs 10 KB

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