namespace Terminal.Gui; /// Control that hosts multiple sub views, presenting a single one at once. public class TabView : View { /// The default to set on new controls. public const uint DefaultMaxTabTextWidth = 30; /// /// This sub view is the main client area of the current tab. It hosts the of the tab, the /// . /// private readonly View _contentView; private readonly List _tabs = new (); /// This sub view is the 2 or 3 line control that represents the actual tabs themselves. private readonly TabRowView _tabsBar; private Tab _selectedTab; private TabToRender [] _tabLocations; private int _tabScrollOffset; /// Initializes a class using layout. public TabView () { CanFocus = true; _tabsBar = new TabRowView (this); _contentView = new View (); ApplyStyleChanges (); base.Add (_tabsBar); base.Add (_contentView); // Things this view knows how to do AddCommand ( Command.Left, () => { SwitchTabBy (-1); return true; } ); AddCommand ( Command.Right, () => { SwitchTabBy (1); return true; } ); AddCommand ( Command.LeftHome, () => { TabScrollOffset = 0; SelectedTab = Tabs.FirstOrDefault (); return true; } ); AddCommand ( Command.RightEnd, () => { TabScrollOffset = Tabs.Count - 1; SelectedTab = Tabs.LastOrDefault (); return true; } ); AddCommand ( Command.NextView, () => { if (_contentView is { HasFocus: false }) { _contentView.SetFocus (); return true; } return false; } ); AddCommand ( Command.PreviousView, () => { SuperView?.FocusPrev (); return true; } ); AddCommand ( Command.PageDown, () => { TabScrollOffset += _tabLocations.Length; SelectedTab = Tabs.ElementAt (TabScrollOffset); return true; } ); AddCommand ( Command.PageUp, () => { TabScrollOffset -= _tabLocations.Length; SelectedTab = Tabs.ElementAt (TabScrollOffset); return true; } ); // Default keybindings for this view KeyBindings.Add (Key.CursorLeft, Command.Left); KeyBindings.Add (Key.CursorRight, Command.Right); KeyBindings.Add (Key.Home, Command.LeftHome); KeyBindings.Add (Key.End, Command.RightEnd); KeyBindings.Add (Key.CursorDown, Command.NextView); KeyBindings.Add (Key.CursorUp, Command.PreviousView); KeyBindings.Add (Key.PageDown, Command.PageDown); KeyBindings.Add (Key.PageUp, Command.PageUp); } /// /// The maximum number of characters to render in a Tab header. This prevents one long tab from pushing out all /// the others. /// public uint MaxTabTextWidth { get; set; } = DefaultMaxTabTextWidth; /// The currently selected member of chosen by the user. /// public Tab SelectedTab { get => _selectedTab; set { UnSetCurrentTabs (); Tab old = _selectedTab; if (_selectedTab is { }) { if (_selectedTab.View is { }) { // remove old content _contentView.Remove (_selectedTab.View); } } _selectedTab = value; if (value is { }) { // add new content if (_selectedTab.View is { }) { _contentView.Add (_selectedTab.View); } } EnsureSelectedTabIsVisible (); if (old != value) { if (old?.HasFocus == true) { SelectedTab.SetFocus (); } OnSelectedTabChanged (old, value); } } } /// Render choices for how to display tabs. After making changes, call . /// public TabStyle Style { get; set; } = new (); /// All tabs currently hosted by the control. /// public IReadOnlyCollection Tabs => _tabs.AsReadOnly (); /// When there are too many tabs to render, this indicates the first tab to render on the screen. /// public int TabScrollOffset { get => _tabScrollOffset; set => _tabScrollOffset = EnsureValidScrollOffsets (value); } /// Adds the given to . /// /// True to make the newly added Tab the . public void AddTab (Tab tab, bool andSelect) { if (_tabs.Contains (tab)) { return; } _tabs.Add (tab); _tabsBar.Add (tab); if (SelectedTab is null || andSelect) { SelectedTab = tab; EnsureSelectedTabIsVisible (); tab.View?.SetFocus (); } SetNeedsDisplay (); } /// /// Updates the control to use the latest state settings in . This can change the size of the /// client area of the tab (for rendering the selected tab's content). This method includes a call to /// . /// public void ApplyStyleChanges () { _contentView.BorderStyle = Style.ShowBorder ? LineStyle.Single : LineStyle.None; _contentView.Width = Dim.Fill (); if (Style.TabsOnBottom) { // Tabs are along the bottom so just dodge the border if (Style.ShowBorder) { _contentView.Border.Thickness = new Thickness (1, 1, 1, 0); } _contentView.Y = 0; int tabHeight = GetTabHeight (false); // Fill client area leaving space at bottom for tabs _contentView.Height = Dim.Fill (tabHeight); _tabsBar.Height = tabHeight; _tabsBar.Y = Pos.Bottom (_contentView); } else { // Tabs are along the top if (Style.ShowBorder) { _contentView.Border.Thickness = new Thickness (1, 0, 1, 1); } _tabsBar.Y = 0; int tabHeight = GetTabHeight (true); //move content down to make space for tabs _contentView.Y = Pos.Bottom (_tabsBar); // Fill client area leaving space at bottom for border _contentView.Height = Dim.Fill (); // The top tab should be 2 or 3 rows high and on the top _tabsBar.Height = tabHeight; // Should be able to just use 0 but switching between top/bottom tabs repeatedly breaks in ValidatePosDim if just using the absolute value 0 } if (IsInitialized) { LayoutSubviews (); } SetNeedsDisplay (); } /// Updates to ensure that is visible. public void EnsureSelectedTabIsVisible () { if (!IsInitialized || SelectedTab is null) { return; } // if current viewport does not include the selected tab if (!CalculateViewport (Viewport).Any (r => Equals (SelectedTab, r.Tab))) { // Set scroll offset so the first tab rendered is the TabScrollOffset = Math.Max (0, Tabs.IndexOf (SelectedTab)); } } /// Updates to be a valid index of . /// The value to validate. /// Changes will not be immediately visible in the display until you call . /// The valid for the given value. public int EnsureValidScrollOffsets (int value) { return Math.Max (Math.Min (value, Tabs.Count - 1), 0); } /// public override void OnDrawContent (Rectangle viewport) { Driver.SetAttribute (GetNormalColor ()); if (Tabs.Any ()) { Rectangle savedClip = ClipToViewport (); _tabsBar.OnDrawContent (viewport); _contentView.SetNeedsDisplay (); _contentView.Draw (); Driver.Clip = savedClip; } } /// public override void OnDrawContentComplete (Rectangle viewport) { _tabsBar.OnDrawContentComplete (viewport); } /// /// Removes the given from . Caller is responsible for disposing the /// tab's hosted if appropriate. /// /// public void RemoveTab (Tab tab) { if (tab is null || !_tabs.Contains (tab)) { return; } // what tab was selected before closing int idx = _tabs.IndexOf (tab); _tabs.Remove (tab); // if the currently selected tab is no longer a member of Tabs if (SelectedTab is null || !Tabs.Contains (SelectedTab)) { // select the tab closest to the one that disappeared int toSelect = Math.Max (idx - 1, 0); if (toSelect < Tabs.Count) { SelectedTab = Tabs.ElementAt (toSelect); } else { SelectedTab = Tabs.LastOrDefault (); } } EnsureSelectedTabIsVisible (); SetNeedsDisplay (); } /// Event for when changes. public event EventHandler SelectedTabChanged; /// /// Changes the by the given . Positive for right, negative for /// left. If no tab is currently selected then the first tab will become selected. /// /// public void SwitchTabBy (int amount) { if (Tabs.Count == 0) { return; } // if there is only one tab anyway or nothing is selected if (Tabs.Count == 1 || SelectedTab is null) { SelectedTab = Tabs.ElementAt (0); SetNeedsDisplay (); return; } int currentIdx = Tabs.IndexOf (SelectedTab); // Currently selected tab has vanished! if (currentIdx == -1) { SelectedTab = Tabs.ElementAt (0); SetNeedsDisplay (); return; } int newIdx = Math.Max (0, Math.Min (currentIdx + amount, Tabs.Count - 1)); SelectedTab = _tabs [newIdx]; SetNeedsDisplay (); EnsureSelectedTabIsVisible (); } /// /// Event fired when a is clicked. Can be used to cancel navigation, show context menu (e.g. on /// right click) etc. /// public event EventHandler TabClicked; /// Disposes the control and all . /// protected override void Dispose (bool disposing) { base.Dispose (disposing); // The selected tab will automatically be disposed but // any tabs not visible will need to be manually disposed foreach (Tab tab in Tabs) { if (!Equals (SelectedTab, tab)) { tab.View?.Dispose (); } } } /// Raises the event. protected virtual void OnSelectedTabChanged (Tab oldTab, Tab newTab) { SelectedTabChanged?.Invoke (this, new TabChangedEventArgs (oldTab, newTab)); } /// Returns which tabs to render at each x location. /// private IEnumerable CalculateViewport (Rectangle bounds) { UnSetCurrentTabs (); var i = 1; View prevTab = null; // Starting at the first or scrolled to tab foreach (Tab tab in Tabs.Skip (TabScrollOffset)) { if (prevTab is { }) { tab.X = Pos.Right (prevTab); } else { tab.X = 0; } tab.Y = 0; // while there is space for the tab int tabTextWidth = tab.DisplayText.EnumerateRunes ().Sum (c => c.GetColumns ()); string text = tab.DisplayText; // The maximum number of characters to use for the tab name as specified // by the user (MaxTabTextWidth). But not more than the width of the view // or we won't even be able to render a single tab! long maxWidth = Math.Max (0, Math.Min (bounds.Width - 3, MaxTabTextWidth)); prevTab = tab; tab.Width = 2; tab.Height = Style.ShowTopLine ? 3 : 2; // if tab view is width <= 3 don't render any tabs if (maxWidth == 0) { tab.Visible = true; tab.MouseClick += Tab_MouseClick; yield return new TabToRender (i, tab, string.Empty, Equals (SelectedTab, tab), 0); break; } if (tabTextWidth > maxWidth) { text = tab.DisplayText.Substring (0, (int)maxWidth); tabTextWidth = (int)maxWidth; } tab.Width = Math.Max (tabTextWidth + 2, 1); tab.Height = Style.ShowTopLine ? 3 : 2; // if there is not enough space for this tab if (i + tabTextWidth >= bounds.Width) { tab.Visible = false; break; } // there is enough space! tab.Visible = true; tab.MouseClick += Tab_MouseClick; yield return new TabToRender (i, tab, text, Equals (SelectedTab, tab), tabTextWidth); i += tabTextWidth + 1; } } /// /// Returns the number of rows occupied by rendering the tabs, this depends on /// and can be 0 (e.g. if and you ask for ). /// /// True to measure the space required at the top of the control, false to measure space at the bottom. /// . /// private int GetTabHeight (bool top) { if (top && Style.TabsOnBottom) { return 0; } if (!top && !Style.TabsOnBottom) { return 0; } return Style.ShowTopLine ? 3 : 2; } private void Tab_MouseClick (object sender, MouseEventEventArgs e) { e.Handled = _tabsBar.OnMouseEvent (e.MouseEvent); } private void UnSetCurrentTabs () { if (_tabLocations is { }) { foreach (TabToRender tabToRender in _tabLocations) { tabToRender.Tab.MouseClick -= Tab_MouseClick; tabToRender.Tab.Visible = false; } _tabLocations = null; } } /// Raises the event. /// private protected virtual void OnTabClicked (TabMouseEventArgs tabMouseEventArgs) { TabClicked?.Invoke (this, tabMouseEventArgs); } private class TabRowView : View { private readonly TabView _host; private readonly View _leftScrollIndicator; private readonly View _rightScrollIndicator; public TabRowView (TabView host) { _host = host; CanFocus = true; Height = 1; Width = Dim.Fill (); _rightScrollIndicator = new View { Id = "rightScrollIndicator", Width = 1, Height = 1, Visible = false, Text = Glyphs.RightArrow.ToString () }; _rightScrollIndicator.MouseClick += _host.Tab_MouseClick; _leftScrollIndicator = new View { Id = "leftScrollIndicator", Width = 1, Height = 1, Visible = false, Text = Glyphs.LeftArrow.ToString () }; _leftScrollIndicator.MouseClick += _host.Tab_MouseClick; Add (_rightScrollIndicator, _leftScrollIndicator); } protected internal override bool OnMouseEvent (MouseEvent me) { Tab hit = me.View is Tab ? (Tab)me.View : null; bool isClick = me.Flags.HasFlag (MouseFlags.Button1Clicked) || me.Flags.HasFlag (MouseFlags.Button2Clicked) || me.Flags.HasFlag (MouseFlags.Button3Clicked); if (isClick) { _host.OnTabClicked (new TabMouseEventArgs (hit, me)); // user canceled click if (me.Handled) { return true; } } if (!me.Flags.HasFlag (MouseFlags.Button1Clicked) && !me.Flags.HasFlag (MouseFlags.Button1DoubleClicked) && !me.Flags.HasFlag (MouseFlags.Button1TripleClicked)) { return false; } if (!HasFocus && CanFocus) { SetFocus (); } if (me.Flags.HasFlag (MouseFlags.Button1Clicked) || me.Flags.HasFlag (MouseFlags.Button1DoubleClicked) || me.Flags.HasFlag (MouseFlags.Button1TripleClicked)) { var scrollIndicatorHit = 0; if (me.View is { } && me.View.Id == "rightScrollIndicator") { scrollIndicatorHit = 1; } else if (me.View is { } && me.View.Id == "leftScrollIndicator") { scrollIndicatorHit = -1; } if (scrollIndicatorHit != 0) { _host.SwitchTabBy (scrollIndicatorHit); SetNeedsDisplay (); return true; } if (hit is { }) { _host.SelectedTab = hit; SetNeedsDisplay (); return true; } } return false; } public override void OnDrawContent (Rectangle viewport) { _host._tabLocations = _host.CalculateViewport (Viewport).ToArray (); // clear any old text Clear (viewport); RenderTabLine (); RenderUnderline (); Driver.SetAttribute (GetNormalColor ()); } public override void OnDrawContentComplete (Rectangle viewport) { if (_host._tabLocations is null) { return; } TabToRender [] tabLocations = _host._tabLocations; int selectedTab = -1; for (var i = 0; i < tabLocations.Length; i++) { View tab = tabLocations [i].Tab; Rectangle vts = tab.ViewportToScreen (tab.Viewport); var lc = new LineCanvas (); int selectedOffset = _host.Style.ShowTopLine && tabLocations [i].IsSelected ? 0 : 1; if (tabLocations [i].IsSelected) { selectedTab = i; if (i == 0 && _host.TabScrollOffset == 0) { if (_host.Style.TabsOnBottom) { // Upper left vertical line lc.AddLine ( new Point (vts.X - 1, vts.Y - 1), -1, Orientation.Vertical, tab.BorderStyle ); } else { // Lower left vertical line lc.AddLine ( new Point (vts.X - 1, vts.Bottom - selectedOffset), -1, Orientation.Vertical, tab.BorderStyle ); } } else if (i > 0 && i <= tabLocations.Length - 1) { if (_host.Style.TabsOnBottom) { // URCorner lc.AddLine ( new Point (vts.X - 1, vts.Y - 1), 1, Orientation.Vertical, tab.BorderStyle ); lc.AddLine ( new Point (vts.X - 1, vts.Y - 1), -1, Orientation.Horizontal, tab.BorderStyle ); } else { // LRCorner lc.AddLine ( new Point (vts.X - 1, vts.Bottom - selectedOffset), -1, Orientation.Vertical, tab.BorderStyle ); lc.AddLine ( new Point (vts.X - 1, vts.Bottom - selectedOffset), -1, Orientation.Horizontal, tab.BorderStyle ); } if (_host.Style.ShowTopLine) { if (_host.Style.TabsOnBottom) { // Lower left tee lc.AddLine ( new Point (vts.X - 1, vts.Bottom), -1, Orientation.Vertical, tab.BorderStyle ); lc.AddLine ( new Point (vts.X - 1, vts.Bottom), 0, Orientation.Horizontal, tab.BorderStyle ); } else { // Upper left tee lc.AddLine ( new Point (vts.X - 1, vts.Y - 1), 1, Orientation.Vertical, tab.BorderStyle ); lc.AddLine ( new Point (vts.X - 1, vts.Y - 1), 0, Orientation.Horizontal, tab.BorderStyle ); } } } if (i < tabLocations.Length - 1) { if (_host.Style.ShowTopLine) { if (_host.Style.TabsOnBottom) { // Lower right tee lc.AddLine ( new Point (vts.Right, vts.Bottom), -1, Orientation.Vertical, tab.BorderStyle ); lc.AddLine ( new Point (vts.Right, vts.Bottom), 0, Orientation.Horizontal, tab.BorderStyle ); } else { // Upper right tee lc.AddLine ( new Point (vts.Right, vts.Y - 1), 1, Orientation.Vertical, tab.BorderStyle ); lc.AddLine ( new Point (vts.Right, vts.Y - 1), 0, Orientation.Horizontal, tab.BorderStyle ); } } } if (_host.Style.TabsOnBottom) { //URCorner lc.AddLine ( new Point (vts.Right, vts.Y - 1), 1, Orientation.Vertical, tab.BorderStyle ); lc.AddLine ( new Point (vts.Right, vts.Y - 1), 1, Orientation.Horizontal, tab.BorderStyle ); } else { //LLCorner lc.AddLine ( new Point (vts.Right, vts.Bottom - selectedOffset), -1, Orientation.Vertical, tab.BorderStyle ); lc.AddLine ( new Point (vts.Right, vts.Bottom - selectedOffset), 1, Orientation.Horizontal, tab.BorderStyle ); } } else if (selectedTab == -1) { if (i == 0 && string.IsNullOrEmpty (tab.Text)) { if (_host.Style.TabsOnBottom) { if (_host.Style.ShowTopLine) { // LLCorner lc.AddLine ( new Point (vts.X - 1, vts.Bottom), -1, Orientation.Vertical, tab.BorderStyle ); lc.AddLine ( new Point (vts.X - 1, vts.Bottom), 1, Orientation.Horizontal, tab.BorderStyle ); } // ULCorner lc.AddLine ( new Point (vts.X - 1, vts.Y - 1), 1, Orientation.Vertical, tab.BorderStyle ); lc.AddLine ( new Point (vts.X - 1, vts.Y - 1), 1, Orientation.Horizontal, tab.BorderStyle ); } else { if (_host.Style.ShowTopLine) { // ULCorner lc.AddLine ( new Point (vts.X - 1, vts.Y - 1), 1, Orientation.Vertical, tab.BorderStyle ); lc.AddLine ( new Point (vts.X - 1, vts.Y - 1), 1, Orientation.Horizontal, tab.BorderStyle ); } // LLCorner lc.AddLine ( new Point (vts.X - 1, vts.Bottom), -1, Orientation.Vertical, tab.BorderStyle ); lc.AddLine ( new Point (vts.X - 1, vts.Bottom), 1, Orientation.Horizontal, tab.BorderStyle ); } } else if (i > 0) { if (_host.Style.ShowTopLine || _host.Style.TabsOnBottom) { // Upper left tee lc.AddLine ( new Point (vts.X - 1, vts.Y - 1), 1, Orientation.Vertical, tab.BorderStyle ); lc.AddLine ( new Point (vts.X - 1, vts.Y - 1), 0, Orientation.Horizontal, tab.BorderStyle ); } // Lower left tee lc.AddLine ( new Point (vts.X - 1, vts.Bottom), -1, Orientation.Vertical, tab.BorderStyle ); lc.AddLine ( new Point (vts.X - 1, vts.Bottom), 0, Orientation.Horizontal, tab.BorderStyle ); } } else if (i < tabLocations.Length - 1) { if (_host.Style.ShowTopLine) { // Upper right tee lc.AddLine ( new Point (vts.Right, vts.Y - 1), 1, Orientation.Vertical, tab.BorderStyle ); lc.AddLine ( new Point (vts.Right, vts.Y - 1), 0, Orientation.Horizontal, tab.BorderStyle ); } if (_host.Style.ShowTopLine || !_host.Style.TabsOnBottom) { // Lower right tee lc.AddLine ( new Point (vts.Right, vts.Bottom), -1, Orientation.Vertical, tab.BorderStyle ); lc.AddLine ( new Point (vts.Right, vts.Bottom), 0, Orientation.Horizontal, tab.BorderStyle ); } else { // Upper right tee lc.AddLine ( new Point (vts.Right, vts.Y - 1), 1, Orientation.Vertical, tab.BorderStyle ); lc.AddLine ( new Point (vts.Right, vts.Y - 1), 0, Orientation.Horizontal, tab.BorderStyle ); } } if (i == 0 && i != selectedTab && _host.TabScrollOffset == 0 && _host.Style.ShowBorder) { if (_host.Style.TabsOnBottom) { // Upper left vertical line lc.AddLine ( new Point (vts.X - 1, vts.Y - 1), 0, Orientation.Vertical, tab.BorderStyle ); lc.AddLine ( new Point (vts.X - 1, vts.Y - 1), 1, Orientation.Horizontal, tab.BorderStyle ); } else { // Lower left vertical line lc.AddLine ( new Point (vts.X - 1, vts.Bottom), 0, Orientation.Vertical, tab.BorderStyle ); lc.AddLine ( new Point (vts.X - 1, vts.Bottom), 1, Orientation.Horizontal, tab.BorderStyle ); } } if (i == tabLocations.Length - 1 && i != selectedTab) { if (_host.Style.TabsOnBottom) { // Upper right tee lc.AddLine ( new Point (vts.Right, vts.Y - 1), 1, Orientation.Vertical, tab.BorderStyle ); lc.AddLine ( new Point (vts.Right, vts.Y - 1), 0, Orientation.Horizontal, tab.BorderStyle ); } else { // Lower right tee lc.AddLine ( new Point (vts.Right, vts.Bottom), -1, Orientation.Vertical, tab.BorderStyle ); lc.AddLine ( new Point (vts.Right, vts.Bottom), 0, Orientation.Horizontal, tab.BorderStyle ); } } if (i == tabLocations.Length - 1) { var arrowOffset = 1; int lastSelectedTab = !_host.Style.ShowTopLine && i == selectedTab ? 1 : _host.Style.TabsOnBottom ? 1 : 0; Rectangle tabsBarVts = ViewportToScreen (Viewport); int lineLength = tabsBarVts.Right - vts.Right; // Right horizontal line if (ShouldDrawRightScrollIndicator ()) { if (lineLength - arrowOffset > 0) { if (_host.Style.TabsOnBottom) { lc.AddLine ( new Point (vts.Right, vts.Y - lastSelectedTab), lineLength - arrowOffset, Orientation.Horizontal, tab.BorderStyle ); } else { lc.AddLine ( new Point ( vts.Right, vts.Bottom - lastSelectedTab ), lineLength - arrowOffset, Orientation.Horizontal, tab.BorderStyle ); } } } else { if (_host.Style.TabsOnBottom) { lc.AddLine ( new Point (vts.Right, vts.Y - lastSelectedTab), lineLength, Orientation.Horizontal, tab.BorderStyle ); } else { lc.AddLine ( new Point (vts.Right, vts.Bottom - lastSelectedTab), lineLength, Orientation.Horizontal, tab.BorderStyle ); } if (_host.Style.ShowBorder) { if (_host.Style.TabsOnBottom) { // More LRCorner lc.AddLine ( new Point ( tabsBarVts.Right - 1, vts.Y - lastSelectedTab ), -1, Orientation.Vertical, tab.BorderStyle ); } else { // More URCorner lc.AddLine ( new Point ( tabsBarVts.Right - 1, vts.Bottom - lastSelectedTab ), 1, Orientation.Vertical, tab.BorderStyle ); } } } } tab.LineCanvas.Merge (lc); tab.OnRenderLineCanvas (); } } public override bool OnEnter (View view) { Driver.SetCursorVisibility (CursorVisibility.Invisible); return base.OnEnter (view); } private int GetUnderlineYPosition () { if (_host.Style.TabsOnBottom) { return 0; } return _host.Style.ShowTopLine ? 2 : 1; } /// Renders the line with the tab names in it. private void RenderTabLine () { TabToRender [] tabLocations = _host._tabLocations; int y; if (_host.Style.TabsOnBottom) { y = 1; } else { y = _host.Style.ShowTopLine ? 1 : 0; } View selected = null; int topLine = _host.Style.ShowTopLine ? 1 : 0; int width = Viewport.Width; foreach (TabToRender toRender in tabLocations) { Tab tab = toRender.Tab; if (toRender.IsSelected) { selected = tab; if (_host.Style.TabsOnBottom) { tab.Border.Thickness = new Thickness (1, 0, 1, topLine); tab.Margin.Thickness = new Thickness (0, 1, 0, 0); } else { tab.Border.Thickness = new Thickness (1, topLine, 1, 0); tab.Margin.Thickness = new Thickness (0, 0, 0, topLine); } } else if (selected is null) { if (_host.Style.TabsOnBottom) { tab.Border.Thickness = new Thickness (1, 1, 0, topLine); tab.Margin.Thickness = new Thickness (0, 0, 0, 0); } else { tab.Border.Thickness = new Thickness (1, topLine, 0, 1); tab.Margin.Thickness = new Thickness (0, 0, 0, 0); } tab.Width = Math.Max (tab.Width.Anchor (0) - 1, 1); } else { if (_host.Style.TabsOnBottom) { tab.Border.Thickness = new Thickness (0, 1, 1, topLine); tab.Margin.Thickness = new Thickness (0, 0, 0, 0); } else { tab.Border.Thickness = new Thickness (0, topLine, 1, 1); tab.Margin.Thickness = new Thickness (0, 0, 0, 0); } tab.Width = Math.Max (tab.Width.Anchor (0) - 1, 1); } tab.Text = toRender.TextToRender; LayoutSubviews (); tab.OnDrawAdornments (); Attribute prevAttr = Driver.GetAttribute (); // if tab is the selected one and focus is inside this control if (toRender.IsSelected && _host.HasFocus) { if (_host.Focused == this) { // if focus is the tab bar ourself then show that they can switch tabs prevAttr = ColorScheme.HotFocus; } else { // Focus is inside the tab prevAttr = ColorScheme.HotNormal; } } tab.TextFormatter.Draw ( tab.ViewportToScreen (tab.Viewport), prevAttr, ColorScheme.HotNormal ); tab.OnRenderLineCanvas (); Driver.SetAttribute (GetNormalColor ()); } } /// Renders the line of the tab that adjoins the content of the tab. private void RenderUnderline () { int y = GetUnderlineYPosition (); TabToRender selected = _host._tabLocations.FirstOrDefault (t => t.IsSelected); if (selected is null) { return; } // draw scroll indicators // if there are more tabs to the left not visible if (_host.TabScrollOffset > 0) { _leftScrollIndicator.X = 0; _leftScrollIndicator.Y = y; // indicate that _leftScrollIndicator.Visible = true; // Ensures this is clicked instead of the first tab BringSubviewToFront (_leftScrollIndicator); _leftScrollIndicator.Draw (); } else { _leftScrollIndicator.Visible = false; } // if there are more tabs to the right not visible if (ShouldDrawRightScrollIndicator ()) { _rightScrollIndicator.X = Viewport.Width - 1; _rightScrollIndicator.Y = y; // indicate that _rightScrollIndicator.Visible = true; // Ensures this is clicked instead of the last tab if under this BringSubviewToFront (_rightScrollIndicator); _rightScrollIndicator.Draw (); } else { _rightScrollIndicator.Visible = false; } } private bool ShouldDrawRightScrollIndicator () { return _host._tabLocations.LastOrDefault ()?.Tab != _host.Tabs.LastOrDefault (); } } private class TabToRender { public TabToRender (int x, Tab tab, string textToRender, bool isSelected, int width) { X = x; Tab = tab; IsSelected = isSelected; Width = width; TextToRender = textToRender; } /// True if the tab that is being rendered is the selected one. /// public bool IsSelected { get; } public Tab Tab { get; } public string TextToRender { get; } public int Width { get; } public int X { get; set; } } }