Browse Source

Merge branch 'v2_3841-ConfigManager' of tig:tig/Terminal.Gui into v2_3841-ConfigManager

Tig 8 months ago
parent
commit
236c6227c7

+ 7 - 2
Terminal.Gui/ConsoleDrivers/WindowsDriver.cs

@@ -1725,8 +1725,13 @@ internal class WindowsDriver : ConsoleDriver
                 }
                 }
             }
             }
 
 
-            // Return the Key (not KeyChar!)
-            return (KeyCode)keyInfo.Key;
+            // If KeyInfo.Key is A...Z and KeyInfo.KeyChar is not a...z then we have an accent.
+            // See https://github.com/gui-cs/Terminal.Gui/issues/3807#issuecomment-2455997595
+            if (keyInfo.KeyChar <= (char)'z')
+            {
+                return (KeyCode)keyInfo.Key;
+            }
+            return (KeyCode)keyInfo.KeyChar;
         }
         }
 
 
         // Handle control keys whose VK codes match the related ASCII value (those below ASCII 33) like ESC
         // Handle control keys whose VK codes match the related ASCII value (those below ASCII 33) like ESC

+ 0 - 1404
Terminal.Gui/Views/TabView.cs

@@ -1,1404 +0,0 @@
-#nullable enable
-namespace Terminal.Gui;
-
-/// <summary>Control that hosts multiple sub views, presenting a single one at once.</summary>
-public class TabView : View
-{
-    /// <summary>The default <see cref="MaxTabTextWidth"/> to set on new <see cref="TabView"/> controls.</summary>
-    public const uint DefaultMaxTabTextWidth = 30;
-
-    /// <summary>
-    ///     This sub view is the main client area of the current tab.  It hosts the <see cref="Tab.View"/> of the tab, the
-    ///     <see cref="SelectedTab"/>.
-    /// </summary>
-    private readonly View _contentView;
-
-    private readonly List<Tab> _tabs = new ();
-
-    /// <summary>This sub view is the 2 or 3 line control that represents the actual tabs themselves.</summary>
-    private readonly TabRowView _tabsBar;
-
-    private Tab? _selectedTab;
-    private TabToRender []? _tabLocations;
-    private int _tabScrollOffset;
-
-    /// <summary>Initializes a <see cref="TabView"/> class.</summary>
-    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);
-    }
-
-    /// <summary>
-    ///     The maximum number of characters to render in a Tab header.  This prevents one long tab from pushing out all
-    ///     the others.
-    /// </summary>
-    public uint MaxTabTextWidth { get; set; } = DefaultMaxTabTextWidth;
-
-    /// <summary>The currently selected member of <see cref="Tabs"/> chosen by the user.</summary>
-    /// <value></value>
-    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 ();
-
-    /// <summary>Render choices for how to display tabs.  After making changes, call <see cref="ApplyStyleChanges()"/>.</summary>
-    /// <value></value>
-    public TabStyle Style
-    {
-        get => _style;
-        set
-        {
-            if (_style == value)
-            {
-                return;
-            }
-            _style = value;
-            SetNeedsLayout ();
-        }
-    }
-
-    /// <summary>All tabs currently hosted by the control.</summary>
-    /// <value></value>
-    public IReadOnlyCollection<Tab> Tabs => _tabs.AsReadOnly ();
-
-    /// <summary>When there are too many tabs to render, this indicates the first tab to render on the screen.</summary>
-    /// <value></value>
-    public int TabScrollOffset
-    {
-        get => _tabScrollOffset;
-        set
-        {
-            _tabScrollOffset = EnsureValidScrollOffsets (value);
-            SetNeedsLayout ();
-        }
-    }
-
-    /// <summary>Adds the given <paramref name="tab"/> to <see cref="Tabs"/>.</summary>
-    /// <param name="tab"></param>
-    /// <param name="andSelect">True to make the newly added Tab the <see cref="SelectedTab"/>.</param>
-    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 ();
-    }
-
-    /// <summary>
-    ///     Updates the control to use the latest state settings in <see cref="Style"/>. 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
-    ///     <see cref="View.SetNeedsDraw()"/>.
-    /// </summary>
-    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 ();
-    }
-
-    /// <summary>Updates <see cref="TabScrollOffset"/> to ensure that <see cref="SelectedTab"/> is visible.</summary>
-    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));
-        }
-    }
-
-    /// <summary>Updates <see cref="TabScrollOffset"/> to be a valid index of <see cref="Tabs"/>.</summary>
-    /// <param name="value">The value to validate.</param>
-    /// <remarks>Changes will not be immediately visible in the display until you call <see cref="View.SetNeedsDraw()"/>.</remarks>
-    /// <returns>The valid <see cref="TabScrollOffset"/> for the given value.</returns>
-    public int EnsureValidScrollOffsets (int value) { return Math.Max (Math.Min (value, Tabs.Count - 1), 0); }
-
-    /// <inheritdoc />
-    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);
-    }
-
-    /// <inheritdoc/>
-    protected override bool OnDrawingContent ()
-    {
-        if (Tabs.Any ())
-        {
-            // Region savedClip = SetClip ();
-            _tabsBar.Draw ();
-            _contentView.SetNeedsDraw ();
-            _contentView.Draw ();
-
-            //if (Driver is { })
-            //{
-            //    Driver.Clip = savedClip;
-            //}
-        }
-
-        return true;
-    }
-
-    /// <summary>
-    ///     Removes the given <paramref name="tab"/> from <see cref="Tabs"/>. Caller is responsible for disposing the
-    ///     tab's hosted <see cref="Tab.View"/> if appropriate.
-    /// </summary>
-    /// <param name="tab"></param>
-    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 ();
-    }
-
-    /// <summary>Event for when <see cref="SelectedTab"/> changes.</summary>
-    public event EventHandler<TabChangedEventArgs>? SelectedTabChanged;
-
-    /// <summary>
-    ///     Changes the <see cref="SelectedTab"/> by the given <paramref name="amount"/>. Positive for right, negative for
-    ///     left.  If no tab is currently selected then the first tab will become selected.
-    /// </summary>
-    /// <param name="amount"></param>
-    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;
-    }
-
-    /// <summary>
-    ///     Event fired when a <see cref="Tab"/> is clicked.  Can be used to cancel navigation, show context menu (e.g. on
-    ///     right click) etc.
-    /// </summary>
-    public event EventHandler<TabMouseEventArgs>? TabClicked;
-
-    /// <summary>Disposes the control and all <see cref="Tabs"/>.</summary>
-    /// <param name="disposing"></param>
-    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 ();
-            }
-        }
-    }
-
-    /// <summary>Raises the <see cref="SelectedTabChanged"/> event.</summary>
-    protected virtual void OnSelectedTabChanged (Tab oldTab, Tab newTab)
-    {
-        SelectedTabChanged?.Invoke (this, new TabChangedEventArgs (oldTab, newTab));
-    }
-
-    /// <summary>Returns which tabs to render at each x location.</summary>
-    /// <returns></returns>
-    private IEnumerable<TabToRender> 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;
-        }
-    }
-
-    /// <summary>
-    ///     Returns the number of rows occupied by rendering the tabs, this depends on <see cref="TabStyle.ShowTopLine"/>
-    ///     and can be 0 (e.g. if <see cref="TabStyle.TabsOnBottom"/> and you ask for <paramref name="top"/>).
-    /// </summary>
-    /// <param name="top">True to measure the space required at the top of the control, false to measure space at the bottom.</param>
-    /// .
-    /// <returns></returns>
-    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;
-        }
-    }
-
-    /// <summary>Raises the <see cref="TabClicked"/> event.</summary>
-    /// <param name="tabMouseEventArgs"></param>
-    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;
-        }
-
-        /// <inheritdoc />
-        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;
-        }
-
-        /// <inheritdoc />
-        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;
-        }
-
-        /// <summary>Renders the line with the tab names in it.</summary>
-        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 ());
-            }
-        }
-
-        /// <summary>Renders the line of the tab that adjoins the content of the tab.</summary>
-        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;
-        }
-
-        /// <summary>True if the tab that is being rendered is the selected one.</summary>
-        /// <value></value>
-        public bool IsSelected { get; }
-
-        public Tab Tab { get; }
-        public string TextToRender { get; }
-    }
-}

+ 1 - 1
Terminal.Gui/Views/Tab.cs → Terminal.Gui/Views/TabView/Tab.cs

@@ -22,7 +22,7 @@ public class Tab : View
         set
         set
         {
         {
             _displayText = value;
             _displayText = value;
-            SetNeedsDraw ();
+            SetNeedsLayout ();
         }
         }
     }
     }
 
 

+ 0 - 0
Terminal.Gui/Views/TabChangedEventArgs.cs → Terminal.Gui/Views/TabView/TabChangedEventArgs.cs


+ 0 - 0
Terminal.Gui/Views/TabMouseEventArgs.cs → Terminal.Gui/Views/TabView/TabMouseEventArgs.cs


+ 793 - 0
Terminal.Gui/Views/TabView/TabRowView.cs

@@ -0,0 +1,793 @@
+#nullable enable
+namespace Terminal.Gui;
+
+internal 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;
+        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)
+    {
+        View? parent = me.View is Adornment adornment ? adornment.Parent : me.View;
+        Tab? hit = parent as Tab;
+
+        if (me.IsSingleClicked)
+        {
+            _host.OnTabClicked (new TabMouseEventArgs (hit!, me));
+
+            // user canceled click
+            if (me.Handled)
+            {
+                return true;
+            }
+
+            if (parent == _host.SelectedTab)
+            {
+                _host.SelectedTab?.SetFocus ();
+            }
+        }
+
+        if (!me.IsSingleDoubleOrTripleClicked)
+        {
+            return false;
+        }
+
+        if (!HasFocus && CanFocus)
+        {
+            SetFocus ();
+        }
+
+        if (me.IsSingleDoubleOrTripleClicked)
+        {
+            var scrollIndicatorHit = 0;
+
+            if (me.View is { Id: "rightScrollIndicator" })
+            {
+                scrollIndicatorHit = 1;
+            }
+            else if (me.View is { Id: "leftScrollIndicator" })
+            {
+                scrollIndicatorHit = -1;
+            }
+
+            if (scrollIndicatorHit != 0)
+            {
+                _host.SwitchTabBy (scrollIndicatorHit);
+
+                return true;
+            }
+
+            if (hit is { })
+            {
+                _host.SelectedTab = hit;
+
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    /// <inheritdoc />
+    protected override void OnHasFocusChanged (bool newHasFocus, View? previousFocusedView, View? focusedView)
+    {
+        if (_host.SelectedTab is { HasFocus: false, CanFocus: true } && focusedView == this)
+        {
+            _host.SelectedTab?.SetFocus ();
+
+            return;
+        }
+
+        base.OnHasFocusChanged (newHasFocus, previousFocusedView, focusedView);
+    }
+
+    /// <inheritdoc/>
+    protected override void OnSubviewLayout (LayoutEventArgs args)
+    {
+        _host._tabLocations = _host.CalculateViewport (Viewport).ToArray ();
+
+        RenderTabLine ();
+
+        RenderUnderline ();
+
+        base.OnSubviewLayout (args);
+    }
+
+    /// <inheritdoc />
+    protected override bool OnRenderingLineCanvas ()
+    {
+        RenderTabLineCanvas ();
+
+        return false;
+    }
+
+    private void RenderTabLineCanvas ()
+    {
+        if (_host._tabLocations is null)
+        {
+            return;
+        }
+
+        Tab [] tabLocations = _host._tabLocations;
+        int selectedTab = -1;
+        var lc = new LineCanvas ();
+
+        for (var i = 0; i < tabLocations.Length; i++)
+        {
+            View tab = tabLocations [i];
+            Rectangle vts = tab.ViewportToScreen (tab.Viewport);
+            int selectedOffset = _host.Style.ShowTopLine && tabLocations [i] == _host.SelectedTab ? 0 : 1;
+
+            if (tabLocations [i] == _host.SelectedTab)
+            {
+                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 is { TabScrollOffset: 0, Style.ShowBorder: true })
+            {
+                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
+                {
+                    // Right corner
+                    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
+                                       );
+                        }
+                    }
+                }
+            }
+        }
+
+        _host.LineCanvas.Merge (lc);
+    }
+
+    private int GetUnderlineYPosition ()
+    {
+        if (_host.Style.TabsOnBottom)
+        {
+            return 0;
+        }
+
+        return _host.Style.ShowTopLine ? 2 : 1;
+    }
+
+    /// <summary>Renders the line with the tab names in it.</summary>
+    private void RenderTabLine ()
+    {
+        if (_host._tabLocations is null)
+        {
+            return;
+        }
+
+        View? selected = null;
+        int topLine = _host.Style.ShowTopLine ? 1 : 0;
+
+        foreach (Tab toRender in _host._tabLocations)
+        {
+            Tab tab = toRender;
+
+            if (toRender == _host.SelectedTab)
+            {
+                selected = tab;
+
+                if (_host.Style.TabsOnBottom)
+                {
+                    tab.Border!.Thickness = new (1, 0, 1, topLine);
+                    tab.Margin!.Thickness = new (0, 1, 0, 0);
+                }
+                else
+                {
+                    tab.Border!.Thickness = new (1, topLine, 1, 0);
+                    tab.Margin!.Thickness = new (0, 0, 0, topLine);
+                }
+            }
+            else if (selected is null)
+            {
+                if (_host.Style.TabsOnBottom)
+                {
+                    tab.Border!.Thickness = new (1, 1, 1, topLine);
+                    tab.Margin!.Thickness = new (0, 0, 0, 0);
+                }
+                else
+                {
+                    tab.Border!.Thickness = new (1, topLine, 1, 1);
+                    tab.Margin!.Thickness = new (0, 0, 0, 0);
+                }
+            }
+            else
+            {
+                if (_host.Style.TabsOnBottom)
+                {
+                    tab.Border!.Thickness = new (1, 1, 1, topLine);
+                    tab.Margin!.Thickness = new (0, 0, 0, 0);
+                }
+                else
+                {
+                    tab.Border!.Thickness = new (1, topLine, 1, 1);
+                    tab.Margin!.Thickness = new (0, 0, 0, 0);
+                }
+            }
+
+            // Ensures updating TextFormatter constrains
+            tab.TextFormatter.ConstrainToWidth = tab.GetContentSize ().Width;
+            tab.TextFormatter.ConstrainToHeight = tab.GetContentSize ().Height;
+        }
+    }
+
+    /// <summary>Renders the line of the tab that adjoins the content of the tab.</summary>
+    private void RenderUnderline ()
+    {
+        int y = GetUnderlineYPosition ();
+
+        Tab? selected = _host._tabLocations?.FirstOrDefault (t => t == _host.SelectedTab);
+
+        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);
+        }
+        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);
+        }
+        else
+        {
+            _rightScrollIndicator.Visible = false;
+        }
+    }
+
+    private bool ShouldDrawRightScrollIndicator () { return _host._tabLocations!.LastOrDefault () != _host.Tabs.LastOrDefault (); }
+}

+ 0 - 0
Terminal.Gui/Views/TabStyle.cs → Terminal.Gui/Views/TabView/TabStyle.cs


+ 585 - 0
Terminal.Gui/Views/TabView/TabView.cs

@@ -0,0 +1,585 @@
+#nullable enable
+namespace Terminal.Gui;
+
+/// <summary>Control that hosts multiple sub views, presenting a single one at once.</summary>
+public class TabView : View
+{
+    /// <summary>The default <see cref="MaxTabTextWidth"/> to set on new <see cref="TabView"/> controls.</summary>
+    public const uint DefaultMaxTabTextWidth = 30;
+
+    /// <summary>
+    ///     This sub view is the main client area of the current tab.  It hosts the <see cref="Tab.View"/> of the tab, the
+    ///     <see cref="SelectedTab"/>.
+    /// </summary>
+    private readonly View _containerView;
+
+    private readonly List<Tab> _tabs = new ();
+
+    /// <summary>This sub view is the 2 or 3 line control that represents the actual tabs themselves.</summary>
+    private readonly TabRowView _tabsBar;
+
+    private Tab? _selectedTab;
+
+    internal Tab []? _tabLocations;
+    private int _tabScrollOffset;
+
+    /// <summary>Initializes a <see cref="TabView"/> class.</summary>
+    public TabView ()
+    {
+        CanFocus = true;
+        TabStop = TabBehavior.TabStop; // Because TabView has focusable subviews, it must be a TabGroup
+        _tabsBar = new TabRowView (this);
+        _containerView = new ();
+        ApplyStyleChanges ();
+
+        base.Add (_tabsBar);
+        base.Add (_containerView);
+
+        // 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);
+    }
+
+    /// <summary>
+    ///     The maximum number of characters to render in a Tab header.  This prevents one long tab from pushing out all
+    ///     the others.
+    /// </summary>
+    public uint MaxTabTextWidth { get; set; } = DefaultMaxTabTextWidth;
+
+    // This is needed to hold initial value because it may change during the setter process
+    private bool _selectedTabHasFocus;
+
+    /// <summary>The currently selected member of <see cref="Tabs"/> chosen by the user.</summary>
+    /// <value></value>
+    public Tab? SelectedTab
+    {
+        get => _selectedTab;
+        set
+        {
+            if (value == _selectedTab)
+            {
+                return;
+            }
+
+            Tab? old = _selectedTab;
+            _selectedTabHasFocus = old is { } && (old.HasFocus || !_containerView.CanFocus);
+
+            if (_selectedTab is { })
+            {
+                if (_selectedTab.View is { })
+                {
+                    _selectedTab.View.CanFocusChanged -= ContainerViewCanFocus!;
+                    // remove old content
+                    _containerView.Remove (_selectedTab.View);
+                }
+            }
+
+            _selectedTab = value;
+
+            // add new content
+            if (_selectedTab?.View != null)
+            {
+                _selectedTab.View.CanFocusChanged += ContainerViewCanFocus!;
+                _containerView.Add (_selectedTab.View);
+            }
+
+            ContainerViewCanFocus (null!, null!);
+
+            EnsureSelectedTabIsVisible ();
+
+            if (old != _selectedTab)
+            {
+                if (TabCanSetFocus ())
+                {
+                    SelectedTab?.SetFocus ();
+                }
+
+                OnSelectedTabChanged (old!, _selectedTab!);
+            }
+            SetNeedsLayout ();
+        }
+    }
+
+    private bool TabCanSetFocus ()
+    {
+        return IsInitialized && SelectedTab is { } && (_selectedTabHasFocus || !_containerView.CanFocus);
+    }
+
+    private void ContainerViewCanFocus (object sender, EventArgs eventArgs)
+    {
+        _containerView.CanFocus = _containerView.Subviews.Count (v => v.CanFocus) > 0;
+    }
+
+    private TabStyle _style = new ();
+
+    /// <summary>Render choices for how to display tabs.  After making changes, call <see cref="ApplyStyleChanges()"/>.</summary>
+    /// <value></value>
+    public TabStyle Style
+    {
+        get => _style;
+        set
+        {
+            if (_style == value)
+            {
+                return;
+            }
+            _style = value;
+            SetNeedsLayout ();
+        }
+    }
+
+    /// <summary>All tabs currently hosted by the control.</summary>
+    /// <value></value>
+    public IReadOnlyCollection<Tab> Tabs => _tabs.AsReadOnly ();
+
+    /// <summary>When there are too many tabs to render, this indicates the first tab to render on the screen.</summary>
+    /// <value></value>
+    public int TabScrollOffset
+    {
+        get => _tabScrollOffset;
+        set
+        {
+            _tabScrollOffset = EnsureValidScrollOffsets (value);
+            SetNeedsLayout ();
+        }
+    }
+
+    /// <summary>Adds the given <paramref name="tab"/> to <see cref="Tabs"/>.</summary>
+    /// <param name="tab"></param>
+    /// <param name="andSelect">True to make the newly added Tab the <see cref="SelectedTab"/>.</param>
+    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 ();
+    }
+
+    /// <summary>
+    ///     Updates the control to use the latest state settings in <see cref="Style"/>. 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
+    ///     <see cref="View.SetNeedsDraw()"/>.
+    /// </summary>
+    public void ApplyStyleChanges ()
+    {
+        _containerView.BorderStyle = Style.ShowBorder ? LineStyle.Single : LineStyle.None;
+        _containerView.Width = Dim.Fill ();
+
+        if (Style.TabsOnBottom)
+        {
+            // Tabs are along the bottom so just dodge the border
+            if (Style.ShowBorder)
+            {
+                _containerView.Border!.Thickness = new Thickness (1, 1, 1, 0);
+            }
+
+            _containerView.Y = 0;
+
+            int tabHeight = GetTabHeight (false);
+
+            // Fill client area leaving space at bottom for tabs
+            _containerView.Height = Dim.Fill (tabHeight);
+
+            _tabsBar.Height = tabHeight;
+
+            _tabsBar.Y = Pos.Bottom (_containerView);
+        }
+        else
+        {
+            // Tabs are along the top
+            if (Style.ShowBorder)
+            {
+                _containerView.Border!.Thickness = new Thickness (1, 0, 1, 1);
+            }
+
+            _tabsBar.Y = 0;
+
+            int tabHeight = GetTabHeight (true);
+
+            //move content down to make space for tabs
+            _containerView.Y = Pos.Bottom (_tabsBar);
+
+            // Fill client area leaving space at bottom for border
+            _containerView.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 ();
+    }
+
+    /// <inheritdoc />
+    protected override void OnViewportChanged (DrawEventArgs e)
+    {
+        _tabLocations = CalculateViewport (Viewport).ToArray ();
+
+        base.OnViewportChanged (e);
+    }
+
+    /// <summary>Updates <see cref="TabScrollOffset"/> to ensure that <see cref="SelectedTab"/> is visible.</summary>
+    public void EnsureSelectedTabIsVisible ()
+    {
+        if (!IsInitialized || SelectedTab is null)
+        {
+            return;
+        }
+
+        // if current viewport does not include the selected tab
+        if (!CalculateViewport (Viewport).Any (t => Equals (SelectedTab, t)))
+        {
+            // Set scroll offset so the first tab rendered is the
+            TabScrollOffset = Math.Max (0, Tabs.IndexOf (SelectedTab));
+        }
+    }
+
+    /// <summary>Updates <see cref="TabScrollOffset"/> to be a valid index of <see cref="Tabs"/>.</summary>
+    /// <param name="value">The value to validate.</param>
+    /// <remarks>Changes will not be immediately visible in the display until you call <see cref="View.SetNeedsDraw()"/>.</remarks>
+    /// <returns>The valid <see cref="TabScrollOffset"/> for the given value.</returns>
+    public int EnsureValidScrollOffsets (int value) { return Math.Max (Math.Min (value, Tabs.Count - 1), 0); }
+
+    /// <inheritdoc />
+    protected override void OnHasFocusChanged (bool newHasFocus, View? previousFocusedView, View? focusedView)
+    {
+        if (SelectedTab is { HasFocus: false } && !_containerView.CanFocus && focusedView == this)
+        {
+            SelectedTab?.SetFocus ();
+
+            return;
+        }
+
+        base.OnHasFocusChanged (newHasFocus, previousFocusedView, focusedView);
+    }
+
+    /// <summary>
+    ///     Removes the given <paramref name="tab"/> from <see cref="Tabs"/>. Caller is responsible for disposing the
+    ///     tab's hosted <see cref="Tab.View"/> if appropriate.
+    /// </summary>
+    /// <param name="tab"></param>
+    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 ();
+    }
+
+    /// <summary>Event for when <see cref="SelectedTab"/> changes.</summary>
+    public event EventHandler<TabChangedEventArgs>? SelectedTabChanged;
+
+    /// <summary>
+    ///     Changes the <see cref="SelectedTab"/> by the given <paramref name="amount"/>. Positive for right, negative for
+    ///     left.  If no tab is currently selected then the first tab will become selected.
+    /// </summary>
+    /// <param name="amount"></param>
+    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;
+    }
+
+    /// <summary>
+    ///     Event fired when a <see cref="Tab"/> is clicked.  Can be used to cancel navigation, show context menu (e.g. on
+    ///     right click) etc.
+    /// </summary>
+    public event EventHandler<TabMouseEventArgs>? TabClicked;
+
+    /// <summary>Disposes the control and all <see cref="Tabs"/>.</summary>
+    /// <param name="disposing"></param>
+    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 ();
+            }
+        }
+    }
+
+    /// <summary>Raises the <see cref="SelectedTabChanged"/> event.</summary>
+    protected virtual void OnSelectedTabChanged (Tab oldTab, Tab newTab)
+    {
+        SelectedTabChanged?.Invoke (this, new TabChangedEventArgs (oldTab, newTab));
+    }
+
+    /// <summary>Returns which tabs to render at each x location.</summary>
+    /// <returns></returns>
+    internal IEnumerable<Tab> 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) - 1;
+            }
+            else
+            {
+                tab.X = 0;
+            }
+
+            tab.Y = 0;
+
+            // while there is space for the tab
+            int tabTextWidth = tab.DisplayText.EnumerateRunes ().Sum (c => c.GetColumns ());
+
+            // 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));
+
+            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!;
+                tab.Border!.MouseClick += Tab_MouseClick!;
+
+                yield return tab;
+
+                break;
+            }
+
+            if (tabTextWidth > maxWidth)
+            {
+                tab.Text = tab.DisplayText.Substring (0, (int)maxWidth);
+                tabTextWidth = (int)maxWidth;
+            }
+            else
+            {
+                tab.Text = tab.DisplayText;
+            }
+
+            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!;
+            tab.Border!.MouseClick += Tab_MouseClick!;
+
+            yield return tab;
+
+            prevTab = tab;
+
+            i += tabTextWidth + 1;
+        }
+
+        if (TabCanSetFocus ())
+        {
+            SelectedTab?.SetFocus ();
+        }
+    }
+
+    /// <summary>
+    ///     Returns the number of rows occupied by rendering the tabs, this depends on <see cref="TabStyle.ShowTopLine"/>
+    ///     and can be 0 (e.g. if <see cref="TabStyle.TabsOnBottom"/> and you ask for <paramref name="top"/>).
+    /// </summary>
+    /// <param name="top">True to measure the space required at the top of the control, false to measure space at the bottom.</param>
+    /// .
+    /// <returns></returns>
+    private int GetTabHeight (bool top)
+    {
+        if (top && Style.TabsOnBottom)
+        {
+            return 0;
+        }
+
+        if (!top && !Style.TabsOnBottom)
+        {
+            return 0;
+        }
+
+        return Style.ShowTopLine ? 3 : 2;
+    }
+
+    internal void Tab_MouseClick (object sender, MouseEventArgs e)
+    {
+        e.Handled = _tabsBar.NewMouseEvent (e) == true;
+    }
+
+    private void UnSetCurrentTabs ()
+    {
+        if (_tabLocations is null)
+        {
+            // Ensures unset any visible tab prior to TabScrollOffset
+            for (int i = 0; i < TabScrollOffset; i++)
+            {
+                Tab tab = Tabs.ElementAt (i);
+
+                if (tab.Visible)
+                {
+                    tab.MouseClick -= Tab_MouseClick!;
+                    tab.Border!.MouseClick -= Tab_MouseClick!;
+                    tab.Visible = false;
+                }
+            }
+        }
+        else if (_tabLocations is { })
+        {
+            foreach (Tab tabToRender in _tabLocations)
+            {
+                tabToRender.MouseClick -= Tab_MouseClick!;
+                tabToRender.Border!.MouseClick -= Tab_MouseClick!;
+                tabToRender.Visible = false;
+            }
+
+            _tabLocations = null;
+        }
+    }
+
+    /// <summary>Raises the <see cref="TabClicked"/> event.</summary>
+    /// <param name="tabMouseEventArgs"></param>
+    internal virtual void OnTabClicked (TabMouseEventArgs tabMouseEventArgs) { TabClicked?.Invoke (this, tabMouseEventArgs); }
+
+
+}

+ 5 - 0
UnitTests/TestHelpers.cs

@@ -197,6 +197,11 @@ public class SetupFakeDriverAttribute : BeforeAfterTestAttribute
         // Turn off diagnostic flags in case some test left them on
         // Turn off diagnostic flags in case some test left them on
         View.Diagnostics = ViewDiagnosticFlags.Off;
         View.Diagnostics = ViewDiagnosticFlags.Off;
 
 
+        if (Application.Driver is { })
+        {
+            ((FakeDriver)Application.Driver).End ();
+        }
+
         Application.Driver = null;
         Application.Driver = null;
         base.After (methodUnderTest);
         base.After (methodUnderTest);
     }
     }

+ 0 - 2
UnitTests/View/Layout/SetLayoutTests.cs

@@ -812,6 +812,4 @@ public class SetLayoutTests (ITestOutputHelper output)
         Assert.Equal (19, v2.Frame.Height);
         Assert.Equal (19, v2.Frame.Height);
         t.Dispose ();
         t.Dispose ();
     }
     }
-
-
 }
 }

+ 209 - 77
UnitTests/Views/TabViewTests.cs

@@ -3,7 +3,6 @@ using Xunit.Abstractions;
 
 
 namespace Terminal.Gui.ViewsTests;
 namespace Terminal.Gui.ViewsTests;
 
 
-#if foo
 public class TabViewTests (ITestOutputHelper output)
 public class TabViewTests (ITestOutputHelper output)
 {
 {
     [Fact]
     [Fact]
@@ -113,8 +112,6 @@ public class TabViewTests (ITestOutputHelper output)
         tv.Width = 20;
         tv.Width = 20;
         tv.Height = 5;
         tv.Height = 5;
 
 
-        tv.Layout ();
-
         tv.Draw ();
         tv.Draw ();
 
 
         View tabRow = tv.Subviews [0];
         View tabRow = tv.Subviews [0];
@@ -146,21 +143,21 @@ public class TabViewTests (ITestOutputHelper output)
         {
         {
             args = new () { ScreenPosition = new (i, 1), Flags = MouseFlags.ReportMousePosition };
             args = new () { ScreenPosition = new (i, 1), Flags = MouseFlags.ReportMousePosition };
             Application.RaiseMouseEvent (args);
             Application.RaiseMouseEvent (args);
-            Application.LayoutAndDrawToplevels ();
+            Application.LayoutAndDraw ();
             Assert.Null (clicked);
             Assert.Null (clicked);
             Assert.Equal (tab1, tv.SelectedTab);
             Assert.Equal (tab1, tv.SelectedTab);
         }
         }
 
 
         args = new () { ScreenPosition = new (3, 1), Flags = MouseFlags.Button1Clicked };
         args = new () { ScreenPosition = new (3, 1), Flags = MouseFlags.Button1Clicked };
         Application.RaiseMouseEvent (args);
         Application.RaiseMouseEvent (args);
-        Application.LayoutAndDrawToplevels ();
+        Application.LayoutAndDraw ();
         Assert.Equal (tab1, clicked);
         Assert.Equal (tab1, clicked);
         Assert.Equal (tab1, tv.SelectedTab);
         Assert.Equal (tab1, tv.SelectedTab);
 
 
         // Click to tab2
         // Click to tab2
         args = new () { ScreenPosition = new (6, 1), Flags = MouseFlags.Button1Clicked };
         args = new () { ScreenPosition = new (6, 1), Flags = MouseFlags.Button1Clicked };
         Application.RaiseMouseEvent (args);
         Application.RaiseMouseEvent (args);
-        Application.LayoutAndDrawToplevels ();
+        Application.LayoutAndDraw ();
         Assert.Equal (tab2, clicked);
         Assert.Equal (tab2, clicked);
         Assert.Equal (tab2, tv.SelectedTab);
         Assert.Equal (tab2, tv.SelectedTab);
 
 
@@ -173,7 +170,7 @@ public class TabViewTests (ITestOutputHelper output)
 
 
         args = new () { ScreenPosition = new (3, 1), Flags = MouseFlags.Button1Clicked };
         args = new () { ScreenPosition = new (3, 1), Flags = MouseFlags.Button1Clicked };
         Application.RaiseMouseEvent (args);
         Application.RaiseMouseEvent (args);
-        Application.LayoutAndDrawToplevels ();
+        Application.LayoutAndDraw ();
 
 
         // Tab 1 was clicked but event handler blocked navigation
         // Tab 1 was clicked but event handler blocked navigation
         Assert.Equal (tab1, clicked);
         Assert.Equal (tab1, clicked);
@@ -181,7 +178,7 @@ public class TabViewTests (ITestOutputHelper output)
 
 
         args = new () { ScreenPosition = new (12, 1), Flags = MouseFlags.Button1Clicked };
         args = new () { ScreenPosition = new (12, 1), Flags = MouseFlags.Button1Clicked };
         Application.RaiseMouseEvent (args);
         Application.RaiseMouseEvent (args);
-        Application.LayoutAndDrawToplevels ();
+        Application.LayoutAndDraw ();
 
 
         // Clicking beyond last tab should raise event with null Tab
         // Clicking beyond last tab should raise event with null Tab
         Assert.Null (clicked);
         Assert.Null (clicked);
@@ -198,8 +195,6 @@ public class TabViewTests (ITestOutputHelper output)
         tv.Width = 7;
         tv.Width = 7;
         tv.Height = 5;
         tv.Height = 5;
 
 
-        tv.LayoutSubviews ();
-
         tv.Draw ();
         tv.Draw ();
 
 
         View tabRow = tv.Subviews [0];
         View tabRow = tv.Subviews [0];
@@ -236,7 +231,7 @@ public class TabViewTests (ITestOutputHelper output)
         // Click the right arrow
         // Click the right arrow
         var args = new MouseEventArgs { ScreenPosition = new (6, 2), Flags = MouseFlags.Button1Clicked };
         var args = new MouseEventArgs { ScreenPosition = new (6, 2), Flags = MouseFlags.Button1Clicked };
         Application.RaiseMouseEvent (args);
         Application.RaiseMouseEvent (args);
-        Application.LayoutAndDrawToplevels ();
+        Application.LayoutAndDraw ();
         Assert.Null (clicked);
         Assert.Null (clicked);
         Assert.Equal (tab1, oldChanged);
         Assert.Equal (tab1, oldChanged);
         Assert.Equal (tab2, newChanged);
         Assert.Equal (tab2, newChanged);
@@ -256,7 +251,7 @@ public class TabViewTests (ITestOutputHelper output)
         // Click the left arrow
         // Click the left arrow
         args = new () { ScreenPosition = new (0, 2), Flags = MouseFlags.Button1Clicked };
         args = new () { ScreenPosition = new (0, 2), Flags = MouseFlags.Button1Clicked };
         Application.RaiseMouseEvent (args);
         Application.RaiseMouseEvent (args);
-        Application.LayoutAndDrawToplevels ();
+        Application.LayoutAndDraw ();
         Assert.Null (clicked);
         Assert.Null (clicked);
         Assert.Equal (tab2, oldChanged);
         Assert.Equal (tab2, oldChanged);
         Assert.Equal (tab1, newChanged);
         Assert.Equal (tab1, newChanged);
@@ -286,8 +281,7 @@ public class TabViewTests (ITestOutputHelper output)
 
 
         Assert.Equal (LineStyle.None, tv.BorderStyle);
         Assert.Equal (LineStyle.None, tv.BorderStyle);
         tv.BorderStyle = LineStyle.Single;
         tv.BorderStyle = LineStyle.Single;
-
-        tv.LayoutSubviews ();
+        tv.Layout ();
 
 
         tv.Draw ();
         tv.Draw ();
 
 
@@ -327,7 +321,7 @@ public class TabViewTests (ITestOutputHelper output)
         // Click the right arrow
         // Click the right arrow
         var args = new MouseEventArgs { ScreenPosition = new (7, 3), Flags = MouseFlags.Button1Clicked };
         var args = new MouseEventArgs { ScreenPosition = new (7, 3), Flags = MouseFlags.Button1Clicked };
         Application.RaiseMouseEvent (args);
         Application.RaiseMouseEvent (args);
-        Application.LayoutAndDrawToplevels ();
+        Application.LayoutAndDraw ();
         Assert.Null (clicked);
         Assert.Null (clicked);
         Assert.Equal (tab1, oldChanged);
         Assert.Equal (tab1, oldChanged);
         Assert.Equal (tab2, newChanged);
         Assert.Equal (tab2, newChanged);
@@ -349,7 +343,7 @@ public class TabViewTests (ITestOutputHelper output)
         // Click the left arrow
         // Click the left arrow
         args = new () { ScreenPosition = new (1, 3), Flags = MouseFlags.Button1Clicked };
         args = new () { ScreenPosition = new (1, 3), Flags = MouseFlags.Button1Clicked };
         Application.RaiseMouseEvent (args);
         Application.RaiseMouseEvent (args);
-        Application.LayoutAndDrawToplevels ();
+        Application.LayoutAndDraw ();
         Assert.Null (clicked);
         Assert.Null (clicked);
         Assert.Equal (tab2, oldChanged);
         Assert.Equal (tab2, oldChanged);
         Assert.Equal (tab1, newChanged);
         Assert.Equal (tab1, newChanged);
@@ -400,7 +394,7 @@ public class TabViewTests (ITestOutputHelper output)
 
 
         // Press the cursor up key to focus the selected tab
         // Press the cursor up key to focus the selected tab
         Application.RaiseKeyDownEvent (Key.CursorUp);
         Application.RaiseKeyDownEvent (Key.CursorUp);
-        Application.LayoutAndDrawToplevels ();
+        Application.LayoutAndDraw ();
 
 
         // Is the selected tab focused
         // Is the selected tab focused
         Assert.Equal (tab1, tv.SelectedTab);
         Assert.Equal (tab1, tv.SelectedTab);
@@ -418,7 +412,7 @@ public class TabViewTests (ITestOutputHelper output)
 
 
         // Press the cursor right key to select the next tab
         // Press the cursor right key to select the next tab
         Application.RaiseKeyDownEvent (Key.CursorRight);
         Application.RaiseKeyDownEvent (Key.CursorRight);
-        Application.LayoutAndDrawToplevels ();
+        Application.LayoutAndDraw ();
         Assert.Equal (tab1, oldChanged);
         Assert.Equal (tab1, oldChanged);
         Assert.Equal (tab2, newChanged);
         Assert.Equal (tab2, newChanged);
         Assert.Equal (tab2, tv.SelectedTab);
         Assert.Equal (tab2, tv.SelectedTab);
@@ -476,7 +470,7 @@ public class TabViewTests (ITestOutputHelper output)
 
 
         // Press the cursor left key to select the previous tab
         // Press the cursor left key to select the previous tab
         Application.RaiseKeyDownEvent (Key.CursorLeft);
         Application.RaiseKeyDownEvent (Key.CursorLeft);
-        Application.LayoutAndDrawToplevels ();
+        Application.LayoutAndDraw ();
         Assert.Equal (tab2, oldChanged);
         Assert.Equal (tab2, oldChanged);
         Assert.Equal (tab1, newChanged);
         Assert.Equal (tab1, newChanged);
         Assert.Equal (tab1, tv.SelectedTab);
         Assert.Equal (tab1, tv.SelectedTab);
@@ -486,7 +480,7 @@ public class TabViewTests (ITestOutputHelper output)
 
 
         // Press the end key to select the last tab
         // Press the end key to select the last tab
         Application.RaiseKeyDownEvent (Key.End);
         Application.RaiseKeyDownEvent (Key.End);
-        Application.LayoutAndDrawToplevels ();
+        Application.LayoutAndDraw ();
         Assert.Equal (tab1, oldChanged);
         Assert.Equal (tab1, oldChanged);
         Assert.Equal (tab2, newChanged);
         Assert.Equal (tab2, newChanged);
         Assert.Equal (tab2, tv.SelectedTab);
         Assert.Equal (tab2, tv.SelectedTab);
@@ -495,7 +489,7 @@ public class TabViewTests (ITestOutputHelper output)
 
 
         // Press the home key to select the first tab
         // Press the home key to select the first tab
         Application.RaiseKeyDownEvent (Key.Home);
         Application.RaiseKeyDownEvent (Key.Home);
-        Application.LayoutAndDrawToplevels ();
+        Application.LayoutAndDraw ();
         Assert.Equal (tab2, oldChanged);
         Assert.Equal (tab2, oldChanged);
         Assert.Equal (tab1, newChanged);
         Assert.Equal (tab1, newChanged);
         Assert.Equal (tab1, tv.SelectedTab);
         Assert.Equal (tab1, tv.SelectedTab);
@@ -504,7 +498,7 @@ public class TabViewTests (ITestOutputHelper output)
 
 
         // Press the page down key to select the next set of tabs
         // Press the page down key to select the next set of tabs
         Application.RaiseKeyDownEvent (Key.PageDown);
         Application.RaiseKeyDownEvent (Key.PageDown);
-        Application.LayoutAndDrawToplevels ();
+        Application.LayoutAndDraw ();
         Assert.Equal (tab1, oldChanged);
         Assert.Equal (tab1, oldChanged);
         Assert.Equal (tab2, newChanged);
         Assert.Equal (tab2, newChanged);
         Assert.Equal (tab2, tv.SelectedTab);
         Assert.Equal (tab2, tv.SelectedTab);
@@ -513,7 +507,7 @@ public class TabViewTests (ITestOutputHelper output)
 
 
         // Press the page up key to select the previous set of tabs
         // Press the page up key to select the previous set of tabs
         Application.RaiseKeyDownEvent (Key.PageUp);
         Application.RaiseKeyDownEvent (Key.PageUp);
-        Application.LayoutAndDrawToplevels ();
+        Application.LayoutAndDraw ();
         Assert.Equal (tab2, oldChanged);
         Assert.Equal (tab2, oldChanged);
         Assert.Equal (tab1, newChanged);
         Assert.Equal (tab1, newChanged);
         Assert.Equal (tab1, tv.SelectedTab);
         Assert.Equal (tab1, tv.SelectedTab);
@@ -610,7 +604,6 @@ public class TabViewTests (ITestOutputHelper output)
         tv.ApplyStyleChanges ();
         tv.ApplyStyleChanges ();
         tv.Layout ();
         tv.Layout ();
 
 
-        View.ClipToScreen ();
         tv.Draw ();
         tv.Draw ();
 
 
         TestHelpers.AssertDriverContentsWithFrameAre (
         TestHelpers.AssertDriverContentsWithFrameAre (
@@ -633,7 +626,7 @@ public class TabViewTests (ITestOutputHelper output)
         tv.Height = 5;
         tv.Height = 5;
         tv.Style = new () { ShowTopLine = false };
         tv.Style = new () { ShowTopLine = false };
         tv.ApplyStyleChanges ();
         tv.ApplyStyleChanges ();
-        tv.LayoutSubviews ();
+        tv.Layout ();
 
 
         tv.Draw ();
         tv.Draw ();
 
 
@@ -658,13 +651,13 @@ public class TabViewTests (ITestOutputHelper output)
         tv.Style = new () { ShowTopLine = false };
         tv.Style = new () { ShowTopLine = false };
         tv.ApplyStyleChanges ();
         tv.ApplyStyleChanges ();
 
 
-        // Ensures that the tab bar subview gets the bounds of the parent TabView
-        tv.LayoutSubviews ();
-
-        // Test two tab names that fit 
+        // Test two tab names that fit
         tab1.DisplayText = "12";
         tab1.DisplayText = "12";
         tab2.DisplayText = "13";
         tab2.DisplayText = "13";
 
 
+        // Ensures that the tab bar subview gets the bounds of the parent TabView
+        tv.Layout ();
+
         tv.Draw ();
         tv.Draw ();
 
 
         TestHelpers.AssertDriverContentsWithFrameAre (
         TestHelpers.AssertDriverContentsWithFrameAre (
@@ -678,8 +671,10 @@ public class TabViewTests (ITestOutputHelper output)
                                                      );
                                                      );
 
 
         tv.SelectedTab = tab2;
         tv.SelectedTab = tab2;
+        Assert.Equal (tab2, tv.Subviews.First (v => v.Id.Contains ("tabRowView")).MostFocused);
 
 
-        View.ClipToScreen ();
+        tv.Layout ();
+        View.SetClipToScreen ();
         tv.Draw ();
         tv.Draw ();
 
 
         TestHelpers.AssertDriverContentsWithFrameAre (
         TestHelpers.AssertDriverContentsWithFrameAre (
@@ -697,8 +692,8 @@ public class TabViewTests (ITestOutputHelper output)
         // Test first tab name too long
         // Test first tab name too long
         tab1.DisplayText = "12345678910";
         tab1.DisplayText = "12345678910";
         tab2.DisplayText = "13";
         tab2.DisplayText = "13";
-
-        View.ClipToScreen ();
+        tv.Layout ();
+        View.SetClipToScreen ();
         tv.Draw ();
         tv.Draw ();
 
 
         TestHelpers.AssertDriverContentsWithFrameAre (
         TestHelpers.AssertDriverContentsWithFrameAre (
@@ -713,9 +708,10 @@ public class TabViewTests (ITestOutputHelper output)
 
 
         //switch to tab2
         //switch to tab2
         tv.SelectedTab = tab2;
         tv.SelectedTab = tab2;
-        View.ClipToScreen ();
-        tv.Draw ();
 
 
+        tv.Layout ();
+        View.SetClipToScreen ();
+        tv.Draw ();
         TestHelpers.AssertDriverContentsWithFrameAre (
         TestHelpers.AssertDriverContentsWithFrameAre (
                                                       @"
                                                       @"
 │13│      
 │13│      
@@ -730,9 +726,9 @@ public class TabViewTests (ITestOutputHelper output)
         tab1.DisplayText = "12345678910";
         tab1.DisplayText = "12345678910";
         tab2.DisplayText = "abcdefghijklmnopq";
         tab2.DisplayText = "abcdefghijklmnopq";
 
 
-        View.ClipToScreen ();
+        tv.Layout ();
+        View.SetClipToScreen ();
         tv.Draw ();
         tv.Draw ();
-
         TestHelpers.AssertDriverContentsWithFrameAre (
         TestHelpers.AssertDriverContentsWithFrameAre (
                                                       @"
                                                       @"
 │abcdefg│ 
 │abcdefg│ 
@@ -753,9 +749,8 @@ public class TabViewTests (ITestOutputHelper output)
         tv.Height = 5;
         tv.Height = 5;
         tv.Style = new () { ShowTopLine = false, TabsOnBottom = true };
         tv.Style = new () { ShowTopLine = false, TabsOnBottom = true };
         tv.ApplyStyleChanges ();
         tv.ApplyStyleChanges ();
-        tv.LayoutSubviews ();
+        tv.Layout ();
 
 
-        View.ClipToScreen ();
         tv.Draw ();
         tv.Draw ();
 
 
         TestHelpers.AssertDriverContentsWithFrameAre (
         TestHelpers.AssertDriverContentsWithFrameAre (
@@ -778,7 +773,7 @@ public class TabViewTests (ITestOutputHelper output)
         tv.Height = 5;
         tv.Height = 5;
         tv.Style = new () { ShowTopLine = false, TabsOnBottom = true };
         tv.Style = new () { ShowTopLine = false, TabsOnBottom = true };
         tv.ApplyStyleChanges ();
         tv.ApplyStyleChanges ();
-        tv.LayoutSubviews ();
+        tv.Layout ();
 
 
         tv.Draw ();
         tv.Draw ();
 
 
@@ -802,15 +797,13 @@ public class TabViewTests (ITestOutputHelper output)
         tv.Height = 5;
         tv.Height = 5;
         tv.Style = new () { ShowTopLine = false, TabsOnBottom = true };
         tv.Style = new () { ShowTopLine = false, TabsOnBottom = true };
         tv.ApplyStyleChanges ();
         tv.ApplyStyleChanges ();
+        tv.Layout ();
 
 
-        // Ensures that the tab bar subview gets the bounds of the parent TabView
-        tv.LayoutSubviews ();
-
-        // Test two tab names that fit 
+        // Test two tab names that fit
         tab1.DisplayText = "12";
         tab1.DisplayText = "12";
         tab2.DisplayText = "13";
         tab2.DisplayText = "13";
-        View.ClipToScreen ();
 
 
+        tv.Layout ();
         tv.Draw ();
         tv.Draw ();
 
 
         TestHelpers.AssertDriverContentsWithFrameAre (
         TestHelpers.AssertDriverContentsWithFrameAre (
@@ -824,8 +817,10 @@ public class TabViewTests (ITestOutputHelper output)
                                                      );
                                                      );
 
 
         tv.SelectedTab = tab2;
         tv.SelectedTab = tab2;
+        Assert.Equal (tab2, tv.Subviews.First (v => v.Id.Contains ("tabRowView")).MostFocused);
 
 
-        View.ClipToScreen ();
+        tv.Layout ();
+        View.SetClipToScreen ();
         tv.Draw ();
         tv.Draw ();
 
 
         TestHelpers.AssertDriverContentsWithFrameAre (
         TestHelpers.AssertDriverContentsWithFrameAre (
@@ -844,7 +839,8 @@ public class TabViewTests (ITestOutputHelper output)
         tab1.DisplayText = "12345678910";
         tab1.DisplayText = "12345678910";
         tab2.DisplayText = "13";
         tab2.DisplayText = "13";
 
 
-        View.ClipToScreen ();
+        tv.Layout ();
+        View.SetClipToScreen ();
         tv.Draw ();
         tv.Draw ();
 
 
         TestHelpers.AssertDriverContentsWithFrameAre (
         TestHelpers.AssertDriverContentsWithFrameAre (
@@ -859,7 +855,9 @@ public class TabViewTests (ITestOutputHelper output)
 
 
         //switch to tab2
         //switch to tab2
         tv.SelectedTab = tab2;
         tv.SelectedTab = tab2;
-        View.ClipToScreen ();
+
+        tv.Layout ();
+        View.SetClipToScreen ();
         tv.Draw ();
         tv.Draw ();
 
 
         TestHelpers.AssertDriverContentsWithFrameAre (
         TestHelpers.AssertDriverContentsWithFrameAre (
@@ -876,7 +874,8 @@ public class TabViewTests (ITestOutputHelper output)
         tab1.DisplayText = "12345678910";
         tab1.DisplayText = "12345678910";
         tab2.DisplayText = "abcdefghijklmnopq";
         tab2.DisplayText = "abcdefghijklmnopq";
 
 
-        View.ClipToScreen ();
+        tv.Layout ();
+        View.SetClipToScreen ();
         tv.Draw ();
         tv.Draw ();
 
 
         TestHelpers.AssertDriverContentsWithFrameAre (
         TestHelpers.AssertDriverContentsWithFrameAre (
@@ -899,7 +898,6 @@ public class TabViewTests (ITestOutputHelper output)
         tv.Height = 5;
         tv.Height = 5;
         tv.Layout ();
         tv.Layout ();
 
 
-        View.ClipToScreen ();
         tv.Draw ();
         tv.Draw ();
 
 
         TestHelpers.AssertDriverContentsWithFrameAre (
         TestHelpers.AssertDriverContentsWithFrameAre (
@@ -922,7 +920,7 @@ public class TabViewTests (ITestOutputHelper output)
         tv.Height = 5;
         tv.Height = 5;
         tv.Layout ();
         tv.Layout ();
 
 
-        View.ClipToScreen ();
+        View.SetClipToScreen ();
         tv.Draw ();
         tv.Draw ();
 
 
         TestHelpers.AssertDriverContentsWithFrameAre (
         TestHelpers.AssertDriverContentsWithFrameAre (
@@ -944,14 +942,11 @@ public class TabViewTests (ITestOutputHelper output)
         tv.Width = 10;
         tv.Width = 10;
         tv.Height = 5;
         tv.Height = 5;
 
 
-        // Ensures that the tab bar subview gets the bounds of the parent TabView
-        tv.LayoutSubviews ();
-
-        // Test two tab names that fit 
+        // Test two tab names that fit
         tab1.DisplayText = "12";
         tab1.DisplayText = "12";
         tab2.DisplayText = "13";
         tab2.DisplayText = "13";
 
 
-        View.ClipToScreen ();
+        tv.Layout ();
         tv.Draw ();
         tv.Draw ();
 
 
         TestHelpers.AssertDriverContentsWithFrameAre (
         TestHelpers.AssertDriverContentsWithFrameAre (
@@ -966,7 +961,8 @@ public class TabViewTests (ITestOutputHelper output)
 
 
         tv.SelectedTab = tab2;
         tv.SelectedTab = tab2;
 
 
-        View.ClipToScreen ();
+        tv.Layout ();
+        View.SetClipToScreen ();
         tv.Draw ();
         tv.Draw ();
 
 
         TestHelpers.AssertDriverContentsWithFrameAre (
         TestHelpers.AssertDriverContentsWithFrameAre (
@@ -985,7 +981,8 @@ public class TabViewTests (ITestOutputHelper output)
         tab1.DisplayText = "12345678910";
         tab1.DisplayText = "12345678910";
         tab2.DisplayText = "13";
         tab2.DisplayText = "13";
 
 
-        View.ClipToScreen ();
+        tv.Layout ();
+        View.SetClipToScreen ();
         tv.Draw ();
         tv.Draw ();
 
 
         TestHelpers.AssertDriverContentsWithFrameAre (
         TestHelpers.AssertDriverContentsWithFrameAre (
@@ -1000,7 +997,9 @@ public class TabViewTests (ITestOutputHelper output)
 
 
         //switch to tab2
         //switch to tab2
         tv.SelectedTab = tab2;
         tv.SelectedTab = tab2;
-        View.ClipToScreen ();
+
+        tv.Layout ();
+        View.SetClipToScreen ();
         tv.Draw ();
         tv.Draw ();
 
 
         TestHelpers.AssertDriverContentsWithFrameAre (
         TestHelpers.AssertDriverContentsWithFrameAre (
@@ -1017,7 +1016,8 @@ public class TabViewTests (ITestOutputHelper output)
         tab1.DisplayText = "12345678910";
         tab1.DisplayText = "12345678910";
         tab2.DisplayText = "abcdefghijklmnopq";
         tab2.DisplayText = "abcdefghijklmnopq";
 
 
-        View.ClipToScreen ();
+        tv.Layout ();
+        View.SetClipToScreen ();
         tv.Draw ();
         tv.Draw ();
 
 
         TestHelpers.AssertDriverContentsWithFrameAre (
         TestHelpers.AssertDriverContentsWithFrameAre (
@@ -1039,13 +1039,11 @@ public class TabViewTests (ITestOutputHelper output)
         tv.Width = 20;
         tv.Width = 20;
         tv.Height = 5;
         tv.Height = 5;
 
 
-        tv.LayoutSubviews ();
-
         tab1.DisplayText = "Tab0";
         tab1.DisplayText = "Tab0";
 
 
         tab2.DisplayText = "Les Mise" + char.ConvertFromUtf32 (int.Parse ("0301", NumberStyles.HexNumber)) + "rables";
         tab2.DisplayText = "Les Mise" + char.ConvertFromUtf32 (int.Parse ("0301", NumberStyles.HexNumber)) + "rables";
 
 
-        View.ClipToScreen ();
+        tv.Layout ();
         tv.Draw ();
         tv.Draw ();
 
 
         TestHelpers.AssertDriverContentsWithFrameAre (
         TestHelpers.AssertDriverContentsWithFrameAre (
@@ -1060,7 +1058,8 @@ public class TabViewTests (ITestOutputHelper output)
 
 
         tv.SelectedTab = tab2;
         tv.SelectedTab = tab2;
 
 
-        View.ClipToScreen ();
+        tv.Layout ();
+        View.SetClipToScreen ();
         tv.Draw ();
         tv.Draw ();
 
 
         TestHelpers.AssertDriverContentsWithFrameAre (
         TestHelpers.AssertDriverContentsWithFrameAre (
@@ -1083,7 +1082,7 @@ public class TabViewTests (ITestOutputHelper output)
         tv.Height = 5;
         tv.Height = 5;
         tv.Style = new () { TabsOnBottom = true };
         tv.Style = new () { TabsOnBottom = true };
         tv.ApplyStyleChanges ();
         tv.ApplyStyleChanges ();
-        tv.LayoutSubviews ();
+        tv.Layout ();
 
 
         tv.Draw ();
         tv.Draw ();
 
 
@@ -1107,7 +1106,7 @@ public class TabViewTests (ITestOutputHelper output)
         tv.Height = 5;
         tv.Height = 5;
         tv.Style = new () { TabsOnBottom = true };
         tv.Style = new () { TabsOnBottom = true };
         tv.ApplyStyleChanges ();
         tv.ApplyStyleChanges ();
-        tv.LayoutSubviews ();
+        tv.Layout ();
 
 
         tv.Draw ();
         tv.Draw ();
 
 
@@ -1131,15 +1130,13 @@ public class TabViewTests (ITestOutputHelper output)
         tv.Height = 5;
         tv.Height = 5;
         tv.Style = new () { TabsOnBottom = true };
         tv.Style = new () { TabsOnBottom = true };
         tv.ApplyStyleChanges ();
         tv.ApplyStyleChanges ();
+        tv.Layout ();
 
 
-        // Ensures that the tab bar subview gets the bounds of the parent TabView
-        tv.LayoutSubviews ();
-
-        // Test two tab names that fit 
+        // Test two tab names that fit
         tab1.DisplayText = "12";
         tab1.DisplayText = "12";
         tab2.DisplayText = "13";
         tab2.DisplayText = "13";
-        View.ClipToScreen ();
 
 
+        tv.Layout ();
         tv.Draw ();
         tv.Draw ();
 
 
         TestHelpers.AssertDriverContentsWithFrameAre (
         TestHelpers.AssertDriverContentsWithFrameAre (
@@ -1156,7 +1153,8 @@ public class TabViewTests (ITestOutputHelper output)
         tab1.DisplayText = "12345678910";
         tab1.DisplayText = "12345678910";
         tab2.DisplayText = "13";
         tab2.DisplayText = "13";
 
 
-        View.ClipToScreen ();
+        tv.Layout ();
+        View.SetClipToScreen ();
         tv.Draw ();
         tv.Draw ();
 
 
         TestHelpers.AssertDriverContentsWithFrameAre (
         TestHelpers.AssertDriverContentsWithFrameAre (
@@ -1171,7 +1169,9 @@ public class TabViewTests (ITestOutputHelper output)
 
 
         //switch to tab2
         //switch to tab2
         tv.SelectedTab = tab2;
         tv.SelectedTab = tab2;
-        View.ClipToScreen ();
+
+        tv.Layout ();
+        View.SetClipToScreen ();
         tv.Draw ();
         tv.Draw ();
 
 
         TestHelpers.AssertDriverContentsWithFrameAre (
         TestHelpers.AssertDriverContentsWithFrameAre (
@@ -1188,7 +1188,8 @@ public class TabViewTests (ITestOutputHelper output)
         tab1.DisplayText = "12345678910";
         tab1.DisplayText = "12345678910";
         tab2.DisplayText = "abcdefghijklmnopq";
         tab2.DisplayText = "abcdefghijklmnopq";
 
 
-        View.ClipToScreen ();
+        tv.Layout ();
+        View.SetClipToScreen ();
         tv.Draw ();
         tv.Draw ();
 
 
         TestHelpers.AssertDriverContentsWithFrameAre (
         TestHelpers.AssertDriverContentsWithFrameAre (
@@ -1212,12 +1213,11 @@ public class TabViewTests (ITestOutputHelper output)
         tv.Style = new () { TabsOnBottom = true };
         tv.Style = new () { TabsOnBottom = true };
         tv.ApplyStyleChanges ();
         tv.ApplyStyleChanges ();
 
 
-        tv.LayoutSubviews ();
-
         tab1.DisplayText = "Tab0";
         tab1.DisplayText = "Tab0";
 
 
         tab2.DisplayText = "Les Mise" + char.ConvertFromUtf32 (int.Parse ("0301", NumberStyles.HexNumber)) + "rables";
         tab2.DisplayText = "Les Mise" + char.ConvertFromUtf32 (int.Parse ("0301", NumberStyles.HexNumber)) + "rables";
 
 
+        tv.Layout ();
         tv.Draw ();
         tv.Draw ();
 
 
         TestHelpers.AssertDriverContentsWithFrameAre (
         TestHelpers.AssertDriverContentsWithFrameAre (
@@ -1232,7 +1232,8 @@ public class TabViewTests (ITestOutputHelper output)
 
 
         tv.SelectedTab = tab2;
         tv.SelectedTab = tab2;
 
 
-        View.ClipToScreen ();
+        tv.Layout ();
+        View.SetClipToScreen ();
         tv.Draw ();
         tv.Draw ();
 
 
         TestHelpers.AssertDriverContentsWithFrameAre (
         TestHelpers.AssertDriverContentsWithFrameAre (
@@ -1322,6 +1323,138 @@ public class TabViewTests (ITestOutputHelper output)
         Application.Shutdown ();
         Application.Shutdown ();
     }
     }
 
 
+    [Fact]
+    [SetupFakeDriver]
+    public void Add_Three_TabsOnTop_ChangesTab ()
+    {
+        TabView tv = GetTabView (out Tab tab1, out Tab tab2, false);
+        Tab tab3;
+
+        tv.AddTab (
+                   tab3 = new () { Id = "tab3", DisplayText = "Tab3", View = new TextView { Id = "tab3.TextView", Width = 3, Height = 1, Text = "hi3" } },
+                   false);
+
+        tv.Width = 20;
+        tv.Height = 5;
+
+        tv.Layout ();
+        tv.Draw ();
+
+        Assert.Equal (tab1, tv.SelectedTab);
+
+        TestHelpers.AssertDriverContentsAre (
+                                             @"
+╭────┬────┬────╮
+│Tab1│Tab2│Tab3│
+│    ╰────┴────┴───╮
+│hi                │
+└──────────────────┘
+",
+                                             output
+                                            );
+
+        tv.SelectedTab = tab2;
+
+        tv.Layout ();
+        View.SetClipToScreen ();
+        tv.Draw ();
+
+        TestHelpers.AssertDriverContentsWithFrameAre (
+                                                      @"
+╭────┬────┬────╮    
+│Tab1│Tab2│Tab3│    
+├────╯    ╰────┴───╮
+│hi2               │
+└──────────────────┘
+",
+                                                      output
+                                                     );
+
+        tv.SelectedTab = tab3;
+
+        tv.Layout ();
+        View.SetClipToScreen ();
+        tv.Draw ();
+
+        TestHelpers.AssertDriverContentsWithFrameAre (
+                                                      @"
+╭────┬────┬────╮    
+│Tab1│Tab2│Tab3│    
+├────┴────╯    ╰───╮
+│hi3               │
+└──────────────────┘
+",
+                                                      output
+                                                     );
+    }
+
+    [Fact]
+    [SetupFakeDriver]
+    public void Add_Three_TabsOnBottom_ChangesTab ()
+    {
+        TabView tv = GetTabView (out Tab tab1, out Tab tab2, false);
+        Tab tab3;
+
+        tv.AddTab (
+                   tab3 = new () { Id = "tab3", DisplayText = "Tab3", View = new TextView { Id = "tab3.TextView", Width = 3, Height = 1, Text = "hi3" } },
+                   false);
+
+        tv.Width = 20;
+        tv.Height = 5;
+        tv.Style = new () { TabsOnBottom = true };
+        tv.ApplyStyleChanges ();
+
+        tv.Layout ();
+        tv.Draw ();
+
+        Assert.Equal (tab1, tv.SelectedTab);
+
+        TestHelpers.AssertDriverContentsAre (
+                                             @"
+┌──────────────────┐
+│hi                │
+│    ╭────┬────┬───╯
+│Tab1│Tab2│Tab3│
+╰────┴────┴────╯
+",
+                                             output
+                                            );
+
+        tv.SelectedTab = tab2;
+
+        tv.Layout ();
+        View.SetClipToScreen ();
+        tv.Draw ();
+
+        TestHelpers.AssertDriverContentsWithFrameAre (
+                                                      @"
+┌──────────────────┐
+│hi2               │
+├────╮    ╭────┬───╯
+│Tab1│Tab2│Tab3│    
+╰────┴────┴────╯    
+",
+                                                      output
+                                                     );
+
+        tv.SelectedTab = tab3;
+
+        tv.Layout ();
+        View.SetClipToScreen ();
+        tv.Draw ();
+
+        TestHelpers.AssertDriverContentsWithFrameAre (
+                                                      @"
+┌──────────────────┐
+│hi3               │
+├────┬────╮    ╭───╯
+│Tab1│Tab2│Tab3│    
+╰────┴────┴────╯    
+",
+                                                      output
+                                                     );
+    }
+
     private TabView GetTabView () { return GetTabView (out _, out _); }
     private TabView GetTabView () { return GetTabView (out _, out _); }
 
 
     private TabView GetTabView (out Tab tab1, out Tab tab2, bool initFakeDriver = true)
     private TabView GetTabView (out Tab tab1, out Tab tab2, bool initFakeDriver = true)
@@ -1355,4 +1488,3 @@ public class TabViewTests (ITestOutputHelper output)
         driver.Init ();
         driver.Init ();
     }
     }
 }
 }
-#endif