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; }
}
}