From a739ad15235785e56b7c5435bf14b8cd793aa69d Mon Sep 17 00:00:00 2001 From: BDisp Date: Mon, 18 Nov 2024 23:26:06 +0000 Subject: [PATCH 01/13] Fixes TabView. --- Terminal.Gui/Views/Tab.cs | 2 +- Terminal.Gui/Views/TabView.cs | 198 ++++++++-------- UnitTests/View/Layout/SetLayoutTests.cs | 100 +++++++++ UnitTests/Views/TabViewTests.cs | 286 +++++++++++++++++------- 4 files changed, 400 insertions(+), 186 deletions(-) diff --git a/Terminal.Gui/Views/Tab.cs b/Terminal.Gui/Views/Tab.cs index b683b04b6f..52fb0bdf9d 100644 --- a/Terminal.Gui/Views/Tab.cs +++ b/Terminal.Gui/Views/Tab.cs @@ -22,7 +22,7 @@ public string DisplayText set { _displayText = value; - SetNeedsDraw (); + SetNeedsLayout (); } } diff --git a/Terminal.Gui/Views/TabView.cs b/Terminal.Gui/Views/TabView.cs index 99d90bec28..9d373f6f2e 100644 --- a/Terminal.Gui/Views/TabView.cs +++ b/Terminal.Gui/Views/TabView.cs @@ -101,6 +101,9 @@ public TabView () /// public uint MaxTabTextWidth { get; set; } = DefaultMaxTabTextWidth; + // This is needed to hold initial value because it may change during the setter process + private bool _selectedTabHasFocus; + /// The currently selected member of chosen by the user. /// public Tab? SelectedTab @@ -108,9 +111,8 @@ public Tab? SelectedTab get => _selectedTab; set { - UnSetCurrentTabs (); - Tab? old = _selectedTab; + _selectedTabHasFocus = old?.HasFocus == true || !_contentView.CanFocus; if (_selectedTab is { }) { @@ -138,7 +140,7 @@ public Tab? SelectedTab if (old != _selectedTab) { - if (old?.HasFocus == true) + if (_selectedTabHasFocus) { SelectedTab?.SetFocus (); } @@ -270,6 +272,14 @@ public void ApplyStyleChanges () SetNeedsLayout (); } + /// + protected override void OnViewportChanged (DrawEventArgs e) + { + _tabLocations = CalculateViewport (Viewport).ToArray (); + + base.OnViewportChanged (e); + } + /// Updates to ensure that is visible. public void EnsureSelectedTabIsVisible () { @@ -295,7 +305,7 @@ public void EnsureSelectedTabIsVisible () /// protected override void OnHasFocusChanged (bool newHasFocus, View? previousFocusedView, View? focusedView) { - if (SelectedTab is { } && !_contentView.CanFocus && focusedView == this) + if (SelectedTab is { HasFocus: false } && !_contentView.CanFocus && focusedView == this) { SelectedTab?.SetFocus (); @@ -305,25 +315,6 @@ protected override void OnHasFocusChanged (bool newHasFocus, View? previousFocus base.OnHasFocusChanged (newHasFocus, previousFocusedView, focusedView); } - /// - protected override bool OnDrawingContent () - { - if (Tabs.Any ()) - { - // Region savedClip = SetClip (); - _tabsBar.Draw (); - _contentView.SetNeedsDraw (); - _contentView.Draw (); - - //if (Driver is { }) - //{ - // Driver.Clip = savedClip; - //} - } - - return true; - } - /// /// Removes the given from . Caller is responsible for disposing the /// tab's hosted if appropriate. @@ -451,7 +442,7 @@ private IEnumerable CalculateViewport (Rectangle bounds) { if (prevTab is { }) { - tab.X = Pos.Right (prevTab); + tab.X = Pos.Right (prevTab) - 1; } else { @@ -470,8 +461,6 @@ private IEnumerable CalculateViewport (Rectangle bounds) // or we won't even be able to render a single tab! long maxWidth = Math.Max (0, Math.Min (bounds.Width - 3, MaxTabTextWidth)); - prevTab = tab; - tab.Width = 2; tab.Height = Style.ShowTopLine ? 3 : 2; @@ -481,16 +470,20 @@ private IEnumerable CalculateViewport (Rectangle bounds) tab.Visible = true; tab.MouseClick += Tab_MouseClick!; - yield return new TabToRender (tab, string.Empty, Equals (SelectedTab, tab)); + yield return new (tab, string.Empty, Equals (SelectedTab, tab)); break; } if (tabTextWidth > maxWidth) { - text = tab.DisplayText.Substring (0, (int)maxWidth); + text = tab.Text = tab.DisplayText.Substring (0, (int)maxWidth); tabTextWidth = (int)maxWidth; } + else + { + tab.Text = text; + } tab.Width = Math.Max (tabTextWidth + 2, 1); tab.Height = Style.ShowTopLine ? 3 : 2; @@ -506,11 +499,19 @@ private IEnumerable CalculateViewport (Rectangle bounds) // there is enough space! tab.Visible = true; tab.MouseClick += Tab_MouseClick!; + tab.Border!.MouseClick += Tab_MouseClick!; - yield return new TabToRender (tab, text, Equals (SelectedTab, tab)); + yield return new (tab, text, Equals (SelectedTab, tab)); + + prevTab = tab; i += tabTextWidth + 1; } + + if (_selectedTabHasFocus) + { + SelectedTab?.SetFocus (); + } } /// @@ -542,7 +543,21 @@ private void Tab_MouseClick (object sender, MouseEventArgs e) private void UnSetCurrentTabs () { - if (_tabLocations is { }) + if (_tabLocations is null) + { + // Ensures unset any visible tab prior to TabScrollOffset + for (int i = 0; i < TabScrollOffset; i++) + { + Tab tab = Tabs.ElementAt (i); + + if (tab.Visible) + { + tab.MouseClick -= Tab_MouseClick!; + tab.Visible = false; + } + } + } + else if (_tabLocations is { }) { foreach (TabToRender tabToRender in _tabLocations) { @@ -570,7 +585,6 @@ public TabRowView (TabView host) Id = "tabRowView"; CanFocus = true; - Height = 1; // BUGBUG: Views should avoid setting Height as doing so implies Frame.Size == GetContentSize (). Width = Dim.Fill (); _rightScrollIndicator = new View @@ -598,7 +612,8 @@ public TabRowView (TabView host) protected override bool OnMouseEvent (MouseEventArgs me) { - Tab? hit = me.View as Tab; + View? parent = me.View is Adornment adornment ? adornment.Parent : me.View; + Tab? hit = parent as Tab; if (me.IsSingleClicked) { @@ -609,6 +624,11 @@ protected override bool OnMouseEvent (MouseEventArgs me) { return true; } + + if (parent == _host.SelectedTab) + { + _host.SelectedTab?.SetFocus (); + } } if (!me.IsSingleDoubleOrTripleClicked) @@ -625,11 +645,11 @@ protected override bool OnMouseEvent (MouseEventArgs me) { var scrollIndicatorHit = 0; - if (me.View is { } && me.View.Id == "rightScrollIndicator") + if (me.View is { Id: "rightScrollIndicator" }) { scrollIndicatorHit = 1; } - else if (me.View is { } && me.View.Id == "leftScrollIndicator") + else if (me.View is { Id: "leftScrollIndicator" }) { scrollIndicatorHit = -1; } @@ -656,15 +676,20 @@ protected override bool OnMouseEvent (MouseEventArgs me) } /// - protected override bool OnClearingViewport () + protected override void OnHasFocusChanged (bool newHasFocus, View? previousFocusedView, View? focusedView) { - // clear any old text - ClearViewport (); + if (_host.SelectedTab is { HasFocus: false, CanFocus: true } && focusedView == this) + { + _host.SelectedTab?.SetFocus (); - return true; + return; + } + + base.OnHasFocusChanged (newHasFocus, previousFocusedView, focusedView); } - protected override bool OnDrawingContent () + /// + protected override void OnSubviewLayout (LayoutEventArgs args) { _host._tabLocations = _host.CalculateViewport (Viewport).ToArray (); @@ -672,20 +697,18 @@ protected override bool OnDrawingContent () RenderUnderline (); - SetAttribute (HasFocus ? GetFocusColor () : GetNormalColor ()); - - return true; + base.OnSubviewLayout (args); } /// - protected override bool OnDrawingSubviews () + protected override bool OnRenderingLineCanvas () { - // RenderTabLine (); + RenderTabLineCanvas (); return false; } - protected override void OnDrawComplete () + private void RenderTabLineCanvas () { if (_host._tabLocations is null) { @@ -694,12 +717,12 @@ protected override void OnDrawComplete () TabToRender [] tabLocations = _host._tabLocations; int selectedTab = -1; + var lc = new LineCanvas (); for (var i = 0; i < tabLocations.Length; i++) { View tab = tabLocations [i].Tab; Rectangle vts = tab.ViewportToScreen (tab.Viewport); - var lc = new LineCanvas (); int selectedOffset = _host.Style.ShowTopLine && tabLocations [i].IsSelected ? 0 : 1; if (tabLocations [i].IsSelected) @@ -1048,7 +1071,7 @@ protected override void OnDrawComplete () } } - if (i == 0 && i != selectedTab && _host.TabScrollOffset == 0 && _host.Style.ShowBorder) + if (i == 0 && i != selectedTab && _host is { TabScrollOffset: 0, Style.ShowBorder: true }) { if (_host.Style.TabsOnBottom) { @@ -1163,6 +1186,7 @@ protected override void OnDrawComplete () } else { + // Right corner if (_host.Style.TabsOnBottom) { lc.AddLine ( @@ -1213,12 +1237,9 @@ protected override void OnDrawComplete () } } } - - tab.LineCanvas.Merge (lc); - tab.RenderLineCanvas (); - - // RenderUnderline (); } + + _host.LineCanvas.Merge (lc); } private int GetUnderlineYPosition () @@ -1234,9 +1255,7 @@ private int GetUnderlineYPosition () /// Renders the line with the tab names in it. private void RenderTabLine () { - TabToRender []? tabLocations = _host._tabLocations; - - if (tabLocations is null) + if (_host._tabLocations is null) { return; } @@ -1244,7 +1263,7 @@ private void RenderTabLine () View? selected = null; int topLine = _host.Style.ShowTopLine ? 1 : 0; - foreach (TabToRender toRender in tabLocations) + foreach (TabToRender toRender in _host._tabLocations) { Tab tab = toRender.Tab; @@ -1254,80 +1273,45 @@ private void RenderTabLine () if (_host.Style.TabsOnBottom) { - tab.Border.Thickness = new Thickness (1, 0, 1, topLine); - tab.Margin.Thickness = new Thickness (0, 1, 0, 0); + tab.Border!.Thickness = new (1, 0, 1, topLine); + tab.Margin!.Thickness = new (0, 1, 0, 0); } else { - tab.Border.Thickness = new Thickness (1, topLine, 1, 0); - tab.Margin.Thickness = new Thickness (0, 0, 0, topLine); + tab.Border!.Thickness = new (1, topLine, 1, 0); + tab.Margin!.Thickness = new (0, 0, 0, topLine); } } else if (selected is null) { if (_host.Style.TabsOnBottom) { - tab.Border.Thickness = new Thickness (1, 1, 0, topLine); - tab.Margin.Thickness = new Thickness (0, 0, 0, 0); + tab.Border!.Thickness = new (1, 1, 1, topLine); + tab.Margin!.Thickness = new (0, 0, 0, 0); } else { - tab.Border.Thickness = new Thickness (1, topLine, 0, 1); - tab.Margin.Thickness = new Thickness (0, 0, 0, 0); + tab.Border!.Thickness = new (1, topLine, 1, 1); + tab.Margin!.Thickness = new (0, 0, 0, 0); } - - tab.Width = Math.Max (tab.Width!.GetAnchor (0) - 1, 1); } else { if (_host.Style.TabsOnBottom) { - tab.Border.Thickness = new Thickness (0, 1, 1, topLine); - tab.Margin.Thickness = new Thickness (0, 0, 0, 0); + tab.Border!.Thickness = new (1, 1, 1, topLine); + tab.Margin!.Thickness = new (0, 0, 0, 0); } else { - tab.Border.Thickness = new Thickness (0, topLine, 1, 1); - tab.Margin.Thickness = new Thickness (0, 0, 0, 0); + tab.Border!.Thickness = new (1, topLine, 1, 1); + tab.Margin!.Thickness = new (0, 0, 0, 0); } - - tab.Width = Math.Max (tab.Width!.GetAnchor (0) - 1, 1); } - tab.Text = toRender.TextToRender; - - // BUGBUG: Layout should only be called from Mainloop iteration! - Layout (); - - tab.DrawBorderAndPadding (); - - Attribute prevAttr = Driver?.GetAttribute () ?? Attribute.Default; - - // if tab is the selected one and focus is inside this control - if (toRender.IsSelected && _host.HasFocus) - { - if (_host.Focused == this) - { - // if focus is the tab bar itself then show that they can switch tabs - prevAttr = ColorScheme.HotFocus; - } - else - { - // Focus is inside the tab - prevAttr = ColorScheme.HotNormal; - } - } - - tab.TextFormatter.Draw ( - tab.ViewportToScreen (tab.Viewport), - prevAttr, - ColorScheme.HotNormal - ); - - tab.DrawBorderAndPadding (); - - - SetAttribute (GetNormalColor ()); + // Ensures updating TextFormatter constrains + tab.TextFormatter.ConstrainToWidth = tab.GetContentSize ().Width; + tab.TextFormatter.ConstrainToHeight = tab.GetContentSize ().Height; } } @@ -1356,7 +1340,6 @@ private void RenderUnderline () // Ensures this is clicked instead of the first tab MoveSubviewToEnd (_leftScrollIndicator); - _leftScrollIndicator.Draw (); } else { @@ -1374,7 +1357,6 @@ private void RenderUnderline () // Ensures this is clicked instead of the last tab if under this MoveSubviewToStart (_rightScrollIndicator); - _rightScrollIndicator.Draw (); } else { diff --git a/UnitTests/View/Layout/SetLayoutTests.cs b/UnitTests/View/Layout/SetLayoutTests.cs index 4859957d62..ce0addd542 100644 --- a/UnitTests/View/Layout/SetLayoutTests.cs +++ b/UnitTests/View/Layout/SetLayoutTests.cs @@ -813,5 +813,105 @@ public void Does_Not_Throw_If_Nested_SubViews_Ref_Topmost_SuperView () t.Dispose (); } + [Fact] + [SetupFakeDriver] + public void Pos_Right_With_Adornments () + { + View view1 = new () { Text = "View1", Width = 7, Height = 3, BorderStyle = LineStyle.Rounded }; + View view2 = new () { Text = "View2", X = Pos.Right (view1) - 1, Width = 7, Height = 3, BorderStyle = LineStyle.Rounded }; + View view3 = new () { Text = "View3", X = Pos.Right (view2) - 1, Width = 7, Height = 3, BorderStyle = LineStyle.Rounded }; + View container = new () { Width = Dim.Fill (), Height = 3 }; + container.Add (view1, view2, view3); + View view4 = new () { Text = "View4", Y = Pos.Bottom (container), Width = 21, Height = 3, BorderStyle = LineStyle.Rounded }; + View superView = new () { Width = Dim.Fill (), Height = Dim.Fill () }; + superView.Add (container, view4); + + superView.Layout (); + superView.Draw (); + + TestHelpers.AssertDriverContentsAre ( + @" +╭─────╭─────╭─────╮ +│View1│View2│View3│ +╰─────╰─────╰─────╯ +╭───────────────────╮ +│View4 │ +╰───────────────────╯ +", + output + ); + + // Remove border bottom from the view1 + view1.Border!.Thickness = new (1, 1, 1, 0); + // Insert margin bottom into the view1 + view1.Margin!.Thickness = new (0, 0, 0, 1); + + View.SetClipToScreen (); + superView.Draw (); + + TestHelpers.AssertDriverContentsAre ( + @" +╭─────╭─────╭─────╮ +│View1│View2│View3│ + ╰─────╰─────╯ +╭───────────────────╮ +│View4 │ +╰───────────────────╯ +", + output + ); + + // Restore view1 border + view1.Border.Thickness = new (1); + // Restore view1 margin + view1.Margin.Thickness = Thickness.Empty; + + // Remove border bottom from the view2 + view2.Border!.Thickness = new (1, 1, 1, 0); + // Insert margin bottom into the view2 + view2.Margin!.Thickness = new (0, 0, 0, 1); + + View.SetClipToScreen (); + superView.Draw (); + + TestHelpers.AssertDriverContentsAre ( + @" +╭─────╭─────╭─────╮ +│View1│View2│View3│ +╰─────╯ ╰─────╯ +╭───────────────────╮ +│View4 │ +╰───────────────────╯ +", + output + ); + + // Restore view2 border + view2.Border.Thickness = new (1); + // Restore view2 margin + view2.Margin.Thickness = Thickness.Empty; + + // Remove border bottom from the view3 + view3.Border!.Thickness = new (1, 1, 1, 0); + // Insert margin bottom into the view3 + view3.Margin!.Thickness = new (0, 0, 0, 1); + + View.SetClipToScreen (); + superView.Draw (); + + TestHelpers.AssertDriverContentsAre ( + @" +╭─────╭─────╭─────╮ +│View1│View2│View3│ +╰─────╰─────╯ +╭───────────────────╮ +│View4 │ +╰───────────────────╯ +", + output + ); + + superView.Dispose (); + } } diff --git a/UnitTests/Views/TabViewTests.cs b/UnitTests/Views/TabViewTests.cs index 3999142fae..1ab880b7f9 100644 --- a/UnitTests/Views/TabViewTests.cs +++ b/UnitTests/Views/TabViewTests.cs @@ -3,7 +3,6 @@ namespace Terminal.Gui.ViewsTests; -#if foo public class TabViewTests (ITestOutputHelper output) { [Fact] @@ -113,8 +112,6 @@ public void MouseClick_ChangesTab () tv.Width = 20; tv.Height = 5; - tv.Layout (); - tv.Draw (); View tabRow = tv.Subviews [0]; @@ -146,21 +143,21 @@ public void MouseClick_ChangesTab () { args = new () { ScreenPosition = new (i, 1), Flags = MouseFlags.ReportMousePosition }; Application.RaiseMouseEvent (args); - Application.LayoutAndDrawToplevels (); + Application.LayoutAndDraw (); Assert.Null (clicked); Assert.Equal (tab1, tv.SelectedTab); } args = new () { ScreenPosition = new (3, 1), Flags = MouseFlags.Button1Clicked }; Application.RaiseMouseEvent (args); - Application.LayoutAndDrawToplevels (); + Application.LayoutAndDraw (); Assert.Equal (tab1, clicked); Assert.Equal (tab1, tv.SelectedTab); // Click to tab2 args = new () { ScreenPosition = new (6, 1), Flags = MouseFlags.Button1Clicked }; Application.RaiseMouseEvent (args); - Application.LayoutAndDrawToplevels (); + Application.LayoutAndDraw (); Assert.Equal (tab2, clicked); Assert.Equal (tab2, tv.SelectedTab); @@ -173,7 +170,7 @@ public void MouseClick_ChangesTab () args = new () { ScreenPosition = new (3, 1), Flags = MouseFlags.Button1Clicked }; Application.RaiseMouseEvent (args); - Application.LayoutAndDrawToplevels (); + Application.LayoutAndDraw (); // Tab 1 was clicked but event handler blocked navigation Assert.Equal (tab1, clicked); @@ -181,7 +178,7 @@ public void MouseClick_ChangesTab () args = new () { ScreenPosition = new (12, 1), Flags = MouseFlags.Button1Clicked }; Application.RaiseMouseEvent (args); - Application.LayoutAndDrawToplevels (); + Application.LayoutAndDraw (); // Clicking beyond last tab should raise event with null Tab Assert.Null (clicked); @@ -198,8 +195,6 @@ public void MouseClick_Right_Left_Arrows_ChangesTab () tv.Width = 7; tv.Height = 5; - tv.LayoutSubviews (); - tv.Draw (); View tabRow = tv.Subviews [0]; @@ -236,7 +231,7 @@ public void MouseClick_Right_Left_Arrows_ChangesTab () // Click the right arrow var args = new MouseEventArgs { ScreenPosition = new (6, 2), Flags = MouseFlags.Button1Clicked }; Application.RaiseMouseEvent (args); - Application.LayoutAndDrawToplevels (); + Application.LayoutAndDraw (); Assert.Null (clicked); Assert.Equal (tab1, oldChanged); Assert.Equal (tab2, newChanged); @@ -256,7 +251,7 @@ public void MouseClick_Right_Left_Arrows_ChangesTab () // Click the left arrow args = new () { ScreenPosition = new (0, 2), Flags = MouseFlags.Button1Clicked }; Application.RaiseMouseEvent (args); - Application.LayoutAndDrawToplevels (); + Application.LayoutAndDraw (); Assert.Null (clicked); Assert.Equal (tab2, oldChanged); Assert.Equal (tab1, newChanged); @@ -286,8 +281,7 @@ public void MouseClick_Right_Left_Arrows_ChangesTab_With_Border () Assert.Equal (LineStyle.None, tv.BorderStyle); tv.BorderStyle = LineStyle.Single; - - tv.LayoutSubviews (); + tv.Layout (); tv.Draw (); @@ -327,7 +321,7 @@ public void MouseClick_Right_Left_Arrows_ChangesTab_With_Border () // Click the right arrow var args = new MouseEventArgs { ScreenPosition = new (7, 3), Flags = MouseFlags.Button1Clicked }; Application.RaiseMouseEvent (args); - Application.LayoutAndDrawToplevels (); + Application.LayoutAndDraw (); Assert.Null (clicked); Assert.Equal (tab1, oldChanged); Assert.Equal (tab2, newChanged); @@ -349,7 +343,7 @@ public void MouseClick_Right_Left_Arrows_ChangesTab_With_Border () // Click the left arrow args = new () { ScreenPosition = new (1, 3), Flags = MouseFlags.Button1Clicked }; Application.RaiseMouseEvent (args); - Application.LayoutAndDrawToplevels (); + Application.LayoutAndDraw (); Assert.Null (clicked); Assert.Equal (tab2, oldChanged); Assert.Equal (tab1, newChanged); @@ -400,7 +394,7 @@ public void ProcessKey_Down_Up_Right_Left_Home_End_PageDown_PageUp () // Press the cursor up key to focus the selected tab Application.RaiseKeyDownEvent (Key.CursorUp); - Application.LayoutAndDrawToplevels (); + Application.LayoutAndDraw (); // Is the selected tab focused Assert.Equal (tab1, tv.SelectedTab); @@ -418,7 +412,7 @@ public void ProcessKey_Down_Up_Right_Left_Home_End_PageDown_PageUp () // Press the cursor right key to select the next tab Application.RaiseKeyDownEvent (Key.CursorRight); - Application.LayoutAndDrawToplevels (); + Application.LayoutAndDraw (); Assert.Equal (tab1, oldChanged); Assert.Equal (tab2, newChanged); Assert.Equal (tab2, tv.SelectedTab); @@ -476,7 +470,7 @@ public void ProcessKey_Down_Up_Right_Left_Home_End_PageDown_PageUp () // Press the cursor left key to select the previous tab Application.RaiseKeyDownEvent (Key.CursorLeft); - Application.LayoutAndDrawToplevels (); + Application.LayoutAndDraw (); Assert.Equal (tab2, oldChanged); Assert.Equal (tab1, newChanged); Assert.Equal (tab1, tv.SelectedTab); @@ -486,7 +480,7 @@ public void ProcessKey_Down_Up_Right_Left_Home_End_PageDown_PageUp () // Press the end key to select the last tab Application.RaiseKeyDownEvent (Key.End); - Application.LayoutAndDrawToplevels (); + Application.LayoutAndDraw (); Assert.Equal (tab1, oldChanged); Assert.Equal (tab2, newChanged); Assert.Equal (tab2, tv.SelectedTab); @@ -495,7 +489,7 @@ public void ProcessKey_Down_Up_Right_Left_Home_End_PageDown_PageUp () // Press the home key to select the first tab Application.RaiseKeyDownEvent (Key.Home); - Application.LayoutAndDrawToplevels (); + Application.LayoutAndDraw (); Assert.Equal (tab2, oldChanged); Assert.Equal (tab1, newChanged); Assert.Equal (tab1, tv.SelectedTab); @@ -504,7 +498,7 @@ public void ProcessKey_Down_Up_Right_Left_Home_End_PageDown_PageUp () // Press the page down key to select the next set of tabs Application.RaiseKeyDownEvent (Key.PageDown); - Application.LayoutAndDrawToplevels (); + Application.LayoutAndDraw (); Assert.Equal (tab1, oldChanged); Assert.Equal (tab2, newChanged); Assert.Equal (tab2, tv.SelectedTab); @@ -513,7 +507,7 @@ public void ProcessKey_Down_Up_Right_Left_Home_End_PageDown_PageUp () // Press the page up key to select the previous set of tabs Application.RaiseKeyDownEvent (Key.PageUp); - Application.LayoutAndDrawToplevels (); + Application.LayoutAndDraw (); Assert.Equal (tab2, oldChanged); Assert.Equal (tab1, newChanged); Assert.Equal (tab1, tv.SelectedTab); @@ -610,7 +604,6 @@ public void ShowTopLine_False_TabsOnBottom_False_TestTabView_Width3 () tv.ApplyStyleChanges (); tv.Layout (); - View.ClipToScreen (); tv.Draw (); TestHelpers.AssertDriverContentsWithFrameAre ( @@ -633,7 +626,7 @@ public void ShowTopLine_False_TabsOnBottom_False_TestTabView_Width4 () tv.Height = 5; tv.Style = new () { ShowTopLine = false }; tv.ApplyStyleChanges (); - tv.LayoutSubviews (); + tv.Layout (); tv.Draw (); @@ -658,13 +651,13 @@ public void ShowTopLine_False_TabsOnBottom_False_TestThinTabView_WithLongNames ( tv.Style = new () { ShowTopLine = false }; tv.ApplyStyleChanges (); - // Ensures that the tab bar subview gets the bounds of the parent TabView - tv.LayoutSubviews (); - - // Test two tab names that fit + // Test two tab names that fit tab1.DisplayText = "12"; tab2.DisplayText = "13"; + // Ensures that the tab bar subview gets the bounds of the parent TabView + tv.Layout (); + tv.Draw (); TestHelpers.AssertDriverContentsWithFrameAre ( @@ -678,8 +671,10 @@ public void ShowTopLine_False_TabsOnBottom_False_TestThinTabView_WithLongNames ( ); tv.SelectedTab = tab2; + Assert.Equal (tab2, tv.Subviews.First (v => v.Id.Contains ("tabRowView")).MostFocused); - View.ClipToScreen (); + tv.Layout (); + View.SetClipToScreen (); tv.Draw (); TestHelpers.AssertDriverContentsWithFrameAre ( @@ -697,8 +692,8 @@ public void ShowTopLine_False_TabsOnBottom_False_TestThinTabView_WithLongNames ( // Test first tab name too long tab1.DisplayText = "12345678910"; tab2.DisplayText = "13"; - - View.ClipToScreen (); + tv.Layout (); + View.SetClipToScreen (); tv.Draw (); TestHelpers.AssertDriverContentsWithFrameAre ( @@ -713,9 +708,10 @@ public void ShowTopLine_False_TabsOnBottom_False_TestThinTabView_WithLongNames ( //switch to tab2 tv.SelectedTab = tab2; - View.ClipToScreen (); - tv.Draw (); + tv.Layout (); + View.SetClipToScreen (); + tv.Draw (); TestHelpers.AssertDriverContentsWithFrameAre ( @" │13│ @@ -730,9 +726,9 @@ public void ShowTopLine_False_TabsOnBottom_False_TestThinTabView_WithLongNames ( tab1.DisplayText = "12345678910"; tab2.DisplayText = "abcdefghijklmnopq"; - View.ClipToScreen (); + tv.Layout (); + View.SetClipToScreen (); tv.Draw (); - TestHelpers.AssertDriverContentsWithFrameAre ( @" │abcdefg│ @@ -753,9 +749,8 @@ public void ShowTopLine_False_TabsOnBottom_True_TestTabView_Width3 () tv.Height = 5; tv.Style = new () { ShowTopLine = false, TabsOnBottom = true }; tv.ApplyStyleChanges (); - tv.LayoutSubviews (); + tv.Layout (); - View.ClipToScreen (); tv.Draw (); TestHelpers.AssertDriverContentsWithFrameAre ( @@ -778,7 +773,7 @@ public void ShowTopLine_False_TabsOnBottom_True_TestTabView_Width4 () tv.Height = 5; tv.Style = new () { ShowTopLine = false, TabsOnBottom = true }; tv.ApplyStyleChanges (); - tv.LayoutSubviews (); + tv.Layout (); tv.Draw (); @@ -802,15 +797,13 @@ public void ShowTopLine_False_TabsOnBottom_True_TestThinTabView_WithLongNames () tv.Height = 5; tv.Style = new () { ShowTopLine = false, TabsOnBottom = true }; tv.ApplyStyleChanges (); + tv.Layout (); - // Ensures that the tab bar subview gets the bounds of the parent TabView - tv.LayoutSubviews (); - - // Test two tab names that fit + // Test two tab names that fit tab1.DisplayText = "12"; tab2.DisplayText = "13"; - View.ClipToScreen (); + tv.Layout (); tv.Draw (); TestHelpers.AssertDriverContentsWithFrameAre ( @@ -824,8 +817,10 @@ public void ShowTopLine_False_TabsOnBottom_True_TestThinTabView_WithLongNames () ); tv.SelectedTab = tab2; + Assert.Equal (tab2, tv.Subviews.First (v => v.Id.Contains ("tabRowView")).MostFocused); - View.ClipToScreen (); + tv.Layout (); + View.SetClipToScreen (); tv.Draw (); TestHelpers.AssertDriverContentsWithFrameAre ( @@ -844,7 +839,8 @@ public void ShowTopLine_False_TabsOnBottom_True_TestThinTabView_WithLongNames () tab1.DisplayText = "12345678910"; tab2.DisplayText = "13"; - View.ClipToScreen (); + tv.Layout (); + View.SetClipToScreen (); tv.Draw (); TestHelpers.AssertDriverContentsWithFrameAre ( @@ -859,7 +855,9 @@ public void ShowTopLine_False_TabsOnBottom_True_TestThinTabView_WithLongNames () //switch to tab2 tv.SelectedTab = tab2; - View.ClipToScreen (); + + tv.Layout (); + View.SetClipToScreen (); tv.Draw (); TestHelpers.AssertDriverContentsWithFrameAre ( @@ -876,7 +874,8 @@ public void ShowTopLine_False_TabsOnBottom_True_TestThinTabView_WithLongNames () tab1.DisplayText = "12345678910"; tab2.DisplayText = "abcdefghijklmnopq"; - View.ClipToScreen (); + tv.Layout (); + View.SetClipToScreen (); tv.Draw (); TestHelpers.AssertDriverContentsWithFrameAre ( @@ -899,7 +898,6 @@ public void ShowTopLine_True_TabsOnBottom_False_TestTabView_Width3 () tv.Height = 5; tv.Layout (); - View.ClipToScreen (); tv.Draw (); TestHelpers.AssertDriverContentsWithFrameAre ( @@ -922,7 +920,7 @@ public void ShowTopLine_True_TabsOnBottom_False_TestTabView_Width4 () tv.Height = 5; tv.Layout (); - View.ClipToScreen (); + View.SetClipToScreen (); tv.Draw (); TestHelpers.AssertDriverContentsWithFrameAre ( @@ -944,14 +942,11 @@ public void ShowTopLine_True_TabsOnBottom_False_TestThinTabView_WithLongNames () tv.Width = 10; tv.Height = 5; - // Ensures that the tab bar subview gets the bounds of the parent TabView - tv.LayoutSubviews (); - - // Test two tab names that fit + // Test two tab names that fit tab1.DisplayText = "12"; tab2.DisplayText = "13"; - View.ClipToScreen (); + tv.Layout (); tv.Draw (); TestHelpers.AssertDriverContentsWithFrameAre ( @@ -966,7 +961,8 @@ public void ShowTopLine_True_TabsOnBottom_False_TestThinTabView_WithLongNames () tv.SelectedTab = tab2; - View.ClipToScreen (); + tv.Layout (); + View.SetClipToScreen (); tv.Draw (); TestHelpers.AssertDriverContentsWithFrameAre ( @@ -985,7 +981,8 @@ public void ShowTopLine_True_TabsOnBottom_False_TestThinTabView_WithLongNames () tab1.DisplayText = "12345678910"; tab2.DisplayText = "13"; - View.ClipToScreen (); + tv.Layout (); + View.SetClipToScreen (); tv.Draw (); TestHelpers.AssertDriverContentsWithFrameAre ( @@ -1000,7 +997,9 @@ public void ShowTopLine_True_TabsOnBottom_False_TestThinTabView_WithLongNames () //switch to tab2 tv.SelectedTab = tab2; - View.ClipToScreen (); + + tv.Layout (); + View.SetClipToScreen (); tv.Draw (); TestHelpers.AssertDriverContentsWithFrameAre ( @@ -1017,7 +1016,8 @@ public void ShowTopLine_True_TabsOnBottom_False_TestThinTabView_WithLongNames () tab1.DisplayText = "12345678910"; tab2.DisplayText = "abcdefghijklmnopq"; - View.ClipToScreen (); + tv.Layout (); + View.SetClipToScreen (); tv.Draw (); TestHelpers.AssertDriverContentsWithFrameAre ( @@ -1039,13 +1039,11 @@ public void ShowTopLine_True_TabsOnBottom_False_With_Unicode () tv.Width = 20; tv.Height = 5; - tv.LayoutSubviews (); - tab1.DisplayText = "Tab0"; tab2.DisplayText = "Les Mise" + char.ConvertFromUtf32 (int.Parse ("0301", NumberStyles.HexNumber)) + "rables"; - View.ClipToScreen (); + tv.Layout (); tv.Draw (); TestHelpers.AssertDriverContentsWithFrameAre ( @@ -1060,7 +1058,8 @@ public void ShowTopLine_True_TabsOnBottom_False_With_Unicode () tv.SelectedTab = tab2; - View.ClipToScreen (); + tv.Layout (); + View.SetClipToScreen (); tv.Draw (); TestHelpers.AssertDriverContentsWithFrameAre ( @@ -1083,7 +1082,7 @@ public void ShowTopLine_True_TabsOnBottom_True_TestTabView_Width3 () tv.Height = 5; tv.Style = new () { TabsOnBottom = true }; tv.ApplyStyleChanges (); - tv.LayoutSubviews (); + tv.Layout (); tv.Draw (); @@ -1107,7 +1106,7 @@ public void ShowTopLine_True_TabsOnBottom_True_TestTabView_Width4 () tv.Height = 5; tv.Style = new () { TabsOnBottom = true }; tv.ApplyStyleChanges (); - tv.LayoutSubviews (); + tv.Layout (); tv.Draw (); @@ -1131,15 +1130,13 @@ public void ShowTopLine_True_TabsOnBottom_True_TestThinTabView_WithLongNames () tv.Height = 5; tv.Style = new () { TabsOnBottom = true }; tv.ApplyStyleChanges (); + tv.Layout (); - // Ensures that the tab bar subview gets the bounds of the parent TabView - tv.LayoutSubviews (); - - // Test two tab names that fit + // Test two tab names that fit tab1.DisplayText = "12"; tab2.DisplayText = "13"; - View.ClipToScreen (); + tv.Layout (); tv.Draw (); TestHelpers.AssertDriverContentsWithFrameAre ( @@ -1156,7 +1153,8 @@ public void ShowTopLine_True_TabsOnBottom_True_TestThinTabView_WithLongNames () tab1.DisplayText = "12345678910"; tab2.DisplayText = "13"; - View.ClipToScreen (); + tv.Layout (); + View.SetClipToScreen (); tv.Draw (); TestHelpers.AssertDriverContentsWithFrameAre ( @@ -1171,7 +1169,9 @@ public void ShowTopLine_True_TabsOnBottom_True_TestThinTabView_WithLongNames () //switch to tab2 tv.SelectedTab = tab2; - View.ClipToScreen (); + + tv.Layout (); + View.SetClipToScreen (); tv.Draw (); TestHelpers.AssertDriverContentsWithFrameAre ( @@ -1188,7 +1188,8 @@ public void ShowTopLine_True_TabsOnBottom_True_TestThinTabView_WithLongNames () tab1.DisplayText = "12345678910"; tab2.DisplayText = "abcdefghijklmnopq"; - View.ClipToScreen (); + tv.Layout (); + View.SetClipToScreen (); tv.Draw (); TestHelpers.AssertDriverContentsWithFrameAre ( @@ -1212,12 +1213,11 @@ public void ShowTopLine_True_TabsOnBottom_True_With_Unicode () tv.Style = new () { TabsOnBottom = true }; tv.ApplyStyleChanges (); - tv.LayoutSubviews (); - tab1.DisplayText = "Tab0"; tab2.DisplayText = "Les Mise" + char.ConvertFromUtf32 (int.Parse ("0301", NumberStyles.HexNumber)) + "rables"; + tv.Layout (); tv.Draw (); TestHelpers.AssertDriverContentsWithFrameAre ( @@ -1232,7 +1232,8 @@ public void ShowTopLine_True_TabsOnBottom_True_With_Unicode () tv.SelectedTab = tab2; - View.ClipToScreen (); + tv.Layout (); + View.SetClipToScreen (); tv.Draw (); TestHelpers.AssertDriverContentsWithFrameAre ( @@ -1322,6 +1323,138 @@ public void RemoveTab_ThatHasFocus () Application.Shutdown (); } + [Fact] + [SetupFakeDriver] + public void Add_Three_TabsOnTop_ChangesTab () + { + TabView tv = GetTabView (out Tab tab1, out Tab tab2, false); + Tab tab3; + + tv.AddTab ( + tab3 = new () { Id = "tab3", DisplayText = "Tab3", View = new TextView { Id = "tab3.TextView", Width = 3, Height = 1, Text = "hi3" } }, + false); + + tv.Width = 20; + tv.Height = 5; + + tv.Layout (); + tv.Draw (); + + Assert.Equal (tab1, tv.SelectedTab); + + TestHelpers.AssertDriverContentsAre ( + @" +╭────┬────┬────╮ +│Tab1│Tab2│Tab3│ +│ ╰────┴────┴───╮ +│hi │ +└──────────────────┘ +", + output + ); + + tv.SelectedTab = tab2; + + tv.Layout (); + View.SetClipToScreen (); + tv.Draw (); + + TestHelpers.AssertDriverContentsWithFrameAre ( + @" +╭────┬────┬────╮ +│Tab1│Tab2│Tab3│ +├────╯ ╰────┴───╮ +│hi2 │ +└──────────────────┘ +", + output + ); + + tv.SelectedTab = tab3; + + tv.Layout (); + View.SetClipToScreen (); + tv.Draw (); + + TestHelpers.AssertDriverContentsWithFrameAre ( + @" +╭────┬────┬────╮ +│Tab1│Tab2│Tab3│ +├────┴────╯ ╰───╮ +│hi3 │ +└──────────────────┘ +", + output + ); + } + + [Fact] + [SetupFakeDriver] + public void Add_Three_TabsOnBottom_ChangesTab () + { + TabView tv = GetTabView (out Tab tab1, out Tab tab2, false); + Tab tab3; + + tv.AddTab ( + tab3 = new () { Id = "tab3", DisplayText = "Tab3", View = new TextView { Id = "tab3.TextView", Width = 3, Height = 1, Text = "hi3" } }, + false); + + tv.Width = 20; + tv.Height = 5; + tv.Style = new () { TabsOnBottom = true }; + tv.ApplyStyleChanges (); + + tv.Layout (); + tv.Draw (); + + Assert.Equal (tab1, tv.SelectedTab); + + TestHelpers.AssertDriverContentsAre ( + @" +┌──────────────────┐ +│hi │ +│ ╭────┬────┬───╯ +│Tab1│Tab2│Tab3│ +╰────┴────┴────╯ +", + output + ); + + tv.SelectedTab = tab2; + + tv.Layout (); + View.SetClipToScreen (); + tv.Draw (); + + TestHelpers.AssertDriverContentsWithFrameAre ( + @" +┌──────────────────┐ +│hi2 │ +├────╮ ╭────┬───╯ +│Tab1│Tab2│Tab3│ +╰────┴────┴────╯ +", + output + ); + + tv.SelectedTab = tab3; + + tv.Layout (); + View.SetClipToScreen (); + tv.Draw (); + + TestHelpers.AssertDriverContentsWithFrameAre ( + @" +┌──────────────────┐ +│hi3 │ +├────┬────╮ ╭───╯ +│Tab1│Tab2│Tab3│ +╰────┴────┴────╯ +", + output + ); + } + private TabView GetTabView () { return GetTabView (out _, out _); } private TabView GetTabView (out Tab tab1, out Tab tab2, bool initFakeDriver = true) @@ -1355,4 +1488,3 @@ private void InitFakeDriver () driver.Init (); } } -#endif From 06c90f0bd5b3eb9280e94d793fbf26f33a650cc4 Mon Sep 17 00:00:00 2001 From: BDisp Date: Mon, 18 Nov 2024 23:50:44 +0000 Subject: [PATCH 02/13] Fix unit test. --- Terminal.Gui/Views/TabView.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Terminal.Gui/Views/TabView.cs b/Terminal.Gui/Views/TabView.cs index 9d373f6f2e..985b6283cf 100644 --- a/Terminal.Gui/Views/TabView.cs +++ b/Terminal.Gui/Views/TabView.cs @@ -112,7 +112,7 @@ public Tab? SelectedTab set { Tab? old = _selectedTab; - _selectedTabHasFocus = old?.HasFocus == true || !_contentView.CanFocus; + _selectedTabHasFocus = old is { } && (old.HasFocus == true || !_contentView.CanFocus); if (_selectedTab is { }) { @@ -140,7 +140,7 @@ public Tab? SelectedTab if (old != _selectedTab) { - if (_selectedTabHasFocus) + if (_selectedTabHasFocus || !_contentView.CanFocus) { SelectedTab?.SetFocus (); } From 1018eaae39f6b7fc130a56d88fa2340416a485ae Mon Sep 17 00:00:00 2001 From: BDisp Date: Tue, 19 Nov 2024 00:07:41 +0000 Subject: [PATCH 03/13] Fix unit test that was causing With_Subview_Using_PosFunc ti fail. --- UnitTests/View/Adornment/PaddingTests.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/UnitTests/View/Adornment/PaddingTests.cs b/UnitTests/View/Adornment/PaddingTests.cs index 0242bcc1da..a4defc9972 100644 --- a/UnitTests/View/Adornment/PaddingTests.cs +++ b/UnitTests/View/Adornment/PaddingTests.cs @@ -33,5 +33,7 @@ P P output ); TestHelpers.AssertDriverAttributesAre ("0", output, null, view.GetNormalColor ()); + + ((FakeDriver)Application.Driver!).End (); } } From 6fdaaeabbe55130099cc400b9cde64c89015e7ee Mon Sep 17 00:00:00 2001 From: BDisp Date: Tue, 19 Nov 2024 12:11:02 +0000 Subject: [PATCH 04/13] Subscribe and unsubscribe more tab border events. --- Terminal.Gui/Views/TabView.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Terminal.Gui/Views/TabView.cs b/Terminal.Gui/Views/TabView.cs index 985b6283cf..61d5b7fbcb 100644 --- a/Terminal.Gui/Views/TabView.cs +++ b/Terminal.Gui/Views/TabView.cs @@ -469,6 +469,7 @@ private IEnumerable CalculateViewport (Rectangle bounds) { tab.Visible = true; tab.MouseClick += Tab_MouseClick!; + tab.Border!.MouseClick += Tab_MouseClick!; yield return new (tab, string.Empty, Equals (SelectedTab, tab)); @@ -553,6 +554,7 @@ private void UnSetCurrentTabs () if (tab.Visible) { tab.MouseClick -= Tab_MouseClick!; + tab.Border!.MouseClick -= Tab_MouseClick!; tab.Visible = false; } } @@ -562,6 +564,7 @@ private void UnSetCurrentTabs () foreach (TabToRender tabToRender in _tabLocations) { tabToRender.Tab.MouseClick -= Tab_MouseClick!; + tabToRender.Tab.Border!.MouseClick -= Tab_MouseClick!; tabToRender.Tab.Visible = false; } From 261a0b9ff75b5a9f346163487b294151d9460053 Mon Sep 17 00:00:00 2001 From: BDisp Date: Tue, 19 Nov 2024 12:34:44 +0000 Subject: [PATCH 05/13] Replace with _containerView. --- Terminal.Gui/Views/TabView.cs | 48 ++++++++++++++++------------------- 1 file changed, 22 insertions(+), 26 deletions(-) diff --git a/Terminal.Gui/Views/TabView.cs b/Terminal.Gui/Views/TabView.cs index 61d5b7fbcb..25c1bf72e7 100644 --- a/Terminal.Gui/Views/TabView.cs +++ b/Terminal.Gui/Views/TabView.cs @@ -11,7 +11,7 @@ public class TabView : View /// This sub view is the main client area of the current tab. It hosts the of the tab, the /// . /// - private readonly View _contentView; + private readonly View _containerView; private readonly List _tabs = new (); @@ -28,14 +28,11 @@ public TabView () CanFocus = true; TabStop = TabBehavior.TabStop; // Because TabView has focusable subviews, it must be a TabGroup _tabsBar = new TabRowView (this); - _contentView = new View () - { - //Id = "TabView._contentView", - }; + _containerView = new (); ApplyStyleChanges (); base.Add (_tabsBar); - base.Add (_contentView); + base.Add (_containerView); // Things this view knows how to do AddCommand (Command.Left, () => SwitchTabBy (-1)); @@ -112,15 +109,15 @@ public Tab? SelectedTab set { Tab? old = _selectedTab; - _selectedTabHasFocus = old is { } && (old.HasFocus == true || !_contentView.CanFocus); + _selectedTabHasFocus = old is { } && (old.HasFocus == true || !_containerView.CanFocus); if (_selectedTab is { }) { if (_selectedTab.View is { }) { - _selectedTab.View.CanFocusChanged -= ContentViewCanFocus!; + _selectedTab.View.CanFocusChanged -= ContainerViewCanFocus!; // remove old content - _contentView.Remove (_selectedTab.View); + _containerView.Remove (_selectedTab.View); } } @@ -129,18 +126,17 @@ public Tab? SelectedTab // add new content if (_selectedTab?.View != null) { - _selectedTab.View.CanFocusChanged += ContentViewCanFocus!; - _contentView.Add (_selectedTab.View); - // _contentView.Id = $"_contentView for {_selectedTab.DisplayText}"; + _selectedTab.View.CanFocusChanged += ContainerViewCanFocus!; + _containerView.Add (_selectedTab.View); } - ContentViewCanFocus (null!, null!); + ContainerViewCanFocus (null!, null!); EnsureSelectedTabIsVisible (); if (old != _selectedTab) { - if (_selectedTabHasFocus || !_contentView.CanFocus) + if (_selectedTabHasFocus || !_containerView.CanFocus) { SelectedTab?.SetFocus (); } @@ -151,9 +147,9 @@ public Tab? SelectedTab } } - private void ContentViewCanFocus (object sender, EventArgs eventArgs) + private void ContainerViewCanFocus (object sender, EventArgs eventArgs) { - _contentView.CanFocus = _contentView.Subviews.Count (v => v.CanFocus) > 0; + _containerView.CanFocus = _containerView.Subviews.Count (v => v.CanFocus) > 0; } private TabStyle _style = new (); @@ -222,34 +218,34 @@ public void AddTab (Tab tab, bool andSelect) /// public void ApplyStyleChanges () { - _contentView.BorderStyle = Style.ShowBorder ? LineStyle.Single : LineStyle.None; - _contentView.Width = Dim.Fill (); + _containerView.BorderStyle = Style.ShowBorder ? LineStyle.Single : LineStyle.None; + _containerView.Width = Dim.Fill (); if (Style.TabsOnBottom) { // Tabs are along the bottom so just dodge the border if (Style.ShowBorder) { - _contentView.Border.Thickness = new Thickness (1, 1, 1, 0); + _containerView.Border!.Thickness = new Thickness (1, 1, 1, 0); } - _contentView.Y = 0; + _containerView.Y = 0; int tabHeight = GetTabHeight (false); // Fill client area leaving space at bottom for tabs - _contentView.Height = Dim.Fill (tabHeight); + _containerView.Height = Dim.Fill (tabHeight); _tabsBar.Height = tabHeight; - _tabsBar.Y = Pos.Bottom (_contentView); + _tabsBar.Y = Pos.Bottom (_containerView); } else { // Tabs are along the top if (Style.ShowBorder) { - _contentView.Border.Thickness = new Thickness (1, 0, 1, 1); + _containerView.Border!.Thickness = new Thickness (1, 0, 1, 1); } _tabsBar.Y = 0; @@ -257,10 +253,10 @@ public void ApplyStyleChanges () int tabHeight = GetTabHeight (true); //move content down to make space for tabs - _contentView.Y = Pos.Bottom (_tabsBar); + _containerView.Y = Pos.Bottom (_tabsBar); // Fill client area leaving space at bottom for border - _contentView.Height = Dim.Fill (); + _containerView.Height = Dim.Fill (); // The top tab should be 2 or 3 rows high and on the top @@ -305,7 +301,7 @@ public void EnsureSelectedTabIsVisible () /// protected override void OnHasFocusChanged (bool newHasFocus, View? previousFocusedView, View? focusedView) { - if (SelectedTab is { HasFocus: false } && !_contentView.CanFocus && focusedView == this) + if (SelectedTab is { HasFocus: false } && !_containerView.CanFocus && focusedView == this) { SelectedTab?.SetFocus (); From b617c2a5283905b8a3aedc23b68a7cd7e4c932aa Mon Sep 17 00:00:00 2001 From: BDisp Date: Tue, 19 Nov 2024 12:45:27 +0000 Subject: [PATCH 06/13] Remove unnecessary TextToRender property and code cleanup. --- Terminal.Gui/Views/TabView.cs | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/Terminal.Gui/Views/TabView.cs b/Terminal.Gui/Views/TabView.cs index 25c1bf72e7..1a4d7c6a5b 100644 --- a/Terminal.Gui/Views/TabView.cs +++ b/Terminal.Gui/Views/TabView.cs @@ -109,7 +109,7 @@ public Tab? SelectedTab set { Tab? old = _selectedTab; - _selectedTabHasFocus = old is { } && (old.HasFocus == true || !_containerView.CanFocus); + _selectedTabHasFocus = old is { } && (old.HasFocus || !_containerView.CanFocus); if (_selectedTab is { }) { @@ -450,8 +450,6 @@ private IEnumerable CalculateViewport (Rectangle bounds) // while there is space for the tab int tabTextWidth = tab.DisplayText.EnumerateRunes ().Sum (c => c.GetColumns ()); - string text = tab.DisplayText; - // The maximum number of characters to use for the tab name as specified // by the user (MaxTabTextWidth). But not more than the width of the view // or we won't even be able to render a single tab! @@ -467,19 +465,19 @@ private IEnumerable CalculateViewport (Rectangle bounds) tab.MouseClick += Tab_MouseClick!; tab.Border!.MouseClick += Tab_MouseClick!; - yield return new (tab, string.Empty, Equals (SelectedTab, tab)); + yield return new (tab, Equals (SelectedTab, tab)); break; } if (tabTextWidth > maxWidth) { - text = tab.Text = tab.DisplayText.Substring (0, (int)maxWidth); + tab.Text = tab.DisplayText.Substring (0, (int)maxWidth); tabTextWidth = (int)maxWidth; } else { - tab.Text = text; + tab.Text = tab.DisplayText; } tab.Width = Math.Max (tabTextWidth + 2, 1); @@ -498,7 +496,7 @@ private IEnumerable CalculateViewport (Rectangle bounds) tab.MouseClick += Tab_MouseClick!; tab.Border!.MouseClick += Tab_MouseClick!; - yield return new (tab, text, Equals (SelectedTab, tab)); + yield return new (tab, Equals (SelectedTab, tab)); prevTab = tab; @@ -616,7 +614,7 @@ protected override bool OnMouseEvent (MouseEventArgs me) if (me.IsSingleClicked) { - _host.OnTabClicked (new TabMouseEventArgs (hit, me)); + _host.OnTabClicked (new TabMouseEventArgs (hit!, me)); // user canceled click if (me.Handled) @@ -1368,11 +1366,10 @@ private void RenderUnderline () private class TabToRender { - public TabToRender (Tab tab, string textToRender, bool isSelected) + public TabToRender (Tab tab, bool isSelected) { Tab = tab; IsSelected = isSelected; - TextToRender = textToRender; } /// True if the tab that is being rendered is the selected one. @@ -1380,6 +1377,5 @@ public TabToRender (Tab tab, string textToRender, bool isSelected) public bool IsSelected { get; } public Tab Tab { get; } - public string TextToRender { get; } } } From 93259b050f18aefcef89cf7dbb646a1e353d2a50 Mon Sep 17 00:00:00 2001 From: Tig Date: Tue, 19 Nov 2024 11:42:53 -0700 Subject: [PATCH 07/13] Code review tweak --- Terminal.Gui/Views/TabView.cs | 1381 ----------------- Terminal.Gui/Views/{ => TabView}/Tab.cs | 0 .../{ => TabView}/TabChangedEventArgs.cs | 0 .../Views/{ => TabView}/TabMouseEventArgs.cs | 0 Terminal.Gui/Views/TabView/TabRowView.cs | 796 ++++++++++ Terminal.Gui/Views/{ => TabView}/TabStyle.cs | 0 Terminal.Gui/Views/TabView/TabToRender.cs | 17 + Terminal.Gui/Views/TabView/TabView.cs | 576 +++++++ UnitTests/View/Layout/SetLayoutTests.cs | 5 + 9 files changed, 1394 insertions(+), 1381 deletions(-) delete mode 100644 Terminal.Gui/Views/TabView.cs rename Terminal.Gui/Views/{ => TabView}/Tab.cs (100%) rename Terminal.Gui/Views/{ => TabView}/TabChangedEventArgs.cs (100%) rename Terminal.Gui/Views/{ => TabView}/TabMouseEventArgs.cs (100%) create mode 100644 Terminal.Gui/Views/TabView/TabRowView.cs rename Terminal.Gui/Views/{ => TabView}/TabStyle.cs (100%) create mode 100644 Terminal.Gui/Views/TabView/TabToRender.cs create mode 100644 Terminal.Gui/Views/TabView/TabView.cs diff --git a/Terminal.Gui/Views/TabView.cs b/Terminal.Gui/Views/TabView.cs deleted file mode 100644 index 1a4d7c6a5b..0000000000 --- a/Terminal.Gui/Views/TabView.cs +++ /dev/null @@ -1,1381 +0,0 @@ -#nullable enable -namespace Terminal.Gui; - -/// Control that hosts multiple sub views, presenting a single one at once. -public class TabView : View -{ - /// The default to set on new controls. - public const uint DefaultMaxTabTextWidth = 30; - - /// - /// This sub view is the main client area of the current tab. It hosts the of the tab, the - /// . - /// - private readonly View _containerView; - - private readonly List _tabs = new (); - - /// This sub view is the 2 or 3 line control that represents the actual tabs themselves. - private readonly TabRowView _tabsBar; - - private Tab? _selectedTab; - private TabToRender []? _tabLocations; - private int _tabScrollOffset; - - /// Initializes a class. - public TabView () - { - CanFocus = true; - TabStop = TabBehavior.TabStop; // Because TabView has focusable subviews, it must be a TabGroup - _tabsBar = new TabRowView (this); - _containerView = new (); - ApplyStyleChanges (); - - base.Add (_tabsBar); - base.Add (_containerView); - - // Things this view knows how to do - AddCommand (Command.Left, () => SwitchTabBy (-1)); - - AddCommand (Command.Right, () => SwitchTabBy (1)); - - AddCommand ( - Command.LeftStart, - () => - { - TabScrollOffset = 0; - SelectedTab = Tabs.FirstOrDefault ()!; - - return true; - } - ); - - AddCommand ( - Command.RightEnd, - () => - { - TabScrollOffset = Tabs.Count - 1; - SelectedTab = Tabs.LastOrDefault ()!; - - return true; - } - ); - - AddCommand ( - Command.PageDown, - () => - { - TabScrollOffset += _tabLocations!.Length; - SelectedTab = Tabs.ElementAt (TabScrollOffset); - - return true; - } - ); - - AddCommand ( - Command.PageUp, - () => - { - TabScrollOffset -= _tabLocations!.Length; - SelectedTab = Tabs.ElementAt (TabScrollOffset); - - return true; - } - ); - - // Default keybindings for this view - KeyBindings.Add (Key.CursorLeft, Command.Left); - KeyBindings.Add (Key.CursorRight, Command.Right); - KeyBindings.Add (Key.Home, Command.LeftStart); - KeyBindings.Add (Key.End, Command.RightEnd); - KeyBindings.Add (Key.PageDown, Command.PageDown); - KeyBindings.Add (Key.PageUp, Command.PageUp); - } - - /// - /// The maximum number of characters to render in a Tab header. This prevents one long tab from pushing out all - /// the others. - /// - public uint MaxTabTextWidth { get; set; } = DefaultMaxTabTextWidth; - - // This is needed to hold initial value because it may change during the setter process - private bool _selectedTabHasFocus; - - /// The currently selected member of chosen by the user. - /// - public Tab? SelectedTab - { - get => _selectedTab; - set - { - Tab? old = _selectedTab; - _selectedTabHasFocus = old is { } && (old.HasFocus || !_containerView.CanFocus); - - if (_selectedTab is { }) - { - if (_selectedTab.View is { }) - { - _selectedTab.View.CanFocusChanged -= ContainerViewCanFocus!; - // remove old content - _containerView.Remove (_selectedTab.View); - } - } - - _selectedTab = value; - - // add new content - if (_selectedTab?.View != null) - { - _selectedTab.View.CanFocusChanged += ContainerViewCanFocus!; - _containerView.Add (_selectedTab.View); - } - - ContainerViewCanFocus (null!, null!); - - EnsureSelectedTabIsVisible (); - - if (old != _selectedTab) - { - if (_selectedTabHasFocus || !_containerView.CanFocus) - { - SelectedTab?.SetFocus (); - } - - OnSelectedTabChanged (old!, _selectedTab!); - } - SetNeedsLayout (); - } - } - - private void ContainerViewCanFocus (object sender, EventArgs eventArgs) - { - _containerView.CanFocus = _containerView.Subviews.Count (v => v.CanFocus) > 0; - } - - private TabStyle _style = new (); - - /// Render choices for how to display tabs. After making changes, call . - /// - public TabStyle Style - { - get => _style; - set - { - if (_style == value) - { - return; - } - _style = value; - SetNeedsLayout (); - } - } - - /// All tabs currently hosted by the control. - /// - public IReadOnlyCollection Tabs => _tabs.AsReadOnly (); - - /// When there are too many tabs to render, this indicates the first tab to render on the screen. - /// - public int TabScrollOffset - { - get => _tabScrollOffset; - set - { - _tabScrollOffset = EnsureValidScrollOffsets (value); - SetNeedsLayout (); - } - } - - /// Adds the given to . - /// - /// True to make the newly added Tab the . - public void AddTab (Tab tab, bool andSelect) - { - if (_tabs.Contains (tab)) - { - return; - } - - _tabs.Add (tab); - _tabsBar.Add (tab); - - if (SelectedTab is null || andSelect) - { - SelectedTab = tab; - - EnsureSelectedTabIsVisible (); - - tab.View?.SetFocus (); - } - - SetNeedsLayout (); - } - - /// - /// Updates the control to use the latest state settings in . This can change the size of the - /// client area of the tab (for rendering the selected tab's content). This method includes a call to - /// . - /// - public void ApplyStyleChanges () - { - _containerView.BorderStyle = Style.ShowBorder ? LineStyle.Single : LineStyle.None; - _containerView.Width = Dim.Fill (); - - if (Style.TabsOnBottom) - { - // Tabs are along the bottom so just dodge the border - if (Style.ShowBorder) - { - _containerView.Border!.Thickness = new Thickness (1, 1, 1, 0); - } - - _containerView.Y = 0; - - int tabHeight = GetTabHeight (false); - - // Fill client area leaving space at bottom for tabs - _containerView.Height = Dim.Fill (tabHeight); - - _tabsBar.Height = tabHeight; - - _tabsBar.Y = Pos.Bottom (_containerView); - } - else - { - // Tabs are along the top - if (Style.ShowBorder) - { - _containerView.Border!.Thickness = new Thickness (1, 0, 1, 1); - } - - _tabsBar.Y = 0; - - int tabHeight = GetTabHeight (true); - - //move content down to make space for tabs - _containerView.Y = Pos.Bottom (_tabsBar); - - // Fill client area leaving space at bottom for border - _containerView.Height = Dim.Fill (); - - // The top tab should be 2 or 3 rows high and on the top - - _tabsBar.Height = tabHeight; - - // Should be able to just use 0 but switching between top/bottom tabs repeatedly breaks in ValidatePosDim if just using the absolute value 0 - } - - SetNeedsLayout (); - } - - /// - protected override void OnViewportChanged (DrawEventArgs e) - { - _tabLocations = CalculateViewport (Viewport).ToArray (); - - base.OnViewportChanged (e); - } - - /// Updates to ensure that is visible. - public void EnsureSelectedTabIsVisible () - { - if (!IsInitialized || SelectedTab is null) - { - return; - } - - // if current viewport does not include the selected tab - if (!CalculateViewport (Viewport).Any (r => Equals (SelectedTab, r.Tab))) - { - // Set scroll offset so the first tab rendered is the - TabScrollOffset = Math.Max (0, Tabs.IndexOf (SelectedTab)); - } - } - - /// Updates to be a valid index of . - /// The value to validate. - /// Changes will not be immediately visible in the display until you call . - /// The valid for the given value. - public int EnsureValidScrollOffsets (int value) { return Math.Max (Math.Min (value, Tabs.Count - 1), 0); } - - /// - protected override void OnHasFocusChanged (bool newHasFocus, View? previousFocusedView, View? focusedView) - { - if (SelectedTab is { HasFocus: false } && !_containerView.CanFocus && focusedView == this) - { - SelectedTab?.SetFocus (); - - return; - } - - base.OnHasFocusChanged (newHasFocus, previousFocusedView, focusedView); - } - - /// - /// Removes the given from . Caller is responsible for disposing the - /// tab's hosted if appropriate. - /// - /// - public void RemoveTab (Tab? tab) - { - if (tab is null || !_tabs.Contains (tab)) - { - return; - } - - // what tab was selected before closing - int idx = _tabs.IndexOf (tab); - - _tabs.Remove (tab); - - // if the currently selected tab is no longer a member of Tabs - if (SelectedTab is null || !Tabs.Contains (SelectedTab)) - { - // select the tab closest to the one that disappeared - int toSelect = Math.Max (idx - 1, 0); - - if (toSelect < Tabs.Count) - { - SelectedTab = Tabs.ElementAt (toSelect); - } - else - { - SelectedTab = Tabs.LastOrDefault (); - } - } - - EnsureSelectedTabIsVisible (); - SetNeedsLayout (); - } - - /// Event for when changes. - public event EventHandler? SelectedTabChanged; - - /// - /// Changes the by the given . Positive for right, negative for - /// left. If no tab is currently selected then the first tab will become selected. - /// - /// - public bool SwitchTabBy (int amount) - { - if (Tabs.Count == 0) - { - return false; - } - - // if there is only one tab anyway or nothing is selected - if (Tabs.Count == 1 || SelectedTab is null) - { - SelectedTab = Tabs.ElementAt (0); - - return SelectedTab is { }; - } - - int currentIdx = Tabs.IndexOf (SelectedTab); - - // Currently selected tab has vanished! - if (currentIdx == -1) - { - SelectedTab = Tabs.ElementAt (0); - return true; - } - - int newIdx = Math.Max (0, Math.Min (currentIdx + amount, Tabs.Count - 1)); - - if (newIdx == currentIdx) - { - return false; - } - - SelectedTab = _tabs [newIdx]; - - EnsureSelectedTabIsVisible (); - - return true; - } - - /// - /// Event fired when a is clicked. Can be used to cancel navigation, show context menu (e.g. on - /// right click) etc. - /// - public event EventHandler? TabClicked; - - /// Disposes the control and all . - /// - protected override void Dispose (bool disposing) - { - base.Dispose (disposing); - - // The selected tab will automatically be disposed but - // any tabs not visible will need to be manually disposed - - foreach (Tab tab in Tabs) - { - if (!Equals (SelectedTab, tab)) - { - tab.View?.Dispose (); - } - } - } - - /// Raises the event. - protected virtual void OnSelectedTabChanged (Tab oldTab, Tab newTab) - { - SelectedTabChanged?.Invoke (this, new TabChangedEventArgs (oldTab, newTab)); - } - - /// Returns which tabs to render at each x location. - /// - private IEnumerable CalculateViewport (Rectangle bounds) - { - UnSetCurrentTabs (); - - var i = 1; - View? prevTab = null; - - // Starting at the first or scrolled to tab - foreach (Tab tab in Tabs.Skip (TabScrollOffset)) - { - if (prevTab is { }) - { - tab.X = Pos.Right (prevTab) - 1; - } - else - { - tab.X = 0; - } - - tab.Y = 0; - - // while there is space for the tab - int tabTextWidth = tab.DisplayText.EnumerateRunes ().Sum (c => c.GetColumns ()); - - // The maximum number of characters to use for the tab name as specified - // by the user (MaxTabTextWidth). But not more than the width of the view - // or we won't even be able to render a single tab! - long maxWidth = Math.Max (0, Math.Min (bounds.Width - 3, MaxTabTextWidth)); - - tab.Width = 2; - tab.Height = Style.ShowTopLine ? 3 : 2; - - // if tab view is width <= 3 don't render any tabs - if (maxWidth == 0) - { - tab.Visible = true; - tab.MouseClick += Tab_MouseClick!; - tab.Border!.MouseClick += Tab_MouseClick!; - - yield return new (tab, Equals (SelectedTab, tab)); - - break; - } - - if (tabTextWidth > maxWidth) - { - tab.Text = tab.DisplayText.Substring (0, (int)maxWidth); - tabTextWidth = (int)maxWidth; - } - else - { - tab.Text = tab.DisplayText; - } - - tab.Width = Math.Max (tabTextWidth + 2, 1); - tab.Height = Style.ShowTopLine ? 3 : 2; - - // if there is not enough space for this tab - if (i + tabTextWidth >= bounds.Width) - { - tab.Visible = false; - - break; - } - - // there is enough space! - tab.Visible = true; - tab.MouseClick += Tab_MouseClick!; - tab.Border!.MouseClick += Tab_MouseClick!; - - yield return new (tab, Equals (SelectedTab, tab)); - - prevTab = tab; - - i += tabTextWidth + 1; - } - - if (_selectedTabHasFocus) - { - SelectedTab?.SetFocus (); - } - } - - /// - /// Returns the number of rows occupied by rendering the tabs, this depends on - /// and can be 0 (e.g. if and you ask for ). - /// - /// True to measure the space required at the top of the control, false to measure space at the bottom. - /// . - /// - private int GetTabHeight (bool top) - { - if (top && Style.TabsOnBottom) - { - return 0; - } - - if (!top && !Style.TabsOnBottom) - { - return 0; - } - - return Style.ShowTopLine ? 3 : 2; - } - - private void Tab_MouseClick (object sender, MouseEventArgs e) - { - e.Handled = _tabsBar.NewMouseEvent (e) == true; - } - - private void UnSetCurrentTabs () - { - if (_tabLocations is null) - { - // Ensures unset any visible tab prior to TabScrollOffset - for (int i = 0; i < TabScrollOffset; i++) - { - Tab tab = Tabs.ElementAt (i); - - if (tab.Visible) - { - tab.MouseClick -= Tab_MouseClick!; - tab.Border!.MouseClick -= Tab_MouseClick!; - tab.Visible = false; - } - } - } - else if (_tabLocations is { }) - { - foreach (TabToRender tabToRender in _tabLocations) - { - tabToRender.Tab.MouseClick -= Tab_MouseClick!; - tabToRender.Tab.Border!.MouseClick -= Tab_MouseClick!; - tabToRender.Tab.Visible = false; - } - - _tabLocations = null; - } - } - - /// Raises the event. - /// - private protected virtual void OnTabClicked (TabMouseEventArgs tabMouseEventArgs) { TabClicked?.Invoke (this, tabMouseEventArgs); } - - private class TabRowView : View - { - private readonly TabView _host; - private readonly View _leftScrollIndicator; - private readonly View _rightScrollIndicator; - - public TabRowView (TabView host) - { - _host = host; - Id = "tabRowView"; - - CanFocus = true; - Width = Dim.Fill (); - - _rightScrollIndicator = new View - { - Id = "rightScrollIndicator", - Width = 1, - Height = 1, - Visible = false, - Text = Glyphs.RightArrow.ToString () - }; - _rightScrollIndicator.MouseClick += _host.Tab_MouseClick!; - - _leftScrollIndicator = new View - { - Id = "leftScrollIndicator", - Width = 1, - Height = 1, - Visible = false, - Text = Glyphs.LeftArrow.ToString () - }; - _leftScrollIndicator.MouseClick += _host.Tab_MouseClick!; - - Add (_rightScrollIndicator, _leftScrollIndicator); - } - - protected override bool OnMouseEvent (MouseEventArgs me) - { - View? parent = me.View is Adornment adornment ? adornment.Parent : me.View; - Tab? hit = parent as Tab; - - if (me.IsSingleClicked) - { - _host.OnTabClicked (new TabMouseEventArgs (hit!, me)); - - // user canceled click - if (me.Handled) - { - return true; - } - - if (parent == _host.SelectedTab) - { - _host.SelectedTab?.SetFocus (); - } - } - - if (!me.IsSingleDoubleOrTripleClicked) - { - return false; - } - - if (!HasFocus && CanFocus) - { - SetFocus (); - } - - if (me.IsSingleDoubleOrTripleClicked) - { - var scrollIndicatorHit = 0; - - if (me.View is { Id: "rightScrollIndicator" }) - { - scrollIndicatorHit = 1; - } - else if (me.View is { Id: "leftScrollIndicator" }) - { - scrollIndicatorHit = -1; - } - - if (scrollIndicatorHit != 0) - { - _host.SwitchTabBy (scrollIndicatorHit); - - SetNeedsLayout (); - - return true; - } - - if (hit is { }) - { - _host.SelectedTab = hit; - SetNeedsLayout (); - - return true; - } - } - - return false; - } - - /// - protected override void OnHasFocusChanged (bool newHasFocus, View? previousFocusedView, View? focusedView) - { - if (_host.SelectedTab is { HasFocus: false, CanFocus: true } && focusedView == this) - { - _host.SelectedTab?.SetFocus (); - - return; - } - - base.OnHasFocusChanged (newHasFocus, previousFocusedView, focusedView); - } - - /// - protected override void OnSubviewLayout (LayoutEventArgs args) - { - _host._tabLocations = _host.CalculateViewport (Viewport).ToArray (); - - RenderTabLine (); - - RenderUnderline (); - - base.OnSubviewLayout (args); - } - - /// - protected override bool OnRenderingLineCanvas () - { - RenderTabLineCanvas (); - - return false; - } - - private void RenderTabLineCanvas () - { - if (_host._tabLocations is null) - { - return; - } - - TabToRender [] tabLocations = _host._tabLocations; - int selectedTab = -1; - var lc = new LineCanvas (); - - for (var i = 0; i < tabLocations.Length; i++) - { - View tab = tabLocations [i].Tab; - Rectangle vts = tab.ViewportToScreen (tab.Viewport); - int selectedOffset = _host.Style.ShowTopLine && tabLocations [i].IsSelected ? 0 : 1; - - if (tabLocations [i].IsSelected) - { - selectedTab = i; - - if (i == 0 && _host.TabScrollOffset == 0) - { - if (_host.Style.TabsOnBottom) - { - // Upper left vertical line - lc.AddLine ( - new Point (vts.X - 1, vts.Y - 1), - -1, - Orientation.Vertical, - tab.BorderStyle - ); - } - else - { - // Lower left vertical line - lc.AddLine ( - new Point (vts.X - 1, vts.Bottom - selectedOffset), - -1, - Orientation.Vertical, - tab.BorderStyle - ); - } - } - else if (i > 0 && i <= tabLocations.Length - 1) - { - if (_host.Style.TabsOnBottom) - { - // URCorner - lc.AddLine ( - new Point (vts.X - 1, vts.Y - 1), - 1, - Orientation.Vertical, - tab.BorderStyle - ); - - lc.AddLine ( - new Point (vts.X - 1, vts.Y - 1), - -1, - Orientation.Horizontal, - tab.BorderStyle - ); - } - else - { - // LRCorner - lc.AddLine ( - new Point (vts.X - 1, vts.Bottom - selectedOffset), - -1, - Orientation.Vertical, - tab.BorderStyle - ); - - lc.AddLine ( - new Point (vts.X - 1, vts.Bottom - selectedOffset), - -1, - Orientation.Horizontal, - tab.BorderStyle - ); - } - - if (_host.Style.ShowTopLine) - { - if (_host.Style.TabsOnBottom) - { - // Lower left tee - lc.AddLine ( - new Point (vts.X - 1, vts.Bottom), - -1, - Orientation.Vertical, - tab.BorderStyle - ); - - lc.AddLine ( - new Point (vts.X - 1, vts.Bottom), - 0, - Orientation.Horizontal, - tab.BorderStyle - ); - } - else - { - // Upper left tee - lc.AddLine ( - new Point (vts.X - 1, vts.Y - 1), - 1, - Orientation.Vertical, - tab.BorderStyle - ); - - lc.AddLine ( - new Point (vts.X - 1, vts.Y - 1), - 0, - Orientation.Horizontal, - tab.BorderStyle - ); - } - } - } - - if (i < tabLocations.Length - 1) - { - if (_host.Style.ShowTopLine) - { - if (_host.Style.TabsOnBottom) - { - // Lower right tee - lc.AddLine ( - new Point (vts.Right, vts.Bottom), - -1, - Orientation.Vertical, - tab.BorderStyle - ); - - lc.AddLine ( - new Point (vts.Right, vts.Bottom), - 0, - Orientation.Horizontal, - tab.BorderStyle - ); - } - else - { - // Upper right tee - lc.AddLine ( - new Point (vts.Right, vts.Y - 1), - 1, - Orientation.Vertical, - tab.BorderStyle - ); - - lc.AddLine ( - new Point (vts.Right, vts.Y - 1), - 0, - Orientation.Horizontal, - tab.BorderStyle - ); - } - } - } - - if (_host.Style.TabsOnBottom) - { - //URCorner - lc.AddLine ( - new Point (vts.Right, vts.Y - 1), - 1, - Orientation.Vertical, - tab.BorderStyle - ); - - lc.AddLine ( - new Point (vts.Right, vts.Y - 1), - 1, - Orientation.Horizontal, - tab.BorderStyle - ); - } - else - { - //LLCorner - lc.AddLine ( - new Point (vts.Right, vts.Bottom - selectedOffset), - -1, - Orientation.Vertical, - tab.BorderStyle - ); - - lc.AddLine ( - new Point (vts.Right, vts.Bottom - selectedOffset), - 1, - Orientation.Horizontal, - tab.BorderStyle - ); - } - } - else if (selectedTab == -1) - { - if (i == 0 && string.IsNullOrEmpty (tab.Text)) - { - if (_host.Style.TabsOnBottom) - { - if (_host.Style.ShowTopLine) - { - // LLCorner - lc.AddLine ( - new Point (vts.X - 1, vts.Bottom), - -1, - Orientation.Vertical, - tab.BorderStyle - ); - - lc.AddLine ( - new Point (vts.X - 1, vts.Bottom), - 1, - Orientation.Horizontal, - tab.BorderStyle - ); - } - - // ULCorner - lc.AddLine ( - new Point (vts.X - 1, vts.Y - 1), - 1, - Orientation.Vertical, - tab.BorderStyle - ); - - lc.AddLine ( - new Point (vts.X - 1, vts.Y - 1), - 1, - Orientation.Horizontal, - tab.BorderStyle - ); - } - else - { - if (_host.Style.ShowTopLine) - { - // ULCorner - lc.AddLine ( - new Point (vts.X - 1, vts.Y - 1), - 1, - Orientation.Vertical, - tab.BorderStyle - ); - - lc.AddLine ( - new Point (vts.X - 1, vts.Y - 1), - 1, - Orientation.Horizontal, - tab.BorderStyle - ); - } - - // LLCorner - lc.AddLine ( - new Point (vts.X - 1, vts.Bottom), - -1, - Orientation.Vertical, - tab.BorderStyle - ); - - lc.AddLine ( - new Point (vts.X - 1, vts.Bottom), - 1, - Orientation.Horizontal, - tab.BorderStyle - ); - } - } - else if (i > 0) - { - if (_host.Style.ShowTopLine || _host.Style.TabsOnBottom) - { - // Upper left tee - lc.AddLine ( - new Point (vts.X - 1, vts.Y - 1), - 1, - Orientation.Vertical, - tab.BorderStyle - ); - - lc.AddLine ( - new Point (vts.X - 1, vts.Y - 1), - 0, - Orientation.Horizontal, - tab.BorderStyle - ); - } - - // Lower left tee - lc.AddLine ( - new Point (vts.X - 1, vts.Bottom), - -1, - Orientation.Vertical, - tab.BorderStyle - ); - - lc.AddLine ( - new Point (vts.X - 1, vts.Bottom), - 0, - Orientation.Horizontal, - tab.BorderStyle - ); - } - } - else if (i < tabLocations.Length - 1) - { - if (_host.Style.ShowTopLine) - { - // Upper right tee - lc.AddLine ( - new Point (vts.Right, vts.Y - 1), - 1, - Orientation.Vertical, - tab.BorderStyle - ); - - lc.AddLine ( - new Point (vts.Right, vts.Y - 1), - 0, - Orientation.Horizontal, - tab.BorderStyle - ); - } - - if (_host.Style.ShowTopLine || !_host.Style.TabsOnBottom) - { - // Lower right tee - lc.AddLine ( - new Point (vts.Right, vts.Bottom), - -1, - Orientation.Vertical, - tab.BorderStyle - ); - - lc.AddLine ( - new Point (vts.Right, vts.Bottom), - 0, - Orientation.Horizontal, - tab.BorderStyle - ); - } - else - { - // Upper right tee - lc.AddLine ( - new Point (vts.Right, vts.Y - 1), - 1, - Orientation.Vertical, - tab.BorderStyle - ); - - lc.AddLine ( - new Point (vts.Right, vts.Y - 1), - 0, - Orientation.Horizontal, - tab.BorderStyle - ); - } - } - - if (i == 0 && i != selectedTab && _host is { TabScrollOffset: 0, Style.ShowBorder: true }) - { - if (_host.Style.TabsOnBottom) - { - // Upper left vertical line - lc.AddLine ( - new Point (vts.X - 1, vts.Y - 1), - 0, - Orientation.Vertical, - tab.BorderStyle - ); - - lc.AddLine ( - new Point (vts.X - 1, vts.Y - 1), - 1, - Orientation.Horizontal, - tab.BorderStyle - ); - } - else - { - // Lower left vertical line - lc.AddLine ( - new Point (vts.X - 1, vts.Bottom), - 0, - Orientation.Vertical, - tab.BorderStyle - ); - - lc.AddLine ( - new Point (vts.X - 1, vts.Bottom), - 1, - Orientation.Horizontal, - tab.BorderStyle - ); - } - } - - if (i == tabLocations.Length - 1 && i != selectedTab) - { - if (_host.Style.TabsOnBottom) - { - // Upper right tee - lc.AddLine ( - new Point (vts.Right, vts.Y - 1), - 1, - Orientation.Vertical, - tab.BorderStyle - ); - - lc.AddLine ( - new Point (vts.Right, vts.Y - 1), - 0, - Orientation.Horizontal, - tab.BorderStyle - ); - } - else - { - // Lower right tee - lc.AddLine ( - new Point (vts.Right, vts.Bottom), - -1, - Orientation.Vertical, - tab.BorderStyle - ); - - lc.AddLine ( - new Point (vts.Right, vts.Bottom), - 0, - Orientation.Horizontal, - tab.BorderStyle - ); - } - } - - if (i == tabLocations.Length - 1) - { - var arrowOffset = 1; - - int lastSelectedTab = !_host.Style.ShowTopLine && i == selectedTab ? 1 : - _host.Style.TabsOnBottom ? 1 : 0; - Rectangle tabsBarVts = ViewportToScreen (Viewport); - int lineLength = tabsBarVts.Right - vts.Right; - - // Right horizontal line - if (ShouldDrawRightScrollIndicator ()) - { - if (lineLength - arrowOffset > 0) - { - if (_host.Style.TabsOnBottom) - { - lc.AddLine ( - new Point (vts.Right, vts.Y - lastSelectedTab), - lineLength - arrowOffset, - Orientation.Horizontal, - tab.BorderStyle - ); - } - else - { - lc.AddLine ( - new Point ( - vts.Right, - vts.Bottom - lastSelectedTab - ), - lineLength - arrowOffset, - Orientation.Horizontal, - tab.BorderStyle - ); - } - } - } - else - { - // Right corner - if (_host.Style.TabsOnBottom) - { - lc.AddLine ( - new Point (vts.Right, vts.Y - lastSelectedTab), - lineLength, - Orientation.Horizontal, - tab.BorderStyle - ); - } - else - { - lc.AddLine ( - new Point (vts.Right, vts.Bottom - lastSelectedTab), - lineLength, - Orientation.Horizontal, - tab.BorderStyle - ); - } - - if (_host.Style.ShowBorder) - { - if (_host.Style.TabsOnBottom) - { - // More LRCorner - lc.AddLine ( - new Point ( - tabsBarVts.Right - 1, - vts.Y - lastSelectedTab - ), - -1, - Orientation.Vertical, - tab.BorderStyle - ); - } - else - { - // More URCorner - lc.AddLine ( - new Point ( - tabsBarVts.Right - 1, - vts.Bottom - lastSelectedTab - ), - 1, - Orientation.Vertical, - tab.BorderStyle - ); - } - } - } - } - } - - _host.LineCanvas.Merge (lc); - } - - private int GetUnderlineYPosition () - { - if (_host.Style.TabsOnBottom) - { - return 0; - } - - return _host.Style.ShowTopLine ? 2 : 1; - } - - /// 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.ShowTopLine ? 1 : 0; - - foreach (TabToRender toRender in _host._tabLocations) - { - Tab tab = toRender.Tab; - - if (toRender.IsSelected) - { - selected = tab; - - if (_host.Style.TabsOnBottom) - { - tab.Border!.Thickness = new (1, 0, 1, topLine); - tab.Margin!.Thickness = new (0, 1, 0, 0); - } - else - { - tab.Border!.Thickness = new (1, topLine, 1, 0); - tab.Margin!.Thickness = new (0, 0, 0, topLine); - } - } - else if (selected is null) - { - if (_host.Style.TabsOnBottom) - { - tab.Border!.Thickness = new (1, 1, 1, topLine); - tab.Margin!.Thickness = new (0, 0, 0, 0); - } - else - { - tab.Border!.Thickness = new (1, topLine, 1, 1); - tab.Margin!.Thickness = new (0, 0, 0, 0); - } - } - else - { - if (_host.Style.TabsOnBottom) - { - tab.Border!.Thickness = new (1, 1, 1, topLine); - tab.Margin!.Thickness = new (0, 0, 0, 0); - } - else - { - tab.Border!.Thickness = new (1, topLine, 1, 1); - tab.Margin!.Thickness = new (0, 0, 0, 0); - } - } - - // Ensures updating TextFormatter constrains - tab.TextFormatter.ConstrainToWidth = tab.GetContentSize ().Width; - tab.TextFormatter.ConstrainToHeight = tab.GetContentSize ().Height; - } - } - - /// Renders the line of the tab that adjoins the content of the tab. - private void RenderUnderline () - { - int y = GetUnderlineYPosition (); - - TabToRender? selected = _host._tabLocations?.FirstOrDefault (t => t.IsSelected); - - if (selected is null) - { - return; - } - - // draw scroll indicators - - // if there are more tabs to the left not visible - if (_host.TabScrollOffset > 0) - { - _leftScrollIndicator.X = 0; - _leftScrollIndicator.Y = y; - - // indicate that - _leftScrollIndicator.Visible = true; - - // Ensures this is clicked instead of the first tab - MoveSubviewToEnd (_leftScrollIndicator); - } - else - { - _leftScrollIndicator.Visible = false; - } - - // if there are more tabs to the right not visible - if (ShouldDrawRightScrollIndicator ()) - { - _rightScrollIndicator.X = Viewport.Width - 1; - _rightScrollIndicator.Y = y; - - // indicate that - _rightScrollIndicator.Visible = true; - - // Ensures this is clicked instead of the last tab if under this - MoveSubviewToStart (_rightScrollIndicator); - } - else - { - _rightScrollIndicator.Visible = false; - } - } - - private bool ShouldDrawRightScrollIndicator () { return _host._tabLocations!.LastOrDefault ()?.Tab != _host.Tabs.LastOrDefault (); } - } - - private class TabToRender - { - public TabToRender (Tab tab, bool isSelected) - { - Tab = tab; - IsSelected = isSelected; - } - - /// True if the tab that is being rendered is the selected one. - /// - public bool IsSelected { get; } - - public Tab Tab { get; } - } -} diff --git a/Terminal.Gui/Views/Tab.cs b/Terminal.Gui/Views/TabView/Tab.cs similarity index 100% rename from Terminal.Gui/Views/Tab.cs rename to Terminal.Gui/Views/TabView/Tab.cs diff --git a/Terminal.Gui/Views/TabChangedEventArgs.cs b/Terminal.Gui/Views/TabView/TabChangedEventArgs.cs similarity index 100% rename from Terminal.Gui/Views/TabChangedEventArgs.cs rename to Terminal.Gui/Views/TabView/TabChangedEventArgs.cs diff --git a/Terminal.Gui/Views/TabMouseEventArgs.cs b/Terminal.Gui/Views/TabView/TabMouseEventArgs.cs similarity index 100% rename from Terminal.Gui/Views/TabMouseEventArgs.cs rename to Terminal.Gui/Views/TabView/TabMouseEventArgs.cs diff --git a/Terminal.Gui/Views/TabView/TabRowView.cs b/Terminal.Gui/Views/TabView/TabRowView.cs new file mode 100644 index 0000000000..afee3fa5b2 --- /dev/null +++ b/Terminal.Gui/Views/TabView/TabRowView.cs @@ -0,0 +1,796 @@ +#nullable enable +namespace Terminal.Gui; + +internal class TabRowView : View +{ + private readonly TabView _host; + private readonly View _leftScrollIndicator; + private readonly View _rightScrollIndicator; + + public TabRowView (TabView host) + { + _host = host; + Id = "tabRowView"; + + CanFocus = true; + Width = Dim.Fill (); + + _rightScrollIndicator = new View + { + Id = "rightScrollIndicator", + Width = 1, + Height = 1, + Visible = false, + Text = Glyphs.RightArrow.ToString () + }; + _rightScrollIndicator.MouseClick += _host.Tab_MouseClick!; + + _leftScrollIndicator = new View + { + Id = "leftScrollIndicator", + Width = 1, + Height = 1, + Visible = false, + Text = Glyphs.LeftArrow.ToString () + }; + _leftScrollIndicator.MouseClick += _host.Tab_MouseClick!; + + Add (_rightScrollIndicator, _leftScrollIndicator); + } + + protected override bool OnMouseEvent (MouseEventArgs me) + { + View? parent = me.View is Adornment adornment ? adornment.Parent : me.View; + Tab? hit = parent as Tab; + + if (me.IsSingleClicked) + { + _host.OnTabClicked (new TabMouseEventArgs (hit!, me)); + + // user canceled click + if (me.Handled) + { + return true; + } + + if (parent == _host.SelectedTab) + { + _host.SelectedTab?.SetFocus (); + } + } + + if (!me.IsSingleDoubleOrTripleClicked) + { + return false; + } + + if (!HasFocus && CanFocus) + { + SetFocus (); + } + + if (me.IsSingleDoubleOrTripleClicked) + { + var scrollIndicatorHit = 0; + + if (me.View is { Id: "rightScrollIndicator" }) + { + scrollIndicatorHit = 1; + } + else if (me.View is { Id: "leftScrollIndicator" }) + { + scrollIndicatorHit = -1; + } + + if (scrollIndicatorHit != 0) + { + _host.SwitchTabBy (scrollIndicatorHit); + + SetNeedsLayout (); + + return true; + } + + if (hit is { }) + { + _host.SelectedTab = hit; + SetNeedsLayout (); + + return true; + } + } + + return false; + } + + /// + protected override void OnHasFocusChanged (bool newHasFocus, View? previousFocusedView, View? focusedView) + { + if (_host.SelectedTab is { HasFocus: false, CanFocus: true } && focusedView == this) + { + _host.SelectedTab?.SetFocus (); + + return; + } + + base.OnHasFocusChanged (newHasFocus, previousFocusedView, focusedView); + } + + /// + protected override void OnSubviewLayout (LayoutEventArgs args) + { + _host._tabLocations = _host.CalculateViewport (Viewport).ToArray (); + + RenderTabLine (); + + RenderUnderline (); + + base.OnSubviewLayout (args); + } + + /// + protected override bool OnRenderingLineCanvas () + { + RenderTabLineCanvas (); + + return false; + } + + private void RenderTabLineCanvas () + { + if (_host._tabLocations is null) + { + return; + } + + TabToRender [] tabLocations = _host._tabLocations; + int selectedTab = -1; + var lc = new LineCanvas (); + + for (var i = 0; i < tabLocations.Length; i++) + { + View tab = tabLocations [i].Tab; + Rectangle vts = tab.ViewportToScreen (tab.Viewport); + int selectedOffset = _host.Style.ShowTopLine && tabLocations [i].IsSelected ? 0 : 1; + + if (tabLocations [i].IsSelected) + { + selectedTab = i; + + if (i == 0 && _host.TabScrollOffset == 0) + { + if (_host.Style.TabsOnBottom) + { + // Upper left vertical line + lc.AddLine ( + new Point (vts.X - 1, vts.Y - 1), + -1, + Orientation.Vertical, + tab.BorderStyle + ); + } + else + { + // Lower left vertical line + lc.AddLine ( + new Point (vts.X - 1, vts.Bottom - selectedOffset), + -1, + Orientation.Vertical, + tab.BorderStyle + ); + } + } + else if (i > 0 && i <= tabLocations.Length - 1) + { + if (_host.Style.TabsOnBottom) + { + // URCorner + lc.AddLine ( + new Point (vts.X - 1, vts.Y - 1), + 1, + Orientation.Vertical, + tab.BorderStyle + ); + + lc.AddLine ( + new Point (vts.X - 1, vts.Y - 1), + -1, + Orientation.Horizontal, + tab.BorderStyle + ); + } + else + { + // LRCorner + lc.AddLine ( + new Point (vts.X - 1, vts.Bottom - selectedOffset), + -1, + Orientation.Vertical, + tab.BorderStyle + ); + + lc.AddLine ( + new Point (vts.X - 1, vts.Bottom - selectedOffset), + -1, + Orientation.Horizontal, + tab.BorderStyle + ); + } + + if (_host.Style.ShowTopLine) + { + if (_host.Style.TabsOnBottom) + { + // Lower left tee + lc.AddLine ( + new Point (vts.X - 1, vts.Bottom), + -1, + Orientation.Vertical, + tab.BorderStyle + ); + + lc.AddLine ( + new Point (vts.X - 1, vts.Bottom), + 0, + Orientation.Horizontal, + tab.BorderStyle + ); + } + else + { + // Upper left tee + lc.AddLine ( + new Point (vts.X - 1, vts.Y - 1), + 1, + Orientation.Vertical, + tab.BorderStyle + ); + + lc.AddLine ( + new Point (vts.X - 1, vts.Y - 1), + 0, + Orientation.Horizontal, + tab.BorderStyle + ); + } + } + } + + if (i < tabLocations.Length - 1) + { + if (_host.Style.ShowTopLine) + { + if (_host.Style.TabsOnBottom) + { + // Lower right tee + lc.AddLine ( + new Point (vts.Right, vts.Bottom), + -1, + Orientation.Vertical, + tab.BorderStyle + ); + + lc.AddLine ( + new Point (vts.Right, vts.Bottom), + 0, + Orientation.Horizontal, + tab.BorderStyle + ); + } + else + { + // Upper right tee + lc.AddLine ( + new Point (vts.Right, vts.Y - 1), + 1, + Orientation.Vertical, + tab.BorderStyle + ); + + lc.AddLine ( + new Point (vts.Right, vts.Y - 1), + 0, + Orientation.Horizontal, + tab.BorderStyle + ); + } + } + } + + if (_host.Style.TabsOnBottom) + { + //URCorner + lc.AddLine ( + new Point (vts.Right, vts.Y - 1), + 1, + Orientation.Vertical, + tab.BorderStyle + ); + + lc.AddLine ( + new Point (vts.Right, vts.Y - 1), + 1, + Orientation.Horizontal, + tab.BorderStyle + ); + } + else + { + //LLCorner + lc.AddLine ( + new Point (vts.Right, vts.Bottom - selectedOffset), + -1, + Orientation.Vertical, + tab.BorderStyle + ); + + lc.AddLine ( + new Point (vts.Right, vts.Bottom - selectedOffset), + 1, + Orientation.Horizontal, + tab.BorderStyle + ); + } + } + else if (selectedTab == -1) + { + if (i == 0 && string.IsNullOrEmpty (tab.Text)) + { + if (_host.Style.TabsOnBottom) + { + if (_host.Style.ShowTopLine) + { + // LLCorner + lc.AddLine ( + new Point (vts.X - 1, vts.Bottom), + -1, + Orientation.Vertical, + tab.BorderStyle + ); + + lc.AddLine ( + new Point (vts.X - 1, vts.Bottom), + 1, + Orientation.Horizontal, + tab.BorderStyle + ); + } + + // ULCorner + lc.AddLine ( + new Point (vts.X - 1, vts.Y - 1), + 1, + Orientation.Vertical, + tab.BorderStyle + ); + + lc.AddLine ( + new Point (vts.X - 1, vts.Y - 1), + 1, + Orientation.Horizontal, + tab.BorderStyle + ); + } + else + { + if (_host.Style.ShowTopLine) + { + // ULCorner + lc.AddLine ( + new Point (vts.X - 1, vts.Y - 1), + 1, + Orientation.Vertical, + tab.BorderStyle + ); + + lc.AddLine ( + new Point (vts.X - 1, vts.Y - 1), + 1, + Orientation.Horizontal, + tab.BorderStyle + ); + } + + // LLCorner + lc.AddLine ( + new Point (vts.X - 1, vts.Bottom), + -1, + Orientation.Vertical, + tab.BorderStyle + ); + + lc.AddLine ( + new Point (vts.X - 1, vts.Bottom), + 1, + Orientation.Horizontal, + tab.BorderStyle + ); + } + } + else if (i > 0) + { + if (_host.Style.ShowTopLine || _host.Style.TabsOnBottom) + { + // Upper left tee + lc.AddLine ( + new Point (vts.X - 1, vts.Y - 1), + 1, + Orientation.Vertical, + tab.BorderStyle + ); + + lc.AddLine ( + new Point (vts.X - 1, vts.Y - 1), + 0, + Orientation.Horizontal, + tab.BorderStyle + ); + } + + // Lower left tee + lc.AddLine ( + new Point (vts.X - 1, vts.Bottom), + -1, + Orientation.Vertical, + tab.BorderStyle + ); + + lc.AddLine ( + new Point (vts.X - 1, vts.Bottom), + 0, + Orientation.Horizontal, + tab.BorderStyle + ); + } + } + else if (i < tabLocations.Length - 1) + { + if (_host.Style.ShowTopLine) + { + // Upper right tee + lc.AddLine ( + new Point (vts.Right, vts.Y - 1), + 1, + Orientation.Vertical, + tab.BorderStyle + ); + + lc.AddLine ( + new Point (vts.Right, vts.Y - 1), + 0, + Orientation.Horizontal, + tab.BorderStyle + ); + } + + if (_host.Style.ShowTopLine || !_host.Style.TabsOnBottom) + { + // Lower right tee + lc.AddLine ( + new Point (vts.Right, vts.Bottom), + -1, + Orientation.Vertical, + tab.BorderStyle + ); + + lc.AddLine ( + new Point (vts.Right, vts.Bottom), + 0, + Orientation.Horizontal, + tab.BorderStyle + ); + } + else + { + // Upper right tee + lc.AddLine ( + new Point (vts.Right, vts.Y - 1), + 1, + Orientation.Vertical, + tab.BorderStyle + ); + + lc.AddLine ( + new Point (vts.Right, vts.Y - 1), + 0, + Orientation.Horizontal, + tab.BorderStyle + ); + } + } + + if (i == 0 && i != selectedTab && _host is { TabScrollOffset: 0, Style.ShowBorder: true }) + { + if (_host.Style.TabsOnBottom) + { + // Upper left vertical line + lc.AddLine ( + new Point (vts.X - 1, vts.Y - 1), + 0, + Orientation.Vertical, + tab.BorderStyle + ); + + lc.AddLine ( + new Point (vts.X - 1, vts.Y - 1), + 1, + Orientation.Horizontal, + tab.BorderStyle + ); + } + else + { + // Lower left vertical line + lc.AddLine ( + new Point (vts.X - 1, vts.Bottom), + 0, + Orientation.Vertical, + tab.BorderStyle + ); + + lc.AddLine ( + new Point (vts.X - 1, vts.Bottom), + 1, + Orientation.Horizontal, + tab.BorderStyle + ); + } + } + + if (i == tabLocations.Length - 1 && i != selectedTab) + { + if (_host.Style.TabsOnBottom) + { + // Upper right tee + lc.AddLine ( + new Point (vts.Right, vts.Y - 1), + 1, + Orientation.Vertical, + tab.BorderStyle + ); + + lc.AddLine ( + new Point (vts.Right, vts.Y - 1), + 0, + Orientation.Horizontal, + tab.BorderStyle + ); + } + else + { + // Lower right tee + lc.AddLine ( + new Point (vts.Right, vts.Bottom), + -1, + Orientation.Vertical, + tab.BorderStyle + ); + + lc.AddLine ( + new Point (vts.Right, vts.Bottom), + 0, + Orientation.Horizontal, + tab.BorderStyle + ); + } + } + + if (i == tabLocations.Length - 1) + { + var arrowOffset = 1; + + int lastSelectedTab = !_host.Style.ShowTopLine && i == selectedTab ? 1 : + _host.Style.TabsOnBottom ? 1 : 0; + Rectangle tabsBarVts = ViewportToScreen (Viewport); + int lineLength = tabsBarVts.Right - vts.Right; + + // Right horizontal line + if (ShouldDrawRightScrollIndicator ()) + { + if (lineLength - arrowOffset > 0) + { + if (_host.Style.TabsOnBottom) + { + lc.AddLine ( + new Point (vts.Right, vts.Y - lastSelectedTab), + lineLength - arrowOffset, + Orientation.Horizontal, + tab.BorderStyle + ); + } + else + { + lc.AddLine ( + new Point ( + vts.Right, + vts.Bottom - lastSelectedTab + ), + lineLength - arrowOffset, + Orientation.Horizontal, + tab.BorderStyle + ); + } + } + } + else + { + // Right corner + if (_host.Style.TabsOnBottom) + { + lc.AddLine ( + new Point (vts.Right, vts.Y - lastSelectedTab), + lineLength, + Orientation.Horizontal, + tab.BorderStyle + ); + } + else + { + lc.AddLine ( + new Point (vts.Right, vts.Bottom - lastSelectedTab), + lineLength, + Orientation.Horizontal, + tab.BorderStyle + ); + } + + if (_host.Style.ShowBorder) + { + if (_host.Style.TabsOnBottom) + { + // More LRCorner + lc.AddLine ( + new Point ( + tabsBarVts.Right - 1, + vts.Y - lastSelectedTab + ), + -1, + Orientation.Vertical, + tab.BorderStyle + ); + } + else + { + // More URCorner + lc.AddLine ( + new Point ( + tabsBarVts.Right - 1, + vts.Bottom - lastSelectedTab + ), + 1, + Orientation.Vertical, + tab.BorderStyle + ); + } + } + } + } + } + + _host.LineCanvas.Merge (lc); + } + + private int GetUnderlineYPosition () + { + if (_host.Style.TabsOnBottom) + { + return 0; + } + + return _host.Style.ShowTopLine ? 2 : 1; + } + + /// 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.ShowTopLine ? 1 : 0; + + foreach (TabToRender toRender in _host._tabLocations) + { + Tab tab = toRender.Tab; + + if (toRender.IsSelected) + { + selected = tab; + + if (_host.Style.TabsOnBottom) + { + tab.Border!.Thickness = new (1, 0, 1, topLine); + tab.Margin!.Thickness = new (0, 1, 0, 0); + } + else + { + tab.Border!.Thickness = new (1, topLine, 1, 0); + tab.Margin!.Thickness = new (0, 0, 0, topLine); + } + } + else if (selected is null) + { + if (_host.Style.TabsOnBottom) + { + tab.Border!.Thickness = new (1, 1, 1, topLine); + tab.Margin!.Thickness = new (0, 0, 0, 0); + } + else + { + tab.Border!.Thickness = new (1, topLine, 1, 1); + tab.Margin!.Thickness = new (0, 0, 0, 0); + } + } + else + { + if (_host.Style.TabsOnBottom) + { + tab.Border!.Thickness = new (1, 1, 1, topLine); + tab.Margin!.Thickness = new (0, 0, 0, 0); + } + else + { + tab.Border!.Thickness = new (1, topLine, 1, 1); + tab.Margin!.Thickness = new (0, 0, 0, 0); + } + } + + // Ensures updating TextFormatter constrains + tab.TextFormatter.ConstrainToWidth = tab.GetContentSize ().Width; + tab.TextFormatter.ConstrainToHeight = tab.GetContentSize ().Height; + } + } + + /// Renders the line of the tab that adjoins the content of the tab. + private void RenderUnderline () + { + int y = GetUnderlineYPosition (); + + TabToRender? selected = _host._tabLocations?.FirstOrDefault (t => t.IsSelected); + + if (selected is null) + { + return; + } + + // draw scroll indicators + + // if there are more tabs to the left not visible + if (_host.TabScrollOffset > 0) + { + _leftScrollIndicator.X = 0; + _leftScrollIndicator.Y = y; + + // indicate that + _leftScrollIndicator.Visible = true; + + // Ensures this is clicked instead of the first tab + MoveSubviewToEnd (_leftScrollIndicator); + } + else + { + _leftScrollIndicator.Visible = false; + } + + // if there are more tabs to the right not visible + if (ShouldDrawRightScrollIndicator ()) + { + _rightScrollIndicator.X = Viewport.Width - 1; + _rightScrollIndicator.Y = y; + + // indicate that + _rightScrollIndicator.Visible = true; + + // Ensures this is clicked instead of the last tab if under this + MoveSubviewToStart (_rightScrollIndicator); + } + else + { + _rightScrollIndicator.Visible = false; + } + } + + private bool ShouldDrawRightScrollIndicator () { return _host._tabLocations!.LastOrDefault ()?.Tab != _host.Tabs.LastOrDefault (); } +} diff --git a/Terminal.Gui/Views/TabStyle.cs b/Terminal.Gui/Views/TabView/TabStyle.cs similarity index 100% rename from Terminal.Gui/Views/TabStyle.cs rename to Terminal.Gui/Views/TabView/TabStyle.cs diff --git a/Terminal.Gui/Views/TabView/TabToRender.cs b/Terminal.Gui/Views/TabView/TabToRender.cs new file mode 100644 index 0000000000..d29930a9e2 --- /dev/null +++ b/Terminal.Gui/Views/TabView/TabToRender.cs @@ -0,0 +1,17 @@ +#nullable enable +namespace Terminal.Gui; + +internal class TabToRender +{ + public TabToRender (Tab tab, bool isSelected) + { + Tab = tab; + IsSelected = isSelected; + } + + /// True if the tab that is being rendered is the selected one. + /// + public bool IsSelected { get; } + + public Tab Tab { get; } +} diff --git a/Terminal.Gui/Views/TabView/TabView.cs b/Terminal.Gui/Views/TabView/TabView.cs new file mode 100644 index 0000000000..627e251fdf --- /dev/null +++ b/Terminal.Gui/Views/TabView/TabView.cs @@ -0,0 +1,576 @@ +#nullable enable +namespace Terminal.Gui; + +/// Control that hosts multiple sub views, presenting a single one at once. +public class TabView : View +{ + /// The default to set on new controls. + public const uint DefaultMaxTabTextWidth = 30; + + /// + /// This sub view is the main client area of the current tab. It hosts the of the tab, the + /// . + /// + private readonly View _containerView; + + private readonly List _tabs = new (); + + /// This sub view is the 2 or 3 line control that represents the actual tabs themselves. + private readonly TabRowView _tabsBar; + + private Tab? _selectedTab; + + // BUGBUG: Horrible containment design. + internal TabToRender []? _tabLocations; + private int _tabScrollOffset; + + /// Initializes a class. + public TabView () + { + CanFocus = true; + TabStop = TabBehavior.TabStop; // Because TabView has focusable subviews, it must be a TabGroup + _tabsBar = new TabRowView (this); + _containerView = new (); + ApplyStyleChanges (); + + base.Add (_tabsBar); + base.Add (_containerView); + + // Things this view knows how to do + AddCommand (Command.Left, () => SwitchTabBy (-1)); + + AddCommand (Command.Right, () => SwitchTabBy (1)); + + AddCommand ( + Command.LeftStart, + () => + { + TabScrollOffset = 0; + SelectedTab = Tabs.FirstOrDefault ()!; + + return true; + } + ); + + AddCommand ( + Command.RightEnd, + () => + { + TabScrollOffset = Tabs.Count - 1; + SelectedTab = Tabs.LastOrDefault ()!; + + return true; + } + ); + + AddCommand ( + Command.PageDown, + () => + { + TabScrollOffset += _tabLocations!.Length; + SelectedTab = Tabs.ElementAt (TabScrollOffset); + + return true; + } + ); + + AddCommand ( + Command.PageUp, + () => + { + TabScrollOffset -= _tabLocations!.Length; + SelectedTab = Tabs.ElementAt (TabScrollOffset); + + return true; + } + ); + + // Default keybindings for this view + KeyBindings.Add (Key.CursorLeft, Command.Left); + KeyBindings.Add (Key.CursorRight, Command.Right); + KeyBindings.Add (Key.Home, Command.LeftStart); + KeyBindings.Add (Key.End, Command.RightEnd); + KeyBindings.Add (Key.PageDown, Command.PageDown); + KeyBindings.Add (Key.PageUp, Command.PageUp); + } + + /// + /// The maximum number of characters to render in a Tab header. This prevents one long tab from pushing out all + /// the others. + /// + public uint MaxTabTextWidth { get; set; } = DefaultMaxTabTextWidth; + + // This is needed to hold initial value because it may change during the setter process + private bool _selectedTabHasFocus; + + /// The currently selected member of chosen by the user. + /// + public Tab? SelectedTab + { + get => _selectedTab; + set + { + Tab? old = _selectedTab; + _selectedTabHasFocus = old is { } && (old.HasFocus || !_containerView.CanFocus); + + if (_selectedTab is { }) + { + if (_selectedTab.View is { }) + { + _selectedTab.View.CanFocusChanged -= ContainerViewCanFocus!; + // remove old content + _containerView.Remove (_selectedTab.View); + } + } + + _selectedTab = value; + + // add new content + if (_selectedTab?.View != null) + { + _selectedTab.View.CanFocusChanged += ContainerViewCanFocus!; + _containerView.Add (_selectedTab.View); + } + + ContainerViewCanFocus (null!, null!); + + EnsureSelectedTabIsVisible (); + + if (old != _selectedTab) + { + if (_selectedTabHasFocus || !_containerView.CanFocus) + { + SelectedTab?.SetFocus (); + } + + OnSelectedTabChanged (old!, _selectedTab!); + } + SetNeedsLayout (); + } + } + + private void ContainerViewCanFocus (object sender, EventArgs eventArgs) + { + _containerView.CanFocus = _containerView.Subviews.Count (v => v.CanFocus) > 0; + } + + private TabStyle _style = new (); + + /// Render choices for how to display tabs. After making changes, call . + /// + public TabStyle Style + { + get => _style; + set + { + if (_style == value) + { + return; + } + _style = value; + SetNeedsLayout (); + } + } + + /// All tabs currently hosted by the control. + /// + public IReadOnlyCollection Tabs => _tabs.AsReadOnly (); + + /// When there are too many tabs to render, this indicates the first tab to render on the screen. + /// + public int TabScrollOffset + { + get => _tabScrollOffset; + set + { + _tabScrollOffset = EnsureValidScrollOffsets (value); + SetNeedsLayout (); + } + } + + /// Adds the given to . + /// + /// True to make the newly added Tab the . + public void AddTab (Tab tab, bool andSelect) + { + if (_tabs.Contains (tab)) + { + return; + } + + _tabs.Add (tab); + _tabsBar.Add (tab); + + if (SelectedTab is null || andSelect) + { + SelectedTab = tab; + + EnsureSelectedTabIsVisible (); + + tab.View?.SetFocus (); + } + + SetNeedsLayout (); + } + + /// + /// Updates the control to use the latest state settings in . This can change the size of the + /// client area of the tab (for rendering the selected tab's content). This method includes a call to + /// . + /// + public void ApplyStyleChanges () + { + _containerView.BorderStyle = Style.ShowBorder ? LineStyle.Single : LineStyle.None; + _containerView.Width = Dim.Fill (); + + if (Style.TabsOnBottom) + { + // Tabs are along the bottom so just dodge the border + if (Style.ShowBorder) + { + _containerView.Border!.Thickness = new Thickness (1, 1, 1, 0); + } + + _containerView.Y = 0; + + int tabHeight = GetTabHeight (false); + + // Fill client area leaving space at bottom for tabs + _containerView.Height = Dim.Fill (tabHeight); + + _tabsBar.Height = tabHeight; + + _tabsBar.Y = Pos.Bottom (_containerView); + } + else + { + // Tabs are along the top + if (Style.ShowBorder) + { + _containerView.Border!.Thickness = new Thickness (1, 0, 1, 1); + } + + _tabsBar.Y = 0; + + int tabHeight = GetTabHeight (true); + + //move content down to make space for tabs + _containerView.Y = Pos.Bottom (_tabsBar); + + // Fill client area leaving space at bottom for border + _containerView.Height = Dim.Fill (); + + // The top tab should be 2 or 3 rows high and on the top + + _tabsBar.Height = tabHeight; + + // Should be able to just use 0 but switching between top/bottom tabs repeatedly breaks in ValidatePosDim if just using the absolute value 0 + } + + SetNeedsLayout (); + } + + /// + protected override void OnViewportChanged (DrawEventArgs e) + { + _tabLocations = CalculateViewport (Viewport).ToArray (); + + base.OnViewportChanged (e); + } + + /// Updates to ensure that is visible. + public void EnsureSelectedTabIsVisible () + { + if (!IsInitialized || SelectedTab is null) + { + return; + } + + // if current viewport does not include the selected tab + if (!CalculateViewport (Viewport).Any (r => Equals (SelectedTab, r.Tab))) + { + // Set scroll offset so the first tab rendered is the + TabScrollOffset = Math.Max (0, Tabs.IndexOf (SelectedTab)); + } + } + + /// Updates to be a valid index of . + /// The value to validate. + /// Changes will not be immediately visible in the display until you call . + /// The valid for the given value. + public int EnsureValidScrollOffsets (int value) { return Math.Max (Math.Min (value, Tabs.Count - 1), 0); } + + /// + protected override void OnHasFocusChanged (bool newHasFocus, View? previousFocusedView, View? focusedView) + { + if (SelectedTab is { HasFocus: false } && !_containerView.CanFocus && focusedView == this) + { + SelectedTab?.SetFocus (); + + return; + } + + base.OnHasFocusChanged (newHasFocus, previousFocusedView, focusedView); + } + + /// + /// Removes the given from . Caller is responsible for disposing the + /// tab's hosted if appropriate. + /// + /// + public void RemoveTab (Tab? tab) + { + if (tab is null || !_tabs.Contains (tab)) + { + return; + } + + // what tab was selected before closing + int idx = _tabs.IndexOf (tab); + + _tabs.Remove (tab); + + // if the currently selected tab is no longer a member of Tabs + if (SelectedTab is null || !Tabs.Contains (SelectedTab)) + { + // select the tab closest to the one that disappeared + int toSelect = Math.Max (idx - 1, 0); + + if (toSelect < Tabs.Count) + { + SelectedTab = Tabs.ElementAt (toSelect); + } + else + { + SelectedTab = Tabs.LastOrDefault (); + } + } + + EnsureSelectedTabIsVisible (); + SetNeedsLayout (); + } + + /// Event for when changes. + public event EventHandler? SelectedTabChanged; + + /// + /// Changes the by the given . Positive for right, negative for + /// left. If no tab is currently selected then the first tab will become selected. + /// + /// + public bool SwitchTabBy (int amount) + { + if (Tabs.Count == 0) + { + return false; + } + + // if there is only one tab anyway or nothing is selected + if (Tabs.Count == 1 || SelectedTab is null) + { + SelectedTab = Tabs.ElementAt (0); + + return SelectedTab is { }; + } + + int currentIdx = Tabs.IndexOf (SelectedTab); + + // Currently selected tab has vanished! + if (currentIdx == -1) + { + SelectedTab = Tabs.ElementAt (0); + return true; + } + + int newIdx = Math.Max (0, Math.Min (currentIdx + amount, Tabs.Count - 1)); + + if (newIdx == currentIdx) + { + return false; + } + + SelectedTab = _tabs [newIdx]; + + EnsureSelectedTabIsVisible (); + + return true; + } + + /// + /// Event fired when a is clicked. Can be used to cancel navigation, show context menu (e.g. on + /// right click) etc. + /// + public event EventHandler? TabClicked; + + /// Disposes the control and all . + /// + protected override void Dispose (bool disposing) + { + base.Dispose (disposing); + + // The selected tab will automatically be disposed but + // any tabs not visible will need to be manually disposed + + foreach (Tab tab in Tabs) + { + if (!Equals (SelectedTab, tab)) + { + tab.View?.Dispose (); + } + } + } + + /// Raises the event. + protected virtual void OnSelectedTabChanged (Tab oldTab, Tab newTab) + { + SelectedTabChanged?.Invoke (this, new TabChangedEventArgs (oldTab, newTab)); + } + + /// Returns which tabs to render at each x location. + /// + internal IEnumerable CalculateViewport (Rectangle bounds) + { + UnSetCurrentTabs (); + + var i = 1; + View? prevTab = null; + + // Starting at the first or scrolled to tab + foreach (Tab tab in Tabs.Skip (TabScrollOffset)) + { + if (prevTab is { }) + { + tab.X = Pos.Right (prevTab) - 1; + } + else + { + tab.X = 0; + } + + tab.Y = 0; + + // while there is space for the tab + int tabTextWidth = tab.DisplayText.EnumerateRunes ().Sum (c => c.GetColumns ()); + + // The maximum number of characters to use for the tab name as specified + // by the user (MaxTabTextWidth). But not more than the width of the view + // or we won't even be able to render a single tab! + long maxWidth = Math.Max (0, Math.Min (bounds.Width - 3, MaxTabTextWidth)); + + tab.Width = 2; + tab.Height = Style.ShowTopLine ? 3 : 2; + + // if tab view is width <= 3 don't render any tabs + if (maxWidth == 0) + { + tab.Visible = true; + tab.MouseClick += Tab_MouseClick!; + tab.Border!.MouseClick += Tab_MouseClick!; + + yield return new (tab, Equals (SelectedTab, tab)); + + break; + } + + if (tabTextWidth > maxWidth) + { + tab.Text = tab.DisplayText.Substring (0, (int)maxWidth); + tabTextWidth = (int)maxWidth; + } + else + { + tab.Text = tab.DisplayText; + } + + tab.Width = Math.Max (tabTextWidth + 2, 1); + tab.Height = Style.ShowTopLine ? 3 : 2; + + // if there is not enough space for this tab + if (i + tabTextWidth >= bounds.Width) + { + tab.Visible = false; + + break; + } + + // there is enough space! + tab.Visible = true; + tab.MouseClick += Tab_MouseClick!; + tab.Border!.MouseClick += Tab_MouseClick!; + + yield return new (tab, Equals (SelectedTab, tab)); + + prevTab = tab; + + i += tabTextWidth + 1; + } + + if (_selectedTabHasFocus) + { + SelectedTab?.SetFocus (); + } + } + + /// + /// Returns the number of rows occupied by rendering the tabs, this depends on + /// and can be 0 (e.g. if and you ask for ). + /// + /// True to measure the space required at the top of the control, false to measure space at the bottom. + /// . + /// + private int GetTabHeight (bool top) + { + if (top && Style.TabsOnBottom) + { + return 0; + } + + if (!top && !Style.TabsOnBottom) + { + return 0; + } + + return Style.ShowTopLine ? 3 : 2; + } + + internal void Tab_MouseClick (object sender, MouseEventArgs e) + { + e.Handled = _tabsBar.NewMouseEvent (e) == true; + } + + private void UnSetCurrentTabs () + { + if (_tabLocations is null) + { + // Ensures unset any visible tab prior to TabScrollOffset + for (int i = 0; i < TabScrollOffset; i++) + { + Tab tab = Tabs.ElementAt (i); + + if (tab.Visible) + { + tab.MouseClick -= Tab_MouseClick!; + tab.Border!.MouseClick -= Tab_MouseClick!; + tab.Visible = false; + } + } + } + else if (_tabLocations is { }) + { + foreach (TabToRender tabToRender in _tabLocations) + { + tabToRender.Tab.MouseClick -= Tab_MouseClick!; + tabToRender.Tab.Border!.MouseClick -= Tab_MouseClick!; + tabToRender.Tab.Visible = false; + } + + _tabLocations = null; + } + } + + /// Raises the event. + /// + internal virtual void OnTabClicked (TabMouseEventArgs tabMouseEventArgs) { TabClicked?.Invoke (this, tabMouseEventArgs); } + + +} \ No newline at end of file diff --git a/UnitTests/View/Layout/SetLayoutTests.cs b/UnitTests/View/Layout/SetLayoutTests.cs index ce0addd542..7cfc5cb0cc 100644 --- a/UnitTests/View/Layout/SetLayoutTests.cs +++ b/UnitTests/View/Layout/SetLayoutTests.cs @@ -813,6 +813,11 @@ public void Does_Not_Throw_If_Nested_SubViews_Ref_Topmost_SuperView () t.Dispose (); } + + // BUGBUG: This is NOT a SetLayoutTest but a Border and SuperViewRendersLineCanvas test + // BUGBUG: Please move this to a more appropriate test class + // BUGBUG: It also tests multiple things and it's not clear what it's really testing. + // BUGBUG: Simplfiy it and break it into separate tests that each test ONE thing. [Fact] [SetupFakeDriver] public void Pos_Right_With_Adornments () From e23b671fa30c3e54169ebba37f686778a8e7eba7 Mon Sep 17 00:00:00 2001 From: BDisp Date: Tue, 19 Nov 2024 19:26:50 +0000 Subject: [PATCH 08/13] Remove forgot unnecessary unit tests. --- UnitTests/View/Layout/SetLayoutTests.cs | 107 ------------------------ 1 file changed, 107 deletions(-) diff --git a/UnitTests/View/Layout/SetLayoutTests.cs b/UnitTests/View/Layout/SetLayoutTests.cs index 7cfc5cb0cc..d348fe2b10 100644 --- a/UnitTests/View/Layout/SetLayoutTests.cs +++ b/UnitTests/View/Layout/SetLayoutTests.cs @@ -812,111 +812,4 @@ public void Does_Not_Throw_If_Nested_SubViews_Ref_Topmost_SuperView () Assert.Equal (19, v2.Frame.Height); t.Dispose (); } - - - // BUGBUG: This is NOT a SetLayoutTest but a Border and SuperViewRendersLineCanvas test - // BUGBUG: Please move this to a more appropriate test class - // BUGBUG: It also tests multiple things and it's not clear what it's really testing. - // BUGBUG: Simplfiy it and break it into separate tests that each test ONE thing. - [Fact] - [SetupFakeDriver] - public void Pos_Right_With_Adornments () - { - View view1 = new () { Text = "View1", Width = 7, Height = 3, BorderStyle = LineStyle.Rounded }; - View view2 = new () { Text = "View2", X = Pos.Right (view1) - 1, Width = 7, Height = 3, BorderStyle = LineStyle.Rounded }; - View view3 = new () { Text = "View3", X = Pos.Right (view2) - 1, Width = 7, Height = 3, BorderStyle = LineStyle.Rounded }; - View container = new () { Width = Dim.Fill (), Height = 3 }; - container.Add (view1, view2, view3); - - View view4 = new () { Text = "View4", Y = Pos.Bottom (container), Width = 21, Height = 3, BorderStyle = LineStyle.Rounded }; - View superView = new () { Width = Dim.Fill (), Height = Dim.Fill () }; - superView.Add (container, view4); - - superView.Layout (); - superView.Draw (); - - TestHelpers.AssertDriverContentsAre ( - @" -╭─────╭─────╭─────╮ -│View1│View2│View3│ -╰─────╰─────╰─────╯ -╭───────────────────╮ -│View4 │ -╰───────────────────╯ -", - output - ); - - // Remove border bottom from the view1 - view1.Border!.Thickness = new (1, 1, 1, 0); - // Insert margin bottom into the view1 - view1.Margin!.Thickness = new (0, 0, 0, 1); - - View.SetClipToScreen (); - superView.Draw (); - - TestHelpers.AssertDriverContentsAre ( - @" -╭─────╭─────╭─────╮ -│View1│View2│View3│ - ╰─────╰─────╯ -╭───────────────────╮ -│View4 │ -╰───────────────────╯ -", - output - ); - - // Restore view1 border - view1.Border.Thickness = new (1); - // Restore view1 margin - view1.Margin.Thickness = Thickness.Empty; - - // Remove border bottom from the view2 - view2.Border!.Thickness = new (1, 1, 1, 0); - // Insert margin bottom into the view2 - view2.Margin!.Thickness = new (0, 0, 0, 1); - - View.SetClipToScreen (); - superView.Draw (); - - TestHelpers.AssertDriverContentsAre ( - @" -╭─────╭─────╭─────╮ -│View1│View2│View3│ -╰─────╯ ╰─────╯ -╭───────────────────╮ -│View4 │ -╰───────────────────╯ -", - output - ); - - // Restore view2 border - view2.Border.Thickness = new (1); - // Restore view2 margin - view2.Margin.Thickness = Thickness.Empty; - - // Remove border bottom from the view3 - view3.Border!.Thickness = new (1, 1, 1, 0); - // Insert margin bottom into the view3 - view3.Margin!.Thickness = new (0, 0, 0, 1); - - View.SetClipToScreen (); - superView.Draw (); - - TestHelpers.AssertDriverContentsAre ( - @" -╭─────╭─────╭─────╮ -│View1│View2│View3│ -╰─────╰─────╯ -╭───────────────────╮ -│View4 │ -╰───────────────────╯ -", - output - ); - - superView.Dispose (); - } } From fabe34fb2d168e54bddf7cfc23362de2f425dce9 Mon Sep 17 00:00:00 2001 From: BDisp Date: Tue, 19 Nov 2024 21:56:41 +0000 Subject: [PATCH 09/13] Fixes #3836. SetupFakeDriver sometimes causes failure in the unit test. --- UnitTests/TestHelpers.cs | 5 +++++ UnitTests/View/Adornment/PaddingTests.cs | 2 -- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/UnitTests/TestHelpers.cs b/UnitTests/TestHelpers.cs index 1b841ee65a..b8d3bb98da 100644 --- a/UnitTests/TestHelpers.cs +++ b/UnitTests/TestHelpers.cs @@ -197,6 +197,11 @@ public override void After (MethodInfo methodUnderTest) // Turn off diagnostic flags in case some test left them on View.Diagnostics = ViewDiagnosticFlags.Off; + if (Application.Driver is { }) + { + ((FakeDriver)Application.Driver).End (); + } + Application.Driver = null; base.After (methodUnderTest); } diff --git a/UnitTests/View/Adornment/PaddingTests.cs b/UnitTests/View/Adornment/PaddingTests.cs index a4defc9972..0242bcc1da 100644 --- a/UnitTests/View/Adornment/PaddingTests.cs +++ b/UnitTests/View/Adornment/PaddingTests.cs @@ -33,7 +33,5 @@ P P output ); TestHelpers.AssertDriverAttributesAre ("0", output, null, view.GetNormalColor ()); - - ((FakeDriver)Application.Driver!).End (); } } From 63a755d6926f8010ea4becea94ed4eb471939f18 Mon Sep 17 00:00:00 2001 From: BDisp Date: Tue, 19 Nov 2024 22:16:45 +0000 Subject: [PATCH 10/13] Remove unnecessary TabToRender class. --- Terminal.Gui/Views/TabView/TabRowView.cs | 18 +++++++++--------- Terminal.Gui/Views/TabView/TabToRender.cs | 17 ----------------- Terminal.Gui/Views/TabView/TabView.cs | 19 +++++++++---------- 3 files changed, 18 insertions(+), 36 deletions(-) delete mode 100644 Terminal.Gui/Views/TabView/TabToRender.cs diff --git a/Terminal.Gui/Views/TabView/TabRowView.cs b/Terminal.Gui/Views/TabView/TabRowView.cs index afee3fa5b2..4ae96614c7 100644 --- a/Terminal.Gui/Views/TabView/TabRowView.cs +++ b/Terminal.Gui/Views/TabView/TabRowView.cs @@ -143,17 +143,17 @@ private void RenderTabLineCanvas () return; } - TabToRender [] tabLocations = _host._tabLocations; + Tab [] tabLocations = _host._tabLocations; int selectedTab = -1; var lc = new LineCanvas (); for (var i = 0; i < tabLocations.Length; i++) { - View tab = tabLocations [i].Tab; + View tab = tabLocations [i]; Rectangle vts = tab.ViewportToScreen (tab.Viewport); - int selectedOffset = _host.Style.ShowTopLine && tabLocations [i].IsSelected ? 0 : 1; + int selectedOffset = _host.Style.ShowTopLine && tabLocations [i] == _host.SelectedTab ? 0 : 1; - if (tabLocations [i].IsSelected) + if (tabLocations [i] == _host.SelectedTab) { selectedTab = i; @@ -691,11 +691,11 @@ private void RenderTabLine () View? selected = null; int topLine = _host.Style.ShowTopLine ? 1 : 0; - foreach (TabToRender toRender in _host._tabLocations) + foreach (Tab toRender in _host._tabLocations) { - Tab tab = toRender.Tab; + Tab tab = toRender; - if (toRender.IsSelected) + if (toRender == _host.SelectedTab) { selected = tab; @@ -748,7 +748,7 @@ private void RenderUnderline () { int y = GetUnderlineYPosition (); - TabToRender? selected = _host._tabLocations?.FirstOrDefault (t => t.IsSelected); + Tab? selected = _host._tabLocations?.FirstOrDefault (t => t == _host.SelectedTab); if (selected is null) { @@ -792,5 +792,5 @@ private void RenderUnderline () } } - private bool ShouldDrawRightScrollIndicator () { return _host._tabLocations!.LastOrDefault ()?.Tab != _host.Tabs.LastOrDefault (); } + private bool ShouldDrawRightScrollIndicator () { return _host._tabLocations!.LastOrDefault () != _host.Tabs.LastOrDefault (); } } diff --git a/Terminal.Gui/Views/TabView/TabToRender.cs b/Terminal.Gui/Views/TabView/TabToRender.cs deleted file mode 100644 index d29930a9e2..0000000000 --- a/Terminal.Gui/Views/TabView/TabToRender.cs +++ /dev/null @@ -1,17 +0,0 @@ -#nullable enable -namespace Terminal.Gui; - -internal class TabToRender -{ - public TabToRender (Tab tab, bool isSelected) - { - Tab = tab; - IsSelected = isSelected; - } - - /// True if the tab that is being rendered is the selected one. - /// - public bool IsSelected { get; } - - public Tab Tab { get; } -} diff --git a/Terminal.Gui/Views/TabView/TabView.cs b/Terminal.Gui/Views/TabView/TabView.cs index 627e251fdf..324ba01def 100644 --- a/Terminal.Gui/Views/TabView/TabView.cs +++ b/Terminal.Gui/Views/TabView/TabView.cs @@ -20,8 +20,7 @@ public class TabView : View private Tab? _selectedTab; - // BUGBUG: Horrible containment design. - internal TabToRender []? _tabLocations; + internal Tab []? _tabLocations; private int _tabScrollOffset; /// Initializes a class. @@ -287,7 +286,7 @@ public void EnsureSelectedTabIsVisible () } // if current viewport does not include the selected tab - if (!CalculateViewport (Viewport).Any (r => Equals (SelectedTab, r.Tab))) + if (!CalculateViewport (Viewport).Any (t => Equals (SelectedTab, t))) { // Set scroll offset so the first tab rendered is the TabScrollOffset = Math.Max (0, Tabs.IndexOf (SelectedTab)); @@ -428,7 +427,7 @@ protected virtual void OnSelectedTabChanged (Tab oldTab, Tab newTab) /// Returns which tabs to render at each x location. /// - internal IEnumerable CalculateViewport (Rectangle bounds) + internal IEnumerable CalculateViewport (Rectangle bounds) { UnSetCurrentTabs (); @@ -467,7 +466,7 @@ internal IEnumerable CalculateViewport (Rectangle bounds) tab.MouseClick += Tab_MouseClick!; tab.Border!.MouseClick += Tab_MouseClick!; - yield return new (tab, Equals (SelectedTab, tab)); + yield return tab; break; } @@ -498,7 +497,7 @@ internal IEnumerable CalculateViewport (Rectangle bounds) tab.MouseClick += Tab_MouseClick!; tab.Border!.MouseClick += Tab_MouseClick!; - yield return new (tab, Equals (SelectedTab, tab)); + yield return tab; prevTab = tab; @@ -557,11 +556,11 @@ private void UnSetCurrentTabs () } else if (_tabLocations is { }) { - foreach (TabToRender tabToRender in _tabLocations) + foreach (Tab tabToRender in _tabLocations) { - tabToRender.Tab.MouseClick -= Tab_MouseClick!; - tabToRender.Tab.Border!.MouseClick -= Tab_MouseClick!; - tabToRender.Tab.Visible = false; + tabToRender.MouseClick -= Tab_MouseClick!; + tabToRender.Border!.MouseClick -= Tab_MouseClick!; + tabToRender.Visible = false; } _tabLocations = null; From 6cb8478b6e32c1605db5d7342f495281aef0a5dd Mon Sep 17 00:00:00 2001 From: BDisp Date: Wed, 20 Nov 2024 12:44:36 +0000 Subject: [PATCH 11/13] Only render tabs if layout is needed. --- Terminal.Gui/Views/TabView/TabRowView.cs | 12 ++++++------ Terminal.Gui/Views/TabView/TabView.cs | 5 +++++ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/Terminal.Gui/Views/TabView/TabRowView.cs b/Terminal.Gui/Views/TabView/TabRowView.cs index 4ae96614c7..60bfbba55e 100644 --- a/Terminal.Gui/Views/TabView/TabRowView.cs +++ b/Terminal.Gui/Views/TabView/TabRowView.cs @@ -86,15 +86,12 @@ protected override bool OnMouseEvent (MouseEventArgs me) { _host.SwitchTabBy (scrollIndicatorHit); - SetNeedsLayout (); - return true; } if (hit is { }) { _host.SelectedTab = hit; - SetNeedsLayout (); return true; } @@ -119,11 +116,14 @@ protected override void OnHasFocusChanged (bool newHasFocus, View? previousFocus /// protected override void OnSubviewLayout (LayoutEventArgs args) { - _host._tabLocations = _host.CalculateViewport (Viewport).ToArray (); + if (NeedsLayout) + { + _host._tabLocations = _host.CalculateViewport (Viewport).ToArray (); - RenderTabLine (); + RenderTabLine (); - RenderUnderline (); + RenderUnderline (); + } base.OnSubviewLayout (args); } diff --git a/Terminal.Gui/Views/TabView/TabView.cs b/Terminal.Gui/Views/TabView/TabView.cs index 324ba01def..9660992378 100644 --- a/Terminal.Gui/Views/TabView/TabView.cs +++ b/Terminal.Gui/Views/TabView/TabView.cs @@ -109,6 +109,11 @@ public Tab? SelectedTab get => _selectedTab; set { + if (value == _selectedTab) + { + return; + } + Tab? old = _selectedTab; _selectedTabHasFocus = old is { } && (old.HasFocus || !_containerView.CanFocus); From be0832febb3670fc528667811f35c45ce2900538 Mon Sep 17 00:00:00 2001 From: BDisp Date: Wed, 20 Nov 2024 12:52:25 +0000 Subject: [PATCH 12/13] Remove unnecessary conditional statement. --- Terminal.Gui/Views/TabView/TabRowView.cs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/Terminal.Gui/Views/TabView/TabRowView.cs b/Terminal.Gui/Views/TabView/TabRowView.cs index 60bfbba55e..b1ec596ecc 100644 --- a/Terminal.Gui/Views/TabView/TabRowView.cs +++ b/Terminal.Gui/Views/TabView/TabRowView.cs @@ -113,17 +113,14 @@ protected override void OnHasFocusChanged (bool newHasFocus, View? previousFocus base.OnHasFocusChanged (newHasFocus, previousFocusedView, focusedView); } - /// + /// protected override void OnSubviewLayout (LayoutEventArgs args) { - if (NeedsLayout) - { - _host._tabLocations = _host.CalculateViewport (Viewport).ToArray (); + _host._tabLocations = _host.CalculateViewport (Viewport).ToArray (); - RenderTabLine (); + RenderTabLine (); - RenderUnderline (); - } + RenderUnderline (); base.OnSubviewLayout (args); } From 8c910fa04d8ee1bb8a925c068f1a170e48c99aad Mon Sep 17 00:00:00 2001 From: BDisp Date: Wed, 20 Nov 2024 14:30:36 +0000 Subject: [PATCH 13/13] Ensure tab get focus if the container can't get focus. --- Terminal.Gui/Views/TabView/TabView.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Terminal.Gui/Views/TabView/TabView.cs b/Terminal.Gui/Views/TabView/TabView.cs index 9660992378..7916911fcd 100644 --- a/Terminal.Gui/Views/TabView/TabView.cs +++ b/Terminal.Gui/Views/TabView/TabView.cs @@ -142,7 +142,7 @@ public Tab? SelectedTab if (old != _selectedTab) { - if (_selectedTabHasFocus || !_containerView.CanFocus) + if (TabCanSetFocus ()) { SelectedTab?.SetFocus (); } @@ -153,6 +153,11 @@ public Tab? SelectedTab } } + 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; @@ -509,7 +514,7 @@ internal IEnumerable CalculateViewport (Rectangle bounds) i += tabTextWidth + 1; } - if (_selectedTabHasFocus) + if (TabCanSetFocus ()) { SelectedTab?.SetFocus (); }