@@ -15,12 +15,191 @@ namespace SharpConsoleUI.Controls
1515{
1616 public partial class MultilineEditControl
1717 {
18+ /// <summary>
19+ /// Tests whether a mouse position falls on the vertical scrollbar column.
20+ /// </summary>
21+ private bool IsOnVerticalScrollbar ( int mouseX )
22+ {
23+ if ( ! _needsVerticalScrollbar ) return false ;
24+ int scrollbarX = _margin . Left + GetGutterWidth ( ) + _effectiveWidth ;
25+ return mouseX == scrollbarX ;
26+ }
27+
28+ /// <summary>
29+ /// Tests whether a mouse position falls on the horizontal scrollbar row.
30+ /// </summary>
31+ private bool IsOnHorizontalScrollbar ( int mouseY )
32+ {
33+ if ( ! _needsHorizontalScrollbar ) return false ;
34+ int scrollbarY = _margin . Top + _effectiveViewportHeight ;
35+ return mouseY == scrollbarY ;
36+ }
37+
1838 /// <inheritdoc/>
1939 public bool ProcessMouseEvent ( MouseEventArgs args )
2040 {
2141 if ( ! IsEnabled || ! WantsMouseEvents )
2242 return false ;
2343
44+ // --- Scrollbar drag-in-progress (must be checked before text drag) ---
45+
46+ if ( _isVerticalScrollbarDragging &&
47+ args . HasAnyFlag ( MouseFlags . Button1Dragged , MouseFlags . Button1Pressed ) )
48+ {
49+ var ( trackHeight , _, sbThumbHeight ) = GetVerticalScrollbarGeometry ( ) ;
50+ int deltaY = args . Position . Y - _verticalScrollbarDragStartY ;
51+ int totalLines = GetTotalWrappedLineCount ( ) ;
52+ int effectiveViewport = GetEffectiveViewportHeight ( ) ;
53+ int maxScroll = Math . Max ( 0 , totalLines - effectiveViewport ) ;
54+ int trackRange = Math . Max ( 1 , trackHeight - sbThumbHeight ) ;
55+ int newOffset = _verticalScrollbarDragStartOffset +
56+ ( int ) ( deltaY * ( double ) maxScroll / trackRange ) ;
57+ newOffset = Math . Clamp ( newOffset , 0 , maxScroll ) ;
58+ _skipUpdateScrollPositionsInRender = true ;
59+ _verticalScrollOffset = newOffset ;
60+ Container ? . Invalidate ( true ) ;
61+ return true ;
62+ }
63+
64+ if ( _isHorizontalScrollbarDragging &&
65+ args . HasAnyFlag ( MouseFlags . Button1Dragged , MouseFlags . Button1Pressed ) )
66+ {
67+ var ( trackWidth , _, sbThumbWidth ) = GetHorizontalScrollbarGeometry ( ) ;
68+ int deltaX = args . Position . X - _horizontalScrollbarDragStartX ;
69+ int maxLineLength = GetMaxLineLength ( ) ;
70+ int maxScroll = Math . Max ( 0 , maxLineLength - _effectiveWidth ) ;
71+ int trackRange = Math . Max ( 1 , trackWidth - sbThumbWidth ) ;
72+ int newOffset = _horizontalScrollbarDragStartOffset +
73+ ( int ) ( deltaX * ( double ) maxScroll / trackRange ) ;
74+ newOffset = Math . Clamp ( newOffset , 0 , maxScroll ) ;
75+ _horizontalScrollOffset = newOffset ;
76+ Container ? . Invalidate ( true ) ;
77+ return true ;
78+ }
79+
80+ // --- Scrollbar drag end ---
81+
82+ if ( args . HasFlag ( MouseFlags . Button1Released ) )
83+ {
84+ if ( _isVerticalScrollbarDragging || _isHorizontalScrollbarDragging )
85+ {
86+ _isVerticalScrollbarDragging = false ;
87+ _isHorizontalScrollbarDragging = false ;
88+ return true ;
89+ }
90+ }
91+
92+ // --- Scrollbar click detection (before text handling) ---
93+
94+ if ( args . HasFlag ( MouseFlags . Button1Pressed ) && _hasFocus )
95+ {
96+ // Vertical scrollbar interaction
97+ if ( IsOnVerticalScrollbar ( args . Position . X ) )
98+ {
99+ _scrollbarInteracted = true ;
100+ int relY = args . Position . Y - _margin . Top ;
101+ var ( trackHeight , sbThumbY , sbThumbHeight ) = GetVerticalScrollbarGeometry ( ) ;
102+ int effectiveViewport = trackHeight ;
103+ int totalLines = GetTotalWrappedLineCount ( ) ;
104+ int maxScroll = Math . Max ( 0 , totalLines - effectiveViewport ) ;
105+
106+ if ( relY >= 0 && relY < trackHeight )
107+ {
108+ if ( relY == 0 && _verticalScrollOffset > 0 )
109+ {
110+ // Arrow up
111+ _skipUpdateScrollPositionsInRender = true ;
112+ _verticalScrollOffset = Math . Max ( 0 , _verticalScrollOffset - ControlDefaults . DefaultScrollWheelLines ) ;
113+ Container ? . Invalidate ( true ) ;
114+ }
115+ else if ( relY == trackHeight - 1 && _verticalScrollOffset < maxScroll )
116+ {
117+ // Arrow down
118+ _skipUpdateScrollPositionsInRender = true ;
119+ _verticalScrollOffset = Math . Min ( maxScroll , _verticalScrollOffset + ControlDefaults . DefaultScrollWheelLines ) ;
120+ Container ? . Invalidate ( true ) ;
121+ }
122+ else if ( relY >= sbThumbY && relY < sbThumbY + sbThumbHeight )
123+ {
124+ // Thumb: start drag
125+ _isVerticalScrollbarDragging = true ;
126+ _verticalScrollbarDragStartY = args . Position . Y ;
127+ _verticalScrollbarDragStartOffset = _verticalScrollOffset ;
128+ }
129+ else if ( relY < sbThumbY )
130+ {
131+ // Track above thumb: page up
132+ _skipUpdateScrollPositionsInRender = true ;
133+ _verticalScrollOffset = Math . Max ( 0 , _verticalScrollOffset - effectiveViewport ) ;
134+ Container ? . Invalidate ( true ) ;
135+ }
136+ else
137+ {
138+ // Track below thumb: page down
139+ _skipUpdateScrollPositionsInRender = true ;
140+ _verticalScrollOffset = Math . Min ( maxScroll , _verticalScrollOffset + effectiveViewport ) ;
141+ Container ? . Invalidate ( true ) ;
142+ }
143+ return true ;
144+ }
145+ }
146+
147+ // Horizontal scrollbar interaction
148+ if ( IsOnHorizontalScrollbar ( args . Position . Y ) )
149+ {
150+ _scrollbarInteracted = true ;
151+ int relX = args . Position . X - _margin . Left - GetGutterWidth ( ) ;
152+ var ( trackWidth , sbThumbX , sbThumbWidth ) = GetHorizontalScrollbarGeometry ( ) ;
153+ int maxLineLength = GetMaxLineLength ( ) ;
154+ int maxScroll = Math . Max ( 0 , maxLineLength - _effectiveWidth ) ;
155+
156+ if ( relX >= 0 && relX < trackWidth )
157+ {
158+ if ( relX == 0 && _horizontalScrollOffset > 0 )
159+ {
160+ // Arrow left
161+ _horizontalScrollOffset = Math . Max ( 0 , _horizontalScrollOffset - ControlDefaults . DefaultScrollWheelLines ) ;
162+ Container ? . Invalidate ( true ) ;
163+ }
164+ else if ( relX == trackWidth - 1 && _horizontalScrollOffset < maxScroll )
165+ {
166+ // Arrow right
167+ _horizontalScrollOffset = Math . Min ( maxScroll , _horizontalScrollOffset + ControlDefaults . DefaultScrollWheelLines ) ;
168+ Container ? . Invalidate ( true ) ;
169+ }
170+ else if ( relX >= sbThumbX && relX < sbThumbX + sbThumbWidth )
171+ {
172+ // Thumb: start drag
173+ _isHorizontalScrollbarDragging = true ;
174+ _horizontalScrollbarDragStartX = args . Position . X ;
175+ _horizontalScrollbarDragStartOffset = _horizontalScrollOffset ;
176+ }
177+ else if ( relX < sbThumbX )
178+ {
179+ // Track left of thumb: page left
180+ _horizontalScrollOffset = Math . Max ( 0 , _horizontalScrollOffset - _effectiveWidth ) ;
181+ Container ? . Invalidate ( true ) ;
182+ }
183+ else
184+ {
185+ // Track right of thumb: page right
186+ _horizontalScrollOffset = Math . Min ( maxScroll , _horizontalScrollOffset + _effectiveWidth ) ;
187+ Container ? . Invalidate ( true ) ;
188+ }
189+ return true ;
190+ }
191+ }
192+ }
193+
194+ // Consume other mouse events on scrollbar areas to prevent text cursor positioning
195+ if ( args . HasAnyFlag ( MouseFlags . Button1Clicked , MouseFlags . Button1Released ,
196+ MouseFlags . Button1DoubleClicked , MouseFlags . Button1TripleClicked ,
197+ MouseFlags . Button1Dragged ) )
198+ {
199+ if ( IsOnVerticalScrollbar ( args . Position . X ) || IsOnHorizontalScrollbar ( args . Position . Y ) )
200+ return true ;
201+ }
202+
24203 // Triple-click: select entire line
25204 if ( args . HasFlag ( MouseFlags . Button1TripleClicked ) )
26205 {
@@ -93,6 +272,7 @@ public bool ProcessMouseEvent(MouseEventArgs args)
93272 else
94273 {
95274 // Fresh press — anchor the selection start
275+ _scrollbarInteracted = false ;
96276 _hasSelection = true ;
97277 _selectionStartX = _cursorX ;
98278 _selectionStartY = _cursorY ;
@@ -146,6 +326,7 @@ public bool ProcessMouseEvent(MouseEventArgs args)
146326 int scrollAmount = Math . Min ( ControlDefaults . DefaultScrollWheelLines , _verticalScrollOffset ) ;
147327 if ( scrollAmount > 0 )
148328 {
329+ _scrollbarInteracted = true ;
149330 _skipUpdateScrollPositionsInRender = true ;
150331 _verticalScrollOffset -= scrollAmount ;
151332 Container ? . Invalidate ( true ) ;
@@ -161,6 +342,7 @@ public bool ProcessMouseEvent(MouseEventArgs args)
161342 int scrollAmount = Math . Min ( ControlDefaults . DefaultScrollWheelLines , maxScroll - _verticalScrollOffset ) ;
162343 if ( scrollAmount > 0 )
163344 {
345+ _scrollbarInteracted = true ;
164346 _skipUpdateScrollPositionsInRender = true ;
165347 _verticalScrollOffset += scrollAmount ;
166348 Container ? . Invalidate ( true ) ;
@@ -213,6 +395,7 @@ private void PositionCursorFromMouseCore(int mouseX, int mouseY)
213395 /// </summary>
214396 private void PositionCursorFromMouse ( int mouseX , int mouseY )
215397 {
398+ _scrollbarInteracted = false ;
216399 PositionCursorFromMouseCore ( mouseX , mouseY ) ;
217400 ClearSelection ( ) ;
218401 EnsureCursorVisible ( ) ;
0 commit comments