2
0

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