Add Git overlay, operation tools, and virtualized preview so large folders stay responsive.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-24 15:04:04 +02:00
parent 9bf451932f
commit a3c54bbb03
127 changed files with 14748 additions and 633 deletions

View File

@@ -0,0 +1,155 @@
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using Explorer.Presentation;
using Explorer.Presentation.ViewModels;
namespace Explorer.App;
public static class FolderViewport
{
public static readonly DependencyProperty IsTrackedProperty =
DependencyProperty.RegisterAttached(
"IsTracked",
typeof(bool),
typeof(FolderViewport),
new PropertyMetadata(false, OnTrackedChanged));
public static void SetIsTracked(ListView element, bool value) => element.SetValue(IsTrackedProperty, value);
public static bool GetIsTracked(ListView element) => (bool)element.GetValue(IsTrackedProperty);
private static void OnTrackedChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is not ListView list)
{
return;
}
if (e.NewValue is true)
{
list.AddHandler(ScrollViewer.ScrollChangedEvent, new ScrollChangedEventHandler(OnScrollChanged), true);
list.Loaded += OnLoaded;
}
else
{
list.RemoveHandler(ScrollViewer.ScrollChangedEvent, new ScrollChangedEventHandler(OnScrollChanged));
list.Loaded -= OnLoaded;
}
}
private static void OnLoaded(object sender, RoutedEventArgs e) => Report(sender as ListView);
private static void OnScrollChanged(object sender, ScrollChangedEventArgs e)
{
var list = sender as ListView ?? FindAncestor<ListView>(e.OriginalSource as DependencyObject);
Report(list);
}
private static void Report(ListView? list)
{
if (list?.DataContext is not ExplorerPaneViewModel pane || list.Visibility != Visibility.Visible)
{
return;
}
var count = list.Items.Count;
if (count == 0)
{
pane.NotifyViewport([]);
return;
}
int first;
int lastExclusive;
if (FindWrapPanel(list) is { } wrap)
{
first = wrap.FirstVisibleIndex;
lastExclusive = Math.Max(first, wrap.LastVisibleIndex);
}
else
{
var viewer = FindScrollViewer(list);
int visible;
if (viewer is null || viewer.ExtentHeight <= 0)
{
first = 0;
visible = Math.Min(80, count);
}
else
{
first = (int)Math.Clamp(Math.Floor(viewer.VerticalOffset / viewer.ExtentHeight * count), 0, count - 1);
visible = Math.Max(8, (int)Math.Ceiling(viewer.ViewportHeight / viewer.ExtentHeight * count) + 8);
}
lastExclusive = Math.Min(count, first + visible);
}
lastExclusive = Math.Clamp(lastExclusive, 0, count);
first = Math.Clamp(first, 0, count);
var paths = new string[Math.Max(0, lastExclusive - first)];
for (var i = first; i < lastExclusive; i++)
{
if (list.Items[i] is FolderItemViewModel item)
{
paths[i - first] = item.FullPath;
}
}
pane.NotifyViewport(paths);
}
private static ScrollViewer? FindScrollViewer(DependencyObject root)
{
if (root is ScrollViewer viewer)
{
return viewer;
}
for (var i = 0; i < VisualTreeHelper.GetChildrenCount(root); i++)
{
var found = FindScrollViewer(VisualTreeHelper.GetChild(root, i));
if (found is not null)
{
return found;
}
}
return null;
}
private static VirtualizingWrapPanel? FindWrapPanel(DependencyObject root)
{
if (root is VirtualizingWrapPanel wrap)
{
return wrap;
}
for (var i = 0; i < VisualTreeHelper.GetChildrenCount(root); i++)
{
var found = FindWrapPanel(VisualTreeHelper.GetChild(root, i));
if (found is not null)
{
return found;
}
}
return null;
}
private static T? FindAncestor<T>(DependencyObject? current)
where T : DependencyObject
{
while (current is not null)
{
if (current is T match)
{
return match;
}
current = VisualTreeHelper.GetParent(current);
}
return null;
}
}