TextFormatter.cs 95 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584
  1. #nullable enable
  2. using System.Buffers;
  3. using System.Diagnostics;
  4. namespace Terminal.Gui.Text;
  5. /// <summary>
  6. /// Provides text formatting. Supports <see cref="View.HotKey"/>s, horizontal and vertical alignment, text direction,
  7. /// multiple lines, and word-based line wrap.
  8. /// </summary>
  9. public class TextFormatter
  10. {
  11. // Utilized in CRLF related helper methods for faster newline char index search.
  12. private static readonly SearchValues<char> NewlineSearchValues = SearchValues.Create(['\r', '\n']);
  13. // New architecture components
  14. private readonly ITextFormatter _formatter;
  15. private readonly ITextRenderer _renderer;
  16. private Key _hotKey = new ();
  17. private int _hotKeyPos = -1;
  18. private List<string> _lines = new ();
  19. private bool _multiLine;
  20. private bool _preserveTrailingSpaces;
  21. private int _tabWidth = 4;
  22. private string? _text;
  23. private Alignment _textAlignment = Alignment.Start;
  24. private TextDirection _textDirection;
  25. private Alignment _textVerticalAlignment = Alignment.Start;
  26. private bool _wordWrap = true;
  27. /// <summary>
  28. /// Initializes a new instance of the <see cref="TextFormatter"/> class.
  29. /// </summary>
  30. public TextFormatter()
  31. {
  32. _formatter = new StandardTextFormatter();
  33. _renderer = new StandardTextRenderer();
  34. }
  35. /// <summary>Get or sets the horizontal text alignment.</summary>
  36. /// <value>The text alignment.</value>
  37. public Alignment Alignment
  38. {
  39. get => _textAlignment;
  40. set
  41. {
  42. _textAlignment = EnableNeedsFormat(value);
  43. _formatter.Alignment = value;
  44. }
  45. }
  46. /// <summary>
  47. /// Gets the cursor position of the <see cref="HotKey"/>. If the <see cref="HotKey"/> is defined, the cursor will
  48. /// be positioned over it.
  49. /// </summary>
  50. public int CursorPosition { get; internal set; }
  51. /// <summary>Gets or sets the text-direction.</summary>
  52. /// <value>The text direction.</value>
  53. public TextDirection Direction
  54. {
  55. get => _textDirection;
  56. set
  57. {
  58. _textDirection = EnableNeedsFormat(value);
  59. _formatter.Direction = value;
  60. }
  61. }
  62. /// <summary>Draws the text held by <see cref="TextFormatter"/> to <see cref="IConsoleDriver"/> using the colors specified.</summary>
  63. /// <remarks>
  64. /// Causes the text to be formatted (references <see cref="GetLines"/>). Sets <see cref="NeedsFormat"/> to
  65. /// <c>false</c>.
  66. /// </remarks>
  67. /// <param name="screen">Specifies the screen-relative location and maximum size for drawing the text.</param>
  68. /// <param name="normalColor">The color to use for all text except the hotkey</param>
  69. /// <param name="hotColor">The color to use to draw the hotkey</param>
  70. /// <param name="maximum">Specifies the screen-relative location and maximum container size.</param>
  71. /// <param name="driver">The console driver currently used by the application.</param>
  72. /// <exception cref="ArgumentOutOfRangeException"></exception>
  73. /// <summary>
  74. /// Draws the text using the new architecture (formatter + renderer separation).
  75. /// This method demonstrates the improved design with better performance and extensibility.
  76. /// </summary>
  77. /// <param name="screen">The screen bounds for drawing.</param>
  78. /// <param name="normalColor">The color for normal text.</param>
  79. /// <param name="hotColor">The color for HotKey text.</param>
  80. /// <param name="maximum">The maximum container bounds.</param>
  81. /// <param name="driver">The console driver to use for drawing.</param>
  82. public void DrawWithNewArchitecture(
  83. Rectangle screen,
  84. Attribute normalColor,
  85. Attribute hotColor,
  86. Rectangle maximum = default,
  87. IConsoleDriver? driver = null)
  88. {
  89. // Sync properties with the new formatter
  90. SyncFormatterProperties();
  91. // Format the text using the new architecture
  92. FormattedText formattedText = _formatter.Format();
  93. // Render using the new renderer
  94. _renderer.Draw(formattedText, screen, normalColor, hotColor, FillRemaining, maximum, driver);
  95. }
  96. /// <summary>
  97. /// Gets the draw region using the new architecture.
  98. /// This provides the same functionality as GetDrawRegion but with improved performance.
  99. /// </summary>
  100. /// <param name="screen">The screen bounds.</param>
  101. /// <param name="maximum">The maximum container bounds.</param>
  102. /// <returns>A region representing the areas that would be drawn.</returns>
  103. public Region GetDrawRegionWithNewArchitecture(Rectangle screen, Rectangle maximum = default)
  104. {
  105. SyncFormatterProperties();
  106. FormattedText formattedText = _formatter.Format();
  107. return _renderer.GetDrawRegion(formattedText, screen, maximum);
  108. }
  109. /// <summary>
  110. /// Gets the formatted size using the new architecture.
  111. /// This addresses the Format/Draw decoupling issues mentioned in the architectural problems.
  112. /// </summary>
  113. /// <returns>The size required for the formatted text.</returns>
  114. public Size GetFormattedSizeWithNewArchitecture()
  115. {
  116. SyncFormatterProperties();
  117. return _formatter.GetFormattedSize();
  118. }
  119. private void SyncFormatterProperties()
  120. {
  121. // Ensure the new formatter has all the current property values
  122. _formatter.Text = _text ?? string.Empty;
  123. _formatter.Alignment = _textAlignment;
  124. _formatter.VerticalAlignment = _textVerticalAlignment;
  125. _formatter.Direction = _textDirection;
  126. _formatter.WordWrap = _wordWrap;
  127. _formatter.MultiLine = _multiLine;
  128. _formatter.HotKeySpecifier = HotKeySpecifier;
  129. _formatter.TabWidth = _tabWidth;
  130. _formatter.PreserveTrailingSpaces = _preserveTrailingSpaces;
  131. _formatter.ConstrainToSize = ConstrainToSize;
  132. }
  133. public void Draw (
  134. Rectangle screen,
  135. Attribute normalColor,
  136. Attribute hotColor,
  137. Rectangle maximum = default,
  138. IConsoleDriver? driver = null
  139. )
  140. {
  141. // With this check, we protect against subclasses with overrides of Text (like Button)
  142. if (string.IsNullOrEmpty (Text))
  143. {
  144. return;
  145. }
  146. if (driver is null)
  147. {
  148. driver = Application.Driver;
  149. }
  150. driver?.SetAttribute (normalColor);
  151. List<string> linesFormatted = GetLines ();
  152. bool isVertical = IsVerticalDirection (Direction);
  153. Rectangle maxScreen = screen;
  154. if (driver is { })
  155. {
  156. // INTENT: What, exactly, is the intent of this?
  157. maxScreen = maximum == default (Rectangle)
  158. ? screen
  159. : new (
  160. Math.Max (maximum.X, screen.X),
  161. Math.Max (maximum.Y, screen.Y),
  162. Math.Max (
  163. Math.Min (maximum.Width, maximum.Right - screen.Left),
  164. 0
  165. ),
  166. Math.Max (
  167. Math.Min (
  168. maximum.Height,
  169. maximum.Bottom - screen.Top
  170. ),
  171. 0
  172. )
  173. );
  174. }
  175. if (maxScreen.Width == 0 || maxScreen.Height == 0)
  176. {
  177. return;
  178. }
  179. int lineOffset = !isVertical && screen.Y < 0 ? Math.Abs (screen.Y) : 0;
  180. for (int line = lineOffset; line < linesFormatted.Count; line++)
  181. {
  182. if ((isVertical && line > screen.Width) || (!isVertical && line > screen.Height))
  183. {
  184. continue;
  185. }
  186. if ((isVertical && line >= maxScreen.Left + maxScreen.Width)
  187. || (!isVertical && line >= maxScreen.Top + maxScreen.Height + lineOffset))
  188. {
  189. break;
  190. }
  191. Rune [] runes = linesFormatted [line].ToRunes ();
  192. // When text is justified, we lost left or right, so we use the direction to align.
  193. int x = 0, y = 0;
  194. // Horizontal Alignment
  195. if (Alignment is Alignment.End)
  196. {
  197. if (isVertical)
  198. {
  199. int runesWidth = GetColumnsRequiredForVerticalText (linesFormatted, line, linesFormatted.Count - line, TabWidth);
  200. x = screen.Right - runesWidth;
  201. CursorPosition = screen.Width - runesWidth + (_hotKeyPos > -1 ? _hotKeyPos : 0);
  202. }
  203. else
  204. {
  205. int runesWidth = StringExtensions.ToString (runes).GetColumns ();
  206. x = screen.Right - runesWidth;
  207. CursorPosition = screen.Width - runesWidth + (_hotKeyPos > -1 ? _hotKeyPos : 0);
  208. }
  209. }
  210. else if (Alignment is Alignment.Start)
  211. {
  212. if (isVertical)
  213. {
  214. int runesWidth = line > 0
  215. ? GetColumnsRequiredForVerticalText (linesFormatted, 0, line, TabWidth)
  216. : 0;
  217. x = screen.Left + runesWidth;
  218. }
  219. else
  220. {
  221. x = screen.Left;
  222. }
  223. CursorPosition = _hotKeyPos > -1 ? _hotKeyPos : 0;
  224. }
  225. else if (Alignment is Alignment.Fill)
  226. {
  227. if (isVertical)
  228. {
  229. int runesWidth = GetColumnsRequiredForVerticalText (linesFormatted, 0, linesFormatted.Count, TabWidth);
  230. int prevLineWidth = line > 0 ? GetColumnsRequiredForVerticalText (linesFormatted, line - 1, 1, TabWidth) : 0;
  231. int firstLineWidth = GetColumnsRequiredForVerticalText (linesFormatted, 0, 1, TabWidth);
  232. int lastLineWidth = GetColumnsRequiredForVerticalText (linesFormatted, linesFormatted.Count - 1, 1, TabWidth);
  233. var interval = (int)Math.Round ((double)(screen.Width + firstLineWidth + lastLineWidth) / linesFormatted.Count);
  234. x = line == 0
  235. ? screen.Left
  236. : line < linesFormatted.Count - 1
  237. ? screen.Width - runesWidth <= lastLineWidth ? screen.Left + prevLineWidth : screen.Left + line * interval
  238. : screen.Right - lastLineWidth;
  239. }
  240. else
  241. {
  242. x = screen.Left;
  243. }
  244. CursorPosition = _hotKeyPos > -1 ? _hotKeyPos : 0;
  245. }
  246. else if (Alignment is Alignment.Center)
  247. {
  248. if (isVertical)
  249. {
  250. int runesWidth = GetColumnsRequiredForVerticalText (linesFormatted, 0, linesFormatted.Count, TabWidth);
  251. int linesWidth = GetColumnsRequiredForVerticalText (linesFormatted, 0, line, TabWidth);
  252. x = screen.Left + linesWidth + (screen.Width - runesWidth) / 2;
  253. CursorPosition = (screen.Width - runesWidth) / 2 + (_hotKeyPos > -1 ? _hotKeyPos : 0);
  254. }
  255. else
  256. {
  257. int runesWidth = StringExtensions.ToString (runes).GetColumns ();
  258. x = screen.Left + (screen.Width - runesWidth) / 2;
  259. CursorPosition = (screen.Width - runesWidth) / 2 + (_hotKeyPos > -1 ? _hotKeyPos : 0);
  260. }
  261. }
  262. else
  263. {
  264. Debug.WriteLine ($"Unsupported Alignment: {nameof (VerticalAlignment)}");
  265. return;
  266. }
  267. // Vertical Alignment
  268. if (VerticalAlignment is Alignment.End)
  269. {
  270. if (isVertical)
  271. {
  272. y = screen.Bottom - runes.Length;
  273. }
  274. else
  275. {
  276. y = screen.Bottom - linesFormatted.Count + line;
  277. }
  278. }
  279. else if (VerticalAlignment is Alignment.Start)
  280. {
  281. if (isVertical)
  282. {
  283. y = screen.Top;
  284. }
  285. else
  286. {
  287. y = screen.Top + line;
  288. }
  289. }
  290. else if (VerticalAlignment is Alignment.Fill)
  291. {
  292. if (isVertical)
  293. {
  294. y = screen.Top;
  295. }
  296. else
  297. {
  298. var interval = (int)Math.Round ((double)(screen.Height + 2) / linesFormatted.Count);
  299. y = line == 0 ? screen.Top :
  300. line < linesFormatted.Count - 1 ? screen.Height - interval <= 1 ? screen.Top + 1 : screen.Top + line * interval : screen.Bottom - 1;
  301. }
  302. }
  303. else if (VerticalAlignment is Alignment.Center)
  304. {
  305. if (isVertical)
  306. {
  307. int s = (screen.Height - runes.Length) / 2;
  308. y = screen.Top + s;
  309. }
  310. else
  311. {
  312. int s = (screen.Height - linesFormatted.Count) / 2;
  313. y = screen.Top + line + s;
  314. }
  315. }
  316. else
  317. {
  318. Debug.WriteLine ($"Unsupported Alignment: {nameof (VerticalAlignment)}");
  319. return;
  320. }
  321. int colOffset = screen.X < 0 ? Math.Abs (screen.X) : 0;
  322. int start = isVertical ? screen.Top : screen.Left;
  323. int size = isVertical ? screen.Height : screen.Width;
  324. int current = start + colOffset;
  325. List<Point?> lastZeroWidthPos = null!;
  326. Rune rune = default;
  327. int zeroLengthCount = isVertical ? runes.Sum (r => r.GetColumns () == 0 ? 1 : 0) : 0;
  328. for (int idx = (isVertical ? start - y : start - x) + colOffset;
  329. current < start + size + zeroLengthCount;
  330. idx++)
  331. {
  332. Rune lastRuneUsed = rune;
  333. if (lastZeroWidthPos is null)
  334. {
  335. if (idx < 0
  336. || (isVertical
  337. ? VerticalAlignment != Alignment.End && current < 0
  338. : Alignment != Alignment.End && x + current + colOffset < 0))
  339. {
  340. current++;
  341. continue;
  342. }
  343. if (!FillRemaining && idx > runes.Length - 1)
  344. {
  345. break;
  346. }
  347. if ((!isVertical
  348. && (current - start > maxScreen.Left + maxScreen.Width - screen.X + colOffset
  349. || (idx < runes.Length && runes [idx].GetColumns () > screen.Width)))
  350. || (isVertical
  351. && ((current > start + size + zeroLengthCount && idx > maxScreen.Top + maxScreen.Height - screen.Y)
  352. || (idx < runes.Length && runes [idx].GetColumns () > screen.Width))))
  353. {
  354. break;
  355. }
  356. }
  357. //if ((!isVertical && idx > maxBounds.Left + maxBounds.Width - viewport.X + colOffset)
  358. // || (isVertical && idx > maxBounds.Top + maxBounds.Height - viewport.Y))
  359. // break;
  360. rune = (Rune)' ';
  361. if (isVertical)
  362. {
  363. if (idx >= 0 && idx < runes.Length)
  364. {
  365. rune = runes [idx];
  366. }
  367. if (lastZeroWidthPos is null)
  368. {
  369. driver?.Move (x, current);
  370. }
  371. else
  372. {
  373. int foundIdx = lastZeroWidthPos.IndexOf (
  374. p =>
  375. p is { } && p.Value.Y == current
  376. );
  377. if (foundIdx > -1)
  378. {
  379. if (rune.IsCombiningMark ())
  380. {
  381. lastZeroWidthPos [foundIdx] =
  382. new Point (
  383. lastZeroWidthPos [foundIdx]!.Value.X + 1,
  384. current
  385. );
  386. driver?.Move (
  387. lastZeroWidthPos [foundIdx]!.Value.X,
  388. current
  389. );
  390. }
  391. else if (!rune.IsCombiningMark () && lastRuneUsed.IsCombiningMark ())
  392. {
  393. current++;
  394. driver?.Move (x, current);
  395. }
  396. else
  397. {
  398. driver?.Move (x, current);
  399. }
  400. }
  401. else
  402. {
  403. driver?.Move (x, current);
  404. }
  405. }
  406. }
  407. else
  408. {
  409. driver?.Move (current, y);
  410. if (idx >= 0 && idx < runes.Length)
  411. {
  412. rune = runes [idx];
  413. }
  414. }
  415. int runeWidth = GetRuneWidth (rune, TabWidth);
  416. if (HotKeyPos > -1 && idx == HotKeyPos)
  417. {
  418. if ((isVertical && VerticalAlignment == Alignment.Fill) || (!isVertical && Alignment == Alignment.Fill))
  419. {
  420. CursorPosition = idx - start;
  421. }
  422. driver?.SetAttribute (hotColor);
  423. driver?.AddRune (rune);
  424. driver?.SetAttribute (normalColor);
  425. }
  426. else
  427. {
  428. if (isVertical)
  429. {
  430. if (runeWidth == 0)
  431. {
  432. if (lastZeroWidthPos is null)
  433. {
  434. lastZeroWidthPos = new ();
  435. }
  436. int foundIdx = lastZeroWidthPos.IndexOf (
  437. p =>
  438. p is { } && p.Value.Y == current
  439. );
  440. if (foundIdx == -1)
  441. {
  442. current--;
  443. lastZeroWidthPos.Add (new Point (x + 1, current));
  444. }
  445. driver?.Move (x + 1, current);
  446. }
  447. }
  448. driver?.AddRune (rune);
  449. }
  450. if (isVertical)
  451. {
  452. if (runeWidth > 0)
  453. {
  454. current++;
  455. }
  456. }
  457. else
  458. {
  459. current += runeWidth;
  460. }
  461. int nextRuneWidth = idx + 1 > -1 && idx + 1 < runes.Length
  462. ? runes [idx + 1].GetColumns ()
  463. : 0;
  464. if (!isVertical && idx + 1 < runes.Length && current + nextRuneWidth > start + size)
  465. {
  466. break;
  467. }
  468. }
  469. }
  470. }
  471. /// <summary>
  472. /// Determines if the viewport width will be used or only the text width will be used,
  473. /// If <see langword="true"/> all the viewport area will be filled with whitespaces and the same background color
  474. /// showing a perfect rectangle.
  475. /// </summary>
  476. public bool FillRemaining { get; set; }
  477. /// <summary>Returns the formatted text, constrained to <see cref="ConstrainToSize"/>.</summary>
  478. /// <remarks>
  479. /// If <see cref="NeedsFormat"/> is <see langword="true"/>, causes a format, resetting <see cref="NeedsFormat"/>
  480. /// to <see langword="false"/>.
  481. /// </remarks>
  482. /// <returns>The formatted text.</returns>
  483. public string Format ()
  484. {
  485. var sb = new StringBuilder ();
  486. // Lines_get causes a Format
  487. foreach (string line in GetLines ())
  488. {
  489. sb.AppendLine (line);
  490. }
  491. return sb.ToString ().TrimEnd (Environment.NewLine.ToCharArray ());
  492. }
  493. /// <summary>Gets the size required to hold the formatted text, given the constraints placed by <see cref="ConstrainToSize"/>.</summary>
  494. /// <remarks>Causes a format, resetting <see cref="NeedsFormat"/> to <see langword="false"/>.</remarks>
  495. /// <param name="constrainSize">
  496. /// If provided, will cause the text to be constrained to the provided size instead of <see cref="ConstrainToWidth"/> and
  497. /// <see cref="ConstrainToHeight"/>.
  498. /// </param>
  499. /// <returns>The size required to hold the formatted text.</returns>
  500. public Size FormatAndGetSize (Size? constrainSize = null)
  501. {
  502. if (string.IsNullOrEmpty (Text))
  503. {
  504. return System.Drawing.Size.Empty;
  505. }
  506. int? prevWidth = _constrainToWidth;
  507. int? prevHeight = _constrainToHeight;
  508. if (constrainSize is { })
  509. {
  510. _constrainToWidth = constrainSize?.Width;
  511. _constrainToHeight = constrainSize?.Height;
  512. }
  513. // HACK: Fill normally will fill the entire constraint size, but we need to know the actual size of the text.
  514. Alignment prevAlignment = Alignment;
  515. if (Alignment == Alignment.Fill)
  516. {
  517. Alignment = Alignment.Start;
  518. }
  519. Alignment prevVerticalAlignment = VerticalAlignment;
  520. if (VerticalAlignment == Alignment.Fill)
  521. {
  522. VerticalAlignment = Alignment.Start;
  523. }
  524. // This calls Format
  525. List<string> lines = GetLines ();
  526. // Undo hacks
  527. Alignment = prevAlignment;
  528. VerticalAlignment = prevVerticalAlignment;
  529. if (constrainSize is { })
  530. {
  531. _constrainToWidth = prevWidth ?? null;
  532. _constrainToHeight = prevHeight ?? null;
  533. }
  534. if (lines.Count == 0)
  535. {
  536. return System.Drawing.Size.Empty;
  537. }
  538. int width;
  539. int height;
  540. if (IsVerticalDirection (Direction))
  541. {
  542. width = GetColumnsRequiredForVerticalText (lines, 0, lines.Count, TabWidth);
  543. height = lines.Max (static line => line.Length);
  544. }
  545. else
  546. {
  547. width = lines.Max (static line => line.GetColumns ());
  548. height = lines.Count;
  549. }
  550. return new (width, height);
  551. }
  552. /// <summary>
  553. /// Gets the width or height of the <see cref="TextFormatter.HotKeySpecifier"/> characters
  554. /// in the <see cref="Text"/> property.
  555. /// </summary>
  556. /// <remarks>
  557. /// Only the first HotKey specifier found in <see cref="Text"/> is supported.
  558. /// </remarks>
  559. /// <param name="isWidth">
  560. /// If <see langword="true"/> (the default) the width required for the HotKey specifier is returned. Otherwise, the
  561. /// height is returned.
  562. /// </param>
  563. /// <returns>
  564. /// The number of characters required for the <see cref="TextFormatter.HotKeySpecifier"/>. If the text
  565. /// direction specified
  566. /// by <see cref="TextDirection"/> does not match the <paramref name="isWidth"/> parameter, <c>0</c> is returned.
  567. /// </returns>
  568. public int GetHotKeySpecifierLength (bool isWidth = true)
  569. {
  570. if (isWidth)
  571. {
  572. return IsHorizontalDirection (Direction) && Text?.Contains ((char)HotKeySpecifier.Value) == true
  573. ? Math.Max (HotKeySpecifier.GetColumns (), 0)
  574. : 0;
  575. }
  576. return IsVerticalDirection (Direction) && Text?.Contains ((char)HotKeySpecifier.Value) == true
  577. ? Math.Max (HotKeySpecifier.GetColumns (), 0)
  578. : 0;
  579. }
  580. /// <summary>Gets a list of formatted lines, constrained to <see cref="ConstrainToSize"/>.</summary>
  581. /// <remarks>
  582. /// <para>
  583. /// If the text needs to be formatted (if <see cref="NeedsFormat"/> is <see langword="true"/>)
  584. /// <see cref="Format()"/> will be called and upon return
  585. /// <see cref="NeedsFormat"/> will be <see langword="false"/>.
  586. /// </para>
  587. /// <para>
  588. /// If either of the dimensions of <see cref="ConstrainToSize"/> are zero, the text will not be formatted and no lines will
  589. /// be returned.
  590. /// </para>
  591. /// </remarks>
  592. public List<string> GetLines ()
  593. {
  594. string text = _text!.ReplaceLineEndings ();
  595. // With this check, we protect against subclasses with overrides of Text
  596. if (string.IsNullOrEmpty (Text) || ConstrainToWidth is 0 || ConstrainToHeight is 0)
  597. {
  598. _lines = [string.Empty];
  599. NeedsFormat = false;
  600. return _lines;
  601. }
  602. if (!NeedsFormat)
  603. {
  604. return _lines;
  605. }
  606. int width = ConstrainToWidth ?? int.MaxValue;
  607. int height = ConstrainToHeight ?? int.MaxValue;
  608. if (FindHotKey (_text!, HotKeySpecifier, out _hotKeyPos, out Key newHotKey))
  609. {
  610. HotKey = newHotKey;
  611. text = RemoveHotKeySpecifier (Text, _hotKeyPos, HotKeySpecifier);
  612. text = ReplaceHotKeyWithTag (text, _hotKeyPos);
  613. }
  614. if (IsVerticalDirection (Direction))
  615. {
  616. int colsWidth = GetSumMaxCharWidth (text, 0, 1, TabWidth);
  617. _lines = Format (
  618. text,
  619. height,
  620. VerticalAlignment == Alignment.Fill,
  621. width > colsWidth && WordWrap,
  622. PreserveTrailingSpaces,
  623. TabWidth,
  624. Direction,
  625. MultiLine,
  626. this
  627. );
  628. colsWidth = GetMaxColsForWidth (_lines, width, TabWidth);
  629. if (_lines.Count > colsWidth)
  630. {
  631. _lines.RemoveRange (colsWidth, _lines.Count - colsWidth);
  632. }
  633. }
  634. else
  635. {
  636. _lines = Format (
  637. text,
  638. width,
  639. Alignment == Alignment.Fill,
  640. height > 1 && WordWrap,
  641. PreserveTrailingSpaces,
  642. TabWidth,
  643. Direction,
  644. MultiLine,
  645. this
  646. );
  647. if (_lines.Count > height)
  648. {
  649. _lines.RemoveRange (height, _lines.Count - height);
  650. }
  651. }
  652. NeedsFormat = false;
  653. return _lines;
  654. }
  655. private int? _constrainToWidth;
  656. /// <summary>Gets or sets the width <see cref="Text"/> will be constrained to when formatted.</summary>
  657. /// <remarks>
  658. /// <para>
  659. /// Does not return the width of the formatted text but the width that will be used to constrain the text when
  660. /// formatted.
  661. /// </para>
  662. /// <para>
  663. /// If <see langword="null"/> the height will be unconstrained. if both <see cref="ConstrainToWidth"/> and <see cref="ConstrainToHeight"/> are <see langword="null"/> the text will be formatted to the size of the text.
  664. /// </para>
  665. /// <para>
  666. /// Use <see cref="FormatAndGetSize"/> to get the size of the formatted text.
  667. /// </para>
  668. /// <para>When set, <see cref="NeedsFormat"/> is set to <see langword="true"/>.</para>
  669. /// </remarks>
  670. public int? ConstrainToWidth
  671. {
  672. get => _constrainToWidth;
  673. set
  674. {
  675. if (_constrainToWidth == value)
  676. {
  677. return;
  678. }
  679. ArgumentOutOfRangeException.ThrowIfNegative (value.GetValueOrDefault (), nameof (ConstrainToWidth));
  680. _constrainToWidth = EnableNeedsFormat (value);
  681. }
  682. }
  683. private int? _constrainToHeight;
  684. /// <summary>Gets or sets the height <see cref="Text"/> will be constrained to when formatted.</summary>
  685. /// <remarks>
  686. /// <para>
  687. /// Does not return the height of the formatted text but the height that will be used to constrain the text when
  688. /// formatted.
  689. /// </para>
  690. /// <para>
  691. /// If <see langword="null"/> the height will be unconstrained. if both <see cref="ConstrainToWidth"/> and <see cref="ConstrainToHeight"/> are <see langword="null"/> the text will be formatted to the size of the text.
  692. /// </para>
  693. /// <para>
  694. /// Use <see cref="FormatAndGetSize"/> to get the size of the formatted text.
  695. /// </para>
  696. /// <para>When set, <see cref="NeedsFormat"/> is set to <see langword="true"/>.</para>
  697. /// </remarks>
  698. public int? ConstrainToHeight
  699. {
  700. get => _constrainToHeight;
  701. set
  702. {
  703. if (_constrainToHeight == value)
  704. {
  705. return;
  706. }
  707. ArgumentOutOfRangeException.ThrowIfNegative (value.GetValueOrDefault (), nameof (ConstrainToHeight));
  708. _constrainToHeight = EnableNeedsFormat (value);
  709. }
  710. }
  711. /// <summary>Gets or sets the width and height <see cref="Text"/> will be constrained to when formatted.</summary>
  712. /// <remarks>
  713. /// <para>
  714. /// Does not return the size of the formatted text but the size that will be used to constrain the text when
  715. /// formatted.
  716. /// </para>
  717. /// <para>
  718. /// If <see langword="null"/> both the width and height will be unconstrained and text will be formatted to the size of the text.
  719. /// </para>
  720. /// <para>
  721. /// Setting this property is the same as setting <see cref="ConstrainToWidth"/> and <see cref="ConstrainToHeight"/> separately.
  722. /// </para>
  723. /// <para>
  724. /// Use <see cref="FormatAndGetSize"/> to get the size of the formatted text.
  725. /// </para>
  726. /// <para>When set, <see cref="NeedsFormat"/> is set to <see langword="true"/>.</para>
  727. /// </remarks>
  728. public Size? ConstrainToSize
  729. {
  730. get
  731. {
  732. if (_constrainToWidth is null || _constrainToHeight is null)
  733. {
  734. return null;
  735. }
  736. return new Size (_constrainToWidth.Value, _constrainToHeight.Value);
  737. }
  738. set
  739. {
  740. if (value is null)
  741. {
  742. _constrainToWidth = null;
  743. _constrainToHeight = null;
  744. EnableNeedsFormat (true);
  745. }
  746. else
  747. {
  748. _constrainToWidth = EnableNeedsFormat (value.Value.Width);
  749. _constrainToHeight = EnableNeedsFormat (value.Value.Height);
  750. }
  751. }
  752. }
  753. /// <summary>Gets or sets the hot key. Fires the <see cref="HotKeyChanged"/> event.</summary>
  754. public Key HotKey
  755. {
  756. get => _hotKey;
  757. internal set
  758. {
  759. if (_hotKey != value)
  760. {
  761. Key oldKey = _hotKey;
  762. _hotKey = value;
  763. HotKeyChanged?.Invoke (this, new (oldKey, value));
  764. }
  765. }
  766. }
  767. /// <summary>Event invoked when the <see cref="HotKey"/> is changed.</summary>
  768. public event EventHandler<KeyChangedEventArgs>? HotKeyChanged;
  769. /// <summary>The position in the text of the hot key. The hot key will be rendered using the hot color.</summary>
  770. public int HotKeyPos
  771. {
  772. get => _hotKeyPos;
  773. internal set => _hotKeyPos = value;
  774. }
  775. /// <summary>
  776. /// The specifier character for the hot key (e.g. '_'). Set to '\xffff' to disable hot key support for this View
  777. /// instance. The default is '\xffff'.
  778. /// </summary>
  779. public Rune HotKeySpecifier { get; set; } = (Rune)0xFFFF;
  780. /// <summary>Gets or sets a value indicating whether multi line is allowed.</summary>
  781. /// <remarks>Multi line is ignored if <see cref="WordWrap"/> is <see langword="true"/>.</remarks>
  782. public bool MultiLine
  783. {
  784. get => _multiLine;
  785. set => _multiLine = EnableNeedsFormat (value);
  786. }
  787. /// <summary>Gets or sets whether the <see cref="TextFormatter"/> needs to format the text.</summary>
  788. /// <remarks>
  789. /// <para>If <see langword="false"/> when Draw is called, the Draw call will be faster.</para>
  790. /// <para>Used by <see cref="Draw"/></para>
  791. /// <para>Set to <see langword="true"/> when any of the properties of <see cref="TextFormatter"/> are set.</para>
  792. /// <para>Set to <see langword="false"/> when the text is formatted (if <see cref="GetLines"/> is accessed).</para>
  793. /// </remarks>
  794. public bool NeedsFormat { get; set; }
  795. /// <summary>
  796. /// Gets or sets whether trailing spaces at the end of word-wrapped lines are preserved or not when
  797. /// <see cref="TextFormatter.WordWrap"/> is enabled. If <see langword="true"/> trailing spaces at the end of wrapped
  798. /// lines will be removed when <see cref="Text"/> is formatted for display. The default is <see langword="false"/>.
  799. /// </summary>
  800. public bool PreserveTrailingSpaces
  801. {
  802. get => _preserveTrailingSpaces;
  803. set => _preserveTrailingSpaces = EnableNeedsFormat (value);
  804. }
  805. /// <summary>Gets or sets the number of columns used for a tab.</summary>
  806. public int TabWidth
  807. {
  808. get => _tabWidth;
  809. set => _tabWidth = EnableNeedsFormat (value);
  810. }
  811. /// <summary>The text to be formatted. This string is never modified.</summary>
  812. public string Text
  813. {
  814. get => _text!;
  815. set
  816. {
  817. _text = EnableNeedsFormat(value);
  818. _formatter.Text = value ?? string.Empty;
  819. }
  820. }
  821. /// <summary>Gets or sets the vertical text-alignment.</summary>
  822. /// <value>The text vertical alignment.</value>
  823. public Alignment VerticalAlignment
  824. {
  825. get => _textVerticalAlignment;
  826. set
  827. {
  828. _textVerticalAlignment = EnableNeedsFormat(value);
  829. _formatter.VerticalAlignment = value;
  830. }
  831. }
  832. /// <summary>Gets or sets whether word wrap will be used to fit <see cref="Text"/> to <see cref="ConstrainToSize"/>.</summary>
  833. public bool WordWrap
  834. {
  835. get => _wordWrap;
  836. set => _wordWrap = EnableNeedsFormat (value);
  837. }
  838. /// <summary>Sets <see cref="NeedsFormat"/> to <see langword="true"/> and returns the value.</summary>
  839. /// <typeparam name="T"></typeparam>
  840. /// <param name="value"></param>
  841. /// <returns></returns>
  842. private T EnableNeedsFormat<T> (T value)
  843. {
  844. NeedsFormat = true;
  845. return value;
  846. }
  847. /// <summary>
  848. /// Calculates and returns a <see cref="Region"/> describing the areas where text would be output, based on the
  849. /// formatting rules of <see cref="TextFormatter"/>.
  850. /// </summary>
  851. /// <remarks>
  852. /// Uses the same formatting logic as <see cref="Draw"/>, including alignment, direction, word wrap, and constraints,
  853. /// but does not perform actual drawing to <see cref="IConsoleDriver"/>.
  854. /// </remarks>
  855. /// <param name="screen">Specifies the screen-relative location and maximum size for drawing the text.</param>
  856. /// <param name="maximum">Specifies the screen-relative location and maximum container size.</param>
  857. /// <returns>A <see cref="Region"/> representing the areas where text would be drawn.</returns>
  858. public Region GetDrawRegion (Rectangle screen, Rectangle maximum = default)
  859. {
  860. Region drawnRegion = new Region ();
  861. // With this check, we protect against subclasses with overrides of Text (like Button)
  862. if (string.IsNullOrEmpty (Text))
  863. {
  864. return drawnRegion;
  865. }
  866. List<string> linesFormatted = GetLines ();
  867. bool isVertical = IsVerticalDirection (Direction);
  868. Rectangle maxScreen = screen;
  869. // INTENT: What, exactly, is the intent of this?
  870. maxScreen = maximum == default (Rectangle)
  871. ? screen
  872. : new (
  873. Math.Max (maximum.X, screen.X),
  874. Math.Max (maximum.Y, screen.Y),
  875. Math.Max (
  876. Math.Min (maximum.Width, maximum.Right - screen.Left),
  877. 0
  878. ),
  879. Math.Max (
  880. Math.Min (
  881. maximum.Height,
  882. maximum.Bottom - screen.Top
  883. ),
  884. 0
  885. )
  886. );
  887. if (maxScreen.Width == 0 || maxScreen.Height == 0)
  888. {
  889. return drawnRegion;
  890. }
  891. int lineOffset = !isVertical && screen.Y < 0 ? Math.Abs (screen.Y) : 0;
  892. for (int line = lineOffset; line < linesFormatted.Count; line++)
  893. {
  894. if ((isVertical && line > screen.Width) || (!isVertical && line > screen.Height))
  895. {
  896. continue;
  897. }
  898. if ((isVertical && line >= maxScreen.Left + maxScreen.Width)
  899. || (!isVertical && line >= maxScreen.Top + maxScreen.Height + lineOffset))
  900. {
  901. break;
  902. }
  903. Rune [] runes = linesFormatted [line].ToRunes ();
  904. // When text is justified, we lost left or right, so we use the direction to align.
  905. int x = 0, y = 0;
  906. switch (Alignment)
  907. {
  908. // Horizontal Alignment
  909. case Alignment.End when isVertical:
  910. {
  911. int runesWidth = GetColumnsRequiredForVerticalText (linesFormatted, line, linesFormatted.Count - line, TabWidth);
  912. x = screen.Right - runesWidth;
  913. break;
  914. }
  915. case Alignment.End:
  916. {
  917. int runesWidth = StringExtensions.ToString (runes).GetColumns ();
  918. x = screen.Right - runesWidth;
  919. break;
  920. }
  921. case Alignment.Start when isVertical:
  922. {
  923. int runesWidth = line > 0
  924. ? GetColumnsRequiredForVerticalText (linesFormatted, 0, line, TabWidth)
  925. : 0;
  926. x = screen.Left + runesWidth;
  927. break;
  928. }
  929. case Alignment.Start:
  930. x = screen.Left;
  931. break;
  932. case Alignment.Fill when isVertical:
  933. {
  934. int runesWidth = GetColumnsRequiredForVerticalText (linesFormatted, 0, linesFormatted.Count, TabWidth);
  935. int prevLineWidth = line > 0 ? GetColumnsRequiredForVerticalText (linesFormatted, line - 1, 1, TabWidth) : 0;
  936. int firstLineWidth = GetColumnsRequiredForVerticalText (linesFormatted, 0, 1, TabWidth);
  937. int lastLineWidth = GetColumnsRequiredForVerticalText (linesFormatted, linesFormatted.Count - 1, 1, TabWidth);
  938. var interval = (int)Math.Round ((double)(screen.Width + firstLineWidth + lastLineWidth) / linesFormatted.Count);
  939. x = line == 0
  940. ? screen.Left
  941. : line < linesFormatted.Count - 1
  942. ? screen.Width - runesWidth <= lastLineWidth ? screen.Left + prevLineWidth : screen.Left + line * interval
  943. : screen.Right - lastLineWidth;
  944. break;
  945. }
  946. case Alignment.Fill:
  947. x = screen.Left;
  948. break;
  949. case Alignment.Center when isVertical:
  950. {
  951. int runesWidth = GetColumnsRequiredForVerticalText (linesFormatted, 0, linesFormatted.Count, TabWidth);
  952. int linesWidth = GetColumnsRequiredForVerticalText (linesFormatted, 0, line, TabWidth);
  953. x = screen.Left + linesWidth + (screen.Width - runesWidth) / 2;
  954. break;
  955. }
  956. case Alignment.Center:
  957. {
  958. int runesWidth = StringExtensions.ToString (runes).GetColumns ();
  959. x = screen.Left + (screen.Width - runesWidth) / 2;
  960. break;
  961. }
  962. default:
  963. Debug.WriteLine ($"Unsupported Alignment: {nameof (VerticalAlignment)}");
  964. return drawnRegion;
  965. }
  966. switch (VerticalAlignment)
  967. {
  968. // Vertical Alignment
  969. case Alignment.End when isVertical:
  970. y = screen.Bottom - runes.Length;
  971. break;
  972. case Alignment.End:
  973. y = screen.Bottom - linesFormatted.Count + line;
  974. break;
  975. case Alignment.Start when isVertical:
  976. y = screen.Top;
  977. break;
  978. case Alignment.Start:
  979. y = screen.Top + line;
  980. break;
  981. case Alignment.Fill when isVertical:
  982. y = screen.Top;
  983. break;
  984. case Alignment.Fill:
  985. {
  986. var interval = (int)Math.Round ((double)(screen.Height + 2) / linesFormatted.Count);
  987. y = line == 0 ? screen.Top :
  988. line < linesFormatted.Count - 1 ? screen.Height - interval <= 1 ? screen.Top + 1 : screen.Top + line * interval : screen.Bottom - 1;
  989. break;
  990. }
  991. case Alignment.Center when isVertical:
  992. {
  993. int s = (screen.Height - runes.Length) / 2;
  994. y = screen.Top + s;
  995. break;
  996. }
  997. case Alignment.Center:
  998. {
  999. int s = (screen.Height - linesFormatted.Count) / 2;
  1000. y = screen.Top + line + s;
  1001. break;
  1002. }
  1003. default:
  1004. Debug.WriteLine ($"Unsupported Alignment: {nameof (VerticalAlignment)}");
  1005. return drawnRegion;
  1006. }
  1007. int colOffset = screen.X < 0 ? Math.Abs (screen.X) : 0;
  1008. int start = isVertical ? screen.Top : screen.Left;
  1009. int size = isVertical ? screen.Height : screen.Width;
  1010. int current = start + colOffset;
  1011. int zeroLengthCount = isVertical ? runes.Sum (r => r.GetColumns () == 0 ? 1 : 0) : 0;
  1012. int lineX = x, lineY = y, lineWidth = 0, lineHeight = 1;
  1013. for (int idx = (isVertical ? start - y : start - x) + colOffset;
  1014. current < start + size + zeroLengthCount;
  1015. idx++)
  1016. {
  1017. if (idx < 0
  1018. || (isVertical
  1019. ? VerticalAlignment != Alignment.End && current < 0
  1020. : Alignment != Alignment.End && x + current + colOffset < 0))
  1021. {
  1022. current++;
  1023. continue;
  1024. }
  1025. if (!FillRemaining && idx > runes.Length - 1)
  1026. {
  1027. break;
  1028. }
  1029. if ((!isVertical
  1030. && (current - start > maxScreen.Left + maxScreen.Width - screen.X + colOffset
  1031. || (idx < runes.Length && runes [idx].GetColumns () > screen.Width)))
  1032. || (isVertical
  1033. && ((current > start + size + zeroLengthCount && idx > maxScreen.Top + maxScreen.Height - screen.Y)
  1034. || (idx < runes.Length && runes [idx].GetColumns () > screen.Width))))
  1035. {
  1036. break;
  1037. }
  1038. Rune rune = idx >= 0 && idx < runes.Length ? runes [idx] : (Rune)' ';
  1039. int runeWidth = GetRuneWidth (rune, TabWidth);
  1040. if (isVertical)
  1041. {
  1042. if (runeWidth > 0)
  1043. {
  1044. // Update line height for vertical text (each rune is a column)
  1045. lineHeight = Math.Max (lineHeight, current - y + 1);
  1046. lineWidth = Math.Max (lineWidth, 1); // Width is 1 per rune in vertical
  1047. }
  1048. }
  1049. else
  1050. {
  1051. // Update line width and position for horizontal text
  1052. lineWidth += runeWidth;
  1053. }
  1054. current += isVertical && runeWidth > 0 ? 1 : runeWidth;
  1055. int nextRuneWidth = idx + 1 > -1 && idx + 1 < runes.Length
  1056. ? runes [idx + 1].GetColumns ()
  1057. : 0;
  1058. if (!isVertical && idx + 1 < runes.Length && current + nextRuneWidth > start + size)
  1059. {
  1060. break;
  1061. }
  1062. }
  1063. // Add the line's drawn region to the overall region
  1064. if (lineWidth > 0 && lineHeight > 0)
  1065. {
  1066. drawnRegion.Union (new Rectangle (lineX, lineY, lineWidth, lineHeight));
  1067. }
  1068. }
  1069. return drawnRegion;
  1070. }
  1071. #region Static Members
  1072. /// <summary>Check if it is a horizontal direction</summary>
  1073. public static bool IsHorizontalDirection (TextDirection textDirection)
  1074. {
  1075. return textDirection switch
  1076. {
  1077. TextDirection.LeftRight_TopBottom => true,
  1078. TextDirection.LeftRight_BottomTop => true,
  1079. TextDirection.RightLeft_TopBottom => true,
  1080. TextDirection.RightLeft_BottomTop => true,
  1081. _ => false
  1082. };
  1083. }
  1084. /// <summary>Check if it is a vertical direction</summary>
  1085. public static bool IsVerticalDirection (TextDirection textDirection)
  1086. {
  1087. return textDirection switch
  1088. {
  1089. TextDirection.TopBottom_LeftRight => true,
  1090. TextDirection.TopBottom_RightLeft => true,
  1091. TextDirection.BottomTop_LeftRight => true,
  1092. TextDirection.BottomTop_RightLeft => true,
  1093. _ => false
  1094. };
  1095. }
  1096. /// <summary>Check if it is Left to Right direction</summary>
  1097. public static bool IsLeftToRight (TextDirection textDirection)
  1098. {
  1099. return textDirection switch
  1100. {
  1101. TextDirection.LeftRight_TopBottom => true,
  1102. TextDirection.LeftRight_BottomTop => true,
  1103. _ => false
  1104. };
  1105. }
  1106. /// <summary>Check if it is Top to Bottom direction</summary>
  1107. public static bool IsTopToBottom (TextDirection textDirection)
  1108. {
  1109. return textDirection switch
  1110. {
  1111. TextDirection.TopBottom_LeftRight => true,
  1112. TextDirection.TopBottom_RightLeft => true,
  1113. _ => false
  1114. };
  1115. }
  1116. // TODO: Move to StringExtensions?
  1117. internal static string StripCRLF (string str, bool keepNewLine = false)
  1118. {
  1119. ReadOnlySpan<char> remaining = str.AsSpan ();
  1120. int firstNewlineCharIndex = remaining.IndexOfAny (NewlineSearchValues);
  1121. // Early exit to avoid StringBuilder allocation if there are no newline characters.
  1122. if (firstNewlineCharIndex < 0)
  1123. {
  1124. return str;
  1125. }
  1126. StringBuilder stringBuilder = new();
  1127. ReadOnlySpan<char> firstSegment = remaining[..firstNewlineCharIndex];
  1128. stringBuilder.Append (firstSegment);
  1129. // The first newline is not yet skipped because the "keepNewLine" condition has not been evaluated.
  1130. // This means there will be 1 extra iteration because the same newline index is checked again in the loop.
  1131. remaining = remaining [firstNewlineCharIndex..];
  1132. while (remaining.Length > 0)
  1133. {
  1134. int newlineCharIndex = remaining.IndexOfAny (NewlineSearchValues);
  1135. if (newlineCharIndex == -1)
  1136. {
  1137. break;
  1138. }
  1139. ReadOnlySpan<char> segment = remaining[..newlineCharIndex];
  1140. stringBuilder.Append (segment);
  1141. int stride = segment.Length;
  1142. // Evaluate how many line break characters to preserve.
  1143. char newlineChar = remaining [newlineCharIndex];
  1144. if (newlineChar == '\n')
  1145. {
  1146. stride++;
  1147. if (keepNewLine)
  1148. {
  1149. stringBuilder.Append ('\n');
  1150. }
  1151. }
  1152. else // '\r'
  1153. {
  1154. int nextCharIndex = newlineCharIndex + 1;
  1155. bool crlf = nextCharIndex < remaining.Length && remaining [nextCharIndex] == '\n';
  1156. if (crlf)
  1157. {
  1158. stride += 2;
  1159. if (keepNewLine)
  1160. {
  1161. stringBuilder.Append ('\n');
  1162. }
  1163. }
  1164. else
  1165. {
  1166. stride++;
  1167. if (keepNewLine)
  1168. {
  1169. stringBuilder.Append ('\r');
  1170. }
  1171. }
  1172. }
  1173. remaining = remaining [stride..];
  1174. }
  1175. stringBuilder.Append (remaining);
  1176. return stringBuilder.ToString ();
  1177. }
  1178. // TODO: Move to StringExtensions?
  1179. internal static string ReplaceCRLFWithSpace (string str)
  1180. {
  1181. ReadOnlySpan<char> remaining = str.AsSpan ();
  1182. int firstNewlineCharIndex = remaining.IndexOfAny (NewlineSearchValues);
  1183. // Early exit to avoid StringBuilder allocation if there are no newline characters.
  1184. if (firstNewlineCharIndex < 0)
  1185. {
  1186. return str;
  1187. }
  1188. StringBuilder stringBuilder = new();
  1189. ReadOnlySpan<char> firstSegment = remaining[..firstNewlineCharIndex];
  1190. stringBuilder.Append (firstSegment);
  1191. // The first newline is not yet skipped because the newline type has not been evaluated.
  1192. // This means there will be 1 extra iteration because the same newline index is checked again in the loop.
  1193. remaining = remaining [firstNewlineCharIndex..];
  1194. while (remaining.Length > 0)
  1195. {
  1196. int newlineCharIndex = remaining.IndexOfAny (NewlineSearchValues);
  1197. if (newlineCharIndex == -1)
  1198. {
  1199. break;
  1200. }
  1201. ReadOnlySpan<char> segment = remaining[..newlineCharIndex];
  1202. stringBuilder.Append (segment);
  1203. int stride = segment.Length;
  1204. // Replace newlines
  1205. char newlineChar = remaining [newlineCharIndex];
  1206. if (newlineChar == '\n')
  1207. {
  1208. stride++;
  1209. stringBuilder.Append (' ');
  1210. }
  1211. else // '\r'
  1212. {
  1213. int nextCharIndex = newlineCharIndex + 1;
  1214. bool crlf = nextCharIndex < remaining.Length && remaining [nextCharIndex] == '\n';
  1215. if (crlf)
  1216. {
  1217. stride += 2;
  1218. stringBuilder.Append (' ');
  1219. }
  1220. else
  1221. {
  1222. stride++;
  1223. stringBuilder.Append (' ');
  1224. }
  1225. }
  1226. remaining = remaining [stride..];
  1227. }
  1228. stringBuilder.Append (remaining);
  1229. return stringBuilder.ToString ();
  1230. }
  1231. // TODO: Move to StringExtensions?
  1232. private static string ReplaceTABWithSpaces (string str, int tabWidth)
  1233. {
  1234. if (tabWidth == 0)
  1235. {
  1236. return str.Replace ("\t", "");
  1237. }
  1238. return str.Replace ("\t", new (' ', tabWidth));
  1239. }
  1240. // TODO: Move to StringExtensions?
  1241. /// <summary>
  1242. /// Splits all newlines in the <paramref name="text"/> into a list and supports both CRLF and LF, preserving the
  1243. /// ending newline.
  1244. /// </summary>
  1245. /// <param name="text">The text.</param>
  1246. /// <returns>A list of text without the newline characters.</returns>
  1247. public static List<string> SplitNewLine (string text)
  1248. {
  1249. List<Rune> runes = text.ToRuneList ();
  1250. List<string> lines = new ();
  1251. var start = 0;
  1252. for (var i = 0; i < runes.Count; i++)
  1253. {
  1254. int end = i;
  1255. switch (runes [i].Value)
  1256. {
  1257. case '\n':
  1258. lines.Add (StringExtensions.ToString (runes.GetRange (start, end - start)));
  1259. i++;
  1260. start = i;
  1261. break;
  1262. case '\r':
  1263. if (i + 1 < runes.Count && runes [i + 1].Value == '\n')
  1264. {
  1265. lines.Add (StringExtensions.ToString (runes.GetRange (start, end - start)));
  1266. i += 2;
  1267. start = i;
  1268. }
  1269. else
  1270. {
  1271. lines.Add (StringExtensions.ToString (runes.GetRange (start, end - start)));
  1272. i++;
  1273. start = i;
  1274. }
  1275. break;
  1276. }
  1277. }
  1278. switch (runes.Count)
  1279. {
  1280. case > 0 when lines.Count == 0:
  1281. lines.Add (StringExtensions.ToString (runes));
  1282. break;
  1283. case > 0 when start < runes.Count:
  1284. lines.Add (StringExtensions.ToString (runes.GetRange (start, runes.Count - start)));
  1285. break;
  1286. default:
  1287. lines.Add ("");
  1288. break;
  1289. }
  1290. return lines;
  1291. }
  1292. // TODO: Move to StringExtensions?
  1293. /// <summary>
  1294. /// Adds trailing whitespace or truncates <paramref name="text"/> so that it fits exactly <paramref name="width"/>
  1295. /// columns. Note that some unicode characters take 2+ columns
  1296. /// </summary>
  1297. /// <param name="text"></param>
  1298. /// <param name="width"></param>
  1299. /// <returns></returns>
  1300. public static string ClipOrPad (string text, int width)
  1301. {
  1302. if (string.IsNullOrEmpty (text))
  1303. {
  1304. return text;
  1305. }
  1306. // if value is not wide enough
  1307. if (text.EnumerateRunes ().Sum (c => c.GetColumns ()) < width)
  1308. {
  1309. // pad it out with spaces to the given Alignment
  1310. int toPad = width - text.EnumerateRunes ().Sum (c => c.GetColumns ());
  1311. return text + new string (' ', toPad);
  1312. }
  1313. // value is too wide
  1314. return new (text.TakeWhile (c => (width -= ((Rune)c).GetColumns ()) >= 0).ToArray ());
  1315. }
  1316. /// <summary>Formats the provided text to fit within the width provided using word wrapping.</summary>
  1317. /// <param name="text">The text to word wrap</param>
  1318. /// <param name="width">The number of columns to constrain the text to</param>
  1319. /// <param name="preserveTrailingSpaces">
  1320. /// If <see langword="true"/> trailing spaces at the end of wrapped lines will be
  1321. /// preserved. If <see langword="false"/> , trailing spaces at the end of wrapped lines will be trimmed.
  1322. /// </param>
  1323. /// <param name="tabWidth">The number of columns used for a tab.</param>
  1324. /// <param name="textDirection">The text direction.</param>
  1325. /// <param name="textFormatter"><see cref="TextFormatter"/> instance to access any of his objects.</param>
  1326. /// <returns>A list of word wrapped lines.</returns>
  1327. /// <remarks>
  1328. /// <para>This method does not do any alignment.</para>
  1329. /// <para>This method strips Newline ('\n' and '\r\n') sequences before processing.</para>
  1330. /// <para>
  1331. /// If <paramref name="preserveTrailingSpaces"/> is <see langword="false"/> at most one space will be preserved
  1332. /// at the end of the last line.
  1333. /// </para>
  1334. /// </remarks>
  1335. /// <returns>A list of lines.</returns>
  1336. public static List<string> WordWrapText (
  1337. string text,
  1338. int width,
  1339. bool preserveTrailingSpaces = false,
  1340. int tabWidth = 0,
  1341. TextDirection textDirection = TextDirection.LeftRight_TopBottom,
  1342. TextFormatter? textFormatter = null
  1343. )
  1344. {
  1345. ArgumentOutOfRangeException.ThrowIfNegative (width, nameof (width));
  1346. List<string> lines = new ();
  1347. if (string.IsNullOrEmpty (text))
  1348. {
  1349. return lines;
  1350. }
  1351. List<Rune> runes = StripCRLF (text).ToRuneList ();
  1352. int start = Math.Max (
  1353. !runes.Contains ((Rune)' ') && textFormatter is { VerticalAlignment: Alignment.End } && IsVerticalDirection (textDirection)
  1354. ? runes.Count - width
  1355. : 0,
  1356. 0);
  1357. int end;
  1358. if (preserveTrailingSpaces)
  1359. {
  1360. while ((end = start) < runes.Count)
  1361. {
  1362. end = GetNextWhiteSpace (start, width, out bool incomplete);
  1363. if (end == 0 && incomplete)
  1364. {
  1365. start = text.GetRuneCount ();
  1366. break;
  1367. }
  1368. lines.Add (StringExtensions.ToString (runes.GetRange (start, end - start)));
  1369. start = end;
  1370. if (incomplete)
  1371. {
  1372. start = text.GetRuneCount ();
  1373. break;
  1374. }
  1375. }
  1376. }
  1377. else
  1378. {
  1379. if (IsHorizontalDirection (textDirection))
  1380. {
  1381. while ((end = start
  1382. + GetLengthThatFits (
  1383. runes.GetRange (start, runes.Count - start),
  1384. width,
  1385. tabWidth,
  1386. textDirection
  1387. ))
  1388. < runes.Count)
  1389. {
  1390. while (runes [end].Value != ' ' && end > start)
  1391. {
  1392. end--;
  1393. }
  1394. if (end == start)
  1395. {
  1396. end = start
  1397. + GetLengthThatFits (
  1398. runes.GetRange (end, runes.Count - end),
  1399. width,
  1400. tabWidth,
  1401. textDirection
  1402. );
  1403. }
  1404. var str = StringExtensions.ToString (runes.GetRange (start, end - start));
  1405. int zeroLength = text.EnumerateRunes ().Sum (r => r.GetColumns () == 0 ? 1 : 0);
  1406. if (end > start && GetRuneWidth (str, tabWidth, textDirection) <= width + zeroLength)
  1407. {
  1408. lines.Add (str);
  1409. start = end;
  1410. if (runes [end].Value == ' ')
  1411. {
  1412. start++;
  1413. }
  1414. }
  1415. else
  1416. {
  1417. end++;
  1418. start = end;
  1419. }
  1420. }
  1421. }
  1422. else
  1423. {
  1424. while ((end = start + width) < runes.Count)
  1425. {
  1426. while (runes [end].Value != ' ' && end > start)
  1427. {
  1428. end--;
  1429. }
  1430. if (end == start)
  1431. {
  1432. end = start + width;
  1433. }
  1434. var zeroLength = 0;
  1435. for (int i = end; i < runes.Count - start; i++)
  1436. {
  1437. Rune r = runes [i];
  1438. if (r.GetColumns () == 0)
  1439. {
  1440. zeroLength++;
  1441. }
  1442. else
  1443. {
  1444. break;
  1445. }
  1446. }
  1447. lines.Add (
  1448. StringExtensions.ToString (
  1449. runes.GetRange (
  1450. start,
  1451. end - start + zeroLength
  1452. )
  1453. )
  1454. );
  1455. end += zeroLength;
  1456. start = end;
  1457. if (runes [end].Value == ' ')
  1458. {
  1459. start++;
  1460. }
  1461. }
  1462. }
  1463. }
  1464. int GetNextWhiteSpace (int from, int cWidth, out bool incomplete, int cLength = 0)
  1465. {
  1466. int to = from;
  1467. int length = cLength;
  1468. incomplete = false;
  1469. while (length < cWidth && to < runes.Count)
  1470. {
  1471. Rune rune = runes [to];
  1472. if (IsHorizontalDirection (textDirection))
  1473. {
  1474. length += rune.GetColumns ();
  1475. }
  1476. else
  1477. {
  1478. length++;
  1479. }
  1480. if (length > cWidth)
  1481. {
  1482. if (to >= runes.Count || (length > 1 && cWidth <= 1))
  1483. {
  1484. incomplete = true;
  1485. }
  1486. return to;
  1487. }
  1488. switch (rune.Value)
  1489. {
  1490. case ' ' when length == cWidth:
  1491. return to + 1;
  1492. case ' ' when length > cWidth:
  1493. return to;
  1494. case ' ':
  1495. return GetNextWhiteSpace (to + 1, cWidth, out incomplete, length);
  1496. case '\t':
  1497. {
  1498. length += tabWidth + 1;
  1499. if (length == tabWidth && tabWidth > cWidth)
  1500. {
  1501. return to + 1;
  1502. }
  1503. if (length > cWidth && tabWidth > cWidth)
  1504. {
  1505. return to;
  1506. }
  1507. return GetNextWhiteSpace (to + 1, cWidth, out incomplete, length);
  1508. }
  1509. default:
  1510. to++;
  1511. break;
  1512. }
  1513. }
  1514. return cLength switch
  1515. {
  1516. > 0 when to < runes.Count && runes [to].Value != ' ' && runes [to].Value != '\t' => from,
  1517. > 0 when to < runes.Count && (runes [to].Value == ' ' || runes [to].Value == '\t') => from,
  1518. _ => to
  1519. };
  1520. }
  1521. if (start < text.GetRuneCount ())
  1522. {
  1523. string str = ReplaceTABWithSpaces (
  1524. StringExtensions.ToString (runes.GetRange (start, runes.Count - start)),
  1525. tabWidth
  1526. );
  1527. if (IsVerticalDirection (textDirection) || preserveTrailingSpaces || str.GetColumns () <= width)
  1528. {
  1529. lines.Add (str);
  1530. }
  1531. }
  1532. return lines;
  1533. }
  1534. /// <summary>Justifies text within a specified width.</summary>
  1535. /// <param name="text">The text to justify.</param>
  1536. /// <param name="width">
  1537. /// The number of columns to clip the text to. Text longer than <paramref name="width"/> will be
  1538. /// clipped.
  1539. /// </param>
  1540. /// <param name="textAlignment">Alignment.</param>
  1541. /// <param name="textDirection">The text direction.</param>
  1542. /// <param name="tabWidth">The number of columns used for a tab.</param>
  1543. /// <param name="textFormatter"><see cref="TextFormatter"/> instance to access any of his objects.</param>
  1544. /// <returns>Justified and clipped text.</returns>
  1545. public static string ClipAndJustify (
  1546. string text,
  1547. int width,
  1548. Alignment textAlignment,
  1549. TextDirection textDirection = TextDirection.LeftRight_TopBottom,
  1550. int tabWidth = 0,
  1551. TextFormatter? textFormatter = null
  1552. )
  1553. {
  1554. return ClipAndJustify (text, width, textAlignment == Alignment.Fill, textDirection, tabWidth, textFormatter);
  1555. }
  1556. /// <summary>Justifies text within a specified width.</summary>
  1557. /// <param name="text">The text to justify.</param>
  1558. /// <param name="width">
  1559. /// The number of columns to clip the text to. Text longer than <paramref name="width"/> will be
  1560. /// clipped.
  1561. /// </param>
  1562. /// <param name="justify">Justify.</param>
  1563. /// <param name="textDirection">The text direction.</param>
  1564. /// <param name="tabWidth">The number of columns used for a tab.</param>
  1565. /// <param name="textFormatter"><see cref="TextFormatter"/> instance to access any of his objects.</param>
  1566. /// <returns>Justified and clipped text.</returns>
  1567. public static string ClipAndJustify (
  1568. string text,
  1569. int width,
  1570. bool justify,
  1571. TextDirection textDirection = TextDirection.LeftRight_TopBottom,
  1572. int tabWidth = 0,
  1573. TextFormatter? textFormatter = null
  1574. )
  1575. {
  1576. ArgumentOutOfRangeException.ThrowIfNegative (width, nameof (width));
  1577. if (string.IsNullOrEmpty (text))
  1578. {
  1579. return text;
  1580. }
  1581. text = ReplaceTABWithSpaces (text, tabWidth);
  1582. List<Rune> runes = text.ToRuneList ();
  1583. int zeroLength = runes.Sum (r => r.GetColumns () == 0 ? 1 : 0);
  1584. if (runes.Count - zeroLength > width)
  1585. {
  1586. if (IsHorizontalDirection (textDirection))
  1587. {
  1588. if (textFormatter is { Alignment: Alignment.End })
  1589. {
  1590. return GetRangeThatFits (runes, runes.Count - width, text, width, tabWidth, textDirection);
  1591. }
  1592. if (textFormatter is { Alignment: Alignment.Center })
  1593. {
  1594. return GetRangeThatFits (runes, Math.Max ((runes.Count - width - zeroLength) / 2, 0), text, width, tabWidth, textDirection);
  1595. }
  1596. return GetRangeThatFits (runes, 0, text, width, tabWidth, textDirection);
  1597. }
  1598. if (IsVerticalDirection (textDirection))
  1599. {
  1600. if (textFormatter is { VerticalAlignment: Alignment.End })
  1601. {
  1602. return GetRangeThatFits (runes, runes.Count - width, text, width, tabWidth, textDirection);
  1603. }
  1604. if (textFormatter is { VerticalAlignment: Alignment.Center })
  1605. {
  1606. return GetRangeThatFits (runes, Math.Max ((runes.Count - width - zeroLength) / 2, 0), text, width, tabWidth, textDirection);
  1607. }
  1608. return GetRangeThatFits (runes, 0, text, width, tabWidth, textDirection);
  1609. }
  1610. return StringExtensions.ToString (runes.GetRange (0, width + zeroLength));
  1611. }
  1612. if (justify)
  1613. {
  1614. return Justify (text, width, ' ', textDirection, tabWidth);
  1615. }
  1616. if (IsHorizontalDirection (textDirection))
  1617. {
  1618. if (textFormatter is { Alignment: Alignment.End })
  1619. {
  1620. if (GetRuneWidth (text, tabWidth, textDirection) > width)
  1621. {
  1622. return GetRangeThatFits (runes, runes.Count - width, text, width, tabWidth, textDirection);
  1623. }
  1624. }
  1625. else if (textFormatter is { Alignment: Alignment.Center })
  1626. {
  1627. return GetRangeThatFits (runes, Math.Max ((runes.Count - width - zeroLength) / 2, 0), text, width, tabWidth, textDirection);
  1628. }
  1629. else if (GetRuneWidth (text, tabWidth, textDirection) > width)
  1630. {
  1631. return GetRangeThatFits (runes, 0, text, width, tabWidth, textDirection);
  1632. }
  1633. }
  1634. if (IsVerticalDirection (textDirection))
  1635. {
  1636. if (textFormatter is { VerticalAlignment: Alignment.End })
  1637. {
  1638. if (runes.Count - zeroLength > width)
  1639. {
  1640. return GetRangeThatFits (runes, runes.Count - width, text, width, tabWidth, textDirection);
  1641. }
  1642. }
  1643. else if (textFormatter is { VerticalAlignment: Alignment.Center })
  1644. {
  1645. return GetRangeThatFits (runes, Math.Max ((runes.Count - width - zeroLength) / 2, 0), text, width, tabWidth, textDirection);
  1646. }
  1647. else if (runes.Count - zeroLength > width)
  1648. {
  1649. return GetRangeThatFits (runes, 0, text, width, tabWidth, textDirection);
  1650. }
  1651. }
  1652. return text;
  1653. }
  1654. private static string GetRangeThatFits (List<Rune> runes, int index, string text, int width, int tabWidth, TextDirection textDirection)
  1655. {
  1656. return StringExtensions.ToString (
  1657. runes.GetRange (
  1658. Math.Max (index, 0),
  1659. GetLengthThatFits (text, width, tabWidth, textDirection)
  1660. )
  1661. );
  1662. }
  1663. /// <summary>
  1664. /// Justifies the text to fill the width provided. Space will be added between words to make the text just fit
  1665. /// <c>width</c>. Spaces will not be added to the start or end.
  1666. /// </summary>
  1667. /// <param name="text"></param>
  1668. /// <param name="width"></param>
  1669. /// <param name="spaceChar">Character to replace whitespace and pad with. For debugging purposes.</param>
  1670. /// <param name="textDirection">The text direction.</param>
  1671. /// <param name="tabWidth">The number of columns used for a tab.</param>
  1672. /// <returns>The justified text.</returns>
  1673. public static string Justify (
  1674. string text,
  1675. int width,
  1676. char spaceChar = ' ',
  1677. TextDirection textDirection = TextDirection.LeftRight_TopBottom,
  1678. int tabWidth = 0
  1679. )
  1680. {
  1681. ArgumentOutOfRangeException.ThrowIfNegative (width, nameof (width));
  1682. if (string.IsNullOrEmpty (text))
  1683. {
  1684. return text;
  1685. }
  1686. text = ReplaceTABWithSpaces (text, tabWidth);
  1687. string [] words = text.Split (' ');
  1688. int textCount;
  1689. if (IsHorizontalDirection (textDirection))
  1690. {
  1691. textCount = words.Sum (arg => GetRuneWidth (arg, tabWidth, textDirection));
  1692. }
  1693. else
  1694. {
  1695. textCount = words.Sum (arg => arg.GetRuneCount ()) - text.EnumerateRunes ().Sum (r => r.GetColumns () == 0 ? 1 : 0);
  1696. }
  1697. int spaces = words.Length > 1 ? (width - textCount) / (words.Length - 1) : 0;
  1698. int extras = words.Length > 1 ? (width - textCount) % (words.Length - 1) : 0;
  1699. var s = new StringBuilder ();
  1700. for (var w = 0; w < words.Length; w++)
  1701. {
  1702. string x = words [w];
  1703. s.Append (x);
  1704. if (w + 1 < words.Length)
  1705. {
  1706. for (var i = 0; i < spaces; i++)
  1707. {
  1708. s.Append (spaceChar);
  1709. }
  1710. }
  1711. if (extras > 0)
  1712. {
  1713. for (var i = 0; i < 1; i++)
  1714. {
  1715. s.Append (spaceChar);
  1716. }
  1717. extras--;
  1718. }
  1719. if (w + 1 == words.Length - 1)
  1720. {
  1721. for (var i = 0; i < extras; i++)
  1722. {
  1723. s.Append (spaceChar);
  1724. }
  1725. }
  1726. }
  1727. return s.ToString ();
  1728. }
  1729. /// <summary>Formats text into lines, applying text alignment and optionally wrapping text to new lines on word boundaries.</summary>
  1730. /// <param name="text"></param>
  1731. /// <param name="width">The number of columns to constrain the text to for word wrapping and clipping.</param>
  1732. /// <param name="textAlignment">Specifies how the text will be aligned horizontally.</param>
  1733. /// <param name="wordWrap">
  1734. /// If <see langword="true"/>, the text will be wrapped to new lines no longer than
  1735. /// <paramref name="width"/>. If <see langword="false"/>, forces text to fit a single line. Line breaks are converted
  1736. /// to spaces. The text will be clipped to <paramref name="width"/>.
  1737. /// </param>
  1738. /// <param name="preserveTrailingSpaces">
  1739. /// If <see langword="true"/> trailing spaces at the end of wrapped lines will be
  1740. /// preserved. If <see langword="false"/> , trailing spaces at the end of wrapped lines will be trimmed.
  1741. /// </param>
  1742. /// <param name="tabWidth">The number of columns used for a tab.</param>
  1743. /// <param name="textDirection">The text direction.</param>
  1744. /// <param name="multiLine">If <see langword="true"/> new lines are allowed.</param>
  1745. /// <param name="textFormatter"><see cref="TextFormatter"/> instance to access any of his objects.</param>
  1746. /// <returns>A list of word wrapped lines.</returns>
  1747. /// <remarks>
  1748. /// <para>An empty <paramref name="text"/> string will result in one empty line.</para>
  1749. /// <para>If <paramref name="width"/> is 0, a single, empty line will be returned.</para>
  1750. /// <para>If <paramref name="width"/> is int.MaxValue, the text will be formatted to the maximum width possible.</para>
  1751. /// </remarks>
  1752. public static List<string> Format (
  1753. string text,
  1754. int width,
  1755. Alignment textAlignment,
  1756. bool wordWrap,
  1757. bool preserveTrailingSpaces = false,
  1758. int tabWidth = 0,
  1759. TextDirection textDirection = TextDirection.LeftRight_TopBottom,
  1760. bool multiLine = false,
  1761. TextFormatter? textFormatter = null
  1762. )
  1763. {
  1764. return Format (
  1765. text,
  1766. width,
  1767. textAlignment == Alignment.Fill,
  1768. wordWrap,
  1769. preserveTrailingSpaces,
  1770. tabWidth,
  1771. textDirection,
  1772. multiLine,
  1773. textFormatter
  1774. );
  1775. }
  1776. /// <summary>Formats text into lines, applying text alignment and optionally wrapping text to new lines on word boundaries.</summary>
  1777. /// <param name="text"></param>
  1778. /// <param name="width">The number of columns to constrain the text to for word wrapping and clipping.</param>
  1779. /// <param name="justify">Specifies whether the text should be justified.</param>
  1780. /// <param name="wordWrap">
  1781. /// If <see langword="true"/>, the text will be wrapped to new lines no longer than
  1782. /// <paramref name="width"/>. If <see langword="false"/>, forces text to fit a single line. Line breaks are converted
  1783. /// to spaces. The text will be clipped to <paramref name="width"/>.
  1784. /// </param>
  1785. /// <param name="preserveTrailingSpaces">
  1786. /// If <see langword="true"/> trailing spaces at the end of wrapped lines will be
  1787. /// preserved. If <see langword="false"/> , trailing spaces at the end of wrapped lines will be trimmed.
  1788. /// </param>
  1789. /// <param name="tabWidth">The number of columns used for a tab.</param>
  1790. /// <param name="textDirection">The text direction.</param>
  1791. /// <param name="multiLine">If <see langword="true"/> new lines are allowed.</param>
  1792. /// <param name="textFormatter"><see cref="TextFormatter"/> instance to access any of his objects.</param>
  1793. /// <returns>A list of word wrapped lines.</returns>
  1794. /// <remarks>
  1795. /// <para>An empty <paramref name="text"/> string will result in one empty line.</para>
  1796. /// <para>If <paramref name="width"/> is 0, a single, empty line will be returned.</para>
  1797. /// <para>If <paramref name="width"/> is int.MaxValue, the text will be formatted to the maximum width possible.</para>
  1798. /// </remarks>
  1799. public static List<string> Format (
  1800. string text,
  1801. int width,
  1802. bool justify,
  1803. bool wordWrap,
  1804. bool preserveTrailingSpaces = false,
  1805. int tabWidth = 0,
  1806. TextDirection textDirection = TextDirection.LeftRight_TopBottom,
  1807. bool multiLine = false,
  1808. TextFormatter? textFormatter = null
  1809. )
  1810. {
  1811. ArgumentOutOfRangeException.ThrowIfNegative (width, nameof (width));
  1812. List<string> lineResult = new ();
  1813. if (string.IsNullOrEmpty (text) || width == 0)
  1814. {
  1815. lineResult.Add (string.Empty);
  1816. return lineResult;
  1817. }
  1818. if (!wordWrap)
  1819. {
  1820. text = ReplaceTABWithSpaces (text, tabWidth);
  1821. if (multiLine)
  1822. {
  1823. // Abhorrent case: Just a new line
  1824. if (text == "\n")
  1825. {
  1826. lineResult.Add (string.Empty);
  1827. return lineResult;
  1828. }
  1829. string []? lines = null;
  1830. if (text.Contains ("\r\n"))
  1831. {
  1832. lines = text.Split ("\r\n");
  1833. }
  1834. else if (text.Contains ('\n'))
  1835. {
  1836. lines = text.Split ('\n');
  1837. }
  1838. lines ??= new [] { text };
  1839. foreach (string line in lines)
  1840. {
  1841. lineResult.Add (
  1842. ClipAndJustify (
  1843. PerformCorrectFormatDirection (textDirection, line),
  1844. width,
  1845. justify,
  1846. textDirection,
  1847. tabWidth,
  1848. textFormatter));
  1849. }
  1850. return PerformCorrectFormatDirection (textDirection, lineResult);
  1851. }
  1852. text = ReplaceCRLFWithSpace (text);
  1853. lineResult.Add (ClipAndJustify (PerformCorrectFormatDirection (textDirection, text), width, justify, textDirection, tabWidth, textFormatter));
  1854. return PerformCorrectFormatDirection (textDirection, lineResult);
  1855. }
  1856. List<Rune> runes = StripCRLF (text, true).ToRuneList ();
  1857. int runeCount = runes.Count;
  1858. var lp = 0;
  1859. for (var i = 0; i < runeCount; i++)
  1860. {
  1861. Rune c = runes [i];
  1862. if (c.Value == '\n')
  1863. {
  1864. List<string> wrappedLines =
  1865. WordWrapText (
  1866. StringExtensions.ToString (PerformCorrectFormatDirection (textDirection, runes.GetRange (lp, i - lp))),
  1867. width,
  1868. preserveTrailingSpaces,
  1869. tabWidth,
  1870. textDirection,
  1871. textFormatter
  1872. );
  1873. foreach (string line in wrappedLines)
  1874. {
  1875. lineResult.Add (ClipAndJustify (line, width, justify, textDirection, tabWidth));
  1876. }
  1877. if (wrappedLines.Count == 0)
  1878. {
  1879. lineResult.Add (string.Empty);
  1880. }
  1881. lp = i + 1;
  1882. }
  1883. }
  1884. foreach (string line in WordWrapText (
  1885. StringExtensions.ToString (PerformCorrectFormatDirection (textDirection, runes.GetRange (lp, runeCount - lp))),
  1886. width,
  1887. preserveTrailingSpaces,
  1888. tabWidth,
  1889. textDirection,
  1890. textFormatter
  1891. ))
  1892. {
  1893. lineResult.Add (ClipAndJustify (line, width, justify, textDirection, tabWidth));
  1894. }
  1895. return PerformCorrectFormatDirection (textDirection, lineResult);
  1896. }
  1897. private static string PerformCorrectFormatDirection (TextDirection textDirection, string line)
  1898. {
  1899. return textDirection switch
  1900. {
  1901. TextDirection.RightLeft_BottomTop
  1902. or TextDirection.RightLeft_TopBottom
  1903. or TextDirection.BottomTop_LeftRight
  1904. or TextDirection.BottomTop_RightLeft => StringExtensions.ToString (line.EnumerateRunes ().Reverse ()),
  1905. _ => line
  1906. };
  1907. }
  1908. private static List<Rune> PerformCorrectFormatDirection (TextDirection textDirection, List<Rune> runes)
  1909. {
  1910. return PerformCorrectFormatDirection (textDirection, StringExtensions.ToString (runes)).ToRuneList ();
  1911. }
  1912. private static List<string> PerformCorrectFormatDirection (TextDirection textDirection, List<string> lines)
  1913. {
  1914. return textDirection switch
  1915. {
  1916. TextDirection.TopBottom_RightLeft
  1917. or TextDirection.LeftRight_BottomTop
  1918. or TextDirection.RightLeft_BottomTop
  1919. or TextDirection.BottomTop_RightLeft => lines.ToArray ().Reverse ().ToList (),
  1920. _ => lines
  1921. };
  1922. }
  1923. /// <summary>
  1924. /// Returns the number of columns required to render <paramref name="lines"/> oriented vertically.
  1925. /// </summary>
  1926. /// <remarks>
  1927. /// This API will return incorrect results if the text includes glyphs whose width is dependent on surrounding
  1928. /// glyphs (e.g. Arabic).
  1929. /// </remarks>
  1930. /// <param name="lines">The lines.</param>
  1931. /// <param name="startLine">The line in the list to start with (any lines before will be ignored).</param>
  1932. /// <param name="linesCount">
  1933. /// The number of lines to process (if less than <c>lines.Count</c>, any lines after will be
  1934. /// ignored).
  1935. /// </param>
  1936. /// <param name="tabWidth">The number of columns used for a tab.</param>
  1937. /// <returns>The width required.</returns>
  1938. public static int GetColumnsRequiredForVerticalText (
  1939. List<string> lines,
  1940. int startLine = -1,
  1941. int linesCount = -1,
  1942. int tabWidth = 0
  1943. )
  1944. {
  1945. var max = 0;
  1946. for (int i = startLine == -1 ? 0 : startLine;
  1947. i < (linesCount == -1 ? lines.Count : startLine + linesCount);
  1948. i++)
  1949. {
  1950. string runes = lines [i];
  1951. if (runes.Length > 0)
  1952. {
  1953. max += runes.EnumerateRunes ().Max (r => GetRuneWidth (r, tabWidth));
  1954. }
  1955. }
  1956. return max;
  1957. }
  1958. /// <summary>
  1959. /// Returns the number of columns in the widest line in the text, without word wrap, accounting for wide-glyphs
  1960. /// (uses <see cref="StringExtensions.GetColumns"/>). <paramref name="text"/> if it contains newlines.
  1961. /// </summary>
  1962. /// <remarks>
  1963. /// This API will return incorrect results if the text includes glyphs whose width is dependent on surrounding
  1964. /// glyphs (e.g. Arabic).
  1965. /// </remarks>
  1966. /// <param name="text">Text, may contain newlines.</param>
  1967. /// <param name="tabWidth">The number of columns used for a tab.</param>
  1968. /// <returns>The length of the longest line.</returns>
  1969. public static int GetWidestLineLength (string text, int tabWidth = 0)
  1970. {
  1971. List<string> result = SplitNewLine (text);
  1972. return result.Max (x => GetRuneWidth (x, tabWidth));
  1973. }
  1974. /// <summary>
  1975. /// Gets the maximum number of columns from the text based on the <paramref name="startIndex"/> and the
  1976. /// <paramref name="length"/>.
  1977. /// </summary>
  1978. /// <remarks>
  1979. /// This API will return incorrect results if the text includes glyphs whose width is dependent on surrounding
  1980. /// glyphs (e.g. Arabic).
  1981. /// </remarks>
  1982. /// <param name="text">The text.</param>
  1983. /// <param name="startIndex">The start index.</param>
  1984. /// <param name="length">The length.</param>
  1985. /// <param name="tabWidth">The number of columns used for a tab.</param>
  1986. /// <returns>The maximum characters width.</returns>
  1987. public static int GetSumMaxCharWidth (string text, int startIndex = -1, int length = -1, int tabWidth = 0)
  1988. {
  1989. var max = 0;
  1990. Rune [] runes = text.ToRunes ();
  1991. for (int i = startIndex == -1 ? 0 : startIndex;
  1992. i < (length == -1 ? runes.Length : startIndex + length);
  1993. i++)
  1994. {
  1995. max += GetRuneWidth (runes [i], tabWidth);
  1996. }
  1997. return max;
  1998. }
  1999. /// <summary>Gets the number of the Runes in the text that will fit in <paramref name="width"/>.</summary>
  2000. /// <remarks>
  2001. /// This API will return incorrect results if the text includes glyphs whose width is dependent on surrounding
  2002. /// glyphs (e.g. Arabic).
  2003. /// </remarks>
  2004. /// <param name="text">The text.</param>
  2005. /// <param name="width">The width.</param>
  2006. /// <param name="tabWidth">The width used for a tab.</param>
  2007. /// <param name="textDirection">The text direction.</param>
  2008. /// <returns>The index of the text that fit the width.</returns>
  2009. public static int GetLengthThatFits (string text, int width, int tabWidth = 0, TextDirection textDirection = TextDirection.LeftRight_TopBottom)
  2010. {
  2011. return GetLengthThatFits (text?.ToRuneList () ?? [], width, tabWidth, textDirection);
  2012. }
  2013. /// <summary>Gets the number of the Runes in a list of Runes that will fit in <paramref name="width"/>.</summary>
  2014. /// <remarks>
  2015. /// This API will return incorrect results if the text includes glyphs whose width is dependent on surrounding
  2016. /// glyphs (e.g. Arabic).
  2017. /// </remarks>
  2018. /// <param name="runes">The list of runes.</param>
  2019. /// <param name="width">The width.</param>
  2020. /// <param name="tabWidth">The width used for a tab.</param>
  2021. /// <param name="textDirection">The text direction.</param>
  2022. /// <returns>The index of the last Rune in <paramref name="runes"/> that fit in <paramref name="width"/>.</returns>
  2023. public static int GetLengthThatFits (List<Rune> runes, int width, int tabWidth = 0, TextDirection textDirection = TextDirection.LeftRight_TopBottom)
  2024. {
  2025. if (runes is null || runes.Count == 0)
  2026. {
  2027. return 0;
  2028. }
  2029. var runesLength = 0;
  2030. var runeIdx = 0;
  2031. for (; runeIdx < runes.Count; runeIdx++)
  2032. {
  2033. int runeWidth = GetRuneWidth (runes [runeIdx], tabWidth, textDirection);
  2034. if (runesLength + runeWidth > width)
  2035. {
  2036. break;
  2037. }
  2038. runesLength += runeWidth;
  2039. }
  2040. return runeIdx;
  2041. }
  2042. private static int GetRuneWidth (string str, int tabWidth, TextDirection textDirection = TextDirection.LeftRight_TopBottom)
  2043. {
  2044. int runesWidth = 0;
  2045. foreach (Rune rune in str.EnumerateRunes ())
  2046. {
  2047. runesWidth += GetRuneWidth (rune, tabWidth, textDirection);
  2048. }
  2049. return runesWidth;
  2050. }
  2051. private static int GetRuneWidth (Rune rune, int tabWidth, TextDirection textDirection = TextDirection.LeftRight_TopBottom)
  2052. {
  2053. int runeWidth = IsHorizontalDirection (textDirection) ? rune.GetColumns () : rune.GetColumns () == 0 ? 0 : 1;
  2054. if (rune.Value == '\t')
  2055. {
  2056. return tabWidth;
  2057. }
  2058. if (runeWidth < 0 || runeWidth > 0)
  2059. {
  2060. return Math.Max (runeWidth, 1);
  2061. }
  2062. return runeWidth;
  2063. }
  2064. /// <summary>Gets the index position from the list based on the <paramref name="width"/>.</summary>
  2065. /// <remarks>
  2066. /// This API will return incorrect results if the text includes glyphs whose width is dependent on surrounding
  2067. /// glyphs (e.g. Arabic).
  2068. /// </remarks>
  2069. /// <param name="lines">The lines.</param>
  2070. /// <param name="width">The width.</param>
  2071. /// <param name="tabWidth">The number of columns used for a tab.</param>
  2072. /// <returns>The index of the list that fit the width.</returns>
  2073. public static int GetMaxColsForWidth (List<string> lines, int width, int tabWidth = 0)
  2074. {
  2075. var runesLength = 0;
  2076. var lineIdx = 0;
  2077. for (; lineIdx < lines.Count; lineIdx++)
  2078. {
  2079. List<Rune> runes = lines [lineIdx].ToRuneList ();
  2080. int maxRruneWidth = runes.Count > 0
  2081. ? runes.Max (r => GetRuneWidth (r, tabWidth))
  2082. : 1;
  2083. if (runesLength + maxRruneWidth > width)
  2084. {
  2085. break;
  2086. }
  2087. runesLength += maxRruneWidth;
  2088. }
  2089. return lineIdx;
  2090. }
  2091. /// <summary>Finds the HotKey and its location in text.</summary>
  2092. /// <param name="text">The text to look in.</param>
  2093. /// <param name="hotKeySpecifier">The HotKey specifier (e.g. '_') to look for.</param>
  2094. /// <param name="hotPos">Outputs the Rune index into <c>text</c>.</param>
  2095. /// <param name="hotKey">Outputs the hotKey. <see cref="Key.Empty"/> if not found.</param>
  2096. /// <param name="firstUpperCase">
  2097. /// If <c>true</c> the legacy behavior of identifying the first upper case character as the
  2098. /// HotKey will be enabled. Regardless of the value of this parameter, <c>hotKeySpecifier</c> takes precedence.
  2099. /// Defaults to <see langword="false"/>.
  2100. /// </param>
  2101. /// <returns><c>true</c> if a HotKey was found; <c>false</c> otherwise.</returns>
  2102. public static bool FindHotKey (
  2103. string text,
  2104. Rune hotKeySpecifier,
  2105. out int hotPos,
  2106. out Key hotKey,
  2107. bool firstUpperCase = false
  2108. )
  2109. {
  2110. if (string.IsNullOrEmpty (text) || hotKeySpecifier == (Rune)0xFFFF)
  2111. {
  2112. hotPos = -1;
  2113. hotKey = Key.Empty;
  2114. return false;
  2115. }
  2116. var curHotKey = (Rune)0;
  2117. int curHotPos = -1;
  2118. // Use first hot_key char passed into 'hotKey'.
  2119. // TODO: Ignore hot_key of two are provided
  2120. // TODO: Do not support non-alphanumeric chars that can't be typed
  2121. var i = 0;
  2122. foreach (Rune c in text.EnumerateRunes ())
  2123. {
  2124. if ((char)c.Value != 0xFFFD)
  2125. {
  2126. if (c == hotKeySpecifier)
  2127. {
  2128. curHotPos = i;
  2129. }
  2130. else if (curHotPos > -1)
  2131. {
  2132. curHotKey = c;
  2133. break;
  2134. }
  2135. }
  2136. i++;
  2137. }
  2138. // Legacy support - use first upper case char if the specifier was not found
  2139. if (curHotPos == -1 && firstUpperCase)
  2140. {
  2141. i = 0;
  2142. foreach (Rune c in text.EnumerateRunes ())
  2143. {
  2144. if ((char)c.Value != 0xFFFD)
  2145. {
  2146. if (Rune.IsUpper (c))
  2147. {
  2148. curHotKey = c;
  2149. curHotPos = i;
  2150. break;
  2151. }
  2152. }
  2153. i++;
  2154. }
  2155. }
  2156. if (curHotKey != (Rune)0 && curHotPos != -1)
  2157. {
  2158. hotPos = curHotPos;
  2159. var newHotKey = (KeyCode)curHotKey.Value;
  2160. if (newHotKey != KeyCode.Null && !(newHotKey == KeyCode.Space || Rune.IsControl (curHotKey)))
  2161. {
  2162. if ((newHotKey & ~KeyCode.Space) is >= KeyCode.A and <= KeyCode.Z)
  2163. {
  2164. newHotKey &= ~KeyCode.Space;
  2165. }
  2166. hotKey = newHotKey;
  2167. //hotKey.Scope = KeyBindingScope.HotKey;
  2168. return true;
  2169. }
  2170. }
  2171. hotPos = -1;
  2172. hotKey = KeyCode.Null;
  2173. return false;
  2174. }
  2175. /// <summary>
  2176. /// Replaces the Rune at the index specified by the <c>hotPos</c> parameter with a tag identifying it as the
  2177. /// hotkey.
  2178. /// </summary>
  2179. /// <param name="text">The text to tag the hotkey in.</param>
  2180. /// <param name="hotPos">The Rune index of the hotkey in <c>text</c>.</param>
  2181. /// <returns>The text with the hotkey tagged.</returns>
  2182. /// <remarks>The returned string will not render correctly without first un-doing the tag. To undo the tag, search for</remarks>
  2183. public static string ReplaceHotKeyWithTag (string text, int hotPos)
  2184. {
  2185. // Set the high bit
  2186. List<Rune> runes = text.ToRuneList ();
  2187. if (Rune.IsLetterOrDigit (runes [hotPos]))
  2188. {
  2189. runes [hotPos] = new ((uint)runes [hotPos].Value);
  2190. }
  2191. return StringExtensions.ToString (runes);
  2192. }
  2193. /// <summary>Removes the hotkey specifier from text.</summary>
  2194. /// <param name="text">The text to manipulate.</param>
  2195. /// <param name="hotKeySpecifier">The hot-key specifier (e.g. '_') to look for.</param>
  2196. /// <param name="hotPos">Returns the position of the hot-key in the text. -1 if not found.</param>
  2197. /// <returns>The input text with the hotkey specifier ('_') removed.</returns>
  2198. public static string RemoveHotKeySpecifier (string text, int hotPos, Rune hotKeySpecifier)
  2199. {
  2200. if (string.IsNullOrEmpty (text))
  2201. {
  2202. return text;
  2203. }
  2204. const int maxStackallocCharBufferSize = 512; // ~1 kB
  2205. char[]? rentedBufferArray = null;
  2206. try
  2207. {
  2208. Span<char> buffer = text.Length <= maxStackallocCharBufferSize
  2209. ? stackalloc char[text.Length]
  2210. : (rentedBufferArray = ArrayPool<char>.Shared.Rent(text.Length));
  2211. int i = 0;
  2212. var remainingBuffer = buffer;
  2213. foreach (Rune c in text.EnumerateRunes ())
  2214. {
  2215. if (c == hotKeySpecifier && i == hotPos)
  2216. {
  2217. i++;
  2218. continue;
  2219. }
  2220. int charsWritten = c.EncodeToUtf16 (remainingBuffer);
  2221. remainingBuffer = remainingBuffer [charsWritten..];
  2222. i++;
  2223. }
  2224. ReadOnlySpan<char> newText = buffer [..^remainingBuffer.Length];
  2225. // If the resulting string would be the same as original then just return the original.
  2226. if (newText.Equals(text, StringComparison.Ordinal))
  2227. {
  2228. return text;
  2229. }
  2230. return new string (newText);
  2231. }
  2232. finally
  2233. {
  2234. if (rentedBufferArray != null)
  2235. {
  2236. ArrayPool<char>.Shared.Return (rentedBufferArray);
  2237. }
  2238. }
  2239. }
  2240. #endregion // Static Members
  2241. }