Skip to content

Commit a9399ea

Browse files
committed
Fix NavigationView focus routing on mouse click and ScrollablePanel last-line clipping
Replace _navPaneHasFocus boolean with computed NavPaneHasFocus property derived from _navScrollPanel.HasFocus (single source of truth). Previously, mouse clicks bypassed the HasFocus setter and never updated _navPaneHasFocus, causing arrow keys to scroll the panel instead of moving selection. Expose FocusedContent on HorizontalGridControl. Refactor FocusNavPane and FocusContentPanel to toggle HasFocus directly on both panels. Fix ScrollablePanel CalculateContentHeight width mismatch: measure at full width first, re-measure at viewport-2 only when content overflows (matching the paint loop's 2-column scrollbar reservation). The previous -1 subtraction caused phantom scrollbars and last-line clipping.
1 parent 9b6216d commit a9399ea

3 files changed

Lines changed: 36 additions & 17 deletions

File tree

SharpConsoleUI/Controls/HorizontalGridControl/HorizontalGridControl.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,11 @@ public partial class HorizontalGridControl : BaseControl, IInteractiveControl, I
7676
private List<ColumnContainer> _columns = new List<ColumnContainer>();
7777
private readonly object _gridLock = new();
7878
private IInteractiveControl? _focusedContent;
79+
80+
/// <summary>
81+
/// Gets the currently focused child control within the grid.
82+
/// </summary>
83+
public IInteractiveControl? FocusedContent => _focusedContent;
7984
private bool _hasFocus;
8085
private Dictionary<IInteractiveControl, ColumnContainer> _interactiveContents = new Dictionary<IInteractiveControl, ColumnContainer>();
8186
private bool _interactiveContentsDirty = true;

SharpConsoleUI/Controls/NavigationView/NavigationView.Input.cs

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,11 @@ public bool ProcessMouseEvent(MouseEventArgs args)
5353
#region IInteractiveControl / IFocusableControl Implementation
5454

5555
private bool _hasFocus;
56-
private bool _navPaneHasFocus;
56+
57+
/// <summary>
58+
/// Whether the nav pane has focus — derived from the scroll panel's own focus state (single source of truth).
59+
/// </summary>
60+
private bool NavPaneHasFocus => _navScrollPanel.HasFocus;
5761

5862
/// <inheritdoc/>
5963
public bool HasFocus
@@ -72,7 +76,6 @@ public bool HasFocus
7276
}
7377
else if (!value && hadFocus)
7478
{
75-
_navPaneHasFocus = false;
7679
_grid.HasFocus = false;
7780
LostFocus?.Invoke(this, EventArgs.Empty);
7881
}
@@ -120,7 +123,7 @@ public void SetFocusWithDirection(bool focus, bool backward)
120123
/// <inheritdoc/>
121124
public bool ProcessKey(ConsoleKeyInfo key)
122125
{
123-
if (_navPaneHasFocus)
126+
if (NavPaneHasFocus)
124127
return ProcessNavPaneKey(key);
125128
else
126129
return ProcessContentPanelKey(key);
@@ -136,10 +139,13 @@ public void NotifyChildFocusChanged(IInteractiveControl child, bool hasFocus)
136139
// The grid is our only child — propagate focus tracking upward
137140
if (child == _grid || child is HorizontalGridControl)
138141
{
139-
if (hasFocus && !_hasFocus)
142+
if (hasFocus)
140143
{
141-
_hasFocus = true;
142-
GotFocus?.Invoke(this, EventArgs.Empty);
144+
if (!_hasFocus)
145+
{
146+
_hasFocus = true;
147+
GotFocus?.Invoke(this, EventArgs.Empty);
148+
}
143149
}
144150
else if (!hasFocus && _hasFocus)
145151
{
@@ -399,18 +405,17 @@ private void FireItemInvoked()
399405

400406
private void FocusNavPane()
401407
{
402-
_navPaneHasFocus = true;
403-
// Remove focus from content panel
408+
// Remove focus from content panel and set it on the nav scroll panel
404409
if (_contentPanel is IFocusableControl fc)
405410
fc.HasFocus = false;
406-
_grid.HasFocus = false;
411+
_navScrollPanel.HasFocus = true;
407412
Container?.Invalidate(true);
408413
}
409414

410415
private void FocusContentPanel()
411416
{
412-
_navPaneHasFocus = false;
413-
// Set focus on the content panel
417+
// Remove focus from nav panel and set it on the content panel
418+
_navScrollPanel.HasFocus = false;
414419
if (_contentPanel is IFocusableControl fc)
415420
fc.HasFocus = true;
416421
Container?.Invalidate(true);

SharpConsoleUI/Controls/ScrollablePanelControl/ScrollablePanelControl.Rendering.cs

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -477,16 +477,25 @@ private void DrawBottomBorder(CharacterBuffer buffer, int x, int y, int width, L
477477

478478
private int CalculateContentHeight(int viewportWidth, int maxHeight = 0)
479479
{
480-
int availableWidth = viewportWidth;
481-
int maxH = maxHeight > 0 ? maxHeight : int.MaxValue;
480+
// Measure at full width first, then re-measure at reduced width only if
481+
// the content actually overflows and a scrollbar will appear.
482+
int fullHeight = MeasureChildrenHeight(viewportWidth, maxHeight);
482483

483-
// Reserve space for scrollbar if we might need it
484-
// This is an approximation - we'll recalculate if needed
485-
if (_showScrollbar && _verticalScrollMode == ScrollMode.Scroll)
484+
int viewportH = maxHeight > 0 ? maxHeight : _viewportHeight;
485+
if (_showScrollbar && _verticalScrollMode == ScrollMode.Scroll && fullHeight > viewportH)
486486
{
487-
availableWidth = Math.Max(1, viewportWidth - 1);
487+
int narrowWidth = Math.Max(1, viewportWidth - 2);
488+
if (narrowWidth != viewportWidth)
489+
return MeasureChildrenHeight(narrowWidth, maxHeight);
488490
}
489491

492+
return fullHeight;
493+
}
494+
495+
private int MeasureChildrenHeight(int availableWidth, int maxHeight)
496+
{
497+
int maxH = maxHeight > 0 ? maxHeight : int.MaxValue;
498+
490499
List<IWindowControl> calcSnapshot;
491500
lock (_childrenLock) { calcSnapshot = new List<IWindowControl>(_children); }
492501

0 commit comments

Comments
 (0)