2
0

TabView.cs 49 KB

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