Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions src/WinUIRichEditor/Controls/RichEditor.BlockSelection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ internal static PointerTarget ChoosePointerTarget(
return PointerTarget.None;
}

private bool TryBeginImageInteraction(Point pt, PointerRoutedEventArgs e)
private bool TryBeginImageInteraction(Point pt, PointerStep s)
{
// Hit-test everything first, then let ChoosePointerTarget decide. The drag is seeded from the
// rect the handle was DRAWN at, for both registries — see _cellImageRects for why.
Expand Down Expand Up @@ -193,12 +193,12 @@ void SelectObject(ImageBlock? block, (Paragraph p, InlineImage img)? inline)
case PointerTarget.SelectedBlockImageHandle:
case PointerTarget.SelectedInlineImageHandle:
BeginImageResize(handle.grip, handle.rect, handle.inline, pt);
_canvas.CapturePointer(e.Pointer);
s.Capture.Capture();
return true;
// A press on the picture itself selects it and arms dragging it (RichEditor.DragBlock.cs).
case PointerTarget.CellImage: SelectObject(cellImage, null); ArmObjectDrag(cellImage, pt, e); return true;
case PointerTarget.InlineImage: SelectObject(null, inlineImage); ArmObjectDrag(inlineImage!.Value.img, pt, e); return true;
case PointerTarget.BlockImage: SelectObject(blockImage, null); ArmObjectDrag(blockImage, pt, e); return true;
case PointerTarget.CellImage: SelectObject(cellImage, null); ArmObjectDrag(cellImage, pt, s); return true;
case PointerTarget.InlineImage: SelectObject(null, inlineImage); ArmObjectDrag(inlineImage!.Value.img, pt, s); return true;
case PointerTarget.BlockImage: SelectObject(blockImage, null); ArmObjectDrag(blockImage, pt, s); return true;
default: return false;
}
}
Expand Down Expand Up @@ -275,11 +275,11 @@ private bool TryResizeImage(Point pt)
return true;
}

private bool EndImageResize(PointerRoutedEventArgs e)
private bool EndImageResize(PointerStep s)
{
if (_resizingImage == null && _resizingInline == null) return false;
FinishImageResize(); // before the release (see EndColumnResize)
_canvas.ReleasePointerCapture(e.Pointer);
s.Capture.Release();
return true;
}

Expand Down
18 changes: 10 additions & 8 deletions src/WinUIRichEditor/Controls/RichEditor.DragBlock.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ public partial class RichEditor
private bool DropPreviewActive => _dragTextActive || _dragObjectActive;

// A press that has just selected an object: arm its drag and take the pointer.
private void ArmObjectDrag(object? obj, Point docPt, PointerRoutedEventArgs e)
private void ArmObjectDrag(object? obj, Point docPt, PointerStep s)
{
if (ArmObjectDragAt(obj, docPt)) _canvas.CapturePointer(e.Pointer);
if (ArmObjectDragAt(obj, docPt)) s.Capture.Capture();
}

// The press minus its pointer capture. Editing only: a viewer's press selects the object for Copy.
Expand All @@ -41,7 +41,7 @@ internal bool ArmObjectDragAt(object? obj, Point docPt)

// Pointer move while armed: past the slop the drag goes live and the drop caret follows the pointer. A
// point the object may not go to (a move into its own cells) shows no caret and the no-drop cursor.
internal void DragObjectMoved(Point docPt)
internal void DragObjectMoved(Point docPt, bool ctrl)
{
if (_dragObject == null) return;
_dragObjectLast = docPt;
Expand All @@ -51,15 +51,15 @@ internal void DragObjectMoved(Point docPt)
_dragObjectActive = true;
}
var tp = GetPositionFromPoint(docPt);
_dropPreview = tp != null && CanDropObject(_dragObject, tp, copy: Ctrl) ? tp : null;
_dropPreview = tp != null && CanDropObject(_dragObject, tp, copy: ctrl) ? tp : null;
SetCursorShape(_dropPreview != null ? InputSystemCursorShape.Arrow : InputSystemCursorShape.UniversalNo);
InvalidateCanvas();
}

private void EndObjectDrag(PointerRoutedEventArgs e)
private void EndObjectDrag(PointerStep s)
{
FinishObjectDrag(copy: Ctrl);
_canvas.ReleasePointerCapture(e.Pointer); // after the drag is cleared, so CaptureLost finds nothing live
FinishObjectDrag(copy: s.Ctrl);
s.Capture.Release(); // after the drag is cleared, so CaptureLost finds nothing live
}

// The release minus its pointer capture. Ctrl is read at the DROP, as for text. Returns whether the
Expand Down Expand Up @@ -90,7 +90,9 @@ private void CancelObjectDrag()
// the point is a valid drop at all (a copy may go into its own cells, a move may not).
private void OnDragModifierChanged()
{
if (_dragObjectActive) DragObjectMoved(_dragObjectLast);
// Driven by the KEY handlers (Ctrl pressed/released mid-drag), so the live keyboard state is the
// right source here — unlike the pointer path, which carries the modifiers on its PointerStep.
if (_dragObjectActive) DragObjectMoved(_dragObjectLast, Ctrl);
else if (_dragTextActive) InvalidateCanvas();
}

Expand Down
14 changes: 7 additions & 7 deletions src/WinUIRichEditor/Controls/RichEditor.DragText.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,14 @@ private bool CanArmTextDragAt(TextPointer tp)
}

// Arms the drag; the caller has checked CanArmTextDragAt (via ChooseTextPress).
private void ArmTextDrag(Point docPt, PointerRoutedEventArgs e)
private void ArmTextDrag(Point docPt, PointerStep s)
{
_dragTextArmed = true;
_dragTextActive = false;
_dragTextStart = docPt;
_dragTextPressCtrl = Ctrl;
_dragTextPressCtrl = s.Ctrl;
_dropPreview = null;
_canvas.CapturePointer(e.Pointer);
s.Capture.Capture();
}

// Pointer move while armed: past the slop the drag activates and the drop preview follows the pointer.
Expand All @@ -70,21 +70,21 @@ private void DragTextMoved(Point docPt)
}

// Pointer release: a real drag performs the move/copy; an unmoved press is the deferred plain click.
private void EndTextDrag(PointerRoutedEventArgs e)
private void EndTextDrag(PointerStep s)
{
bool wasDrag = _dragTextActive;
var drop = _dropPreview;
_dragTextArmed = false;
_dragTextActive = false;
_dropPreview = null;
_canvas.ReleasePointerCapture(e.Pointer);
s.Capture.Release();
SetCursorShape(InputSystemCursorShape.IBeam);

if (!wasDrag)
{
// The press deferred the usual click handling (so the selection survived a potential drag);
// do it now: caret to the click point, selection collapsed.
var pt = ViewToDoc(e.GetCurrentPoint(_canvas).Position);
var pt = ViewToDoc(s.ViewPos);
if (GetPositionFromPoint(pt) is { Paragraph: not null } tp)
{
_caret = tp;
Expand All @@ -106,7 +106,7 @@ private void EndTextDrag(PointerRoutedEventArgs e)
}

if (drop?.Paragraph == null) { InvalidateCanvas(); return; }
PerformTextDrop(drop, copy: Ctrl);
PerformTextDrop(drop, copy: s.Ctrl);
}

// Moves (or, with copy, duplicates) the selected content to `drop`. The move deletes the selection
Expand Down
76 changes: 46 additions & 30 deletions src/WinUIRichEditor/Controls/RichEditor.Input.cs
Original file line number Diff line number Diff line change
Expand Up @@ -301,27 +301,31 @@ private static void WireBlockParents(Block block, object parent)
internal bool BeginResizeDragAt(Point pt)
=> BeginImageResizeAt(pt) || BeginColumnResizeAt(pt) || BeginRowResizeAt(pt);

private void OnCanvasPointerPressed(object sender, PointerRoutedEventArgs e)
private void OnCanvasPointerPressed(object sender, PointerRoutedEventArgs e) => PointerPressedCore(StepFrom(e));

/// <summary>The press, driven by a <see cref="PointerStep"/> so a test can run a whole gesture in
/// order (see RichEditor.PointerPipeline.cs). The handler above only adapts the event.</summary>
internal void PointerPressedCore(PointerStep s)
{
_pressLink = null; // every press re-arms; a stale value must never fire on a later release
_canvas.Focus(FocusState.Pointer);
// Right button is handled by RightTapped (context menu); don't collapse the selection here.
if (e.GetCurrentPoint(_canvas).Properties.IsRightButtonPressed) return;
var ptp = ViewToDoc(e.GetCurrentPoint(_canvas).Position); // doc space (identity unless paged)
if (TableDrawPointerPressed(ptp, e)) return; // "draw table" mode: drag from the caret to size it
if (BeginResizeDragAt(ptp)) { _canvas.CapturePointer(e.Pointer); return; } // picture handle / table boundary
if (s.RightButton) return;
var ptp = ViewToDoc(s.ViewPos); // doc space (identity unless paged)
if (TableDrawPointerPressed(ptp, s)) return; // "draw table" mode: drag from the caret to size it
if (BeginResizeDragAt(ptp)) { s.Capture.Capture(); return; } // picture handle / table boundary
// Table left/top border -> select the whole table, and arm dragging it (RichEditor.DragBlock.cs).
if (TrySelectTableBlock(ptp)) { ArmObjectDrag(_selectedBlock, ptp, e); return; }
if (TrySelectInlineTable(ptp)) { ArmObjectDrag(_selectedInlineTable?.it, ptp, e); return; }
if (TryBeginImageInteraction(ptp, e)) return; // image resize handle or selection
if (TrySelectTableBlock(ptp)) { ArmObjectDrag(_selectedBlock, ptp, s); return; }
if (TrySelectInlineTable(ptp)) { ArmObjectDrag(_selectedInlineTable?.it, ptp, s); return; }
if (TryBeginImageInteraction(ptp, s)) return; // image resize handle or selection
ClearObjectSelection(); // any other press clears an image selection
var tp = GetPositionFromPoint(ptp);
if (tp == null) return;

var now = DateTime.UtcNow;
bool repeat = (now - _lastPressTime).TotalMilliseconds < _multiClickMs
&& Math.Abs(ptp.X - _lastPressPos.X) + Math.Abs(ptp.Y - _lastPressPos.Y) < MultiClickSlop;
var press = ChooseTextPress(Ctrl, Shift, repeat, CanArmTextDragAt(tp));
var press = ChooseTextPress(s.Ctrl, s.Shift, repeat, CanArmTextDragAt(tp));

// Ctrl+click on a hyperlink opens it (Word/browser convention); the caret still moves there.
if (press == TextPress.CtrlClick && tp.Paragraph != null)
Expand All @@ -342,7 +346,7 @@ private void OnCanvasPointerPressed(object sender, PointerRoutedEventArgs e)

// Read-only viewer: a PLAIN click on a link opens it (no Ctrl needed — browser convention).
// Armed here, launched on release only when no drag-selection happened in between.
if (IsReadOnly && !Shift && LinkRunAtPoint(ptp)?.NavigateUri is { Length: > 0 } roUri)
if (IsReadOnly && !s.Shift && LinkRunAtPoint(ptp)?.NavigateUri is { Length: > 0 } roUri)
_pressLink = (roUri, new Point(ptp.X, ptp.Y));

_clickCount = repeat ? _clickCount + 1 : 1;
Expand All @@ -351,17 +355,17 @@ private void OnCanvasPointerPressed(object sender, PointerRoutedEventArgs e)

// A single (non-repeat) press strictly inside the existing selection arms text drag & drop —
// caret/selection stay put; release decides click vs move/copy (RichEditor.DragText.cs).
if (press == TextPress.ArmDrag) { ArmTextDrag(new Point(ptp.X, ptp.Y), e); return; }
if (press == TextPress.ArmDrag) { ArmTextDrag(new Point(ptp.X, ptp.Y), s); return; }

_caret = tp;
_desiredCaretX = ptp.X;
_coalesceKey = null; // click starts a fresh undo group for subsequent typing
_pendingCaretStyles = null;
_canvas.CapturePointer(e.Pointer);
s.Capture.Capture();

// Double-click selects the word under the caret; triple-click (or more) selects the paragraph.
// No drag-select in these modes — keep the word/paragraph selection intact.
if (_clickCount >= 2 && !Shift && tp.Paragraph != null)
if (_clickCount >= 2 && !s.Shift && tp.Paragraph != null)
{
if (_clickCount == 2)
{
Expand All @@ -385,7 +389,7 @@ private void OnCanvasPointerPressed(object sender, PointerRoutedEventArgs e)
return;
}

if (Shift) { _selEnd = Clone(tp); }
if (s.Shift) { _selEnd = Clone(tp); }
else { _selStart = Clone(tp); _selEnd = Clone(tp); }
_isSelecting = true;
RestartBlink();
Expand All @@ -394,17 +398,20 @@ private void OnCanvasPointerPressed(object sender, PointerRoutedEventArgs e)
RaiseStatusChanged();
}

private void OnCanvasPointerMoved(object sender, PointerRoutedEventArgs e)
private void OnCanvasPointerMoved(object sender, PointerRoutedEventArgs e) => PointerMovedCore(StepFrom(e));

/// <summary>The move, driven by a <see cref="PointerStep"/> (see RichEditor.PointerPipeline.cs).</summary>
internal void PointerMovedCore(PointerStep s)
{
var vpos = e.GetCurrentPoint(_canvas).Position; // canvas (physical) coords
var vpos = s.ViewPos; // canvas (physical) coords
var pt = ViewToDoc(vpos); // doc space (identity unless paged)
if (TableDrawPointerMoved(pt)) return; // "draw table" mode: extend the rubber-band
if (_resizingColumn) { ResizeColumn(pt); return; }
if (_resizingRow) { ResizeRow(pt); return; }
if (_resizingImage != null || _resizingInline != null) { TryResizeImage(pt); return; }
if (_dragObject != null) { DragObjectMoved(pt); return; }
if (_dragObject != null) { DragObjectMoved(pt, s.Ctrl); return; }
if (_dragTextArmed) { DragTextMoved(pt); return; }
UpdateHoverCursor(pt);
UpdateHoverCursor(pt, s.Ctrl);
if (!_isSelecting) return;
var tp = GetPositionFromPoint(pt);
if (tp == null) return;
Expand Down Expand Up @@ -463,11 +470,11 @@ private void OnAutoScrollTick(object? sender, object e)
// ProtectedCursor is set on the editor (this control); it resolves up the tree for the inner canvas.
private InputSystemCursorShape _cursorShape = InputSystemCursorShape.IBeam;

private void UpdateHoverCursor(Point pt)
private void UpdateHoverCursor(Point pt, bool ctrl)
{
// Hand cursor over a hyperlink: always in a read-only viewer (plain click opens — browser
// convention); with Ctrl held when editable (Ctrl+click opens — Word convention).
if ((Ctrl || IsReadOnly) && LinkAtPoint(pt)) { SetCursorShape(InputSystemCursorShape.Hand); return; }
if ((ctrl || IsReadOnly) && LinkAtPoint(pt)) { SetCursorShape(InputSystemCursorShape.Hand); return; }
// Same order as the press: a selected picture's handle before a table boundary under it.
var grip = IsReadOnly ? ResizeGrip.None : SelectedGripAt(pt).grip;
if (grip != ResizeGrip.None) { SetCursorShape(GripCursor(grip)); return; }
Expand Down Expand Up @@ -507,7 +514,11 @@ private void OnCanvasPointerWheel(object sender, PointerRoutedEventArgs e)
// A normal release clears its own drag first (see EndColumnResize), so arriving after one is a no-op.
// Text drag & drop is left alone: ending it drops the text, and a lost capture is not a drop. An object
// drag is cancelled for the same reason — cancelled, not finished.
private void OnCanvasPointerCaptureLost(object sender, PointerRoutedEventArgs e) => EndPointerDrags();
private void OnCanvasPointerCaptureLost(object sender, PointerRoutedEventArgs e) => PointerCaptureLostCore();

/// <summary>What a lost capture runs. A test's <c>IPointerCapture.Release</c> calls this the way WinUI
/// does — synchronously (see RichEditor.PointerPipeline.cs).</summary>
internal void PointerCaptureLostCore() => EndPointerDrags();

private void EndPointerDrags()
{
Expand All @@ -519,24 +530,29 @@ private void EndPointerDrags()
if (_isSelecting) { _isSelecting = false; StopAutoScroll(); }
}

private void OnCanvasPointerReleased(object sender, PointerRoutedEventArgs e)
private void OnCanvasPointerReleased(object sender, PointerRoutedEventArgs e) => PointerReleasedCore(StepFrom(e));

/// <summary>The release, driven by a <see cref="PointerStep"/>. Each branch below finishes its work
/// BEFORE <c>s.Capture.Release()</c>, because that raises capture-lost synchronously and capture-lost
/// abandons whatever is live (see RichEditor.PointerPipeline.cs).</summary>
internal void PointerReleasedCore(PointerStep s)
{
if (TableDrawPointerReleased(e)) return; // "draw table" mode: insert at the caret, dragged size
if (EndColumnResize(e)) return;
if (EndRowResize(e)) return;
if (EndImageResize(e)) return;
if (_dragObject != null) { EndObjectDrag(e); return; }
if (_dragTextArmed) { EndTextDrag(e); return; }
if (TableDrawPointerReleased(s)) return; // "draw table" mode: insert at the caret, dragged size
if (EndColumnResize(s)) return;
if (EndRowResize(s)) return;
if (EndImageResize(s)) return;
if (_dragObject != null) { EndObjectDrag(s); return; }
if (_dragTextArmed) { EndTextDrag(s); return; }
_isSelecting = false;
StopAutoScroll();
_canvas.ReleasePointerCapture(e.Pointer);
s.Capture.Release();
if (IsFormatPainterActive) ApplyFormatPainterToSelection();
// Read-only link click: launch only if the press stayed a CLICK (no selection was dragged out
// and the pointer didn't travel) — a drag over link text selects, exactly like a browser.
if (_pressLink is { } pl)
{
_pressLink = null;
var rp = ViewToDoc(e.GetCurrentPoint(_canvas).Position);
var rp = ViewToDoc(s.ViewPos);
if (!HasSelection && Math.Abs(rp.X - pl.pos.X) + Math.Abs(rp.Y - pl.pos.Y) < MultiClickSlop)
_ = OpenUriAsync(pl.uri); // the URI captured at press, not a re-read of the caret
}
Expand Down
Loading
Loading