TabView.cs 48 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381
  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 || !_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. // The maximum number of characters to use for the tab name as specified
  364. // by the user (MaxTabTextWidth). But not more than the width of the view
  365. // or we won't even be able to render a single tab!
  366. long maxWidth = Math.Max (0, Math.Min (bounds.Width - 3, MaxTabTextWidth));
  367. tab.Width = 2;
  368. tab.Height = Style.ShowTopLine ? 3 : 2;
  369. // if tab view is width <= 3 don't render any tabs
  370. if (maxWidth == 0)
  371. {
  372. tab.Visible = true;
  373. tab.MouseClick += Tab_MouseClick!;
  374. tab.Border!.MouseClick += Tab_MouseClick!;
  375. yield return new (tab, Equals (SelectedTab, tab));
  376. break;
  377. }
  378. if (tabTextWidth > maxWidth)
  379. {
  380. tab.Text = tab.DisplayText.Substring (0, (int)maxWidth);
  381. tabTextWidth = (int)maxWidth;
  382. }
  383. else
  384. {
  385. tab.Text = tab.DisplayText;
  386. }
  387. tab.Width = Math.Max (tabTextWidth + 2, 1);
  388. tab.Height = Style.ShowTopLine ? 3 : 2;
  389. // if there is not enough space for this tab
  390. if (i + tabTextWidth >= bounds.Width)
  391. {
  392. tab.Visible = false;
  393. break;
  394. }
  395. // there is enough space!
  396. tab.Visible = true;
  397. tab.MouseClick += Tab_MouseClick!;
  398. tab.Border!.MouseClick += Tab_MouseClick!;
  399. yield return new (tab, Equals (SelectedTab, tab));
  400. prevTab = tab;
  401. i += tabTextWidth + 1;
  402. }
  403. if (_selectedTabHasFocus)
  404. {
  405. SelectedTab?.SetFocus ();
  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, MouseEventArgs e)
  428. {
  429. e.Handled = _tabsBar.NewMouseEvent (e) == true;
  430. }
  431. private void UnSetCurrentTabs ()
  432. {
  433. if (_tabLocations is null)
  434. {
  435. // Ensures unset any visible tab prior to TabScrollOffset
  436. for (int i = 0; i < TabScrollOffset; i++)
  437. {
  438. Tab tab = Tabs.ElementAt (i);
  439. if (tab.Visible)
  440. {
  441. tab.MouseClick -= Tab_MouseClick!;
  442. tab.Border!.MouseClick -= Tab_MouseClick!;
  443. tab.Visible = false;
  444. }
  445. }
  446. }
  447. else if (_tabLocations is { })
  448. {
  449. foreach (TabToRender tabToRender in _tabLocations)
  450. {
  451. tabToRender.Tab.MouseClick -= Tab_MouseClick!;
  452. tabToRender.Tab.Border!.MouseClick -= Tab_MouseClick!;
  453. tabToRender.Tab.Visible = false;
  454. }
  455. _tabLocations = null;
  456. }
  457. }
  458. /// <summary>Raises the <see cref="TabClicked"/> event.</summary>
  459. /// <param name="tabMouseEventArgs"></param>
  460. private protected virtual void OnTabClicked (TabMouseEventArgs tabMouseEventArgs) { TabClicked?.Invoke (this, tabMouseEventArgs); }
  461. private class TabRowView : View
  462. {
  463. private readonly TabView _host;
  464. private readonly View _leftScrollIndicator;
  465. private readonly View _rightScrollIndicator;
  466. public TabRowView (TabView host)
  467. {
  468. _host = host;
  469. Id = "tabRowView";
  470. CanFocus = true;
  471. Width = Dim.Fill ();
  472. _rightScrollIndicator = new View
  473. {
  474. Id = "rightScrollIndicator",
  475. Width = 1,
  476. Height = 1,
  477. Visible = false,
  478. Text = Glyphs.RightArrow.ToString ()
  479. };
  480. _rightScrollIndicator.MouseClick += _host.Tab_MouseClick!;
  481. _leftScrollIndicator = new View
  482. {
  483. Id = "leftScrollIndicator",
  484. Width = 1,
  485. Height = 1,
  486. Visible = false,
  487. Text = Glyphs.LeftArrow.ToString ()
  488. };
  489. _leftScrollIndicator.MouseClick += _host.Tab_MouseClick!;
  490. Add (_rightScrollIndicator, _leftScrollIndicator);
  491. }
  492. protected override bool OnMouseEvent (MouseEventArgs me)
  493. {
  494. View? parent = me.View is Adornment adornment ? adornment.Parent : me.View;
  495. Tab? hit = parent as Tab;
  496. if (me.IsSingleClicked)
  497. {
  498. _host.OnTabClicked (new TabMouseEventArgs (hit!, me));
  499. // user canceled click
  500. if (me.Handled)
  501. {
  502. return true;
  503. }
  504. if (parent == _host.SelectedTab)
  505. {
  506. _host.SelectedTab?.SetFocus ();
  507. }
  508. }
  509. if (!me.IsSingleDoubleOrTripleClicked)
  510. {
  511. return false;
  512. }
  513. if (!HasFocus && CanFocus)
  514. {
  515. SetFocus ();
  516. }
  517. if (me.IsSingleDoubleOrTripleClicked)
  518. {
  519. var scrollIndicatorHit = 0;
  520. if (me.View is { Id: "rightScrollIndicator" })
  521. {
  522. scrollIndicatorHit = 1;
  523. }
  524. else if (me.View is { Id: "leftScrollIndicator" })
  525. {
  526. scrollIndicatorHit = -1;
  527. }
  528. if (scrollIndicatorHit != 0)
  529. {
  530. _host.SwitchTabBy (scrollIndicatorHit);
  531. SetNeedsLayout ();
  532. return true;
  533. }
  534. if (hit is { })
  535. {
  536. _host.SelectedTab = hit;
  537. SetNeedsLayout ();
  538. return true;
  539. }
  540. }
  541. return false;
  542. }
  543. /// <inheritdoc />
  544. protected override void OnHasFocusChanged (bool newHasFocus, View? previousFocusedView, View? focusedView)
  545. {
  546. if (_host.SelectedTab is { HasFocus: false, CanFocus: true } && focusedView == this)
  547. {
  548. _host.SelectedTab?.SetFocus ();
  549. return;
  550. }
  551. base.OnHasFocusChanged (newHasFocus, previousFocusedView, focusedView);
  552. }
  553. /// <inheritdoc />
  554. protected override void OnSubviewLayout (LayoutEventArgs args)
  555. {
  556. _host._tabLocations = _host.CalculateViewport (Viewport).ToArray ();
  557. RenderTabLine ();
  558. RenderUnderline ();
  559. base.OnSubviewLayout (args);
  560. }
  561. /// <inheritdoc />
  562. protected override bool OnRenderingLineCanvas ()
  563. {
  564. RenderTabLineCanvas ();
  565. return false;
  566. }
  567. private void RenderTabLineCanvas ()
  568. {
  569. if (_host._tabLocations is null)
  570. {
  571. return;
  572. }
  573. TabToRender [] tabLocations = _host._tabLocations;
  574. int selectedTab = -1;
  575. var lc = new LineCanvas ();
  576. for (var i = 0; i < tabLocations.Length; i++)
  577. {
  578. View tab = tabLocations [i].Tab;
  579. Rectangle vts = tab.ViewportToScreen (tab.Viewport);
  580. int selectedOffset = _host.Style.ShowTopLine && tabLocations [i].IsSelected ? 0 : 1;
  581. if (tabLocations [i].IsSelected)
  582. {
  583. selectedTab = i;
  584. if (i == 0 && _host.TabScrollOffset == 0)
  585. {
  586. if (_host.Style.TabsOnBottom)
  587. {
  588. // Upper left vertical line
  589. lc.AddLine (
  590. new Point (vts.X - 1, vts.Y - 1),
  591. -1,
  592. Orientation.Vertical,
  593. tab.BorderStyle
  594. );
  595. }
  596. else
  597. {
  598. // Lower left vertical line
  599. lc.AddLine (
  600. new Point (vts.X - 1, vts.Bottom - selectedOffset),
  601. -1,
  602. Orientation.Vertical,
  603. tab.BorderStyle
  604. );
  605. }
  606. }
  607. else if (i > 0 && i <= tabLocations.Length - 1)
  608. {
  609. if (_host.Style.TabsOnBottom)
  610. {
  611. // URCorner
  612. lc.AddLine (
  613. new Point (vts.X - 1, vts.Y - 1),
  614. 1,
  615. Orientation.Vertical,
  616. tab.BorderStyle
  617. );
  618. lc.AddLine (
  619. new Point (vts.X - 1, vts.Y - 1),
  620. -1,
  621. Orientation.Horizontal,
  622. tab.BorderStyle
  623. );
  624. }
  625. else
  626. {
  627. // LRCorner
  628. lc.AddLine (
  629. new Point (vts.X - 1, vts.Bottom - selectedOffset),
  630. -1,
  631. Orientation.Vertical,
  632. tab.BorderStyle
  633. );
  634. lc.AddLine (
  635. new Point (vts.X - 1, vts.Bottom - selectedOffset),
  636. -1,
  637. Orientation.Horizontal,
  638. tab.BorderStyle
  639. );
  640. }
  641. if (_host.Style.ShowTopLine)
  642. {
  643. if (_host.Style.TabsOnBottom)
  644. {
  645. // Lower left tee
  646. lc.AddLine (
  647. new Point (vts.X - 1, vts.Bottom),
  648. -1,
  649. Orientation.Vertical,
  650. tab.BorderStyle
  651. );
  652. lc.AddLine (
  653. new Point (vts.X - 1, vts.Bottom),
  654. 0,
  655. Orientation.Horizontal,
  656. tab.BorderStyle
  657. );
  658. }
  659. else
  660. {
  661. // Upper left tee
  662. lc.AddLine (
  663. new Point (vts.X - 1, vts.Y - 1),
  664. 1,
  665. Orientation.Vertical,
  666. tab.BorderStyle
  667. );
  668. lc.AddLine (
  669. new Point (vts.X - 1, vts.Y - 1),
  670. 0,
  671. Orientation.Horizontal,
  672. tab.BorderStyle
  673. );
  674. }
  675. }
  676. }
  677. if (i < tabLocations.Length - 1)
  678. {
  679. if (_host.Style.ShowTopLine)
  680. {
  681. if (_host.Style.TabsOnBottom)
  682. {
  683. // Lower right tee
  684. lc.AddLine (
  685. new Point (vts.Right, vts.Bottom),
  686. -1,
  687. Orientation.Vertical,
  688. tab.BorderStyle
  689. );
  690. lc.AddLine (
  691. new Point (vts.Right, vts.Bottom),
  692. 0,
  693. Orientation.Horizontal,
  694. tab.BorderStyle
  695. );
  696. }
  697. else
  698. {
  699. // Upper right tee
  700. lc.AddLine (
  701. new Point (vts.Right, vts.Y - 1),
  702. 1,
  703. Orientation.Vertical,
  704. tab.BorderStyle
  705. );
  706. lc.AddLine (
  707. new Point (vts.Right, vts.Y - 1),
  708. 0,
  709. Orientation.Horizontal,
  710. tab.BorderStyle
  711. );
  712. }
  713. }
  714. }
  715. if (_host.Style.TabsOnBottom)
  716. {
  717. //URCorner
  718. lc.AddLine (
  719. new Point (vts.Right, vts.Y - 1),
  720. 1,
  721. Orientation.Vertical,
  722. tab.BorderStyle
  723. );
  724. lc.AddLine (
  725. new Point (vts.Right, vts.Y - 1),
  726. 1,
  727. Orientation.Horizontal,
  728. tab.BorderStyle
  729. );
  730. }
  731. else
  732. {
  733. //LLCorner
  734. lc.AddLine (
  735. new Point (vts.Right, vts.Bottom - selectedOffset),
  736. -1,
  737. Orientation.Vertical,
  738. tab.BorderStyle
  739. );
  740. lc.AddLine (
  741. new Point (vts.Right, vts.Bottom - selectedOffset),
  742. 1,
  743. Orientation.Horizontal,
  744. tab.BorderStyle
  745. );
  746. }
  747. }
  748. else if (selectedTab == -1)
  749. {
  750. if (i == 0 && string.IsNullOrEmpty (tab.Text))
  751. {
  752. if (_host.Style.TabsOnBottom)
  753. {
  754. if (_host.Style.ShowTopLine)
  755. {
  756. // LLCorner
  757. lc.AddLine (
  758. new Point (vts.X - 1, vts.Bottom),
  759. -1,
  760. Orientation.Vertical,
  761. tab.BorderStyle
  762. );
  763. lc.AddLine (
  764. new Point (vts.X - 1, vts.Bottom),
  765. 1,
  766. Orientation.Horizontal,
  767. tab.BorderStyle
  768. );
  769. }
  770. // ULCorner
  771. lc.AddLine (
  772. new Point (vts.X - 1, vts.Y - 1),
  773. 1,
  774. Orientation.Vertical,
  775. tab.BorderStyle
  776. );
  777. lc.AddLine (
  778. new Point (vts.X - 1, vts.Y - 1),
  779. 1,
  780. Orientation.Horizontal,
  781. tab.BorderStyle
  782. );
  783. }
  784. else
  785. {
  786. if (_host.Style.ShowTopLine)
  787. {
  788. // ULCorner
  789. lc.AddLine (
  790. new Point (vts.X - 1, vts.Y - 1),
  791. 1,
  792. Orientation.Vertical,
  793. tab.BorderStyle
  794. );
  795. lc.AddLine (
  796. new Point (vts.X - 1, vts.Y - 1),
  797. 1,
  798. Orientation.Horizontal,
  799. tab.BorderStyle
  800. );
  801. }
  802. // LLCorner
  803. lc.AddLine (
  804. new Point (vts.X - 1, vts.Bottom),
  805. -1,
  806. Orientation.Vertical,
  807. tab.BorderStyle
  808. );
  809. lc.AddLine (
  810. new Point (vts.X - 1, vts.Bottom),
  811. 1,
  812. Orientation.Horizontal,
  813. tab.BorderStyle
  814. );
  815. }
  816. }
  817. else if (i > 0)
  818. {
  819. if (_host.Style.ShowTopLine || _host.Style.TabsOnBottom)
  820. {
  821. // Upper left tee
  822. lc.AddLine (
  823. new Point (vts.X - 1, vts.Y - 1),
  824. 1,
  825. Orientation.Vertical,
  826. tab.BorderStyle
  827. );
  828. lc.AddLine (
  829. new Point (vts.X - 1, vts.Y - 1),
  830. 0,
  831. Orientation.Horizontal,
  832. tab.BorderStyle
  833. );
  834. }
  835. // Lower left tee
  836. lc.AddLine (
  837. new Point (vts.X - 1, vts.Bottom),
  838. -1,
  839. Orientation.Vertical,
  840. tab.BorderStyle
  841. );
  842. lc.AddLine (
  843. new Point (vts.X - 1, vts.Bottom),
  844. 0,
  845. Orientation.Horizontal,
  846. tab.BorderStyle
  847. );
  848. }
  849. }
  850. else if (i < tabLocations.Length - 1)
  851. {
  852. if (_host.Style.ShowTopLine)
  853. {
  854. // Upper right tee
  855. lc.AddLine (
  856. new Point (vts.Right, vts.Y - 1),
  857. 1,
  858. Orientation.Vertical,
  859. tab.BorderStyle
  860. );
  861. lc.AddLine (
  862. new Point (vts.Right, vts.Y - 1),
  863. 0,
  864. Orientation.Horizontal,
  865. tab.BorderStyle
  866. );
  867. }
  868. if (_host.Style.ShowTopLine || !_host.Style.TabsOnBottom)
  869. {
  870. // Lower right tee
  871. lc.AddLine (
  872. new Point (vts.Right, vts.Bottom),
  873. -1,
  874. Orientation.Vertical,
  875. tab.BorderStyle
  876. );
  877. lc.AddLine (
  878. new Point (vts.Right, vts.Bottom),
  879. 0,
  880. Orientation.Horizontal,
  881. tab.BorderStyle
  882. );
  883. }
  884. else
  885. {
  886. // Upper right tee
  887. lc.AddLine (
  888. new Point (vts.Right, vts.Y - 1),
  889. 1,
  890. Orientation.Vertical,
  891. tab.BorderStyle
  892. );
  893. lc.AddLine (
  894. new Point (vts.Right, vts.Y - 1),
  895. 0,
  896. Orientation.Horizontal,
  897. tab.BorderStyle
  898. );
  899. }
  900. }
  901. if (i == 0 && i != selectedTab && _host is { TabScrollOffset: 0, Style.ShowBorder: true })
  902. {
  903. if (_host.Style.TabsOnBottom)
  904. {
  905. // Upper left vertical line
  906. lc.AddLine (
  907. new Point (vts.X - 1, vts.Y - 1),
  908. 0,
  909. Orientation.Vertical,
  910. tab.BorderStyle
  911. );
  912. lc.AddLine (
  913. new Point (vts.X - 1, vts.Y - 1),
  914. 1,
  915. Orientation.Horizontal,
  916. tab.BorderStyle
  917. );
  918. }
  919. else
  920. {
  921. // Lower left vertical line
  922. lc.AddLine (
  923. new Point (vts.X - 1, vts.Bottom),
  924. 0,
  925. Orientation.Vertical,
  926. tab.BorderStyle
  927. );
  928. lc.AddLine (
  929. new Point (vts.X - 1, vts.Bottom),
  930. 1,
  931. Orientation.Horizontal,
  932. tab.BorderStyle
  933. );
  934. }
  935. }
  936. if (i == tabLocations.Length - 1 && i != selectedTab)
  937. {
  938. if (_host.Style.TabsOnBottom)
  939. {
  940. // Upper right tee
  941. lc.AddLine (
  942. new Point (vts.Right, vts.Y - 1),
  943. 1,
  944. Orientation.Vertical,
  945. tab.BorderStyle
  946. );
  947. lc.AddLine (
  948. new Point (vts.Right, vts.Y - 1),
  949. 0,
  950. Orientation.Horizontal,
  951. tab.BorderStyle
  952. );
  953. }
  954. else
  955. {
  956. // Lower right tee
  957. lc.AddLine (
  958. new Point (vts.Right, vts.Bottom),
  959. -1,
  960. Orientation.Vertical,
  961. tab.BorderStyle
  962. );
  963. lc.AddLine (
  964. new Point (vts.Right, vts.Bottom),
  965. 0,
  966. Orientation.Horizontal,
  967. tab.BorderStyle
  968. );
  969. }
  970. }
  971. if (i == tabLocations.Length - 1)
  972. {
  973. var arrowOffset = 1;
  974. int lastSelectedTab = !_host.Style.ShowTopLine && i == selectedTab ? 1 :
  975. _host.Style.TabsOnBottom ? 1 : 0;
  976. Rectangle tabsBarVts = ViewportToScreen (Viewport);
  977. int lineLength = tabsBarVts.Right - vts.Right;
  978. // Right horizontal line
  979. if (ShouldDrawRightScrollIndicator ())
  980. {
  981. if (lineLength - arrowOffset > 0)
  982. {
  983. if (_host.Style.TabsOnBottom)
  984. {
  985. lc.AddLine (
  986. new Point (vts.Right, vts.Y - lastSelectedTab),
  987. lineLength - arrowOffset,
  988. Orientation.Horizontal,
  989. tab.BorderStyle
  990. );
  991. }
  992. else
  993. {
  994. lc.AddLine (
  995. new Point (
  996. vts.Right,
  997. vts.Bottom - lastSelectedTab
  998. ),
  999. lineLength - arrowOffset,
  1000. Orientation.Horizontal,
  1001. tab.BorderStyle
  1002. );
  1003. }
  1004. }
  1005. }
  1006. else
  1007. {
  1008. // Right corner
  1009. if (_host.Style.TabsOnBottom)
  1010. {
  1011. lc.AddLine (
  1012. new Point (vts.Right, vts.Y - lastSelectedTab),
  1013. lineLength,
  1014. Orientation.Horizontal,
  1015. tab.BorderStyle
  1016. );
  1017. }
  1018. else
  1019. {
  1020. lc.AddLine (
  1021. new Point (vts.Right, vts.Bottom - lastSelectedTab),
  1022. lineLength,
  1023. Orientation.Horizontal,
  1024. tab.BorderStyle
  1025. );
  1026. }
  1027. if (_host.Style.ShowBorder)
  1028. {
  1029. if (_host.Style.TabsOnBottom)
  1030. {
  1031. // More LRCorner
  1032. lc.AddLine (
  1033. new Point (
  1034. tabsBarVts.Right - 1,
  1035. vts.Y - lastSelectedTab
  1036. ),
  1037. -1,
  1038. Orientation.Vertical,
  1039. tab.BorderStyle
  1040. );
  1041. }
  1042. else
  1043. {
  1044. // More URCorner
  1045. lc.AddLine (
  1046. new Point (
  1047. tabsBarVts.Right - 1,
  1048. vts.Bottom - lastSelectedTab
  1049. ),
  1050. 1,
  1051. Orientation.Vertical,
  1052. tab.BorderStyle
  1053. );
  1054. }
  1055. }
  1056. }
  1057. }
  1058. }
  1059. _host.LineCanvas.Merge (lc);
  1060. }
  1061. private int GetUnderlineYPosition ()
  1062. {
  1063. if (_host.Style.TabsOnBottom)
  1064. {
  1065. return 0;
  1066. }
  1067. return _host.Style.ShowTopLine ? 2 : 1;
  1068. }
  1069. /// <summary>Renders the line with the tab names in it.</summary>
  1070. private void RenderTabLine ()
  1071. {
  1072. if (_host._tabLocations is null)
  1073. {
  1074. return;
  1075. }
  1076. View? selected = null;
  1077. int topLine = _host.Style.ShowTopLine ? 1 : 0;
  1078. foreach (TabToRender toRender in _host._tabLocations)
  1079. {
  1080. Tab tab = toRender.Tab;
  1081. if (toRender.IsSelected)
  1082. {
  1083. selected = tab;
  1084. if (_host.Style.TabsOnBottom)
  1085. {
  1086. tab.Border!.Thickness = new (1, 0, 1, topLine);
  1087. tab.Margin!.Thickness = new (0, 1, 0, 0);
  1088. }
  1089. else
  1090. {
  1091. tab.Border!.Thickness = new (1, topLine, 1, 0);
  1092. tab.Margin!.Thickness = new (0, 0, 0, topLine);
  1093. }
  1094. }
  1095. else if (selected is null)
  1096. {
  1097. if (_host.Style.TabsOnBottom)
  1098. {
  1099. tab.Border!.Thickness = new (1, 1, 1, topLine);
  1100. tab.Margin!.Thickness = new (0, 0, 0, 0);
  1101. }
  1102. else
  1103. {
  1104. tab.Border!.Thickness = new (1, topLine, 1, 1);
  1105. tab.Margin!.Thickness = new (0, 0, 0, 0);
  1106. }
  1107. }
  1108. else
  1109. {
  1110. if (_host.Style.TabsOnBottom)
  1111. {
  1112. tab.Border!.Thickness = new (1, 1, 1, topLine);
  1113. tab.Margin!.Thickness = new (0, 0, 0, 0);
  1114. }
  1115. else
  1116. {
  1117. tab.Border!.Thickness = new (1, topLine, 1, 1);
  1118. tab.Margin!.Thickness = new (0, 0, 0, 0);
  1119. }
  1120. }
  1121. // Ensures updating TextFormatter constrains
  1122. tab.TextFormatter.ConstrainToWidth = tab.GetContentSize ().Width;
  1123. tab.TextFormatter.ConstrainToHeight = tab.GetContentSize ().Height;
  1124. }
  1125. }
  1126. /// <summary>Renders the line of the tab that adjoins the content of the tab.</summary>
  1127. private void RenderUnderline ()
  1128. {
  1129. int y = GetUnderlineYPosition ();
  1130. TabToRender? selected = _host._tabLocations?.FirstOrDefault (t => t.IsSelected);
  1131. if (selected is null)
  1132. {
  1133. return;
  1134. }
  1135. // draw scroll indicators
  1136. // if there are more tabs to the left not visible
  1137. if (_host.TabScrollOffset > 0)
  1138. {
  1139. _leftScrollIndicator.X = 0;
  1140. _leftScrollIndicator.Y = y;
  1141. // indicate that
  1142. _leftScrollIndicator.Visible = true;
  1143. // Ensures this is clicked instead of the first tab
  1144. MoveSubviewToEnd (_leftScrollIndicator);
  1145. }
  1146. else
  1147. {
  1148. _leftScrollIndicator.Visible = false;
  1149. }
  1150. // if there are more tabs to the right not visible
  1151. if (ShouldDrawRightScrollIndicator ())
  1152. {
  1153. _rightScrollIndicator.X = Viewport.Width - 1;
  1154. _rightScrollIndicator.Y = y;
  1155. // indicate that
  1156. _rightScrollIndicator.Visible = true;
  1157. // Ensures this is clicked instead of the last tab if under this
  1158. MoveSubviewToStart (_rightScrollIndicator);
  1159. }
  1160. else
  1161. {
  1162. _rightScrollIndicator.Visible = false;
  1163. }
  1164. }
  1165. private bool ShouldDrawRightScrollIndicator () { return _host._tabLocations!.LastOrDefault ()?.Tab != _host.Tabs.LastOrDefault (); }
  1166. }
  1167. private class TabToRender
  1168. {
  1169. public TabToRender (Tab tab, bool isSelected)
  1170. {
  1171. Tab = tab;
  1172. IsSelected = isSelected;
  1173. }
  1174. /// <summary>True if the tab that is being rendered is the selected one.</summary>
  1175. /// <value></value>
  1176. public bool IsSelected { get; }
  1177. public Tab Tab { get; }
  1178. }
  1179. }