TextFormatter.cs 70 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055
  1. namespace Terminal.Gui;
  2. /// <summary>Text alignment enumeration, controls how text is displayed.</summary>
  3. public enum TextAlignment
  4. {
  5. /// <summary>The text will be left-aligned.</summary>
  6. Left,
  7. /// <summary>The text will be right-aligned.</summary>
  8. Right,
  9. /// <summary>The text will be centered horizontally.</summary>
  10. Centered,
  11. /// <summary>
  12. /// The text will be justified (spaces will be added to existing spaces such that the text fills the container
  13. /// horizontally).
  14. /// </summary>
  15. Justified
  16. }
  17. /// <summary>Vertical text alignment enumeration, controls how text is displayed.</summary>
  18. public enum VerticalTextAlignment
  19. {
  20. /// <summary>The text will be top-aligned.</summary>
  21. Top,
  22. /// <summary>The text will be bottom-aligned.</summary>
  23. Bottom,
  24. /// <summary>The text will centered vertically.</summary>
  25. Middle,
  26. /// <summary>
  27. /// The text will be justified (spaces will be added to existing spaces such that the text fills the container
  28. /// vertically).
  29. /// </summary>
  30. Justified
  31. }
  32. /// <summary>Text direction enumeration, controls how text is displayed.</summary>
  33. /// <remarks>
  34. /// <para>TextDirection [H] = Horizontal [V] = Vertical</para>
  35. /// <table>
  36. /// <tr>
  37. /// <th>TextDirection</th> <th>Description</th>
  38. /// </tr>
  39. /// <tr>
  40. /// <td>LeftRight_TopBottom [H]</td> <td>Normal</td>
  41. /// </tr>
  42. /// <tr>
  43. /// <td>TopBottom_LeftRight [V]</td> <td>Normal</td>
  44. /// </tr>
  45. /// <tr>
  46. /// <td>RightLeft_TopBottom [H]</td> <td>Invert Text</td>
  47. /// </tr>
  48. /// <tr>
  49. /// <td>TopBottom_RightLeft [V]</td> <td>Invert Lines</td>
  50. /// </tr>
  51. /// <tr>
  52. /// <td>LeftRight_BottomTop [H]</td> <td>Invert Lines</td>
  53. /// </tr>
  54. /// <tr>
  55. /// <td>BottomTop_LeftRight [V]</td> <td>Invert Text</td>
  56. /// </tr>
  57. /// <tr>
  58. /// <td>RightLeft_BottomTop [H]</td> <td>Invert Text + Invert Lines</td>
  59. /// </tr>
  60. /// <tr>
  61. /// <td>BottomTop_RightLeft [V]</td> <td>Invert Text + Invert Lines</td>
  62. /// </tr>
  63. /// </table>
  64. /// </remarks>
  65. public enum TextDirection
  66. {
  67. /// <summary>Normal horizontal direction. <code>HELLO<br/>WORLD</code></summary>
  68. LeftRight_TopBottom,
  69. /// <summary>Normal vertical direction. <code>H W<br/>E O<br/>L R<br/>L L<br/>O D</code></summary>
  70. TopBottom_LeftRight,
  71. /// <summary>This is a horizontal direction. <br/> RTL <code>OLLEH<br/>DLROW</code></summary>
  72. RightLeft_TopBottom,
  73. /// <summary>This is a vertical direction. <code>W H<br/>O E<br/>R L<br/>L L<br/>D O</code></summary>
  74. TopBottom_RightLeft,
  75. /// <summary>This is a horizontal direction. <code>WORLD<br/>HELLO</code></summary>
  76. LeftRight_BottomTop,
  77. /// <summary>This is a vertical direction. <code>O D<br/>L L<br/>L R<br/>E O<br/>H W</code></summary>
  78. BottomTop_LeftRight,
  79. /// <summary>This is a horizontal direction. <code>DLROW<br/>OLLEH</code></summary>
  80. RightLeft_BottomTop,
  81. /// <summary>This is a vertical direction. <code>D O<br/>L L<br/>R L<br/>O E<br/>W H</code></summary>
  82. BottomTop_RightLeft
  83. }
  84. /// <summary>
  85. /// Provides text formatting. Supports <see cref="View.HotKey"/>s, horizontal alignment, vertical alignment, multiple
  86. /// lines, and word-based line wrap.
  87. /// </summary>
  88. public class TextFormatter
  89. {
  90. private bool _autoSize;
  91. private Key _hotKey = new ();
  92. private int _hotKeyPos = -1;
  93. private List<string> _lines = new ();
  94. private bool _multiLine;
  95. private bool _preserveTrailingSpaces;
  96. private Size _size;
  97. private int _tabWidth = 4;
  98. private string _text;
  99. private TextAlignment _textAlignment;
  100. private TextDirection _textDirection;
  101. private VerticalTextAlignment _textVerticalAlignment;
  102. private bool _wordWrap = true;
  103. /// <summary>Controls the horizontal text-alignment property.</summary>
  104. /// <value>The text alignment.</value>
  105. public TextAlignment Alignment { get => _textAlignment; set => _textAlignment = EnableNeedsFormat (value); }
  106. /// <summary>Gets or sets whether the <see cref="Size"/> should be automatically changed to fit the <see cref="Text"/>.</summary>
  107. /// <remarks>
  108. /// <para>Used by <see cref="View.AutoSize"/> to resize the view's <see cref="View.Bounds"/> to fit <see cref="Size"/>.</para>
  109. /// <para>
  110. /// AutoSize is ignored if <see cref="TextAlignment.Justified"/> and <see cref="VerticalTextAlignment.Justified"/>
  111. /// are used.
  112. /// </para>
  113. /// </remarks>
  114. public bool AutoSize
  115. {
  116. get => _autoSize;
  117. set
  118. {
  119. _autoSize = EnableNeedsFormat (value);
  120. if (_autoSize && Alignment != TextAlignment.Justified && VerticalAlignment != VerticalTextAlignment.Justified)
  121. {
  122. Size = CalcRect (0, 0, _text, Direction, TabWidth).Size;
  123. }
  124. }
  125. }
  126. /// <summary>
  127. /// Gets the cursor position from <see cref="HotKey"/>. If the <see cref="HotKey"/> is defined, the cursor will be
  128. /// positioned over it.
  129. /// </summary>
  130. public int CursorPosition { get; internal set; }
  131. /// <summary>Controls the text-direction property.</summary>
  132. /// <value>The text vertical alignment.</value>
  133. public TextDirection Direction
  134. {
  135. get => _textDirection;
  136. set
  137. {
  138. _textDirection = EnableNeedsFormat (value);
  139. if (AutoSize && Alignment != TextAlignment.Justified && VerticalAlignment != VerticalTextAlignment.Justified)
  140. {
  141. Size = CalcRect (0, 0, Text, Direction, TabWidth).Size;
  142. }
  143. }
  144. }
  145. /// <summary>
  146. /// Gets or sets the hot key. Must be be an upper case letter or digit. Fires the <see cref="HotKeyChanged"/>
  147. /// event.
  148. /// </summary>
  149. public Key HotKey
  150. {
  151. get => _hotKey;
  152. internal set
  153. {
  154. if (_hotKey != value)
  155. {
  156. Key oldKey = _hotKey;
  157. _hotKey = value;
  158. HotKeyChanged?.Invoke (this, new KeyChangedEventArgs (oldKey, value));
  159. }
  160. }
  161. }
  162. /// <summary>The position in the text of the hot key. The hot key will be rendered using the hot color.</summary>
  163. public int HotKeyPos { get => _hotKeyPos; internal set => _hotKeyPos = value; }
  164. /// <summary>
  165. /// The specifier character for the hot key (e.g. '_'). Set to '\xffff' to disable hot key support for this View
  166. /// instance. The default is '\xffff'.
  167. /// </summary>
  168. public Rune HotKeySpecifier { get; set; } = (Rune)0xFFFF;
  169. /// <summary>Gets the formatted lines.</summary>
  170. /// <remarks>
  171. /// <para>
  172. /// Upon a 'get' of this property, if the text needs to be formatted (if <see cref="NeedsFormat"/> is <c>true</c>)
  173. /// <see cref="Format(string, int, bool, bool, bool, int, TextDirection, bool)"/> will be called internally.
  174. /// </para>
  175. /// </remarks>
  176. public List<string> Lines
  177. {
  178. get
  179. {
  180. // With this check, we protect against subclasses with overrides of Text
  181. if (string.IsNullOrEmpty (Text) || Size.IsEmpty)
  182. {
  183. _lines = new List<string>
  184. {
  185. string.Empty
  186. };
  187. NeedsFormat = false;
  188. return _lines;
  189. }
  190. if (NeedsFormat)
  191. {
  192. string shown_text = _text;
  193. if (FindHotKey (_text, HotKeySpecifier, out _hotKeyPos, out Key newHotKey))
  194. {
  195. HotKey = newHotKey;
  196. shown_text = RemoveHotKeySpecifier (Text, _hotKeyPos, HotKeySpecifier);
  197. shown_text = ReplaceHotKeyWithTag (shown_text, _hotKeyPos);
  198. }
  199. if (IsVerticalDirection (Direction))
  200. {
  201. int colsWidth = GetSumMaxCharWidth (shown_text, 0, 1, TabWidth);
  202. _lines = Format (
  203. shown_text,
  204. Size.Height,
  205. VerticalAlignment == VerticalTextAlignment.Justified,
  206. Size.Width > colsWidth && WordWrap,
  207. PreserveTrailingSpaces,
  208. TabWidth,
  209. Direction,
  210. MultiLine);
  211. if (!AutoSize)
  212. {
  213. colsWidth = GetMaxColsForWidth (_lines, Size.Width, TabWidth);
  214. if (_lines.Count > colsWidth)
  215. {
  216. _lines.RemoveRange (colsWidth, _lines.Count - colsWidth);
  217. }
  218. }
  219. }
  220. else
  221. {
  222. _lines = Format (
  223. shown_text,
  224. Size.Width,
  225. Alignment == TextAlignment.Justified,
  226. Size.Height > 1 && WordWrap,
  227. PreserveTrailingSpaces,
  228. TabWidth,
  229. Direction,
  230. MultiLine);
  231. if (!AutoSize && _lines.Count > Size.Height)
  232. {
  233. _lines.RemoveRange (Size.Height, _lines.Count - Size.Height);
  234. }
  235. }
  236. NeedsFormat = false;
  237. }
  238. return _lines;
  239. }
  240. }
  241. /// <summary>Gets or sets a value indicating whether multi line is allowed.</summary>
  242. /// <remarks>Multi line is ignored if <see cref="WordWrap"/> is <see langword="true"/>.</remarks>
  243. public bool MultiLine { get => _multiLine; set => _multiLine = EnableNeedsFormat (value); }
  244. /// <summary>Gets or sets whether the <see cref="TextFormatter"/> needs to format the text.</summary>
  245. /// <remarks>
  246. /// <para>If <c>false</c> when Draw is called, the Draw call will be faster.</para>
  247. /// <para>Used by <see cref="Draw(Rect, Attribute, Attribute, Rect, bool, ConsoleDriver)"/></para>
  248. /// <para>This is set to true when the properties of <see cref="TextFormatter"/> are set.</para>
  249. /// </remarks>
  250. public bool NeedsFormat { get; set; }
  251. /// <summary>
  252. /// Gets or sets whether trailing spaces at the end of word-wrapped lines are preserved or not when
  253. /// <see cref="TextFormatter.WordWrap"/> is enabled. If <see langword="true"/> trailing spaces at the end of wrapped
  254. /// lines will be removed when <see cref="Text"/> is formatted for display. The default is <see langword="false"/>.
  255. /// </summary>
  256. public bool PreserveTrailingSpaces { get => _preserveTrailingSpaces; set => _preserveTrailingSpaces = EnableNeedsFormat (value); }
  257. /// <summary>Gets or sets the size <see cref="Text"/> will be constrained to when formatted.</summary>
  258. /// <remarks>
  259. /// Does not return the size of the formatted text but the size that will be used to constrain the text when
  260. /// formatted.
  261. /// </remarks>
  262. public Size Size
  263. {
  264. get => _size;
  265. set
  266. {
  267. if (AutoSize && Alignment != TextAlignment.Justified && VerticalAlignment != VerticalTextAlignment.Justified)
  268. {
  269. _size = EnableNeedsFormat (CalcRect (0, 0, Text, Direction, TabWidth).Size);
  270. }
  271. else
  272. {
  273. _size = EnableNeedsFormat (value);
  274. }
  275. }
  276. }
  277. /// <summary>Gets or sets the number of columns used for a tab.</summary>
  278. public int TabWidth { get => _tabWidth; set => _tabWidth = EnableNeedsFormat (value); }
  279. /// <summary>The text to be displayed. This string is never modified.</summary>
  280. public virtual string Text
  281. {
  282. get => _text;
  283. set
  284. {
  285. bool textWasNull = _text == null && value != null;
  286. _text = EnableNeedsFormat (value);
  287. if ((AutoSize && Alignment != TextAlignment.Justified && VerticalAlignment != VerticalTextAlignment.Justified) || (textWasNull && Size.IsEmpty))
  288. {
  289. Size = CalcRect (0, 0, _text, Direction, TabWidth).Size;
  290. }
  291. //if (_text != null && _text.GetRuneCount () > 0 && (Size.Width == 0 || Size.Height == 0 || Size.Width != _text.GetColumns ())) {
  292. // // Provide a default size (width = length of longest line, height = 1)
  293. // // TODO: It might makes more sense for the default to be width = length of first line?
  294. // Size = new Size (TextFormatter.MaxWidth (Text, int.MaxValue), 1);
  295. //}
  296. }
  297. }
  298. /// <summary>Controls the vertical text-alignment property.</summary>
  299. /// <value>The text vertical alignment.</value>
  300. public VerticalTextAlignment VerticalAlignment { get => _textVerticalAlignment; set => _textVerticalAlignment = EnableNeedsFormat (value); }
  301. /// <summary>Gets or sets whether word wrap will be used to fit <see cref="Text"/> to <see cref="Size"/>.</summary>
  302. public bool WordWrap { get => _wordWrap; set => _wordWrap = EnableNeedsFormat (value); }
  303. /// <summary>Draws the text held by <see cref="TextFormatter"/> to <see cref="ConsoleDriver"/> using the colors specified.</summary>
  304. /// <param name="bounds">Specifies the screen-relative location and maximum size for drawing the text.</param>
  305. /// <param name="normalColor">The color to use for all text except the hotkey</param>
  306. /// <param name="hotColor">The color to use to draw the hotkey</param>
  307. /// <param name="containerBounds">Specifies the screen-relative location and maximum container size.</param>
  308. /// <param name="fillRemaining">Determines if the bounds width will be used (default) or only the text width will be used.</param>
  309. /// <param name="driver">The console driver currently used by the application.</param>
  310. public void Draw (
  311. Rect bounds,
  312. Attribute normalColor,
  313. Attribute hotColor,
  314. Rect containerBounds = default,
  315. bool fillRemaining = true,
  316. ConsoleDriver driver = null
  317. )
  318. {
  319. // With this check, we protect against subclasses with overrides of Text (like Button)
  320. if (string.IsNullOrEmpty (_text))
  321. {
  322. return;
  323. }
  324. if (driver == null)
  325. {
  326. driver = Application.Driver;
  327. }
  328. driver?.SetAttribute (normalColor);
  329. // Use "Lines" to ensure a Format (don't use "lines"))
  330. List<string> linesFormated = Lines;
  331. switch (Direction)
  332. {
  333. case TextDirection.TopBottom_RightLeft:
  334. case TextDirection.LeftRight_BottomTop:
  335. case TextDirection.RightLeft_BottomTop:
  336. case TextDirection.BottomTop_RightLeft:
  337. linesFormated.Reverse ();
  338. break;
  339. }
  340. bool isVertical = IsVerticalDirection (Direction);
  341. Rect maxBounds = bounds;
  342. if (driver != null)
  343. {
  344. maxBounds = containerBounds == default (Rect)
  345. ? bounds
  346. : new Rect (
  347. Math.Max (containerBounds.X, bounds.X),
  348. Math.Max (containerBounds.Y, bounds.Y),
  349. Math.Max (Math.Min (containerBounds.Width, containerBounds.Right - bounds.Left), 0),
  350. Math.Max (Math.Min (containerBounds.Height, containerBounds.Bottom - bounds.Top), 0));
  351. }
  352. if ((maxBounds.Width == 0) || (maxBounds.Height == 0))
  353. {
  354. return;
  355. }
  356. // BUGBUG: v2 - TextFormatter should not change the clip region. If a caller wants to break out of the clip region it should do
  357. // so explicitly.
  358. //var savedClip = Application.Driver?.Clip;
  359. //if (Application.Driver != null) {
  360. // Application.Driver.Clip = maxBounds;
  361. //}
  362. int lineOffset = !isVertical && bounds.Y < 0 ? Math.Abs (bounds.Y) : 0;
  363. for (int line = lineOffset; line < linesFormated.Count; line++)
  364. {
  365. if ((isVertical && line > bounds.Width) || (!isVertical && line > bounds.Height))
  366. {
  367. continue;
  368. }
  369. if ((isVertical && line >= maxBounds.Left + maxBounds.Width)
  370. || (!isVertical && line >= maxBounds.Top + maxBounds.Height + lineOffset))
  371. {
  372. break;
  373. }
  374. Rune [] runes = _lines [line].ToRunes ();
  375. switch (Direction)
  376. {
  377. case TextDirection.RightLeft_BottomTop:
  378. case TextDirection.RightLeft_TopBottom:
  379. case TextDirection.BottomTop_LeftRight:
  380. case TextDirection.BottomTop_RightLeft:
  381. runes = runes.Reverse ().ToArray ();
  382. break;
  383. }
  384. // When text is justified, we lost left or right, so we use the direction to align.
  385. int x, y;
  386. // Horizontal Alignment
  387. if ((_textAlignment == TextAlignment.Right) || (_textAlignment == TextAlignment.Justified && !IsLeftToRight (Direction)))
  388. {
  389. if (isVertical)
  390. {
  391. int runesWidth = GetSumMaxCharWidth (Lines, line, TabWidth);
  392. x = bounds.Right - runesWidth;
  393. CursorPosition = bounds.Width - runesWidth + (_hotKeyPos > -1 ? _hotKeyPos : 0);
  394. }
  395. else
  396. {
  397. int runesWidth = StringExtensions.ToString (runes).GetColumns ();
  398. x = bounds.Right - runesWidth;
  399. CursorPosition = bounds.Width - runesWidth + (_hotKeyPos > -1 ? _hotKeyPos : 0);
  400. }
  401. }
  402. else if ((_textAlignment == TextAlignment.Left) || (_textAlignment == TextAlignment.Justified))
  403. {
  404. if (isVertical)
  405. {
  406. int runesWidth = line > 0 ? GetSumMaxCharWidth (Lines, 0, line, TabWidth) : 0;
  407. x = bounds.Left + runesWidth;
  408. }
  409. else
  410. {
  411. x = bounds.Left;
  412. }
  413. CursorPosition = _hotKeyPos > -1 ? _hotKeyPos : 0;
  414. }
  415. else if (_textAlignment == TextAlignment.Centered)
  416. {
  417. if (isVertical)
  418. {
  419. int runesWidth = GetSumMaxCharWidth (Lines, line, TabWidth);
  420. x = bounds.Left + line + (bounds.Width - runesWidth) / 2;
  421. CursorPosition = (bounds.Width - runesWidth) / 2 + (_hotKeyPos > -1 ? _hotKeyPos : 0);
  422. }
  423. else
  424. {
  425. int runesWidth = StringExtensions.ToString (runes).GetColumns ();
  426. x = bounds.Left + (bounds.Width - runesWidth) / 2;
  427. CursorPosition = (bounds.Width - runesWidth) / 2 + (_hotKeyPos > -1 ? _hotKeyPos : 0);
  428. }
  429. }
  430. else
  431. {
  432. throw new ArgumentOutOfRangeException ();
  433. }
  434. // Vertical Alignment
  435. if ((_textVerticalAlignment == VerticalTextAlignment.Bottom)
  436. || (_textVerticalAlignment == VerticalTextAlignment.Justified && !IsTopToBottom (Direction)))
  437. {
  438. if (isVertical)
  439. {
  440. y = bounds.Bottom - runes.Length;
  441. }
  442. else
  443. {
  444. y = bounds.Bottom - Lines.Count + line;
  445. }
  446. }
  447. else if ((_textVerticalAlignment == VerticalTextAlignment.Top) || (_textVerticalAlignment == VerticalTextAlignment.Justified))
  448. {
  449. if (isVertical)
  450. {
  451. y = bounds.Top;
  452. }
  453. else
  454. {
  455. y = bounds.Top + line;
  456. }
  457. }
  458. else if (_textVerticalAlignment == VerticalTextAlignment.Middle)
  459. {
  460. if (isVertical)
  461. {
  462. int s = (bounds.Height - runes.Length) / 2;
  463. y = bounds.Top + s;
  464. }
  465. else
  466. {
  467. int s = (bounds.Height - Lines.Count) / 2;
  468. y = bounds.Top + line + s;
  469. }
  470. }
  471. else
  472. {
  473. throw new ArgumentOutOfRangeException ();
  474. }
  475. int colOffset = bounds.X < 0 ? Math.Abs (bounds.X) : 0;
  476. int start = isVertical ? bounds.Top : bounds.Left;
  477. int size = isVertical ? bounds.Height : bounds.Width;
  478. int current = start + colOffset;
  479. List<Point?> lastZeroWidthPos = null;
  480. Rune rune = default;
  481. Rune lastRuneUsed;
  482. int zeroLengthCount = isVertical ? runes.Sum (r => r.GetColumns () == 0 ? 1 : 0) : 0;
  483. for (int idx = (isVertical ? start - y : start - x) + colOffset; current < start + size + zeroLengthCount; idx++)
  484. {
  485. lastRuneUsed = rune;
  486. if (lastZeroWidthPos == null)
  487. {
  488. if ((idx < 0) || (x + current + colOffset < 0))
  489. {
  490. current++;
  491. continue;
  492. }
  493. if (!fillRemaining && idx > runes.Length - 1)
  494. {
  495. break;
  496. }
  497. if ((!isVertical && current - start > maxBounds.Left + maxBounds.Width - bounds.X + colOffset)
  498. || (isVertical && idx > maxBounds.Top + maxBounds.Height - bounds.Y))
  499. {
  500. break;
  501. }
  502. }
  503. //if ((!isVertical && idx > maxBounds.Left + maxBounds.Width - bounds.X + colOffset)
  504. // || (isVertical && idx > maxBounds.Top + maxBounds.Height - bounds.Y))
  505. // break;
  506. rune = (Rune)' ';
  507. if (isVertical)
  508. {
  509. if (idx >= 0 && idx < runes.Length)
  510. {
  511. rune = runes [idx];
  512. }
  513. if (lastZeroWidthPos == null)
  514. {
  515. driver?.Move (x, current);
  516. }
  517. else
  518. {
  519. int foundIdx = lastZeroWidthPos.IndexOf (p => p.Value.Y == current);
  520. if (foundIdx > -1)
  521. {
  522. if (rune.IsCombiningMark ())
  523. {
  524. lastZeroWidthPos [foundIdx] = new Point (lastZeroWidthPos [foundIdx].Value.X + 1, current);
  525. driver?.Move (lastZeroWidthPos [foundIdx].Value.X, current);
  526. }
  527. else if (!rune.IsCombiningMark () && lastRuneUsed.IsCombiningMark ())
  528. {
  529. current++;
  530. driver?.Move (x, current);
  531. }
  532. else
  533. {
  534. driver?.Move (x, current);
  535. }
  536. }
  537. else
  538. {
  539. driver?.Move (x, current);
  540. }
  541. }
  542. }
  543. else
  544. {
  545. driver?.Move (current, y);
  546. if (idx >= 0 && idx < runes.Length)
  547. {
  548. rune = runes [idx];
  549. }
  550. }
  551. int runeWidth = GetRuneWidth (rune, TabWidth);
  552. if (HotKeyPos > -1 && idx == HotKeyPos)
  553. {
  554. if ((isVertical && _textVerticalAlignment == VerticalTextAlignment.Justified) || (!isVertical && _textAlignment == TextAlignment.Justified))
  555. {
  556. CursorPosition = idx - start;
  557. }
  558. driver?.SetAttribute (hotColor);
  559. driver?.AddRune (rune);
  560. driver?.SetAttribute (normalColor);
  561. }
  562. else
  563. {
  564. if (isVertical)
  565. {
  566. if (runeWidth == 0)
  567. {
  568. if (lastZeroWidthPos == null)
  569. {
  570. lastZeroWidthPos = new List<Point?> ();
  571. }
  572. int foundIdx = lastZeroWidthPos.IndexOf (p => p.Value.Y == current);
  573. if (foundIdx == -1)
  574. {
  575. current--;
  576. lastZeroWidthPos.Add (new Point (x + 1, current));
  577. }
  578. driver?.Move (x + 1, current);
  579. }
  580. }
  581. driver?.AddRune (rune);
  582. }
  583. if (isVertical)
  584. {
  585. if (runeWidth > 0)
  586. {
  587. current++;
  588. }
  589. }
  590. else
  591. {
  592. current += runeWidth;
  593. }
  594. int nextRuneWidth = idx + 1 > -1 && idx + 1 < runes.Length ? runes [idx + 1].GetColumns () : 0;
  595. if (!isVertical && idx + 1 < runes.Length && current + nextRuneWidth > start + size)
  596. {
  597. break;
  598. }
  599. }
  600. }
  601. //if (Application.Driver != null) {
  602. // Application.Driver.Clip = (Rect)savedClip;
  603. //}
  604. }
  605. /// <summary>Causes the <see cref="TextFormatter"/> to reformat the text.</summary>
  606. /// <returns>The formatted text.</returns>
  607. public string Format ()
  608. {
  609. var sb = new StringBuilder ();
  610. // Lines_get causes a Format
  611. foreach (string line in Lines)
  612. {
  613. sb.AppendLine (line);
  614. }
  615. return sb.ToString ().TrimEnd (Environment.NewLine.ToCharArray ());
  616. }
  617. /// <summary>Gets the size required to hold the formatted text, given the constraints placed by <see cref="Size"/>.</summary>
  618. /// <remarks>Causes a format, resetting <see cref="NeedsFormat"/>.</remarks>
  619. /// <returns></returns>
  620. public Size GetFormattedSize ()
  621. {
  622. List<string> lines = Lines;
  623. if (Lines.Count > 0)
  624. {
  625. int width = Lines.Max (line => line.GetColumns ());
  626. int height = Lines.Count;
  627. return new Size (width, height);
  628. }
  629. return Size.Empty;
  630. }
  631. /// <summary>Event invoked when the <see cref="HotKey"/> is changed.</summary>
  632. public event EventHandler<KeyChangedEventArgs> HotKeyChanged;
  633. /// <summary>Check if it is a horizontal direction</summary>
  634. public static bool IsHorizontalDirection (TextDirection textDirection)
  635. {
  636. switch (textDirection)
  637. {
  638. case TextDirection.LeftRight_TopBottom:
  639. case TextDirection.LeftRight_BottomTop:
  640. case TextDirection.RightLeft_TopBottom:
  641. case TextDirection.RightLeft_BottomTop:
  642. return true;
  643. default:
  644. return false;
  645. }
  646. }
  647. /// <summary>Check if it is Left to Right direction</summary>
  648. public static bool IsLeftToRight (TextDirection textDirection)
  649. {
  650. switch (textDirection)
  651. {
  652. case TextDirection.LeftRight_TopBottom:
  653. case TextDirection.LeftRight_BottomTop:
  654. return true;
  655. default:
  656. return false;
  657. }
  658. }
  659. /// <summary>Check if it is Top to Bottom direction</summary>
  660. public static bool IsTopToBottom (TextDirection textDirection)
  661. {
  662. switch (textDirection)
  663. {
  664. case TextDirection.TopBottom_LeftRight:
  665. case TextDirection.TopBottom_RightLeft:
  666. return true;
  667. default:
  668. return false;
  669. }
  670. }
  671. /// <summary>Check if it is a vertical direction</summary>
  672. public static bool IsVerticalDirection (TextDirection textDirection)
  673. {
  674. switch (textDirection)
  675. {
  676. case TextDirection.TopBottom_LeftRight:
  677. case TextDirection.TopBottom_RightLeft:
  678. case TextDirection.BottomTop_LeftRight:
  679. case TextDirection.BottomTop_RightLeft:
  680. return true;
  681. default:
  682. return false;
  683. }
  684. }
  685. private T EnableNeedsFormat<T> (T value)
  686. {
  687. NeedsFormat = true;
  688. return value;
  689. }
  690. #region Static Members
  691. private static string StripCRLF (string str, bool keepNewLine = false)
  692. {
  693. List<Rune> runes = str.ToRuneList ();
  694. for (var i = 0; i < runes.Count; i++)
  695. {
  696. switch ((char)runes [i].Value)
  697. {
  698. case '\n':
  699. if (!keepNewLine)
  700. {
  701. runes.RemoveAt (i);
  702. }
  703. break;
  704. case '\r':
  705. if (i + 1 < runes.Count && runes [i + 1].Value == '\n')
  706. {
  707. runes.RemoveAt (i);
  708. if (!keepNewLine)
  709. {
  710. runes.RemoveAt (i);
  711. }
  712. i++;
  713. }
  714. else
  715. {
  716. if (!keepNewLine)
  717. {
  718. runes.RemoveAt (i);
  719. }
  720. }
  721. break;
  722. }
  723. }
  724. return StringExtensions.ToString (runes);
  725. }
  726. private static string ReplaceCRLFWithSpace (string str)
  727. {
  728. List<Rune> runes = str.ToRuneList ();
  729. for (var i = 0; i < runes.Count; i++)
  730. {
  731. switch (runes [i].Value)
  732. {
  733. case '\n':
  734. runes [i] = (Rune)' ';
  735. break;
  736. case '\r':
  737. if (i + 1 < runes.Count && runes [i + 1].Value == '\n')
  738. {
  739. runes [i] = (Rune)' ';
  740. runes.RemoveAt (i + 1);
  741. i++;
  742. }
  743. else
  744. {
  745. runes [i] = (Rune)' ';
  746. }
  747. break;
  748. }
  749. }
  750. return StringExtensions.ToString (runes);
  751. }
  752. private static string ReplaceTABWithSpaces (string str, int tabWidth)
  753. {
  754. if (tabWidth == 0)
  755. {
  756. return str.Replace ("\t", "");
  757. }
  758. return str.Replace ("\t", new string (' ', tabWidth));
  759. }
  760. /// <summary>
  761. /// Splits all newlines in the <paramref name="text"/> into a list and supports both CRLF and LF, preserving the ending
  762. /// newline.
  763. /// </summary>
  764. /// <param name="text">The text.</param>
  765. /// <returns>A list of text without the newline characters.</returns>
  766. public static List<string> SplitNewLine (string text)
  767. {
  768. List<Rune> runes = text.ToRuneList ();
  769. List<string> lines = new ();
  770. var start = 0;
  771. var end = 0;
  772. for (var i = 0; i < runes.Count; i++)
  773. {
  774. end = i;
  775. switch (runes [i].Value)
  776. {
  777. case '\n':
  778. lines.Add (StringExtensions.ToString (runes.GetRange (start, end - start)));
  779. i++;
  780. start = i;
  781. break;
  782. case '\r':
  783. if (i + 1 < runes.Count && runes [i + 1].Value == '\n')
  784. {
  785. lines.Add (StringExtensions.ToString (runes.GetRange (start, end - start)));
  786. i += 2;
  787. start = i;
  788. }
  789. else
  790. {
  791. lines.Add (StringExtensions.ToString (runes.GetRange (start, end - start)));
  792. i++;
  793. start = i;
  794. }
  795. break;
  796. }
  797. }
  798. if (runes.Count > 0 && lines.Count == 0)
  799. {
  800. lines.Add (StringExtensions.ToString (runes));
  801. }
  802. else if (runes.Count > 0 && start < runes.Count)
  803. {
  804. lines.Add (StringExtensions.ToString (runes.GetRange (start, runes.Count - start)));
  805. }
  806. else
  807. {
  808. lines.Add ("");
  809. }
  810. return lines;
  811. }
  812. /// <summary>
  813. /// Adds trailing whitespace or truncates <paramref name="text"/> so that it fits exactly <paramref name="width"/>
  814. /// console units. Note that some unicode characters take 2+ columns
  815. /// </summary>
  816. /// <param name="text"></param>
  817. /// <param name="width"></param>
  818. /// <returns></returns>
  819. public static string ClipOrPad (string text, int width)
  820. {
  821. if (string.IsNullOrEmpty (text))
  822. {
  823. return text;
  824. }
  825. // if value is not wide enough
  826. if (text.EnumerateRunes ().Sum (c => c.GetColumns ()) < width)
  827. {
  828. // pad it out with spaces to the given alignment
  829. int toPad = width - text.EnumerateRunes ().Sum (c => c.GetColumns ());
  830. return text + new string (' ', toPad);
  831. }
  832. // value is too wide
  833. return new string (text.TakeWhile (c => (width -= ((Rune)c).GetColumns ()) >= 0).ToArray ());
  834. }
  835. /// <summary>Formats the provided text to fit within the width provided using word wrapping.</summary>
  836. /// <param name="text">The text to word wrap</param>
  837. /// <param name="width">The number of columns to constrain the text to</param>
  838. /// <param name="preserveTrailingSpaces">
  839. /// If <see langword="true"/> trailing spaces at the end of wrapped lines will be preserved. If <see langword="false"/>
  840. /// , trailing spaces at the end of wrapped lines will be trimmed.
  841. /// </param>
  842. /// <param name="tabWidth">The number of columns used for a tab.</param>
  843. /// <param name="textDirection">The text direction.</param>
  844. /// <returns>A list of word wrapped lines.</returns>
  845. /// <remarks>
  846. /// <para>This method does not do any justification.</para>
  847. /// <para>This method strips Newline ('\n' and '\r\n') sequences before processing.</para>
  848. /// <para>
  849. /// If <paramref name="preserveTrailingSpaces"/> is <see langword="false"/> at most one space will be preserved at
  850. /// the end of the last line.
  851. /// </para>
  852. /// </remarks>
  853. public static List<string> WordWrapText (
  854. string text,
  855. int width,
  856. bool preserveTrailingSpaces = false,
  857. int tabWidth = 0,
  858. TextDirection textDirection = TextDirection.LeftRight_TopBottom
  859. )
  860. {
  861. if (width < 0)
  862. {
  863. throw new ArgumentOutOfRangeException ("Width cannot be negative.");
  864. }
  865. int start = 0, end;
  866. List<string> lines = new ();
  867. if (string.IsNullOrEmpty (text))
  868. {
  869. return lines;
  870. }
  871. List<Rune> runes = StripCRLF (text).ToRuneList ();
  872. if (preserveTrailingSpaces)
  873. {
  874. while ((end = start) < runes.Count)
  875. {
  876. end = GetNextWhiteSpace (start, width, out bool incomplete);
  877. if (end == 0 && incomplete)
  878. {
  879. start = text.GetRuneCount ();
  880. break;
  881. }
  882. lines.Add (StringExtensions.ToString (runes.GetRange (start, end - start)));
  883. start = end;
  884. if (incomplete)
  885. {
  886. start = text.GetRuneCount ();
  887. break;
  888. }
  889. }
  890. }
  891. else
  892. {
  893. if (IsHorizontalDirection (textDirection))
  894. {
  895. //if (GetLengthThatFits (runes.GetRange (start, runes.Count - start), width) > 0) {
  896. // // while there's still runes left and end is not past end...
  897. // while (start < runes.Count &&
  898. // (end = start + Math.Max (GetLengthThatFits (runes.GetRange (start, runes.Count - start), width) - 1, 0)) < runes.Count) {
  899. // // end now points to start + LengthThatFits
  900. // // Walk back over trailing spaces
  901. // while (runes [end] == ' ' && end > start) {
  902. // end--;
  903. // }
  904. // // end now points to start + LengthThatFits - any trailing spaces; start saving new line
  905. // var line = runes.GetRange (start, end - start + 1);
  906. // if (end == start && width > 1) {
  907. // // it was all trailing spaces; now walk forward to next non-space
  908. // do {
  909. // start++;
  910. // } while (start < runes.Count && runes [start] == ' ');
  911. // // start now points to first non-space we haven't seen yet or we're done
  912. // if (start < runes.Count) {
  913. // // we're not done. we have remaining = width - line.Count columns left;
  914. // var remaining = width - line.Count;
  915. // if (remaining > 1) {
  916. // // add a space for all the spaces we walked over
  917. // line.Add (' ');
  918. // }
  919. // var count = GetLengthThatFits (runes.GetRange (start, runes.Count - start), width - line.Count);
  920. // // [start..count] now has rest of line
  921. // line.AddRange (runes.GetRange (start, count));
  922. // start += count;
  923. // }
  924. // } else {
  925. // start += line.Count;
  926. // }
  927. // //// if the previous line was just a ' ' and the new line is just a ' '
  928. // //// don't add new line
  929. // //if (line [0] == ' ' && (lines.Count > 0 && lines [lines.Count - 1] [0] == ' ')) {
  930. // //} else {
  931. // //}
  932. // lines.Add (string.Make (line));
  933. // // move forward to next non-space
  934. // while (width > 1 && start < runes.Count && runes [start] == ' ') {
  935. // start++;
  936. // }
  937. // }
  938. //}
  939. while ((end = start + GetLengthThatFits (runes.GetRange (start, runes.Count - start), width, tabWidth)) < runes.Count)
  940. {
  941. while (runes [end].Value != ' ' && end > start)
  942. {
  943. end--;
  944. }
  945. if (end == start)
  946. {
  947. end = start + GetLengthThatFits (runes.GetRange (end, runes.Count - end), width, tabWidth);
  948. }
  949. var str = StringExtensions.ToString (runes.GetRange (start, end - start));
  950. if (end > start && GetRuneWidth (str, tabWidth) <= width)
  951. {
  952. lines.Add (str);
  953. start = end;
  954. if (runes [end].Value == ' ')
  955. {
  956. start++;
  957. }
  958. }
  959. else
  960. {
  961. end++;
  962. start = end;
  963. }
  964. }
  965. }
  966. else
  967. {
  968. while ((end = start + width) < runes.Count)
  969. {
  970. while (runes [end].Value != ' ' && end > start)
  971. {
  972. end--;
  973. }
  974. if (end == start)
  975. {
  976. end = start + width;
  977. }
  978. var zeroLength = 0;
  979. for (int i = end; i < runes.Count - start; i++)
  980. {
  981. Rune r = runes [i];
  982. if (r.GetColumns () == 0)
  983. {
  984. zeroLength++;
  985. }
  986. else
  987. {
  988. break;
  989. }
  990. }
  991. lines.Add (StringExtensions.ToString (runes.GetRange (start, end - start + zeroLength)));
  992. end += zeroLength;
  993. start = end;
  994. if (runes [end].Value == ' ')
  995. {
  996. start++;
  997. }
  998. }
  999. }
  1000. }
  1001. int GetNextWhiteSpace (int from, int cWidth, out bool incomplete, int cLength = 0)
  1002. {
  1003. int lastFrom = from;
  1004. int to = from;
  1005. int length = cLength;
  1006. incomplete = false;
  1007. while (length < cWidth && to < runes.Count)
  1008. {
  1009. Rune rune = runes [to];
  1010. if (IsHorizontalDirection (textDirection))
  1011. {
  1012. length += rune.GetColumns ();
  1013. }
  1014. else
  1015. {
  1016. length++;
  1017. }
  1018. if (length > cWidth)
  1019. {
  1020. if ((to >= runes.Count) || (length > 1 && cWidth <= 1))
  1021. {
  1022. incomplete = true;
  1023. }
  1024. return to;
  1025. }
  1026. if (rune.Value == ' ')
  1027. {
  1028. if (length == cWidth)
  1029. {
  1030. return to + 1;
  1031. }
  1032. if (length > cWidth)
  1033. {
  1034. return to;
  1035. }
  1036. return GetNextWhiteSpace (to + 1, cWidth, out incomplete, length);
  1037. }
  1038. if (rune.Value == '\t')
  1039. {
  1040. length += tabWidth + 1;
  1041. if (length == tabWidth && tabWidth > cWidth)
  1042. {
  1043. return to + 1;
  1044. }
  1045. if (length > cWidth && tabWidth > cWidth)
  1046. {
  1047. return to;
  1048. }
  1049. return GetNextWhiteSpace (to + 1, cWidth, out incomplete, length);
  1050. }
  1051. to++;
  1052. }
  1053. if (cLength > 0 && to < runes.Count && runes [to].Value != ' ' && runes [to].Value != '\t')
  1054. {
  1055. return from;
  1056. }
  1057. if (cLength > 0 && to < runes.Count && ((runes [to].Value == ' ') || (runes [to].Value == '\t')))
  1058. {
  1059. return lastFrom;
  1060. }
  1061. return to;
  1062. }
  1063. if (start < text.GetRuneCount ())
  1064. {
  1065. string str = ReplaceTABWithSpaces (StringExtensions.ToString (runes.GetRange (start, runes.Count - start)), tabWidth);
  1066. if (IsVerticalDirection (textDirection) || preserveTrailingSpaces || (!preserveTrailingSpaces && str.GetColumns () <= width))
  1067. {
  1068. lines.Add (str);
  1069. }
  1070. }
  1071. return lines;
  1072. }
  1073. /// <summary>Justifies text within a specified width.</summary>
  1074. /// <param name="text">The text to justify.</param>
  1075. /// <param name="width">
  1076. /// The number of columns to clip the text to. Text longer than <paramref name="width"/> will be
  1077. /// clipped.
  1078. /// </param>
  1079. /// <param name="talign">Alignment.</param>
  1080. /// <param name="textDirection">The text direction.</param>
  1081. /// <param name="tabWidth">The number of columns used for a tab.</param>
  1082. /// <returns>Justified and clipped text.</returns>
  1083. public static string ClipAndJustify (
  1084. string text,
  1085. int width,
  1086. TextAlignment talign,
  1087. TextDirection textDirection = TextDirection.LeftRight_TopBottom,
  1088. int tabWidth = 0
  1089. )
  1090. {
  1091. return ClipAndJustify (text, width, talign == TextAlignment.Justified, textDirection, tabWidth);
  1092. }
  1093. /// <summary>Justifies text within a specified width.</summary>
  1094. /// <param name="text">The text to justify.</param>
  1095. /// <param name="width">
  1096. /// The number of columns to clip the text to. Text longer than <paramref name="width"/> will be
  1097. /// clipped.
  1098. /// </param>
  1099. /// <param name="justify">Justify.</param>
  1100. /// <param name="textDirection">The text direction.</param>
  1101. /// <param name="tabWidth">The number of columns used for a tab.</param>
  1102. /// <returns>Justified and clipped text.</returns>
  1103. public static string ClipAndJustify (
  1104. string text,
  1105. int width,
  1106. bool justify,
  1107. TextDirection textDirection = TextDirection.LeftRight_TopBottom,
  1108. int tabWidth = 0
  1109. )
  1110. {
  1111. if (width < 0)
  1112. {
  1113. throw new ArgumentOutOfRangeException ("Width cannot be negative.");
  1114. }
  1115. if (string.IsNullOrEmpty (text))
  1116. {
  1117. return text;
  1118. }
  1119. text = ReplaceTABWithSpaces (text, tabWidth);
  1120. List<Rune> runes = text.ToRuneList ();
  1121. int slen = runes.Count;
  1122. if (slen > width)
  1123. {
  1124. if (IsHorizontalDirection (textDirection))
  1125. {
  1126. return StringExtensions.ToString (runes.GetRange (0, GetLengthThatFits (text, width, tabWidth)));
  1127. }
  1128. int zeroLength = runes.Sum (r => r.GetColumns () == 0 ? 1 : 0);
  1129. return StringExtensions.ToString (runes.GetRange (0, width + zeroLength));
  1130. }
  1131. if (justify)
  1132. {
  1133. return Justify (text, width, ' ', textDirection, tabWidth);
  1134. }
  1135. if (IsHorizontalDirection (textDirection) && GetRuneWidth (text, tabWidth) > width)
  1136. {
  1137. return StringExtensions.ToString (runes.GetRange (0, GetLengthThatFits (text, width, tabWidth)));
  1138. }
  1139. return text;
  1140. }
  1141. /// <summary>
  1142. /// Justifies the text to fill the width provided. Space will be added between words (demarked by spaces and tabs) to
  1143. /// make the text just fit <c>width</c>. Spaces will not be added to the ends.
  1144. /// </summary>
  1145. /// <param name="text"></param>
  1146. /// <param name="width"></param>
  1147. /// <param name="spaceChar">Character to replace whitespace and pad with. For debugging purposes.</param>
  1148. /// <param name="textDirection">The text direction.</param>
  1149. /// <param name="tabWidth">The number of columns used for a tab.</param>
  1150. /// <returns>The justified text.</returns>
  1151. public static string Justify (
  1152. string text,
  1153. int width,
  1154. char spaceChar = ' ',
  1155. TextDirection textDirection = TextDirection.LeftRight_TopBottom,
  1156. int tabWidth = 0
  1157. )
  1158. {
  1159. if (width < 0)
  1160. {
  1161. throw new ArgumentOutOfRangeException ("Width cannot be negative.");
  1162. }
  1163. if (string.IsNullOrEmpty (text))
  1164. {
  1165. return text;
  1166. }
  1167. text = ReplaceTABWithSpaces (text, tabWidth);
  1168. string [] words = text.Split (' ');
  1169. int textCount;
  1170. if (IsHorizontalDirection (textDirection))
  1171. {
  1172. textCount = words.Sum (arg => GetRuneWidth (arg, tabWidth));
  1173. }
  1174. else
  1175. {
  1176. textCount = words.Sum (arg => arg.GetRuneCount ());
  1177. }
  1178. int spaces = words.Length > 1 ? (width - textCount) / (words.Length - 1) : 0;
  1179. int extras = words.Length > 1 ? (width - textCount) % (words.Length - 1) : 0;
  1180. var s = new StringBuilder ();
  1181. for (var w = 0; w < words.Length; w++)
  1182. {
  1183. string x = words [w];
  1184. s.Append (x);
  1185. if (w + 1 < words.Length)
  1186. {
  1187. for (var i = 0; i < spaces; i++)
  1188. {
  1189. s.Append (spaceChar);
  1190. }
  1191. }
  1192. if (extras > 0)
  1193. {
  1194. for (var i = 0; i < 1; i++)
  1195. {
  1196. s.Append (spaceChar);
  1197. }
  1198. extras--;
  1199. }
  1200. if (w + 1 == words.Length - 1)
  1201. {
  1202. for (var i = 0; i < extras; i++)
  1203. {
  1204. s.Append (spaceChar);
  1205. }
  1206. }
  1207. }
  1208. return s.ToString ();
  1209. }
  1210. //static char [] whitespace = new char [] { ' ', '\t' };
  1211. /// <summary>
  1212. /// Reformats text into lines, applying text alignment and optionally wrapping text to new lines on word
  1213. /// boundaries.
  1214. /// </summary>
  1215. /// <param name="text"></param>
  1216. /// <param name="width">The number of columns to constrain the text to for word wrapping and clipping.</param>
  1217. /// <param name="talign">Specifies how the text will be aligned horizontally.</param>
  1218. /// <param name="wordWrap">
  1219. /// If <see langword="true"/>, the text will be wrapped to new lines no longer than <paramref name="width"/>. If
  1220. /// <see langword="false"/>, forces text to fit a single line. Line breaks are converted to spaces. The text will be
  1221. /// clipped to <paramref name="width"/>.
  1222. /// </param>
  1223. /// <param name="preserveTrailingSpaces">
  1224. /// If <see langword="true"/> trailing spaces at the end of wrapped lines will be preserved. If <see langword="false"/>
  1225. /// , trailing spaces at the end of wrapped lines will be trimmed.
  1226. /// </param>
  1227. /// <param name="tabWidth">The number of columns used for a tab.</param>
  1228. /// <param name="textDirection">The text direction.</param>
  1229. /// <param name="multiLine">If <see langword="true"/> new lines are allowed.</param>
  1230. /// <returns>A list of word wrapped lines.</returns>
  1231. /// <remarks>
  1232. /// <para>An empty <paramref name="text"/> string will result in one empty line.</para>
  1233. /// <para>If <paramref name="width"/> is 0, a single, empty line will be returned.</para>
  1234. /// <para>If <paramref name="width"/> is int.MaxValue, the text will be formatted to the maximum width possible.</para>
  1235. /// </remarks>
  1236. public static List<string> Format (
  1237. string text,
  1238. int width,
  1239. TextAlignment talign,
  1240. bool wordWrap,
  1241. bool preserveTrailingSpaces = false,
  1242. int tabWidth = 0,
  1243. TextDirection textDirection = TextDirection.LeftRight_TopBottom,
  1244. bool multiLine = false
  1245. )
  1246. {
  1247. return Format (text, width, talign == TextAlignment.Justified, wordWrap, preserveTrailingSpaces, tabWidth, textDirection, multiLine);
  1248. }
  1249. /// <summary>
  1250. /// Reformats text into lines, applying text alignment and optionally wrapping text to new lines on word
  1251. /// boundaries.
  1252. /// </summary>
  1253. /// <param name="text"></param>
  1254. /// <param name="width">The number of columns to constrain the text to for word wrapping and clipping.</param>
  1255. /// <param name="justify">Specifies whether the text should be justified.</param>
  1256. /// <param name="wordWrap">
  1257. /// If <see langword="true"/>, the text will be wrapped to new lines no longer than <paramref name="width"/>. If
  1258. /// <see langword="false"/>, forces text to fit a single line. Line breaks are converted to spaces. The text will be
  1259. /// clipped to <paramref name="width"/>.
  1260. /// </param>
  1261. /// <param name="preserveTrailingSpaces">
  1262. /// If <see langword="true"/> trailing spaces at the end of wrapped lines will be preserved. If <see langword="false"/>
  1263. /// , trailing spaces at the end of wrapped lines will be trimmed.
  1264. /// </param>
  1265. /// <param name="tabWidth">The number of columns used for a tab.</param>
  1266. /// <param name="textDirection">The text direction.</param>
  1267. /// <param name="multiLine">If <see langword="true"/> new lines are allowed.</param>
  1268. /// <returns>A list of word wrapped lines.</returns>
  1269. /// <remarks>
  1270. /// <para>An empty <paramref name="text"/> string will result in one empty line.</para>
  1271. /// <para>If <paramref name="width"/> is 0, a single, empty line will be returned.</para>
  1272. /// <para>If <paramref name="width"/> is int.MaxValue, the text will be formatted to the maximum width possible.</para>
  1273. /// </remarks>
  1274. public static List<string> Format (
  1275. string text,
  1276. int width,
  1277. bool justify,
  1278. bool wordWrap,
  1279. bool preserveTrailingSpaces = false,
  1280. int tabWidth = 0,
  1281. TextDirection textDirection = TextDirection.LeftRight_TopBottom,
  1282. bool multiLine = false
  1283. )
  1284. {
  1285. if (width < 0)
  1286. {
  1287. throw new ArgumentOutOfRangeException ("width cannot be negative");
  1288. }
  1289. List<string> lineResult = new ();
  1290. if (string.IsNullOrEmpty (text) || (width == 0))
  1291. {
  1292. lineResult.Add (string.Empty);
  1293. return lineResult;
  1294. }
  1295. if (!wordWrap)
  1296. {
  1297. text = ReplaceTABWithSpaces (text, tabWidth);
  1298. if (multiLine)
  1299. {
  1300. string [] lines = null;
  1301. if (text.Contains ("\r\n"))
  1302. {
  1303. lines = text.Split ("\r\n");
  1304. }
  1305. else if (text.Contains ('\n'))
  1306. {
  1307. lines = text.Split ('\n');
  1308. }
  1309. if (lines == null)
  1310. {
  1311. lines = new [] { text };
  1312. }
  1313. foreach (string line in lines)
  1314. {
  1315. lineResult.Add (ClipAndJustify (line, width, justify, textDirection, tabWidth));
  1316. }
  1317. return lineResult;
  1318. }
  1319. text = ReplaceCRLFWithSpace (text);
  1320. lineResult.Add (ClipAndJustify (text, width, justify, textDirection, tabWidth));
  1321. return lineResult;
  1322. }
  1323. List<Rune> runes = StripCRLF (text, true).ToRuneList ();
  1324. int runeCount = runes.Count;
  1325. var lp = 0;
  1326. for (var i = 0; i < runeCount; i++)
  1327. {
  1328. Rune c = runes [i];
  1329. if (c.Value == '\n')
  1330. {
  1331. List<string> wrappedLines = WordWrapText (
  1332. StringExtensions.ToString (runes.GetRange (lp, i - lp)),
  1333. width,
  1334. preserveTrailingSpaces,
  1335. tabWidth,
  1336. textDirection);
  1337. foreach (string line in wrappedLines)
  1338. {
  1339. lineResult.Add (ClipAndJustify (line, width, justify, textDirection, tabWidth));
  1340. }
  1341. if (wrappedLines.Count == 0)
  1342. {
  1343. lineResult.Add (string.Empty);
  1344. }
  1345. lp = i + 1;
  1346. }
  1347. }
  1348. foreach (string line in WordWrapText (
  1349. StringExtensions.ToString (runes.GetRange (lp, runeCount - lp)),
  1350. width,
  1351. preserveTrailingSpaces,
  1352. tabWidth,
  1353. textDirection))
  1354. {
  1355. lineResult.Add (ClipAndJustify (line, width, justify, textDirection, tabWidth));
  1356. }
  1357. return lineResult;
  1358. }
  1359. /// <summary>Computes the number of lines needed to render the specified text given the width.</summary>
  1360. /// <returns>Number of lines.</returns>
  1361. /// <param name="text">Text, may contain newlines.</param>
  1362. /// <param name="width">The minimum width for the text.</param>
  1363. public static int MaxLines (string text, int width)
  1364. {
  1365. List<string> result = Format (text, width, false, true);
  1366. return result.Count;
  1367. }
  1368. /// <summary>
  1369. /// Computes the maximum width needed to render the text (single line or multiple lines, word wrapped) given a number
  1370. /// of columns to constrain the text to.
  1371. /// </summary>
  1372. /// <returns>Width of the longest line after formatting the text constrained by <paramref name="maxColumns"/>.</returns>
  1373. /// <param name="text">Text, may contain newlines.</param>
  1374. /// <param name="maxColumns">The number of columns to constrain the text to for formatting.</param>
  1375. /// <param name="tabWidth">The number of columns used for a tab.</param>
  1376. public static int MaxWidth (string text, int maxColumns, int tabWidth = 0)
  1377. {
  1378. List<string> result = Format (text, maxColumns, false, true);
  1379. var max = 0;
  1380. result.ForEach (
  1381. s =>
  1382. {
  1383. var m = 0;
  1384. s.ToRuneList ().ForEach (r => m += GetRuneWidth (r, tabWidth));
  1385. if (m > max)
  1386. {
  1387. max = m;
  1388. }
  1389. });
  1390. return max;
  1391. }
  1392. /// <summary>
  1393. /// Returns the width of the widest line in the text, accounting for wide-glyphs (uses
  1394. /// <see cref="StringExtensions.GetColumns"/>). <paramref name="text"/> if it contains newlines.
  1395. /// </summary>
  1396. /// <param name="text">Text, may contain newlines.</param>
  1397. /// <param name="tabWidth">The number of columns used for a tab.</param>
  1398. /// <returns>The length of the longest line.</returns>
  1399. public static int MaxWidthLine (string text, int tabWidth = 0)
  1400. {
  1401. List<string> result = SplitNewLine (text);
  1402. return result.Max (x => GetRuneWidth (x, tabWidth));
  1403. }
  1404. /// <summary>
  1405. /// Gets the maximum characters width from the list based on the <paramref name="startIndex"/> and the
  1406. /// <paramref name="length"/>.
  1407. /// </summary>
  1408. /// <param name="lines">The lines.</param>
  1409. /// <param name="startIndex">The start index.</param>
  1410. /// <param name="length">The length.</param>
  1411. /// <param name="tabWidth">The number of columns used for a tab.</param>
  1412. /// <returns>The maximum characters width.</returns>
  1413. public static int GetSumMaxCharWidth (List<string> lines, int startIndex = -1, int length = -1, int tabWidth = 0)
  1414. {
  1415. var max = 0;
  1416. for (int i = startIndex == -1 ? 0 : startIndex; i < (length == -1 ? lines.Count : startIndex + length); i++)
  1417. {
  1418. string runes = lines [i];
  1419. if (runes.Length > 0)
  1420. {
  1421. max += runes.EnumerateRunes ().Max (r => GetRuneWidth (r, tabWidth));
  1422. }
  1423. }
  1424. return max;
  1425. }
  1426. /// <summary>
  1427. /// Gets the maximum characters width from the text based on the <paramref name="startIndex"/> and the
  1428. /// <paramref name="length"/>.
  1429. /// </summary>
  1430. /// <param name="text">The text.</param>
  1431. /// <param name="startIndex">The start index.</param>
  1432. /// <param name="length">The length.</param>
  1433. /// <param name="tabWidth">The number of columns used for a tab.</param>
  1434. /// <returns>The maximum characters width.</returns>
  1435. public static int GetSumMaxCharWidth (string text, int startIndex = -1, int length = -1, int tabWidth = 0)
  1436. {
  1437. var max = 0;
  1438. Rune [] runes = text.ToRunes ();
  1439. for (int i = startIndex == -1 ? 0 : startIndex; i < (length == -1 ? runes.Length : startIndex + length); i++)
  1440. {
  1441. max += GetRuneWidth (runes [i], tabWidth);
  1442. }
  1443. return max;
  1444. }
  1445. /// <summary>Gets the number of the Runes in a <see cref="string"/> that will fit in <paramref name="columns"/>.</summary>
  1446. /// <param name="text">The text.</param>
  1447. /// <param name="columns">The width.</param>
  1448. /// <param name="tabWidth">The number of columns used for a tab.</param>
  1449. /// <returns>The index of the text that fit the width.</returns>
  1450. public static int GetLengthThatFits (string text, int columns, int tabWidth = 0) { return GetLengthThatFits (text?.ToRuneList (), columns, tabWidth); }
  1451. /// <summary>Gets the number of the Runes in a list of Runes that will fit in <paramref name="columns"/>.</summary>
  1452. /// <param name="runes">The list of runes.</param>
  1453. /// <param name="columns">The width.</param>
  1454. /// <param name="tabWidth">The number of columns used for a tab.</param>
  1455. /// <returns>The index of the last Rune in <paramref name="runes"/> that fit in <paramref name="columns"/>.</returns>
  1456. public static int GetLengthThatFits (List<Rune> runes, int columns, int tabWidth = 0)
  1457. {
  1458. if ((runes == null) || (runes.Count == 0))
  1459. {
  1460. return 0;
  1461. }
  1462. var runesLength = 0;
  1463. var runeIdx = 0;
  1464. for (; runeIdx < runes.Count; runeIdx++)
  1465. {
  1466. int runeWidth = GetRuneWidth (runes [runeIdx], tabWidth);
  1467. if (runesLength + runeWidth > columns)
  1468. {
  1469. break;
  1470. }
  1471. runesLength += runeWidth;
  1472. }
  1473. return runeIdx;
  1474. }
  1475. private static int GetRuneWidth (string str, int tabWidth) { return GetRuneWidth (str.EnumerateRunes ().ToList (), tabWidth); }
  1476. private static int GetRuneWidth (List<Rune> runes, int tabWidth) { return runes.Sum (r => GetRuneWidth (r, tabWidth)); }
  1477. private static int GetRuneWidth (Rune rune, int tabWidth)
  1478. {
  1479. int runeWidth = rune.GetColumns ();
  1480. if (rune.Value == '\t')
  1481. {
  1482. return tabWidth;
  1483. }
  1484. if ((runeWidth < 0) || (runeWidth > 0))
  1485. {
  1486. return Math.Max (runeWidth, 1);
  1487. }
  1488. return runeWidth;
  1489. }
  1490. /// <summary>Gets the index position from the list based on the <paramref name="width"/>.</summary>
  1491. /// <param name="lines">The lines.</param>
  1492. /// <param name="width">The width.</param>
  1493. /// <param name="tabWidth">The number of columns used for a tab.</param>
  1494. /// <returns>The index of the list that fit the width.</returns>
  1495. public static int GetMaxColsForWidth (List<string> lines, int width, int tabWidth = 0)
  1496. {
  1497. var runesLength = 0;
  1498. var lineIdx = 0;
  1499. for (; lineIdx < lines.Count; lineIdx++)
  1500. {
  1501. List<Rune> runes = lines [lineIdx].ToRuneList ();
  1502. int maxRruneWidth = runes.Count > 0
  1503. ? runes.Max (r => GetRuneWidth (r, tabWidth))
  1504. : 1;
  1505. if (runesLength + maxRruneWidth > width)
  1506. {
  1507. break;
  1508. }
  1509. runesLength += maxRruneWidth;
  1510. }
  1511. return lineIdx;
  1512. }
  1513. /// <summary>Calculates the rectangle required to hold a formatted string of text.</summary>
  1514. /// <param name="x">The x location of the rectangle</param>
  1515. /// <param name="y">The y location of the rectangle</param>
  1516. /// <param name="text">The text to measure</param>
  1517. /// <param name="direction">The text direction.</param>
  1518. /// <param name="tabWidth">The number of columns used for a tab.</param>
  1519. /// <returns></returns>
  1520. public static Rect CalcRect (int x, int y, string text, TextDirection direction = TextDirection.LeftRight_TopBottom, int tabWidth = 0)
  1521. {
  1522. if (string.IsNullOrEmpty (text))
  1523. {
  1524. return new Rect (new Point (x, y), Size.Empty);
  1525. }
  1526. int w, h;
  1527. if (IsHorizontalDirection (direction))
  1528. {
  1529. var mw = 0;
  1530. var ml = 1;
  1531. var cols = 0;
  1532. foreach (Rune rune in text.EnumerateRunes ())
  1533. {
  1534. if (rune.Value == '\n')
  1535. {
  1536. ml++;
  1537. if (cols > mw)
  1538. {
  1539. mw = cols;
  1540. }
  1541. cols = 0;
  1542. }
  1543. else if (rune.Value != '\r')
  1544. {
  1545. cols++;
  1546. var rw = 0;
  1547. if (rune.Value == '\t')
  1548. {
  1549. rw += tabWidth - 1;
  1550. }
  1551. else
  1552. {
  1553. rw = rune.GetColumns ();
  1554. if (rw > 0)
  1555. {
  1556. rw--;
  1557. }
  1558. else if (rw == 0)
  1559. {
  1560. cols--;
  1561. }
  1562. }
  1563. cols += rw;
  1564. }
  1565. }
  1566. if (cols > mw)
  1567. {
  1568. mw = cols;
  1569. }
  1570. w = mw;
  1571. h = ml;
  1572. }
  1573. else
  1574. {
  1575. int vw = 1, cw = 1;
  1576. var vh = 0;
  1577. var rows = 0;
  1578. foreach (Rune rune in text.EnumerateRunes ())
  1579. {
  1580. if (rune.Value == '\n')
  1581. {
  1582. vw++;
  1583. if (rows > vh)
  1584. {
  1585. vh = rows;
  1586. }
  1587. rows = 0;
  1588. cw = 1;
  1589. }
  1590. else if (rune.Value != '\r')
  1591. {
  1592. rows++;
  1593. var rw = 0;
  1594. if (rune.Value == '\t')
  1595. {
  1596. rw += tabWidth - 1;
  1597. rows += rw;
  1598. }
  1599. else
  1600. {
  1601. rw = rune.GetColumns ();
  1602. if (rw == 0)
  1603. {
  1604. rows--;
  1605. }
  1606. else if (cw < rw)
  1607. {
  1608. cw = rw;
  1609. vw++;
  1610. }
  1611. }
  1612. }
  1613. }
  1614. if (rows > vh)
  1615. {
  1616. vh = rows;
  1617. }
  1618. w = vw;
  1619. h = vh;
  1620. }
  1621. return new Rect (x, y, w, h);
  1622. }
  1623. /// <summary>Finds the HotKey and its location in text.</summary>
  1624. /// <param name="text">The text to look in.</param>
  1625. /// <param name="hotKeySpecifier">The HotKey specifier (e.g. '_') to look for.</param>
  1626. /// <param name="hotPos">Outputs the Rune index into <c>text</c>.</param>
  1627. /// <param name="hotKey">Outputs the hotKey. <see cref="Key.Empty"/> if not found.</param>
  1628. /// <param name="firstUpperCase">
  1629. /// If <c>true</c> the legacy behavior of identifying the first upper case character as the HotKey will be enabled.
  1630. /// Regardless of the value of this parameter, <c>hotKeySpecifier</c> takes precedence. Defaults to
  1631. /// <see langword="false"/>.
  1632. /// </param>
  1633. /// <returns><c>true</c> if a HotKey was found; <c>false</c> otherwise.</returns>
  1634. public static bool FindHotKey (string text, Rune hotKeySpecifier, out int hotPos, out Key hotKey, bool firstUpperCase = false)
  1635. {
  1636. if (string.IsNullOrEmpty (text) || (hotKeySpecifier == (Rune)0xFFFF))
  1637. {
  1638. hotPos = -1;
  1639. hotKey = KeyCode.Null;
  1640. return false;
  1641. }
  1642. var hot_key = (Rune)0;
  1643. int hot_pos = -1;
  1644. // Use first hot_key char passed into 'hotKey'.
  1645. // TODO: Ignore hot_key of two are provided
  1646. // TODO: Do not support non-alphanumeric chars that can't be typed
  1647. var i = 0;
  1648. foreach (Rune c in text.EnumerateRunes ())
  1649. {
  1650. if ((char)c.Value != 0xFFFD)
  1651. {
  1652. if (c == hotKeySpecifier)
  1653. {
  1654. hot_pos = i;
  1655. }
  1656. else if (hot_pos > -1)
  1657. {
  1658. hot_key = c;
  1659. break;
  1660. }
  1661. }
  1662. i++;
  1663. }
  1664. // Legacy support - use first upper case char if the specifier was not found
  1665. if (hot_pos == -1 && firstUpperCase)
  1666. {
  1667. i = 0;
  1668. foreach (Rune c in text.EnumerateRunes ())
  1669. {
  1670. if ((char)c.Value != 0xFFFD)
  1671. {
  1672. if (Rune.IsUpper (c))
  1673. {
  1674. hot_key = c;
  1675. hot_pos = i;
  1676. break;
  1677. }
  1678. }
  1679. i++;
  1680. }
  1681. }
  1682. if (hot_key != (Rune)0 && hot_pos != -1)
  1683. {
  1684. hotPos = hot_pos;
  1685. var newHotKey = (KeyCode)hot_key.Value;
  1686. if (newHotKey != KeyCode.Null && !((newHotKey == KeyCode.Space) || Rune.IsControl (hot_key)))
  1687. {
  1688. if ((newHotKey & ~KeyCode.Space) is >= KeyCode.A and <= KeyCode.Z)
  1689. {
  1690. newHotKey &= ~KeyCode.Space;
  1691. }
  1692. hotKey = newHotKey;
  1693. return true;
  1694. }
  1695. }
  1696. hotPos = -1;
  1697. hotKey = KeyCode.Null;
  1698. return false;
  1699. }
  1700. /// <summary>
  1701. /// Replaces the Rune at the index specified by the <c>hotPos</c> parameter with a tag identifying it as the
  1702. /// hotkey.
  1703. /// </summary>
  1704. /// <param name="text">The text to tag the hotkey in.</param>
  1705. /// <param name="hotPos">The Rune index of the hotkey in <c>text</c>.</param>
  1706. /// <returns>The text with the hotkey tagged.</returns>
  1707. /// <remarks>The returned string will not render correctly without first un-doing the tag. To undo the tag, search for</remarks>
  1708. public string ReplaceHotKeyWithTag (string text, int hotPos)
  1709. {
  1710. // Set the high bit
  1711. List<Rune> runes = text.ToRuneList ();
  1712. if (Rune.IsLetterOrDigit (runes [hotPos]))
  1713. {
  1714. runes [hotPos] = new Rune ((uint)runes [hotPos].Value);
  1715. }
  1716. return StringExtensions.ToString (runes);
  1717. }
  1718. /// <summary>Removes the hotkey specifier from text.</summary>
  1719. /// <param name="text">The text to manipulate.</param>
  1720. /// <param name="hotKeySpecifier">The hot-key specifier (e.g. '_') to look for.</param>
  1721. /// <param name="hotPos">Returns the position of the hot-key in the text. -1 if not found.</param>
  1722. /// <returns>The input text with the hotkey specifier ('_') removed.</returns>
  1723. public static string RemoveHotKeySpecifier (string text, int hotPos, Rune hotKeySpecifier)
  1724. {
  1725. if (string.IsNullOrEmpty (text))
  1726. {
  1727. return text;
  1728. }
  1729. // Scan
  1730. var start = string.Empty;
  1731. var i = 0;
  1732. foreach (Rune c in text)
  1733. {
  1734. if (c == hotKeySpecifier && i == hotPos)
  1735. {
  1736. i++;
  1737. continue;
  1738. }
  1739. start += c;
  1740. i++;
  1741. }
  1742. return start;
  1743. }
  1744. #endregion // Static Members
  1745. }