Skip to content

Commit 3d5f6ef

Browse files
committed
Add mouse drag support to SplitterControl with mouse capture
- Implement IMouseAwareControl on SplitterControl for click-and-drag column resizing via ProcessMouseEvent (press/drag/release lifecycle) - Add mouse capture mechanism to WindowEventDispatcher so drag/release events route to the originating control regardless of cursor position - Fix MouseEventArgs.HasFlag/HasAnyFlag to use bitwise checking instead of exact equality, supporting both X10 (separate list entries) and SGR (combined flag values) mouse formats - Place capture check before enter/leave tracking to prevent synthetic MouseEnter/MouseLeave events from interfering with active drags - Use WindowPosition for stable delta computation since the splitter physically moves during resize, making control-relative coords unreliable
1 parent 92a5849 commit 3d5f6ef

3 files changed

Lines changed: 125 additions & 5 deletions

File tree

SharpConsoleUI/Controls/SplitterControl.cs

Lines changed: 89 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,16 @@
1414
using System.Drawing;
1515
using Color = Spectre.Console.Color;
1616

17+
using SharpConsoleUI.Drivers;
18+
using SharpConsoleUI.Events;
1719
using SharpConsoleUI.Extensions;
1820
namespace SharpConsoleUI.Controls
1921
{
2022
/// <summary>
2123
/// A vertical splitter control that allows users to resize adjacent columns in a <see cref="HorizontalGridControl"/>.
2224
/// Supports keyboard-based resizing with arrow keys and provides visual feedback during focus and dragging.
2325
/// </summary>
24-
public class SplitterControl : IWindowControl, IInteractiveControl, IFocusableControl, IDOMPaintable
26+
public class SplitterControl : IWindowControl, IInteractiveControl, IFocusableControl, IDOMPaintable, IMouseAwareControl
2527
{
2628
private const int DEFAULT_WIDTH = 1;
2729
private const float MIN_COLUMN_PERCENTAGE = 0.1f; // Minimum 10% width for any column
@@ -39,6 +41,8 @@ public class SplitterControl : IWindowControl, IInteractiveControl, IFocusableCo
3941
private bool _hasFocus;
4042
private bool _isDragging;
4143
private bool _isEnabled = true;
44+
private bool _isMouseDragging = false;
45+
private int _lastMouseX = 0;
4246

4347
// References to the columns on either side of this splitter
4448
private ColumnContainer? _leftColumn;
@@ -239,6 +243,11 @@ public bool HasFocus
239243
/// </summary>
240244
public bool IsDragging => _isDragging;
241245

246+
/// <summary>
247+
/// Gets a value indicating whether the splitter is currently being dragged by the mouse.
248+
/// </summary>
249+
public bool IsMouseDragging => _isMouseDragging;
250+
242251
/// <inheritdoc/>
243252
public bool IsEnabled
244253
{
@@ -447,6 +456,84 @@ public void PaintDOM(CharacterBuffer buffer, LayoutRect bounds, LayoutRect clipR
447456

448457
#endregion
449458

459+
#region IMouseAwareControl Implementation
460+
461+
/// <inheritdoc/>
462+
public bool WantsMouseEvents => IsEnabled;
463+
464+
/// <inheritdoc/>
465+
public bool CanFocusWithMouse => true;
466+
467+
#pragma warning disable CS0067
468+
/// <inheritdoc/>
469+
public event EventHandler<MouseEventArgs>? MouseClick;
470+
471+
/// <inheritdoc/>
472+
public event EventHandler<MouseEventArgs>? MouseDoubleClick;
473+
474+
/// <inheritdoc/>
475+
public event EventHandler<MouseEventArgs>? MouseEnter;
476+
477+
/// <inheritdoc/>
478+
public event EventHandler<MouseEventArgs>? MouseLeave;
479+
480+
/// <inheritdoc/>
481+
public event EventHandler<MouseEventArgs>? MouseMove;
482+
#pragma warning restore CS0067
483+
484+
/// <inheritdoc/>
485+
public bool ProcessMouseEvent(MouseEventArgs args)
486+
{
487+
if (!IsEnabled) return false;
488+
489+
// Ignore synthetic enter/leave events — they carry the original press/drag
490+
// flags and would incorrectly start or process a drag.
491+
if (args.HasAnyFlag(MouseFlags.MouseEnter, MouseFlags.MouseLeave))
492+
return false;
493+
494+
// Use WindowPosition for delta computation because the splitter physically
495+
// moves as it resizes, making control-relative Position unreliable.
496+
int mouseX = args.WindowPosition.X;
497+
498+
// Handle drag-in-progress
499+
if (_isMouseDragging && args.HasAnyFlag(MouseFlags.Button1Dragged, MouseFlags.Button1Pressed))
500+
{
501+
int deltaX = mouseX - _lastMouseX;
502+
if (deltaX != 0 && _leftColumn != null && _rightColumn != null)
503+
{
504+
MoveSplitter(deltaX);
505+
_lastMouseX = mouseX;
506+
}
507+
args.Handled = true;
508+
return true;
509+
}
510+
511+
// Handle drag end
512+
if (args.HasFlag(MouseFlags.Button1Released) && _isMouseDragging)
513+
{
514+
_isMouseDragging = false;
515+
_isDragging = false;
516+
Container?.Invalidate(true);
517+
args.Handled = true;
518+
return true;
519+
}
520+
521+
// Handle press to start drag
522+
if (args.HasFlag(MouseFlags.Button1Pressed) && !_isMouseDragging)
523+
{
524+
_isMouseDragging = true;
525+
_lastMouseX = mouseX;
526+
_isDragging = true;
527+
Container?.Invalidate(true);
528+
args.Handled = true;
529+
return true;
530+
}
531+
532+
return false;
533+
}
534+
535+
#endregion
536+
450537
/// <summary>
451538
/// Sets the columns that this splitter will resize
452539
/// </summary>
@@ -480,6 +567,7 @@ public void SetFocus(bool focus, FocusReason reason = FocusReason.Programmatic)
480567
if (!focus && _isDragging)
481568
{
482569
_isDragging = false;
570+
_isMouseDragging = false;
483571
Container?.Invalidate(true); // Force redraw with normal colors
484572
}
485573

SharpConsoleUI/Events/MouseEventArgs.cs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,19 +60,24 @@ public MouseEventArgs(List<MouseFlags> flags, Point position, Point absolutePosi
6060
}
6161

6262
/// <summary>
63-
/// Convenience method to check if specific mouse flags are present
63+
/// Convenience method to check if specific mouse flags are present.
64+
/// Uses bitwise checking to support both separate list entries (X10)
65+
/// and combined flag values (SGR).
6466
/// </summary>
6567
public bool HasFlag(MouseFlags flag)
6668
{
67-
return Flags.Contains(flag);
69+
if (flag == MouseFlags.None) return false;
70+
return Flags.Any(f => (f & flag) == flag);
6871
}
6972

7073
/// <summary>
71-
/// Convenience method to check if any of the specified flags are present
74+
/// Convenience method to check if any of the specified flags are present.
75+
/// Uses bitwise checking to support both separate list entries (X10)
76+
/// and combined flag values (SGR).
7277
/// </summary>
7378
public bool HasAnyFlag(params MouseFlags[] flags)
7479
{
75-
return flags.Any(flag => Flags.Contains(flag));
80+
return flags.Any(flag => Flags.Any(f => (f & flag) == flag));
7681
}
7782

7883
/// <summary>

SharpConsoleUI/Windows/WindowEventDispatcher.cs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ public class WindowEventDispatcher
3434
private DateTime _lastClickTime;
3535
private Point _lastClickPosition;
3636

37+
// Mouse capture: routes drag/release events to the control that received Button1Pressed
38+
private IWindowControl? _mouseCaptureControl;
39+
3740
// Escape key tracking: remembers control for Tab restore after Escape
3841
private IInteractiveControl? _escapedFromControl;
3942

@@ -90,6 +93,26 @@ public bool ProcessMouseEvent(Events.MouseEventArgs args)
9093
// Ensure layout is current before processing mouse events
9194
UpdateControlLayout();
9295

96+
// Route drag/release events to the captured control (mouse capture).
97+
// This must run before enter/leave tracking to prevent synthetic
98+
// enter/leave events from interfering with active drags.
99+
// Include Button1Pressed because SGR mouse format uses Button1Pressed+ReportMousePosition
100+
// for drag events (without Button1Dragged flag).
101+
if (_mouseCaptureControl != null && args.HasAnyFlag(MouseFlags.Button1Pressed, MouseFlags.Button1Dragged, MouseFlags.Button1Released))
102+
{
103+
var capturedControl = _mouseCaptureControl;
104+
105+
if (args.HasFlag(MouseFlags.Button1Released))
106+
_mouseCaptureControl = null;
107+
108+
if (capturedControl is Controls.IMouseAwareControl mouseCapture && mouseCapture.WantsMouseEvents)
109+
{
110+
var controlPosition = GetControlRelativePosition(capturedControl, args.WindowPosition);
111+
var controlArgs = args.WithPosition(controlPosition);
112+
return mouseCapture.ProcessMouseEvent(controlArgs);
113+
}
114+
}
115+
93116
// Find the control at the current mouse position
94117
Controls.IWindowControl? currentControl = null;
95118
if (IsClickInWindowContent(args.WindowPosition))
@@ -201,6 +224,10 @@ public bool ProcessMouseEvent(Events.MouseEventArgs args)
201224
// Propagate mouse event to control if applicable
202225
if (targetControl != null && targetControl is Controls.IMouseAwareControl mouseAware && mouseAware.WantsMouseEvents)
203226
{
227+
// Set mouse capture on Button1Pressed so drag/release events route here
228+
if (args.HasFlag(MouseFlags.Button1Pressed))
229+
_mouseCaptureControl = targetControl;
230+
204231
var controlPosition = GetControlRelativePosition(targetControl, args.WindowPosition);
205232
var controlArgs = args.WithPosition(controlPosition);
206233

0 commit comments

Comments
 (0)