Skip to content

Commit 11197de

Browse files
committed
fix: scroll by 1 line per wheel tick and fix double-fire on scrollbar clicks
Change DefaultScrollWheelLines from 3 to 1 for TUI. Fix all controls to use the constant instead of hardcoded values. Split mouse handlers so Button1Pressed only initiates thumb drag while Button1Clicked handles arrows, track, and header sort to prevent double-firing.
1 parent fffebb0 commit 11197de

7 files changed

Lines changed: 125 additions & 55 deletions

File tree

SharpConsoleUI/Builders/ListBuilder.cs

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

9+
using SharpConsoleUI.Configuration;
910
using SharpConsoleUI.Controls;
1011
using SharpConsoleUI.DataBinding;
1112
using SharpConsoleUI.Events;
@@ -60,7 +61,7 @@ public sealed class ListBuilder : IControlBuilder<ListControl>
6061
private bool _hoverHighlightsItems = true;
6162
private bool _autoHighlightOnFocus = true;
6263
private ScrollbarVisibility _scrollbarVisibility = ScrollbarVisibility.Auto;
63-
private int _mouseWheelScrollSpeed = 3;
64+
private int _mouseWheelScrollSpeed = ControlDefaults.DefaultScrollWheelLines;
6465
private bool _doubleClickActivates = true;
6566
private int _doubleClickThresholdMs = 500;
6667

SharpConsoleUI/Configuration/ControlDefaults.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -198,9 +198,9 @@ public static class ControlDefaults
198198
public const int DefaultEditorViewportHeight = 10;
199199

200200
/// <summary>
201-
/// Number of lines to scroll per mouse wheel tick (default: 3)
201+
/// Number of lines to scroll per mouse wheel tick (default: 1)
202202
/// </summary>
203-
public const int DefaultScrollWheelLines = 3;
203+
public const int DefaultScrollWheelLines = 1;
204204

205205
/// <summary>
206206
/// Default tab size in spaces for multiline editor (default: 4)

SharpConsoleUI/Controls/ListControl/ListControl.Mouse.cs

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -223,8 +223,18 @@ public bool ProcessMouseEvent(MouseEventArgs args)
223223
}
224224
}
225225

226-
// Handle scrollbar click (for both Button1Clicked and Button1Pressed)
227-
if (mouseOnScrollbar && (args.HasFlag(MouseFlags.Button1Clicked) || args.HasFlag(MouseFlags.Button1Pressed)))
226+
// Handle scrollbar thumb drag initiation (needs Button1Pressed for responsive dragging)
227+
if (mouseOnScrollbar && args.HasFlag(MouseFlags.Button1Pressed))
228+
{
229+
if (!HasFocus && CanFocusWithMouse)
230+
this.GetParentWindow()?.FocusManager.SetFocus(this, FocusReason.Mouse);
231+
HandleScrollbarThumbPress(args);
232+
args.Handled = true;
233+
return true;
234+
}
235+
236+
// Handle scrollbar arrow/track clicks (Button1Clicked only to avoid double-firing)
237+
if (mouseOnScrollbar && args.HasFlag(MouseFlags.Button1Clicked))
228238
{
229239
if (!HasFocus && CanFocusWithMouse)
230240
this.GetParentWindow()?.FocusManager.SetFocus(this, FocusReason.Mouse);
@@ -383,6 +393,25 @@ private bool IsClickOnScrollbar(MouseEventArgs args)
383393
return (scrollbarStartY, Math.Max(0, scrollbarHeight));
384394
}
385395

396+
private void HandleScrollbarThumbPress(MouseEventArgs args)
397+
{
398+
int effectiveMaxVisibleItems = GetEffectiveVisibleItems();
399+
var (scrollbarStartY, scrollbarHeight) = GetScrollbarLayout();
400+
if (scrollbarHeight <= 0) return;
401+
402+
var (_, trackHeight, thumbY, thumbHeight) =
403+
ScrollbarHelper.GetVerticalGeometry(scrollbarHeight, _items.Count, effectiveMaxVisibleItems, _scrollOffset);
404+
405+
int relY = args.Position.Y - scrollbarStartY;
406+
var zone = ScrollbarHelper.HitTest(relY, trackHeight, thumbY, thumbHeight);
407+
if (zone == ScrollbarHitZone.Thumb)
408+
{
409+
_isScrollbarDragging = true;
410+
_scrollbarDragStartY = args.Position.Y;
411+
_scrollbarDragStartOffset = _scrollOffset;
412+
}
413+
}
414+
386415
private void HandleScrollbarClick(MouseEventArgs args)
387416
{
388417
int effectiveMaxVisibleItems = GetEffectiveVisibleItems();
@@ -404,11 +433,6 @@ private void HandleScrollbarClick(MouseEventArgs args)
404433
case ScrollbarHitZone.DownArrow:
405434
_scrollOffset = Math.Min(maxOffset, _scrollOffset + 1);
406435
break;
407-
case ScrollbarHitZone.Thumb:
408-
_isScrollbarDragging = true;
409-
_scrollbarDragStartY = args.Position.Y;
410-
_scrollbarDragStartOffset = _scrollOffset;
411-
break;
412436
case ScrollbarHitZone.TrackAbove:
413437
_scrollOffset = Math.Max(0, _scrollOffset - effectiveMaxVisibleItems);
414438
break;

SharpConsoleUI/Controls/ListControl/ListControl.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ public static Builders.ListBuilder Create()
7474
private bool _hoverHighlightsItems = true;
7575
private bool _autoHighlightOnFocus = true;
7676
private bool _truncationFade;
77-
private int _mouseWheelScrollSpeed = ControlDefaults.DefaultMinimumVisibleItems;
77+
private int _mouseWheelScrollSpeed = ControlDefaults.DefaultScrollWheelLines;
7878
private bool _doubleClickActivates = true;
7979
private int _doubleClickThresholdMs = ControlDefaults.DefaultDoubleClickThresholdMs;
8080

SharpConsoleUI/Controls/ScrollablePanelControl/ScrollablePanelControl.Mouse.cs

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,22 @@ public bool ProcessMouseEvent(MouseEventArgs args)
232232
int contentWidth = _viewportWidth - 2;
233233
bool isOnScrollbar = viewportX >= contentWidth;
234234

235+
// Thumb drag initiation (needs Button1Pressed for responsive dragging)
235236
if (isOnScrollbar && args.HasFlag(Drivers.MouseFlags.Button1Pressed))
237+
{
238+
int relY = args.Position.Y - Margin.Top - ContentInsetTop;
239+
if (relY >= sbThumbY && relY < sbThumbY + sbThumbHeight)
240+
{
241+
_isScrollbarDragging = true;
242+
_scrollbarDragStartY = args.Position.Y;
243+
_scrollbarDragStartOffset = _verticalScrollOffset;
244+
args.Handled = true;
245+
return true;
246+
}
247+
}
248+
249+
// Arrow and track clicks (Button1Clicked only to avoid double-firing)
250+
if (isOnScrollbar && args.HasFlag(Drivers.MouseFlags.Button1Clicked))
236251
{
237252
int relY = args.Position.Y - Margin.Top - ContentInsetTop;
238253
int maxScroll = Math.Max(0, _contentHeight - _viewportHeight);
@@ -247,19 +262,12 @@ public bool ProcessMouseEvent(MouseEventArgs args)
247262
// Arrow down
248263
ScrollVerticalBy(ControlDefaults.DefaultScrollWheelLines);
249264
}
250-
else if (relY >= sbThumbY && relY < sbThumbY + sbThumbHeight)
251-
{
252-
// Thumb: start drag
253-
_isScrollbarDragging = true;
254-
_scrollbarDragStartY = args.Position.Y;
255-
_scrollbarDragStartOffset = _verticalScrollOffset;
256-
}
257265
else if (relY < sbThumbY)
258266
{
259267
// Track above thumb: page up
260268
ScrollVerticalBy(-_viewportHeight);
261269
}
262-
else
270+
else if (relY >= sbThumbY + sbThumbHeight)
263271
{
264272
// Track below thumb: page down
265273
ScrollVerticalBy(_viewportHeight);
@@ -268,11 +276,11 @@ public bool ProcessMouseEvent(MouseEventArgs args)
268276
return true;
269277
}
270278

271-
if (isOnScrollbar && args.HasAnyFlag(Drivers.MouseFlags.Button1Clicked,
279+
if (isOnScrollbar && args.HasAnyFlag(
272280
Drivers.MouseFlags.Button1Released, Drivers.MouseFlags.Button1DoubleClicked,
273281
Drivers.MouseFlags.Button1TripleClicked))
274282
{
275-
// Consume scrollbar click events to prevent propagation to children
283+
// Consume scrollbar events to prevent propagation to children
276284
args.Handled = true;
277285
return true;
278286
}

SharpConsoleUI/Controls/TableControl/TableControl.Mouse.cs

Lines changed: 71 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@
66
// License: MIT
77
// -----------------------------------------------------------------------
88

9+
using SharpConsoleUI.Configuration;
910
using SharpConsoleUI.Drivers;
1011
using SharpConsoleUI.Events;
11-
1212
using SharpConsoleUI.Extensions;
1313
namespace SharpConsoleUI.Controls;
1414

@@ -124,7 +124,7 @@ public bool ProcessMouseEvent(MouseEventArgs args)
124124
if (args.HasFlag(MouseFlags.ButtonShift))
125125
{
126126
int oldH = _horizontalScrollOffset;
127-
_horizontalScrollOffset = Math.Max(0, _horizontalScrollOffset - 3);
127+
_horizontalScrollOffset = Math.Max(0, _horizontalScrollOffset - ControlDefaults.DefaultScrollWheelLines);
128128
if (_horizontalScrollOffset != oldH)
129129
{
130130
Container?.Invalidate(true);
@@ -134,7 +134,7 @@ public bool ProcessMouseEvent(MouseEventArgs args)
134134
}
135135

136136
int oldOffset = _scrollOffset;
137-
ScrollOffset = Math.Max(0, _scrollOffset - 3);
137+
ScrollOffset = Math.Max(0, _scrollOffset - ControlDefaults.DefaultScrollWheelLines);
138138
return _scrollOffset != oldOffset; // bubble if didn't scroll
139139
}
140140

@@ -143,7 +143,7 @@ public bool ProcessMouseEvent(MouseEventArgs args)
143143
if (args.HasFlag(MouseFlags.ButtonShift))
144144
{
145145
int oldH = _horizontalScrollOffset;
146-
_horizontalScrollOffset += 3;
146+
_horizontalScrollOffset += ControlDefaults.DefaultScrollWheelLines;
147147
if (_horizontalScrollOffset != oldH)
148148
{
149149
Container?.Invalidate(true);
@@ -154,12 +154,12 @@ public bool ProcessMouseEvent(MouseEventArgs args)
154154

155155
int oldOffset = _scrollOffset;
156156
int maxOffset = Math.Max(0, RowCount - GetVisibleRowCount());
157-
ScrollOffset = Math.Min(maxOffset, _scrollOffset + 3);
157+
ScrollOffset = Math.Min(maxOffset, _scrollOffset + ControlDefaults.DefaultScrollWheelLines);
158158
return _scrollOffset != oldOffset; // bubble if didn't scroll
159159
}
160160

161-
// Left-click
162-
if (args.HasFlag(MouseFlags.Button1Clicked) || args.HasFlag(MouseFlags.Button1Pressed))
161+
// Button1Pressed: handle operations that need immediate response (drag initiation)
162+
if (args.HasFlag(MouseFlags.Button1Pressed))
163163
{
164164
// Cancel any active cell edit before changing selection
165165
if (_isEditing)
@@ -169,25 +169,50 @@ public bool ProcessMouseEvent(MouseEventArgs args)
169169
if (!HasFocus && CanFocusWithMouse)
170170
this.GetParentWindow()?.FocusManager.SetFocus(this, FocusReason.Mouse);
171171

172-
// Check if clicking on scrollbar
172+
// Scrollbar thumb drag initiation
173173
if (IsClickOnVerticalScrollbar(args))
174174
{
175-
HandleVerticalScrollbarClick(args);
175+
HandleVerticalScrollbarThumbPress(args);
176176
return true;
177177
}
178178

179179
if (IsClickOnHorizontalScrollbar(args))
180180
{
181-
HandleHorizontalScrollbarClick(args);
181+
HandleHorizontalScrollbarThumbPress(args);
182182
return true;
183183
}
184184

185-
// Check if clicking on column resize border (only when not read-only)
185+
// Column resize initiation (only when not read-only)
186186
if (!_readOnly && _columnResizeEnabled && IsClickOnColumnBorder(args))
187187
{
188188
BeginColumnResize(args);
189189
return true;
190190
}
191+
}
192+
193+
// Button1Clicked: handle discrete click actions (arrows, track, sorting, selection)
194+
if (args.HasFlag(MouseFlags.Button1Clicked))
195+
{
196+
// Cancel any active cell edit before changing selection
197+
if (_isEditing)
198+
CancelEdit();
199+
200+
// Set focus on click
201+
if (!HasFocus && CanFocusWithMouse)
202+
this.GetParentWindow()?.FocusManager.SetFocus(this, FocusReason.Mouse);
203+
204+
// Scrollbar arrow/track clicks
205+
if (IsClickOnVerticalScrollbar(args))
206+
{
207+
HandleVerticalScrollbarClick(args);
208+
return true;
209+
}
210+
211+
if (IsClickOnHorizontalScrollbar(args))
212+
{
213+
HandleHorizontalScrollbarClick(args);
214+
return true;
215+
}
191216

192217
// Check if clicking on header (for sorting)
193218
if (IsClickOnHeader(args))
@@ -444,10 +469,24 @@ private int GetScrollbarDataStartY()
444469
return dataStartY;
445470
}
446471

472+
private void HandleVerticalScrollbarThumbPress(MouseEventArgs args)
473+
{
474+
int contentAreaHeight = GetScrollbarContentHeight();
475+
var (_, _, thumbY, thumbHeight) = GetVerticalScrollbarGeometry(contentAreaHeight);
476+
int relY = args.Position.Y - GetScrollbarDataStartY();
477+
478+
if (relY >= thumbY && relY < thumbY + thumbHeight)
479+
{
480+
_isVerticalScrollbarDragging = true;
481+
_scrollbarDragStartY = args.Position.Y;
482+
_scrollbarDragStartOffset = _scrollOffset;
483+
}
484+
}
485+
447486
private void HandleVerticalScrollbarClick(MouseEventArgs args)
448487
{
449488
int contentAreaHeight = GetScrollbarContentHeight();
450-
var (trackTop, trackHeight, thumbY, thumbHeight) = GetVerticalScrollbarGeometry(contentAreaHeight);
489+
var (_, trackHeight, thumbY, thumbHeight) = GetVerticalScrollbarGeometry(contentAreaHeight);
451490
int relY = args.Position.Y - GetScrollbarDataStartY();
452491
int maxOffset = Math.Max(0, RowCount - GetVisibleRowCount());
453492

@@ -461,19 +500,12 @@ private void HandleVerticalScrollbarClick(MouseEventArgs args)
461500
// Arrow down
462501
ScrollOffset = Math.Min(maxOffset, _scrollOffset + 1);
463502
}
464-
else if (relY >= thumbY && relY < thumbY + thumbHeight)
465-
{
466-
// Thumb: start drag
467-
_isVerticalScrollbarDragging = true;
468-
_scrollbarDragStartY = args.Position.Y;
469-
_scrollbarDragStartOffset = _scrollOffset;
470-
}
471503
else if (relY < thumbY)
472504
{
473505
// Track above thumb: page up
474506
ScrollOffset = Math.Max(0, _scrollOffset - GetVisibleRowCount());
475507
}
476-
else
508+
else if (relY >= thumbY + thumbHeight)
477509
{
478510
// Track below thumb: page down
479511
ScrollOffset = Math.Min(maxOffset, _scrollOffset + GetVisibleRowCount());
@@ -506,11 +538,28 @@ private int GetScrollbarContentWidth()
506538
return Math.Max(0, contentWidth);
507539
}
508540

541+
private void HandleHorizontalScrollbarThumbPress(MouseEventArgs args)
542+
{
543+
int contentWidth = GetScrollbarContentWidth();
544+
int totalColumnsWidth = GetTotalColumnsWidth();
545+
var (_, _, thumbX, thumbWidth) = GetHorizontalScrollbarGeometry(contentWidth, totalColumnsWidth);
546+
547+
int relX = args.Position.X - Margin.Left;
548+
if (_borderStyle != BorderStyle.None) relX--;
549+
550+
if (relX >= thumbX && relX < thumbX + thumbWidth)
551+
{
552+
_isHorizontalScrollbarDragging = true;
553+
_scrollbarDragStartX = args.Position.X;
554+
_scrollbarDragStartOffset = _horizontalScrollOffset;
555+
}
556+
}
557+
509558
private void HandleHorizontalScrollbarClick(MouseEventArgs args)
510559
{
511560
int contentWidth = GetScrollbarContentWidth();
512561
int totalColumnsWidth = GetTotalColumnsWidth();
513-
var (trackLeft, trackWidth, thumbX, thumbWidth) = GetHorizontalScrollbarGeometry(contentWidth, totalColumnsWidth);
562+
var (_, trackWidth, thumbX, thumbWidth) = GetHorizontalScrollbarGeometry(contentWidth, totalColumnsWidth);
514563

515564
int relX = args.Position.X - Margin.Left;
516565
if (_borderStyle != BorderStyle.None) relX--; // account for left border
@@ -526,19 +575,12 @@ private void HandleHorizontalScrollbarClick(MouseEventArgs args)
526575
// Arrow right
527576
_horizontalScrollOffset = Math.Min(maxHScroll, _horizontalScrollOffset + 1);
528577
}
529-
else if (relX >= thumbX && relX < thumbX + thumbWidth)
530-
{
531-
// Thumb: start drag
532-
_isHorizontalScrollbarDragging = true;
533-
_scrollbarDragStartX = args.Position.X;
534-
_scrollbarDragStartOffset = _horizontalScrollOffset;
535-
}
536578
else if (relX < thumbX)
537579
{
538580
// Track left of thumb: page left
539581
_horizontalScrollOffset = Math.Max(0, _horizontalScrollOffset - contentWidth);
540582
}
541-
else
583+
else if (relX >= thumbX + thumbWidth)
542584
{
543585
// Track right of thumb: page right
544586
_horizontalScrollOffset = Math.Min(maxHScroll, _horizontalScrollOffset + contentWidth);

SharpConsoleUI/Controls/TreeControl/TreeControl.Mouse.cs

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -293,11 +293,6 @@ public bool ProcessMouseEvent(MouseEventArgs args)
293293
case ScrollbarHitZone.DownArrow:
294294
_scrollOffset = Math.Min(maxOffset, _scrollOffset + 1);
295295
break;
296-
case ScrollbarHitZone.Thumb:
297-
_isScrollbarDragging = true;
298-
_scrollbarDragStartY = args.Position.Y;
299-
_scrollbarDragStartOffset = _scrollOffset;
300-
break;
301296
case ScrollbarHitZone.TrackAbove:
302297
_scrollOffset = Math.Max(0, _scrollOffset - effectiveVis);
303298
break;

0 commit comments

Comments
 (0)