TextFormatter.cs 95 KB

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