ViewText.cs 15 KB

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