TabView.cs 49 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374
  1. using System.Diagnostics;
  2. namespace Terminal.Gui;
  3. /// <summary>Control that hosts multiple sub views, presenting a single one at once.</summary>
  4. public class TabView : View
  5. {
  6. /// <summary>The default <see cref="MaxTabTextWidth"/> to set on new <see cref="TabView"/> controls.</summary>
  7. public const uint DefaultMaxTabTextWidth = 30;
  8. /// <summary>
  9. /// This sub view is the main client area of the current tab. It hosts the <see cref="Tab.View"/> of the tab, the
  10. /// <see cref="SelectedTab"/>.
  11. /// </summary>
  12. private readonly View _contentView;
  13. private readonly List<Tab> _tabs = new ();
  14. /// <summary>This sub view is the 2 or 3 line control that represents the actual tabs themselves.</summary>
  15. private readonly TabRowView _tabsBar;
  16. private Tab _selectedTab;
  17. private TabToRender [] _tabLocations;
  18. private int _tabScrollOffset;
  19. /// <summary>Initializes a <see cref="TabView"/> class.</summary>
  20. public TabView ()
  21. {
  22. CanFocus = true;
  23. TabStop = TabBehavior.TabStop; // Because TabView has focusable subviews, it must be a TabGroup
  24. _tabsBar = new TabRowView (this);
  25. _contentView = new View ()
  26. {
  27. //Id = "TabView._contentView",
  28. };
  29. ApplyStyleChanges ();
  30. base.Add (_tabsBar);
  31. base.Add (_contentView);
  32. // Things this view knows how to do
  33. AddCommand (Command.Left, () => SwitchTabBy (-1));
  34. AddCommand (Command.Right, () => SwitchTabBy (1));
  35. AddCommand (
  36. Command.LeftStart,
  37. () =>
  38. {
  39. TabScrollOffset = 0;
  40. SelectedTab = Tabs.FirstOrDefault ();
  41. return true;
  42. }
  43. );
  44. AddCommand (
  45. Command.RightEnd,
  46. () =>
  47. {
  48. TabScrollOffset = Tabs.Count - 1;
  49. SelectedTab = Tabs.LastOrDefault ();
  50. return true;
  51. }
  52. );
  53. AddCommand (
  54. Command.PageDown,
  55. () =>
  56. {
  57. TabScrollOffset += _tabLocations.Length;
  58. SelectedTab = Tabs.ElementAt (TabScrollOffset);
  59. return true;
  60. }
  61. );
  62. AddCommand (
  63. Command.PageUp,
  64. () =>
  65. {
  66. TabScrollOffset -= _tabLocations.Length;
  67. SelectedTab = Tabs.ElementAt (TabScrollOffset);
  68. return true;
  69. }
  70. );
  71. // Default keybindings for this view
  72. KeyBindings.Add (Key.CursorLeft, Command.Left);
  73. KeyBindings.Add (Key.CursorRight, Command.Right);
  74. KeyBindings.Add (Key.Home, Command.LeftStart);
  75. KeyBindings.Add (Key.End, Command.RightEnd);
  76. KeyBindings.Add (Key.PageDown, Command.PageDown);
  77. KeyBindings.Add (Key.PageUp, Command.PageUp);
  78. }
  79. /// <summary>
  80. /// The maximum number of characters to render in a Tab header. This prevents one long tab from pushing out all
  81. /// the others.
  82. /// </summary>
  83. public uint MaxTabTextWidth { get; set; } = DefaultMaxTabTextWidth;
  84. /// <summary>The currently selected member of <see cref="Tabs"/> chosen by the user.</summary>
  85. /// <value></value>
  86. public Tab SelectedTab
  87. {
  88. get => _selectedTab;
  89. set
  90. {
  91. UnSetCurrentTabs ();
  92. Tab old = _selectedTab;
  93. if (_selectedTab is { })
  94. {
  95. if (_selectedTab.View is { })
  96. {
  97. // remove old content
  98. _contentView.Remove (_selectedTab.View);
  99. }
  100. }
  101. _selectedTab = value;
  102. if (value is { })
  103. {
  104. // add new content
  105. if (_selectedTab.View is { })
  106. {
  107. _contentView.Add (_selectedTab.View);
  108. // _contentView.Id = $"_contentView for {_selectedTab.DisplayText}";
  109. }
  110. }
  111. _contentView.CanFocus = _contentView.Subviews.Count (v => v.CanFocus) > 0;
  112. EnsureSelectedTabIsVisible ();
  113. if (old != value)
  114. {
  115. if (old?.HasFocus == true)
  116. {
  117. SelectedTab?.SetFocus ();
  118. }
  119. OnSelectedTabChanged (old, value);
  120. }
  121. }
  122. }
  123. /// <summary>Render choices for how to display tabs. After making changes, call <see cref="ApplyStyleChanges()"/>.</summary>
  124. /// <value></value>
  125. public TabStyle Style { get; set; } = new ();
  126. /// <summary>All tabs currently hosted by the control.</summary>
  127. /// <value></value>
  128. public IReadOnlyCollection<Tab> Tabs => _tabs.AsReadOnly ();
  129. /// <summary>When there are too many tabs to render, this indicates the first tab to render on the screen.</summary>
  130. /// <value></value>
  131. public int TabScrollOffset
  132. {
  133. get => _tabScrollOffset;
  134. set => _tabScrollOffset = EnsureValidScrollOffsets (value);
  135. }
  136. /// <summary>Adds the given <paramref name="tab"/> to <see cref="Tabs"/>.</summary>
  137. /// <param name="tab"></param>
  138. /// <param name="andSelect">True to make the newly added Tab the <see cref="SelectedTab"/>.</param>
  139. public void AddTab (Tab tab, bool andSelect)
  140. {
  141. if (_tabs.Contains (tab))
  142. {
  143. return;
  144. }
  145. _tabs.Add (tab);
  146. _tabsBar.Add (tab);
  147. if (SelectedTab is null || andSelect)
  148. {
  149. SelectedTab = tab;
  150. EnsureSelectedTabIsVisible ();
  151. tab.View?.SetFocus ();
  152. }
  153. SetNeedsDisplay ();
  154. }
  155. /// <summary>
  156. /// Updates the control to use the latest state settings in <see cref="Style"/>. This can change the size of the
  157. /// client area of the tab (for rendering the selected tab's content). This method includes a call to
  158. /// <see cref="View.SetNeedsDisplay()"/>.
  159. /// </summary>
  160. public void ApplyStyleChanges ()
  161. {
  162. _contentView.BorderStyle = Style.ShowBorder ? LineStyle.Single : LineStyle.None;
  163. _contentView.Width = Dim.Fill ();
  164. if (Style.TabsOnBottom)
  165. {
  166. // Tabs are along the bottom so just dodge the border
  167. if (Style.ShowBorder)
  168. {
  169. _contentView.Border.Thickness = new Thickness (1, 1, 1, 0);
  170. }
  171. _contentView.Y = 0;
  172. int tabHeight = GetTabHeight (false);
  173. // Fill client area leaving space at bottom for tabs
  174. _contentView.Height = Dim.Fill (tabHeight);
  175. _tabsBar.Height = tabHeight;
  176. _tabsBar.Y = Pos.Bottom (_contentView);
  177. }
  178. else
  179. {
  180. // Tabs are along the top
  181. if (Style.ShowBorder)
  182. {
  183. _contentView.Border.Thickness = new Thickness (1, 0, 1, 1);
  184. }
  185. _tabsBar.Y = 0;
  186. int tabHeight = GetTabHeight (true);
  187. //move content down to make space for tabs
  188. _contentView.Y = Pos.Bottom (_tabsBar) ;
  189. // Fill client area leaving space at bottom for border
  190. _contentView.Height = Dim.Fill ();
  191. // The top tab should be 2 or 3 rows high and on the top
  192. _tabsBar.Height = tabHeight;
  193. // Should be able to just use 0 but switching between top/bottom tabs repeatedly breaks in ValidatePosDim if just using the absolute value 0
  194. }
  195. if (IsInitialized)
  196. {
  197. LayoutSubviews ();
  198. }
  199. SetNeedsDisplay ();
  200. }
  201. /// <summary>Updates <see cref="TabScrollOffset"/> to ensure that <see cref="SelectedTab"/> is visible.</summary>
  202. public void EnsureSelectedTabIsVisible ()
  203. {
  204. if (!IsInitialized || SelectedTab is null)
  205. {
  206. return;
  207. }
  208. // if current viewport does not include the selected tab
  209. if (!CalculateViewport (Viewport).Any (r => Equals (SelectedTab, r.Tab)))
  210. {
  211. // Set scroll offset so the first tab rendered is the
  212. TabScrollOffset = Math.Max (0, Tabs.IndexOf (SelectedTab));
  213. }
  214. }
  215. /// <summary>Updates <see cref="TabScrollOffset"/> to be a valid index of <see cref="Tabs"/>.</summary>
  216. /// <param name="value">The value to validate.</param>
  217. /// <remarks>Changes will not be immediately visible in the display until you call <see cref="View.SetNeedsDisplay()"/>.</remarks>
  218. /// <returns>The valid <see cref="TabScrollOffset"/> for the given value.</returns>
  219. public int EnsureValidScrollOffsets (int value) { return Math.Max (Math.Min (value, Tabs.Count - 1), 0); }
  220. /// <inheritdoc/>
  221. public override void OnDrawContent (Rectangle viewport)
  222. {
  223. Driver.SetAttribute (GetNormalColor ());
  224. if (Tabs.Any ())
  225. {
  226. Rectangle savedClip = SetClip ();
  227. _tabsBar.OnDrawContent (viewport);
  228. _contentView.SetNeedsDisplay ();
  229. _contentView.Draw ();
  230. Driver.Clip = savedClip;
  231. }
  232. }
  233. /// <inheritdoc/>
  234. public override void OnDrawContentComplete (Rectangle viewport) { _tabsBar.OnDrawContentComplete (viewport); }
  235. /// <summary>
  236. /// Removes the given <paramref name="tab"/> from <see cref="Tabs"/>. Caller is responsible for disposing the
  237. /// tab's hosted <see cref="Tab.View"/> if appropriate.
  238. /// </summary>
  239. /// <param name="tab"></param>
  240. public void RemoveTab (Tab tab)
  241. {
  242. if (tab is null || !_tabs.Contains (tab))
  243. {
  244. return;
  245. }
  246. // what tab was selected before closing
  247. int idx = _tabs.IndexOf (tab);
  248. _tabs.Remove (tab);
  249. // if the currently selected tab is no longer a member of Tabs
  250. if (SelectedTab is null || !Tabs.Contains (SelectedTab))
  251. {
  252. // select the tab closest to the one that disappeared
  253. int toSelect = Math.Max (idx - 1, 0);
  254. if (toSelect < Tabs.Count)
  255. {
  256. SelectedTab = Tabs.ElementAt (toSelect);
  257. }
  258. else
  259. {
  260. SelectedTab = Tabs.LastOrDefault ();
  261. }
  262. }
  263. EnsureSelectedTabIsVisible ();
  264. SetNeedsDisplay ();
  265. }
  266. /// <summary>Event for when <see cref="SelectedTab"/> changes.</summary>
  267. public event EventHandler<TabChangedEventArgs> SelectedTabChanged;
  268. /// <summary>
  269. /// Changes the <see cref="SelectedTab"/> by the given <paramref name="amount"/>. Positive for right, negative for
  270. /// left. If no tab is currently selected then the first tab will become selected.
  271. /// </summary>
  272. /// <param name="amount"></param>
  273. public bool SwitchTabBy (int amount)
  274. {
  275. if (Tabs.Count == 0)
  276. {
  277. return false;
  278. }
  279. // if there is only one tab anyway or nothing is selected
  280. if (Tabs.Count == 1 || SelectedTab is null)
  281. {
  282. SelectedTab = Tabs.ElementAt (0);
  283. SetNeedsDisplay ();
  284. return SelectedTab is { };
  285. }
  286. int currentIdx = Tabs.IndexOf (SelectedTab);
  287. // Currently selected tab has vanished!
  288. if (currentIdx == -1)
  289. {
  290. SelectedTab = Tabs.ElementAt (0);
  291. SetNeedsDisplay ();
  292. return true;
  293. }
  294. int newIdx = Math.Max (0, Math.Min (currentIdx + amount, Tabs.Count - 1));
  295. if (newIdx == currentIdx)
  296. {
  297. return false;
  298. }
  299. SelectedTab = _tabs [newIdx];
  300. SetNeedsDisplay ();
  301. EnsureSelectedTabIsVisible ();
  302. return true;
  303. }
  304. /// <summary>
  305. /// Event fired when a <see cref="Tab"/> is clicked. Can be used to cancel navigation, show context menu (e.g. on
  306. /// right click) etc.
  307. /// </summary>
  308. public event EventHandler<TabMouseEventArgs> TabClicked;
  309. /// <summary>Disposes the control and all <see cref="Tabs"/>.</summary>
  310. /// <param name="disposing"></param>
  311. protected override void Dispose (bool disposing)
  312. {
  313. base.Dispose (disposing);
  314. // The selected tab will automatically be disposed but
  315. // any tabs not visible will need to be manually disposed
  316. foreach (Tab tab in Tabs)
  317. {
  318. if (!Equals (SelectedTab, tab))
  319. {
  320. tab.View?.Dispose ();
  321. }
  322. }
  323. }
  324. /// <summary>Raises the <see cref="SelectedTabChanged"/> event.</summary>
  325. protected virtual void OnSelectedTabChanged (Tab oldTab, Tab newTab)
  326. {
  327. SelectedTabChanged?.Invoke (this, new TabChangedEventArgs (oldTab, newTab));
  328. }
  329. /// <summary>Returns which tabs to render at each x location.</summary>
  330. /// <returns></returns>
  331. private IEnumerable<TabToRender> CalculateViewport (Rectangle bounds)
  332. {
  333. UnSetCurrentTabs ();
  334. var i = 1;
  335. View prevTab = null;
  336. // Starting at the first or scrolled to tab
  337. foreach (Tab tab in Tabs.Skip (TabScrollOffset))
  338. {
  339. if (prevTab is { })
  340. {
  341. tab.X = Pos.Right (prevTab);
  342. }
  343. else
  344. {
  345. tab.X = 0;
  346. }
  347. tab.Y = 0;
  348. // while there is space for the tab
  349. int tabTextWidth = tab.DisplayText.EnumerateRunes ().Sum (c => c.GetColumns ());
  350. string text = tab.DisplayText;
  351. // The maximum number of characters to use for the tab name as specified
  352. // by the user (MaxTabTextWidth). But not more than the width of the view
  353. // or we won't even be able to render a single tab!
  354. long maxWidth = Math.Max (0, Math.Min (bounds.Width - 3, MaxTabTextWidth));
  355. prevTab = tab;
  356. tab.Width = 2;
  357. tab.Height = Style.ShowTopLine ? 3 : 2;
  358. // if tab view is width <= 3 don't render any tabs
  359. if (maxWidth == 0)
  360. {
  361. tab.Visible = true;
  362. tab.MouseClick += Tab_MouseClick;
  363. yield return new TabToRender (i, tab, string.Empty, Equals (SelectedTab, tab), 0);
  364. break;
  365. }
  366. if (tabTextWidth > maxWidth)
  367. {
  368. text = tab.DisplayText.Substring (0, (int)maxWidth);
  369. tabTextWidth = (int)maxWidth;
  370. }
  371. tab.Width = Math.Max (tabTextWidth + 2, 1);
  372. tab.Height = Style.ShowTopLine ? 3 : 2;
  373. // if there is not enough space for this tab
  374. if (i + tabTextWidth >= bounds.Width)
  375. {
  376. tab.Visible = false;
  377. break;
  378. }
  379. // there is enough space!
  380. tab.Visible = true;
  381. tab.MouseClick += Tab_MouseClick;
  382. yield return new TabToRender (i, tab, text, Equals (SelectedTab, tab), tabTextWidth);
  383. i += tabTextWidth + 1;
  384. }
  385. }
  386. /// <summary>
  387. /// Returns the number of rows occupied by rendering the tabs, this depends on <see cref="TabStyle.ShowTopLine"/>
  388. /// and can be 0 (e.g. if <see cref="TabStyle.TabsOnBottom"/> and you ask for <paramref name="top"/>).
  389. /// </summary>
  390. /// <param name="top">True to measure the space required at the top of the control, false to measure space at the bottom.</param>
  391. /// .
  392. /// <returns></returns>
  393. private int GetTabHeight (bool top)
  394. {
  395. if (top && Style.TabsOnBottom)
  396. {
  397. return 0;
  398. }
  399. if (!top && !Style.TabsOnBottom)
  400. {
  401. return 0;
  402. }
  403. return Style.ShowTopLine ? 3 : 2;
  404. }
  405. private void Tab_MouseClick (object sender, MouseEventEventArgs e)
  406. {
  407. e.Handled = _tabsBar.NewMouseEvent (e.MouseEvent) == true;
  408. }
  409. private void UnSetCurrentTabs ()
  410. {
  411. if (_tabLocations is { })
  412. {
  413. foreach (TabToRender tabToRender in _tabLocations)
  414. {
  415. tabToRender.Tab.MouseClick -= Tab_MouseClick;
  416. tabToRender.Tab.Visible = false;
  417. }
  418. _tabLocations = null;
  419. }
  420. }
  421. /// <summary>Raises the <see cref="TabClicked"/> event.</summary>
  422. /// <param name="tabMouseEventArgs"></param>
  423. private protected virtual void OnTabClicked (TabMouseEventArgs tabMouseEventArgs) { TabClicked?.Invoke (this, tabMouseEventArgs); }
  424. private class TabRowView : View
  425. {
  426. private readonly TabView _host;
  427. private readonly View _leftScrollIndicator;
  428. private readonly View _rightScrollIndicator;
  429. public TabRowView (TabView host)
  430. {
  431. _host = host;
  432. Id = "tabRowView";
  433. CanFocus = true;
  434. Height = 1; // BUGBUG: Views should avoid setting Height as doing so implies Frame.Size == GetContentSize ().
  435. Width = Dim.Fill ();
  436. _rightScrollIndicator = new View
  437. {
  438. Id = "rightScrollIndicator",
  439. Width = 1,
  440. Height = 1,
  441. Visible = false,
  442. Text = Glyphs.RightArrow.ToString ()
  443. };
  444. _rightScrollIndicator.MouseClick += _host.Tab_MouseClick;
  445. _leftScrollIndicator = new View
  446. {
  447. Id = "leftScrollIndicator",
  448. Width = 1,
  449. Height = 1,
  450. Visible = false,
  451. Text = Glyphs.LeftArrow.ToString ()
  452. };
  453. _leftScrollIndicator.MouseClick += _host.Tab_MouseClick;
  454. Add (_rightScrollIndicator, _leftScrollIndicator);
  455. }
  456. protected internal override bool OnMouseEvent (MouseEvent me)
  457. {
  458. Tab hit = me.View is Tab ? (Tab)me.View : null;
  459. bool isClick = me.Flags.HasFlag (MouseFlags.Button1Clicked)
  460. || me.Flags.HasFlag (MouseFlags.Button2Clicked)
  461. || me.Flags.HasFlag (MouseFlags.Button3Clicked);
  462. if (isClick)
  463. {
  464. _host.OnTabClicked (new TabMouseEventArgs (hit, me));
  465. // user canceled click
  466. if (me.Handled)
  467. {
  468. return true;
  469. }
  470. }
  471. if (!me.Flags.HasFlag (MouseFlags.Button1Clicked)
  472. && !me.Flags.HasFlag (MouseFlags.Button1DoubleClicked)
  473. && !me.Flags.HasFlag (MouseFlags.Button1TripleClicked))
  474. {
  475. return false;
  476. }
  477. if (!HasFocus && CanFocus)
  478. {
  479. SetFocus ();
  480. }
  481. if (me.Flags.HasFlag (MouseFlags.Button1Clicked)
  482. || me.Flags.HasFlag (MouseFlags.Button1DoubleClicked)
  483. || me.Flags.HasFlag (MouseFlags.Button1TripleClicked))
  484. {
  485. var scrollIndicatorHit = 0;
  486. if (me.View is { } && me.View.Id == "rightScrollIndicator")
  487. {
  488. scrollIndicatorHit = 1;
  489. }
  490. else if (me.View is { } && me.View.Id == "leftScrollIndicator")
  491. {
  492. scrollIndicatorHit = -1;
  493. }
  494. if (scrollIndicatorHit != 0)
  495. {
  496. _host.SwitchTabBy (scrollIndicatorHit);
  497. SetNeedsDisplay ();
  498. return true;
  499. }
  500. if (hit is { })
  501. {
  502. _host.SelectedTab = hit;
  503. SetNeedsDisplay ();
  504. return true;
  505. }
  506. }
  507. return false;
  508. }
  509. public override void OnDrawContent (Rectangle viewport)
  510. {
  511. _host._tabLocations = _host.CalculateViewport (Viewport).ToArray ();
  512. // clear any old text
  513. Clear ();
  514. RenderTabLine ();
  515. RenderUnderline ();
  516. Driver.SetAttribute (HasFocus ? GetFocusColor () : GetNormalColor ());
  517. }
  518. public override void OnDrawContentComplete (Rectangle viewport)
  519. {
  520. if (_host._tabLocations is null)
  521. {
  522. return;
  523. }
  524. TabToRender [] tabLocations = _host._tabLocations;
  525. int selectedTab = -1;
  526. for (var i = 0; i < tabLocations.Length; i++)
  527. {
  528. View tab = tabLocations [i].Tab;
  529. Rectangle vts = tab.ViewportToScreen (tab.Viewport);
  530. var lc = new LineCanvas ();
  531. int selectedOffset = _host.Style.ShowTopLine && tabLocations [i].IsSelected ? 0 : 1;
  532. if (tabLocations [i].IsSelected)
  533. {
  534. selectedTab = i;
  535. if (i == 0 && _host.TabScrollOffset == 0)
  536. {
  537. if (_host.Style.TabsOnBottom)
  538. {
  539. // Upper left vertical line
  540. lc.AddLine (
  541. new Point (vts.X - 1, vts.Y - 1),
  542. -1,
  543. Orientation.Vertical,
  544. tab.BorderStyle
  545. );
  546. }
  547. else
  548. {
  549. // Lower left vertical line
  550. lc.AddLine (
  551. new Point (vts.X - 1, vts.Bottom - selectedOffset),
  552. -1,
  553. Orientation.Vertical,
  554. tab.BorderStyle
  555. );
  556. }
  557. }
  558. else if (i > 0 && i <= tabLocations.Length - 1)
  559. {
  560. if (_host.Style.TabsOnBottom)
  561. {
  562. // URCorner
  563. lc.AddLine (
  564. new Point (vts.X - 1, vts.Y - 1),
  565. 1,
  566. Orientation.Vertical,
  567. tab.BorderStyle
  568. );
  569. lc.AddLine (
  570. new Point (vts.X - 1, vts.Y - 1),
  571. -1,
  572. Orientation.Horizontal,
  573. tab.BorderStyle
  574. );
  575. }
  576. else
  577. {
  578. // LRCorner
  579. lc.AddLine (
  580. new Point (vts.X - 1, vts.Bottom - selectedOffset),
  581. -1,
  582. Orientation.Vertical,
  583. tab.BorderStyle
  584. );
  585. lc.AddLine (
  586. new Point (vts.X - 1, vts.Bottom - selectedOffset),
  587. -1,
  588. Orientation.Horizontal,
  589. tab.BorderStyle
  590. );
  591. }
  592. if (_host.Style.ShowTopLine)
  593. {
  594. if (_host.Style.TabsOnBottom)
  595. {
  596. // Lower left tee
  597. lc.AddLine (
  598. new Point (vts.X - 1, vts.Bottom),
  599. -1,
  600. Orientation.Vertical,
  601. tab.BorderStyle
  602. );
  603. lc.AddLine (
  604. new Point (vts.X - 1, vts.Bottom),
  605. 0,
  606. Orientation.Horizontal,
  607. tab.BorderStyle
  608. );
  609. }
  610. else
  611. {
  612. // Upper left tee
  613. lc.AddLine (
  614. new Point (vts.X - 1, vts.Y - 1),
  615. 1,
  616. Orientation.Vertical,
  617. tab.BorderStyle
  618. );
  619. lc.AddLine (
  620. new Point (vts.X - 1, vts.Y - 1),
  621. 0,
  622. Orientation.Horizontal,
  623. tab.BorderStyle
  624. );
  625. }
  626. }
  627. }
  628. if (i < tabLocations.Length - 1)
  629. {
  630. if (_host.Style.ShowTopLine)
  631. {
  632. if (_host.Style.TabsOnBottom)
  633. {
  634. // Lower right tee
  635. lc.AddLine (
  636. new Point (vts.Right, vts.Bottom),
  637. -1,
  638. Orientation.Vertical,
  639. tab.BorderStyle
  640. );
  641. lc.AddLine (
  642. new Point (vts.Right, vts.Bottom),
  643. 0,
  644. Orientation.Horizontal,
  645. tab.BorderStyle
  646. );
  647. }
  648. else
  649. {
  650. // Upper right tee
  651. lc.AddLine (
  652. new Point (vts.Right, vts.Y - 1),
  653. 1,
  654. Orientation.Vertical,
  655. tab.BorderStyle
  656. );
  657. lc.AddLine (
  658. new Point (vts.Right, vts.Y - 1),
  659. 0,
  660. Orientation.Horizontal,
  661. tab.BorderStyle
  662. );
  663. }
  664. }
  665. }
  666. if (_host.Style.TabsOnBottom)
  667. {
  668. //URCorner
  669. lc.AddLine (
  670. new Point (vts.Right, vts.Y - 1),
  671. 1,
  672. Orientation.Vertical,
  673. tab.BorderStyle
  674. );
  675. lc.AddLine (
  676. new Point (vts.Right, vts.Y - 1),
  677. 1,
  678. Orientation.Horizontal,
  679. tab.BorderStyle
  680. );
  681. }
  682. else
  683. {
  684. //LLCorner
  685. lc.AddLine (
  686. new Point (vts.Right, vts.Bottom - selectedOffset),
  687. -1,
  688. Orientation.Vertical,
  689. tab.BorderStyle
  690. );
  691. lc.AddLine (
  692. new Point (vts.Right, vts.Bottom - selectedOffset),
  693. 1,
  694. Orientation.Horizontal,
  695. tab.BorderStyle
  696. );
  697. }
  698. }
  699. else if (selectedTab == -1)
  700. {
  701. if (i == 0 && string.IsNullOrEmpty (tab.Text))
  702. {
  703. if (_host.Style.TabsOnBottom)
  704. {
  705. if (_host.Style.ShowTopLine)
  706. {
  707. // LLCorner
  708. lc.AddLine (
  709. new Point (vts.X - 1, vts.Bottom),
  710. -1,
  711. Orientation.Vertical,
  712. tab.BorderStyle
  713. );
  714. lc.AddLine (
  715. new Point (vts.X - 1, vts.Bottom),
  716. 1,
  717. Orientation.Horizontal,
  718. tab.BorderStyle
  719. );
  720. }
  721. // ULCorner
  722. lc.AddLine (
  723. new Point (vts.X - 1, vts.Y - 1),
  724. 1,
  725. Orientation.Vertical,
  726. tab.BorderStyle
  727. );
  728. lc.AddLine (
  729. new Point (vts.X - 1, vts.Y - 1),
  730. 1,
  731. Orientation.Horizontal,
  732. tab.BorderStyle
  733. );
  734. }
  735. else
  736. {
  737. if (_host.Style.ShowTopLine)
  738. {
  739. // ULCorner
  740. lc.AddLine (
  741. new Point (vts.X - 1, vts.Y - 1),
  742. 1,
  743. Orientation.Vertical,
  744. tab.BorderStyle
  745. );
  746. lc.AddLine (
  747. new Point (vts.X - 1, vts.Y - 1),
  748. 1,
  749. Orientation.Horizontal,
  750. tab.BorderStyle
  751. );
  752. }
  753. // LLCorner
  754. lc.AddLine (
  755. new Point (vts.X - 1, vts.Bottom),
  756. -1,
  757. Orientation.Vertical,
  758. tab.BorderStyle
  759. );
  760. lc.AddLine (
  761. new Point (vts.X - 1, vts.Bottom),
  762. 1,
  763. Orientation.Horizontal,
  764. tab.BorderStyle
  765. );
  766. }
  767. }
  768. else if (i > 0)
  769. {
  770. if (_host.Style.ShowTopLine || _host.Style.TabsOnBottom)
  771. {
  772. // Upper left tee
  773. lc.AddLine (
  774. new Point (vts.X - 1, vts.Y - 1),
  775. 1,
  776. Orientation.Vertical,
  777. tab.BorderStyle
  778. );
  779. lc.AddLine (
  780. new Point (vts.X - 1, vts.Y - 1),
  781. 0,
  782. Orientation.Horizontal,
  783. tab.BorderStyle
  784. );
  785. }
  786. // Lower left tee
  787. lc.AddLine (
  788. new Point (vts.X - 1, vts.Bottom),
  789. -1,
  790. Orientation.Vertical,
  791. tab.BorderStyle
  792. );
  793. lc.AddLine (
  794. new Point (vts.X - 1, vts.Bottom),
  795. 0,
  796. Orientation.Horizontal,
  797. tab.BorderStyle
  798. );
  799. }
  800. }
  801. else if (i < tabLocations.Length - 1)
  802. {
  803. if (_host.Style.ShowTopLine)
  804. {
  805. // Upper right tee
  806. lc.AddLine (
  807. new Point (vts.Right, vts.Y - 1),
  808. 1,
  809. Orientation.Vertical,
  810. tab.BorderStyle
  811. );
  812. lc.AddLine (
  813. new Point (vts.Right, vts.Y - 1),
  814. 0,
  815. Orientation.Horizontal,
  816. tab.BorderStyle
  817. );
  818. }
  819. if (_host.Style.ShowTopLine || !_host.Style.TabsOnBottom)
  820. {
  821. // Lower right tee
  822. lc.AddLine (
  823. new Point (vts.Right, vts.Bottom),
  824. -1,
  825. Orientation.Vertical,
  826. tab.BorderStyle
  827. );
  828. lc.AddLine (
  829. new Point (vts.Right, vts.Bottom),
  830. 0,
  831. Orientation.Horizontal,
  832. tab.BorderStyle
  833. );
  834. }
  835. else
  836. {
  837. // Upper right tee
  838. lc.AddLine (
  839. new Point (vts.Right, vts.Y - 1),
  840. 1,
  841. Orientation.Vertical,
  842. tab.BorderStyle
  843. );
  844. lc.AddLine (
  845. new Point (vts.Right, vts.Y - 1),
  846. 0,
  847. Orientation.Horizontal,
  848. tab.BorderStyle
  849. );
  850. }
  851. }
  852. if (i == 0 && i != selectedTab && _host.TabScrollOffset == 0 && _host.Style.ShowBorder)
  853. {
  854. if (_host.Style.TabsOnBottom)
  855. {
  856. // Upper left vertical line
  857. lc.AddLine (
  858. new Point (vts.X - 1, vts.Y - 1),
  859. 0,
  860. Orientation.Vertical,
  861. tab.BorderStyle
  862. );
  863. lc.AddLine (
  864. new Point (vts.X - 1, vts.Y - 1),
  865. 1,
  866. Orientation.Horizontal,
  867. tab.BorderStyle
  868. );
  869. }
  870. else
  871. {
  872. // Lower left vertical line
  873. lc.AddLine (
  874. new Point (vts.X - 1, vts.Bottom),
  875. 0,
  876. Orientation.Vertical,
  877. tab.BorderStyle
  878. );
  879. lc.AddLine (
  880. new Point (vts.X - 1, vts.Bottom),
  881. 1,
  882. Orientation.Horizontal,
  883. tab.BorderStyle
  884. );
  885. }
  886. }
  887. if (i == tabLocations.Length - 1 && i != selectedTab)
  888. {
  889. if (_host.Style.TabsOnBottom)
  890. {
  891. // Upper right tee
  892. lc.AddLine (
  893. new Point (vts.Right, vts.Y - 1),
  894. 1,
  895. Orientation.Vertical,
  896. tab.BorderStyle
  897. );
  898. lc.AddLine (
  899. new Point (vts.Right, vts.Y - 1),
  900. 0,
  901. Orientation.Horizontal,
  902. tab.BorderStyle
  903. );
  904. }
  905. else
  906. {
  907. // Lower right tee
  908. lc.AddLine (
  909. new Point (vts.Right, vts.Bottom),
  910. -1,
  911. Orientation.Vertical,
  912. tab.BorderStyle
  913. );
  914. lc.AddLine (
  915. new Point (vts.Right, vts.Bottom),
  916. 0,
  917. Orientation.Horizontal,
  918. tab.BorderStyle
  919. );
  920. }
  921. }
  922. if (i == tabLocations.Length - 1)
  923. {
  924. var arrowOffset = 1;
  925. int lastSelectedTab = !_host.Style.ShowTopLine && i == selectedTab ? 1 :
  926. _host.Style.TabsOnBottom ? 1 : 0;
  927. Rectangle tabsBarVts = ViewportToScreen (Viewport);
  928. int lineLength = tabsBarVts.Right - vts.Right;
  929. // Right horizontal line
  930. if (ShouldDrawRightScrollIndicator ())
  931. {
  932. if (lineLength - arrowOffset > 0)
  933. {
  934. if (_host.Style.TabsOnBottom)
  935. {
  936. lc.AddLine (
  937. new Point (vts.Right, vts.Y - lastSelectedTab),
  938. lineLength - arrowOffset,
  939. Orientation.Horizontal,
  940. tab.BorderStyle
  941. );
  942. }
  943. else
  944. {
  945. lc.AddLine (
  946. new Point (
  947. vts.Right,
  948. vts.Bottom - lastSelectedTab
  949. ),
  950. lineLength - arrowOffset,
  951. Orientation.Horizontal,
  952. tab.BorderStyle
  953. );
  954. }
  955. }
  956. }
  957. else
  958. {
  959. if (_host.Style.TabsOnBottom)
  960. {
  961. lc.AddLine (
  962. new Point (vts.Right, vts.Y - lastSelectedTab),
  963. lineLength,
  964. Orientation.Horizontal,
  965. tab.BorderStyle
  966. );
  967. }
  968. else
  969. {
  970. lc.AddLine (
  971. new Point (vts.Right, vts.Bottom - lastSelectedTab),
  972. lineLength,
  973. Orientation.Horizontal,
  974. tab.BorderStyle
  975. );
  976. }
  977. if (_host.Style.ShowBorder)
  978. {
  979. if (_host.Style.TabsOnBottom)
  980. {
  981. // More LRCorner
  982. lc.AddLine (
  983. new Point (
  984. tabsBarVts.Right - 1,
  985. vts.Y - lastSelectedTab
  986. ),
  987. -1,
  988. Orientation.Vertical,
  989. tab.BorderStyle
  990. );
  991. }
  992. else
  993. {
  994. // More URCorner
  995. lc.AddLine (
  996. new Point (
  997. tabsBarVts.Right - 1,
  998. vts.Bottom - lastSelectedTab
  999. ),
  1000. 1,
  1001. Orientation.Vertical,
  1002. tab.BorderStyle
  1003. );
  1004. }
  1005. }
  1006. }
  1007. }
  1008. tab.LineCanvas.Merge (lc);
  1009. tab.OnRenderLineCanvas ();
  1010. }
  1011. }
  1012. private int GetUnderlineYPosition ()
  1013. {
  1014. if (_host.Style.TabsOnBottom)
  1015. {
  1016. return 0;
  1017. }
  1018. return _host.Style.ShowTopLine ? 2 : 1;
  1019. }
  1020. /// <summary>Renders the line with the tab names in it.</summary>
  1021. private void RenderTabLine ()
  1022. {
  1023. TabToRender [] tabLocations = _host._tabLocations;
  1024. int y;
  1025. if (_host.Style.TabsOnBottom)
  1026. {
  1027. y = 1;
  1028. }
  1029. else
  1030. {
  1031. y = _host.Style.ShowTopLine ? 1 : 0;
  1032. }
  1033. View selected = null;
  1034. int topLine = _host.Style.ShowTopLine ? 1 : 0;
  1035. int width = Viewport.Width;
  1036. foreach (TabToRender toRender in tabLocations)
  1037. {
  1038. Tab tab = toRender.Tab;
  1039. if (toRender.IsSelected)
  1040. {
  1041. selected = tab;
  1042. if (_host.Style.TabsOnBottom)
  1043. {
  1044. tab.Border.Thickness = new Thickness (1, 0, 1, topLine);
  1045. tab.Margin.Thickness = new Thickness (0, 1, 0, 0);
  1046. }
  1047. else
  1048. {
  1049. tab.Border.Thickness = new Thickness (1, topLine, 1, 0);
  1050. tab.Margin.Thickness = new Thickness (0, 0, 0, topLine);
  1051. }
  1052. }
  1053. else if (selected is null)
  1054. {
  1055. if (_host.Style.TabsOnBottom)
  1056. {
  1057. tab.Border.Thickness = new Thickness (1, 1, 0, topLine);
  1058. tab.Margin.Thickness = new Thickness (0, 0, 0, 0);
  1059. }
  1060. else
  1061. {
  1062. tab.Border.Thickness = new Thickness (1, topLine, 0, 1);
  1063. tab.Margin.Thickness = new Thickness (0, 0, 0, 0);
  1064. }
  1065. tab.Width = Math.Max (tab.Width.GetAnchor (0) - 1, 1);
  1066. }
  1067. else
  1068. {
  1069. if (_host.Style.TabsOnBottom)
  1070. {
  1071. tab.Border.Thickness = new Thickness (0, 1, 1, topLine);
  1072. tab.Margin.Thickness = new Thickness (0, 0, 0, 0);
  1073. }
  1074. else
  1075. {
  1076. tab.Border.Thickness = new Thickness (0, topLine, 1, 1);
  1077. tab.Margin.Thickness = new Thickness (0, 0, 0, 0);
  1078. }
  1079. tab.Width = Math.Max (tab.Width.GetAnchor (0) - 1, 1);
  1080. }
  1081. tab.Text = toRender.TextToRender;
  1082. LayoutSubviews ();
  1083. tab.OnDrawAdornments ();
  1084. Attribute prevAttr = Driver.GetAttribute ();
  1085. // if tab is the selected one and focus is inside this control
  1086. if (toRender.IsSelected && _host.HasFocus)
  1087. {
  1088. if (_host.Focused == this)
  1089. {
  1090. // if focus is the tab bar itself then show that they can switch tabs
  1091. prevAttr = ColorScheme.HotFocus;
  1092. }
  1093. else
  1094. {
  1095. // Focus is inside the tab
  1096. prevAttr = ColorScheme.HotNormal;
  1097. }
  1098. }
  1099. tab.TextFormatter.Draw (
  1100. tab.ViewportToScreen (tab.Viewport),
  1101. prevAttr,
  1102. ColorScheme.HotNormal
  1103. );
  1104. tab.OnRenderLineCanvas ();
  1105. Driver.SetAttribute (GetNormalColor ());
  1106. }
  1107. }
  1108. /// <summary>Renders the line of the tab that adjoins the content of the tab.</summary>
  1109. private void RenderUnderline ()
  1110. {
  1111. int y = GetUnderlineYPosition ();
  1112. TabToRender selected = _host._tabLocations.FirstOrDefault (t => t.IsSelected);
  1113. if (selected is null)
  1114. {
  1115. return;
  1116. }
  1117. // draw scroll indicators
  1118. // if there are more tabs to the left not visible
  1119. if (_host.TabScrollOffset > 0)
  1120. {
  1121. _leftScrollIndicator.X = 0;
  1122. _leftScrollIndicator.Y = y;
  1123. // indicate that
  1124. _leftScrollIndicator.Visible = true;
  1125. // Ensures this is clicked instead of the first tab
  1126. MoveSubviewToEnd (_leftScrollIndicator);
  1127. _leftScrollIndicator.Draw ();
  1128. }
  1129. else
  1130. {
  1131. _leftScrollIndicator.Visible = false;
  1132. }
  1133. // if there are more tabs to the right not visible
  1134. if (ShouldDrawRightScrollIndicator ())
  1135. {
  1136. _rightScrollIndicator.X = Viewport.Width - 1;
  1137. _rightScrollIndicator.Y = y;
  1138. // indicate that
  1139. _rightScrollIndicator.Visible = true;
  1140. // Ensures this is clicked instead of the last tab if under this
  1141. MoveSubviewToStart (_rightScrollIndicator);
  1142. _rightScrollIndicator.Draw ();
  1143. }
  1144. else
  1145. {
  1146. _rightScrollIndicator.Visible = false;
  1147. }
  1148. }
  1149. private bool ShouldDrawRightScrollIndicator () { return _host._tabLocations.LastOrDefault ()?.Tab != _host.Tabs.LastOrDefault (); }
  1150. }
  1151. private class TabToRender
  1152. {
  1153. public TabToRender (int x, Tab tab, string textToRender, bool isSelected, int width)
  1154. {
  1155. X = x;
  1156. Tab = tab;
  1157. IsSelected = isSelected;
  1158. Width = width;
  1159. TextToRender = textToRender;
  1160. }
  1161. /// <summary>True if the tab that is being rendered is the selected one.</summary>
  1162. /// <value></value>
  1163. public bool IsSelected { get; }
  1164. public Tab Tab { get; }
  1165. public string TextToRender { get; }
  1166. public int Width { get; }
  1167. public int X { get; set; }
  1168. }
  1169. }