Skip to content

Commit 993c808

Browse files
committed
Fix ScrollablePanelControl Fill layout and TabControl focus leak on tab switch
ScrollablePanelControl: use two-pass measurement for children — measure non-Fill children first, then give Fill children the remaining space. Previously Fill children received the full viewport height, causing 1-row overflow when mixed with fixed-height children. TabControl: clear focus from controls inside the old tab's content tree when switching tabs. Walks the content subtree top-down via GetChildren() to find the focused control, since the Container chain is overwritten by TabControl's Container setter.
1 parent bc28898 commit 993c808

2 files changed

Lines changed: 76 additions & 11 deletions

File tree

SharpConsoleUI/Controls/ScrollablePanelControl.cs

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1309,6 +1309,24 @@ public override void PaintDOM(CharacterBuffer buffer, LayoutRect bounds, LayoutR
13091309

13101310
List<IWindowControl> paintSnapshot;
13111311
lock (_childrenLock) { paintSnapshot = new List<IWindowControl>(_children); }
1312+
1313+
// Two-pass: measure non-Fill children first to determine remaining space for Fill children.
1314+
int fixedHeight = 0;
1315+
if (_viewportHeight > 0)
1316+
{
1317+
foreach (var child in paintSnapshot)
1318+
{
1319+
if (!child.Visible || child.VerticalAlignment == VerticalAlignment.Fill) continue;
1320+
var node = LayoutNodeFactory.CreateSubtree(child);
1321+
node.IsVisible = true;
1322+
node.Measure(new LayoutConstraints(1, contentWidth, 1, int.MaxValue));
1323+
fixedHeight += node.DesiredSize.Height;
1324+
}
1325+
}
1326+
int fillCount = paintSnapshot.Count(c => c.Visible && c.VerticalAlignment == VerticalAlignment.Fill);
1327+
int perFillHeight = (_viewportHeight > 0 && fillCount > 0)
1328+
? Math.Max(0, (_viewportHeight - fixedHeight) / fillCount) : _viewportHeight;
1329+
13121330
foreach (var child in paintSnapshot)
13131331
{
13141332
if (!child.Visible) continue;
@@ -1318,10 +1336,10 @@ public override void PaintDOM(CharacterBuffer buffer, LayoutRect bounds, LayoutR
13181336
childNode.IsVisible = true;
13191337

13201338
// Measure using full layout pipeline.
1321-
// Fill-aligned children: cap to viewport so they fill the visible area.
1339+
// Fill-aligned children: get remaining space after fixed children.
13221340
// Content-sized children: measure unbounded for correct scroll positioning.
13231341
int maxChildHeight = (_viewportHeight > 0 && child.VerticalAlignment == VerticalAlignment.Fill)
1324-
? _viewportHeight : int.MaxValue;
1342+
? perFillHeight : int.MaxValue;
13251343
var constraints = new LayoutConstraints(1, contentWidth, 1, maxChildHeight);
13261344
childNode.Measure(constraints);
13271345
int childHeight = childNode.DesiredSize.Height;
@@ -1457,18 +1475,36 @@ private int CalculateContentHeight(int viewportWidth, int maxHeight = 0)
14571475
availableWidth = Math.Max(1, viewportWidth - 1);
14581476
}
14591477

1460-
int totalHeight = 0;
14611478
List<IWindowControl> calcSnapshot;
14621479
lock (_childrenLock) { calcSnapshot = new List<IWindowControl>(_children); }
1463-
foreach (var child in calcSnapshot.Where(c => c.Visible))
1480+
var visible = calcSnapshot.Where(c => c.Visible).ToList();
1481+
1482+
// Two-pass measurement: fixed children first, then Fill children get remaining space.
1483+
// Pass 1: measure non-Fill children to determine fixed height.
1484+
int fixedHeight = 0;
1485+
foreach (var child in visible)
1486+
{
1487+
if (child.VerticalAlignment == VerticalAlignment.Fill) continue;
1488+
var childNode = LayoutNodeFactory.CreateSubtree(child);
1489+
childNode.IsVisible = true;
1490+
var constraints = new LayoutConstraints(1, availableWidth, 1, int.MaxValue);
1491+
childNode.Measure(constraints);
1492+
fixedHeight += childNode.DesiredSize.Height;
1493+
}
1494+
1495+
// Pass 2: measure Fill children with remaining space.
1496+
int fillCount = visible.Count(c => c.VerticalAlignment == VerticalAlignment.Fill);
1497+
int remainingHeight = (maxH < int.MaxValue) ? Math.Max(0, maxH - fixedHeight) : int.MaxValue;
1498+
int perFillHeight = (fillCount > 0 && remainingHeight < int.MaxValue)
1499+
? Math.Max(0, remainingHeight / fillCount) : int.MaxValue;
1500+
1501+
int totalHeight = fixedHeight;
1502+
foreach (var child in visible)
14641503
{
1504+
if (child.VerticalAlignment != VerticalAlignment.Fill) continue;
14651505
var childNode = LayoutNodeFactory.CreateSubtree(child);
14661506
childNode.IsVisible = true;
1467-
// Fill-aligned children: measure with viewport constraint so they fill the visible area.
1468-
// Content-sized children: measure unbounded to get natural height for scroll range.
1469-
int childMaxH = (child.VerticalAlignment == VerticalAlignment.Fill && maxH < int.MaxValue)
1470-
? maxH : int.MaxValue;
1471-
var constraints = new LayoutConstraints(1, availableWidth, 1, childMaxH);
1507+
var constraints = new LayoutConstraints(1, availableWidth, 1, perFillHeight);
14721508
childNode.Measure(constraints);
14731509
totalHeight += childNode.DesiredSize.Height;
14741510
}

SharpConsoleUI/Controls/TabControl.cs

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
// License: MIT
77
// -----------------------------------------------------------------------
88

9+
using SharpConsoleUI.Core;
910
using SharpConsoleUI.Extensions;
1011
using SharpConsoleUI.Helpers;
1112
using SharpConsoleUI.Layout;
@@ -149,9 +150,18 @@ public int ActiveTabIndex
149150
// Re-validate in case state changed while we were outside the lock
150151
if (value >= 0 && value < _tabPages.Count)
151152
{
152-
// Toggle visibility
153+
// Toggle visibility and release focus from old tab's content
153154
if (_activeTabIndex >= 0 && _activeTabIndex < _tabPages.Count)
154-
_tabPages[_activeTabIndex].Content.Visible = false;
155+
{
156+
var oldContent = _tabPages[_activeTabIndex].Content;
157+
oldContent.Visible = false;
158+
159+
// If the focused control lives inside the old tab, clear focus
160+
var window = this.GetParentWindow();
161+
var focused = window?.FocusService?.FocusedControl as IWindowControl;
162+
if (focused != null && ContainsFocusedControl(oldContent, focused))
163+
window!.FocusService!.ClearControlFocus(FocusChangeReason.Programmatic);
164+
}
155165

156166
_activeTabIndex = value;
157167
_tabPages[_activeTabIndex].Content.Visible = true;
@@ -986,6 +996,25 @@ public void SetFocus(bool focus, FocusReason reason = FocusReason.Programmatic)
986996
#pragma warning restore CS0067
987997

988998
#endregion
999+
1000+
/// <summary>
1001+
/// Checks whether the target control exists anywhere in the subtree
1002+
/// rooted at the given ancestor, using top-down child enumeration.
1003+
/// </summary>
1004+
private static bool ContainsFocusedControl(IWindowControl root, IWindowControl target)
1005+
{
1006+
if (ReferenceEquals(root, target))
1007+
return true;
1008+
if (root is IContainerControl container)
1009+
{
1010+
foreach (var child in container.GetChildren())
1011+
{
1012+
if (ContainsFocusedControl(child, target))
1013+
return true;
1014+
}
1015+
}
1016+
return false;
1017+
}
9891018
}
9901019

9911020
/// <summary>

0 commit comments

Comments
 (0)