feat(D.2b): chat text selection + Ctrl-C copy
Windows-like selection in the retail chat window: left-click-drag selects characters, Ctrl-C copies, Ctrl-A selects all. The selected span paints a translucent highlight behind the text. - UiElement.CapturesPointerDrag: a per-element opt-out so an interior drag is delivered to the widget (text selection) instead of moving/resizing the host window. UiRoot.OnMouseDown honours it AFTER edge-resize (a resizable window is still resizable from its frame) and BEFORE window-move. - UiChatView: AcceptsFocus + IsEditControl + CapturesPointerDrag; caches the OnDraw layout so OnEvent hit-tests the same geometry; HitChar maps a local point to (line,col) with glyph-midpoint caret snapping; SelectedText joins a multi-line span with \n; Ctrl-C writes to IKeyboard.ClipboardText (only when non-empty, so an empty copy never clobbers the clipboard). - UiHost exposes the wired IKeyboard (clipboard + Ctrl modifier state). Adversarial-review fix (the 99 tests would have stayed green without it): a coordinate-frame mismatch between MouseDown and MouseMove. UiRoot.OnMouseDown dispatched HitTestTopDown's coords, which are relative to the TOP-LEVEL child, while MouseMove/MouseUp use target.ScreenPosition. For the chat view inset at (8,8) inside its window the anchor landed ~8px off the click. OnMouseDown now delivers target-LOCAL coords like the other mouse events. Added a UiRoot regression test asserting MouseDown and MouseMove share the target-local frame for a nested child. Decomp ref: SurfaceWindow text/selection model; clipboard via Silk.NET IKeyboard.ClipboardText. Built with the chat-select-copy implement->review workflow. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
36bd3522f4
commit
4e60c03a74
7 changed files with 507 additions and 12 deletions
|
|
@ -197,7 +197,7 @@ public sealed class UiRoot : UiElement
|
|||
if (Modal is not null && !ContainsAbsolute(Modal, x, y))
|
||||
return;
|
||||
|
||||
var (target, lx, ly) = HitTestTopDown(x, y);
|
||||
var (target, _, _) = HitTestTopDown(x, y);
|
||||
if (target is null)
|
||||
{
|
||||
WorldMouseFallThrough?.Invoke(btn, x, y, flags);
|
||||
|
|
@ -218,6 +218,8 @@ public sealed class UiRoot : UiElement
|
|||
var edges = window.Resizable ? HitEdges(window, x, y, ResizeGrip) : ResizeEdges.None;
|
||||
if (edges != ResizeEdges.None)
|
||||
{
|
||||
// Edge resize still wins, even over a CapturesPointerDrag child:
|
||||
// a resizable chat window can be resized from its frame.
|
||||
_resizeTarget = window;
|
||||
_resizeEdges = edges;
|
||||
_resizeStartX = window.Left; _resizeStartY = window.Top;
|
||||
|
|
@ -225,6 +227,14 @@ public sealed class UiRoot : UiElement
|
|||
_resizeMouseX = x; _resizeMouseY = y;
|
||||
_dragCandidate = false;
|
||||
}
|
||||
else if (target.CapturesPointerDrag)
|
||||
{
|
||||
// The pressed widget owns interior drags (e.g. text selection):
|
||||
// do NOT move the ancestor window. The already-dispatched MouseDown
|
||||
// event + SetCapture(target) let the target drive its own drag via
|
||||
// the MouseMove events it receives while captured.
|
||||
_dragCandidate = false;
|
||||
}
|
||||
else if (window.Draggable)
|
||||
{
|
||||
_windowDragTarget = window;
|
||||
|
|
@ -234,6 +244,11 @@ public sealed class UiRoot : UiElement
|
|||
}
|
||||
else { _dragCandidate = true; }
|
||||
}
|
||||
else if (target.CapturesPointerDrag)
|
||||
{
|
||||
// No window ancestor, but the target still owns its interior drag.
|
||||
_dragCandidate = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
_dragCandidate = true;
|
||||
|
|
@ -247,8 +262,13 @@ public sealed class UiRoot : UiElement
|
|||
UiMouseButton.Middle => UiEventType.MiddleDown,
|
||||
_ => UiEventType.MouseDown,
|
||||
};
|
||||
// Deliver TARGET-LOCAL coords (consistent with MouseMove/MouseUp, which use
|
||||
// target.ScreenPosition). HitTestTopDown's lx/ly are relative to the TOP-LEVEL
|
||||
// child, so for a nested target (e.g. the chat view inset inside its window)
|
||||
// they'd be offset by the child's position — which mis-anchored drag-select.
|
||||
var sp = target.ScreenPosition;
|
||||
var e = new UiEvent(target.EventId, target, rawType,
|
||||
Data0: (int)flags, Data1: (int)lx, Data2: (int)ly);
|
||||
Data0: (int)flags, Data1: (int)(x - sp.X), Data2: (int)(y - sp.Y));
|
||||
BubbleEvent(target, in e);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue