StandardTextFormatter.cs 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336
  1. #nullable enable
  2. using System;
  3. using System.Collections.Generic;
  4. using System.Drawing;
  5. using System.Linq;
  6. using System.Text;
  7. namespace Terminal.Gui.Text;
  8. /// <summary>
  9. /// Standard implementation of <see cref="ITextFormatter"/> that provides the same functionality
  10. /// as the original TextFormatter but with proper separation of concerns.
  11. /// </summary>
  12. public class StandardTextFormatter : ITextFormatter
  13. {
  14. private string _text = string.Empty;
  15. private Size? _constrainToSize;
  16. private Alignment _alignment = Alignment.Start;
  17. private Alignment _verticalAlignment = Alignment.Start;
  18. private TextDirection _direction = TextDirection.LeftRight_TopBottom;
  19. private bool _wordWrap = true;
  20. private bool _multiLine = true;
  21. private Rune _hotKeySpecifier = (Rune)0xFFFF;
  22. private int _tabWidth = 4;
  23. private bool _preserveTrailingSpaces = false;
  24. // Caching
  25. private FormattedText? _cachedResult;
  26. private int _cacheHash;
  27. /// <inheritdoc />
  28. public string Text
  29. {
  30. get => _text;
  31. set
  32. {
  33. if (_text != value)
  34. {
  35. _text = value ?? string.Empty;
  36. InvalidateCache();
  37. }
  38. }
  39. }
  40. /// <inheritdoc />
  41. public Size? ConstrainToSize
  42. {
  43. get => _constrainToSize;
  44. set
  45. {
  46. if (_constrainToSize != value)
  47. {
  48. _constrainToSize = value;
  49. InvalidateCache();
  50. }
  51. }
  52. }
  53. /// <inheritdoc />
  54. public Alignment Alignment
  55. {
  56. get => _alignment;
  57. set
  58. {
  59. if (_alignment != value)
  60. {
  61. _alignment = value;
  62. InvalidateCache();
  63. }
  64. }
  65. }
  66. /// <inheritdoc />
  67. public Alignment VerticalAlignment
  68. {
  69. get => _verticalAlignment;
  70. set
  71. {
  72. if (_verticalAlignment != value)
  73. {
  74. _verticalAlignment = value;
  75. InvalidateCache();
  76. }
  77. }
  78. }
  79. /// <inheritdoc />
  80. public TextDirection Direction
  81. {
  82. get => _direction;
  83. set
  84. {
  85. if (_direction != value)
  86. {
  87. _direction = value;
  88. InvalidateCache();
  89. }
  90. }
  91. }
  92. /// <inheritdoc />
  93. public bool WordWrap
  94. {
  95. get => _wordWrap;
  96. set
  97. {
  98. if (_wordWrap != value)
  99. {
  100. _wordWrap = value;
  101. InvalidateCache();
  102. }
  103. }
  104. }
  105. /// <inheritdoc />
  106. public bool MultiLine
  107. {
  108. get => _multiLine;
  109. set
  110. {
  111. if (_multiLine != value)
  112. {
  113. _multiLine = value;
  114. InvalidateCache();
  115. }
  116. }
  117. }
  118. /// <inheritdoc />
  119. public Rune HotKeySpecifier
  120. {
  121. get => _hotKeySpecifier;
  122. set
  123. {
  124. if (_hotKeySpecifier.Value != value.Value)
  125. {
  126. _hotKeySpecifier = value;
  127. InvalidateCache();
  128. }
  129. }
  130. }
  131. /// <inheritdoc />
  132. public int TabWidth
  133. {
  134. get => _tabWidth;
  135. set
  136. {
  137. if (_tabWidth != value)
  138. {
  139. _tabWidth = value;
  140. InvalidateCache();
  141. }
  142. }
  143. }
  144. /// <inheritdoc />
  145. public bool PreserveTrailingSpaces
  146. {
  147. get => _preserveTrailingSpaces;
  148. set
  149. {
  150. if (_preserveTrailingSpaces != value)
  151. {
  152. _preserveTrailingSpaces = value;
  153. InvalidateCache();
  154. }
  155. }
  156. }
  157. /// <inheritdoc />
  158. public FormattedText Format()
  159. {
  160. // Check cache first
  161. int currentHash = GetSettingsHash();
  162. if (_cachedResult != null && _cacheHash == currentHash)
  163. {
  164. return _cachedResult;
  165. }
  166. // Perform formatting
  167. var result = DoFormat();
  168. // Update cache
  169. _cachedResult = result;
  170. _cacheHash = currentHash;
  171. return result;
  172. }
  173. /// <inheritdoc />
  174. public Size GetFormattedSize()
  175. {
  176. return Format().RequiredSize;
  177. }
  178. private void InvalidateCache()
  179. {
  180. _cachedResult = null;
  181. }
  182. private int GetSettingsHash()
  183. {
  184. var hash = new HashCode();
  185. hash.Add(_text);
  186. hash.Add(_constrainToSize);
  187. hash.Add(_alignment);
  188. hash.Add(_verticalAlignment);
  189. hash.Add(_direction);
  190. hash.Add(_wordWrap);
  191. hash.Add(_multiLine);
  192. hash.Add(_hotKeySpecifier.Value);
  193. hash.Add(_tabWidth);
  194. hash.Add(_preserveTrailingSpaces);
  195. return hash.ToHashCode();
  196. }
  197. private FormattedText DoFormat()
  198. {
  199. if (string.IsNullOrEmpty(_text))
  200. {
  201. return new FormattedText(Array.Empty<FormattedLine>(), Size.Empty);
  202. }
  203. // Process HotKey
  204. var processedText = _text;
  205. var hotKey = Key.Empty;
  206. var hotKeyPos = -1;
  207. if (_hotKeySpecifier.Value != 0xFFFF && TextFormatter.FindHotKey(_text, _hotKeySpecifier, out hotKeyPos, out hotKey))
  208. {
  209. processedText = TextFormatter.RemoveHotKeySpecifier(_text, hotKeyPos, _hotKeySpecifier);
  210. }
  211. // Get constraints
  212. int width = _constrainToSize?.Width ?? int.MaxValue;
  213. int height = _constrainToSize?.Height ?? int.MaxValue;
  214. // Handle zero constraints
  215. if (width == 0 || height == 0)
  216. {
  217. return new FormattedText(Array.Empty<FormattedLine>(), Size.Empty, hotKey, hotKeyPos);
  218. }
  219. // Format the text using existing TextFormatter static methods
  220. List<string> lines;
  221. if (TextFormatter.IsVerticalDirection(_direction))
  222. {
  223. int colsWidth = TextFormatter.GetSumMaxCharWidth(processedText, 0, 1, _tabWidth);
  224. lines = TextFormatter.Format(
  225. processedText,
  226. height,
  227. _verticalAlignment == Alignment.Fill,
  228. width > colsWidth && _wordWrap,
  229. _preserveTrailingSpaces,
  230. _tabWidth,
  231. _direction,
  232. _multiLine
  233. );
  234. colsWidth = TextFormatter.GetMaxColsForWidth(lines, width, _tabWidth);
  235. if (lines.Count > colsWidth)
  236. {
  237. lines.RemoveRange(colsWidth, lines.Count - colsWidth);
  238. }
  239. }
  240. else
  241. {
  242. lines = TextFormatter.Format(
  243. processedText,
  244. width,
  245. _alignment == Alignment.Fill,
  246. height > 1 && _wordWrap,
  247. _preserveTrailingSpaces,
  248. _tabWidth,
  249. _direction,
  250. _multiLine
  251. );
  252. if (lines.Count > height)
  253. {
  254. lines.RemoveRange(height, lines.Count - height);
  255. }
  256. }
  257. // Convert to FormattedText structure
  258. var formattedLines = new List<FormattedLine>();
  259. foreach (string line in lines)
  260. {
  261. var runs = new List<FormattedRun>();
  262. // For now, create simple runs - we can enhance this later for HotKey highlighting
  263. if (!string.IsNullOrEmpty(line))
  264. {
  265. // Check if this line contains the HotKey
  266. if (hotKeyPos >= 0 && hotKey != Key.Empty)
  267. {
  268. // Simple implementation - just mark the whole line for now
  269. // TODO: Implement proper HotKey run detection
  270. runs.Add(new FormattedRun(line, false));
  271. }
  272. else
  273. {
  274. runs.Add(new FormattedRun(line, false));
  275. }
  276. }
  277. int lineWidth = TextFormatter.IsVerticalDirection(_direction)
  278. ? TextFormatter.GetColumnsRequiredForVerticalText(new List<string> { line }, 0, 1, _tabWidth)
  279. : line.GetColumns();
  280. formattedLines.Add(new FormattedLine(runs, lineWidth));
  281. }
  282. // Calculate required size
  283. Size requiredSize;
  284. if (TextFormatter.IsVerticalDirection(_direction))
  285. {
  286. requiredSize = new Size(
  287. TextFormatter.GetColumnsRequiredForVerticalText(lines, 0, lines.Count, _tabWidth),
  288. lines.Max(line => line.Length)
  289. );
  290. }
  291. else
  292. {
  293. requiredSize = new Size(
  294. lines.Max(line => line.GetColumns()),
  295. lines.Count
  296. );
  297. }
  298. return new FormattedText(formattedLines, requiredSize, hotKey, hotKeyPos);
  299. }
  300. }