TabView.cs 50 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402
  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.TabStop;
  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.Focused 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.Focused 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. TabStop = TabBehavior.TabStop;
  456. Height = 1; // BUGBUG: Views should avoid setting Height as doing so implies Frame.Size == GetContentSize ().
  457. Width = Dim.Fill ();
  458. _rightScrollIndicator = new View
  459. {
  460. Id = "rightScrollIndicator",
  461. Width = 1,
  462. Height = 1,
  463. Visible = false,
  464. Text = Glyphs.RightArrow.ToString ()
  465. };
  466. _rightScrollIndicator.MouseClick += _host.Tab_MouseClick;
  467. _leftScrollIndicator = new View
  468. {
  469. Id = "leftScrollIndicator",
  470. Width = 1,
  471. Height = 1,
  472. Visible = false,
  473. Text = Glyphs.LeftArrow.ToString ()
  474. };
  475. _leftScrollIndicator.MouseClick += _host.Tab_MouseClick;
  476. Add (_rightScrollIndicator, _leftScrollIndicator);
  477. }
  478. protected internal override bool OnMouseEvent (MouseEvent me)
  479. {
  480. Tab hit = me.View is Tab ? (Tab)me.View : null;
  481. bool isClick = me.Flags.HasFlag (MouseFlags.Button1Clicked)
  482. || me.Flags.HasFlag (MouseFlags.Button2Clicked)
  483. || me.Flags.HasFlag (MouseFlags.Button3Clicked);
  484. if (isClick)
  485. {
  486. _host.OnTabClicked (new TabMouseEventArgs (hit, me));
  487. // user canceled click
  488. if (me.Handled)
  489. {
  490. return true;
  491. }
  492. }
  493. if (!me.Flags.HasFlag (MouseFlags.Button1Clicked)
  494. && !me.Flags.HasFlag (MouseFlags.Button1DoubleClicked)
  495. && !me.Flags.HasFlag (MouseFlags.Button1TripleClicked))
  496. {
  497. return false;
  498. }
  499. if (!HasFocus && CanFocus)
  500. {
  501. SetFocus ();
  502. }
  503. if (me.Flags.HasFlag (MouseFlags.Button1Clicked)
  504. || me.Flags.HasFlag (MouseFlags.Button1DoubleClicked)
  505. || me.Flags.HasFlag (MouseFlags.Button1TripleClicked))
  506. {
  507. var scrollIndicatorHit = 0;
  508. if (me.View is { } && me.View.Id == "rightScrollIndicator")
  509. {
  510. scrollIndicatorHit = 1;
  511. }
  512. else if (me.View is { } && me.View.Id == "leftScrollIndicator")
  513. {
  514. scrollIndicatorHit = -1;
  515. }
  516. if (scrollIndicatorHit != 0)
  517. {
  518. _host.SwitchTabBy (scrollIndicatorHit);
  519. SetNeedsDisplay ();
  520. return true;
  521. }
  522. if (hit is { })
  523. {
  524. _host.SelectedTab = hit;
  525. SetNeedsDisplay ();
  526. return true;
  527. }
  528. }
  529. return false;
  530. }
  531. public override void OnDrawContent (Rectangle viewport)
  532. {
  533. _host._tabLocations = _host.CalculateViewport (Viewport).ToArray ();
  534. // clear any old text
  535. Clear ();
  536. RenderTabLine ();
  537. RenderUnderline ();
  538. Driver.SetAttribute (HasFocus ? GetFocusColor () : GetNormalColor ());
  539. }
  540. public override void OnDrawContentComplete (Rectangle viewport)
  541. {
  542. if (_host._tabLocations is null)
  543. {
  544. return;
  545. }
  546. TabToRender [] tabLocations = _host._tabLocations;
  547. int selectedTab = -1;
  548. for (var i = 0; i < tabLocations.Length; i++)
  549. {
  550. View tab = tabLocations [i].Tab;
  551. Rectangle vts = tab.ViewportToScreen (tab.Viewport);
  552. var lc = new LineCanvas ();
  553. int selectedOffset = _host.Style.ShowTopLine && tabLocations [i].IsSelected ? 0 : 1;
  554. if (tabLocations [i].IsSelected)
  555. {
  556. selectedTab = i;
  557. if (i == 0 && _host.TabScrollOffset == 0)
  558. {
  559. if (_host.Style.TabsOnBottom)
  560. {
  561. // Upper left vertical line
  562. lc.AddLine (
  563. new Point (vts.X - 1, vts.Y - 1),
  564. -1,
  565. Orientation.Vertical,
  566. tab.BorderStyle
  567. );
  568. }
  569. else
  570. {
  571. // Lower left vertical line
  572. lc.AddLine (
  573. new Point (vts.X - 1, vts.Bottom - selectedOffset),
  574. -1,
  575. Orientation.Vertical,
  576. tab.BorderStyle
  577. );
  578. }
  579. }
  580. else if (i > 0 && i <= tabLocations.Length - 1)
  581. {
  582. if (_host.Style.TabsOnBottom)
  583. {
  584. // URCorner
  585. lc.AddLine (
  586. new Point (vts.X - 1, vts.Y - 1),
  587. 1,
  588. Orientation.Vertical,
  589. tab.BorderStyle
  590. );
  591. lc.AddLine (
  592. new Point (vts.X - 1, vts.Y - 1),
  593. -1,
  594. Orientation.Horizontal,
  595. tab.BorderStyle
  596. );
  597. }
  598. else
  599. {
  600. // LRCorner
  601. lc.AddLine (
  602. new Point (vts.X - 1, vts.Bottom - selectedOffset),
  603. -1,
  604. Orientation.Vertical,
  605. tab.BorderStyle
  606. );
  607. lc.AddLine (
  608. new Point (vts.X - 1, vts.Bottom - selectedOffset),
  609. -1,
  610. Orientation.Horizontal,
  611. tab.BorderStyle
  612. );
  613. }
  614. if (_host.Style.ShowTopLine)
  615. {
  616. if (_host.Style.TabsOnBottom)
  617. {
  618. // Lower left tee
  619. lc.AddLine (
  620. new Point (vts.X - 1, vts.Bottom),
  621. -1,
  622. Orientation.Vertical,
  623. tab.BorderStyle
  624. );
  625. lc.AddLine (
  626. new Point (vts.X - 1, vts.Bottom),
  627. 0,
  628. Orientation.Horizontal,
  629. tab.BorderStyle
  630. );
  631. }
  632. else
  633. {
  634. // Upper left tee
  635. lc.AddLine (
  636. new Point (vts.X - 1, vts.Y - 1),
  637. 1,
  638. Orientation.Vertical,
  639. tab.BorderStyle
  640. );
  641. lc.AddLine (
  642. new Point (vts.X - 1, vts.Y - 1),
  643. 0,
  644. Orientation.Horizontal,
  645. tab.BorderStyle
  646. );
  647. }
  648. }
  649. }
  650. if (i < tabLocations.Length - 1)
  651. {
  652. if (_host.Style.ShowTopLine)
  653. {
  654. if (_host.Style.TabsOnBottom)
  655. {
  656. // Lower right tee
  657. lc.AddLine (
  658. new Point (vts.Right, vts.Bottom),
  659. -1,
  660. Orientation.Vertical,
  661. tab.BorderStyle
  662. );
  663. lc.AddLine (
  664. new Point (vts.Right, vts.Bottom),
  665. 0,
  666. Orientation.Horizontal,
  667. tab.BorderStyle
  668. );
  669. }
  670. else
  671. {
  672. // Upper right tee
  673. lc.AddLine (
  674. new Point (vts.Right, vts.Y - 1),
  675. 1,
  676. Orientation.Vertical,
  677. tab.BorderStyle
  678. );
  679. lc.AddLine (
  680. new Point (vts.Right, vts.Y - 1),
  681. 0,
  682. Orientation.Horizontal,
  683. tab.BorderStyle
  684. );
  685. }
  686. }
  687. }
  688. if (_host.Style.TabsOnBottom)
  689. {
  690. //URCorner
  691. lc.AddLine (
  692. new Point (vts.Right, vts.Y - 1),
  693. 1,
  694. Orientation.Vertical,
  695. tab.BorderStyle
  696. );
  697. lc.AddLine (
  698. new Point (vts.Right, vts.Y - 1),
  699. 1,
  700. Orientation.Horizontal,
  701. tab.BorderStyle
  702. );
  703. }
  704. else
  705. {
  706. //LLCorner
  707. lc.AddLine (
  708. new Point (vts.Right, vts.Bottom - selectedOffset),
  709. -1,
  710. Orientation.Vertical,
  711. tab.BorderStyle
  712. );
  713. lc.AddLine (
  714. new Point (vts.Right, vts.Bottom - selectedOffset),
  715. 1,
  716. Orientation.Horizontal,
  717. tab.BorderStyle
  718. );
  719. }
  720. }
  721. else if (selectedTab == -1)
  722. {
  723. if (i == 0 && string.IsNullOrEmpty (tab.Text))
  724. {
  725. if (_host.Style.TabsOnBottom)
  726. {
  727. if (_host.Style.ShowTopLine)
  728. {
  729. // LLCorner
  730. lc.AddLine (
  731. new Point (vts.X - 1, vts.Bottom),
  732. -1,
  733. Orientation.Vertical,
  734. tab.BorderStyle
  735. );
  736. lc.AddLine (
  737. new Point (vts.X - 1, vts.Bottom),
  738. 1,
  739. Orientation.Horizontal,
  740. tab.BorderStyle
  741. );
  742. }
  743. // ULCorner
  744. lc.AddLine (
  745. new Point (vts.X - 1, vts.Y - 1),
  746. 1,
  747. Orientation.Vertical,
  748. tab.BorderStyle
  749. );
  750. lc.AddLine (
  751. new Point (vts.X - 1, vts.Y - 1),
  752. 1,
  753. Orientation.Horizontal,
  754. tab.BorderStyle
  755. );
  756. }
  757. else
  758. {
  759. if (_host.Style.ShowTopLine)
  760. {
  761. // ULCorner
  762. lc.AddLine (
  763. new Point (vts.X - 1, vts.Y - 1),
  764. 1,
  765. Orientation.Vertical,
  766. tab.BorderStyle
  767. );
  768. lc.AddLine (
  769. new Point (vts.X - 1, vts.Y - 1),
  770. 1,
  771. Orientation.Horizontal,
  772. tab.BorderStyle
  773. );
  774. }
  775. // LLCorner
  776. lc.AddLine (
  777. new Point (vts.X - 1, vts.Bottom),
  778. -1,
  779. Orientation.Vertical,
  780. tab.BorderStyle
  781. );
  782. lc.AddLine (
  783. new Point (vts.X - 1, vts.Bottom),
  784. 1,
  785. Orientation.Horizontal,
  786. tab.BorderStyle
  787. );
  788. }
  789. }
  790. else if (i > 0)
  791. {
  792. if (_host.Style.ShowTopLine || _host.Style.TabsOnBottom)
  793. {
  794. // Upper left tee
  795. lc.AddLine (
  796. new Point (vts.X - 1, vts.Y - 1),
  797. 1,
  798. Orientation.Vertical,
  799. tab.BorderStyle
  800. );
  801. lc.AddLine (
  802. new Point (vts.X - 1, vts.Y - 1),
  803. 0,
  804. Orientation.Horizontal,
  805. tab.BorderStyle
  806. );
  807. }
  808. // Lower left tee
  809. lc.AddLine (
  810. new Point (vts.X - 1, vts.Bottom),
  811. -1,
  812. Orientation.Vertical,
  813. tab.BorderStyle
  814. );
  815. lc.AddLine (
  816. new Point (vts.X - 1, vts.Bottom),
  817. 0,
  818. Orientation.Horizontal,
  819. tab.BorderStyle
  820. );
  821. }
  822. }
  823. else if (i < tabLocations.Length - 1)
  824. {
  825. if (_host.Style.ShowTopLine)
  826. {
  827. // Upper right tee
  828. lc.AddLine (
  829. new Point (vts.Right, vts.Y - 1),
  830. 1,
  831. Orientation.Vertical,
  832. tab.BorderStyle
  833. );
  834. lc.AddLine (
  835. new Point (vts.Right, vts.Y - 1),
  836. 0,
  837. Orientation.Horizontal,
  838. tab.BorderStyle
  839. );
  840. }
  841. if (_host.Style.ShowTopLine || !_host.Style.TabsOnBottom)
  842. {
  843. // Lower right tee
  844. lc.AddLine (
  845. new Point (vts.Right, vts.Bottom),
  846. -1,
  847. Orientation.Vertical,
  848. tab.BorderStyle
  849. );
  850. lc.AddLine (
  851. new Point (vts.Right, vts.Bottom),
  852. 0,
  853. Orientation.Horizontal,
  854. tab.BorderStyle
  855. );
  856. }
  857. else
  858. {
  859. // Upper right tee
  860. lc.AddLine (
  861. new Point (vts.Right, vts.Y - 1),
  862. 1,
  863. Orientation.Vertical,
  864. tab.BorderStyle
  865. );
  866. lc.AddLine (
  867. new Point (vts.Right, vts.Y - 1),
  868. 0,
  869. Orientation.Horizontal,
  870. tab.BorderStyle
  871. );
  872. }
  873. }
  874. if (i == 0 && i != selectedTab && _host.TabScrollOffset == 0 && _host.Style.ShowBorder)
  875. {
  876. if (_host.Style.TabsOnBottom)
  877. {
  878. // Upper left vertical line
  879. lc.AddLine (
  880. new Point (vts.X - 1, vts.Y - 1),
  881. 0,
  882. Orientation.Vertical,
  883. tab.BorderStyle
  884. );
  885. lc.AddLine (
  886. new Point (vts.X - 1, vts.Y - 1),
  887. 1,
  888. Orientation.Horizontal,
  889. tab.BorderStyle
  890. );
  891. }
  892. else
  893. {
  894. // Lower left vertical line
  895. lc.AddLine (
  896. new Point (vts.X - 1, vts.Bottom),
  897. 0,
  898. Orientation.Vertical,
  899. tab.BorderStyle
  900. );
  901. lc.AddLine (
  902. new Point (vts.X - 1, vts.Bottom),
  903. 1,
  904. Orientation.Horizontal,
  905. tab.BorderStyle
  906. );
  907. }
  908. }
  909. if (i == tabLocations.Length - 1 && i != selectedTab)
  910. {
  911. if (_host.Style.TabsOnBottom)
  912. {
  913. // Upper right tee
  914. lc.AddLine (
  915. new Point (vts.Right, vts.Y - 1),
  916. 1,
  917. Orientation.Vertical,
  918. tab.BorderStyle
  919. );
  920. lc.AddLine (
  921. new Point (vts.Right, vts.Y - 1),
  922. 0,
  923. Orientation.Horizontal,
  924. tab.BorderStyle
  925. );
  926. }
  927. else
  928. {
  929. // Lower right tee
  930. lc.AddLine (
  931. new Point (vts.Right, vts.Bottom),
  932. -1,
  933. Orientation.Vertical,
  934. tab.BorderStyle
  935. );
  936. lc.AddLine (
  937. new Point (vts.Right, vts.Bottom),
  938. 0,
  939. Orientation.Horizontal,
  940. tab.BorderStyle
  941. );
  942. }
  943. }
  944. if (i == tabLocations.Length - 1)
  945. {
  946. var arrowOffset = 1;
  947. int lastSelectedTab = !_host.Style.ShowTopLine && i == selectedTab ? 1 :
  948. _host.Style.TabsOnBottom ? 1 : 0;
  949. Rectangle tabsBarVts = ViewportToScreen (Viewport);
  950. int lineLength = tabsBarVts.Right - vts.Right;
  951. // Right horizontal line
  952. if (ShouldDrawRightScrollIndicator ())
  953. {
  954. if (lineLength - arrowOffset > 0)
  955. {
  956. if (_host.Style.TabsOnBottom)
  957. {
  958. lc.AddLine (
  959. new Point (vts.Right, vts.Y - lastSelectedTab),
  960. lineLength - arrowOffset,
  961. Orientation.Horizontal,
  962. tab.BorderStyle
  963. );
  964. }
  965. else
  966. {
  967. lc.AddLine (
  968. new Point (
  969. vts.Right,
  970. vts.Bottom - lastSelectedTab
  971. ),
  972. lineLength - arrowOffset,
  973. Orientation.Horizontal,
  974. tab.BorderStyle
  975. );
  976. }
  977. }
  978. }
  979. else
  980. {
  981. if (_host.Style.TabsOnBottom)
  982. {
  983. lc.AddLine (
  984. new Point (vts.Right, vts.Y - lastSelectedTab),
  985. lineLength,
  986. Orientation.Horizontal,
  987. tab.BorderStyle
  988. );
  989. }
  990. else
  991. {
  992. lc.AddLine (
  993. new Point (vts.Right, vts.Bottom - lastSelectedTab),
  994. lineLength,
  995. Orientation.Horizontal,
  996. tab.BorderStyle
  997. );
  998. }
  999. if (_host.Style.ShowBorder)
  1000. {
  1001. if (_host.Style.TabsOnBottom)
  1002. {
  1003. // More LRCorner
  1004. lc.AddLine (
  1005. new Point (
  1006. tabsBarVts.Right - 1,
  1007. vts.Y - lastSelectedTab
  1008. ),
  1009. -1,
  1010. Orientation.Vertical,
  1011. tab.BorderStyle
  1012. );
  1013. }
  1014. else
  1015. {
  1016. // More URCorner
  1017. lc.AddLine (
  1018. new Point (
  1019. tabsBarVts.Right - 1,
  1020. vts.Bottom - lastSelectedTab
  1021. ),
  1022. 1,
  1023. Orientation.Vertical,
  1024. tab.BorderStyle
  1025. );
  1026. }
  1027. }
  1028. }
  1029. }
  1030. tab.LineCanvas.Merge (lc);
  1031. tab.OnRenderLineCanvas ();
  1032. }
  1033. }
  1034. private int GetUnderlineYPosition ()
  1035. {
  1036. if (_host.Style.TabsOnBottom)
  1037. {
  1038. return 0;
  1039. }
  1040. return _host.Style.ShowTopLine ? 2 : 1;
  1041. }
  1042. /// <summary>Renders the line with the tab names in it.</summary>
  1043. private void RenderTabLine ()
  1044. {
  1045. TabToRender [] tabLocations = _host._tabLocations;
  1046. int y;
  1047. if (_host.Style.TabsOnBottom)
  1048. {
  1049. y = 1;
  1050. }
  1051. else
  1052. {
  1053. y = _host.Style.ShowTopLine ? 1 : 0;
  1054. }
  1055. View selected = null;
  1056. int topLine = _host.Style.ShowTopLine ? 1 : 0;
  1057. int width = Viewport.Width;
  1058. foreach (TabToRender toRender in tabLocations)
  1059. {
  1060. Tab tab = toRender.Tab;
  1061. if (toRender.IsSelected)
  1062. {
  1063. selected = tab;
  1064. if (_host.Style.TabsOnBottom)
  1065. {
  1066. tab.Border.Thickness = new Thickness (1, 0, 1, topLine);
  1067. tab.Margin.Thickness = new Thickness (0, 1, 0, 0);
  1068. }
  1069. else
  1070. {
  1071. tab.Border.Thickness = new Thickness (1, topLine, 1, 0);
  1072. tab.Margin.Thickness = new Thickness (0, 0, 0, topLine);
  1073. }
  1074. }
  1075. else if (selected is null)
  1076. {
  1077. if (_host.Style.TabsOnBottom)
  1078. {
  1079. tab.Border.Thickness = new Thickness (1, 1, 0, topLine);
  1080. tab.Margin.Thickness = new Thickness (0, 0, 0, 0);
  1081. }
  1082. else
  1083. {
  1084. tab.Border.Thickness = new Thickness (1, topLine, 0, 1);
  1085. tab.Margin.Thickness = new Thickness (0, 0, 0, 0);
  1086. }
  1087. tab.Width = Math.Max (tab.Width.GetAnchor (0) - 1, 1);
  1088. }
  1089. else
  1090. {
  1091. if (_host.Style.TabsOnBottom)
  1092. {
  1093. tab.Border.Thickness = new Thickness (0, 1, 1, topLine);
  1094. tab.Margin.Thickness = new Thickness (0, 0, 0, 0);
  1095. }
  1096. else
  1097. {
  1098. tab.Border.Thickness = new Thickness (0, topLine, 1, 1);
  1099. tab.Margin.Thickness = new Thickness (0, 0, 0, 0);
  1100. }
  1101. tab.Width = Math.Max (tab.Width.GetAnchor (0) - 1, 1);
  1102. }
  1103. tab.Text = toRender.TextToRender;
  1104. LayoutSubviews ();
  1105. tab.OnDrawAdornments ();
  1106. Attribute prevAttr = Driver.GetAttribute ();
  1107. // if tab is the selected one and focus is inside this control
  1108. if (toRender.IsSelected && _host.HasFocus)
  1109. {
  1110. if (_host.Focused == this)
  1111. {
  1112. // if focus is the tab bar itself then show that they can switch tabs
  1113. prevAttr = ColorScheme.HotFocus;
  1114. }
  1115. else
  1116. {
  1117. // Focus is inside the tab
  1118. prevAttr = ColorScheme.HotNormal;
  1119. }
  1120. }
  1121. tab.TextFormatter.Draw (
  1122. tab.ViewportToScreen (tab.Viewport),
  1123. prevAttr,
  1124. ColorScheme.HotNormal
  1125. );
  1126. tab.OnRenderLineCanvas ();
  1127. Driver.SetAttribute (GetNormalColor ());
  1128. }
  1129. }
  1130. /// <summary>Renders the line of the tab that adjoins the content of the tab.</summary>
  1131. private void RenderUnderline ()
  1132. {
  1133. int y = GetUnderlineYPosition ();
  1134. TabToRender selected = _host._tabLocations.FirstOrDefault (t => t.IsSelected);
  1135. if (selected is null)
  1136. {
  1137. return;
  1138. }
  1139. // draw scroll indicators
  1140. // if there are more tabs to the left not visible
  1141. if (_host.TabScrollOffset > 0)
  1142. {
  1143. _leftScrollIndicator.X = 0;
  1144. _leftScrollIndicator.Y = y;
  1145. // indicate that
  1146. _leftScrollIndicator.Visible = true;
  1147. // Ensures this is clicked instead of the first tab
  1148. BringSubviewToFront (_leftScrollIndicator);
  1149. _leftScrollIndicator.Draw ();
  1150. }
  1151. else
  1152. {
  1153. _leftScrollIndicator.Visible = false;
  1154. }
  1155. // if there are more tabs to the right not visible
  1156. if (ShouldDrawRightScrollIndicator ())
  1157. {
  1158. _rightScrollIndicator.X = Viewport.Width - 1;
  1159. _rightScrollIndicator.Y = y;
  1160. // indicate that
  1161. _rightScrollIndicator.Visible = true;
  1162. // Ensures this is clicked instead of the last tab if under this
  1163. BringSubviewToFront (_rightScrollIndicator);
  1164. _rightScrollIndicator.Draw ();
  1165. }
  1166. else
  1167. {
  1168. _rightScrollIndicator.Visible = false;
  1169. }
  1170. }
  1171. private bool ShouldDrawRightScrollIndicator () { return _host._tabLocations.LastOrDefault ()?.Tab != _host.Tabs.LastOrDefault (); }
  1172. }
  1173. private class TabToRender
  1174. {
  1175. public TabToRender (int x, Tab tab, string textToRender, bool isSelected, int width)
  1176. {
  1177. X = x;
  1178. Tab = tab;
  1179. IsSelected = isSelected;
  1180. Width = width;
  1181. TextToRender = textToRender;
  1182. }
  1183. /// <summary>True if the tab that is being rendered is the selected one.</summary>
  1184. /// <value></value>
  1185. public bool IsSelected { get; }
  1186. public Tab Tab { get; }
  1187. public string TextToRender { get; }
  1188. public int Width { get; }
  1189. public int X { get; set; }
  1190. }
  1191. }