From cb7682f9a61ff2828858effadf81baa4d4d9a8b7 Mon Sep 17 00:00:00 2001 From: BDisp Date: Mon, 25 Nov 2024 21:41:09 +0000 Subject: [PATCH 01/18] Add TabSide enum. --- Terminal.Gui/Views/TabView/TabRow.cs | 744 +++++++++++++++---------- Terminal.Gui/Views/TabView/TabSide.cs | 27 + Terminal.Gui/Views/TabView/TabStyle.cs | 9 +- Terminal.Gui/Views/TabView/TabView.cs | 75 +-- UICatalog/Scenarios/TabViewExample.cs | 57 +- UnitTests/Views/TabViewTests.cs | 16 +- 6 files changed, 564 insertions(+), 364 deletions(-) create mode 100644 Terminal.Gui/Views/TabView/TabSide.cs diff --git a/Terminal.Gui/Views/TabView/TabRow.cs b/Terminal.Gui/Views/TabView/TabRow.cs index 2ca80a69ae..ffff8ab4d8 100644 --- a/Terminal.Gui/Views/TabView/TabRow.cs +++ b/Terminal.Gui/Views/TabView/TabRow.cs @@ -157,187 +157,249 @@ private void RenderTabLineCanvas () if (i == 0 && _host.TabScrollOffset == 0) { - if (_host.Style.TabsOnBottom) + switch (_host.Style.TabsSide) { - // 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 + case TabSide.Top: + // Lower left vertical line lc.AddLine ( - new Point (vts.X - 1, vts.Bottom), + new Point (vts.X - 1, vts.Bottom - selectedOffset), -1, Orientation.Vertical, tab.BorderStyle ); - lc.AddLine ( - new Point (vts.X - 1, vts.Bottom), - 0, - Orientation.Horizontal, - tab.BorderStyle - ); - } - else - { - // Upper left tee + break; + case TabSide.Bottom: + // Upper left vertical line lc.AddLine ( new Point (vts.X - 1, vts.Y - 1), - 1, + -1, Orientation.Vertical, tab.BorderStyle ); - lc.AddLine ( - new Point (vts.X - 1, vts.Y - 1), - 0, - Orientation.Horizontal, - tab.BorderStyle - ); - } + break; + case TabSide.Left: + break; + case TabSide.Right: + break; + default: + throw new ArgumentOutOfRangeException (); } } - - if (i < tabLocations.Length - 1) + else if (i > 0 && i <= tabLocations.Length - 1) { - if (_host.Style.ShowTopLine) + switch (_host.Style.TabsSide) { - if (_host.Style.TabsOnBottom) - { - // Lower right tee + case TabSide.Top: + // LRCorner lc.AddLine ( - new Point (vts.Right, vts.Bottom), + new Point (vts.X - 1, vts.Bottom - selectedOffset), -1, Orientation.Vertical, tab.BorderStyle ); lc.AddLine ( - new Point (vts.Right, vts.Bottom), - 0, + new Point (vts.X - 1, vts.Bottom - selectedOffset), + -1, Orientation.Horizontal, tab.BorderStyle ); - } - else - { - // Upper right tee + + break; + case TabSide.Bottom: + // URCorner lc.AddLine ( - new Point (vts.Right, vts.Y - 1), + new Point (vts.X - 1, vts.Y - 1), 1, Orientation.Vertical, tab.BorderStyle ); lc.AddLine ( - new Point (vts.Right, vts.Y - 1), - 0, + new Point (vts.X - 1, vts.Y - 1), + -1, Orientation.Horizontal, tab.BorderStyle ); + + break; + case TabSide.Left: + break; + case TabSide.Right: + break; + default: + throw new ArgumentOutOfRangeException (); + } + + if (_host.Style.ShowTopLine) + { + switch (_host.Style.TabsSide) + { + case TabSide.Top: + // 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 + ); + + break; + case TabSide.Bottom: + // 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 + ); + + break; + case TabSide.Left: + break; + case TabSide.Right: + break; + default: + throw new ArgumentOutOfRangeException (); } } } - if (_host.Style.TabsOnBottom) + if (i < tabLocations.Length - 1) { - //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 - ); + if (_host.Style.ShowTopLine) + { + switch (_host.Style.TabsSide) + { + case TabSide.Top: + // 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 + ); + + break; + case TabSide.Bottom: + // 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 + ); + + break; + case TabSide.Left: + break; + case TabSide.Right: + break; + default: + throw new ArgumentOutOfRangeException (); + } + } } - else + + switch (_host.Style.TabsSide) { - //LLCorner - lc.AddLine ( - new Point (vts.Right, vts.Bottom - selectedOffset), - -1, - Orientation.Vertical, - tab.BorderStyle - ); + case TabSide.Top: + //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 - ); + lc.AddLine ( + new Point (vts.Right, vts.Bottom - selectedOffset), + 1, + Orientation.Horizontal, + tab.BorderStyle + ); + + break; + case TabSide.Bottom: + //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 + ); + + break; + case TabSide.Left: + break; + case TabSide.Right: + break; + default: + throw new ArgumentOutOfRangeException (); } } else if (selectedTab == -1) { if (i == 0 && string.IsNullOrEmpty (tab.Text)) { - if (_host.Style.TabsOnBottom) + switch (_host.Style.TabsSide) { - if (_host.Style.ShowTopLine) - { + case TabSide.Top: + 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), @@ -352,27 +414,27 @@ private void RenderTabLineCanvas () Orientation.Horizontal, tab.BorderStyle ); - } - // ULCorner - lc.AddLine ( - new Point (vts.X - 1, vts.Y - 1), - 1, - Orientation.Vertical, - tab.BorderStyle - ); + break; + case TabSide.Bottom: + 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 + ); + } - 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), @@ -387,27 +449,19 @@ private void RenderTabLineCanvas () 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 - ); + break; + case TabSide.Left: + break; + case TabSide.Right: + break; + default: + throw new ArgumentOutOfRangeException (); } } else if (i > 0) { - if (_host.Style.ShowTopLine || _host.Style.TabsOnBottom) + if (_host.Style.ShowTopLine || _host.Style.TabsSide == TabSide.Bottom) { // Upper left tee lc.AddLine ( @@ -461,7 +515,7 @@ private void RenderTabLineCanvas () ); } - if (_host.Style.ShowTopLine || !_host.Style.TabsOnBottom) + if (_host.Style.ShowTopLine || _host.Style.TabsSide == TabSide.Top) { // Lower right tee lc.AddLine ( @@ -499,77 +553,95 @@ private void RenderTabLineCanvas () if (i == 0 && i != selectedTab && _host is { TabScrollOffset: 0, Style.ShowBorder: true }) { - if (_host.Style.TabsOnBottom) + switch (_host.Style.TabsSide) { - // Upper left vertical line - lc.AddLine ( - new Point (vts.X - 1, vts.Y - 1), - 0, - Orientation.Vertical, - tab.BorderStyle - ); + case TabSide.Top: + // 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.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 + ); - lc.AddLine ( - new Point (vts.X - 1, vts.Bottom), - 1, - Orientation.Horizontal, - tab.BorderStyle - ); + break; + case TabSide.Bottom: + // 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 + ); + + break; + case TabSide.Left: + break; + case TabSide.Right: + break; + default: + throw new ArgumentOutOfRangeException (); } } if (i == tabLocations.Length - 1 && i != selectedTab) { - if (_host.Style.TabsOnBottom) + switch (_host.Style.TabsSide) { - // Upper right tee - lc.AddLine ( - new Point (vts.Right, vts.Y - 1), - 1, - Orientation.Vertical, - tab.BorderStyle - ); + case TabSide.Top: + // Lower right tee + lc.AddLine ( + new Point (vts.Right, vts.Bottom), + -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 + ); - lc.AddLine ( - new Point (vts.Right, vts.Bottom), - 0, - Orientation.Horizontal, - tab.BorderStyle - ); + break; + case TabSide.Bottom: + // 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 + ); + + break; + case TabSide.Left: + break; + case TabSide.Right: + break; + default: + throw new ArgumentOutOfRangeException (); } } @@ -578,7 +650,7 @@ private void RenderTabLineCanvas () var arrowOffset = 1; int lastSelectedTab = !_host.Style.ShowTopLine && i == selectedTab ? 1 : - _host.Style.TabsOnBottom ? 1 : 0; + _host.Style.TabsSide == TabSide.Bottom ? 1 : 0; Rectangle tabsBarVts = ViewportToScreen (Viewport); int lineLength = tabsBarVts.Right - vts.Right; @@ -587,78 +659,105 @@ private void RenderTabLineCanvas () { if (lineLength - arrowOffset > 0) { - if (_host.Style.TabsOnBottom) - { - lc.AddLine ( - new Point (vts.Right, vts.Y - lastSelectedTab), - lineLength - arrowOffset, - Orientation.Horizontal, - tab.BorderStyle - ); - } - else + switch (_host.Style.TabsSide) { - lc.AddLine ( - new Point ( - vts.Right, - vts.Bottom - lastSelectedTab - ), - lineLength - arrowOffset, - Orientation.Horizontal, - tab.BorderStyle - ); + case TabSide.Top: + lc.AddLine ( + new Point ( + vts.Right, + vts.Bottom - lastSelectedTab + ), + lineLength - arrowOffset, + Orientation.Horizontal, + tab.BorderStyle + ); + + break; + case TabSide.Bottom: + lc.AddLine ( + new Point (vts.Right, vts.Y - lastSelectedTab), + lineLength - arrowOffset, + Orientation.Horizontal, + tab.BorderStyle + ); + + break; + case TabSide.Left: + break; + case TabSide.Right: + break; + default: + throw new ArgumentOutOfRangeException (); } } } else { // Right corner - if (_host.Style.TabsOnBottom) + switch (_host.Style.TabsSide) { - 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 + case TabSide.Top: lc.AddLine ( - new Point ( - tabsBarVts.Right - 1, - vts.Y - lastSelectedTab - ), - -1, - Orientation.Vertical, + new Point (vts.Right, vts.Bottom - lastSelectedTab), + lineLength, + Orientation.Horizontal, tab.BorderStyle ); - } - else - { - // More URCorner + + break; + case TabSide.Bottom: lc.AddLine ( - new Point ( - tabsBarVts.Right - 1, - vts.Bottom - lastSelectedTab - ), - 1, - Orientation.Vertical, + new Point (vts.Right, vts.Y - lastSelectedTab), + lineLength, + Orientation.Horizontal, tab.BorderStyle ); + + break; + case TabSide.Left: + break; + case TabSide.Right: + break; + default: + throw new ArgumentOutOfRangeException (); + } + + if (_host.Style.ShowBorder) + { + switch (_host.Style.TabsSide) + { + case TabSide.Top: + // More URCorner + lc.AddLine ( + new Point ( + tabsBarVts.Right - 1, + vts.Bottom - lastSelectedTab + ), + 1, + Orientation.Vertical, + tab.BorderStyle + ); + + break; + case TabSide.Bottom: + // More LRCorner + lc.AddLine ( + new Point ( + tabsBarVts.Right - 1, + vts.Y - lastSelectedTab + ), + -1, + Orientation.Vertical, + tab.BorderStyle + ); + + break; + case TabSide.Left: + break; + case TabSide.Right: + break; + default: + throw new ArgumentOutOfRangeException (); } } } @@ -670,7 +769,7 @@ private void RenderTabLineCanvas () private int GetUnderlineYPosition () { - if (_host.Style.TabsOnBottom) + if (_host.Style.TabsSide == TabSide.Bottom) { return 0; } @@ -697,41 +796,68 @@ private void RenderTabLine () { selected = tab; - if (_host.Style.TabsOnBottom) + switch (_host.Style.TabsSide) { - 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); + case TabSide.Top: + tab.Border!.Thickness = new (1, topLine, 1, 0); + tab.Margin!.Thickness = new (0, 0, 0, topLine); + + break; + case TabSide.Bottom: + tab.Border!.Thickness = new (1, 0, 1, topLine); + tab.Margin!.Thickness = new (0, 1, 0, 0); + + break; + case TabSide.Left: + break; + case TabSide.Right: + break; + default: + throw new ArgumentOutOfRangeException (); } } 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 + switch (_host.Style.TabsSide) { - tab.Border!.Thickness = new (1, topLine, 1, 1); - tab.Margin!.Thickness = new (0, 0, 0, 0); + case TabSide.Top: + tab.Border!.Thickness = new (1, topLine, 1, 1); + tab.Margin!.Thickness = new (0, 0, 0, 0); + + break; + case TabSide.Bottom: + tab.Border!.Thickness = new (1, 1, 1, topLine); + tab.Margin!.Thickness = new (0, 0, 0, 0); + + break; + case TabSide.Left: + break; + case TabSide.Right: + break; + default: + throw new ArgumentOutOfRangeException (); } } else { - if (_host.Style.TabsOnBottom) - { - tab.Border!.Thickness = new (1, 1, 1, topLine); - tab.Margin!.Thickness = new (0, 0, 0, 0); - } - else + switch (_host.Style.TabsSide) { - tab.Border!.Thickness = new (1, topLine, 1, 1); - tab.Margin!.Thickness = new (0, 0, 0, 0); + case TabSide.Top: + tab.Border!.Thickness = new (1, topLine, 1, 1); + tab.Margin!.Thickness = new (0, 0, 0, 0); + + break; + case TabSide.Bottom: + tab.Border!.Thickness = new (1, 1, 1, topLine); + tab.Margin!.Thickness = new (0, 0, 0, 0); + + break; + case TabSide.Left: + break; + case TabSide.Right: + break; + default: + throw new ArgumentOutOfRangeException (); } } diff --git a/Terminal.Gui/Views/TabView/TabSide.cs b/Terminal.Gui/Views/TabView/TabSide.cs new file mode 100644 index 0000000000..eef3a51c39 --- /dev/null +++ b/Terminal.Gui/Views/TabView/TabSide.cs @@ -0,0 +1,27 @@ +namespace Terminal.Gui; + +/// +/// Defines tab side. +/// +public enum TabSide +{ + /// + /// Top side. + /// + Top, + + /// + /// Bottom side. + /// + Bottom, + + /// + /// Left side. + /// + Left, + + /// + /// Right side. + /// + Right +} diff --git a/Terminal.Gui/Views/TabView/TabStyle.cs b/Terminal.Gui/Views/TabView/TabStyle.cs index 85404d05d9..c6734260c9 100644 --- a/Terminal.Gui/Views/TabView/TabStyle.cs +++ b/Terminal.Gui/Views/TabView/TabStyle.cs @@ -1,4 +1,5 @@ -namespace Terminal.Gui; +#nullable enable +namespace Terminal.Gui; /// Describes render stylistic selections of a public class TabStyle @@ -9,10 +10,10 @@ public class TabStyle /// /// True to show the top lip of tabs. False to directly begin with tab text during rendering. When true header /// line occupies 3 rows, when false only 2. Defaults to true. - /// When is enabled this instead applies to the bottommost line of the control + /// When is enabled this instead applies to the bottommost line of the control /// public bool ShowTopLine { get; set; } = true; - /// True to render tabs at the bottom of the view instead of the top - public bool TabsOnBottom { get; set; } = false; + /// Gets or sets the tabs side to render. + public TabSide TabsSide { get; set; } } diff --git a/Terminal.Gui/Views/TabView/TabView.cs b/Terminal.Gui/Views/TabView/TabView.cs index 73f286b564..fe856451c4 100644 --- a/Terminal.Gui/Views/TabView/TabView.cs +++ b/Terminal.Gui/Views/TabView/TabView.cs @@ -232,48 +232,59 @@ public void ApplyStyleChanges () _containerView.BorderStyle = Style.ShowBorder ? LineStyle.Single : LineStyle.None; _containerView.Width = Dim.Fill (); - if (Style.TabsOnBottom) + int tabHeight; + + switch (Style.TabsSide) { - // Tabs are along the bottom so just dodge the border - if (Style.ShowBorder) - { - _containerView.Border!.Thickness = new Thickness (1, 1, 1, 0); - } + case TabSide.Top: + // Tabs are along the top + if (Style.ShowBorder) + { + _containerView.Border!.Thickness = new Thickness (1, 0, 1, 1); + } - _containerView.Y = 0; + _tabsBar.Y = 0; - int tabHeight = GetTabHeight (false); + tabHeight = GetTabHeight (true); - // Fill client area leaving space at bottom for tabs - _containerView.Height = Dim.Fill (tabHeight); + //move content down to make space for tabs + _containerView.Y = Pos.Bottom (_tabsBar); - _tabsBar.Height = tabHeight; + // Fill client area leaving space at bottom for border + _containerView.Height = Dim.Fill (); - _tabsBar.Y = Pos.Bottom (_containerView); - } - else - { - // Tabs are along the top - if (Style.ShowBorder) - { - _containerView.Border!.Thickness = new Thickness (1, 0, 1, 1); - } + // The top tab should be 2 or 3 rows high and on the top + + _tabsBar.Height = tabHeight; - _tabsBar.Y = 0; + // Should be able to just use 0 but switching between top/bottom tabs repeatedly breaks in ValidatePosDim if just using the absolute value 0 - int tabHeight = GetTabHeight (true); + break; + case TabSide.Bottom: + // Tabs are along the bottom so just dodge the border + if (Style.ShowBorder) + { + _containerView.Border!.Thickness = new Thickness (1, 1, 1, 0); + } - //move content down to make space for tabs - _containerView.Y = Pos.Bottom (_tabsBar); + _containerView.Y = 0; - // Fill client area leaving space at bottom for border - _containerView.Height = Dim.Fill (); + tabHeight = GetTabHeight (false); - // The top tab should be 2 or 3 rows high and on the top + // Fill client area leaving space at bottom for tabs + _containerView.Height = Dim.Fill (tabHeight); - _tabsBar.Height = tabHeight; + _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 + _tabsBar.Y = Pos.Bottom (_containerView); + + break; + case TabSide.Left: + break; + case TabSide.Right: + break; + default: + throw new ArgumentOutOfRangeException (); } SetNeedsLayout (); @@ -526,19 +537,19 @@ internal IEnumerable CalculateViewport (Rectangle bounds) /// /// Returns the number of rows occupied by rendering the tabs, this depends on - /// and can be 0 (e.g. if and you ask for ). + /// and can be 0 (e.g. if and you ask for ). /// /// True to measure the space required at the top of the control, false to measure space at the bottom. /// . /// private int GetTabHeight (bool top) { - if (top && Style.TabsOnBottom) + if (top && Style.TabsSide == TabSide.Bottom) { return 0; } - if (!top && !Style.TabsOnBottom) + if (!top && Style.TabsSide == TabSide.Top) { return 0; } diff --git a/UICatalog/Scenarios/TabViewExample.cs b/UICatalog/Scenarios/TabViewExample.cs index 9d48c05685..b3f896e2e6 100644 --- a/UICatalog/Scenarios/TabViewExample.cs +++ b/UICatalog/Scenarios/TabViewExample.cs @@ -1,4 +1,6 @@ -using System.Linq; +using System; +using System.Collections.Generic; +using System.Linq; using System.Text; using Terminal.Gui; @@ -12,7 +14,8 @@ public class TabViewExample : Scenario private MenuItem _miShowBorder; private MenuItem _miShowTabViewBorder; private MenuItem _miShowTopLine; - private MenuItem _miTabsOnBottom; + private MenuItem [] _miTabsSide; + private MenuItem _cachedTabsSide; private TabView _tabView; public override void Main () @@ -23,6 +26,8 @@ public override void Main () // Setup - Create a top-level application window and configure it. Toplevel appWindow = new (); + _miTabsSide = SetTabsSide (); + var menu = new MenuBar { Menus = @@ -54,11 +59,12 @@ public override void Main () { Checked = true, CheckType = MenuItemCheckStyle.Checked }, - _miTabsOnBottom = - new ("_Tabs On Bottom", "", SetTabsOnBottom) - { - Checked = false, CheckType = MenuItemCheckStyle.Checked - }, + null, + _miTabsSide [0], + _miTabsSide [1], + _miTabsSide [2], + _miTabsSide [3], + null, _miShowTabViewBorder = new ( "_Show TabView Border", @@ -241,12 +247,41 @@ private View GetInteractiveTab () private void Quit () { Application.RequestStop (); } - private void SetTabsOnBottom () + private MenuItem [] SetTabsSide () { - _miTabsOnBottom.Checked = !_miTabsOnBottom.Checked; + List menuItems = []; - _tabView.Style.TabsOnBottom = (bool)_miTabsOnBottom.Checked; - _tabView.ApplyStyleChanges (); + foreach (TabSide side in Enum.GetValues (typeof (TabSide))) + { + string sideName = Enum.GetName (typeof (TabSide), side); + var item = new MenuItem { Title = $"_{sideName}", Data = side }; + item.CheckType |= MenuItemCheckStyle.Radio; + + item.Action += () => + { + if (_cachedTabsSide == item) + { + return; + } + + _cachedTabsSide.Checked = false; + item.Checked = true; + _cachedTabsSide = item; + _tabView.Style.TabsSide = (TabSide)item.Data; + _tabView.ApplyStyleChanges (); + }; + item.ShortcutKey = ((Key)sideName! [0].ToString ().ToLower ()).WithCtrl; + + if (sideName == "Top") + { + item.Checked = true; + _cachedTabsSide = item; + } + + menuItems.Add (item); + } + + return menuItems.ToArray (); } private void ShowBorder () diff --git a/UnitTests/Views/TabViewTests.cs b/UnitTests/Views/TabViewTests.cs index ac12a55564..8ffa49c2e8 100644 --- a/UnitTests/Views/TabViewTests.cs +++ b/UnitTests/Views/TabViewTests.cs @@ -736,7 +736,7 @@ public void ShowTopLine_False_TabsOnBottom_True_TestTabView_Width3 () TabView tv = GetTabView (out _, out _); tv.Width = 3; tv.Height = 5; - tv.Style = new () { ShowTopLine = false, TabsOnBottom = true }; + tv.Style = new () { ShowTopLine = false, TabsSide = TabSide.Bottom }; tv.ApplyStyleChanges (); tv.Layout (); @@ -760,7 +760,7 @@ public void ShowTopLine_False_TabsOnBottom_True_TestTabView_Width4 () TabView tv = GetTabView (out _, out _); tv.Width = 4; tv.Height = 5; - tv.Style = new () { ShowTopLine = false, TabsOnBottom = true }; + tv.Style = new () { ShowTopLine = false, TabsSide = TabSide.Bottom }; tv.ApplyStyleChanges (); tv.Layout (); @@ -784,7 +784,7 @@ public void ShowTopLine_False_TabsOnBottom_True_TestThinTabView_WithLongNames () TabView tv = GetTabView (out Tab tab1, out Tab tab2); tv.Width = 10; tv.Height = 5; - tv.Style = new () { ShowTopLine = false, TabsOnBottom = true }; + tv.Style = new () { ShowTopLine = false, TabsSide = TabSide.Bottom }; tv.ApplyStyleChanges (); tv.Layout (); @@ -1069,7 +1069,7 @@ public void ShowTopLine_True_TabsOnBottom_True_TestTabView_Width3 () TabView tv = GetTabView (out _, out _); tv.Width = 3; tv.Height = 5; - tv.Style = new () { TabsOnBottom = true }; + tv.Style = new () { TabsSide = TabSide.Bottom }; tv.ApplyStyleChanges (); tv.Layout (); @@ -1093,7 +1093,7 @@ public void ShowTopLine_True_TabsOnBottom_True_TestTabView_Width4 () TabView tv = GetTabView (out _, out _); tv.Width = 4; tv.Height = 5; - tv.Style = new () { TabsOnBottom = true }; + tv.Style = new () { TabsSide = TabSide.Bottom }; tv.ApplyStyleChanges (); tv.Layout (); @@ -1117,7 +1117,7 @@ public void ShowTopLine_True_TabsOnBottom_True_TestThinTabView_WithLongNames () TabView tv = GetTabView (out Tab tab1, out Tab tab2); tv.Width = 10; tv.Height = 5; - tv.Style = new () { TabsOnBottom = true }; + tv.Style = new () { TabsSide = TabSide.Bottom }; tv.ApplyStyleChanges (); tv.Layout (); @@ -1199,7 +1199,7 @@ public void ShowTopLine_True_TabsOnBottom_True_With_Unicode () TabView tv = GetTabView (out Tab tab1, out Tab tab2); tv.Width = 20; tv.Height = 5; - tv.Style = new () { TabsOnBottom = true }; + tv.Style = new () { TabsSide = TabSide.Bottom }; tv.ApplyStyleChanges (); tab1.DisplayText = "Tab0"; @@ -1381,7 +1381,7 @@ public void Add_Three_TabsOnBottom_ChangesTab () tv.Width = 20; tv.Height = 5; - tv.Style = new () { TabsOnBottom = true }; + tv.Style = new () { TabsSide = TabSide.Bottom }; tv.ApplyStyleChanges (); tv.Layout (); From 6dd1016d4c548f6ca511d54bada17ece04656e11 Mon Sep 17 00:00:00 2001 From: BDisp Date: Mon, 25 Nov 2024 22:18:28 +0000 Subject: [PATCH 02/18] Rename to ShowInitialLine. --- Terminal.Gui/Views/TabView/TabRow.cs | 22 +++++++++++----------- Terminal.Gui/Views/TabView/TabStyle.cs | 12 ++++++++---- Terminal.Gui/Views/TabView/TabView.cs | 8 ++++---- UICatalog/Scenarios/TabViewExample.cs | 2 +- UnitTests/Views/TabViewTests.cs | 12 ++++++------ 5 files changed, 30 insertions(+), 26 deletions(-) diff --git a/Terminal.Gui/Views/TabView/TabRow.cs b/Terminal.Gui/Views/TabView/TabRow.cs index ffff8ab4d8..0ee4f2b805 100644 --- a/Terminal.Gui/Views/TabView/TabRow.cs +++ b/Terminal.Gui/Views/TabView/TabRow.cs @@ -149,7 +149,7 @@ private void RenderTabLineCanvas () { View tab = tabLocations [i]; Rectangle vts = tab.ViewportToScreen (tab.Viewport); - int selectedOffset = _host.Style.ShowTopLine && tabLocations [i] == _host.SelectedTab ? 0 : 1; + int selectedOffset = _host.Style.ShowInitialLine && tabLocations [i] == _host.SelectedTab ? 0 : 1; if (tabLocations [i] == _host.SelectedTab) { @@ -233,7 +233,7 @@ private void RenderTabLineCanvas () throw new ArgumentOutOfRangeException (); } - if (_host.Style.ShowTopLine) + if (_host.Style.ShowInitialLine) { switch (_host.Style.TabsSide) { @@ -283,7 +283,7 @@ private void RenderTabLineCanvas () if (i < tabLocations.Length - 1) { - if (_host.Style.ShowTopLine) + if (_host.Style.ShowInitialLine) { switch (_host.Style.TabsSide) { @@ -382,7 +382,7 @@ private void RenderTabLineCanvas () switch (_host.Style.TabsSide) { case TabSide.Top: - if (_host.Style.ShowTopLine) + if (_host.Style.ShowInitialLine) { // ULCorner lc.AddLine ( @@ -417,7 +417,7 @@ private void RenderTabLineCanvas () break; case TabSide.Bottom: - if (_host.Style.ShowTopLine) + if (_host.Style.ShowInitialLine) { // LLCorner lc.AddLine ( @@ -461,7 +461,7 @@ private void RenderTabLineCanvas () } else if (i > 0) { - if (_host.Style.ShowTopLine || _host.Style.TabsSide == TabSide.Bottom) + if (_host.Style.ShowInitialLine || _host.Style.TabsSide == TabSide.Bottom) { // Upper left tee lc.AddLine ( @@ -497,7 +497,7 @@ private void RenderTabLineCanvas () } else if (i < tabLocations.Length - 1) { - if (_host.Style.ShowTopLine) + if (_host.Style.ShowInitialLine) { // Upper right tee lc.AddLine ( @@ -515,7 +515,7 @@ private void RenderTabLineCanvas () ); } - if (_host.Style.ShowTopLine || _host.Style.TabsSide == TabSide.Top) + if (_host.Style.ShowInitialLine || _host.Style.TabsSide == TabSide.Top) { // Lower right tee lc.AddLine ( @@ -649,7 +649,7 @@ private void RenderTabLineCanvas () { var arrowOffset = 1; - int lastSelectedTab = !_host.Style.ShowTopLine && i == selectedTab ? 1 : + int lastSelectedTab = !_host.Style.ShowInitialLine && i == selectedTab ? 1 : _host.Style.TabsSide == TabSide.Bottom ? 1 : 0; Rectangle tabsBarVts = ViewportToScreen (Viewport); int lineLength = tabsBarVts.Right - vts.Right; @@ -774,7 +774,7 @@ private int GetUnderlineYPosition () return 0; } - return _host.Style.ShowTopLine ? 2 : 1; + return _host.Style.ShowInitialLine ? 2 : 1; } /// Renders the line with the tab names in it. @@ -786,7 +786,7 @@ private void RenderTabLine () } View? selected = null; - int topLine = _host.Style.ShowTopLine ? 1 : 0; + int topLine = _host.Style.ShowInitialLine ? 1 : 0; foreach (Tab toRender in _host._tabLocations) { diff --git a/Terminal.Gui/Views/TabView/TabStyle.cs b/Terminal.Gui/Views/TabView/TabStyle.cs index c6734260c9..900b882631 100644 --- a/Terminal.Gui/Views/TabView/TabStyle.cs +++ b/Terminal.Gui/Views/TabView/TabStyle.cs @@ -8,11 +8,15 @@ public class TabStyle public bool ShowBorder { get; set; } = true; /// - /// True to show the top lip of tabs. False to directly begin with tab text during rendering. When true header - /// line occupies 3 rows, when false only 2. Defaults to true. - /// When is enabled this instead applies to the bottommost line of the control + /// True to show the top lip of tabs. False to directly begin with tab text during rendering. Defaults to true. + /// When true and or , header + /// line occupies 3 rows, when false only 2. + /// When is enabled this instead applies to the bottommost line of the control + /// When true and or , header + /// line occupies 1 more column, when false 1 column less. + /// When is enabled this instead applies to the rightmost column of the control /// - public bool ShowTopLine { get; set; } = true; + public bool ShowInitialLine { get; set; } = true; /// Gets or sets the tabs side to render. public TabSide TabsSide { get; set; } diff --git a/Terminal.Gui/Views/TabView/TabView.cs b/Terminal.Gui/Views/TabView/TabView.cs index fe856451c4..fe67db4006 100644 --- a/Terminal.Gui/Views/TabView/TabView.cs +++ b/Terminal.Gui/Views/TabView/TabView.cs @@ -478,7 +478,7 @@ internal IEnumerable CalculateViewport (Rectangle bounds) long maxWidth = Math.Max (0, Math.Min (bounds.Width - 3, MaxTabTextWidth)); tab.Width = 2; - tab.Height = Style.ShowTopLine ? 3 : 2; + tab.Height = Style.ShowInitialLine ? 3 : 2; // if tab view is width <= 3 don't render any tabs if (maxWidth == 0) @@ -503,7 +503,7 @@ internal IEnumerable CalculateViewport (Rectangle bounds) } tab.Width = Math.Max (tabTextWidth + 2, 1); - tab.Height = Style.ShowTopLine ? 3 : 2; + tab.Height = Style.ShowInitialLine ? 3 : 2; // if there is not enough space for this tab if (i + tabTextWidth >= bounds.Width) @@ -536,7 +536,7 @@ internal IEnumerable CalculateViewport (Rectangle bounds) } /// - /// Returns the number of rows occupied by rendering the tabs, this depends on + /// Returns the number of rows occupied by rendering the tabs, this depends on /// and can be 0 (e.g. if and you ask for ). /// /// True to measure the space required at the top of the control, false to measure space at the bottom. @@ -554,7 +554,7 @@ private int GetTabHeight (bool top) return 0; } - return Style.ShowTopLine ? 3 : 2; + return Style.ShowInitialLine ? 3 : 2; } internal void Tab_MouseClick (object sender, MouseEventArgs e) diff --git a/UICatalog/Scenarios/TabViewExample.cs b/UICatalog/Scenarios/TabViewExample.cs index b3f896e2e6..1d107aee97 100644 --- a/UICatalog/Scenarios/TabViewExample.cs +++ b/UICatalog/Scenarios/TabViewExample.cs @@ -306,7 +306,7 @@ private void ShowTopLine () { _miShowTopLine.Checked = !_miShowTopLine.Checked; - _tabView.Style.ShowTopLine = (bool)_miShowTopLine.Checked; + _tabView.Style.ShowInitialLine = (bool)_miShowTopLine.Checked; _tabView.ApplyStyleChanges (); } } diff --git a/UnitTests/Views/TabViewTests.cs b/UnitTests/Views/TabViewTests.cs index 8ffa49c2e8..d97f8e73ab 100644 --- a/UnitTests/Views/TabViewTests.cs +++ b/UnitTests/Views/TabViewTests.cs @@ -589,7 +589,7 @@ public void ShowTopLine_False_TabsOnBottom_False_TestTabView_Width3 () TabView tv = GetTabView (out _, out _); tv.Width = 3; tv.Height = 5; - tv.Style = new () { ShowTopLine = false }; + tv.Style = new () { ShowInitialLine = false }; tv.ApplyStyleChanges (); tv.Layout (); @@ -613,7 +613,7 @@ public void ShowTopLine_False_TabsOnBottom_False_TestTabView_Width4 () TabView tv = GetTabView (out _, out _); tv.Width = 4; tv.Height = 5; - tv.Style = new () { ShowTopLine = false }; + tv.Style = new () { ShowInitialLine = false }; tv.ApplyStyleChanges (); tv.Layout (); @@ -637,7 +637,7 @@ public void ShowTopLine_False_TabsOnBottom_False_TestThinTabView_WithLongNames ( TabView tv = GetTabView (out Tab tab1, out Tab tab2); tv.Width = 10; tv.Height = 5; - tv.Style = new () { ShowTopLine = false }; + tv.Style = new () { ShowInitialLine = false }; tv.ApplyStyleChanges (); // Test two tab names that fit @@ -736,7 +736,7 @@ public void ShowTopLine_False_TabsOnBottom_True_TestTabView_Width3 () TabView tv = GetTabView (out _, out _); tv.Width = 3; tv.Height = 5; - tv.Style = new () { ShowTopLine = false, TabsSide = TabSide.Bottom }; + tv.Style = new () { ShowInitialLine = false, TabsSide = TabSide.Bottom }; tv.ApplyStyleChanges (); tv.Layout (); @@ -760,7 +760,7 @@ public void ShowTopLine_False_TabsOnBottom_True_TestTabView_Width4 () TabView tv = GetTabView (out _, out _); tv.Width = 4; tv.Height = 5; - tv.Style = new () { ShowTopLine = false, TabsSide = TabSide.Bottom }; + tv.Style = new () { ShowInitialLine = false, TabsSide = TabSide.Bottom }; tv.ApplyStyleChanges (); tv.Layout (); @@ -784,7 +784,7 @@ public void ShowTopLine_False_TabsOnBottom_True_TestThinTabView_WithLongNames () TabView tv = GetTabView (out Tab tab1, out Tab tab2); tv.Width = 10; tv.Height = 5; - tv.Style = new () { ShowTopLine = false, TabsSide = TabSide.Bottom }; + tv.Style = new () { ShowInitialLine = false, TabsSide = TabSide.Bottom }; tv.ApplyStyleChanges (); tv.Layout (); From 58e929df33da6a07e84633b43a00684b3047b80c Mon Sep 17 00:00:00 2001 From: BDisp Date: Tue, 26 Nov 2024 13:35:33 +0000 Subject: [PATCH 03/18] Replace ShowTopLine with ShowInitialLine. --- UnitTests/Views/TabViewTests.cs | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/UnitTests/Views/TabViewTests.cs b/UnitTests/Views/TabViewTests.cs index d97f8e73ab..f6bda567c4 100644 --- a/UnitTests/Views/TabViewTests.cs +++ b/UnitTests/Views/TabViewTests.cs @@ -584,7 +584,7 @@ public void SelectedTabChanged_Called () [Fact] [SetupFakeDriver] - public void ShowTopLine_False_TabsOnBottom_False_TestTabView_Width3 () + public void ShowInitialLine_False_TabsOnBottom_False_TestTabView_Width3 () { TabView tv = GetTabView (out _, out _); tv.Width = 3; @@ -608,7 +608,7 @@ public void ShowTopLine_False_TabsOnBottom_False_TestTabView_Width3 () [Fact] [SetupFakeDriver] - public void ShowTopLine_False_TabsOnBottom_False_TestTabView_Width4 () + public void ShowInitialLine_False_TabsOnBottom_False_TestTabView_Width4 () { TabView tv = GetTabView (out _, out _); tv.Width = 4; @@ -632,7 +632,7 @@ public void ShowTopLine_False_TabsOnBottom_False_TestTabView_Width4 () [Fact] [SetupFakeDriver] - public void ShowTopLine_False_TabsOnBottom_False_TestThinTabView_WithLongNames () + public void ShowInitialLine_False_TabsOnBottom_False_TestThinTabView_WithLongNames () { TabView tv = GetTabView (out Tab tab1, out Tab tab2); tv.Width = 10; @@ -731,7 +731,7 @@ public void ShowTopLine_False_TabsOnBottom_False_TestThinTabView_WithLongNames ( [Fact] [SetupFakeDriver] - public void ShowTopLine_False_TabsOnBottom_True_TestTabView_Width3 () + public void ShowInitialLine_False_TabsOnBottom_True_TestTabView_Width3 () { TabView tv = GetTabView (out _, out _); tv.Width = 3; @@ -755,7 +755,7 @@ public void ShowTopLine_False_TabsOnBottom_True_TestTabView_Width3 () [Fact] [SetupFakeDriver] - public void ShowTopLine_False_TabsOnBottom_True_TestTabView_Width4 () + public void ShowInitialLine_False_TabsOnBottom_True_TestTabView_Width4 () { TabView tv = GetTabView (out _, out _); tv.Width = 4; @@ -779,7 +779,7 @@ public void ShowTopLine_False_TabsOnBottom_True_TestTabView_Width4 () [Fact] [SetupFakeDriver] - public void ShowTopLine_False_TabsOnBottom_True_TestThinTabView_WithLongNames () + public void ShowInitialLine_False_TabsOnBottom_True_TestThinTabView_WithLongNames () { TabView tv = GetTabView (out Tab tab1, out Tab tab2); tv.Width = 10; @@ -880,7 +880,7 @@ public void ShowTopLine_False_TabsOnBottom_True_TestThinTabView_WithLongNames () [Fact] [SetupFakeDriver] - public void ShowTopLine_True_TabsOnBottom_False_TestTabView_Width3 () + public void ShowInitialLine_True_TabsOnBottom_False_TestTabView_Width3 () { TabView tv = GetTabView (out _, out _); tv.Width = 3; @@ -902,7 +902,7 @@ public void ShowTopLine_True_TabsOnBottom_False_TestTabView_Width3 () [Fact] [SetupFakeDriver] - public void ShowTopLine_True_TabsOnBottom_False_TestTabView_Width4 () + public void ShowInitialLine_True_TabsOnBottom_False_TestTabView_Width4 () { TabView tv = GetTabView (out _, out _); tv.Width = 4; @@ -925,7 +925,7 @@ public void ShowTopLine_True_TabsOnBottom_False_TestTabView_Width4 () [Fact] [SetupFakeDriver] - public void ShowTopLine_True_TabsOnBottom_False_TestThinTabView_WithLongNames () + public void ShowInitialLine_True_TabsOnBottom_False_TestThinTabView_WithLongNames () { TabView tv = GetTabView (out Tab tab1, out Tab tab2); tv.Width = 10; @@ -1022,7 +1022,7 @@ public void ShowTopLine_True_TabsOnBottom_False_TestThinTabView_WithLongNames () [Fact] [SetupFakeDriver] - public void ShowTopLine_True_TabsOnBottom_False_With_Unicode () + public void ShowInitialLine_True_TabsOnBottom_False_With_Unicode () { TabView tv = GetTabView (out Tab tab1, out Tab tab2); tv.Width = 20; @@ -1064,7 +1064,7 @@ public void ShowTopLine_True_TabsOnBottom_False_With_Unicode () [Fact] [SetupFakeDriver] - public void ShowTopLine_True_TabsOnBottom_True_TestTabView_Width3 () + public void ShowInitialLine_True_TabsOnBottom_True_TestTabView_Width3 () { TabView tv = GetTabView (out _, out _); tv.Width = 3; @@ -1088,7 +1088,7 @@ public void ShowTopLine_True_TabsOnBottom_True_TestTabView_Width3 () [Fact] [SetupFakeDriver] - public void ShowTopLine_True_TabsOnBottom_True_TestTabView_Width4 () + public void ShowInitialLine_True_TabsOnBottom_True_TestTabView_Width4 () { TabView tv = GetTabView (out _, out _); tv.Width = 4; @@ -1112,7 +1112,7 @@ public void ShowTopLine_True_TabsOnBottom_True_TestTabView_Width4 () [Fact] [SetupFakeDriver] - public void ShowTopLine_True_TabsOnBottom_True_TestThinTabView_WithLongNames () + public void ShowInitialLine_True_TabsOnBottom_True_TestThinTabView_WithLongNames () { TabView tv = GetTabView (out Tab tab1, out Tab tab2); tv.Width = 10; @@ -1194,7 +1194,7 @@ public void ShowTopLine_True_TabsOnBottom_True_TestThinTabView_WithLongNames () [Fact] [SetupFakeDriver] - public void ShowTopLine_True_TabsOnBottom_True_With_Unicode () + public void ShowInitialLine_True_TabsOnBottom_True_With_Unicode () { TabView tv = GetTabView (out Tab tab1, out Tab tab2); tv.Width = 20; From a1d14136150e0e2ee63ae73854c216b72df02382 Mon Sep 17 00:00:00 2001 From: BDisp Date: Tue, 26 Nov 2024 13:48:47 +0000 Subject: [PATCH 04/18] Replace TabsOnBottom_False with TabsSide_Top. --- UnitTests/Views/TabViewTests.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/UnitTests/Views/TabViewTests.cs b/UnitTests/Views/TabViewTests.cs index f6bda567c4..5768ac832c 100644 --- a/UnitTests/Views/TabViewTests.cs +++ b/UnitTests/Views/TabViewTests.cs @@ -584,7 +584,7 @@ public void SelectedTabChanged_Called () [Fact] [SetupFakeDriver] - public void ShowInitialLine_False_TabsOnBottom_False_TestTabView_Width3 () + public void ShowInitialLine_False_TabsSide_Top_TestTabView_Width3 () { TabView tv = GetTabView (out _, out _); tv.Width = 3; @@ -608,7 +608,7 @@ public void ShowInitialLine_False_TabsOnBottom_False_TestTabView_Width3 () [Fact] [SetupFakeDriver] - public void ShowInitialLine_False_TabsOnBottom_False_TestTabView_Width4 () + public void ShowInitialLine_False_TabsSide_Top_TestTabView_Width4 () { TabView tv = GetTabView (out _, out _); tv.Width = 4; @@ -632,7 +632,7 @@ public void ShowInitialLine_False_TabsOnBottom_False_TestTabView_Width4 () [Fact] [SetupFakeDriver] - public void ShowInitialLine_False_TabsOnBottom_False_TestThinTabView_WithLongNames () + public void ShowInitialLine_False_TabsSide_Top_TestThinTabView_WithLongNames () { TabView tv = GetTabView (out Tab tab1, out Tab tab2); tv.Width = 10; @@ -880,7 +880,7 @@ public void ShowInitialLine_False_TabsOnBottom_True_TestThinTabView_WithLongName [Fact] [SetupFakeDriver] - public void ShowInitialLine_True_TabsOnBottom_False_TestTabView_Width3 () + public void ShowInitialLine_True_TabsSide_Top_TestTabView_Width3 () { TabView tv = GetTabView (out _, out _); tv.Width = 3; @@ -902,7 +902,7 @@ public void ShowInitialLine_True_TabsOnBottom_False_TestTabView_Width3 () [Fact] [SetupFakeDriver] - public void ShowInitialLine_True_TabsOnBottom_False_TestTabView_Width4 () + public void ShowInitialLine_True_TabsSide_Top_TestTabView_Width4 () { TabView tv = GetTabView (out _, out _); tv.Width = 4; @@ -925,7 +925,7 @@ public void ShowInitialLine_True_TabsOnBottom_False_TestTabView_Width4 () [Fact] [SetupFakeDriver] - public void ShowInitialLine_True_TabsOnBottom_False_TestThinTabView_WithLongNames () + public void ShowInitialLine_True_TabsSide_Top_TestThinTabView_WithLongNames () { TabView tv = GetTabView (out Tab tab1, out Tab tab2); tv.Width = 10; @@ -1022,7 +1022,7 @@ public void ShowInitialLine_True_TabsOnBottom_False_TestThinTabView_WithLongName [Fact] [SetupFakeDriver] - public void ShowInitialLine_True_TabsOnBottom_False_With_Unicode () + public void ShowInitialLine_True_TabsSide_Top_With_Unicode () { TabView tv = GetTabView (out Tab tab1, out Tab tab2); tv.Width = 20; From 76332b262d87867058b98773e0ad98f57d8c64a4 Mon Sep 17 00:00:00 2001 From: BDisp Date: Tue, 26 Nov 2024 13:50:52 +0000 Subject: [PATCH 05/18] Replace TabsOnBottom_True with TabsSide_Bottom. --- UnitTests/Views/TabViewTests.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/UnitTests/Views/TabViewTests.cs b/UnitTests/Views/TabViewTests.cs index 5768ac832c..457291b966 100644 --- a/UnitTests/Views/TabViewTests.cs +++ b/UnitTests/Views/TabViewTests.cs @@ -731,7 +731,7 @@ public void ShowInitialLine_False_TabsSide_Top_TestThinTabView_WithLongNames () [Fact] [SetupFakeDriver] - public void ShowInitialLine_False_TabsOnBottom_True_TestTabView_Width3 () + public void ShowInitialLine_False_TabsSide_Bottom_TestTabView_Width3 () { TabView tv = GetTabView (out _, out _); tv.Width = 3; @@ -755,7 +755,7 @@ public void ShowInitialLine_False_TabsOnBottom_True_TestTabView_Width3 () [Fact] [SetupFakeDriver] - public void ShowInitialLine_False_TabsOnBottom_True_TestTabView_Width4 () + public void ShowInitialLine_False_TabsSide_Bottom_TestTabView_Width4 () { TabView tv = GetTabView (out _, out _); tv.Width = 4; @@ -779,7 +779,7 @@ public void ShowInitialLine_False_TabsOnBottom_True_TestTabView_Width4 () [Fact] [SetupFakeDriver] - public void ShowInitialLine_False_TabsOnBottom_True_TestThinTabView_WithLongNames () + public void ShowInitialLine_False_TabsSide_Bottom_TestThinTabView_WithLongNames () { TabView tv = GetTabView (out Tab tab1, out Tab tab2); tv.Width = 10; @@ -1064,7 +1064,7 @@ public void ShowInitialLine_True_TabsSide_Top_With_Unicode () [Fact] [SetupFakeDriver] - public void ShowInitialLine_True_TabsOnBottom_True_TestTabView_Width3 () + public void ShowInitialLine_True_TabsSide_Bottom_TestTabView_Width3 () { TabView tv = GetTabView (out _, out _); tv.Width = 3; @@ -1088,7 +1088,7 @@ public void ShowInitialLine_True_TabsOnBottom_True_TestTabView_Width3 () [Fact] [SetupFakeDriver] - public void ShowInitialLine_True_TabsOnBottom_True_TestTabView_Width4 () + public void ShowInitialLine_True_TabsSide_Bottom_TestTabView_Width4 () { TabView tv = GetTabView (out _, out _); tv.Width = 4; @@ -1112,7 +1112,7 @@ public void ShowInitialLine_True_TabsOnBottom_True_TestTabView_Width4 () [Fact] [SetupFakeDriver] - public void ShowInitialLine_True_TabsOnBottom_True_TestThinTabView_WithLongNames () + public void ShowInitialLine_True_TabsSide_Bottom_TestThinTabView_WithLongNames () { TabView tv = GetTabView (out Tab tab1, out Tab tab2); tv.Width = 10; @@ -1194,7 +1194,7 @@ public void ShowInitialLine_True_TabsOnBottom_True_TestThinTabView_WithLongNames [Fact] [SetupFakeDriver] - public void ShowInitialLine_True_TabsOnBottom_True_With_Unicode () + public void ShowInitialLine_True_TabsSide_Bottom_With_Unicode () { TabView tv = GetTabView (out Tab tab1, out Tab tab2); tv.Width = 20; From b2b32889eed326ec63aae700d36d85c62dd7ae60 Mon Sep 17 00:00:00 2001 From: BDisp Date: Tue, 26 Nov 2024 17:37:25 +0000 Subject: [PATCH 06/18] Fix bug that was passing the TabRow.Viewport instead of the TabView.Viewport. --- Terminal.Gui/Views/TabView/TabRow.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Terminal.Gui/Views/TabView/TabRow.cs b/Terminal.Gui/Views/TabView/TabRow.cs index 0ee4f2b805..f421e4c106 100644 --- a/Terminal.Gui/Views/TabView/TabRow.cs +++ b/Terminal.Gui/Views/TabView/TabRow.cs @@ -117,7 +117,7 @@ protected override void OnHasFocusChanged (bool newHasFocus, View? previousFocus /// protected override void OnSubviewLayout (LayoutEventArgs args) { - _host._tabLocations = _host.CalculateViewport (Viewport).ToArray (); + _host._tabLocations = _host.CalculateViewport (_host.Viewport).ToArray (); RenderTabLine (); From 7d6bb3718dc466dbe13c18d381b73e5f4a8c0585 Mon Sep 17 00:00:00 2001 From: BDisp Date: Wed, 27 Nov 2024 21:35:34 +0000 Subject: [PATCH 07/18] Add DisplayTextChanged event. --- Terminal.Gui/Views/TabView/Tab.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Terminal.Gui/Views/TabView/Tab.cs b/Terminal.Gui/Views/TabView/Tab.cs index 52fb0bdf9d..8451b74136 100644 --- a/Terminal.Gui/Views/TabView/Tab.cs +++ b/Terminal.Gui/Views/TabView/Tab.cs @@ -22,6 +22,7 @@ public string DisplayText set { _displayText = value; + DisplayTextChanged?.Invoke (this, EventArgs.Empty); SetNeedsLayout (); } } @@ -29,4 +30,9 @@ public string DisplayText /// The control to display when the tab is selected. /// public View? View { get; set; } + + /// + /// Raised when changed. + /// + public event EventHandler? DisplayTextChanged; } From 97246256ed5cf46255a8c0e3bd8d04ae11bc1a17 Mon Sep 17 00:00:00 2001 From: BDisp Date: Wed, 27 Nov 2024 21:37:43 +0000 Subject: [PATCH 08/18] Fix unit test. Now F6 focus the select tab. --- UnitTests/Views/TabViewTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/UnitTests/Views/TabViewTests.cs b/UnitTests/Views/TabViewTests.cs index 457291b966..4cced1ff5d 100644 --- a/UnitTests/Views/TabViewTests.cs +++ b/UnitTests/Views/TabViewTests.cs @@ -378,8 +378,8 @@ public void ProcessKey_Down_Up_Right_Left_Home_End_PageDown_PageUp_F6 () Assert.Equal (tv.MostFocused, top.Focused.MostFocused); Assert.Equal (tv.SelectedTab.View, top.Focused.MostFocused); - // Press the cursor up key to focus the selected tab - Assert.True (Application.RaiseKeyDownEvent (Key.CursorUp)); + // Press F6 key to focus the selected tab + Assert.True (Application.RaiseKeyDownEvent (Key.F6)); Application.LayoutAndDraw (); // Is the selected tab focused From fa1bdae84d9faaf9a3a7e17f585509347fc91441 Mon Sep 17 00:00:00 2001 From: BDisp Date: Thu, 28 Nov 2024 00:06:11 +0000 Subject: [PATCH 09/18] Add Command.Up and Command.Down. --- Terminal.Gui/Views/TabView/TabRow.cs | 1 + Terminal.Gui/Views/TabView/TabView.cs | 48 ++++++++++++++++++++++++--- 2 files changed, 44 insertions(+), 5 deletions(-) diff --git a/Terminal.Gui/Views/TabView/TabRow.cs b/Terminal.Gui/Views/TabView/TabRow.cs index f421e4c106..e9aecf7727 100644 --- a/Terminal.Gui/Views/TabView/TabRow.cs +++ b/Terminal.Gui/Views/TabView/TabRow.cs @@ -13,6 +13,7 @@ public TabRow (TabView host) Id = "tabRow"; CanFocus = true; + // Because TabRow has focusable subviews, it must be a TabGroup TabStop = TabBehavior.TabGroup; Width = Dim.Fill (); diff --git a/Terminal.Gui/Views/TabView/TabView.cs b/Terminal.Gui/Views/TabView/TabView.cs index fe67db4006..3da5baf232 100644 --- a/Terminal.Gui/Views/TabView/TabView.cs +++ b/Terminal.Gui/Views/TabView/TabView.cs @@ -27,8 +27,8 @@ public class TabView : View public TabView () { CanFocus = true; - TabStop = TabBehavior.TabStop; // Because TabView has focusable subviews, it must be a TabGroup - _tabsBar = new TabRow (this); + TabStop = TabBehavior.TabStop; + _tabsBar = new (this); _containerView = new (); ApplyStyleChanges (); @@ -36,9 +36,45 @@ public TabView () base.Add (_containerView); // Things this view knows how to do - AddCommand (Command.Left, () => SwitchTabBy (-1)); - - AddCommand (Command.Right, () => SwitchTabBy (1)); + AddCommand (Command.Left, () => + { + if (Style.TabsSide is TabSide.Top or TabSide.Bottom) + { + return SwitchTabBy (-1); + } + + return false; + }); + + AddCommand (Command.Right, () => + { + if (Style.TabsSide is TabSide.Top or TabSide.Bottom) + { + return SwitchTabBy (1); + } + + return false; + }); + + AddCommand (Command.Up, () => + { + if (Style.TabsSide is TabSide.Left or TabSide.Right) + { + return SwitchTabBy (-1); + } + + return false; + }); + + AddCommand (Command.Down, () => + { + if (Style.TabsSide is TabSide.Left or TabSide.Right) + { + return SwitchTabBy (1); + } + + return false; + }); AddCommand ( Command.LeftStart, @@ -87,6 +123,8 @@ public TabView () // Default keybindings for this view KeyBindings.Add (Key.CursorLeft, Command.Left); KeyBindings.Add (Key.CursorRight, Command.Right); + KeyBindings.Add (Key.CursorUp, Command.Up); + KeyBindings.Add (Key.CursorDown, Command.Down); KeyBindings.Add (Key.Home, Command.LeftStart); KeyBindings.Add (Key.End, Command.RightEnd); KeyBindings.Add (Key.PageDown, Command.PageDown); From 427d1183d56456bec20942a8a546e5df7a520c8f Mon Sep 17 00:00:00 2001 From: BDisp Date: Thu, 28 Nov 2024 00:19:40 +0000 Subject: [PATCH 10/18] Starting TabSide.Left feature. --- Terminal.Gui/Views/TabView/TabRow.cs | 111 ++------ Terminal.Gui/Views/TabView/TabView.cs | 376 +++++++++++++++++++++----- 2 files changed, 321 insertions(+), 166 deletions(-) diff --git a/Terminal.Gui/Views/TabView/TabRow.cs b/Terminal.Gui/Views/TabView/TabRow.cs index e9aecf7727..26ef662ae8 100644 --- a/Terminal.Gui/Views/TabView/TabRow.cs +++ b/Terminal.Gui/Views/TabView/TabRow.cs @@ -40,6 +40,14 @@ public TabRow (TabView host) Add (_rightScrollIndicator, _leftScrollIndicator); } + /// + public override void EndInit () + { + _host._tabLocations = _host.CalculateViewport (Viewport); + + base.EndInit (); + } + protected override bool OnMouseEvent (MouseEventArgs me) { View? parent = me.View is Adornment adornment ? adornment.Parent : me.View; @@ -118,9 +126,16 @@ protected override void OnHasFocusChanged (bool newHasFocus, View? previousFocus /// protected override void OnSubviewLayout (LayoutEventArgs args) { - _host._tabLocations = _host.CalculateViewport (_host.Viewport).ToArray (); + if (_host._tabLocations is null) + { + return; + } - RenderTabLine (); + if (_host is { SelectedTab: { }, _tabLocations: { } } && !_host._tabLocations!.Contains (_host.SelectedTab)) + { + _host.SelectedTab = _host._tabLocations [0]; + Application.Invoke (() => _host.SetNeedsLayout ()); + } RenderUnderline (); @@ -765,7 +780,7 @@ private void RenderTabLineCanvas () } } - _host.LineCanvas.Merge (lc); + LineCanvas.Merge (lc); } private int GetUnderlineYPosition () @@ -778,96 +793,6 @@ private int GetUnderlineYPosition () return _host.Style.ShowInitialLine ? 2 : 1; } - /// Renders the line with the tab names in it. - private void RenderTabLine () - { - if (_host._tabLocations is null) - { - return; - } - - View? selected = null; - int topLine = _host.Style.ShowInitialLine ? 1 : 0; - - foreach (Tab toRender in _host._tabLocations) - { - Tab tab = toRender; - - if (toRender == _host.SelectedTab) - { - selected = tab; - - switch (_host.Style.TabsSide) - { - case TabSide.Top: - tab.Border!.Thickness = new (1, topLine, 1, 0); - tab.Margin!.Thickness = new (0, 0, 0, topLine); - - break; - case TabSide.Bottom: - tab.Border!.Thickness = new (1, 0, 1, topLine); - tab.Margin!.Thickness = new (0, 1, 0, 0); - - break; - case TabSide.Left: - break; - case TabSide.Right: - break; - default: - throw new ArgumentOutOfRangeException (); - } - } - else if (selected is null) - { - switch (_host.Style.TabsSide) - { - case TabSide.Top: - tab.Border!.Thickness = new (1, topLine, 1, 1); - tab.Margin!.Thickness = new (0, 0, 0, 0); - - break; - case TabSide.Bottom: - tab.Border!.Thickness = new (1, 1, 1, topLine); - tab.Margin!.Thickness = new (0, 0, 0, 0); - - break; - case TabSide.Left: - break; - case TabSide.Right: - break; - default: - throw new ArgumentOutOfRangeException (); - } - } - else - { - switch (_host.Style.TabsSide) - { - case TabSide.Top: - tab.Border!.Thickness = new (1, topLine, 1, 1); - tab.Margin!.Thickness = new (0, 0, 0, 0); - - break; - case TabSide.Bottom: - tab.Border!.Thickness = new (1, 1, 1, topLine); - tab.Margin!.Thickness = new (0, 0, 0, 0); - - break; - case TabSide.Left: - break; - case TabSide.Right: - break; - default: - throw new ArgumentOutOfRangeException (); - } - } - - // Ensures updating TextFormatter constrains - tab.TextFormatter.ConstrainToWidth = tab.GetContentSize ().Width; - tab.TextFormatter.ConstrainToHeight = tab.GetContentSize ().Height; - } - } - /// Renders the line of the tab that adjoins the content of the tab. private void RenderUnderline () { diff --git a/Terminal.Gui/Views/TabView/TabView.cs b/Terminal.Gui/Views/TabView/TabView.cs index 3da5baf232..a136a199c1 100644 --- a/Terminal.Gui/Views/TabView/TabView.cs +++ b/Terminal.Gui/Views/TabView/TabView.cs @@ -267,57 +267,71 @@ public void AddTab (Tab tab, bool andSelect) /// public void ApplyStyleChanges () { + _tabLocations = CalculateViewport (Viewport); + _containerView.BorderStyle = Style.ShowBorder ? LineStyle.Single : LineStyle.None; _containerView.Width = Dim.Fill (); - int tabHeight; - switch (Style.TabsSide) { case TabSide.Top: // Tabs are along the top if (Style.ShowBorder) { - _containerView.Border!.Thickness = new Thickness (1, 0, 1, 1); + _containerView.Border!.Thickness = new (1, 0, 1, 1); } + _tabsBar.X = 0; _tabsBar.Y = 0; + _tabsBar.Width = Dim.Fill (); + _tabsBar.Height = GetTabHeight (true); - tabHeight = GetTabHeight (true); - + _containerView.X = 0; //move content down to make space for tabs _containerView.Y = Pos.Bottom (_tabsBar); - - // Fill client area leaving space at bottom for border + _containerView.Width = Dim.Fill (); _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 - break; case TabSide.Bottom: // Tabs are along the bottom so just dodge the border if (Style.ShowBorder) { - _containerView.Border!.Thickness = new Thickness (1, 1, 1, 0); + _containerView.Border!.Thickness = new (1, 1, 1, 0); } - _containerView.Y = 0; - - tabHeight = GetTabHeight (false); + _tabsBar.X = 0; + _tabsBar.Width = Dim.Fill (); + int tabHeight = GetTabHeight (false); + _tabsBar.Height = tabHeight; + _containerView.X = 0; + _containerView.Y = 0; + _containerView.Width = Dim.Fill (); // Fill client area leaving space at bottom for tabs _containerView.Height = Dim.Fill (tabHeight); - _tabsBar.Height = tabHeight; - _tabsBar.Y = Pos.Bottom (_containerView); break; case TabSide.Left: + // Tabs are along the left + if (Style.ShowBorder) + { + _containerView.Border!.Thickness = new (0, 1, 1, 1); + } + + _tabsBar.X = 0; + _tabsBar.Y = 0; + _tabsBar.Height = Dim.Fill (); + + //move content right to make space for tabs + _containerView.X = Pos.Right (_tabsBar); + _containerView.Y = 0; + // Fill client area leaving space at left for tabs + _containerView.Width = Dim.Fill (); + _containerView.Height = Dim.Fill (); + break; case TabSide.Right: break; @@ -331,7 +345,7 @@ public void ApplyStyleChanges () /// protected override void OnViewportChanged (DrawEventArgs e) { - _tabLocations = CalculateViewport (Viewport).ToArray (); + _tabLocations = CalculateViewport (Viewport); base.OnViewportChanged (e); } @@ -345,10 +359,15 @@ public void EnsureSelectedTabIsVisible () } // if current viewport does not include the selected tab - if (!CalculateViewport (Viewport).Any (t => Equals (SelectedTab, t))) + if (_tabLocations is null || (_tabLocations is { } && !_tabLocations.Any (t => Equals (SelectedTab, t)))) { // Set scroll offset so the first tab rendered is the TabScrollOffset = Math.Max (0, Tabs.IndexOf (SelectedTab)); + _tabLocations = CalculateViewport (Viewport); + } + else + { + RenderTabLine (_tabLocations); } } @@ -486,81 +505,180 @@ protected virtual void OnSelectedTabChanged (Tab oldTab, Tab newTab) /// Returns which tabs to render at each x location. /// - internal IEnumerable CalculateViewport (Rectangle bounds) + internal Tab []? CalculateViewport (Rectangle bounds) { UnSetCurrentTabs (); + List tabs = []; var i = 1; View? prevTab = null; - // Starting at the first or scrolled to tab - foreach (Tab tab in Tabs.Skip (TabScrollOffset)) + switch (Style.TabsSide) { - if (prevTab is { }) - { - tab.X = Pos.Right (prevTab) - 1; - } - else - { - tab.X = 0; - } + case TabSide.Top: + case TabSide.Bottom: + // 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; + tab.Y = 0; - // while there is space for the tab - int tabTextWidth = tab.DisplayText.EnumerateRunes ().Sum (c => c.GetColumns ()); + // 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)); + // 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.ShowInitialLine ? 3 : 2; + tab.Width = 2; + tab.Height = Style.ShowInitialLine ? 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!; + // 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!; + tab.DisplayTextChanged += Tab_DisplayTextChanged; - yield return tab; + tabs.Add (tab); - break; - } + break; + } - if (tabTextWidth > maxWidth) - { - tab.Text = tab.DisplayText.Substring (0, (int)maxWidth); - tabTextWidth = (int)maxWidth; - } - else - { - tab.Text = tab.DisplayText; - } + 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.ShowInitialLine ? 3 : 2; + tab.Width = tabTextWidth + 2; + tab.Height = Style.ShowInitialLine ? 3 : 2; - // if there is not enough space for this tab - if (i + tabTextWidth >= bounds.Width) - { - tab.Visible = false; + // 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!; + tab.DisplayTextChanged += Tab_DisplayTextChanged; + + tabs.Add (tab); + + prevTab = tab; + + i += tabTextWidth + 1; + } break; - } + case TabSide.Left: + case TabSide.Right: + int maxColWidth = 0; + + // Starting at the first or scrolled to tab + foreach (Tab tab in Tabs.Skip (TabScrollOffset)) + { + tab.X = 0; - // there is enough space! - tab.Visible = true; - tab.MouseClick += Tab_MouseClick!; - tab.Border!.MouseClick += Tab_MouseClick!; + if (prevTab is { }) + { + tab.Y = Pos.Bottom (prevTab) - 1; + } + else + { + tab.Y = 0; + } - yield return tab; + // while there is space for the tab + int tabTextWidth = tab.DisplayText.EnumerateRunes ().Sum (c => c.GetColumns ()); - prevTab = tab; + // 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 - (Style.ShowInitialLine ? 2 : 1), MaxTabTextWidth)); + + // The maximum height to use for the tab. But not more than the height of the view + // or we won't even be able to render a single tab! + int maxHeight = Math.Max (0, Math.Min (bounds.Height - 3, 3)); + + tab.Height = 2; + + // if tab view is height <= 3 don't render any tabs + if (maxHeight == 0) + { + tab.Width = maxColWidth = Math.Max (Style.ShowInitialLine ? 3 : 2, maxColWidth); + tab.Visible = true; + tab.MouseClick += Tab_MouseClick!; + tab.Border!.MouseClick += Tab_MouseClick!; + tab.DisplayTextChanged += Tab_DisplayTextChanged; + + tabs.Add (tab); + + break; + } + + if (tabTextWidth > maxWidth) + { + tab.Text = tab.DisplayText.Substring (0, (int)maxWidth); + tabTextWidth = (int)maxWidth; + } + else + { + tab.Text = tab.DisplayText; + } - i += tabTextWidth + 1; + maxColWidth = Math.Max (tabTextWidth + 2, maxColWidth); + tab.Height = 3; + + // if there is not enough space for this tab + if (i + 1 >= bounds.Height) + { + tab.Visible = false; + + break; + } + + // there is enough space! + tab.Visible = true; + tab.MouseClick += Tab_MouseClick!; + tab.Border!.MouseClick += Tab_MouseClick!; + tab.DisplayTextChanged += Tab_DisplayTextChanged; + + tabs.Add (tab); + + prevTab = tab; + + i += 2; + } + + foreach (Tab t in tabs) + { + t.Width = maxColWidth; + } + _tabsBar.Width = maxColWidth; + + break; + default: + throw new ArgumentOutOfRangeException (); } if (TabCanSetFocus ()) @@ -571,6 +689,116 @@ internal IEnumerable CalculateViewport (Rectangle bounds) { SelectedTab?.View?.SetFocus (); } + + RenderTabLine (tabs.Count == 0 ? null : tabs.ToArray ()); + + SetNeedsLayout (); + + return tabs.Count == 0 ? null : tabs.ToArray (); + } + + private void Tab_DisplayTextChanged (object? sender, EventArgs e) + { + _tabLocations = CalculateViewport (Viewport); + } + + /// Renders the line with the tab names in it. + private void RenderTabLine (Tab []? tabLocations) + { + if (tabLocations is null) + { + return; + } + + View? selected = null; + int topLine = Style.ShowInitialLine ? 1 : 0; + + foreach (Tab toRender in tabLocations) + { + Tab tab = toRender; + + if (toRender == SelectedTab) + { + selected = tab; + + switch (Style.TabsSide) + { + case TabSide.Top: + tab.Border!.Thickness = new (1, topLine, 1, 0); + tab.Margin!.Thickness = new (0, 0, 0, topLine); + + break; + case TabSide.Bottom: + tab.Border!.Thickness = new (1, 0, 1, topLine); + tab.Margin!.Thickness = new (0, 1, 0, 0); + + break; + case TabSide.Left: + tab.Border!.Thickness = new (topLine, 1, 0, 1); + tab.Margin!.Thickness = new (0, 0, topLine, 0); + + break; + case TabSide.Right: + break; + default: + throw new ArgumentOutOfRangeException (); + } + } + else if (selected is null) + { + switch (Style.TabsSide) + { + case TabSide.Top: + tab.Border!.Thickness = new (1, topLine, 1, 1); + tab.Margin!.Thickness = new (0, 0, 0, 0); + + break; + case TabSide.Bottom: + tab.Border!.Thickness = new (1, 1, 1, topLine); + tab.Margin!.Thickness = new (0, 0, 0, 0); + + break; + case TabSide.Left: + tab.Border!.Thickness = new (topLine, 1, 1, 1); + tab.Margin!.Thickness = new (0, 0, 0, 0); + + break; + case TabSide.Right: + break; + default: + throw new ArgumentOutOfRangeException (); + } + } + else + { + switch (Style.TabsSide) + { + case TabSide.Top: + tab.Border!.Thickness = new (1, topLine, 1, 1); + tab.Margin!.Thickness = new (0, 0, 0, 0); + + break; + case TabSide.Bottom: + tab.Border!.Thickness = new (1, 1, 1, topLine); + tab.Margin!.Thickness = new (0, 0, 0, 0); + + break; + case TabSide.Left: + tab.Border!.Thickness = new (topLine, 1, 1, 1); + tab.Margin!.Thickness = new (0, 0, 0, 0); + + break; + case TabSide.Right: + break; + default: + throw new ArgumentOutOfRangeException (); + } + } + + // Ensures updating TextFormatter constrains + tab.TextFormatter.ConstrainToWidth = tab.GetContentSize ().Width; + tab.TextFormatter.ConstrainToHeight = tab.GetContentSize ().Height; + } } /// @@ -613,6 +841,7 @@ private void UnSetCurrentTabs () { tab.MouseClick -= Tab_MouseClick!; tab.Border!.MouseClick -= Tab_MouseClick!; + tab.DisplayTextChanged -= Tab_DisplayTextChanged; tab.Visible = false; } } @@ -623,6 +852,7 @@ private void UnSetCurrentTabs () { tabToRender.MouseClick -= Tab_MouseClick!; tabToRender.Border!.MouseClick -= Tab_MouseClick!; + tabToRender.DisplayTextChanged -= Tab_DisplayTextChanged; tabToRender.Visible = false; } From bb0491ed260543332a5e525f823c8dd7f9772574 Mon Sep 17 00:00:00 2001 From: BDisp Date: Thu, 28 Nov 2024 16:43:48 +0000 Subject: [PATCH 11/18] Add up and down arrows feature. --- Terminal.Gui/Views/TabView/TabRow.cs | 127 +++++++++++++++++++-------- 1 file changed, 92 insertions(+), 35 deletions(-) diff --git a/Terminal.Gui/Views/TabView/TabRow.cs b/Terminal.Gui/Views/TabView/TabRow.cs index 26ef662ae8..31aa909e19 100644 --- a/Terminal.Gui/Views/TabView/TabRow.cs +++ b/Terminal.Gui/Views/TabView/TabRow.cs @@ -4,8 +4,8 @@ namespace Terminal.Gui; internal class TabRow : View { private readonly TabView _host; - private readonly View _leftScrollIndicator; - private readonly View _rightScrollIndicator; + private readonly View _leftUpScrollIndicator; + private readonly View _rightDownScrollIndicator; public TabRow (TabView host) { @@ -17,27 +17,25 @@ public TabRow (TabView host) TabStop = TabBehavior.TabGroup; Width = Dim.Fill (); - _rightScrollIndicator = new View + _rightDownScrollIndicator = new View { - Id = "rightScrollIndicator", + Id = "rightDownScrollIndicator", Width = 1, Height = 1, - Visible = false, - Text = Glyphs.RightArrow.ToString () + Visible = false }; - _rightScrollIndicator.MouseClick += _host.Tab_MouseClick!; + _rightDownScrollIndicator.MouseClick += _host.Tab_MouseClick!; - _leftScrollIndicator = new View + _leftUpScrollIndicator = new View { - Id = "leftScrollIndicator", + Id = "leftUpScrollIndicator", Width = 1, Height = 1, - Visible = false, - Text = Glyphs.LeftArrow.ToString () + Visible = false }; - _leftScrollIndicator.MouseClick += _host.Tab_MouseClick!; + _leftUpScrollIndicator.MouseClick += _host.Tab_MouseClick!; - Add (_rightScrollIndicator, _leftScrollIndicator); + Add (_rightDownScrollIndicator, _leftUpScrollIndicator); } /// @@ -83,11 +81,11 @@ protected override bool OnMouseEvent (MouseEventArgs me) { var scrollIndicatorHit = 0; - if (me.View is { Id: "rightScrollIndicator" } || me.Flags.HasFlag (MouseFlags.WheeledDown) || me.Flags.HasFlag (MouseFlags.WheeledRight)) + if (me.View is { Id: "rightDownScrollIndicator" } || me.Flags.HasFlag (MouseFlags.WheeledDown) || me.Flags.HasFlag (MouseFlags.WheeledRight)) { scrollIndicatorHit = 1; } - else if (me.View is { Id: "leftScrollIndicator" } || me.Flags.HasFlag (MouseFlags.WheeledUp) || me.Flags.HasFlag (MouseFlags.WheeledLeft)) + else if (me.View is { Id: "leftUpScrollIndicator" } || me.Flags.HasFlag (MouseFlags.WheeledUp) || me.Flags.HasFlag (MouseFlags.WheeledLeft)) { scrollIndicatorHit = -1; } @@ -671,7 +669,7 @@ private void RenderTabLineCanvas () int lineLength = tabsBarVts.Right - vts.Right; // Right horizontal line - if (ShouldDrawRightScrollIndicator ()) + if (ShouldDrawRightDownScrollIndicator ()) { if (lineLength - arrowOffset > 0) { @@ -783,20 +781,30 @@ private void RenderTabLineCanvas () LineCanvas.Merge (lc); } - private int GetUnderlineYPosition () + private int GetUnderlineXOrYPosition () { - if (_host.Style.TabsSide == TabSide.Bottom) + switch (_host.Style.TabsSide) { - return 0; - } + case TabSide.Top: + + return _host.Style.ShowInitialLine ? 2 : 1; + case TabSide.Bottom: + + return 0; + case TabSide.Left: - return _host.Style.ShowInitialLine ? 2 : 1; + return _host.Style.ShowInitialLine ? Frame.Right - 1 : Frame.Right; + case TabSide.Right: + return 0; + default: + throw new ArgumentOutOfRangeException (); + } } /// Renders the line of the tab that adjoins the content of the tab. private void RenderUnderline () { - int y = GetUnderlineYPosition (); + int xOrY = GetUnderlineXOrYPosition (); Tab? selected = _host._tabLocations?.FirstOrDefault (t => t == _host.SelectedTab); @@ -805,42 +813,91 @@ private void RenderUnderline () return; } - // draw scroll indicators + // Set the correct glyphs for scroll indicators + switch (_host.Style.TabsSide) + { + case TabSide.Top: + case TabSide.Bottom: + _rightDownScrollIndicator.Text = Glyphs.RightArrow.ToString (); + _leftUpScrollIndicator.Text = Glyphs.LeftArrow.ToString (); + + break; + case TabSide.Left: + case TabSide.Right: + _rightDownScrollIndicator.Text = Glyphs.DownArrow.ToString (); + _leftUpScrollIndicator.Text = Glyphs.UpArrow.ToString (); + + break; + default: + throw new ArgumentOutOfRangeException (); + } + + // position scroll indicators // if there are more tabs to the left not visible if (_host.TabScrollOffset > 0) { - _leftScrollIndicator.X = 0; - _leftScrollIndicator.Y = y; + switch (_host.Style.TabsSide) + { + case TabSide.Top: + case TabSide.Bottom: + _leftUpScrollIndicator.X = 0; + _leftUpScrollIndicator.Y = xOrY; + + break; + case TabSide.Left: + case TabSide.Right: + _leftUpScrollIndicator.X = xOrY; + _leftUpScrollIndicator.Y = 0; + + break; + default: + throw new ArgumentOutOfRangeException (); + } // indicate that - _leftScrollIndicator.Visible = true; + _leftUpScrollIndicator.Visible = true; // Ensures this is clicked instead of the first tab - MoveSubviewToEnd (_leftScrollIndicator); + MoveSubviewToEnd (_leftUpScrollIndicator); } else { - _leftScrollIndicator.Visible = false; + _leftUpScrollIndicator.Visible = false; } // if there are more tabs to the right not visible - if (ShouldDrawRightScrollIndicator ()) + if (ShouldDrawRightDownScrollIndicator ()) { - _rightScrollIndicator.X = Viewport.Width - 1; - _rightScrollIndicator.Y = y; + switch (_host.Style.TabsSide) + { + case TabSide.Top: + case TabSide.Bottom: + _rightDownScrollIndicator.X = Viewport.Width - 1; + _rightDownScrollIndicator.Y = xOrY; + + break; + case TabSide.Left: + case TabSide.Right: + _rightDownScrollIndicator.X = xOrY; + _rightDownScrollIndicator.Y = Viewport.Height - 1; + + break; + default: + throw new ArgumentOutOfRangeException (); + } // indicate that - _rightScrollIndicator.Visible = true; + _rightDownScrollIndicator.Visible = true; // Ensures this is clicked instead of the last tab if under this - MoveSubviewToStart (_rightScrollIndicator); + MoveSubviewToStart (_rightDownScrollIndicator); } else { - _rightScrollIndicator.Visible = false; + _rightDownScrollIndicator.Visible = false; } } - private bool ShouldDrawRightScrollIndicator () { return _host._tabLocations!.LastOrDefault () != _host.Tabs.LastOrDefault (); } + private bool ShouldDrawRightDownScrollIndicator () { return _host._tabLocations!.LastOrDefault () != _host.Tabs.LastOrDefault (); } } From 3c6e8a1b9a859bdd3d41d538e8e4edc5ec1ea448 Mon Sep 17 00:00:00 2001 From: BDisp Date: Thu, 28 Nov 2024 16:49:45 +0000 Subject: [PATCH 12/18] Cleanup Point. --- Terminal.Gui/Views/TabView/TabRow.cs | 100 +++++++++++++-------------- 1 file changed, 50 insertions(+), 50 deletions(-) diff --git a/Terminal.Gui/Views/TabView/TabRow.cs b/Terminal.Gui/Views/TabView/TabRow.cs index 31aa909e19..529cde99a4 100644 --- a/Terminal.Gui/Views/TabView/TabRow.cs +++ b/Terminal.Gui/Views/TabView/TabRow.cs @@ -176,7 +176,7 @@ private void RenderTabLineCanvas () case TabSide.Top: // Lower left vertical line lc.AddLine ( - new Point (vts.X - 1, vts.Bottom - selectedOffset), + new (vts.X - 1, vts.Bottom - selectedOffset), -1, Orientation.Vertical, tab.BorderStyle @@ -186,7 +186,7 @@ private void RenderTabLineCanvas () case TabSide.Bottom: // Upper left vertical line lc.AddLine ( - new Point (vts.X - 1, vts.Y - 1), + new (vts.X - 1, vts.Y - 1), -1, Orientation.Vertical, tab.BorderStyle @@ -208,14 +208,14 @@ private void RenderTabLineCanvas () case TabSide.Top: // LRCorner lc.AddLine ( - new Point (vts.X - 1, vts.Bottom - selectedOffset), + new (vts.X - 1, vts.Bottom - selectedOffset), -1, Orientation.Vertical, tab.BorderStyle ); lc.AddLine ( - new Point (vts.X - 1, vts.Bottom - selectedOffset), + new (vts.X - 1, vts.Bottom - selectedOffset), -1, Orientation.Horizontal, tab.BorderStyle @@ -225,14 +225,14 @@ private void RenderTabLineCanvas () case TabSide.Bottom: // URCorner lc.AddLine ( - new Point (vts.X - 1, vts.Y - 1), + new (vts.X - 1, vts.Y - 1), 1, Orientation.Vertical, tab.BorderStyle ); lc.AddLine ( - new Point (vts.X - 1, vts.Y - 1), + new (vts.X - 1, vts.Y - 1), -1, Orientation.Horizontal, tab.BorderStyle @@ -254,14 +254,14 @@ private void RenderTabLineCanvas () case TabSide.Top: // Upper left tee lc.AddLine ( - new Point (vts.X - 1, vts.Y - 1), + new (vts.X - 1, vts.Y - 1), 1, Orientation.Vertical, tab.BorderStyle ); lc.AddLine ( - new Point (vts.X - 1, vts.Y - 1), + new (vts.X - 1, vts.Y - 1), 0, Orientation.Horizontal, tab.BorderStyle @@ -271,14 +271,14 @@ private void RenderTabLineCanvas () case TabSide.Bottom: // Lower left tee lc.AddLine ( - new Point (vts.X - 1, vts.Bottom), + new (vts.X - 1, vts.Bottom), -1, Orientation.Vertical, tab.BorderStyle ); lc.AddLine ( - new Point (vts.X - 1, vts.Bottom), + new (vts.X - 1, vts.Bottom), 0, Orientation.Horizontal, tab.BorderStyle @@ -304,14 +304,14 @@ private void RenderTabLineCanvas () case TabSide.Top: // Upper right tee lc.AddLine ( - new Point (vts.Right, vts.Y - 1), + new (vts.Right, vts.Y - 1), 1, Orientation.Vertical, tab.BorderStyle ); lc.AddLine ( - new Point (vts.Right, vts.Y - 1), + new (vts.Right, vts.Y - 1), 0, Orientation.Horizontal, tab.BorderStyle @@ -321,14 +321,14 @@ private void RenderTabLineCanvas () case TabSide.Bottom: // Lower right tee lc.AddLine ( - new Point (vts.Right, vts.Bottom), + new (vts.Right, vts.Bottom), -1, Orientation.Vertical, tab.BorderStyle ); lc.AddLine ( - new Point (vts.Right, vts.Bottom), + new (vts.Right, vts.Bottom), 0, Orientation.Horizontal, tab.BorderStyle @@ -350,14 +350,14 @@ private void RenderTabLineCanvas () case TabSide.Top: //LLCorner lc.AddLine ( - new Point (vts.Right, vts.Bottom - selectedOffset), + new (vts.Right, vts.Bottom - selectedOffset), -1, Orientation.Vertical, tab.BorderStyle ); lc.AddLine ( - new Point (vts.Right, vts.Bottom - selectedOffset), + new (vts.Right, vts.Bottom - selectedOffset), 1, Orientation.Horizontal, tab.BorderStyle @@ -367,14 +367,14 @@ private void RenderTabLineCanvas () case TabSide.Bottom: //URCorner lc.AddLine ( - new Point (vts.Right, vts.Y - 1), + new (vts.Right, vts.Y - 1), 1, Orientation.Vertical, tab.BorderStyle ); lc.AddLine ( - new Point (vts.Right, vts.Y - 1), + new (vts.Right, vts.Y - 1), 1, Orientation.Horizontal, tab.BorderStyle @@ -400,14 +400,14 @@ private void RenderTabLineCanvas () { // ULCorner lc.AddLine ( - new Point (vts.X - 1, vts.Y - 1), + new (vts.X - 1, vts.Y - 1), 1, Orientation.Vertical, tab.BorderStyle ); lc.AddLine ( - new Point (vts.X - 1, vts.Y - 1), + new (vts.X - 1, vts.Y - 1), 1, Orientation.Horizontal, tab.BorderStyle @@ -416,14 +416,14 @@ private void RenderTabLineCanvas () // LLCorner lc.AddLine ( - new Point (vts.X - 1, vts.Bottom), + new (vts.X - 1, vts.Bottom), -1, Orientation.Vertical, tab.BorderStyle ); lc.AddLine ( - new Point (vts.X - 1, vts.Bottom), + new (vts.X - 1, vts.Bottom), 1, Orientation.Horizontal, tab.BorderStyle @@ -435,14 +435,14 @@ private void RenderTabLineCanvas () { // LLCorner lc.AddLine ( - new Point (vts.X - 1, vts.Bottom), + new (vts.X - 1, vts.Bottom), -1, Orientation.Vertical, tab.BorderStyle ); lc.AddLine ( - new Point (vts.X - 1, vts.Bottom), + new (vts.X - 1, vts.Bottom), 1, Orientation.Horizontal, tab.BorderStyle @@ -451,14 +451,14 @@ private void RenderTabLineCanvas () // ULCorner lc.AddLine ( - new Point (vts.X - 1, vts.Y - 1), + new (vts.X - 1, vts.Y - 1), 1, Orientation.Vertical, tab.BorderStyle ); lc.AddLine ( - new Point (vts.X - 1, vts.Y - 1), + new (vts.X - 1, vts.Y - 1), 1, Orientation.Horizontal, tab.BorderStyle @@ -479,14 +479,14 @@ private void RenderTabLineCanvas () { // Upper left tee lc.AddLine ( - new Point (vts.X - 1, vts.Y - 1), + new (vts.X - 1, vts.Y - 1), 1, Orientation.Vertical, tab.BorderStyle ); lc.AddLine ( - new Point (vts.X - 1, vts.Y - 1), + new (vts.X - 1, vts.Y - 1), 0, Orientation.Horizontal, tab.BorderStyle @@ -495,14 +495,14 @@ private void RenderTabLineCanvas () // Lower left tee lc.AddLine ( - new Point (vts.X - 1, vts.Bottom), + new (vts.X - 1, vts.Bottom), -1, Orientation.Vertical, tab.BorderStyle ); lc.AddLine ( - new Point (vts.X - 1, vts.Bottom), + new (vts.X - 1, vts.Bottom), 0, Orientation.Horizontal, tab.BorderStyle @@ -515,14 +515,14 @@ private void RenderTabLineCanvas () { // Upper right tee lc.AddLine ( - new Point (vts.Right, vts.Y - 1), + new (vts.Right, vts.Y - 1), 1, Orientation.Vertical, tab.BorderStyle ); lc.AddLine ( - new Point (vts.Right, vts.Y - 1), + new (vts.Right, vts.Y - 1), 0, Orientation.Horizontal, tab.BorderStyle @@ -533,14 +533,14 @@ private void RenderTabLineCanvas () { // Lower right tee lc.AddLine ( - new Point (vts.Right, vts.Bottom), + new (vts.Right, vts.Bottom), -1, Orientation.Vertical, tab.BorderStyle ); lc.AddLine ( - new Point (vts.Right, vts.Bottom), + new (vts.Right, vts.Bottom), 0, Orientation.Horizontal, tab.BorderStyle @@ -550,14 +550,14 @@ private void RenderTabLineCanvas () { // Upper right tee lc.AddLine ( - new Point (vts.Right, vts.Y - 1), + new (vts.Right, vts.Y - 1), 1, Orientation.Vertical, tab.BorderStyle ); lc.AddLine ( - new Point (vts.Right, vts.Y - 1), + new (vts.Right, vts.Y - 1), 0, Orientation.Horizontal, tab.BorderStyle @@ -572,14 +572,14 @@ private void RenderTabLineCanvas () case TabSide.Top: // Lower left vertical line lc.AddLine ( - new Point (vts.X - 1, vts.Bottom), + new (vts.X - 1, vts.Bottom), 0, Orientation.Vertical, tab.BorderStyle ); lc.AddLine ( - new Point (vts.X - 1, vts.Bottom), + new (vts.X - 1, vts.Bottom), 1, Orientation.Horizontal, tab.BorderStyle @@ -589,14 +589,14 @@ private void RenderTabLineCanvas () case TabSide.Bottom: // Upper left vertical line lc.AddLine ( - new Point (vts.X - 1, vts.Y - 1), + new (vts.X - 1, vts.Y - 1), 0, Orientation.Vertical, tab.BorderStyle ); lc.AddLine ( - new Point (vts.X - 1, vts.Y - 1), + new (vts.X - 1, vts.Y - 1), 1, Orientation.Horizontal, tab.BorderStyle @@ -619,14 +619,14 @@ private void RenderTabLineCanvas () case TabSide.Top: // Lower right tee lc.AddLine ( - new Point (vts.Right, vts.Bottom), + new (vts.Right, vts.Bottom), -1, Orientation.Vertical, tab.BorderStyle ); lc.AddLine ( - new Point (vts.Right, vts.Bottom), + new (vts.Right, vts.Bottom), 0, Orientation.Horizontal, tab.BorderStyle @@ -636,14 +636,14 @@ private void RenderTabLineCanvas () case TabSide.Bottom: // Upper right tee lc.AddLine ( - new Point (vts.Right, vts.Y - 1), + new (vts.Right, vts.Y - 1), 1, Orientation.Vertical, tab.BorderStyle ); lc.AddLine ( - new Point (vts.Right, vts.Y - 1), + new (vts.Right, vts.Y - 1), 0, Orientation.Horizontal, tab.BorderStyle @@ -677,7 +677,7 @@ private void RenderTabLineCanvas () { case TabSide.Top: lc.AddLine ( - new Point ( + new ( vts.Right, vts.Bottom - lastSelectedTab ), @@ -689,7 +689,7 @@ private void RenderTabLineCanvas () break; case TabSide.Bottom: lc.AddLine ( - new Point (vts.Right, vts.Y - lastSelectedTab), + new (vts.Right, vts.Y - lastSelectedTab), lineLength - arrowOffset, Orientation.Horizontal, tab.BorderStyle @@ -712,7 +712,7 @@ private void RenderTabLineCanvas () { case TabSide.Top: lc.AddLine ( - new Point (vts.Right, vts.Bottom - lastSelectedTab), + new (vts.Right, vts.Bottom - lastSelectedTab), lineLength, Orientation.Horizontal, tab.BorderStyle @@ -721,7 +721,7 @@ private void RenderTabLineCanvas () break; case TabSide.Bottom: lc.AddLine ( - new Point (vts.Right, vts.Y - lastSelectedTab), + new (vts.Right, vts.Y - lastSelectedTab), lineLength, Orientation.Horizontal, tab.BorderStyle @@ -743,7 +743,7 @@ private void RenderTabLineCanvas () case TabSide.Top: // More URCorner lc.AddLine ( - new Point ( + new ( tabsBarVts.Right - 1, vts.Bottom - lastSelectedTab ), @@ -756,7 +756,7 @@ private void RenderTabLineCanvas () case TabSide.Bottom: // More LRCorner lc.AddLine ( - new Point ( + new ( tabsBarVts.Right - 1, vts.Y - lastSelectedTab ), From ee4923e0c5cf2cec9ebcbca5a52bc7ed3981a641 Mon Sep 17 00:00:00 2001 From: BDisp Date: Sat, 30 Nov 2024 19:26:50 +0000 Subject: [PATCH 13/18] Implemented almost left side feature. --- Terminal.Gui/Views/TabView/TabRow.cs | 635 ++++++++++++++++++++------ Terminal.Gui/Views/TabView/TabView.cs | 188 ++++---- UnitTests/Views/TabViewTests.cs | 543 +++++++++++++++++++++- 3 files changed, 1129 insertions(+), 237 deletions(-) diff --git a/Terminal.Gui/Views/TabView/TabRow.cs b/Terminal.Gui/Views/TabView/TabRow.cs index 529cde99a4..175d500710 100644 --- a/Terminal.Gui/Views/TabView/TabRow.cs +++ b/Terminal.Gui/Views/TabView/TabRow.cs @@ -13,11 +13,12 @@ public TabRow (TabView host) Id = "tabRow"; CanFocus = true; + // Because TabRow has focusable subviews, it must be a TabGroup TabStop = TabBehavior.TabGroup; Width = Dim.Fill (); - _rightDownScrollIndicator = new View + _rightDownScrollIndicator = new () { Id = "rightDownScrollIndicator", Width = 1, @@ -26,7 +27,7 @@ public TabRow (TabView host) }; _rightDownScrollIndicator.MouseClick += _host.Tab_MouseClick!; - _leftUpScrollIndicator = new View + _leftUpScrollIndicator = new () { Id = "leftUpScrollIndicator", Width = 1, @@ -38,7 +39,7 @@ public TabRow (TabView host) Add (_rightDownScrollIndicator, _leftUpScrollIndicator); } - /// + /// public override void EndInit () { _host._tabLocations = _host.CalculateViewport (Viewport); @@ -49,11 +50,11 @@ public override void EndInit () protected override bool OnMouseEvent (MouseEventArgs me) { View? parent = me.View is Adornment adornment ? adornment.Parent : me.View; - Tab? hit = parent as Tab; + var hit = parent as Tab; if (me.IsSingleClicked) { - _host.OnTabClicked (new TabMouseEventArgs (hit!, me)); + _host.OnTabClicked (new (hit!, me)); // user canceled click if (me.Handled) @@ -108,7 +109,7 @@ protected override bool OnMouseEvent (MouseEventArgs me) return false; } - /// + /// protected override void OnHasFocusChanged (bool newHasFocus, View? previousFocusedView, View? focusedView) { if (_host.SelectedTab is { HasFocus: false, CanFocus: true } && focusedView == this) @@ -140,7 +141,7 @@ protected override void OnSubviewLayout (LayoutEventArgs args) base.OnSubviewLayout (args); } - /// + /// protected override bool OnRenderingLineCanvas () { RenderTabLineCanvas (); @@ -163,7 +164,6 @@ private void RenderTabLineCanvas () { View tab = tabLocations [i]; Rectangle vts = tab.ViewportToScreen (tab.Viewport); - int selectedOffset = _host.Style.ShowInitialLine && tabLocations [i] == _host.SelectedTab ? 0 : 1; if (tabLocations [i] == _host.SelectedTab) { @@ -176,7 +176,7 @@ private void RenderTabLineCanvas () case TabSide.Top: // Lower left vertical line lc.AddLine ( - new (vts.X - 1, vts.Bottom - selectedOffset), + new (vts.X - 1, vts.Bottom), -1, Orientation.Vertical, tab.BorderStyle @@ -194,6 +194,14 @@ private void RenderTabLineCanvas () break; case TabSide.Left: + // Upper horizontal line + lc.AddLine ( + new (vts.Right, vts.Y - 1), + -1, + Orientation.Horizontal, + tab.BorderStyle + ); + break; case TabSide.Right: break; @@ -208,14 +216,14 @@ private void RenderTabLineCanvas () case TabSide.Top: // LRCorner lc.AddLine ( - new (vts.X - 1, vts.Bottom - selectedOffset), + new (vts.X - 1, vts.Bottom), -1, Orientation.Vertical, tab.BorderStyle ); lc.AddLine ( - new (vts.X - 1, vts.Bottom - selectedOffset), + new (vts.X - 1, vts.Bottom), -1, Orientation.Horizontal, tab.BorderStyle @@ -240,6 +248,24 @@ private void RenderTabLineCanvas () break; case TabSide.Left: + if (Frame.Bottom > tab.Frame.Bottom) + { + // LRCorner + lc.AddLine ( + new (vts.Right, vts.Bottom), + 1, + Orientation.Vertical, + tab.BorderStyle + ); + + lc.AddLine ( + new (vts.Right, vts.Bottom), + -1, + Orientation.Horizontal, + tab.BorderStyle + ); + } + break; case TabSide.Right: break; @@ -286,6 +312,21 @@ private void RenderTabLineCanvas () break; case TabSide.Left: + // Upper left tee + lc.AddLine ( + new (vts.X - 1, vts.Y - 1), + 0, + Orientation.Vertical, + tab.BorderStyle + ); + + lc.AddLine ( + new (vts.X - 1, vts.Y - 1), + 1, + Orientation.Horizontal, + tab.BorderStyle + ); + break; case TabSide.Right: break; @@ -336,6 +377,21 @@ private void RenderTabLineCanvas () break; case TabSide.Left: + // Upper right tee + lc.AddLine ( + new (vts.X - 1, vts.Bottom), + 0, + Orientation.Vertical, + tab.BorderStyle + ); + + lc.AddLine ( + new (vts.X - 1, vts.Bottom), + 1, + Orientation.Horizontal, + tab.BorderStyle + ); + break; case TabSide.Right: break; @@ -348,16 +404,16 @@ private void RenderTabLineCanvas () switch (_host.Style.TabsSide) { case TabSide.Top: - //LLCorner + //LRCorner lc.AddLine ( - new (vts.Right, vts.Bottom - selectedOffset), + new (vts.Right, vts.Bottom), -1, Orientation.Vertical, tab.BorderStyle ); lc.AddLine ( - new (vts.Right, vts.Bottom - selectedOffset), + new (vts.Right, vts.Bottom), 1, Orientation.Horizontal, tab.BorderStyle @@ -382,6 +438,24 @@ private void RenderTabLineCanvas () break; case TabSide.Left: + if (Frame.Bottom > tab.Frame.Bottom) + { + //LRCorner + lc.AddLine ( + new (vts.Right, vts.Bottom), + 1, + Orientation.Vertical, + tab.BorderStyle + ); + + lc.AddLine ( + new (vts.Right, vts.Bottom), + -1, + Orientation.Horizontal, + tab.BorderStyle + ); + } + break; case TabSide.Right: break; @@ -466,6 +540,39 @@ private void RenderTabLineCanvas () break; case TabSide.Left: + if (_host.Style.ShowInitialLine) + { + // ULCorner + lc.AddLine ( + new (vts.X - 1, vts.Y - 1), + 1, + Orientation.Vertical, + tab.BorderStyle + ); + + lc.AddLine ( + new (vts.X - 1, vts.Y - 1), + 1, + Orientation.Horizontal, + tab.BorderStyle + ); + } + + // LLCorner + lc.AddLine ( + new (vts.X - 1, vts.Bottom), + -1, + Orientation.Vertical, + tab.BorderStyle + ); + + lc.AddLine ( + new (vts.X - 1, vts.Bottom), + 1, + Orientation.Horizontal, + tab.BorderStyle + ); + break; case TabSide.Right: break; @@ -475,93 +582,239 @@ private void RenderTabLineCanvas () } else if (i > 0) { - if (_host.Style.ShowInitialLine || _host.Style.TabsSide == TabSide.Bottom) + switch (_host.Style.TabsSide) { - // Upper left tee - lc.AddLine ( - new (vts.X - 1, vts.Y - 1), - 1, - Orientation.Vertical, - tab.BorderStyle - ); + case TabSide.Top: + case TabSide.Bottom: + if (_host.Style.ShowInitialLine || _host.Style.TabsSide == TabSide.Bottom) + { + // Upper left tee + lc.AddLine ( + new (vts.X - 1, vts.Y - 1), + 1, + Orientation.Vertical, + tab.BorderStyle + ); - lc.AddLine ( - new (vts.X - 1, vts.Y - 1), - 0, - Orientation.Horizontal, - tab.BorderStyle - ); - } + lc.AddLine ( + new (vts.X - 1, vts.Y - 1), + 0, + Orientation.Horizontal, + tab.BorderStyle + ); + } + + // Lower left tee + lc.AddLine ( + new (vts.X - 1, vts.Bottom), + -1, + Orientation.Vertical, + tab.BorderStyle + ); - // Lower left tee - lc.AddLine ( - new (vts.X - 1, vts.Bottom), - -1, - Orientation.Vertical, - tab.BorderStyle - ); - - lc.AddLine ( - new (vts.X - 1, vts.Bottom), - 0, - Orientation.Horizontal, - tab.BorderStyle - ); + lc.AddLine ( + new (vts.X - 1, vts.Bottom), + 0, + Orientation.Horizontal, + tab.BorderStyle + ); + + break; + case TabSide.Left: + if (_host.Style.ShowInitialLine || _host.Style.TabsSide == TabSide.Right) + { + // Upper left tee + lc.AddLine ( + new (vts.X - 1, vts.Y - 1), + 0, + Orientation.Vertical, + tab.BorderStyle + ); + + lc.AddLine ( + new (vts.X - 1, vts.Y - 1), + 1, + Orientation.Horizontal, + tab.BorderStyle + ); + } + + // Lower left tee + lc.AddLine ( + new (vts.Right, vts.Y - 1), + 0, + Orientation.Vertical, + tab.BorderStyle + ); + + lc.AddLine ( + new (vts.Right, vts.Y - 1), + -1, + Orientation.Horizontal, + tab.BorderStyle + ); + + break; + case TabSide.Right: + break; + default: + throw new ArgumentOutOfRangeException (); + } } } else if (i < tabLocations.Length - 1) { if (_host.Style.ShowInitialLine) { - // Upper right tee - lc.AddLine ( - new (vts.Right, vts.Y - 1), - 1, - Orientation.Vertical, - tab.BorderStyle - ); - - lc.AddLine ( - new (vts.Right, vts.Y - 1), - 0, - Orientation.Horizontal, - tab.BorderStyle - ); + switch (_host.Style.TabsSide) + { + case TabSide.Top: + case TabSide.Bottom: + // Upper right tee + lc.AddLine ( + new (vts.Right, vts.Y - 1), + 1, + Orientation.Vertical, + tab.BorderStyle + ); + + lc.AddLine ( + new (vts.Right, vts.Y - 1), + 0, + Orientation.Horizontal, + tab.BorderStyle + ); + + break; + case TabSide.Left: + // Upper right tee + lc.AddLine ( + new (vts.X - 1, vts.Bottom), + 0, + Orientation.Vertical, + tab.BorderStyle + ); + + lc.AddLine ( + new (vts.X - 1, vts.Bottom), + 1, + Orientation.Horizontal, + tab.BorderStyle + ); + + break; + case TabSide.Right: + break; + default: + throw new ArgumentOutOfRangeException (); + } } - if (_host.Style.ShowInitialLine || _host.Style.TabsSide == TabSide.Top) + if (_host.Style.ShowInitialLine) { - // Lower right tee - lc.AddLine ( - new (vts.Right, vts.Bottom), - -1, - Orientation.Vertical, - tab.BorderStyle - ); - - lc.AddLine ( - new (vts.Right, vts.Bottom), - 0, - Orientation.Horizontal, - tab.BorderStyle - ); + switch (_host.Style.TabsSide) + { + case TabSide.Top: + case TabSide.Bottom: + // Lower right tee + lc.AddLine ( + new (vts.Right, vts.Bottom), + -1, + Orientation.Vertical, + tab.BorderStyle + ); + + lc.AddLine ( + new (vts.Right, vts.Bottom), + 0, + Orientation.Horizontal, + tab.BorderStyle + ); + + break; + case TabSide.Left: + // Lower right tee + lc.AddLine ( + new (vts.Right, vts.Bottom), + 0, + Orientation.Vertical, + tab.BorderStyle + ); + + lc.AddLine ( + new (vts.Right, vts.Bottom), + -1, + Orientation.Horizontal, + tab.BorderStyle + ); + + break; + case TabSide.Right: + break; + default: + throw new ArgumentOutOfRangeException (); + } } else { - // Upper right tee - lc.AddLine ( - new (vts.Right, vts.Y - 1), - 1, - Orientation.Vertical, - tab.BorderStyle - ); - - lc.AddLine ( - new (vts.Right, vts.Y - 1), - 0, - Orientation.Horizontal, - tab.BorderStyle - ); + switch (_host.Style.TabsSide) + { + case TabSide.Top: + // Lower right tee + lc.AddLine ( + new (vts.Right, vts.Bottom), + -1, + Orientation.Vertical, + tab.BorderStyle + ); + + lc.AddLine ( + new (vts.Right, vts.Bottom), + 0, + Orientation.Horizontal, + tab.BorderStyle + ); + + break; + case TabSide.Bottom: + // Upper right tee + lc.AddLine ( + new (vts.Right, vts.Y - 1), + 1, + Orientation.Vertical, + tab.BorderStyle + ); + + lc.AddLine ( + new (vts.Right, vts.Y - 1), + 0, + Orientation.Horizontal, + tab.BorderStyle + ); + + break; + case TabSide.Left: + // Lower right tee + lc.AddLine ( + new (vts.Right, vts.Bottom), + 0, + Orientation.Vertical, + tab.BorderStyle + ); + + lc.AddLine ( + new (vts.Right, vts.Bottom), + -1, + Orientation.Horizontal, + tab.BorderStyle + ); + + break; + case TabSide.Right: + break; + default: + throw new ArgumentOutOfRangeException (); + } } } @@ -604,6 +857,21 @@ private void RenderTabLineCanvas () break; case TabSide.Left: + // Upper horizontal line + lc.AddLine ( + new (vts.Right, vts.Y - 1), + 1, + Orientation.Vertical, + tab.BorderStyle + ); + + lc.AddLine ( + new (vts.Right, vts.Y - 1), + 0, + Orientation.Horizontal, + tab.BorderStyle + ); + break; case TabSide.Right: break; @@ -651,6 +919,24 @@ private void RenderTabLineCanvas () break; case TabSide.Left: + if (Frame.Bottom > tab.Frame.Bottom) + { + // Lower right tee + lc.AddLine ( + new (vts.Right, vts.Bottom), + 0, + Orientation.Vertical, + tab.BorderStyle + ); + + lc.AddLine ( + new (vts.Right, vts.Bottom), + -1, + Orientation.Horizontal, + tab.BorderStyle + ); + } + break; case TabSide.Right: break; @@ -663,12 +949,26 @@ private void RenderTabLineCanvas () { var arrowOffset = 1; - int lastSelectedTab = !_host.Style.ShowInitialLine && i == selectedTab ? 1 : - _host.Style.TabsSide == TabSide.Bottom ? 1 : 0; Rectangle tabsBarVts = ViewportToScreen (Viewport); - int lineLength = tabsBarVts.Right - vts.Right; + int lineLength; + + switch (_host.Style.TabsSide) + { + case TabSide.Top: + case TabSide.Bottom: + lineLength = tabsBarVts.Right - vts.Right; + + break; + case TabSide.Left: + case TabSide.Right: + lineLength = tabsBarVts.Bottom - vts.Bottom; - // Right horizontal line + break; + default: + throw new ArgumentOutOfRangeException (); + } + + // Right horizontal/vertical line if (ShouldDrawRightDownScrollIndicator ()) { if (lineLength - arrowOffset > 0) @@ -678,9 +978,9 @@ private void RenderTabLineCanvas () case TabSide.Top: lc.AddLine ( new ( - vts.Right, - vts.Bottom - lastSelectedTab - ), + vts.Right, + vts.Bottom + ), lineLength - arrowOffset, Orientation.Horizontal, tab.BorderStyle @@ -689,7 +989,8 @@ private void RenderTabLineCanvas () break; case TabSide.Bottom: lc.AddLine ( - new (vts.Right, vts.Y - lastSelectedTab), + + new (vts.Right, vts.Y - 1), lineLength - arrowOffset, Orientation.Horizontal, tab.BorderStyle @@ -697,6 +998,16 @@ private void RenderTabLineCanvas () break; case TabSide.Left: + lc.AddLine ( + new ( + vts.Right, + vts.Bottom + ), + lineLength - arrowOffset, + Orientation.Vertical, + tab.BorderStyle + ); + break; case TabSide.Right: break; @@ -712,7 +1023,7 @@ private void RenderTabLineCanvas () { case TabSide.Top: lc.AddLine ( - new (vts.Right, vts.Bottom - lastSelectedTab), + new (vts.Right, vts.Bottom), lineLength, Orientation.Horizontal, tab.BorderStyle @@ -721,7 +1032,8 @@ private void RenderTabLineCanvas () break; case TabSide.Bottom: lc.AddLine ( - new (vts.Right, vts.Y - lastSelectedTab), + + new (vts.Right, vts.Y - 1), lineLength, Orientation.Horizontal, tab.BorderStyle @@ -729,6 +1041,65 @@ private void RenderTabLineCanvas () break; case TabSide.Left: + if (i == selectedTab) + { + if (Frame.Bottom == tab.Frame.Bottom) + { + // Lower right horizontal line + lc.AddLine ( + new (vts.Right, vts.Bottom), + 0, + Orientation.Horizontal, + tab.BorderStyle + ); + } + else + { + lc.AddLine ( + new (vts.Right, vts.Bottom), + lineLength, + Orientation.Vertical, + tab.BorderStyle + ); + } + } + else + { + if (Frame.Bottom == tab.Frame.Bottom) + { + lc.AddLine ( + new (vts.Right, vts.Bottom), + -1, + Orientation.Vertical, + tab.BorderStyle + ); + + lc.AddLine ( + new (vts.Right, vts.Bottom), + 0, + Orientation.Horizontal, + tab.BorderStyle + ); + + } + else + { + lc.AddLine ( + new (vts.Right, vts.Bottom), + lineLength, + Orientation.Vertical, + tab.BorderStyle + ); + + lc.AddLine ( + new (vts.Right, tabsBarVts.Bottom), + 1, + Orientation.Horizontal, + tab.BorderStyle + ); + } + } + break; case TabSide.Right: break; @@ -744,9 +1115,9 @@ private void RenderTabLineCanvas () // More URCorner lc.AddLine ( new ( - tabsBarVts.Right - 1, - vts.Bottom - lastSelectedTab - ), + tabsBarVts.Right - 1, + vts.Bottom + ), 1, Orientation.Vertical, tab.BorderStyle @@ -757,9 +1128,9 @@ private void RenderTabLineCanvas () // More LRCorner lc.AddLine ( new ( - tabsBarVts.Right - 1, - vts.Y - lastSelectedTab - ), + tabsBarVts.Right - 1, + vts.Y - 1 + ), -1, Orientation.Vertical, tab.BorderStyle @@ -767,6 +1138,20 @@ private void RenderTabLineCanvas () break; case TabSide.Left: + if (Frame.Bottom > tab.Frame.Bottom) + { + // More URCorner + lc.AddLine ( + new ( + vts.Right, + tabsBarVts.Bottom - 1 + ), + 1, + Orientation.Horizontal, + tab.BorderStyle + ); + } + break; case TabSide.Right: break; @@ -781,31 +1166,9 @@ private void RenderTabLineCanvas () LineCanvas.Merge (lc); } - private int GetUnderlineXOrYPosition () - { - switch (_host.Style.TabsSide) - { - case TabSide.Top: - - return _host.Style.ShowInitialLine ? 2 : 1; - case TabSide.Bottom: - - return 0; - case TabSide.Left: - - return _host.Style.ShowInitialLine ? Frame.Right - 1 : Frame.Right; - case TabSide.Right: - return 0; - default: - throw new ArgumentOutOfRangeException (); - } - } - /// Renders the line of the tab that adjoins the content of the tab. private void RenderUnderline () { - int xOrY = GetUnderlineXOrYPosition (); - Tab? selected = _host._tabLocations?.FirstOrDefault (t => t == _host.SelectedTab); if (selected is null) @@ -840,16 +1203,21 @@ private void RenderUnderline () switch (_host.Style.TabsSide) { case TabSide.Top: + _leftUpScrollIndicator.X = 0; + _leftUpScrollIndicator.Y = Pos.AnchorEnd (1); + + break; case TabSide.Bottom: _leftUpScrollIndicator.X = 0; - _leftUpScrollIndicator.Y = xOrY; + _leftUpScrollIndicator.Y = 0; break; case TabSide.Left: - case TabSide.Right: - _leftUpScrollIndicator.X = xOrY; + _leftUpScrollIndicator.X = Pos.AnchorEnd (1); _leftUpScrollIndicator.Y = 0; + break; + case TabSide.Right: break; default: throw new ArgumentOutOfRangeException (); @@ -872,16 +1240,17 @@ private void RenderUnderline () switch (_host.Style.TabsSide) { case TabSide.Top: + case TabSide.Left: + _rightDownScrollIndicator.X = Pos.AnchorEnd (1); + _rightDownScrollIndicator.Y = Pos.AnchorEnd (1); + + break; case TabSide.Bottom: - _rightDownScrollIndicator.X = Viewport.Width - 1; - _rightDownScrollIndicator.Y = xOrY; + _rightDownScrollIndicator.X = Pos.AnchorEnd (1); + _rightDownScrollIndicator.Y = 0; break; - case TabSide.Left: case TabSide.Right: - _rightDownScrollIndicator.X = xOrY; - _rightDownScrollIndicator.Y = Viewport.Height - 1; - break; default: throw new ArgumentOutOfRangeException (); diff --git a/Terminal.Gui/Views/TabView/TabView.cs b/Terminal.Gui/Views/TabView/TabView.cs index a136a199c1..c04229a679 100644 --- a/Terminal.Gui/Views/TabView/TabView.cs +++ b/Terminal.Gui/Views/TabView/TabView.cs @@ -36,45 +36,53 @@ public TabView () base.Add (_containerView); // Things this view knows how to do - AddCommand (Command.Left, () => - { - if (Style.TabsSide is TabSide.Top or TabSide.Bottom) - { - return SwitchTabBy (-1); - } - - return false; - }); - - AddCommand (Command.Right, () => - { - if (Style.TabsSide is TabSide.Top or TabSide.Bottom) - { - return SwitchTabBy (1); - } - - return false; - }); - - AddCommand (Command.Up, () => - { - if (Style.TabsSide is TabSide.Left or TabSide.Right) - { - return SwitchTabBy (-1); - } - - return false; - }); - - AddCommand (Command.Down, () => - { - if (Style.TabsSide is TabSide.Left or TabSide.Right) - { - return SwitchTabBy (1); - } - - return false; - }); + AddCommand ( + Command.Left, + () => + { + if (Style.TabsSide is TabSide.Top or TabSide.Bottom) + { + return SwitchTabBy (-1); + } + + return false; + }); + + AddCommand ( + Command.Right, + () => + { + if (Style.TabsSide is TabSide.Top or TabSide.Bottom) + { + return SwitchTabBy (1); + } + + return false; + }); + + AddCommand ( + Command.Up, + () => + { + if (Style.TabsSide is TabSide.Left or TabSide.Right) + { + return SwitchTabBy (-1); + } + + return false; + }); + + AddCommand ( + Command.Down, + () => + { + if (Style.TabsSide is TabSide.Left or TabSide.Right) + { + return SwitchTabBy (1); + } + + return false; + }); AddCommand ( Command.LeftStart, @@ -160,6 +168,7 @@ public Tab? SelectedTab if (_selectedTab.View is { }) { _selectedTab.View.CanFocusChanged -= ContainerViewCanFocus!; + // remove old content _containerView.Remove (_selectedTab.View); } @@ -187,19 +196,14 @@ public Tab? SelectedTab OnSelectedTabChanged (old!, _selectedTab!); } + SetNeedsLayout (); } } - private bool TabCanSetFocus () - { - return IsInitialized && SelectedTab is { } && (_selectedTabHasFocus || !_containerView.CanFocus); - } + 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 void ContainerViewCanFocus (object sender, EventArgs eventArgs) { _containerView.CanFocus = _containerView.Subviews.Count (v => v.CanFocus) > 0; } private TabStyle _style = new (); @@ -214,6 +218,7 @@ public TabStyle Style { return; } + _style = value; SetNeedsLayout (); } @@ -287,6 +292,7 @@ public void ApplyStyleChanges () _tabsBar.Height = GetTabHeight (true); _containerView.X = 0; + //move content down to make space for tabs _containerView.Y = Pos.Bottom (_tabsBar); _containerView.Width = Dim.Fill (); @@ -308,6 +314,7 @@ public void ApplyStyleChanges () _containerView.X = 0; _containerView.Y = 0; _containerView.Width = Dim.Fill (); + // Fill client area leaving space at bottom for tabs _containerView.Height = Dim.Fill (tabHeight); @@ -328,6 +335,7 @@ public void ApplyStyleChanges () //move content right to make space for tabs _containerView.X = Pos.Right (_tabsBar); _containerView.Y = 0; + // Fill client area leaving space at left for tabs _containerView.Width = Dim.Fill (); _containerView.Height = Dim.Fill (); @@ -342,7 +350,7 @@ public void ApplyStyleChanges () SetNeedsLayout (); } - /// + /// protected override void OnViewportChanged (DrawEventArgs e) { _tabLocations = CalculateViewport (Viewport); @@ -377,7 +385,7 @@ public void EnsureSelectedTabIsVisible () /// The valid for the given value. public int EnsureValidScrollOffsets (int value) { return Math.Max (Math.Min (value, Tabs.Count - 1), 0); } - /// + /// protected override void OnHasFocusChanged (bool newHasFocus, View? previousFocusedView, View? focusedView) { if (SelectedTab is { HasFocus: false } && !_containerView.CanFocus && focusedView == this) @@ -456,6 +464,7 @@ public bool SwitchTabBy (int amount) if (currentIdx == -1) { SelectedTab = Tabs.ElementAt (0); + return true; } @@ -498,10 +507,7 @@ protected override void Dispose (bool disposing) } /// Raises the event. - protected virtual void OnSelectedTabChanged (Tab oldTab, Tab newTab) - { - SelectedTabChanged?.Invoke (this, new TabChangedEventArgs (oldTab, newTab)); - } + protected virtual void OnSelectedTabChanged (Tab oldTab, Tab newTab) { SelectedTabChanged?.Invoke (this, new (oldTab, newTab)); } /// Returns which tabs to render at each x location. /// @@ -592,7 +598,7 @@ protected virtual void OnSelectedTabChanged (Tab oldTab, Tab newTab) break; case TabSide.Left: case TabSide.Right: - int maxColWidth = 0; + var maxColWidth = 0; // Starting at the first or scrolled to tab foreach (Tab tab in Tabs.Skip (TabScrollOffset)) @@ -616,16 +622,18 @@ protected virtual void OnSelectedTabChanged (Tab oldTab, Tab newTab) // or we won't even be able to render a single tab! long maxWidth = Math.Max (0, Math.Min (bounds.Width - (Style.ShowInitialLine ? 2 : 1), MaxTabTextWidth)); + maxColWidth = GetMaxColWidth (Math.Min (tabTextWidth, (int)maxWidth)); + // The maximum height to use for the tab. But not more than the height of the view // or we won't even be able to render a single tab! - int maxHeight = Math.Max (0, Math.Min (bounds.Height - 3, 3)); + int maxHeight = Math.Max (0, Math.Min (bounds.Height - 2, 2)); tab.Height = 2; // if tab view is height <= 3 don't render any tabs if (maxHeight == 0) { - tab.Width = maxColWidth = Math.Max (Style.ShowInitialLine ? 3 : 2, maxColWidth); + tab.Width = maxColWidth; tab.Visible = true; tab.MouseClick += Tab_MouseClick!; tab.Border!.MouseClick += Tab_MouseClick!; @@ -646,7 +654,7 @@ protected virtual void OnSelectedTabChanged (Tab oldTab, Tab newTab) tab.Text = tab.DisplayText; } - maxColWidth = Math.Max (tabTextWidth + 2, maxColWidth); + maxColWidth = GetMaxColWidth (tabTextWidth); tab.Height = 3; // if there is not enough space for this tab @@ -674,8 +682,21 @@ protected virtual void OnSelectedTabChanged (Tab oldTab, Tab newTab) { t.Width = maxColWidth; } + _tabsBar.Width = maxColWidth; + int GetMaxColWidth (int textWidth) + { + int maxViewportWidth = Math.Max (0, Viewport.Width - (Style.ShowBorder ? 2 : 0)); + + if (Math.Max (textWidth + (Style.ShowInitialLine ? 2 : 1), maxColWidth) > maxViewportWidth) + { + return maxViewportWidth; + } + + return Math.Max (textWidth + (Style.ShowInitialLine ? 2 : 1), maxColWidth); + } + break; default: throw new ArgumentOutOfRangeException (); @@ -697,10 +718,7 @@ protected virtual void OnSelectedTabChanged (Tab oldTab, Tab newTab) return tabs.Count == 0 ? null : tabs.ToArray (); } - private void Tab_DisplayTextChanged (object? sender, EventArgs e) - { - _tabLocations = CalculateViewport (Viewport); - } + private void Tab_DisplayTextChanged (object? sender, EventArgs e) { _tabLocations = CalculateViewport (Viewport); } /// Renders the line with the tab names in it. private void RenderTabLine (Tab []? tabLocations) @@ -710,7 +728,6 @@ private void RenderTabLine (Tab []? tabLocations) return; } - View? selected = null; int topLine = Style.ShowInitialLine ? 1 : 0; foreach (Tab toRender in tabLocations) @@ -719,13 +736,11 @@ private void RenderTabLine (Tab []? tabLocations) if (toRender == SelectedTab) { - selected = tab; - switch (Style.TabsSide) { case TabSide.Top: tab.Border!.Thickness = new (1, topLine, 1, 0); - tab.Margin!.Thickness = new (0, 0, 0, topLine); + tab.Margin!.Thickness = new (0, 0, 0, 1); break; case TabSide.Bottom: @@ -735,32 +750,7 @@ private void RenderTabLine (Tab []? tabLocations) break; case TabSide.Left: tab.Border!.Thickness = new (topLine, 1, 0, 1); - tab.Margin!.Thickness = new (0, 0, topLine, 0); - - break; - case TabSide.Right: - break; - default: - throw new ArgumentOutOfRangeException (); - } - } - else if (selected is null) - { - switch (Style.TabsSide) - { - case TabSide.Top: - tab.Border!.Thickness = new (1, topLine, 1, 1); - tab.Margin!.Thickness = new (0, 0, 0, 0); - - break; - case TabSide.Bottom: - tab.Border!.Thickness = new (1, 1, 1, topLine); - tab.Margin!.Thickness = new (0, 0, 0, 0); - - break; - case TabSide.Left: - tab.Border!.Thickness = new (topLine, 1, 1, 1); - tab.Margin!.Thickness = new (0, 0, 0, 0); + tab.Margin!.Thickness = new (0, 0, 1, 0); break; case TabSide.Right: @@ -771,21 +761,20 @@ private void RenderTabLine (Tab []? tabLocations) } else { + tab.Margin!.Thickness = new (0, 0, 0, 0); + switch (Style.TabsSide) { case TabSide.Top: tab.Border!.Thickness = new (1, topLine, 1, 1); - tab.Margin!.Thickness = new (0, 0, 0, 0); break; case TabSide.Bottom: tab.Border!.Thickness = new (1, 1, 1, topLine); - tab.Margin!.Thickness = new (0, 0, 0, 0); break; case TabSide.Left: tab.Border!.Thickness = new (topLine, 1, 1, 1); - tab.Margin!.Thickness = new (0, 0, 0, 0); break; case TabSide.Right: @@ -823,17 +812,14 @@ private int GetTabHeight (bool top) return Style.ShowInitialLine ? 3 : 2; } - internal void Tab_MouseClick (object sender, MouseEventArgs e) - { - e.Handled = _tabsBar.NewMouseEvent (e) == true; - } + 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++) + for (var i = 0; i < TabScrollOffset; i++) { Tab tab = Tabs.ElementAt (i); @@ -863,6 +849,4 @@ private void UnSetCurrentTabs () /// Raises the event. /// internal virtual void OnTabClicked (TabMouseEventArgs tabMouseEventArgs) { TabClicked?.Invoke (this, tabMouseEventArgs); } - - -} \ No newline at end of file +} diff --git a/UnitTests/Views/TabViewTests.cs b/UnitTests/Views/TabViewTests.cs index 4cced1ff5d..071a160423 100644 --- a/UnitTests/Views/TabViewTests.cs +++ b/UnitTests/Views/TabViewTests.cs @@ -1236,6 +1236,545 @@ public void ShowInitialLine_True_TabsSide_Bottom_With_Unicode () ); } + [Fact] + [SetupFakeDriver] + public void ShowInitialLine_True_TabsSide_Left_TestTabView_Height3 () + { + TabView tv = GetTabView (out _, out _); + tv.Width = 5; + tv.Height = 3; + tv.Style = new () { TabsSide = TabSide.Left }; + tv.ApplyStyleChanges (); + tv.Layout (); + + tv.Draw (); + + TestHelpers.AssertDriverContentsWithFrameAre ( + @" +╭───┐ +│T h│ +╰─▼─┘", + output + ); + } + + [Fact] + [SetupFakeDriver] + public void ShowInitialLine_True_TabsSide_Left_TestTabView_Height4 () + { + TabView tv = GetTabView (out _, out _); + tv.Width = 5; + tv.Height = 4; + tv.Style = new () { TabsSide = TabSide.Left }; + tv.ApplyStyleChanges (); + tv.Layout (); + + tv.Draw (); + + TestHelpers.AssertDriverContentsWithFrameAre ( + @" +╭───┐ +│T h│ +╰─╮ │ + ▼─┘", + output + ); + } + + [Fact] + [SetupFakeDriver] + public void ShowInitialLine_True_TabsSide_Left_TestTabView_Height5_One_Tab () + { + TabView tv = GetTabView (out _, out Tab tab2); + tv.RemoveTab (tab2); + tv.Width = 5; + tv.Height = 5; + tv.Style = new () { TabsSide = TabSide.Left }; + tv.ApplyStyleChanges (); + tv.Layout (); + + tv.Draw (); + + TestHelpers.AssertDriverContentsWithFrameAre ( + @" +╭───┐ +│T h│ +╰─╮ │ + │ │ + ╰─┘", + output + ); + } + + [Fact] + [SetupFakeDriver] + public void ShowInitialLine_True_TabsSide_Left_TestTabView_Height6_Two_Tab () + { + TabView tv = GetTabView (out _, out Tab tab2); + tv.Width = 5; + tv.Height = 6; + tv.Style = new () { TabsSide = TabSide.Left }; + tv.ApplyStyleChanges (); + tv.Layout (); + + tv.Draw (); + + TestHelpers.AssertDriverContentsWithFrameAre ( + @" +╭───┐ +│T h│ +├─╮ │ +│T│ │ +╰─┤ │ + ╰─┘", + output + ); + + tv.SelectedTab = tab2; + + tv.Layout (); + View.SetClipToScreen (); + tv.Draw (); + + TestHelpers.AssertDriverContentsWithFrameAre ( + @" +╭─┬─┐ +│T│h│ +├─╯ │ +│T │ +╰─╮ │ + ╰─┘", + output + ); + } + + [Fact] + [SetupFakeDriver] + public void ShowInitialLine_True_TabsSide_Left_TestTabView_Height7_Two_Tab () + { + TabView tv = GetTabView (out _, out Tab tab2); + tv.Width = 5; + tv.Height = 7; + tv.Style = new () { TabsSide = TabSide.Left }; + tv.ApplyStyleChanges (); + tv.Layout (); + + tv.Draw (); + + TestHelpers.AssertDriverContentsWithFrameAre ( + @" +╭───┐ +│T h│ +├─╮ │ +│T│ │ +╰─┤ │ + │ │ + ╰─┘", + output + ); + + tv.SelectedTab = tab2; + + tv.Layout (); + View.SetClipToScreen (); + tv.Draw (); + + TestHelpers.AssertDriverContentsWithFrameAre ( + @" +╭─┬─┐ +│T│h│ +├─╯ │ +│T │ +╰─╮ │ + │ │ + ╰─┘", + output + ); + } + + [Fact] + [SetupFakeDriver] + public void ShowInitialLine_True_TabsSide_Left_TestThinTabView_WithLongNames () + { + TabView tv = GetTabView (out Tab tab1, out Tab tab2); + tv.Width = 10; + tv.Height = 5; + tv.Style = new () { TabsSide = TabSide.Left }; + tv.ApplyStyleChanges (); + tv.Layout (); + + // Test two tab names that fit + tab1.DisplayText = "12"; + tab2.DisplayText = "13"; + + tv.Layout (); + tv.Draw (); + + TestHelpers.AssertDriverContentsWithFrameAre ( + @" +╭────────┐ +│12 hi │ +├──╮ │ +│13│ │ +╰──┴─────┘", + output + ); + + // Test first tab name too long + tab1.DisplayText = "12345678910"; + tab2.DisplayText = "13"; + + tv.Layout (); + View.SetClipToScreen (); + tv.Draw (); + + TestHelpers.AssertDriverContentsWithFrameAre ( + @" +╭────────┐ +│123456 h│ +├──────╮ │ +│13 │ │ +╰──────┴─┘", + output + ); + + //switch to tab2 + tv.SelectedTab = tab2; + + tv.Layout (); + View.SetClipToScreen (); + tv.Draw (); + + TestHelpers.AssertDriverContentsWithFrameAre ( + @" +╭──────┬─┐ +│123456│h│ +├──────╯ │ +│13 │ +╰────────┘", + output + ); + + // now make both tabs too long + tab1.DisplayText = "12345678910"; + tab2.DisplayText = "abcdefghijklmnopq"; + + tv.Layout (); + View.SetClipToScreen (); + tv.Draw (); + + TestHelpers.AssertDriverContentsWithFrameAre ( + @" +╭──────┬─┐ +│123456│h│ +├──────╯ │ +│abcdef │ +╰────────┘", + output + ); + } + + [Fact] + [SetupFakeDriver] + public void ShowInitialLine_True_TabsSide_Left_With_Unicode () + { + TabView tv = GetTabView (out Tab tab1, out Tab tab2); + tv.Width = 20; + tv.Height = 5; + tv.Style = new () { TabsSide = TabSide.Left }; + tv.ApplyStyleChanges (); + + tab1.DisplayText = "Tab0"; + + tab2.DisplayText = "Les Mise" + char.ConvertFromUtf32 (int.Parse ("0301", NumberStyles.HexNumber)) + "rables"; + + tv.Layout (); + tv.Draw (); + + TestHelpers.AssertDriverContentsWithFrameAre ( + @" +╭──────────────────┐ +│Tab0 hi │ +├──────────────╮ │ +│Les Misérables│ │ +╰──────────────┴───┘", + output + ); + + tv.SelectedTab = tab2; + + tv.Layout (); + View.SetClipToScreen (); + tv.Draw (); + + TestHelpers.AssertDriverContentsWithFrameAre ( + @" +╭──────────────┬───┐ +│Tab0 │hi2│ +├──────────────╯ │ +│Les Misérables │ +╰──────────────────┘", + output + ); + } + + [Fact] + [SetupFakeDriver] + public void ShowInitialLine_False_TabsSide_Left_TestTabView_Height3 () + { + TabView tv = GetTabView (out _, out _); + tv.Width = 5; + tv.Height = 3; + tv.Style = new () { ShowInitialLine = false, TabsSide = TabSide.Left }; + tv.ApplyStyleChanges (); + tv.Layout (); + + tv.Draw (); + + TestHelpers.AssertDriverContentsWithFrameAre ( + @" +────┐ +Ta h│ +──▼─┘", + output + ); + } + + [Fact] + [SetupFakeDriver] + public void ShowInitialLine_False_TabsSide_Left_TestTabView_Height4 () + { + TabView tv = GetTabView (out _, out _); + tv.Width = 5; + tv.Height = 4; + tv.Style = new () { ShowInitialLine = false, TabsSide = TabSide.Left }; + tv.ApplyStyleChanges (); + tv.Layout (); + + tv.Draw (); + + TestHelpers.AssertDriverContentsWithFrameAre ( + @" +────┐ +Ta h│ +──╮ │ + ▼─┘", + output + ); + } + + [Fact] + [SetupFakeDriver] + public void ShowInitialLine_False_TabsSide_Left_TestTabView_Height5_One_Tab () + { + TabView tv = GetTabView (out _, out Tab tab2); + tv.RemoveTab (tab2); + tv.Width = 5; + tv.Height = 5; + tv.Style = new () { TabsSide = TabSide.Left, ShowInitialLine = false }; + tv.ApplyStyleChanges (); + tv.Layout (); + + tv.Draw (); + + TestHelpers.AssertDriverContentsWithFrameAre ( + @" +────┐ +Ta h│ +──╮ │ + │ │ + ╰─┘", + output + ); + } + + [Fact] + [SetupFakeDriver] + public void ShowInitialLine_False_TabsSide_Left_TestTabView_Height6_Two_Tab () + { + TabView tv = GetTabView (out _, out Tab tab2); + tv.Width = 5; + tv.Height = 6; + tv.Style = new () { TabsSide = TabSide.Left, ShowInitialLine = false }; + tv.ApplyStyleChanges (); + tv.Layout (); + + tv.Draw (); + + TestHelpers.AssertDriverContentsWithFrameAre ( + @" +────┐ +Ta h│ +──╮ │ +Ta│ │ +──┤ │ + ╰─┘", + output + ); + + tv.SelectedTab = tab2; + + tv.Layout (); + View.SetClipToScreen (); + tv.Draw (); + + TestHelpers.AssertDriverContentsWithFrameAre ( + @" +──┬─┐ +Ta│h│ +──╯ │ +Ta │ +──╮ │ + ╰─┘", + output + ); + } + + [Fact] + [SetupFakeDriver] + public void ShowInitialLine_False_TabsSide_Left_TestTabView_Height7_Two_Tab () + { + TabView tv = GetTabView (out _, out Tab tab2); + tv.Width = 5; + tv.Height = 7; + tv.Style = new () { TabsSide = TabSide.Left, ShowInitialLine = false }; + tv.ApplyStyleChanges (); + tv.Layout (); + + tv.Draw (); + + TestHelpers.AssertDriverContentsWithFrameAre ( + @" +────┐ +Ta h│ +──╮ │ +Ta│ │ +──┤ │ + │ │ + ╰─┘", + output + ); + + tv.SelectedTab = tab2; + + tv.Layout (); + View.SetClipToScreen (); + tv.Draw (); + + TestHelpers.AssertDriverContentsWithFrameAre ( + @" +──┬─┐ +Ta│h│ +──╯ │ +Ta │ +──╮ │ + │ │ + ╰─┘", + output + ); + } + + [Fact] + [SetupFakeDriver] + public void ShowInitialLine_False_TabsSide_Left_TestThinTabView_WithLongNames () + { + TabView tv = GetTabView (out Tab tab1, out Tab tab2); + tv.Width = 10; + tv.Height = 5; + tv.Style = new () { ShowInitialLine = false, TabsSide = TabSide.Left }; + tv.ApplyStyleChanges (); + tv.Layout (); + + // Test two tab names that fit + tab1.DisplayText = "12"; + tab2.DisplayText = "13"; + + tv.Layout (); + tv.Draw (); + + TestHelpers.AssertDriverContentsWithFrameAre ( + @" +─────────┐ +12 hi │ +──╮ │ +13│ │ +──┴──────┘", + output + ); + + tv.SelectedTab = tab2; + Assert.Equal (tab2, tv.Subviews.First (v => v.Id.Contains ("tabRow")).MostFocused); + + tv.Layout (); + View.SetClipToScreen (); + tv.Draw (); + + TestHelpers.AssertDriverContentsWithFrameAre ( + @" +──┬──────┐ +12│hi2 │ +──╯ │ +13 │ +─────────┘", + output + ); + + tv.SelectedTab = tab1; + + // Test first tab name too long + tab1.DisplayText = "12345678910"; + tab2.DisplayText = "13"; + + tv.Layout (); + View.SetClipToScreen (); + tv.Draw (); + + TestHelpers.AssertDriverContentsWithFrameAre ( + @" +─────────┐ +1234567 h│ +───────╮ │ +13 │ │ +───────┴─┘", + output + ); + + //switch to tab2 + tv.SelectedTab = tab2; + + tv.Layout (); + View.SetClipToScreen (); + tv.Draw (); + + TestHelpers.AssertDriverContentsWithFrameAre ( + @" +───────┬─┐ +1234567│h│ +───────╯ │ +13 │ +─────────┘", + output + ); + + // now make both tabs too long + tab1.DisplayText = "12345678910"; + tab2.DisplayText = "abcdefghijklmnopq"; + + tv.Layout (); + View.SetClipToScreen (); + tv.Draw (); + + TestHelpers.AssertDriverContentsWithFrameAre ( + @" +───────┬─┐ +1234567│h│ +───────╯ │ +abcdefg │ +─────────┘", + output + ); + } + [Fact] public void SwitchTabBy_NormalUsage () { @@ -1305,7 +1844,7 @@ public void RemoveTab_ThatHasFocus () [Fact] [SetupFakeDriver] - public void Add_Three_TabsOnTop_ChangesTab () + public void Add_Three_TabsSide_Top_ChangesTab () { TabView tv = GetTabView (out Tab tab1, out Tab tab2); Tab tab3; @@ -1370,7 +1909,7 @@ public void Add_Three_TabsOnTop_ChangesTab () [Fact] [SetupFakeDriver] - public void Add_Three_TabsOnBottom_ChangesTab () + public void Add_Three_TabsSide_Bottom_ChangesTab () { TabView tv = GetTabView (out Tab tab1, out Tab tab2); Tab tab3; From bd666934985a677496312fbd3a099f2691cb4b5c Mon Sep 17 00:00:00 2001 From: BDisp Date: Sun, 1 Dec 2024 12:16:43 +0000 Subject: [PATCH 14/18] Implemented right side feature. --- Terminal.Gui/Views/TabView/TabRow.cs | 259 ++++- Terminal.Gui/Views/TabView/TabView.cs | 28 +- UnitTests/Views/TabViewTests.cs | 1389 +++++++++++++++++++++++-- 3 files changed, 1606 insertions(+), 70 deletions(-) diff --git a/Terminal.Gui/Views/TabView/TabRow.cs b/Terminal.Gui/Views/TabView/TabRow.cs index 175d500710..7f6ec99bb9 100644 --- a/Terminal.Gui/Views/TabView/TabRow.cs +++ b/Terminal.Gui/Views/TabView/TabRow.cs @@ -204,6 +204,14 @@ private void RenderTabLineCanvas () break; case TabSide.Right: + // Upper horizontal line + lc.AddLine ( + new (vts.X - 1, vts.Y - 1), + -1, + Orientation.Horizontal, + tab.BorderStyle + ); + break; default: throw new ArgumentOutOfRangeException (); @@ -268,6 +276,24 @@ private void RenderTabLineCanvas () break; case TabSide.Right: + if (Frame.Bottom > tab.Frame.Bottom) + { + // LRCorner + lc.AddLine ( + new (vts.X - 1, vts.Bottom), + 1, + Orientation.Vertical, + tab.BorderStyle + ); + + lc.AddLine ( + new (vts.X - 1, vts.Bottom), + 1, + Orientation.Horizontal, + tab.BorderStyle + ); + } + break; default: throw new ArgumentOutOfRangeException (); @@ -329,6 +355,21 @@ private void RenderTabLineCanvas () break; case TabSide.Right: + // Upper left tee + lc.AddLine ( + new (vts.Right, vts.Y - 1), + 0, + Orientation.Vertical, + tab.BorderStyle + ); + + lc.AddLine ( + new (vts.Right, vts.Y - 1), + -1, + Orientation.Horizontal, + tab.BorderStyle + ); + break; default: throw new ArgumentOutOfRangeException (); @@ -394,6 +435,21 @@ private void RenderTabLineCanvas () break; case TabSide.Right: + // Upper right tee + lc.AddLine ( + new (vts.Right, vts.Bottom), + 0, + Orientation.Vertical, + tab.BorderStyle + ); + + lc.AddLine ( + new (vts.Right, vts.Bottom), + -1, + Orientation.Horizontal, + tab.BorderStyle + ); + break; default: throw new ArgumentOutOfRangeException (); @@ -458,6 +514,24 @@ private void RenderTabLineCanvas () break; case TabSide.Right: + if (Frame.Bottom > tab.Frame.Bottom) + { + //LRCorner + lc.AddLine ( + new (vts.X - 1, vts.Bottom), + 1, + Orientation.Vertical, + tab.BorderStyle + ); + + lc.AddLine ( + new (vts.X - 1, vts.Bottom), + 1, + Orientation.Horizontal, + tab.BorderStyle + ); + } + break; default: throw new ArgumentOutOfRangeException (); @@ -656,6 +730,39 @@ private void RenderTabLineCanvas () break; case TabSide.Right: + if (_host.Style.ShowInitialLine || _host.Style.TabsSide == TabSide.Right) + { + // Upper left tee + lc.AddLine ( + new (vts.Right, vts.Y - 1), + 0, + Orientation.Vertical, + tab.BorderStyle + ); + + lc.AddLine ( + new (vts.Right, vts.Y - 1), + -1, + Orientation.Horizontal, + tab.BorderStyle + ); + } + + // Lower left tee + lc.AddLine ( + new (vts.X - 1, vts.Y - 1), + 0, + Orientation.Vertical, + tab.BorderStyle + ); + + lc.AddLine ( + new (vts.X - 1, vts.Y - 1), + 1, + Orientation.Horizontal, + tab.BorderStyle + ); + break; default: throw new ArgumentOutOfRangeException (); @@ -704,6 +811,21 @@ private void RenderTabLineCanvas () break; case TabSide.Right: + // Upper right tee + lc.AddLine ( + new (vts.X - 1, vts.Bottom), + 0, + Orientation.Vertical, + tab.BorderStyle + ); + + lc.AddLine ( + new (vts.X - 1, vts.Bottom), + 1, + Orientation.Horizontal, + tab.BorderStyle + ); + break; default: throw new ArgumentOutOfRangeException (); @@ -750,6 +872,21 @@ private void RenderTabLineCanvas () break; case TabSide.Right: + // Lower right tee + lc.AddLine ( + new (vts.Right, vts.Bottom), + 0, + Orientation.Vertical, + tab.BorderStyle + ); + + lc.AddLine ( + new (vts.Right, vts.Bottom), + -1, + Orientation.Horizontal, + tab.BorderStyle + ); + break; default: throw new ArgumentOutOfRangeException (); @@ -874,6 +1011,21 @@ private void RenderTabLineCanvas () break; case TabSide.Right: + // Upper horizontal line + lc.AddLine ( + new (vts.X - 1, vts.Y - 1), + 1, + Orientation.Vertical, + tab.BorderStyle + ); + + lc.AddLine ( + new (vts.X - 1, vts.Y - 1), + 0, + Orientation.Horizontal, + tab.BorderStyle + ); + break; default: throw new ArgumentOutOfRangeException (); @@ -939,6 +1091,24 @@ private void RenderTabLineCanvas () break; case TabSide.Right: + if (Frame.Bottom > tab.Frame.Bottom) + { + // Lower right tee + lc.AddLine ( + new (vts.X - 1, vts.Bottom), + 0, + Orientation.Vertical, + tab.BorderStyle + ); + + lc.AddLine ( + new (vts.X - 1, vts.Bottom), + 1, + Orientation.Horizontal, + tab.BorderStyle + ); + } + break; default: throw new ArgumentOutOfRangeException (); @@ -1010,6 +1180,16 @@ private void RenderTabLineCanvas () break; case TabSide.Right: + lc.AddLine ( + new ( + vts.X - 1, + vts.Bottom + ), + lineLength - arrowOffset, + Orientation.Vertical, + tab.BorderStyle + ); + break; default: throw new ArgumentOutOfRangeException (); @@ -1102,6 +1282,65 @@ private void RenderTabLineCanvas () break; case TabSide.Right: + if (i == selectedTab) + { + if (Frame.Bottom == tab.Frame.Bottom) + { + // Lower right horizontal line + lc.AddLine ( + new (vts.X - 1, vts.Bottom), + 0, + Orientation.Horizontal, + tab.BorderStyle + ); + } + else + { + lc.AddLine ( + new (vts.X - 1, vts.Bottom), + lineLength, + Orientation.Vertical, + tab.BorderStyle + ); + } + } + else + { + if (Frame.Bottom == tab.Frame.Bottom) + { + lc.AddLine ( + new (vts.X - 1, vts.Bottom), + -1, + Orientation.Vertical, + tab.BorderStyle + ); + + lc.AddLine ( + new (vts.X - 1, vts.Bottom), + 0, + Orientation.Horizontal, + tab.BorderStyle + ); + + } + else + { + lc.AddLine ( + new (vts.X - 1, vts.Bottom), + lineLength, + Orientation.Vertical, + tab.BorderStyle + ); + + lc.AddLine ( + new (vts.X - 1, tabsBarVts.Bottom), + 1, + Orientation.Horizontal, + tab.BorderStyle + ); + } + } + break; default: throw new ArgumentOutOfRangeException (); @@ -1154,6 +1393,20 @@ private void RenderTabLineCanvas () break; case TabSide.Right: + if (Frame.Bottom > tab.Frame.Bottom) + { + // More URCorner + lc.AddLine ( + new ( + vts.X - 1, + tabsBarVts.Bottom - 1 + ), + -1, + Orientation.Horizontal, + tab.BorderStyle + ); + } + break; default: throw new ArgumentOutOfRangeException (); @@ -1208,6 +1461,7 @@ private void RenderUnderline () break; case TabSide.Bottom: + case TabSide.Right: _leftUpScrollIndicator.X = 0; _leftUpScrollIndicator.Y = 0; @@ -1216,8 +1470,6 @@ private void RenderUnderline () _leftUpScrollIndicator.X = Pos.AnchorEnd (1); _leftUpScrollIndicator.Y = 0; - break; - case TabSide.Right: break; default: throw new ArgumentOutOfRangeException (); @@ -1251,6 +1503,9 @@ private void RenderUnderline () break; case TabSide.Right: + _rightDownScrollIndicator.X = 0; + _rightDownScrollIndicator.Y = Pos.AnchorEnd (1); + break; default: throw new ArgumentOutOfRangeException (); diff --git a/Terminal.Gui/Views/TabView/TabView.cs b/Terminal.Gui/Views/TabView/TabView.cs index c04229a679..96c036e8af 100644 --- a/Terminal.Gui/Views/TabView/TabView.cs +++ b/Terminal.Gui/Views/TabView/TabView.cs @@ -275,7 +275,6 @@ public void ApplyStyleChanges () _tabLocations = CalculateViewport (Viewport); _containerView.BorderStyle = Style.ShowBorder ? LineStyle.Single : LineStyle.None; - _containerView.Width = Dim.Fill (); switch (Style.TabsSide) { @@ -342,6 +341,21 @@ public void ApplyStyleChanges () break; case TabSide.Right: + // Tabs are along the right + if (Style.ShowBorder) + { + _containerView.Border!.Thickness = new (1, 1, 0, 1); + } + + _tabsBar.Y = 0; + _tabsBar.Height = Dim.Fill (); + + //move content left to make space for tabs + _containerView.X = 0; + _containerView.Y = 0; + + _containerView.Height = Dim.Fill (); + break; default: throw new ArgumentOutOfRangeException (); @@ -685,6 +699,13 @@ protected override void Dispose (bool disposing) _tabsBar.Width = maxColWidth; + if (Style.TabsSide == TabSide.Right) + { + _tabsBar.X = Pos.AnchorEnd (maxColWidth); + // Fill client area leaving space at right for tabs + _containerView.Width = Dim.Fill (maxColWidth); + } + int GetMaxColWidth (int textWidth) { int maxViewportWidth = Math.Max (0, Viewport.Width - (Style.ShowBorder ? 2 : 0)); @@ -754,6 +775,9 @@ private void RenderTabLine (Tab []? tabLocations) break; case TabSide.Right: + tab.Border!.Thickness = new (0, 1, topLine, 1); + tab.Margin!.Thickness = new (1, 0, 0, 0); + break; default: throw new ArgumentOutOfRangeException (); @@ -778,6 +802,8 @@ private void RenderTabLine (Tab []? tabLocations) break; case TabSide.Right: + tab.Border!.Thickness = new (1, 1, topLine, 1); + break; default: throw new ArgumentOutOfRangeException (); diff --git a/UnitTests/Views/TabViewTests.cs b/UnitTests/Views/TabViewTests.cs index 071a160423..1a0921df2c 100644 --- a/UnitTests/Views/TabViewTests.cs +++ b/UnitTests/Views/TabViewTests.cs @@ -1775,76 +1775,1238 @@ 1234567 h│ ); } + [Fact] + [SetupFakeDriver] + public void ShowInitialLine_True_TabsSide_Right_TestTabView_Height3 () + { + TabView tv = GetTabView (out _, out _); + tv.Width = 5; + tv.Height = 3; + tv.Style = new () { TabsSide = TabSide.Right }; + tv.ApplyStyleChanges (); + tv.Layout (); + + tv.Draw (); + + TestHelpers.AssertDriverContentsWithFrameAre ( + @" +┌───╮ +│h T│ +└─▼─╯", + output + ); + } + + [Fact] + [SetupFakeDriver] + public void ShowInitialLine_True_TabsSide_Right_TestTabView_Height4 () + { + TabView tv = GetTabView (out _, out _); + tv.Width = 5; + tv.Height = 4; + tv.Style = new () { TabsSide = TabSide.Right }; + tv.ApplyStyleChanges (); + tv.Layout (); + + tv.Draw (); + + TestHelpers.AssertDriverContentsWithFrameAre ( + @" +┌───╮ +│h T│ +│ ╭─╯ +└─▼ ", + output + ); + } + + [Fact] + [SetupFakeDriver] + public void ShowInitialLine_True_TabsSide_Right_TestTabView_Height5_One_Tab () + { + TabView tv = GetTabView (out _, out Tab tab2); + tv.RemoveTab (tab2); + tv.Width = 5; + tv.Height = 5; + tv.Style = new () { TabsSide = TabSide.Right }; + tv.ApplyStyleChanges (); + tv.Layout (); + + tv.Draw (); + + TestHelpers.AssertDriverContentsWithFrameAre ( + @" +┌───╮ +│h T│ +│ ╭─╯ +│ │ +└─╯ ", + output + ); + } + + [Fact] + [SetupFakeDriver] + public void ShowInitialLine_True_TabsSide_Right_TestTabView_Height6_Two_Tab () + { + TabView tv = GetTabView (out _, out Tab tab2); + tv.Width = 5; + tv.Height = 6; + tv.Style = new () { TabsSide = TabSide.Right }; + tv.ApplyStyleChanges (); + tv.Layout (); + + tv.Draw (); + + TestHelpers.AssertDriverContentsWithFrameAre ( + @" +┌───╮ +│h T│ +│ ╭─┤ +│ │T│ +│ ├─╯ +└─╯ ", + output + ); + + tv.SelectedTab = tab2; + + tv.Layout (); + View.SetClipToScreen (); + tv.Draw (); + + TestHelpers.AssertDriverContentsWithFrameAre ( + @" +┌─┬─╮ +│h│T│ +│ ╰─┤ +│ T│ +│ ╭─╯ +└─╯ ", + output + ); + } + + [Fact] + [SetupFakeDriver] + public void ShowInitialLine_True_TabsSide_Right_TestTabView_Height7_Two_Tab () + { + TabView tv = GetTabView (out _, out Tab tab2); + tv.Width = 5; + tv.Height = 7; + tv.Style = new () { TabsSide = TabSide.Right }; + tv.ApplyStyleChanges (); + tv.Layout (); + + tv.Draw (); + + TestHelpers.AssertDriverContentsWithFrameAre ( + @" +┌───╮ +│h T│ +│ ╭─┤ +│ │T│ +│ ├─╯ +│ │ +└─╯ ", + output + ); + + tv.SelectedTab = tab2; + + tv.Layout (); + View.SetClipToScreen (); + tv.Draw (); + + TestHelpers.AssertDriverContentsWithFrameAre ( + @" +┌─┬─╮ +│h│T│ +│ ╰─┤ +│ T│ +│ ╭─╯ +│ │ +└─╯ ", + output + ); + } + + [Fact] + [SetupFakeDriver] + public void ShowInitialLine_True_TabsSide_Right_TestThinTabView_WithLongNames () + { + TabView tv = GetTabView (out Tab tab1, out Tab tab2); + tv.Width = 10; + tv.Height = 5; + tv.Style = new () { TabsSide = TabSide.Right }; + tv.ApplyStyleChanges (); + tv.Layout (); + + // Test two tab names that fit + tab1.DisplayText = "12"; + tab2.DisplayText = "13"; + + tv.Layout (); + tv.Draw (); + + TestHelpers.AssertDriverContentsWithFrameAre ( + @" +┌────────╮ +│hi 12│ +│ ╭──┤ +│ │13│ +└─────┴──╯", + output + ); + + // Test first tab name too long + tab1.DisplayText = "12345678910"; + tab2.DisplayText = "13"; + + tv.Layout (); + View.SetClipToScreen (); + tv.Draw (); + + TestHelpers.AssertDriverContentsWithFrameAre ( + @" +┌────────╮ +│h 123456│ +│ ╭──────┤ +│ │13 │ +└─┴──────╯", + output + ); + + //switch to tab2 + tv.SelectedTab = tab2; + + tv.Layout (); + View.SetClipToScreen (); + tv.Draw (); + + TestHelpers.AssertDriverContentsWithFrameAre ( + @" +┌─┬──────╮ +│h│123456│ +│ ╰──────┤ +│ 13 │ +└────────╯", + output + ); + + // now make both tabs too long + tab1.DisplayText = "12345678910"; + tab2.DisplayText = "abcdefghijklmnopq"; + + tv.Layout (); + View.SetClipToScreen (); + tv.Draw (); + + TestHelpers.AssertDriverContentsWithFrameAre ( + @" +┌─┬──────╮ +│h│123456│ +│ ╰──────┤ +│ abcdef│ +└────────╯", + output + ); + } + + [Fact] + [SetupFakeDriver] + public void ShowInitialLine_True_TabsSide_Right_With_Unicode () + { + TabView tv = GetTabView (out Tab tab1, out Tab tab2); + tv.Width = 20; + tv.Height = 5; + tv.Style = new () { TabsSide = TabSide.Right }; + tv.ApplyStyleChanges (); + + tab1.DisplayText = "Tab0"; + + tab2.DisplayText = "Les Mise" + char.ConvertFromUtf32 (int.Parse ("0301", NumberStyles.HexNumber)) + "rables"; + + tv.Layout (); + tv.Draw (); + + TestHelpers.AssertDriverContentsWithFrameAre ( + @" +┌──────────────────╮ +│hi Tab0 │ +│ ╭──────────────┤ +│ │Les Misérables│ +└───┴──────────────╯", + output + ); + + tv.SelectedTab = tab2; + + tv.Layout (); + View.SetClipToScreen (); + tv.Draw (); + + TestHelpers.AssertDriverContentsWithFrameAre ( + @" +┌───┬──────────────╮ +│hi2│Tab0 │ +│ ╰──────────────┤ +│ Les Misérables│ +└──────────────────╯", + output + ); + } + + [Fact] + [SetupFakeDriver] + public void ShowInitialLine_False_TabsSide_Right_TestTabView_Height3 () + { + TabView tv = GetTabView (out _, out _); + tv.Width = 5; + tv.Height = 3; + tv.Style = new () { ShowInitialLine = false, TabsSide = TabSide.Right }; + tv.ApplyStyleChanges (); + tv.Layout (); + + tv.Draw (); + + TestHelpers.AssertDriverContentsWithFrameAre ( + @" +┌──── +│h Ta +└─▼──", + output + ); + } + + [Fact] + [SetupFakeDriver] + public void ShowInitialLine_False_TabsSide_Right_TestTabView_Height4 () + { + TabView tv = GetTabView (out _, out _); + tv.Width = 5; + tv.Height = 4; + tv.Style = new () { ShowInitialLine = false, TabsSide = TabSide.Right }; + tv.ApplyStyleChanges (); + tv.Layout (); + + tv.Draw (); + + TestHelpers.AssertDriverContentsWithFrameAre ( + @" +┌──── +│h Ta +│ ╭── +└─▼ ", + output + ); + } + + [Fact] + [SetupFakeDriver] + public void ShowInitialLine_False_TabsSide_Right_TestTabView_Height5_One_Tab () + { + TabView tv = GetTabView (out _, out Tab tab2); + tv.RemoveTab (tab2); + tv.Width = 5; + tv.Height = 5; + tv.Style = new () { TabsSide = TabSide.Right, ShowInitialLine = false }; + tv.ApplyStyleChanges (); + tv.Layout (); + + tv.Draw (); + + TestHelpers.AssertDriverContentsWithFrameAre ( + @" +┌──── +│h Ta +│ ╭── +│ │ +└─╯ ", + output + ); + } + + [Fact] + [SetupFakeDriver] + public void ShowInitialLine_False_TabsSide_Right_TestTabView_Height6_Two_Tab () + { + TabView tv = GetTabView (out _, out Tab tab2); + tv.Width = 5; + tv.Height = 6; + tv.Style = new () { TabsSide = TabSide.Right, ShowInitialLine = false }; + tv.ApplyStyleChanges (); + tv.Layout (); + + tv.Draw (); + + TestHelpers.AssertDriverContentsWithFrameAre ( + @" +┌──── +│h Ta +│ ╭── +│ │Ta +│ ├── +└─╯ ", + output + ); + + tv.SelectedTab = tab2; + + tv.Layout (); + View.SetClipToScreen (); + tv.Draw (); + + TestHelpers.AssertDriverContentsWithFrameAre ( + @" +┌─┬── +│h│Ta +│ ╰── +│ Ta +│ ╭── +└─╯ ", + output + ); + } + + [Fact] + [SetupFakeDriver] + public void ShowInitialLine_False_TabsSide_Right_TestTabView_Height7_Two_Tab () + { + TabView tv = GetTabView (out _, out Tab tab2); + tv.Width = 5; + tv.Height = 7; + tv.Style = new () { TabsSide = TabSide.Right, ShowInitialLine = false }; + tv.ApplyStyleChanges (); + tv.Layout (); + + tv.Draw (); + + TestHelpers.AssertDriverContentsWithFrameAre ( + @" +┌──── +│h Ta +│ ╭── +│ │Ta +│ ├── +│ │ +└─╯ ", + output + ); + + tv.SelectedTab = tab2; + + tv.Layout (); + View.SetClipToScreen (); + tv.Draw (); + + TestHelpers.AssertDriverContentsWithFrameAre ( + @" +┌─┬── +│h│Ta +│ ╰── +│ Ta +│ ╭── +│ │ +└─╯ ", + output + ); + } + + [Fact] + [SetupFakeDriver] + public void ShowInitialLine_False_TabsSide_Right_TestThinTabView_WithLongNames () + { + TabView tv = GetTabView (out Tab tab1, out Tab tab2); + tv.Width = 10; + tv.Height = 5; + tv.Style = new () { ShowInitialLine = false, TabsSide = TabSide.Right }; + tv.ApplyStyleChanges (); + tv.Layout (); + + // Test two tab names that fit + tab1.DisplayText = "12"; + tab2.DisplayText = "13"; + + tv.Layout (); + tv.Draw (); + + TestHelpers.AssertDriverContentsWithFrameAre ( + @" +┌───────── +│hi 12 +│ ╭── +│ │13 +└──────┴──", + output + ); + + tv.SelectedTab = tab2; + Assert.Equal (tab2, tv.Subviews.First (v => v.Id.Contains ("tabRow")).MostFocused); + + tv.Layout (); + View.SetClipToScreen (); + tv.Draw (); + + TestHelpers.AssertDriverContentsWithFrameAre ( + @" +┌──────┬── +│hi2 │12 +│ ╰── +│ 13 +└─────────", + output + ); + + tv.SelectedTab = tab1; + + // Test first tab name too long + tab1.DisplayText = "12345678910"; + tab2.DisplayText = "13"; + + tv.Layout (); + View.SetClipToScreen (); + tv.Draw (); + + TestHelpers.AssertDriverContentsWithFrameAre ( + @" +┌───────── +│h 1234567 +│ ╭─────── +│ │13 +└─┴───────", + output + ); + + //switch to tab2 + tv.SelectedTab = tab2; + + tv.Layout (); + View.SetClipToScreen (); + tv.Draw (); + + TestHelpers.AssertDriverContentsWithFrameAre ( + @" +┌─┬─────── +│h│1234567 +│ ╰─────── +│ 13 +└─────────", + output + ); + + // now make both tabs too long + tab1.DisplayText = "12345678910"; + tab2.DisplayText = "abcdefghijklmnopq"; + + tv.Layout (); + View.SetClipToScreen (); + tv.Draw (); + + TestHelpers.AssertDriverContentsWithFrameAre ( + @" +┌─┬─────── +│h│1234567 +│ ╰─────── +│ abcdefg +└─────────", + output + ); + } + [Fact] public void SwitchTabBy_NormalUsage () { TabView tv = GetTabView (out Tab tab1, out Tab tab2); - + + Tab tab3; + Tab tab4; + Tab tab5; + + tv.AddTab (tab3 = new (), false); + tv.AddTab (tab4 = new (), false); + tv.AddTab (tab5 = new (), false); + + tv.SelectedTab = tab1; + + var called = 0; + tv.SelectedTabChanged += (s, e) => { called++; }; + + tv.SwitchTabBy (1); + + Assert.Equal (1, called); + Assert.Equal (tab2, tv.SelectedTab); + + //reset called counter + called = 0; + + // go right 2 + tv.SwitchTabBy (2); + + // even though we go right 2 indexes the event should only be called once + Assert.Equal (1, called); + Assert.Equal (tab4, tv.SelectedTab); + } + + [Fact] + public void SwitchTabBy_OutOfTabsRange () + { + TabView tv = GetTabView (out Tab tab1, out Tab tab2); + + tv.SelectedTab = tab1; + tv.SwitchTabBy (500); + + Assert.Equal (tab2, tv.SelectedTab); + + tv.SwitchTabBy (-500); + + Assert.Equal (tab1, tv.SelectedTab); + } + + [Fact] + public void RemoveTab_ThatHasFocus () + { + TabView tv = GetTabView (out Tab _, out Tab tab2); + + tv.SelectedTab = tab2; + tab2.HasFocus = true; + + Assert.Equal (2, tv.Tabs.Count); + + foreach (Tab t in tv.Tabs.ToArray ()) + { + tv.RemoveTab (t); + } + + Assert.Empty (tv.Tabs); + } + + [Fact] + [SetupFakeDriver] + public void Add_Three_TabsSide_Top_ShowInitialLine_True_ChangesTab () + { + TabView tv = GetTabView (out Tab tab1, out Tab tab2); + 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_TabsSide_Top_ShowInitialLine_False_ChangesTab () + { + TabView tv = GetTabView (out Tab tab1, out Tab tab2); + 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 () { ShowInitialLine = false }; + tv.ApplyStyleChanges (); + + 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_TabsSide_Bottom_ShowInitialLine_True_ChangesTab () + { + TabView tv = GetTabView (out Tab tab1, out Tab tab2); + 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 () { TabsSide = TabSide.Bottom }; + 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 + ); + } + + [Fact] + [SetupFakeDriver] + public void Add_Three_TabsSide_Bottom_ShowInitialLine_False_ChangesTab () + { + TabView tv = GetTabView (out Tab tab1, out Tab tab2); + 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 () { TabsSide = TabSide.Bottom, ShowInitialLine = false }; + 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 + ); + } + + [Fact] + [SetupFakeDriver] + public void Add_Three_TabsSide_Left_ShowInitialLine_True_ChangesTab_Height5 () + { + TabView tv = GetTabView (out Tab tab1, out Tab tab2); + 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 () { TabsSide = TabSide.Left }; + tv.ApplyStyleChanges (); + + tv.Layout (); + tv.Draw (); + + Assert.Equal (tab1, tv.SelectedTab); + + TestHelpers.AssertDriverContentsAre ( + @" +╭──────────────────┐ +│Tab1 hi │ +├────╮ │ +│Tab2│ │ +╰────▼─────────────┘ +", + output + ); + + tv.SelectedTab = tab2; + + tv.Layout (); + View.SetClipToScreen (); + tv.Draw (); + + TestHelpers.AssertDriverContentsWithFrameAre ( + @" +╭────┬─────────────┐ +│Tab1│hi2 │ +├────╯ │ +│Tab2 │ +╰────▼─────────────┘ +", + output + ); + + tv.SelectedTab = tab3; + + tv.Layout (); + View.SetClipToScreen (); + tv.Draw (); + + TestHelpers.AssertDriverContentsWithFrameAre ( + @" +╭────▲─────────────┐ +│Tab3 hi3 │ +╰────╮ │ + │ │ + ╰─────────────┘ +", + output + ); + } + + [Fact] + [SetupFakeDriver] + public void Add_Three_TabsSide_Left_ShowInitialLine_False_ChangesTab_Height5 () + { + TabView tv = GetTabView (out Tab tab1, out Tab tab2); + 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 () { TabsSide = TabSide.Left, ShowInitialLine = false }; + tv.ApplyStyleChanges (); + + tv.Layout (); + tv.Draw (); + + Assert.Equal (tab1, tv.SelectedTab); + + TestHelpers.AssertDriverContentsAre ( + @" +───────────────────┐ +Tab1 hi │ +────╮ │ +Tab2│ │ +────▼──────────────┘ +", + output + ); + + tv.SelectedTab = tab2; + + tv.Layout (); + View.SetClipToScreen (); + tv.Draw (); + + TestHelpers.AssertDriverContentsWithFrameAre ( + @" +────┬──────────────┐ +Tab1│hi2 │ +────╯ │ +Tab2 │ +────▼──────────────┘ +", + output + ); + + tv.SelectedTab = tab3; + + tv.Layout (); + View.SetClipToScreen (); + tv.Draw (); + + TestHelpers.AssertDriverContentsWithFrameAre ( + @" +────▲──────────────┐ +Tab3 hi3 │ +────╮ │ + │ │ + ╰──────────────┘ +", + output + ); + } + + [Fact] + [SetupFakeDriver] + public void Add_Three_TabsSide_Left_ShowInitialLine_True_ChangesTab_Height9 () + { + TabView tv = GetTabView (out Tab tab1, out Tab tab2); Tab tab3; - Tab tab4; - Tab tab5; - tv.AddTab (tab3 = new (), false); - tv.AddTab (tab4 = new (), false); - tv.AddTab (tab5 = new (), false); + tv.AddTab ( + tab3 = new () { Id = "tab3", DisplayText = "Tab3", View = new TextView { Id = "tab3.TextView", Width = 3, Height = 1, Text = "hi3" } }, + false); - tv.SelectedTab = tab1; + tv.Width = 20; + tv.Height = 9; + tv.Style = new () { TabsSide = TabSide.Left }; + tv.ApplyStyleChanges (); - var called = 0; - tv.SelectedTabChanged += (s, e) => { called++; }; + tv.Layout (); + tv.Draw (); - tv.SwitchTabBy (1); + Assert.Equal (tab1, tv.SelectedTab); - Assert.Equal (1, called); - Assert.Equal (tab2, tv.SelectedTab); + TestHelpers.AssertDriverContentsAre ( + @" +╭──────────────────┐ +│Tab1 hi │ +├────╮ │ +│Tab2│ │ +├────┤ │ +│Tab3│ │ +╰────┤ │ + │ │ + ╰─────────────┘", + output + ); - //reset called counter - called = 0; + tv.SelectedTab = tab2; - // go right 2 - tv.SwitchTabBy (2); + tv.Layout (); + View.SetClipToScreen (); + tv.Draw (); - // even though we go right 2 indexes the event should only be called once - Assert.Equal (1, called); - Assert.Equal (tab4, tv.SelectedTab); + TestHelpers.AssertDriverContentsWithFrameAre ( + @" +╭────┬─────────────┐ +│Tab1│hi2 │ +├────╯ │ +│Tab2 │ +├────╮ │ +│Tab3│ │ +╰────┤ │ + │ │ + ╰─────────────┘ +", + output + ); + + tv.SelectedTab = tab3; + + tv.Layout (); + View.SetClipToScreen (); + tv.Draw (); + + TestHelpers.AssertDriverContentsWithFrameAre ( + @" +╭────┬─────────────┐ +│Tab1│hi3 │ +├────┤ │ +│Tab2│ │ +├────╯ │ +│Tab3 │ +╰────╮ │ + │ │ + ╰─────────────┘ +", + output + ); } [Fact] - public void SwitchTabBy_OutOfTabsRange () + [SetupFakeDriver] + public void Add_Three_TabsSide_Left_ShowInitialLine_False_ChangesTab_Height9 () { TabView tv = GetTabView (out Tab tab1, out Tab tab2); + Tab tab3; - tv.SelectedTab = tab1; - tv.SwitchTabBy (500); + tv.AddTab ( + tab3 = new () { Id = "tab3", DisplayText = "Tab3", View = new TextView { Id = "tab3.TextView", Width = 3, Height = 1, Text = "hi3" } }, + false); - Assert.Equal (tab2, tv.SelectedTab); + tv.Width = 20; + tv.Height = 9; + tv.Style = new () { TabsSide = TabSide.Left, ShowInitialLine = false }; + tv.ApplyStyleChanges (); - tv.SwitchTabBy (-500); + tv.Layout (); + tv.Draw (); Assert.Equal (tab1, tv.SelectedTab); + + TestHelpers.AssertDriverContentsAre ( + @" +───────────────────┐ +Tab1 hi │ +────╮ │ +Tab2│ │ +────┤ │ +Tab3│ │ +────┤ │ + │ │ + ╰──────────────┘ +", + output + ); + + tv.SelectedTab = tab2; + + tv.Layout (); + View.SetClipToScreen (); + tv.Draw (); + + TestHelpers.AssertDriverContentsWithFrameAre ( + @" +────┬──────────────┐ +Tab1│hi2 │ +────╯ │ +Tab2 │ +────╮ │ +Tab3│ │ +────┤ │ + │ │ + ╰──────────────┘ +", + output + ); + + tv.SelectedTab = tab3; + + tv.Layout (); + View.SetClipToScreen (); + tv.Draw (); + + TestHelpers.AssertDriverContentsWithFrameAre ( + @" +────┬──────────────┐ +Tab1│hi3 │ +────┤ │ +Tab2│ │ +────╯ │ +Tab3 │ +────╮ │ + │ │ + ╰──────────────┘ +", + output + ); } [Fact] - public void RemoveTab_ThatHasFocus () + [SetupFakeDriver] + public void Add_Three_TabsSide_Right_ShowInitialLine_True_ChangesTab_Height5 () { - TabView tv = GetTabView (out Tab _, out Tab tab2); + TabView tv = GetTabView (out Tab tab1, out Tab tab2); + 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 () { TabsSide = TabSide.Right }; + tv.ApplyStyleChanges (); + + tv.Layout (); + tv.Draw (); + + Assert.Equal (tab1, tv.SelectedTab); + + TestHelpers.AssertDriverContentsAre ( + @" +┌──────────────────╮ +│hi Tab1│ +│ ╭────┤ +│ │Tab2│ +└─────────────▼────╯ +", + output + ); tv.SelectedTab = tab2; - tab2.HasFocus = true; - Assert.Equal (2, tv.Tabs.Count); + tv.Layout (); + View.SetClipToScreen (); + tv.Draw (); - foreach (Tab t in tv.Tabs.ToArray ()) - { - tv.RemoveTab (t); - } + TestHelpers.AssertDriverContentsWithFrameAre ( + @" +┌─────────────┬────╮ +│hi2 │Tab1│ +│ ╰────┤ +│ Tab2│ +└─────────────▼────╯ +", + output + ); - Assert.Empty (tv.Tabs); + tv.SelectedTab = tab3; + + tv.Layout (); + View.SetClipToScreen (); + tv.Draw (); + + TestHelpers.AssertDriverContentsWithFrameAre ( + @" +┌─────────────▲────╮ +│hi3 Tab3│ +│ ╭────╯ +│ │ +└─────────────╯ +", + output + ); } [Fact] [SetupFakeDriver] - public void Add_Three_TabsSide_Top_ChangesTab () + public void Add_Three_TabsSide_Right_ShowInitialLine_False_ChangesTab_Height5 () { TabView tv = GetTabView (out Tab tab1, out Tab tab2); Tab tab3; @@ -1855,6 +3017,8 @@ public void Add_Three_TabsSide_Top_ChangesTab () tv.Width = 20; tv.Height = 5; + tv.Style = new () { TabsSide = TabSide.Right, ShowInitialLine = false }; + tv.ApplyStyleChanges (); tv.Layout (); tv.Draw (); @@ -1863,11 +3027,11 @@ public void Add_Three_TabsSide_Top_ChangesTab () TestHelpers.AssertDriverContentsAre ( @" -╭────┬────┬────╮ -│Tab1│Tab2│Tab3│ -│ ╰────┴────┴───╮ -│hi │ -└──────────────────┘ +┌─────────────────── +│hi Tab1 +│ ╭──── +│ │Tab2 +└──────────────▼──── ", output ); @@ -1880,11 +3044,11 @@ public void Add_Three_TabsSide_Top_ChangesTab () TestHelpers.AssertDriverContentsWithFrameAre ( @" -╭────┬────┬────╮ -│Tab1│Tab2│Tab3│ -├────╯ ╰────┴───╮ -│hi2 │ -└──────────────────┘ +┌──────────────┬──── +│hi2 │Tab1 +│ ╰──── +│ Tab2 +└──────────────▼──── ", output ); @@ -1897,11 +3061,11 @@ public void Add_Three_TabsSide_Top_ChangesTab () TestHelpers.AssertDriverContentsWithFrameAre ( @" -╭────┬────┬────╮ -│Tab1│Tab2│Tab3│ -├────┴────╯ ╰───╮ -│hi3 │ -└──────────────────┘ +┌──────────────▲──── +│hi3 Tab3 +│ ╭──── +│ │ +└──────────────╯ ", output ); @@ -1909,7 +3073,7 @@ public void Add_Three_TabsSide_Top_ChangesTab () [Fact] [SetupFakeDriver] - public void Add_Three_TabsSide_Bottom_ChangesTab () + public void Add_Three_TabsSide_Right_ShowInitialLine_True_ChangesTab_Height9 () { TabView tv = GetTabView (out Tab tab1, out Tab tab2); Tab tab3; @@ -1919,8 +3083,8 @@ public void Add_Three_TabsSide_Bottom_ChangesTab () false); tv.Width = 20; - tv.Height = 5; - tv.Style = new () { TabsSide = TabSide.Bottom }; + tv.Height = 9; + tv.Style = new () { TabsSide = TabSide.Right }; tv.ApplyStyleChanges (); tv.Layout (); @@ -1930,11 +3094,15 @@ public void Add_Three_TabsSide_Bottom_ChangesTab () TestHelpers.AssertDriverContentsAre ( @" -┌──────────────────┐ -│hi │ -│ ╭────┬────┬───╯ -│Tab1│Tab2│Tab3│ -╰────┴────┴────╯ +┌──────────────────╮ +│hi Tab1│ +│ ╭────┤ +│ │Tab2│ +│ ├────┤ +│ │Tab3│ +│ ├────╯ +│ │ +└─────────────╯ ", output ); @@ -1947,11 +3115,15 @@ public void Add_Three_TabsSide_Bottom_ChangesTab () TestHelpers.AssertDriverContentsWithFrameAre ( @" -┌──────────────────┐ -│hi2 │ -├────╮ ╭────┬───╯ -│Tab1│Tab2│Tab3│ -╰────┴────┴────╯ +┌─────────────┬────╮ +│hi2 │Tab1│ +│ ╰────┤ +│ Tab2│ +│ ╭────┤ +│ │Tab3│ +│ ├────╯ +│ │ +└─────────────╯ ", output ); @@ -1964,11 +3136,94 @@ public void Add_Three_TabsSide_Bottom_ChangesTab () TestHelpers.AssertDriverContentsWithFrameAre ( @" -┌──────────────────┐ -│hi3 │ -├────┬────╮ ╭───╯ -│Tab1│Tab2│Tab3│ -╰────┴────┴────╯ +┌─────────────┬────╮ +│hi3 │Tab1│ +│ ├────┤ +│ │Tab2│ +│ ╰────┤ +│ Tab3│ +│ ╭────╯ +│ │ +└─────────────╯ +", + output + ); + } + + [Fact] + [SetupFakeDriver] + public void Add_Three_TabsSide_Right_ShowInitialLine_False_ChangesTab_Height9 () + { + TabView tv = GetTabView (out Tab tab1, out Tab tab2); + 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 = 9; + tv.Style = new () { TabsSide = TabSide.Right, ShowInitialLine = false }; + 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 ); From 43c83ee1cf28a3e857f91a499812e35cde3b35d3 Mon Sep 17 00:00:00 2001 From: BDisp Date: Mon, 2 Dec 2024 00:41:24 +0000 Subject: [PATCH 15/18] Fix ShowInitialLine right side bug. --- Terminal.Gui/Views/TabView/TabRow.cs | 15 +++++++++++++++ UnitTests/Views/TabViewTests.cs | 2 +- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/Terminal.Gui/Views/TabView/TabRow.cs b/Terminal.Gui/Views/TabView/TabRow.cs index 7f6ec99bb9..925b019f38 100644 --- a/Terminal.Gui/Views/TabView/TabRow.cs +++ b/Terminal.Gui/Views/TabView/TabRow.cs @@ -948,6 +948,21 @@ private void RenderTabLineCanvas () break; case TabSide.Right: + // Lower right tee + lc.AddLine ( + new (vts.X - 1, vts.Bottom), + 0, + Orientation.Vertical, + tab.BorderStyle + ); + + lc.AddLine ( + new (vts.X - 1, vts.Bottom), + 1, + Orientation.Horizontal, + tab.BorderStyle + ); + break; default: throw new ArgumentOutOfRangeException (); diff --git a/UnitTests/Views/TabViewTests.cs b/UnitTests/Views/TabViewTests.cs index 1a0921df2c..2315394777 100644 --- a/UnitTests/Views/TabViewTests.cs +++ b/UnitTests/Views/TabViewTests.cs @@ -3177,7 +3177,7 @@ public void Add_Three_TabsSide_Right_ShowInitialLine_False_ChangesTab_Height9 () │hi Tab1 │ ╭──── │ │Tab2 -│ ╭──── +│ ├──── │ │Tab3 │ ├──── │ │ From 97c2d528002e15d9a0cebc654b8e8d71e731a304 Mon Sep 17 00:00:00 2001 From: BDisp Date: Mon, 2 Dec 2024 01:38:14 +0000 Subject: [PATCH 16/18] Add TabsTextAlignment feature. --- Terminal.Gui/Views/TabView/TabStyle.cs | 5 + Terminal.Gui/Views/TabView/TabView.cs | 1 + UICatalog/Scenarios/TabViewExample.cs | 47 ++- UnitTests/Views/TabViewTests.cs | 445 +++++++++++++++++++++++++ 4 files changed, 497 insertions(+), 1 deletion(-) diff --git a/Terminal.Gui/Views/TabView/TabStyle.cs b/Terminal.Gui/Views/TabView/TabStyle.cs index 900b882631..74107d2c1e 100644 --- a/Terminal.Gui/Views/TabView/TabStyle.cs +++ b/Terminal.Gui/Views/TabView/TabStyle.cs @@ -20,4 +20,9 @@ public class TabStyle /// Gets or sets the tabs side to render. public TabSide TabsSide { get; set; } + + /// + /// Gets or sets the tabs text alignments. + /// + public Alignment TabsTextAlignment { get; set; } } diff --git a/Terminal.Gui/Views/TabView/TabView.cs b/Terminal.Gui/Views/TabView/TabView.cs index 96c036e8af..6a910a16f1 100644 --- a/Terminal.Gui/Views/TabView/TabView.cs +++ b/Terminal.Gui/Views/TabView/TabView.cs @@ -643,6 +643,7 @@ protected override void Dispose (bool disposing) int maxHeight = Math.Max (0, Math.Min (bounds.Height - 2, 2)); tab.Height = 2; + tab.TextAlignment = Style.TabsTextAlignment; // if tab view is height <= 3 don't render any tabs if (maxHeight == 0) diff --git a/UICatalog/Scenarios/TabViewExample.cs b/UICatalog/Scenarios/TabViewExample.cs index 1d107aee97..6a6f574d99 100644 --- a/UICatalog/Scenarios/TabViewExample.cs +++ b/UICatalog/Scenarios/TabViewExample.cs @@ -16,6 +16,8 @@ public class TabViewExample : Scenario private MenuItem _miShowTopLine; private MenuItem [] _miTabsSide; private MenuItem _cachedTabsSide; + private MenuItem [] _miTabsTextAlignment; + private MenuItem _cachedTabsTextAlignment; private TabView _tabView; public override void Main () @@ -27,6 +29,7 @@ public override void Main () Toplevel appWindow = new (); _miTabsSide = SetTabsSide (); + _miTabsTextAlignment = SetTabsTextAlignment (); var menu = new MenuBar { @@ -70,7 +73,12 @@ public override void Main () "_Show TabView Border", "", ShowTabViewBorder - ) { Checked = true, CheckType = MenuItemCheckStyle.Checked } + ) { Checked = true, CheckType = MenuItemCheckStyle.Checked }, + null, + _miTabsTextAlignment [0], + _miTabsTextAlignment [1], + _miTabsTextAlignment [2], + _miTabsTextAlignment [3] } ) ] @@ -284,6 +292,43 @@ private MenuItem [] SetTabsSide () return menuItems.ToArray (); } + private MenuItem [] SetTabsTextAlignment () + { + List menuItems = []; + + foreach (TabSide align in Enum.GetValues (typeof (Alignment))) + { + string alignName = Enum.GetName (typeof (Alignment), align); + var item = new MenuItem { Title = $"_{alignName}", Data = align }; + item.CheckType |= MenuItemCheckStyle.Radio; + + item.Action += () => + { + if (_cachedTabsTextAlignment == item) + { + return; + } + + _cachedTabsTextAlignment.Checked = false; + item.Checked = true; + _cachedTabsTextAlignment = item; + _tabView.Style.TabsTextAlignment = (Alignment)item.Data; + _tabView.ApplyStyleChanges (); + }; + item.ShortcutKey = ((Key)alignName! [0].ToString ().ToLower ()).WithCtrl; + + if (alignName == "Start") + { + item.Checked = true; + _cachedTabsTextAlignment = item; + } + + menuItems.Add (item); + } + + return menuItems.ToArray (); + } + private void ShowBorder () { _miShowBorder.Checked = !_miShowBorder.Checked; diff --git a/UnitTests/Views/TabViewTests.cs b/UnitTests/Views/TabViewTests.cs index 2315394777..ef39655b6b 100644 --- a/UnitTests/Views/TabViewTests.cs +++ b/UnitTests/Views/TabViewTests.cs @@ -3278,6 +3278,451 @@ public void Mouse_Wheel_Changes_Tab () top.Dispose (); } + [Fact] + [SetupFakeDriver] + public void Tabs_Alignments () + { + TabView tv = GetTabView (out Tab tab1, out Tab tab2); + tv.Width = 20; + tv.Height = 5; + + tab1.DisplayText = "Tab 1"; + tab2.DisplayText = "Long Text"; + + tv.Layout (); + tv.Draw (); + + string top = @" +╭─────┬─────────╮ +│Tab 1│Long Text│ +│ ╰─────────┴──╮ +│hi │ +└──────────────────┘ +"; + + TestHelpers.AssertDriverContentsWithFrameAre (top, output); + + tv.Style = new () { TabsSide = TabSide.Bottom }; + tv.ApplyStyleChanges (); + tv.Layout (); + View.SetClipToScreen (); + tv.Draw (); + + string bottom = @" +┌──────────────────┐ +│hi │ +│ ╭─────────┬──╯ +│Tab 1│Long Text│ +╰─────┴─────────╯ +"; + + TestHelpers.AssertDriverContentsWithFrameAre (bottom, output); + + tv.Style = new () { TabsSide = TabSide.Left }; + tv.ApplyStyleChanges (); + tv.Layout (); + View.SetClipToScreen (); + tv.Draw (); + + TestHelpers.AssertDriverContentsWithFrameAre ( + @" +╭──────────────────┐ +│Tab 1 hi │ +├─────────╮ │ +│Long Text│ │ +╰─────────┴────────┘ +", + output + ); + + tv.Style = new () { TabsSide = TabSide.Right }; + tv.ApplyStyleChanges (); + tv.Layout (); + View.SetClipToScreen (); + tv.Draw (); + + TestHelpers.AssertDriverContentsWithFrameAre ( + @" +┌──────────────────╮ +│hi Tab 1 │ +│ ╭─────────┤ +│ │Long Text│ +└────────┴─────────╯ +", + output + ); + + Assert.Equal (Alignment.Start, tv.Style.TabsTextAlignment); + + tv.Style = new () { TabsSide = TabSide.Top, TabsTextAlignment = Alignment.End }; + tv.ApplyStyleChanges (); + tv.Layout (); + View.SetClipToScreen (); + tv.Draw (); + + TestHelpers.AssertDriverContentsWithFrameAre (top, output); + + tv.Style = new () { TabsSide = TabSide.Bottom, TabsTextAlignment = Alignment.End }; + tv.ApplyStyleChanges (); + tv.Layout (); + View.SetClipToScreen (); + tv.Draw (); + + TestHelpers.AssertDriverContentsWithFrameAre (bottom, output); + + tv.Style = new () { TabsSide = TabSide.Left, TabsTextAlignment = Alignment.End }; + tv.ApplyStyleChanges (); + tv.Layout (); + View.SetClipToScreen (); + tv.Draw (); + + TestHelpers.AssertDriverContentsWithFrameAre ( + @" +╭──────────────────┐ +│ Tab 1 hi │ +├─────────╮ │ +│Long Text│ │ +╰─────────┴────────┘ +", + output + ); + + tv.Style = new () { TabsSide = TabSide.Right, TabsTextAlignment = Alignment.End }; + tv.ApplyStyleChanges (); + tv.Layout (); + View.SetClipToScreen (); + tv.Draw (); + + TestHelpers.AssertDriverContentsWithFrameAre ( + @" +┌──────────────────╮ +│hi Tab 1│ +│ ╭─────────┤ +│ │Long Text│ +└────────┴─────────╯ +", + output + ); + + tv.Style = new () { TabsSide = TabSide.Top, TabsTextAlignment = Alignment.Center }; + tv.ApplyStyleChanges (); + tv.Layout (); + View.SetClipToScreen (); + tv.Draw (); + + TestHelpers.AssertDriverContentsWithFrameAre (top, output); + + tv.Style = new () { TabsSide = TabSide.Bottom, TabsTextAlignment = Alignment.Center }; + tv.ApplyStyleChanges (); + tv.Layout (); + View.SetClipToScreen (); + tv.Draw (); + + TestHelpers.AssertDriverContentsWithFrameAre (bottom, output); + + tv.Style = new () { TabsSide = TabSide.Left, TabsTextAlignment = Alignment.Center }; + tv.ApplyStyleChanges (); + tv.Layout (); + View.SetClipToScreen (); + tv.Draw (); + + TestHelpers.AssertDriverContentsWithFrameAre ( + @" +╭──────────────────┐ +│ Tab 1 hi │ +├─────────╮ │ +│Long Text│ │ +╰─────────┴────────┘ +", + output + ); + + tv.Style = new () { TabsSide = TabSide.Right, TabsTextAlignment = Alignment.Center }; + tv.ApplyStyleChanges (); + tv.Layout (); + View.SetClipToScreen (); + tv.Draw (); + + TestHelpers.AssertDriverContentsWithFrameAre ( + @" +┌──────────────────╮ +│hi Tab 1 │ +│ ╭─────────┤ +│ │Long Text│ +└────────┴─────────╯ +", + output + ); + + tv.Style = new () { TabsSide = TabSide.Top, TabsTextAlignment = Alignment.Fill }; + tv.ApplyStyleChanges (); + tv.Layout (); + View.SetClipToScreen (); + tv.Draw (); + + TestHelpers.AssertDriverContentsWithFrameAre (top, output); + + tv.Style = new () { TabsSide = TabSide.Bottom, TabsTextAlignment = Alignment.Fill }; + tv.ApplyStyleChanges (); + tv.Layout (); + View.SetClipToScreen (); + tv.Draw (); + + TestHelpers.AssertDriverContentsWithFrameAre (bottom, output); + + tv.Style = new () { TabsSide = TabSide.Left, TabsTextAlignment = Alignment.Fill }; + tv.ApplyStyleChanges (); + tv.Layout (); + View.SetClipToScreen (); + tv.Draw (); + + TestHelpers.AssertDriverContentsWithFrameAre ( + @" +╭──────────────────┐ +│Tab 1 hi │ +├─────────╮ │ +│Long Text│ │ +╰─────────┴────────┘ +", + output + ); + + tv.Style = new () { TabsSide = TabSide.Right, TabsTextAlignment = Alignment.Fill }; + tv.ApplyStyleChanges (); + tv.Layout (); + View.SetClipToScreen (); + tv.Draw (); + + TestHelpers.AssertDriverContentsWithFrameAre ( + @" +┌──────────────────╮ +│hi Tab 1│ +│ ╭─────────┤ +│ │Long Text│ +└────────┴─────────╯ +", + output + ); + + tv.Style = new () { TabsSide = TabSide.Top, ShowInitialLine = false, TabsTextAlignment = Alignment.Start }; + tv.ApplyStyleChanges (); + tv.Layout (); + View.SetClipToScreen (); + tv.Draw (); + + top = @" +│Tab 1│Long Text│ +│ ╰─────────┴──╮ +│hi │ +│ │ +└──────────────────┘ +"; + + TestHelpers.AssertDriverContentsWithFrameAre (top, output); + + // ShowInitialLine false + + tv.Style = new () { TabsSide = TabSide.Bottom, ShowInitialLine = false, TabsTextAlignment = Alignment.Start }; + tv.ApplyStyleChanges (); + tv.Layout (); + View.SetClipToScreen (); + tv.Draw (); + + bottom = @" +┌──────────────────┐ +│hi │ +│ │ +│ ╭─────────┬──╯ +│Tab 1│Long Text│ +"; + + TestHelpers.AssertDriverContentsWithFrameAre (bottom, output); + + tv.Style = new () { TabsSide = TabSide.Left, ShowInitialLine = false, TabsTextAlignment = Alignment.Start }; + tv.ApplyStyleChanges (); + tv.Layout (); + View.SetClipToScreen (); + tv.Draw (); + + TestHelpers.AssertDriverContentsWithFrameAre ( + @" +───────────────────┐ +Tab 1 hi │ +─────────╮ │ +Long Text│ │ +─────────┴─────────┘ +", + output + ); + + tv.Style = new () { TabsSide = TabSide.Right, ShowInitialLine = false, TabsTextAlignment = Alignment.Start }; + tv.ApplyStyleChanges (); + tv.Layout (); + View.SetClipToScreen (); + tv.Draw (); + + TestHelpers.AssertDriverContentsWithFrameAre ( + @" +┌─────────────────── +│hi Tab 1 +│ ╭───────── +│ │Long Text +└─────────┴───────── +", + output + ); + + tv.Style = new () { TabsSide = TabSide.Top, ShowInitialLine = false, TabsTextAlignment = Alignment.End }; + tv.ApplyStyleChanges (); + tv.Layout (); + View.SetClipToScreen (); + tv.Draw (); + + TestHelpers.AssertDriverContentsWithFrameAre (top, output); + + tv.Style = new () { TabsSide = TabSide.Bottom, ShowInitialLine = false, TabsTextAlignment = Alignment.End }; + tv.ApplyStyleChanges (); + tv.Layout (); + View.SetClipToScreen (); + tv.Draw (); + + TestHelpers.AssertDriverContentsWithFrameAre (bottom, output); + + tv.Style = new () { TabsSide = TabSide.Left, ShowInitialLine = false, TabsTextAlignment = Alignment.End }; + tv.ApplyStyleChanges (); + tv.Layout (); + View.SetClipToScreen (); + tv.Draw (); + + TestHelpers.AssertDriverContentsWithFrameAre ( + @" +───────────────────┐ + Tab 1 hi │ +─────────╮ │ +Long Text│ │ +─────────┴─────────┘ +", + output + ); + + tv.Style = new () { TabsSide = TabSide.Right, ShowInitialLine = false, TabsTextAlignment = Alignment.End }; + tv.ApplyStyleChanges (); + tv.Layout (); + View.SetClipToScreen (); + tv.Draw (); + + TestHelpers.AssertDriverContentsWithFrameAre ( + @" +┌─────────────────── +│hi Tab 1 +│ ╭───────── +│ │Long Text +└─────────┴───────── +", + output + ); + + tv.Style = new () { TabsSide = TabSide.Top, ShowInitialLine = false, TabsTextAlignment = Alignment.Center }; + tv.ApplyStyleChanges (); + tv.Layout (); + View.SetClipToScreen (); + tv.Draw (); + + TestHelpers.AssertDriverContentsWithFrameAre (top, output); + + tv.Style = new () { TabsSide = TabSide.Bottom, ShowInitialLine = false, TabsTextAlignment = Alignment.Center }; + tv.ApplyStyleChanges (); + tv.Layout (); + View.SetClipToScreen (); + tv.Draw (); + + TestHelpers.AssertDriverContentsWithFrameAre (bottom, output); + + tv.Style = new () { TabsSide = TabSide.Left, ShowInitialLine = false, TabsTextAlignment = Alignment.Center }; + tv.ApplyStyleChanges (); + tv.Layout (); + View.SetClipToScreen (); + tv.Draw (); + + TestHelpers.AssertDriverContentsWithFrameAre ( + @" +───────────────────┐ + Tab 1 hi │ +─────────╮ │ +Long Text│ │ +─────────┴─────────┘ +", + output + ); + + tv.Style = new () { TabsSide = TabSide.Right, ShowInitialLine = false, TabsTextAlignment = Alignment.Center }; + tv.ApplyStyleChanges (); + tv.Layout (); + View.SetClipToScreen (); + tv.Draw (); + + TestHelpers.AssertDriverContentsWithFrameAre ( + @" +┌─────────────────── +│hi Tab 1 +│ ╭───────── +│ │Long Text +└─────────┴───────── +", + output + ); + + tv.Style = new () { TabsSide = TabSide.Top, ShowInitialLine = false, TabsTextAlignment = Alignment.Fill }; + tv.ApplyStyleChanges (); + tv.Layout (); + View.SetClipToScreen (); + tv.Draw (); + + TestHelpers.AssertDriverContentsWithFrameAre (top, output); + + tv.Style = new () { TabsSide = TabSide.Bottom, ShowInitialLine = false, TabsTextAlignment = Alignment.Fill }; + tv.ApplyStyleChanges (); + tv.Layout (); + View.SetClipToScreen (); + tv.Draw (); + + TestHelpers.AssertDriverContentsWithFrameAre (bottom, output); + + tv.Style = new () { TabsSide = TabSide.Left, ShowInitialLine = false, TabsTextAlignment = Alignment.Fill }; + tv.ApplyStyleChanges (); + tv.Layout (); + View.SetClipToScreen (); + tv.Draw (); + + TestHelpers.AssertDriverContentsWithFrameAre ( + @" +───────────────────┐ +Tab 1 hi │ +─────────╮ │ +Long Text│ │ +─────────┴─────────┘ +", + output + ); + + tv.Style = new () { TabsSide = TabSide.Right, ShowInitialLine = false, TabsTextAlignment = Alignment.Fill }; + tv.ApplyStyleChanges (); + tv.Layout (); + View.SetClipToScreen (); + tv.Draw (); + + TestHelpers.AssertDriverContentsWithFrameAre ( + @" +┌─────────────────── +│hi Tab 1 +│ ╭───────── +│ │Long Text +└─────────┴───────── +", + output + ); + } + private TabView GetTabView () { return GetTabView (out _, out _); } private TabView GetTabView (out Tab tab1, out Tab tab2) From 1c77b0748260043d367f66525184d3216846d5fc Mon Sep 17 00:00:00 2001 From: BDisp Date: Mon, 2 Dec 2024 01:49:54 +0000 Subject: [PATCH 17/18] Fixes #3873. TextFormatter isn't properly handling combining marks on alignments. --- Terminal.Gui/Text/TextFormatter.cs | 12 ++-- UnitTests/Text/TextFormatterTests.cs | 84 ++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+), 6 deletions(-) diff --git a/Terminal.Gui/Text/TextFormatter.cs b/Terminal.Gui/Text/TextFormatter.cs index 90152452fd..a4a8b7060a 100644 --- a/Terminal.Gui/Text/TextFormatter.cs +++ b/Terminal.Gui/Text/TextFormatter.cs @@ -1411,7 +1411,7 @@ public static string ClipAndJustify ( if (textFormatter is { Alignment: Alignment.Center }) { - return GetRangeThatFits (runes, Math.Max ((runes.Count - width) / 2, 0), text, width, tabWidth, textDirection); + return GetRangeThatFits (runes, Math.Max ((runes.Count - width - zeroLength) / 2, 0), text, width, tabWidth, textDirection); } return GetRangeThatFits (runes, 0, text, width, tabWidth, textDirection); @@ -1426,7 +1426,7 @@ public static string ClipAndJustify ( if (textFormatter is { VerticalAlignment: Alignment.Center }) { - return GetRangeThatFits (runes, Math.Max ((runes.Count - width) / 2, 0), text, width, tabWidth, textDirection); + return GetRangeThatFits (runes, Math.Max ((runes.Count - width - zeroLength) / 2, 0), text, width, tabWidth, textDirection); } return GetRangeThatFits (runes, 0, text, width, tabWidth, textDirection); @@ -1451,7 +1451,7 @@ public static string ClipAndJustify ( } else if (textFormatter is { Alignment: Alignment.Center }) { - return GetRangeThatFits (runes, Math.Max ((runes.Count - width) / 2, 0), text, width, tabWidth, textDirection); + return GetRangeThatFits (runes, Math.Max ((runes.Count - width - zeroLength) / 2, 0), text, width, tabWidth, textDirection); } else if (GetRuneWidth (text, tabWidth, textDirection) > width) { @@ -1470,7 +1470,7 @@ public static string ClipAndJustify ( } else if (textFormatter is { VerticalAlignment: Alignment.Center }) { - return GetRangeThatFits (runes, Math.Max ((runes.Count - width) / 2, 0), text, width, tabWidth, textDirection); + return GetRangeThatFits (runes, Math.Max ((runes.Count - width - zeroLength) / 2, 0), text, width, tabWidth, textDirection); } else if (runes.Count - zeroLength > width) { @@ -1526,7 +1526,7 @@ public static string Justify ( } else { - textCount = words.Sum (arg => arg.GetRuneCount ()); + textCount = words.Sum (arg => arg.GetRuneCount ()) - text.EnumerateRunes ().Sum (r => r.GetColumns () == 0 ? 1 : 0); } int spaces = words.Length > 1 ? (width - textCount) / (words.Length - 1) : 0; @@ -1936,7 +1936,7 @@ private static int GetRuneWidth (List runes, int tabWidth, TextDirection t private static int GetRuneWidth (Rune rune, int tabWidth, TextDirection textDirection = TextDirection.LeftRight_TopBottom) { - int runeWidth = IsHorizontalDirection (textDirection) ? rune.GetColumns () : 1; + int runeWidth = IsHorizontalDirection (textDirection) ? rune.GetColumns () : rune.GetColumns () == 0 ? 0 : 1; if (rune.Value == '\t') { diff --git a/UnitTests/Text/TextFormatterTests.cs b/UnitTests/Text/TextFormatterTests.cs index d83ab398f2..5435d02044 100644 --- a/UnitTests/Text/TextFormatterTests.cs +++ b/UnitTests/Text/TextFormatterTests.cs @@ -4629,6 +4629,90 @@ string expectedWrappedText Assert.Equal (expectedWrappedText, wrappedText); } + [Theory] + [InlineData ( + "Les Mise\u0301rables", + 14, + -1, + false, + new [] { "Les Misérables" }, + "Les Misérables" + )] + [InlineData ( + "Les Mise\u0328\u0301rables", + 14, + -2, + false, + new [] { "Les Misę́rables" }, + "Les Misę́rables" + )] + public void Format_Combining_Marks_Alignments ( + string text, + int maxWidth, + int widthOffset, + bool wrap, + IEnumerable resultLines, + string expectedText + ) + { + Assert.Equal (maxWidth, text.GetRuneCount () + widthOffset); + + // Horizontal text direction + foreach (Alignment alignment in Enum.GetValues (typeof (Alignment))) + { + TextFormatter tf = new () { Text = text, ConstrainToSize = new (maxWidth, 1), WordWrap = wrap, Alignment = alignment }; + + List list = TextFormatter.Format ( + text, + maxWidth, + alignment, + wrap, + tf.PreserveTrailingSpaces, + tf.TabWidth, + tf.Direction, + tf.MultiLine, + tf); + Assert.Equal (list.Count, resultLines.Count ()); + Assert.Equal (resultLines, list); + var formattedText = string.Empty; + + foreach (string txt in list) + { + formattedText += txt; + } + + Assert.Equal (expectedText, formattedText); + } + + // Vertical text direction + foreach (Alignment alignment in Enum.GetValues (typeof (Alignment))) + { + TextFormatter tf = new () + { Text = text, ConstrainToSize = new (1, maxWidth), WordWrap = wrap, VerticalAlignment = alignment, Direction = TextDirection.TopBottom_LeftRight }; + + List list = TextFormatter.Format ( + text, + maxWidth, + alignment, + wrap, + tf.PreserveTrailingSpaces, + tf.TabWidth, + tf.Direction, + tf.MultiLine, + tf); + Assert.Equal (list.Count, resultLines.Count ()); + Assert.Equal (resultLines, list); + var formattedText = string.Empty; + + foreach (string txt in list) + { + formattedText += txt; + } + + Assert.Equal (expectedText, formattedText); + } + } + public static IEnumerable FormatEnvironmentNewLine => new List { From c93230689be4a4181e919b40f561a6e7f2d818f4 Mon Sep 17 00:00:00 2001 From: BDisp Date: Tue, 3 Dec 2024 22:30:08 +0000 Subject: [PATCH 18/18] Fix bug with the _rightDownScrollIndicator. --- Terminal.Gui/Views/TabView/TabRow.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Terminal.Gui/Views/TabView/TabRow.cs b/Terminal.Gui/Views/TabView/TabRow.cs index 925b019f38..d1a72b4322 100644 --- a/Terminal.Gui/Views/TabView/TabRow.cs +++ b/Terminal.Gui/Views/TabView/TabRow.cs @@ -1530,7 +1530,7 @@ private void RenderUnderline () _rightDownScrollIndicator.Visible = true; // Ensures this is clicked instead of the last tab if under this - MoveSubviewToStart (_rightDownScrollIndicator); + MoveSubviewToEnd (_rightDownScrollIndicator); } else {