ViewText.cs 14 KB

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