Desktop: virtualise the transfer queue and the import lists

This commit is contained in:
jaap-jan committed 2026-10-02 15:14:34 +02:00
1 parent 7b072bcded
commit 8759b12d47
4 files changed
+108 -16

No files matched your search

@@ -253,8 +253,20 @@
</Grid>
</Border>
<ScrollViewer Grid.Row="1">
<ItemsControl ItemsSource="{Binding Rows}" Margin="12,8">
<!-- Virtualised, for an inventory of hundreds of machines: see the transfer queue's own note. -->
<ItemsControl Grid.Row="1" ItemsSource="{Binding Rows}">
<ItemsControl.Template>
<ControlTemplate>
<ScrollViewer>
<ItemsPresenter Name="PART_ItemsPresenter" ItemsPanel="{TemplateBinding ItemsPanel}" Margin="12,8" />
</ScrollViewer>
</ControlTemplate>
</ItemsControl.Template>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:HetznerRowViewModel">
<Border Classes="importrow" CornerRadius="8">
@@ -290,8 +302,7 @@
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</ItemsControl>
</Grid>
</Border>
@@ -131,8 +131,20 @@
</Grid>
</Border>
<ScrollViewer Grid.Row="1">
<ItemsControl ItemsSource="{Binding Rows}" Margin="12,8">
<!-- Virtualised, for an inventory of hundreds of machines: see the transfer queue's own note. -->
<ItemsControl Grid.Row="1" ItemsSource="{Binding Rows}">
<ItemsControl.Template>
<ControlTemplate>
<ScrollViewer>
<ItemsPresenter Name="PART_ItemsPresenter" ItemsPanel="{TemplateBinding ItemsPanel}" Margin="12,8" />
</ScrollViewer>
</ControlTemplate>
</ItemsControl.Template>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:ImportRowViewModel">
<Border Classes="importrow" CornerRadius="8">
@@ -161,8 +173,7 @@
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</ItemsControl>
</Grid>
</Border>
@@ -1190,8 +1190,24 @@
Text="{Binding Status}" IsVisible="{Binding IsConnected}" />
</Grid>
<ScrollViewer Grid.Row="2">
<ItemsControl ItemsSource="{Binding Transfers}">
<!--
Virtualised: a folder download queues a row per file, and a stack panel inside an outer scroller
laid out and drew every one of them, thousands deep. The scroller is the control's own so the panel
knows the viewport it has to fill and realises only those rows.
-->
<ItemsControl Grid.Row="2" ItemsSource="{Binding Transfers}">
<ItemsControl.Template>
<ControlTemplate>
<ScrollViewer>
<ItemsPresenter Name="PART_ItemsPresenter" ItemsPanel="{TemplateBinding ItemsPanel}" />
</ScrollViewer>
</ControlTemplate>
</ItemsControl.Template>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:TransferRowViewModel">
<!--
@@ -1239,7 +1255,6 @@
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
<Button Grid.Row="3" Classes="ghost" Content="CLEAR FINISHED" Margin="0,10,0,0"
HorizontalAlignment="Left" Command="{Binding ClearCompletedCommand}" />
@@ -1386,6 +1386,48 @@ public sealed class ScreenLayoutTests : IAsyncLifetime
await MeasureTransfersAsync(faults => faults.ShouldBeEmpty());
}
/// <remarks>
/// A folder download queues a row per file. The queue was a plain stack inside an outer scroller, which
/// measured and drew every row however deep — so the number of rows built here is the claim, not the
/// layout: a few screens' worth out of two thousand.
/// </remarks>
[Fact]
public async Task ADeepQueue_BuildsOnlyTheRowsOnScreen()
{
transfers.IsConnected = true;
for (var file = 0; file < 2000; file++)
{
Enqueue(TransferDirection.Download, $"asset-{file:0000}.png", 4096, 0, TransferState.Queued);
}
await LayoutHarness.OnTheUiThreadAsync(
() =>
{
var screen = new TransfersScreen { DataContext = transfers };
var window = LayoutHarness.HostAtMinimumSize(
screen,
LayoutHarness.SessionScreenWidth(transfers.IsConnected),
LayoutHarness.SessionScreenHeight);
try
{
var built = screen.GetVisualDescendants()
.OfType<ProgressBar>()
.Count(bar => bar.Classes.Contains("transferbar"));
built.ShouldBeGreaterThan(0, "the rows on screen are still drawn");
built.ShouldBeLessThan(200, "the rest of the two thousand are not");
}
finally
{
window.Close();
}
},
Token);
}
/// <remarks>
/// The edit strip, which shares the questions' Grid row and is the tallest thing that can be in it: a
/// heading, a path, a scrolling log of saves, the conflict card and a button. Its log is bounded for the
@@ -1632,11 +1674,24 @@ public sealed class ScreenLayoutTests : IAsyncLifetime
try
{
var words = screen.GetVisualDescendants()
.OfType<TextBlock>()
.Where(text => text.Classes.Contains("transferstatus"))
.Select(text => text.Text ?? string.Empty)
.ToList();
// Row by row, scrolled to: the queue is virtualised, so a row off the bottom of the strip is
// not built until it is scrolled to — see ADeepQueue_BuildsOnlyTheRowsOnScreen.
var queue = screen.GetVisualDescendants().OfType<ItemsControl>()
.First(control => ReferenceEquals(control.ItemsSource, transfers.Transfers));
var words = new List<string>();
for (var row = 0; row < expected.Length; row++)
{
queue.ScrollIntoView(row);
window.UpdateLayout();
words.Add(queue.ContainerFromIndex(row).ShouldNotBeNull()
.GetVisualDescendants()
.OfType<TextBlock>()
.Single(text => text.Classes.Contains("transferstatus"))
.Text ?? string.Empty);
}
words.ShouldBe(expected);
}