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
66 changes: 66 additions & 0 deletions src/Capacitor.App/Views/ChatTabView.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ public partial class ChatTabView : UserControl {
bool _readerGesture;
int _listInputVersion;
bool _thumbDragging;
bool _dragSelecting;
bool _edgeScrollQueued;
Point _dragInScroll;
MarkdownView? _dragView;
double? _thumbFraction;
double _smoothRemaining;
bool _smoothFrameQueued;
Expand All @@ -49,7 +53,9 @@ public ChatTabView() {
// Press and release both: a click's command runs on the release, in a later dispatcher turn
// than the press, by which time a flag armed on the press alone has been released.
ChatItems.AddHandler(PointerPressedEvent, OnReaderGesture, RoutingStrategies.Tunnel, handledEventsToo: true);
ChatItems.AddHandler(PointerMovedEvent, OnSelectionDrag, RoutingStrategies.Bubble, handledEventsToo: true);
ChatItems.AddHandler(PointerReleasedEvent, OnReaderGesture, RoutingStrategies.Tunnel, handledEventsToo: true);
ChatItems.AddHandler(PointerReleasedEvent, OnSelectionDragEnded, RoutingStrategies.Tunnel, handledEventsToo: true);
ChatItems.AddHandler(PointerWheelChangedEvent, OnReaderGesture, RoutingStrategies.Tunnel, handledEventsToo: true);
ChatItems.AddHandler(PointerWheelChangedEvent, OnChatWheelChanged, RoutingStrategies.Tunnel);
ChatItems.AddHandler(KeyDownEvent, OnReaderGesture, RoutingStrategies.Tunnel, handledEventsToo: true);
Expand Down Expand Up @@ -95,6 +101,8 @@ protected override void OnDataContextChanged(EventArgs e) {
_thumbDragging = false;
_thumbFraction = null;
_readerGesture = false;
_dragSelecting = false;
_dragView = null;
_followTail = true;
base.OnDataContextChanged(e);
}
Expand All @@ -104,6 +112,8 @@ protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e
CancelSmoothScroll();
_thumbDragging = false;
_thumbFraction = null;
_dragSelecting = false;
_dragView = null;
_attachments?.Dispose();
_attachments = null;
base.OnDetachedFromVisualTree(e);
Expand Down Expand Up @@ -385,6 +395,62 @@ void OnScrollChanged(object? sender, ScrollChangedEventArgs e) {
if (_followTail && !atBottom) scroll.ScrollToEnd();
}

void OnSelectionDrag(object? sender, PointerEventArgs e) {
EnsureChatScroll();
if (_scroll is null) return;
if (!e.GetCurrentPoint(ChatItems).Properties.IsLeftButtonPressed || OriginatesFromTextBox(e)) {
_dragSelecting = false;
return;
}
if (e.Source is not Visual visual) return;
var view = visual.GetSelfAndVisualAncestors().OfType<MarkdownView>().FirstOrDefault();
if (!_dragSelecting) {
if (view is not { IsSelecting: true }) return;
_dragView = view;
_dragSelecting = true;
} else if (view is { IsSelecting: true }) {
_dragView = view;
}
if (_dragView is { HasSelection: true }) _followTail = false;
_dragInScroll = e.GetPosition(_scroll);
QueueEdgeScroll();
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

void OnSelectionDragEnded(object? sender, RoutedEventArgs e) => _dragSelecting = false;

void QueueEdgeScroll() {
if (_edgeScrollQueued || !_dragSelecting || _scroll is null) return;
if (TopLevel.GetTopLevel(this) is not { } top) return;
_edgeScrollQueued = true;
top.RequestAnimationFrame(_ => {
_edgeScrollQueued = false;
if (_dragSelecting) StepEdgeScroll();
});
}

void StepEdgeScroll() {
if (_scroll is null || !_dragSelecting) return;
const double edge = 28;
const double step = 18;
var y = _dragInScroll.Y;
var height = _scroll.Viewport.Height;
var delta = y < edge ? -step : y > height - edge ? step : 0.0;
if (Math.Abs(delta) < 0.01) return;
var max = Math.Max(0, _scroll.Extent.Height - _scroll.Viewport.Height);
var next = Math.Clamp(_scroll.Offset.Y + delta, 0, max);
if (Math.Abs(next - _scroll.Offset.Y) > 0.5) {
var wasGesture = _readerGesture;
_readerGesture = true;
_scroll.Offset = new Vector(_scroll.Offset.X, next);
if (!wasGesture) Dispatcher.UIThread.Post(() => _readerGesture = false, DispatcherPriority.Background);
}
if (_dragView?.TranslatePoint(default, _scroll) is { } origin) {
_dragView.ExtendSelection(new Point(_dragInScroll.X - origin.X, _dragInScroll.Y - origin.Y));
if (_dragView.HasSelection) _followTail = false;
}
QueueEdgeScroll();
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

static bool OriginatesFromTextBox(RoutedEventArgs e) =>
e.Source is Visual visual && (visual is TextBox || visual.GetVisualAncestors().OfType<TextBox>().Any());

Expand Down
14 changes: 14 additions & 0 deletions src/Capacitor.App/Views/MarkViewSelection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,20 @@ public static void Attach(Control host, MarkdownViewer viewer, Func<Task> copy)
host.AddHandler(InputElement.KeyDownEvent, (_, e) => OnKeyDown(copy, e), RoutingStrategies.Tunnel);
}

/// Continues a drag at a point in the viewer's coordinates. Used when the chat scrolls under a
/// pointer that is sitting on the viewport edge, so the selection grows without a new move.
public static void Extend(MarkdownViewer viewer, Point pointInViewer) {
if (LayerOf(viewer) is not Visual layer) return;
if (Anchor(layer) is null) return;
IsDragging(viewer) = true;
var point = viewer.TranslatePoint(pointInViewer, layer) ?? pointInViewer;
if (HitTestOffset(layer, point) is not null) MoveFocus(layer, point);
else if (Nearest(layer, point) is { } inside) MoveFocus(layer, inside);
}

public static bool IsSelecting(MarkdownViewer viewer) =>
IsDragging(viewer) && LayerOf(viewer) is { } layer && Anchor(layer) is not null;

public static bool HasSelection(MarkdownViewer viewer) =>
LayerOf(viewer) is { } layer && Anchor(layer) is { } anchor && Focus(layer) is { } focus && anchor != focus;

Expand Down
6 changes: 6 additions & 0 deletions src/Capacitor.App/Views/MarkdownView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,12 @@ public MarkdownView() {
Content = _viewer;
}

internal bool IsSelecting => Content is MarkdownViewer && MarkViewSelection.IsSelecting(_viewer);

internal void ExtendSelection(Point pointInView) {
if (Content is MarkdownViewer) MarkViewSelection.Extend(_viewer, pointInView);
Comment thread
qodo-code-review[bot] marked this conversation as resolved.
}

public bool HasSelection => Content is SelectableTextBlock plain
? !string.IsNullOrEmpty(plain.SelectedText)
: MarkViewSelection.HasSelection(_viewer);
Expand Down
Loading