ViewText.cs 15 KB

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