diff --git a/Terminal.Gui/Views/TabChangedEventArgs.cs b/Terminal.Gui/Views/TabChangedEventArgs.cs
deleted file mode 100644
index 0fe003f617..0000000000
--- a/Terminal.Gui/Views/TabChangedEventArgs.cs
+++ /dev/null
@@ -1,20 +0,0 @@
-namespace Terminal.Gui;
-
-/// Describes a change in
-public class TabChangedEventArgs : EventArgs
-{
- /// Documents a tab change
- ///
- ///
- public TabChangedEventArgs (Tab oldTab, Tab newTab)
- {
- OldTab = oldTab;
- NewTab = newTab;
- }
-
- /// The currently selected tab. May be null
- public Tab NewTab { get; }
-
- /// The previously selected tab. May be null
- public Tab OldTab { get; }
-}
diff --git a/Terminal.Gui/Views/TabView.cs b/Terminal.Gui/Views/TabView.cs
deleted file mode 100644
index 99d90bec28..0000000000
--- a/Terminal.Gui/Views/TabView.cs
+++ /dev/null
@@ -1,1404 +0,0 @@
-#nullable enable
-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.
- public TabView ()
- {
- CanFocus = true;
- TabStop = TabBehavior.TabStop; // Because TabView has focusable subviews, it must be a TabGroup
- _tabsBar = new TabRowView (this);
- _contentView = new View ()
- {
- //Id = "TabView._contentView",
- };
- ApplyStyleChanges ();
-
- base.Add (_tabsBar);
- base.Add (_contentView);
-
- // Things this view knows how to do
- AddCommand (Command.Left, () => SwitchTabBy (-1));
-
- AddCommand (Command.Right, () => SwitchTabBy (1));
-
- AddCommand (
- Command.LeftStart,
- () =>
- {
- TabScrollOffset = 0;
- SelectedTab = Tabs.FirstOrDefault ()!;
-
- return true;
- }
- );
-
- AddCommand (
- Command.RightEnd,
- () =>
- {
- TabScrollOffset = Tabs.Count - 1;
- SelectedTab = Tabs.LastOrDefault ()!;
-
- 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.LeftStart);
- KeyBindings.Add (Key.End, Command.RightEnd);
- 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 { })
- {
- _selectedTab.View.CanFocusChanged -= ContentViewCanFocus!;
- // remove old content
- _contentView.Remove (_selectedTab.View);
- }
- }
-
- _selectedTab = value;
-
- // add new content
- if (_selectedTab?.View != null)
- {
- _selectedTab.View.CanFocusChanged += ContentViewCanFocus!;
- _contentView.Add (_selectedTab.View);
- // _contentView.Id = $"_contentView for {_selectedTab.DisplayText}";
- }
-
- ContentViewCanFocus (null!, null!);
-
- EnsureSelectedTabIsVisible ();
-
- if (old != _selectedTab)
- {
- if (old?.HasFocus == true)
- {
- SelectedTab?.SetFocus ();
- }
-
- OnSelectedTabChanged (old!, _selectedTab!);
- }
- SetNeedsLayout ();
- }
- }
-
- private void ContentViewCanFocus (object sender, EventArgs eventArgs)
- {
- _contentView.CanFocus = _contentView.Subviews.Count (v => v.CanFocus) > 0;
- }
-
- private TabStyle _style = new ();
-
- /// Render choices for how to display tabs. After making changes, call .
- ///
- public TabStyle Style
- {
- get => _style;
- set
- {
- if (_style == value)
- {
- return;
- }
- _style = value;
- SetNeedsLayout ();
- }
- }
-
- /// 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);
- SetNeedsLayout ();
- }
- }
-
- /// 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 ();
- }
-
- SetNeedsLayout ();
- }
-
- ///
- /// 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
- }
-
- SetNeedsLayout ();
- }
-
- /// 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); }
-
- ///
- protected override void OnHasFocusChanged (bool newHasFocus, View? previousFocusedView, View? focusedView)
- {
- if (SelectedTab is { } && !_contentView.CanFocus && focusedView == this)
- {
- SelectedTab?.SetFocus ();
-
- return;
- }
-
- base.OnHasFocusChanged (newHasFocus, previousFocusedView, focusedView);
- }
-
- ///
- protected override bool OnDrawingContent ()
- {
- if (Tabs.Any ())
- {
- // Region savedClip = SetClip ();
- _tabsBar.Draw ();
- _contentView.SetNeedsDraw ();
- _contentView.Draw ();
-
- //if (Driver is { })
- //{
- // Driver.Clip = savedClip;
- //}
- }
-
- return true;
- }
-
- ///
- /// 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 ();
- SetNeedsLayout ();
- }
-
- /// 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 bool SwitchTabBy (int amount)
- {
- if (Tabs.Count == 0)
- {
- return false;
- }
-
- // if there is only one tab anyway or nothing is selected
- if (Tabs.Count == 1 || SelectedTab is null)
- {
- SelectedTab = Tabs.ElementAt (0);
-
- return SelectedTab is { };
- }
-
- int currentIdx = Tabs.IndexOf (SelectedTab);
-
- // Currently selected tab has vanished!
- if (currentIdx == -1)
- {
- SelectedTab = Tabs.ElementAt (0);
- return true;
- }
-
- int newIdx = Math.Max (0, Math.Min (currentIdx + amount, Tabs.Count - 1));
-
- if (newIdx == currentIdx)
- {
- return false;
- }
-
- SelectedTab = _tabs [newIdx];
-
- EnsureSelectedTabIsVisible ();
-
- return true;
- }
-
- ///
- /// 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 (tab, string.Empty, Equals (SelectedTab, tab));
-
- 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 (tab, text, Equals (SelectedTab, tab));
-
- 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, MouseEventArgs e)
- {
- e.Handled = _tabsBar.NewMouseEvent (e) == true;
- }
-
- 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;
- Id = "tabRowView";
-
- CanFocus = true;
- Height = 1; // BUGBUG: Views should avoid setting Height as doing so implies Frame.Size == GetContentSize ().
- 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 override bool OnMouseEvent (MouseEventArgs me)
- {
- Tab? hit = me.View as Tab;
-
- if (me.IsSingleClicked)
- {
- _host.OnTabClicked (new TabMouseEventArgs (hit, me));
-
- // user canceled click
- if (me.Handled)
- {
- return true;
- }
- }
-
- if (!me.IsSingleDoubleOrTripleClicked)
- {
- return false;
- }
-
- if (!HasFocus && CanFocus)
- {
- SetFocus ();
- }
-
- if (me.IsSingleDoubleOrTripleClicked)
- {
- 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);
-
- SetNeedsLayout ();
-
- return true;
- }
-
- if (hit is { })
- {
- _host.SelectedTab = hit;
- SetNeedsLayout ();
-
- return true;
- }
- }
-
- return false;
- }
-
- ///
- protected override bool OnClearingViewport ()
- {
- // clear any old text
- ClearViewport ();
-
- return true;
- }
-
- protected override bool OnDrawingContent ()
- {
- _host._tabLocations = _host.CalculateViewport (Viewport).ToArray ();
-
- RenderTabLine ();
-
- RenderUnderline ();
-
- SetAttribute (HasFocus ? GetFocusColor () : GetNormalColor ());
-
- return true;
- }
-
- ///
- protected override bool OnDrawingSubviews ()
- {
- // RenderTabLine ();
-
- return false;
- }
-
- protected override void OnDrawComplete ()
- {
- 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.RenderLineCanvas ();
-
- // RenderUnderline ();
- }
- }
-
- 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;
-
- if (tabLocations is null)
- {
- return;
- }
-
- View? selected = null;
- int topLine = _host.Style.ShowTopLine ? 1 : 0;
-
- 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!.GetAnchor (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!.GetAnchor (0) - 1, 1);
- }
-
- tab.Text = toRender.TextToRender;
-
- // BUGBUG: Layout should only be called from Mainloop iteration!
- Layout ();
-
- tab.DrawBorderAndPadding ();
-
- Attribute prevAttr = Driver?.GetAttribute () ?? Attribute.Default;
-
- // 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 itself 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.DrawBorderAndPadding ();
-
-
- 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
- MoveSubviewToEnd (_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
- MoveSubviewToStart (_rightScrollIndicator);
- _rightScrollIndicator.Draw ();
- }
- else
- {
- _rightScrollIndicator.Visible = false;
- }
- }
-
- private bool ShouldDrawRightScrollIndicator () { return _host._tabLocations!.LastOrDefault ()?.Tab != _host.Tabs.LastOrDefault (); }
- }
-
- private class TabToRender
- {
- public TabToRender (Tab tab, string textToRender, bool isSelected)
- {
- Tab = tab;
- IsSelected = isSelected;
- 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; }
- }
-}
diff --git a/Terminal.Gui/Views/Tab.cs b/Terminal.Gui/Views/TabView/Tab.cs
similarity index 66%
rename from Terminal.Gui/Views/Tab.cs
rename to Terminal.Gui/Views/TabView/Tab.cs
index b683b04b6f..c7afba2ce9 100644
--- a/Terminal.Gui/Views/Tab.cs
+++ b/Terminal.Gui/Views/TabView/Tab.cs
@@ -1,4 +1,6 @@
#nullable enable
+using System.Net.Security;
+
namespace Terminal.Gui;
/// A single tab in a .
@@ -9,9 +11,11 @@ public class Tab : View
/// Creates a new unnamed tab with no controls inside.
public Tab ()
{
- BorderStyle = LineStyle.Rounded;
+ BorderStyle = LineStyle.Single;
CanFocus = true;
- TabStop = TabBehavior.NoStop;
+ TabStop = TabBehavior.TabStop;
+ Width = Dim.Auto (DimAutoStyle.Text);
+ SuperViewRendersLineCanvas = true;
}
/// The text to display in a .
@@ -26,7 +30,7 @@ public string DisplayText
}
}
- /// The control to display when the tab is selected.
+ /// The View that will be made visible in the content area when the tab is selected.
///
public View? View { get; set; }
}
diff --git a/Terminal.Gui/Views/TabView/TabChangedEventArgs.cs b/Terminal.Gui/Views/TabView/TabChangedEventArgs.cs
new file mode 100644
index 0000000000..6c959867aa
--- /dev/null
+++ b/Terminal.Gui/Views/TabView/TabChangedEventArgs.cs
@@ -0,0 +1,20 @@
+namespace Terminal.Gui;
+
+/// Describes a change in
+public class TabChangedEventArgs : EventArgs
+{
+ /// Documents a tab change
+ ///
+ ///
+ public TabChangedEventArgs (int? oldTabIndex, int? newTabIndex)
+ {
+ OldTabIndex = oldTabIndex;
+ NewTabIndex = newTabIndex;
+ }
+
+ /// The currently selected tab.
+ public int? NewTabIndex { get; }
+
+ /// The previously selected tab.
+ public int? OldTabIndex{ get; }
+}
diff --git a/Terminal.Gui/Views/TabMouseEventArgs.cs b/Terminal.Gui/Views/TabView/TabMouseEventArgs.cs
similarity index 100%
rename from Terminal.Gui/Views/TabMouseEventArgs.cs
rename to Terminal.Gui/Views/TabView/TabMouseEventArgs.cs
diff --git a/Terminal.Gui/Views/TabView/TabRow.cs b/Terminal.Gui/Views/TabView/TabRow.cs
new file mode 100644
index 0000000000..bb68f25434
--- /dev/null
+++ b/Terminal.Gui/Views/TabView/TabRow.cs
@@ -0,0 +1,203 @@
+#nullable enable
+namespace Terminal.Gui;
+
+///
+/// The host of the objects for a . Scrolls the Tabs horizontally within
+/// it's Viewport (the ContentSize is set to the sum of the widths of all the Tabs). Includes two scroll buttons
+/// that are made visible when needed to enable mouse scrolling across the tabs.
+///
+public class TabRow : View
+{
+ private readonly View _leftScrollButton;
+ private readonly View _rightScrollButton;
+
+ public TabRow ()
+ {
+ Id = "tabRowView";
+
+ CanFocus = true;
+ Height = Dim.Auto ();
+ Width = Dim.Fill ();
+ SuperViewRendersLineCanvas = true;
+
+ _rightScrollButton = new View
+ {
+ Id = "rightScrollButton",
+ X = Pos.Func (() => Viewport.X + Viewport.Width - 1),
+ Y = Pos.AnchorEnd (),
+ Width = 1,
+ Height = 1,
+ Visible = true,
+ Text = Glyphs.RightArrow.ToString ()
+ };
+
+ _leftScrollButton = new View
+ {
+ Id = "leftScrollButton",
+ X = Pos.Func (() => Viewport.X),
+ Y = Pos.AnchorEnd (),
+ Width = 1,
+ Height = 1,
+ Visible = true,
+ Text = Glyphs.LeftArrow.ToString ()
+ };
+
+ Add (_rightScrollButton, _leftScrollButton);
+
+ Initialized += OnInitialized;
+ }
+
+ private void OnInitialized (object? sender, EventArgs e)
+ {
+ if (SuperView is TabView tabView)
+ {
+ _leftScrollButton.MouseClick += (o, args) =>
+ {
+ tabView.InvokeCommand (Command.ScrollLeft);
+ };
+ _rightScrollButton.MouseClick += (o, args) =>
+ {
+ tabView.InvokeCommand (Command.ScrollRight);
+ };
+ tabView.SelectedTabChanged += TabView_SelectedTabChanged;
+ }
+
+ CalcContentSize ();
+ }
+
+ private void TabView_SelectedTabChanged (object? sender, TabChangedEventArgs e)
+ {
+ _selectedTabIndex = e.NewTabIndex;
+ CalcContentSize ();
+ }
+
+ ///
+ public override void OnAdded (SuperViewChangedEventArgs e)
+ {
+ if (e.SubView is Tab tab)
+ {
+ MoveSubviewToEnd (_leftScrollButton);
+ MoveSubviewToEnd (_rightScrollButton);
+
+ tab.HasFocusChanged += TabOnHasFocusChanged;
+ tab.Selecting += Tab_Selecting;
+ }
+ CalcContentSize ();
+ }
+
+ private void Tab_Selecting (object? sender, CommandEventArgs e)
+ {
+ e.Cancel = RaiseSelecting (new CommandContext (Command.Select, null, data: Tabs.ToArray ().IndexOf (sender))) is true;
+ }
+
+ private void TabOnHasFocusChanged (object? sender, HasFocusEventArgs e)
+ {
+ //TabView? host = SuperView as TabView;
+
+ //if (host is null)
+ //{
+ // return;
+ //}
+
+
+ //if (e is { NewFocused: Tab tab, NewValue: true })
+ //{
+ // e.Cancel = RaiseSInvokeCommand (Command.Select, new CommandContext () { Data = tab }) is true;
+ //}
+ }
+
+ // TODO: This is hacky - it both calculates the content size AND changes the Adornments of the Tabs
+ // TODO: These concepts should separated into two methods.
+ public void CalcContentSize ()
+ {
+ TabView? host = SuperView as TabView;
+
+ if (host is null)
+ {
+ return;
+ }
+
+ Tab? selected = null;
+ int topLine = host!.Style.ShowTopLine ? 1 : 0;
+
+ Tab [] tabs = Tabs.ToArray ();
+
+ for (int i = 0; i < tabs.Length; i++)
+ {
+ tabs [i].Height = Dim.Fill ();
+ if (i == 0)
+ {
+ tabs [i].X = 0;
+ }
+ else
+ {
+ tabs [i].X = Pos.Right (tabs [i - 1]);
+ }
+
+ if (i == _selectedTabIndex)
+ {
+ selected = tabs [i];
+
+ if (host.Style.TabsOnBottom)
+ {
+ tabs [i].Border.Thickness = new Thickness (1, 0, 1, topLine);
+ tabs [i].Margin.Thickness = new Thickness (0, 1, 0, 0);
+ }
+ else
+ {
+ tabs [i].Border.Thickness = new Thickness (1, topLine, 1, 0);
+ tabs [i].Margin.Thickness = new Thickness (0, 0, 0, topLine);
+ }
+ }
+ else if (selected is null)
+ {
+ if (host.Style.TabsOnBottom)
+ {
+ tabs [i].Border.Thickness = new Thickness (1, 1, 0, topLine);
+ tabs [i].Margin.Thickness = new Thickness (0, 0, 0, 0);
+ }
+ else
+ {
+ tabs [i].Border.Thickness = new Thickness (1, topLine, 0, 1);
+ tabs [i].Margin.Thickness = new Thickness (0, 0, 0, 0);
+ }
+
+ //tabs [i].Width = Math.Max (tabs [i].Width!.GetAnchor (0) - 1, 1);
+ }
+ else
+ {
+ if (host.Style.TabsOnBottom)
+ {
+ tabs [i].Border.Thickness = new Thickness (0, 1, 1, topLine);
+ tabs [i].Margin.Thickness = new Thickness (0, 0, 0, 0);
+ }
+ else
+ {
+ tabs [i].Border.Thickness = new Thickness (0, topLine, 1, 1);
+ tabs [i].Margin.Thickness = new Thickness (0, 0, 0, 0);
+ }
+
+ //tabs [i].Width = Math.Max (tabs [i].Width!.GetAnchor (0) - 1, 1);
+ }
+
+ //tabs [i].Text = toRender.TextToRender;
+ }
+
+ SetContentSize (null);
+ Layout (Application.Screen.Size);
+
+ var width = 0;
+ foreach (Tab t in tabs)
+ {
+ width += t.Frame.Width;
+ }
+ SetContentSize (new (width, Viewport.Height));
+ }
+
+ ///
+ /// Gets the Subviews of this View, cast to type .
+ ///
+ public IEnumerable Tabs => Subviews.Where (v => v is Tab).Cast ();
+
+ private int? _selectedTabIndex = null;
+}
diff --git a/Terminal.Gui/Views/TabStyle.cs b/Terminal.Gui/Views/TabView/TabStyle.cs
similarity index 100%
rename from Terminal.Gui/Views/TabStyle.cs
rename to Terminal.Gui/Views/TabView/TabStyle.cs
diff --git a/Terminal.Gui/Views/TabView/TabView.cs b/Terminal.Gui/Views/TabView/TabView.cs
new file mode 100644
index 0000000000..5e92254561
--- /dev/null
+++ b/Terminal.Gui/Views/TabView/TabView.cs
@@ -0,0 +1,590 @@
+#nullable enable
+using System.Linq;
+using static Terminal.Gui.SpinnerStyle;
+using static Unix.Terminal.Delegates;
+
+namespace Terminal.Gui;
+
+/// Control that hosts multiple sub views, presenting a single one at once.
+public class TabView : View, IDesignable
+{
+ /// The default to set on new controls.
+ public const uint DefaultMaxTabTextWidth = 30;
+
+ /// This SubView is the 2 or 3 line control that represents the actual tabs themselves.
+ private readonly TabRow _tabRow;
+
+ // private TabToRender []? _tabLocations;
+
+ /// Initializes a class.
+ public TabView ()
+ {
+ CanFocus = true;
+ TabStop = TabBehavior.TabStop; // Because TabView has focusable subviews, it must be a TabGroup
+ SuperViewRendersLineCanvas = true;
+
+ Width = Dim.Fill ();
+ Height = Dim.Auto (minimumContentDim: GetTabHeight (!Style.TabsOnBottom));
+
+ _tabRow = new TabRow ();
+ _tabRow.Selecting += _tabRowView_Selecting;
+ base.Add (_tabRow);
+
+ ApplyStyleChanges ();
+
+ // Things this view knows how to do
+ AddCommand (Command.Left, () => SwitchTabBy (-1));
+
+ AddCommand (Command.Right, () => SwitchTabBy (1));
+
+ AddCommand (
+ Command.LeftStart,
+ () =>
+ {
+ FirstVisibleTabIndex = 0;
+ SelectedTabIndex = 0;
+
+ return true;
+ }
+ );
+
+ AddCommand (
+ Command.RightEnd,
+ () =>
+ {
+ FirstVisibleTabIndex = Tabs.Count - 1;
+ SelectedTabIndex = Tabs.Count - 1;
+
+ return true;
+ }
+ );
+
+ AddCommand (
+ Command.PageDown,
+ () =>
+ {
+ // FirstVisibleTabIndex += _tabLocations!.Length;
+ SelectedTabIndex = FirstVisibleTabIndex;
+
+ return true;
+ }
+ );
+
+ AddCommand (
+ Command.PageUp,
+ () =>
+ {
+ // FirstVisibleTabIndex -= _tabLocations!.Length;
+ SelectedTabIndex = FirstVisibleTabIndex;
+
+ return true;
+ }
+ );
+
+ AddCommand (Command.ScrollLeft, () =>
+ {
+ var visibleTabs = GetTabsThatCanBeVisible (Viewport).ToArray ();
+ int? first = visibleTabs.FirstOrDefault ();
+
+ if (first > 0)
+ {
+ int scroll = -_tabRow.Tabs.ToArray () [first.Value].Frame.Width;
+ _tabRow.Viewport = _tabRow.Viewport with { X = _tabRow.Viewport.X + scroll };
+ SetNeedsLayout ();
+ FirstVisibleTabIndex--;
+ return true;
+ }
+
+ return false;
+ });
+
+ AddCommand (Command.ScrollRight, () =>
+ {
+ var visibleTabs = GetTabsThatCanBeVisible (Viewport).ToArray ();
+ int? last = visibleTabs.LastOrDefault ();
+
+ if (last is { })
+ {
+ _tabRow.ScrollHorizontal (_tabRow.Tabs.ToArray () [last.Value + 1].Frame.Width);
+ SetNeedsLayout ();
+ FirstVisibleTabIndex++;
+ return true;
+ }
+
+ return false;
+ });
+
+ //// Space or single-click - Raise Selecting
+ //AddCommand (Command.Select, (ctx) =>
+ // {
+ // //if (RaiseSelecting (ctx) is true)
+ // //{
+ // // return true;
+ // //}
+
+ // if (ctx.Data is Tab tab)
+ // {
+ // int? current = SelectedTabIndex;
+ // SelectedTabIndex = _tabRowView.Tabs.ToArray ().IndexOf (tab);
+ // SetNeedsDraw ();
+
+ // // e.Cancel = HasFocus;
+ // return true;
+ // }
+
+ // return false;
+ // });
+
+ // Default keybindings for this view
+ KeyBindings.Add (Key.CursorLeft, Command.Left);
+ KeyBindings.Add (Key.CursorRight, Command.Right);
+ KeyBindings.Add (Key.Home, Command.LeftStart);
+ KeyBindings.Add (Key.End, Command.RightEnd);
+ KeyBindings.Add (Key.PageDown, Command.PageDown);
+ KeyBindings.Add (Key.PageUp, Command.PageUp);
+ }
+
+ private void _tabRowView_Selecting (object? sender, CommandEventArgs e)
+ {
+ if (e.Context.Data is int tabIndex)
+ {
+ int? current = SelectedTabIndex;
+ SelectedTabIndex = tabIndex;
+ Layout ();
+ e.Cancel = true;
+ }
+ }
+
+ ///
+ protected override void OnSubviewLayout (LayoutEventArgs args)
+ {
+ _tabRow.CalcContentSize ();
+ }
+
+ ///
+ protected override void OnSubviewsLaidOut (LayoutEventArgs args)
+ {
+ // hide all that can't fit
+ var visibleTabs = GetTabsThatCanBeVisible (Viewport).ToArray ();
+
+ for (var index = 0; index < _tabRow.Tabs.ToArray ().Length; index++)
+ {
+ Tab tab = _tabRow.Tabs.ToArray () [index];
+ tab.Visible = visibleTabs.Contains (index);
+ }
+ }
+
+ ///
+ public bool EnableForDesign ()
+ {
+ AddTab (new () { Text = "Tab_1", Id = "tab1", View = new Label { Text = "Label in Tab1" } }, false);
+ AddTab (new () { Text = "Tab _2", Id = "tab2", View = new TextField { Text = "TextField in Tab2", Width = 10 } }, false);
+ AddTab (new () { Text = "Tab _Three", Id = "tab3", View = new Label { Text = "Label in Tab3" } }, false);
+ AddTab (new () { Text = "Tab _Quattro", Id = "tab4", View = new TextField { Text = "TextField in Tab4", Width = 10 } }, false);
+
+ return true;
+ }
+
+ ///
+ /// 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;
+
+ private int? _selectedTabIndex;
+
+ /// The currently selected member of chosen by the user.
+ ///
+ public int? SelectedTabIndex
+ {
+ get => _selectedTabIndex;
+ set
+ {
+ // If value is outside the range of Tabs, throw an exception
+ if (value < 0 || value >= Tabs.Count)
+ {
+ throw new ArgumentOutOfRangeException (nameof (value), value, @"SelectedTab the range of Tabs.");
+ }
+
+ if (value == _selectedTabIndex)
+ {
+ return;
+ }
+
+ int? old = _selectedTabIndex;
+
+ // Get once to avoid multiple enumerations
+ Tab [] tabs = _tabRow.Tabs.ToArray ();
+
+ if (_selectedTabIndex is { } && tabs [_selectedTabIndex.Value].View is { })
+ {
+ Remove (tabs [_selectedTabIndex.Value].View);
+ }
+
+ _selectedTabIndex = value;
+
+ if (_selectedTabIndex is { } && tabs [_selectedTabIndex.Value].View is { })
+ {
+ Add (tabs [_selectedTabIndex.Value].View);
+ }
+
+ EnsureSelectedTabIsVisible ();
+
+ if (_selectedTabIndex is { })
+ {
+ ApplyStyleChanges ();
+
+ if (HasFocus)
+ {
+ tabs [_selectedTabIndex.Value].View.SetFocus ();
+ }
+ }
+
+ OnSelectedTabIndexChanged (old, _selectedTabIndex!);
+ SelectedTabChanged?.Invoke (this, new TabChangedEventArgs (old, _selectedTabIndex));
+ SetNeedsLayout ();
+ }
+ }
+
+ private TabStyle _style = new ();
+
+ /// Render choices for how to display tabs. After making changes, call .
+ ///
+ public TabStyle Style
+ {
+ get => _style;
+ set
+ {
+ if (_style == value)
+ {
+ return;
+ }
+
+ _style = value;
+ SetNeedsLayout ();
+ }
+ }
+
+ /// All tabs currently hosted by the control.
+ ///
+ public IReadOnlyCollection Tabs => _tabRow.Tabs.ToArray ().AsReadOnly ();
+
+ private int _firstVisibleTabIndex;
+
+ /// Gets or sets the index of first visible tab. This enables horizontal scrolling of the tabs.
+ ///
+ ///
+ /// On set, if the value is less than 0, it will be set to 0. If the value is greater than the number of tabs
+ /// it will be set to the last tab index.
+ ///
+ ///
+ public int FirstVisibleTabIndex
+ {
+ get => _firstVisibleTabIndex;
+ set
+ {
+ _firstVisibleTabIndex = Math.Max (Math.Min (value, Tabs.Count - 1), 0);
+ ;
+ SetNeedsLayout ();
+ }
+ }
+
+ /// Adds the given to .
+ ///
+ /// True to make the newly added Tab the .
+ public void AddTab (Tab tab, bool andSelect)
+ {
+ // Ok to use Subviews here instead of Tabs
+ if (_tabRow.Subviews.Contains (tab))
+ {
+ return;
+ }
+
+ // Add to the TabRowView as a subview
+ _tabRow.Add (tab);
+
+ if (_tabRow.Tabs.Count () == 1 || andSelect)
+ {
+ SelectedTabIndex = _tabRow.Tabs.Count () - 1;
+
+ EnsureSelectedTabIsVisible ();
+
+ if (HasFocus)
+ {
+ tab.View?.SetFocus ();
+ }
+ }
+
+ ApplyStyleChanges ();
+ SetNeedsLayout ();
+ }
+
+
+ ///
+ /// Removes the given from . Caller is responsible for disposing the
+ /// tab's hosted if appropriate.
+ ///
+ ///
+ public void RemoveTab (Tab? tab)
+ {
+ if (tab is null || !_tabRow.Subviews.Contains (tab))
+ {
+ return;
+ }
+
+ int idx = _tabRow.Tabs.ToArray ().IndexOf (tab);
+ if (idx == SelectedTabIndex)
+ {
+ SelectedTabIndex = null;
+ }
+
+ _tabRow.Remove (tab);
+
+ // Get once to avoid multiple enumerations
+ Tab [] tabs = _tabRow.Tabs.ToArray ();
+
+ if (SelectedTabIndex is null)
+ {
+ // Either no tab was previously selected or the selected tab was removed
+
+ // select the tab closest to the one that disappeared
+ int toSelect = Math.Max (idx - 1, 0);
+
+ if (toSelect < tabs.Length)
+ {
+ SelectedTabIndex = toSelect;
+ }
+ else
+ {
+ SelectedTabIndex = tabs.Length - 1;
+ }
+ }
+
+ if (SelectedTabIndex > tabs.Length - 1)
+ {
+ // Removing the tab, caused the selected tab to be out of range
+ SelectedTabIndex = tabs.Length - 1;
+ }
+
+ EnsureSelectedTabIsVisible ();
+ SetNeedsLayout ();
+ }
+
+ ///
+ /// Applies the settings in . This can change the dimensions of
+ /// (for rendering the selected tab's content). This method includes a call to
+ /// .
+ ///
+ public void ApplyStyleChanges ()
+ {
+ // Get once to avoid multiple enumerations
+ Tab [] tabs = _tabRow.Tabs.ToArray ();
+
+ View? selectedView = null;
+
+ if (SelectedTabIndex is { })
+ {
+ selectedView = tabs [SelectedTabIndex.Value].View;
+ }
+
+ if (selectedView is { })
+ {
+ selectedView.BorderStyle = Style.ShowBorder ? LineStyle.Single : LineStyle.None;
+ selectedView.Width = Dim.Fill ();
+ }
+
+ int tabHeight = GetTabHeight (!Style.TabsOnBottom);
+
+ if (Style.TabsOnBottom)
+ {
+ _tabRow.Height = tabHeight;
+ _tabRow.Y = Pos.AnchorEnd ();
+
+ if (selectedView is { })
+ {
+ // Tabs are along the bottom so just dodge the border
+ if (Style.ShowBorder && selectedView?.Border is { })
+ {
+ selectedView.Border.Thickness = new Thickness (1, 1, 1, 0);
+ }
+
+ // Fill client area leaving space at bottom for tabs
+ selectedView!.Y = 0;
+ selectedView.Height = Dim.Fill (tabHeight);
+ }
+ }
+ else
+ {
+ // Tabs are along the top
+ _tabRow.Height = tabHeight;
+ _tabRow.Y = 0;
+
+ if (selectedView is { })
+ {
+ if (Style.ShowBorder && selectedView.Border is { })
+ {
+ selectedView.Border.Thickness = new Thickness (1, 0, 1, 1);
+ }
+
+
+ //move content down to make space for tabs
+ selectedView.Y = Pos.Bottom (_tabRow);
+
+ // Fill client area leaving space at bottom for border
+ selectedView.Height = Dim.Fill ();
+ }
+ }
+
+ SetNeedsLayout ();
+ }
+
+ /// Updates to ensure that is visible.
+ public void EnsureSelectedTabIsVisible ()
+ {
+ if (SelectedTabIndex is null)
+ {
+ return;
+ }
+
+ // Get once to avoid multiple enumerations
+ Tab [] tabs = _tabRow.Tabs.ToArray ();
+ View? selectedView = tabs [SelectedTabIndex.Value].View;
+
+ if (selectedView is null)
+ {
+ return;
+ }
+
+ // if current viewport does not include the selected tab
+ if (!GetTabsThatCanBeVisible (Viewport).Any (r => Equals (SelectedTabIndex.Value, r)))
+ {
+ // Set scroll offset so the first tab rendered is the
+ FirstVisibleTabIndex = Math.Max (0, SelectedTabIndex.Value);
+ }
+ }
+
+ /// 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.
+ ///
+ ///
+ /// if a change was made.
+ public bool SwitchTabBy (int amount)
+ {
+
+ // Get once to avoid multiple enumerations
+ Tab [] tabs = _tabRow.Tabs.ToArray ();
+
+ if (tabs.Length == 0)
+ {
+ return false;
+ }
+
+ int? currentIdx = SelectedTabIndex;
+
+ // if there is only one tab anyway or nothing is selected
+ if (tabs.Length == 1)
+ {
+ SelectedTabIndex = 0;
+
+ return SelectedTabIndex != currentIdx;
+ }
+
+ // Currently selected tab has vanished!
+ if (currentIdx is null)
+ {
+ SelectedTabIndex = 0;
+
+ return true;
+ }
+
+ int newIdx = Math.Max (0, Math.Min (currentIdx.Value + amount, tabs.Length - 1));
+
+ if (newIdx == currentIdx)
+ {
+ return false;
+ }
+
+ SelectedTabIndex = newIdx;
+
+ return true;
+ }
+
+ /// Called when the has changed.
+ protected virtual void OnSelectedTabIndexChanged (int? oldTabIndex, int? newTabIndex) { }
+
+ /// Returns which tabs will be visible given the dimensions of the TabView, which tab is selected, and how the tabs have been scrolled.
+ /// Same as this.Frame.
+ ///
+ private IEnumerable GetTabsThatCanBeVisible (Rectangle bounds)
+ {
+ var curWidth = 1;
+ View? prevTab = null;
+
+ // Get once to avoid multiple enumerations
+ Tab [] tabs = _tabRow.Tabs.ToArray ();
+
+ // Starting at the first or scrolled to tab
+ for (int i = FirstVisibleTabIndex; i < tabs.Length; i++)
+ {
+ if (curWidth >= bounds.Width)
+ {
+ break;
+ }
+
+ if (curWidth + tabs [i].Frame.Width < bounds.Width)
+ {
+ yield return i;
+ }
+ curWidth += tabs [i].Frame.Width;
+ }
+ }
+
+ ///
+ /// 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;
+ }
+
+ ///
+ protected override void Dispose (bool disposing)
+ {
+ if (disposing)
+ {
+ // Get once to avoid multiple enumerations
+ Tab [] tabs = _tabRow.Tabs.ToArray ();
+ if (SelectedTabIndex is { })
+ {
+ Remove (tabs [SelectedTabIndex.Value].View);
+ }
+ foreach (Tab tab in tabs)
+ {
+ tab.View?.Dispose ();
+ tab.View = null;
+ }
+ };
+ base.Dispose (disposing);
+ }
+
+
+
+}
\ No newline at end of file
diff --git a/UICatalog/Scenarios/Editor.cs b/UICatalog/Scenarios/Editor.cs
index 8012011328..06862e29e6 100644
--- a/UICatalog/Scenarios/Editor.cs
+++ b/UICatalog/Scenarios/Editor.cs
@@ -743,8 +743,8 @@ private void ShowFindReplace (bool isFind = true)
_findReplaceWindow.Visible = true;
_findReplaceWindow.SuperView.MoveSubviewToStart (_findReplaceWindow);
_tabView.SetFocus ();
- _tabView.SelectedTab = isFind ? _tabView.Tabs.ToArray () [0] : _tabView.Tabs.ToArray () [1];
- _tabView.SelectedTab.View.FocusDeepest (NavigationDirection.Forward, null);
+ _tabView.SelectedTabIndex = isFind ? 0 : 1;
+ // _tabView.SelectedTabIndex.View.FocusDeepest (NavigationDirection.Forward, null);
}
private void CreateFindReplace ()
@@ -758,7 +758,7 @@ private void CreateFindReplace ()
_tabView.AddTab (new () { DisplayText = "Find", View = CreateFindTab () }, true);
_tabView.AddTab (new () { DisplayText = "Replace", View = CreateReplaceTab () }, false);
- _tabView.SelectedTabChanged += (s, e) => _tabView.SelectedTab.View.FocusDeepest (NavigationDirection.Forward, null);
+ //_tabView.SelectedTabChanged += (s, e) => _tabView.SelectedTabIndex.View.FocusDeepest (NavigationDirection.Forward, null);
_findReplaceWindow.Add (_tabView);
// _tabView.SelectedTab.View.FocusLast (null); // Hack to get the first tab to be focused
diff --git a/UICatalog/Scenarios/Images.cs b/UICatalog/Scenarios/Images.cs
index 6a7aebffc2..24cdcbe58d 100644
--- a/UICatalog/Scenarios/Images.cs
+++ b/UICatalog/Scenarios/Images.cs
@@ -293,11 +293,11 @@ private void OpenImage (object sender, CommandEventArgs e)
private void ApplyShowTabViewHack ()
{
- // TODO HACK: This hack seems to be required to make tabview actually refresh itself
- _tabView.SetNeedsDraw ();
- var orig = _tabView.SelectedTab;
- _tabView.SelectedTab = _tabView.Tabs.Except (new [] { orig }).ElementAt (0);
- _tabView.SelectedTab = orig;
+ //// TODO HACK: This hack seems to be required to make tabview actually refresh itself
+ //_tabView.SetNeedsDraw ();
+ //var orig = _tabView.SelectedTabIndex;
+ //_tabView.SelectedTabIndex = _tabView.Tabs.Except (new [] { orig }).ElementAt (0);
+ //_tabView.SelectedTabIndex = orig;
}
private void BuildBasicTab (Tab tabBasic)
diff --git a/UICatalog/Scenarios/Notepad.cs b/UICatalog/Scenarios/Notepad.cs
index 1a0871d557..ffb701b14e 100644
--- a/UICatalog/Scenarios/Notepad.cs
+++ b/UICatalog/Scenarios/Notepad.cs
@@ -98,7 +98,7 @@ public override void Main ()
Application.Shutdown ();
}
- public void Save () { Save (_focusedTabView, _focusedTabView.SelectedTab); }
+ public void Save () { Save (_focusedTabView, _focusedTabView.Tabs.ToArray () [_focusedTabView.SelectedTabIndex!.Value]); }
public void Save (TabView tabViewToSave, Tab tabToSave)
{
@@ -120,7 +120,7 @@ public void Save (TabView tabViewToSave, Tab tabToSave)
public bool SaveAs ()
{
- var tab = _focusedTabView.SelectedTab as OpenedFile;
+ var tab = _focusedTabView.Tabs.ToArray () [_focusedTabView.SelectedTabIndex!.Value] as OpenedFile;
if (tab == null)
{
@@ -153,7 +153,7 @@ public bool SaveAs ()
return true;
}
- private void Close () { Close (_focusedTabView, _focusedTabView.SelectedTab); }
+ private void Close () { Close (_focusedTabView, _focusedTabView.Tabs.ToArray()[_focusedTabView.SelectedTabIndex!.Value]); }
private void Close (TabView tv, Tab tabToClose)
{
@@ -239,7 +239,7 @@ private TabView CreateNewTabView ()
{
var tv = new TabView { X = 0, Y = 0, Width = Dim.Fill (), Height = Dim.Fill () };
- tv.TabClicked += TabView_TabClicked;
+ // tv.TabClicked += TabView_TabClicked;
tv.SelectedTabChanged += TabView_SelectedTabChanged;
tv.HasFocusChanging += (s, e) => _focusedTabView = tv;
@@ -320,9 +320,10 @@ private void Split (int offset, Orientation orientation, TabView sender, OpenedF
private void TabView_SelectedTabChanged (object sender, TabChangedEventArgs e)
{
- LenShortcut.Title = $"Len:{e.NewTab?.View?.Text?.Length ?? 0}";
+ Tab tab = _focusedTabView.Tabs.ToArray () [e.NewTabIndex!.Value];
+ LenShortcut.Title = $"Len:{tab?.View?.Text?.Length ?? 0}";
- e.NewTab?.View?.SetFocus ();
+ tab?.View?.SetFocus ();
}
private void TabView_TabClicked (object sender, TabMouseEventArgs e)
diff --git a/UICatalog/Scenarios/TabViewExample.cs b/UICatalog/Scenarios/TabViewExample.cs
index 9d48c05685..530948e1a9 100644
--- a/UICatalog/Scenarios/TabViewExample.cs
+++ b/UICatalog/Scenarios/TabViewExample.cs
@@ -35,7 +35,7 @@ public override void Main ()
new (
"_Clear SelectedTab",
"",
- () => _tabView.SelectedTab = null
+ () => _tabView.SelectedTabIndex = null
),
new ("_Quit", "", Quit)
}
@@ -129,7 +129,7 @@ public override void Main ()
);
}
- _tabView.SelectedTab = _tabView.Tabs.First ();
+ _tabView.SelectedTabIndex = 0;
appWindow.Add (_tabView);