TabView.cs 49 KB

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