TabView.cs 50 KB

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