From bec26b2232143ea2e97d6d667a9b00207e50a163 Mon Sep 17 00:00:00 2001 From: Mika Kuns Date: Thu, 25 Jun 2026 22:39:30 +0200 Subject: [PATCH] feat(ui): replace OLE task-row drag with custom ghost drag Task rows now drive a hand-built pointer-capture drag instead of DragDrop.DoDragDropAsync: armed on press, begins past a 4px threshold so a plain click still selects. The ghost follows the screen cursor across windows; on release the action is decided by what is under the cursor -- over the Mission Control window queues the task (geometric DragHitTest, no OLE drop), over another row in the same user list reorders, anywhere else cancels and restores the row. Drag starts from any list kind (drag-to-queue everywhere) but reorder-on-drop stays gated on CanReorder. Removes the obsolete OLE TaskRowFormat path from both the source and MissionControlView (pane PaneFormat reorder is untouched). --- .../Views/Islands/TasksIslandView.axaml | 24 +- .../Views/Islands/TasksIslandView.axaml.cs | 255 +++++++++++------- .../MissionControlView.axaml.cs | 16 +- 3 files changed, 171 insertions(+), 124 deletions(-) diff --git a/src/ClaudeDo.Ui/Views/Islands/TasksIslandView.axaml b/src/ClaudeDo.Ui/Views/Islands/TasksIslandView.axaml index 6838c30..54a4bd1 100644 --- a/src/ClaudeDo.Ui/Views/Islands/TasksIslandView.axaml +++ b/src/ClaudeDo.Ui/Views/Islands/TasksIslandView.axaml @@ -95,13 +95,7 @@ @@ -120,13 +114,7 @@ @@ -159,13 +147,7 @@ diff --git a/src/ClaudeDo.Ui/Views/Islands/TasksIslandView.axaml.cs b/src/ClaudeDo.Ui/Views/Islands/TasksIslandView.axaml.cs index 0bdd650..464b377 100644 --- a/src/ClaudeDo.Ui/Views/Islands/TasksIslandView.axaml.cs +++ b/src/ClaudeDo.Ui/Views/Islands/TasksIslandView.axaml.cs @@ -1,28 +1,43 @@ +using System; using System.Linq; using Avalonia; using Avalonia.Controls; +using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Layout; using Avalonia.Media; using Avalonia.Threading; using Avalonia.VisualTree; +using ClaudeDo.Ui.ViewModels; using ClaudeDo.Ui.ViewModels.Islands; using ClaudeDo.Ui.ViewModels.Modals; +using ClaudeDo.Ui.Views.Controls; +using ClaudeDo.Ui.Views.MissionControl; using ClaudeDo.Ui.Views.Modals; namespace ClaudeDo.Ui.Views.Islands; public partial class TasksIslandView : UserControl { - // Public so the Mission Control window can accept the same drag payload (drop-to-queue). - public static readonly DataFormat TaskRowFormat = - DataFormat.CreateStringApplicationFormat("claudedo-task-row"); + private readonly TaskDragController _drag = new(); + + // Custom-drag gesture state. The drag is ARMED on press and BEGINS once the pointer moves + // past the threshold, so a plain click still selects the row. + private const double DragThreshold = 4; + private Point _pressPoint; + private TaskRowViewModel? _pressRow; + private Control? _pressControl; + private bool _dragArmed; + private bool _dragging; public TasksIslandView() { InitializeComponent(); AddHandler(PointerPressedEvent, OnTunnelPointerPressed, RoutingStrategies.Tunnel); + AddHandler(PointerMovedEvent, OnPointerMovedDrag, RoutingStrategies.Tunnel); + AddHandler(PointerReleasedEvent, OnPointerReleasedDrag, RoutingStrategies.Tunnel); + AddHandler(PointerCaptureLostEvent, OnPointerCaptureLost); DataContextChanged += (_, _) => { if (DataContext is TasksIslandViewModel vm) @@ -103,9 +118,15 @@ public partial class TasksIslandView : UserControl return await tcs.Task; } - private async void OnTunnelPointerPressed(object? sender, PointerPressedEventArgs e) + // ── Custom ghost drag ──────────────────────────────────────────────────── + // Replaces both the OLE DoDragDropAsync reorder and the OLE drop-to-queue path: a hand-built + // drag (pointer capture + a transparent topmost ghost window) is the only way to get a + // translucent follower that crosses from this window into the separate Mission Control window. + + private void OnTunnelPointerPressed(object? sender, PointerPressedEventArgs e) { - if (DataContext is not TasksIslandViewModel vm) return; + ResetPressState(); + if (DataContext is not TasksIslandViewModel) return; if (e.Source is not Visual src) return; var button = src as Button ?? src.FindAncestorOfType