fix(ui): auto-scroll the lists sidebar while dragging near its top/bottom edge

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
mika kuns
2026-10-02 17:16:34 +02:00
co-authored by Claude Sonnet 5.5
parent ff7197c410
commit bba7117d54
3 changed files with 91 additions and 1 deletions
@@ -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();
}
}