Desktop: rebuild the hosts board once the typing in the find box pauses, not once a letter

This commit is contained in:
jaap-jan committed 2026-10-02 15:23:58 +02:00
1 parent 8759b12d47
commit 24c18205c3
3 files changed
+94 -1

No files matched your search

@@ -230,9 +230,13 @@
Ctrl+K is named on it because the palette is the other way to reach a host by typing, and somebody Ctrl+K is named on it because the palette is the other way to reach a host by typing, and somebody
who has found this box should know about the one that also connects on Enter. who has found this box should know about the one that also connects on Enter.
One way, and passed back by the code-behind once the typing pauses: every change to the filter
rebuilds the board's sections and their cards, and a name typed at speed used to do that once a
letter. The view model still filters the moment it is told. See HostsScreen.OnFilterTyped.
--> -->
<Border Grid.Row="1" Margin="26,0,26,16"> <Border Grid.Row="1" Margin="26,0,26,16">
<TextBox x:Name="HostFilter" Text="{Binding HostFilter}" Height="40" CornerRadius="10" <TextBox x:Name="HostFilter" Text="{Binding HostFilter, Mode=OneWay}" Height="40" CornerRadius="10"
FontFamily="{StaticResource MonoFont}" FontFamily="{StaticResource MonoFont}"
PlaceholderText="Find a host by name, address or note… · Ctrl+K searches and connects" /> PlaceholderText="Find a host by name, address or note… · Ctrl+K searches and connects" />
</Border> </Border>
@@ -106,6 +106,48 @@ internal sealed partial class HostsScreen : UserControl
Board.AddHandler(ContextRequestedEvent, OnContextRequested, RoutingStrategies.Tunnel); Board.AddHandler(ContextRequestedEvent, OnContextRequested, RoutingStrategies.Tunnel);
WireTheBand(); WireTheBand();
HostFilter.TextChanged += OnFilterTyped;
}
/// <summary>How long the find box waits for the typing to pause before the board is rebuilt.</summary>
private static readonly TimeSpan FilterPause = TimeSpan.FromMilliseconds(150);
/// <summary>The wait itself, replaceable so a test can say when a pause is over rather than sleep through one.</summary>
internal Func<TimeSpan, Task> Pause { get; set; } = static span => Task.Delay(span);
/// <summary>How many times the find box has changed, so a wait can tell it is no longer the latest.</summary>
private int typed;
/// <summary>Hands the find box's text to the vault once the typing pauses.</summary>
/// <remarks>
/// <para>
/// Every change to the filter rebuilds the board's sections and every card in them, and a name typed at
/// speed used to do that once a letter. Now it is done once, with the whole of what was typed.
/// </para>
/// <para>
/// A wait rather than a binding delay, and on the dispatcher's own queue: the box is bound one way, so a
/// filter the vault sets itself — cleared, or restored — still lands in the box at once, and the
/// change it makes to the box comes back here as the same text, which changes nothing.
/// </para>
/// </remarks>
private void OnFilterTyped(object? sender, TextChangedEventArgs e) =>
_ = PassTheFilterOnAsync(HostFilter.Text ?? string.Empty, ++typed);
private async Task PassTheFilterOnAsync(string text, int keystroke)
{
await Pause(FilterPause).ConfigureAwait(true);
// A later letter has its own wait, and it is the one that speaks.
if (keystroke != typed)
{
return;
}
if (Vault is { } vault && !string.Equals(vault.HostFilter, text, StringComparison.Ordinal))
{
vault.HostFilter = text;
}
} }
/// <summary>The band's own wiring, and the two keys that go with a set.</summary> /// <summary>The band's own wiring, and the two keys that go with a set.</summary>
@@ -520,6 +520,53 @@ public sealed class HostGridTests : IAsyncLifetime
}); });
} }
/// <remarks>
/// Every change to the filter rebuilds the board's sections and every card in them, and a name typed at
/// speed used to do that once a letter. The box now tells the vault once the typing pauses — and then
/// exactly once, with the whole of what was typed.
/// </remarks>
[Fact]
public async Task TypingIntoTheFindBox_RebuildsTheBoardOnceThePauseComes_NotOnceALetter()
{
await OnTheBoardAsync((screen, _) =>
{
// Each letter's wait, ended by hand: the pause is real time in the application, and this says
// when it is over instead of sleeping through it.
var pauses = new List<TaskCompletionSource>();
screen.Pause = _ =>
{
var pause = new TaskCompletionSource();
pauses.Add(pause);
return pause.Task;
};
var rebuilds = 0;
vault.HostSections.CollectionChanged += (_, _) => rebuilds++;
var box = screen.FindControl<TextBox>("HostFilter").ShouldNotBeNull();
box.Text = "p";
box.Text = "pr";
box.Text = "pro";
Dispatcher.UIThread.RunJobs();
vault.HostFilter.ShouldBeNullOrEmpty("nothing has reached the vault while the typing goes on");
rebuilds.ShouldBe(0);
foreach (var pause in pauses)
{
pause.SetResult();
}
Dispatcher.UIThread.RunJobs();
vault.HostFilter.ShouldBe("pro");
rebuilds.ShouldBe(1, "only the last letter's wait passes the text on");
vault.VisibleHosts.ShouldHaveSingleItem().Label.ShouldBe("prod-db");
});
}
// ---- Helpers ---- // ---- Helpers ----
private static void RightClick(Visual card, Visual window) private static void RightClick(Visual card, Visual window)