ViewText.cs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405
  1. using static Terminal.Gui.SpinnerStyle;
  2. namespace Terminal.Gui;
  3. public partial class View
  4. {
  5. private string _text;
  6. /// <summary>
  7. /// Gets or sets whether trailing spaces at the end of word-wrapped lines are preserved
  8. /// or not when <see cref="TextFormatter.WordWrap"/> is enabled.
  9. /// If <see langword="true"/> trailing spaces at the end of wrapped lines will be removed when
  10. /// <see cref="Text"/> is formatted for display. The default is <see langword="false"/>.
  11. /// </summary>
  12. public virtual bool PreserveTrailingSpaces
  13. {
  14. get => TextFormatter.PreserveTrailingSpaces;
  15. set
  16. {
  17. if (TextFormatter.PreserveTrailingSpaces != value)
  18. {
  19. TextFormatter.PreserveTrailingSpaces = value;
  20. TextFormatter.NeedsFormat = true;
  21. }
  22. }
  23. }
  24. /// <summary>
  25. /// The text displayed by the <see cref="View"/>.
  26. /// </summary>
  27. /// <remarks>
  28. /// <para>
  29. /// The text will be drawn before any subviews are drawn.
  30. /// </para>
  31. /// <para>
  32. /// The text will be drawn starting at the view origin (0, 0) and will be formatted according
  33. /// to <see cref="TextAlignment"/> and <see cref="TextDirection"/>.
  34. /// </para>
  35. /// <para>
  36. /// The text will word-wrap to additional lines if it does not fit horizontally. If <see cref="ContentSize"/>'s height
  37. /// is 1, the text will be clipped.
  38. /// </para>
  39. /// <para>If <see cref="AutoSize"/> is <c>true</c>, the <see cref="ContentSize"/> will be adjusted to fit the text.</para>
  40. /// <para>When the text changes, the <see cref="TextChanged"/> is fired.</para>
  41. /// </remarks>
  42. public virtual string Text
  43. {
  44. get => _text;
  45. set
  46. {
  47. string old = _text;
  48. _text = value;
  49. UpdateTextFormatterText ();
  50. OnResizeNeeded ();
  51. #if DEBUG
  52. if (_text is { } && string.IsNullOrEmpty (Id))
  53. {
  54. Id = _text;
  55. }
  56. #endif
  57. OnTextChanged (old, Text);
  58. }
  59. }
  60. /// <summary>
  61. /// Called when the <see cref="Text"/> has changed. Fires the <see cref="TextChanged"/> event.
  62. /// </summary>
  63. /// <param name="oldValue"></param>
  64. /// <param name="newValue"></param>
  65. public void OnTextChanged (string oldValue, string newValue)
  66. {
  67. TextChanged?.Invoke (this, new StateEventArgs<string> (oldValue, newValue));
  68. }
  69. /// <summary>
  70. /// Text changed event, raised when the text has changed.
  71. /// </summary>
  72. public event EventHandler<StateEventArgs<string>> TextChanged;
  73. /// <summary>
  74. /// Gets or sets how the View's <see cref="Text"/> is aligned horizontally when drawn. Changing this property will
  75. /// redisplay the <see cref="View"/>.
  76. /// </summary>
  77. /// <remarks>
  78. /// <para>If <see cref="AutoSize"/> is <c>true</c>, the <see cref="ContentSize"/> will be adjusted to fit the text.</para>
  79. /// </remarks>
  80. /// <value>The text alignment.</value>
  81. public virtual TextAlignment TextAlignment
  82. {
  83. get => TextFormatter.Alignment;
  84. set
  85. {
  86. TextFormatter.Alignment = value;
  87. UpdateTextFormatterText ();
  88. OnResizeNeeded ();
  89. }
  90. }
  91. /// <summary>
  92. /// Gets or sets the direction of the View's <see cref="Text"/>. Changing this property will redisplay the
  93. /// <see cref="View"/>.
  94. /// </summary>
  95. /// <remarks>
  96. /// <para>If <see cref="AutoSize"/> is <c>true</c>, the <see cref="ContentSize"/> will be adjusted to fit the text.</para>
  97. /// </remarks>
  98. /// <value>The text alignment.</value>
  99. public virtual TextDirection TextDirection
  100. {
  101. get => TextFormatter.Direction;
  102. set
  103. {
  104. UpdateTextDirection (value);
  105. TextFormatter.Direction = value;
  106. }
  107. }
  108. /// <summary>
  109. /// Gets or sets the <see cref="Gui.TextFormatter"/> used to format <see cref="Text"/>.
  110. /// </summary>
  111. public TextFormatter TextFormatter { get; init; } = new ();
  112. /// <summary>
  113. /// Gets or sets how the View's <see cref="Text"/> is aligned vertically when drawn. Changing this property will
  114. /// redisplay
  115. /// the <see cref="View"/>.
  116. /// </summary>
  117. /// <remarks>
  118. /// <para>If <see cref="AutoSize"/> is <c>true</c>, the <see cref="ContentSize"/> will be adjusted to fit the text.</para>
  119. /// </remarks>
  120. /// <value>The text alignment.</value>
  121. public virtual VerticalTextAlignment VerticalTextAlignment
  122. {
  123. get => TextFormatter.VerticalAlignment;
  124. set
  125. {
  126. TextFormatter.VerticalAlignment = value;
  127. SetNeedsDisplay ();
  128. }
  129. }
  130. /// <summary>
  131. /// Gets the width or height of the <see cref="TextFormatter.HotKeySpecifier"/> characters
  132. /// in the <see cref="Text"/> property.
  133. /// </summary>
  134. /// <remarks>
  135. /// Only the first HotKey specifier found in <see cref="Text"/> is supported.
  136. /// </remarks>
  137. /// <param name="isWidth">
  138. /// If <see langword="true"/> (the default) the width required for the HotKey specifier is returned. Otherwise the
  139. /// height
  140. /// is returned.
  141. /// </param>
  142. /// <returns>
  143. /// The number of characters required for the <see cref="TextFormatter.HotKeySpecifier"/>. If the text
  144. /// direction specified
  145. /// by <see cref="TextDirection"/> does not match the <paramref name="isWidth"/> parameter, <c>0</c> is returned.
  146. /// </returns>
  147. public int GetHotKeySpecifierLength (bool isWidth = true)
  148. {
  149. if (isWidth)
  150. {
  151. return TextFormatter.IsHorizontalDirection (TextDirection) && TextFormatter.Text?.Contains ((char)HotKeySpecifier.Value) == true
  152. ? Math.Max (HotKeySpecifier.GetColumns (), 0)
  153. : 0;
  154. }
  155. return TextFormatter.IsVerticalDirection (TextDirection) && TextFormatter.Text?.Contains ((char)HotKeySpecifier.Value) == true
  156. ? Math.Max (HotKeySpecifier.GetColumns (), 0)
  157. : 0;
  158. }
  159. ///// <summary>
  160. ///// Gets dimensions required to fit <see cref="Text"/> within <see cref="ContentSize"/> using the text
  161. ///// <see cref="NavigationDirection"/> specified by the <see cref="TextFormatter"/> property and accounting for any
  162. ///// <see cref="HotKeySpecifier"/> characters.
  163. ///// </summary>
  164. ///// <remarks>
  165. ///// </remarks>
  166. ///// <returns>The <see cref="Size"/> of the <see cref="ContentSize"/> required to fit the formatted text.</returns>
  167. //public Size GetTextAutoSize ()
  168. //{
  169. // var x = 0;
  170. // var y = 0;
  171. // if (IsInitialized)
  172. // {
  173. // x = Viewport.X;
  174. // y = Viewport.Y;
  175. // }
  176. // // Get the size of the text without the hot key specifier
  177. // Rectangle rect = TextFormatter.CalcRect (x, y, TextFormatter.Text, TextFormatter.Direction);
  178. // int newWidth = rect.Size.Width - GetHotKeySpecifierLength ();
  179. // int newHeight = rect.Size.Height - GetHotKeySpecifierLength (false);
  180. // return new (newWidth, newHeight);
  181. //}
  182. /// <summary>
  183. /// Can be overridden if the <see cref="Terminal.Gui.TextFormatter.Text"/> has
  184. /// different format than the default.
  185. /// </summary>
  186. protected virtual void UpdateTextFormatterText ()
  187. {
  188. if (TextFormatter is { })
  189. {
  190. TextFormatter.Text = _text;
  191. }
  192. }
  193. /// <summary>
  194. /// Gets the dimensions required for <see cref="Text"/> ignoring a <see cref="TextFormatter.HotKeySpecifier"/>.
  195. /// </summary>
  196. /// <returns></returns>
  197. internal Size GetSizeNeededForTextWithoutHotKey ()
  198. {
  199. return new Size (
  200. TextFormatter.Size.Width - GetHotKeySpecifierLength (),
  201. TextFormatter.Size.Height - GetHotKeySpecifierLength (false));
  202. }
  203. /// <summary>
  204. /// Internal API. Sets <see cref="TextFormatter"/>.Size to the current <see cref="Viewport"/> size, adjusted for
  205. /// <see cref="TextFormatter.HotKeySpecifier"/>.
  206. /// </summary>
  207. /// <remarks>
  208. /// Use this API to set <see cref="TextFormatter.Size"/> when the view has changed such that the
  209. /// size required to fit the text has changed.
  210. /// changes.
  211. /// </remarks>
  212. /// <returns></returns>
  213. internal void SetTextFormatterSize ()
  214. {
  215. UpdateTextFormatterText ();
  216. //if (!IsInitialized)
  217. //{
  218. // return;
  219. //}
  220. //Dim.DimAuto widthAuto = Width as Dim.DimAuto;
  221. //Dim.DimAuto heightAuto = Height as Dim.DimAuto;
  222. // TODO: This is a hack. Figure out how to move this into DimDimAuto
  223. if ((Width is Dim.DimAuto widthAuto && widthAuto._style != Dim.DimAutoStyle.Subviews)
  224. || (Height is Dim.DimAuto heightAuto && heightAuto._style != Dim.DimAutoStyle.Subviews))
  225. {
  226. // This updates TextFormatter.Size to the text size
  227. TextFormatter.AutoSize = true;
  228. // Whenever DimAutoStyle.Text is set, ContentSize will match TextFormatter.Size.
  229. ContentSize = TextFormatter.Size;
  230. return;
  231. }
  232. TextFormatter.AutoSize = false;
  233. TextFormatter.Size = new Size (ContentSize.Width, ContentSize.Height);
  234. }
  235. ////private bool IsValidAutoSize (out Size autoSize)
  236. ////{
  237. //// Rectangle rect = TextFormatter.CalcRect (_frame.X, _frame.Y, TextFormatter.Text, TextDirection);
  238. //// autoSize = new Size (
  239. //// rect.Size.Width - GetHotKeySpecifierLength (),
  240. //// rect.Size.Height - GetHotKeySpecifierLength (false));
  241. //// return !((ValidatePosDim && (!(Width is Dim.DimAbsolute) || !(Height is Dim.DimAbsolute)))
  242. //// || _frame.Size.Width != rect.Size.Width - GetHotKeySpecifierLength ()
  243. //// || _frame.Size.Height != rect.Size.Height - GetHotKeySpecifierLength (false));
  244. ////}
  245. //private bool IsValidAutoSizeHeight (Dim height)
  246. //{
  247. // Rectangle rect = TextFormatter.CalcRect (_frame.X, _frame.Y, TextFormatter.Text, TextDirection);
  248. // int dimValue = height.Anchor (0);
  249. // return !((ValidatePosDim && !(height is Dim.DimAbsolute)) || dimValue != rect.Size.Height - GetHotKeySpecifierLength (false));
  250. //}
  251. //private bool IsValidAutoSizeWidth (Dim width)
  252. //{
  253. // Rectangle rect = TextFormatter.CalcRect (_frame.X, _frame.Y, TextFormatter.Text, TextDirection);
  254. // int dimValue = width.Anchor (0);
  255. // return !((ValidatePosDim && !(width is Dim.DimAbsolute)) || dimValue != rect.Size.Width - GetHotKeySpecifierLength ());
  256. //}
  257. ///// <summary>
  258. ///// Sets the size of the View to the minimum width or height required to fit <see cref="Text"/>.
  259. ///// </summary>
  260. ///// <returns>
  261. ///// <see langword="true"/> if the size was changed; <see langword="false"/> if <see cref="AutoSize"/> ==
  262. ///// <see langword="true"/> or
  263. ///// <see cref="Text"/> will not fit.
  264. ///// </returns>
  265. ///// <remarks>
  266. ///// Always returns <see langword="false"/> if <see cref="AutoSize"/> is <see langword="true"/> or
  267. ///// if <see cref="Height"/> (Horizontal) or <see cref="Width"/> (Vertical) are not not set or zero.
  268. ///// Does not take into account word wrapping.
  269. ///// </remarks>
  270. //private bool SetFrameToFitText ()
  271. //{
  272. // if (AutoSize == false)
  273. // {
  274. // throw new InvalidOperationException ("SetFrameToFitText can only be called when AutoSize is true");
  275. // }
  276. // // BUGBUG: This API is broken - should not assume Frame.Height == ContentSize.Height
  277. // // <summary>
  278. // // Gets the minimum dimensions required to fit the View's <see cref="Text"/>, factoring in <see cref="TextDirection"/>.
  279. // // </summary>
  280. // // <param name="sizeRequired">The minimum dimensions required.</param>
  281. // // <returns><see langword="true"/> if the dimensions fit within the View's <see cref="ContentSize"/>, <see langword="false"/> otherwise.</returns>
  282. // // <remarks>
  283. // // Always returns <see langword="false"/> if <see cref="AutoSize"/> is <see langword="true"/> or
  284. // // if <see cref="Height"/> (Horizontal) or <see cref="Width"/> (Vertical) are not not set or zero.
  285. // // Does not take into account word wrapping.
  286. // // </remarks>
  287. // bool GetMinimumSizeOfText (out Size sizeRequired)
  288. // {
  289. // if (!IsInitialized)
  290. // {
  291. // sizeRequired = Size.Empty;
  292. // return false;
  293. // }
  294. // sizeRequired = ContentSize;
  295. // if (AutoSize || string.IsNullOrEmpty (TextFormatter.Text))
  296. // {
  297. // return false;
  298. // }
  299. // switch (TextFormatter.IsVerticalDirection (TextDirection))
  300. // {
  301. // case true:
  302. // int colWidth = TextFormatter.GetSumMaxCharWidth (TextFormatter.Text, 0, 1);
  303. // // TODO: v2 - This uses frame.Width; it should only use ContentSize
  304. // if (_frame.Width < colWidth
  305. // && (Width is null || (ContentSize.Width >= 0 && Width is Dim.DimAbsolute && Width.Anchor (0) >= 0 && Width.Anchor (0) < colWidth)))
  306. // {
  307. // sizeRequired = new (colWidth, ContentSize.Height);
  308. // return true;
  309. // }
  310. // break;
  311. // default:
  312. // if (_frame.Height < 1 && (Height is null || (Height is Dim.DimAbsolute && Height.Anchor (0) == 0)))
  313. // {
  314. // sizeRequired = new (ContentSize.Width, 1);
  315. // return true;
  316. // }
  317. // break;
  318. // }
  319. // return false;
  320. // }
  321. // if (GetMinimumSizeOfText (out Size size))
  322. // {
  323. // // TODO: This is a hack.
  324. // //_width = size.Width;
  325. // //_height = size.Height;
  326. // SetFrame (new (_frame.Location, size));
  327. // //throw new InvalidOperationException ("This is a hack.");
  328. // return true;
  329. // }
  330. // return false;
  331. //}
  332. private void UpdateTextDirection (TextDirection newDirection)
  333. {
  334. bool directionChanged = TextFormatter.IsHorizontalDirection (TextFormatter.Direction) != TextFormatter.IsHorizontalDirection (newDirection);
  335. TextFormatter.Direction = newDirection;
  336. //bool isValidOldAutoSize = AutoSize && IsValidAutoSize (out Size _);
  337. UpdateTextFormatterText ();
  338. if (directionChanged)
  339. {
  340. OnResizeNeeded ();
  341. }
  342. //if ((!ValidatePosDim && directionChanged && AutoSize) || (ValidatePosDim && directionChanged && AutoSize && isValidOldAutoSize))
  343. //{
  344. // OnResizeNeeded ();
  345. //}
  346. //else if (directionChanged && IsAdded)
  347. //{
  348. // ResizeViewportToFit (Viewport.Size);
  349. //}
  350. SetTextFormatterSize ();
  351. SetNeedsDisplay ();
  352. }
  353. }