fix(claude-do): merge Lists sidebar: auto-scroll while dragging a list/folder near
ClaudeDo-Task: 653f3991bb564379ae80edd3d16e8224
This commit is contained in:
@@ -255,7 +255,7 @@
|
||||
</Border>
|
||||
|
||||
<!-- ── Scrollable body ── -->
|
||||
<ScrollViewer>
|
||||
<ScrollViewer x:Name="BodyScroll" DragDrop.AllowDrop="True">
|
||||
<StackPanel Margin="6,0,6,4">
|
||||
|
||||
<!-- SMART LISTS section -->
|
||||
|
||||
@@ -5,6 +5,7 @@ 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;
|
||||
@@ -13,6 +14,23 @@ using ClaudeDo.Ui.Views.Modals;
|
||||
|
||||
namespace ClaudeDo.Ui.Views.Islands;
|
||||
|
||||
/// <summary>Edge-band scroll step for dragging inside the lists sidebar.</summary>
|
||||
public static class ListsAutoScroll
|
||||
{
|
||||
private const double Band = 40;
|
||||
private const double MaxStep = 14;
|
||||
|
||||
/// <summary>Per-tick scroll delta for a pointer at <paramref name="y"/> in a viewport of <paramref name="height"/>: negative near the top, positive near the bottom, 0 outside the bands.</summary>
|
||||
public static double Delta(double y, double height)
|
||||
{
|
||||
var band = Math.Min(Band, height / 2);
|
||||
if (band <= 0) return 0;
|
||||
if (y < band) return -MaxStep * Math.Min(1, (band - y) / band);
|
||||
if (y > height - band) return MaxStep * Math.Min(1, (y - (height - band)) / band);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public partial class ListsIslandView : UserControl
|
||||
{
|
||||
private static readonly DataFormat<string> ListRowFormat =
|
||||
@@ -33,12 +51,42 @@ public partial class ListsIslandView : UserControl
|
||||
_pendingFolderDragPress = null;
|
||||
}
|
||||
|
||||
private DispatcherTimer? _autoScrollTimer;
|
||||
private double _autoScrollDelta;
|
||||
|
||||
/// <summary>Keeps the sidebar scrolling while the drag pointer sits in an edge band; DragOver alone may not repeat on a still pointer.</summary>
|
||||
private void OnBodyDragOver(object? sender, DragEventArgs e)
|
||||
{
|
||||
// Gaps and section labels are only drop targets so DragOver reaches us — they accept nothing.
|
||||
if (FindListItemBorder(e.Source as Visual) is null) e.DragEffects = DragDropEffects.None;
|
||||
|
||||
_autoScrollDelta = ListsAutoScroll.Delta(e.GetPosition(BodyScroll).Y, BodyScroll.Bounds.Height);
|
||||
if (_autoScrollDelta == 0) { StopAutoScroll(); return; }
|
||||
|
||||
_autoScrollTimer ??= new DispatcherTimer(TimeSpan.FromMilliseconds(16), DispatcherPriority.Background, (_, _) =>
|
||||
{
|
||||
var maxY = Math.Max(0, BodyScroll.Extent.Height - BodyScroll.Viewport.Height);
|
||||
var y = Math.Clamp(BodyScroll.Offset.Y + _autoScrollDelta, 0, maxY);
|
||||
BodyScroll.Offset = new Vector(BodyScroll.Offset.X, y);
|
||||
});
|
||||
_autoScrollTimer.Start();
|
||||
}
|
||||
|
||||
private void StopAutoScroll()
|
||||
{
|
||||
_autoScrollDelta = 0;
|
||||
_autoScrollTimer?.Stop();
|
||||
}
|
||||
|
||||
public ListsIslandView()
|
||||
{
|
||||
InitializeComponent();
|
||||
AddHandler(PointerPressedEvent, OnTunnelPointerPressed, RoutingStrategies.Tunnel);
|
||||
AddHandler(PointerMovedEvent, OnTunnelPointerMoved, RoutingStrategies.Tunnel);
|
||||
AddHandler(PointerReleasedEvent, (_, _) => CancelPendingFolderDrag(), RoutingStrategies.Tunnel);
|
||||
BodyScroll.AddHandler(DragDrop.DragOverEvent, OnBodyDragOver, RoutingStrategies.Bubble, handledEventsToo: true);
|
||||
BodyScroll.AddHandler(DragDrop.DragLeaveEvent, (_, _) => StopAutoScroll(), RoutingStrategies.Bubble, handledEventsToo: true);
|
||||
BodyScroll.AddHandler(DragDrop.DropEvent, (_, _) => StopAutoScroll(), RoutingStrategies.Bubble, handledEventsToo: true);
|
||||
DataContextChanged += (_, _) =>
|
||||
{
|
||||
if (DataContext is ListsIslandViewModel vm)
|
||||
@@ -92,6 +140,7 @@ public partial class ListsIslandView : UserControl
|
||||
}
|
||||
finally
|
||||
{
|
||||
StopAutoScroll();
|
||||
vm.ClearDropHints();
|
||||
}
|
||||
}
|
||||
@@ -115,6 +164,7 @@ public partial class ListsIslandView : UserControl
|
||||
}
|
||||
finally
|
||||
{
|
||||
StopAutoScroll();
|
||||
vm.ClearDropHints();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
using ClaudeDo.Ui.Views.Islands;
|
||||
using Xunit;
|
||||
|
||||
namespace ClaudeDo.Ui.Tests;
|
||||
|
||||
public class ListsAutoScrollTests
|
||||
{
|
||||
[Fact]
|
||||
public void Middle_of_viewport_does_not_scroll() =>
|
||||
Assert.Equal(0, ListsAutoScroll.Delta(300, 600));
|
||||
|
||||
[Fact]
|
||||
public void Top_band_scrolls_up_and_speeds_up_towards_the_edge()
|
||||
{
|
||||
var near = ListsAutoScroll.Delta(30, 600);
|
||||
var edge = ListsAutoScroll.Delta(0, 600);
|
||||
Assert.True(near < 0);
|
||||
Assert.True(edge < near);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Bottom_band_scrolls_down_and_speeds_up_towards_the_edge()
|
||||
{
|
||||
var near = ListsAutoScroll.Delta(570, 600);
|
||||
var edge = ListsAutoScroll.Delta(600, 600);
|
||||
Assert.True(near > 0);
|
||||
Assert.True(edge > near);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pointer_outside_the_viewport_is_clamped_to_max_step() =>
|
||||
Assert.Equal(ListsAutoScroll.Delta(0, 600), ListsAutoScroll.Delta(-50, 600));
|
||||
|
||||
[Fact]
|
||||
public void Tiny_or_empty_viewport_is_safe()
|
||||
{
|
||||
Assert.Equal(0, ListsAutoScroll.Delta(0, 0));
|
||||
Assert.True(ListsAutoScroll.Delta(5, 20) < 0);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user