Allow selections to wrap around

This commit is contained in:
Salvage 2022-09-25 01:23:05 +02:00 committed by Phil Scott
parent eb02c3d534
commit 4f0ec87522
7 changed files with 181 additions and 6 deletions

View File

@ -15,7 +15,8 @@ internal sealed class ListPrompt<T>
public async Task<ListPromptState<T>> Show(
ListPromptTree<T> tree,
CancellationToken cancellationToken,
int requestedPageSize = 15)
int requestedPageSize = 15,
bool wrapAround = false)
{
if (tree is null)
{
@ -37,7 +38,7 @@ internal sealed class ListPrompt<T>
}
var nodes = tree.Traverse().ToList();
var state = new ListPromptState<T>(nodes, _strategy.CalculatePageSize(_console, nodes.Count, requestedPageSize));
var state = new ListPromptState<T>(nodes, _strategy.CalculatePageSize(_console, nodes.Count, requestedPageSize), wrapAround);
var hook = new ListPromptRenderHook<T>(_console, () => BuildRenderable(state));
using (new RenderHookScope(_console, hook))

View File

@ -6,15 +6,17 @@ internal sealed class ListPromptState<T>
public int Index { get; private set; }
public int ItemCount => Items.Count;
public int PageSize { get; }
public bool WrapAround { get; }
public IReadOnlyList<ListPromptItem<T>> Items { get; }
public ListPromptItem<T> Current => Items[Index];
public ListPromptState(IReadOnlyList<ListPromptItem<T>> items, int pageSize)
public ListPromptState(IReadOnlyList<ListPromptItem<T>> items, int pageSize, bool wrapAround)
{
Index = 0;
Items = items;
PageSize = pageSize;
WrapAround = wrapAround;
}
public bool Update(ConsoleKey key)
@ -30,7 +32,9 @@ internal sealed class ListPromptState<T>
_ => Index,
};
index = index.Clamp(0, ItemCount - 1);
index = WrapAround
? (ItemCount + (index % ItemCount)) % ItemCount
: index.Clamp(0, ItemCount - 1);
if (index != Index)
{
Index = index;

View File

@ -18,6 +18,12 @@ public sealed class MultiSelectionPrompt<T> : IPrompt<List<T>>, IListPromptStrat
/// </summary>
public int PageSize { get; set; } = 10;
/// <summary>
/// Gets or sets whether the selection should wrap around when reaching the edge.
/// Defaults to <c>false</c>.
/// </summary>
public bool WrapAround { get; set; } = false;
/// <summary>
/// Gets or sets the highlight style of the selected choice.
/// </summary>
@ -88,7 +94,7 @@ public sealed class MultiSelectionPrompt<T> : IPrompt<List<T>>, IListPromptStrat
{
// Create the list prompt
var prompt = new ListPrompt<T>(console, this);
var result = await prompt.Show(Tree, cancellationToken, PageSize).ConfigureAwait(false);
var result = await prompt.Show(Tree, cancellationToken, PageSize, WrapAround).ConfigureAwait(false);
if (Mode == SelectionMode.Leaf)
{

View File

@ -211,6 +211,25 @@ public static class MultiSelectionPromptExtensions
return obj;
}
/// <summary>
/// Sets whether the selection should wrap around when reaching its edges.
/// </summary>
/// <typeparam name="T">The prompt result type.</typeparam>
/// <param name="obj">The prompt.</param>
/// <param name="shouldWrap">Whether the selection should wrap around.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static MultiSelectionPrompt<T> WrapAround<T>(this MultiSelectionPrompt<T> obj, bool shouldWrap = true)
where T : notnull
{
if (obj is null)
{
throw new ArgumentNullException(nameof(obj));
}
obj.WrapAround = shouldWrap;
return obj;
}
/// <summary>
/// Sets the highlight style of the selected choice.
/// </summary>

View File

@ -20,6 +20,12 @@ public sealed class SelectionPrompt<T> : IPrompt<T>, IListPromptStrategy<T>
/// </summary>
public int PageSize { get; set; } = 10;
/// <summary>
/// Gets or sets whether the selection should wrap around when reaching the edge.
/// Defaults to <c>false</c>.
/// </summary>
public bool WrapAround { get; set; } = false;
/// <summary>
/// Gets or sets the highlight style of the selected choice.
/// </summary>
@ -78,7 +84,7 @@ public sealed class SelectionPrompt<T> : IPrompt<T>, IListPromptStrategy<T>
{
// Create the list prompt
var prompt = new ListPrompt<T>(console, this);
var result = await prompt.Show(_tree, cancellationToken, PageSize).ConfigureAwait(false);
var result = await prompt.Show(_tree, cancellationToken, PageSize, WrapAround).ConfigureAwait(false);
// Return the selected item
return result.Items[result.Index].Data;

View File

@ -163,6 +163,25 @@ public static class SelectionPromptExtensions
return obj;
}
/// <summary>
/// Sets whether the selection should wrap around when reaching its edges.
/// </summary>
/// <typeparam name="T">The prompt result type.</typeparam>
/// <param name="obj">The prompt.</param>
/// <param name="shouldWrap">Whether the selection should wrap around.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static SelectionPrompt<T> WrapAround<T>(this SelectionPrompt<T> obj, bool shouldWrap = true)
where T : notnull
{
if (obj is null)
{
throw new ArgumentNullException(nameof(obj));
}
obj.WrapAround = shouldWrap;
return obj;
}
/// <summary>
/// Sets the highlight style of the selected choice.
/// </summary>

View File

@ -0,0 +1,120 @@
namespace Spectre.Console.Tests.Unit;
public sealed class ListPromptStateTests
{
private ListPromptState<string> CreateListPromptState(int count, int pageSize, bool shouldWrap)
=> new(Enumerable.Repeat(new ListPromptItem<string>(string.Empty), count).ToList(), pageSize, shouldWrap);
[Fact]
public void Should_Have_Start_Index_Zero()
{
// Given
var state = CreateListPromptState(100, 10, false);
// When
/* noop */
// Then
state.Index.ShouldBe(0);
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public void Should_Increase_Index(bool wrap)
{
// Given
var state = CreateListPromptState(100, 10, wrap);
var index = state.Index;
// When
state.Update(ConsoleKey.DownArrow);
// Then
state.Index.ShouldBe(index + 1);
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public void Should_Go_To_End(bool wrap)
{
// Given
var state = CreateListPromptState(100, 10, wrap);
// When
state.Update(ConsoleKey.End);
// Then
state.Index.ShouldBe(99);
}
[Fact]
public void Should_Clamp_Index_If_No_Wrap()
{
// Given
var state = CreateListPromptState(100, 10, false);
state.Update(ConsoleKey.End);
// When
state.Update(ConsoleKey.DownArrow);
// Then
state.Index.ShouldBe(99);
}
[Fact]
public void Should_Wrap_Index_If_Wrap()
{
// Given
var state = CreateListPromptState(100, 10, true);
state.Update(ConsoleKey.End);
// When
state.Update(ConsoleKey.DownArrow);
// Then
state.Index.ShouldBe(0);
}
[Fact]
public void Should_Wrap_Index_If_Wrap_And_Down()
{
// Given
var state = CreateListPromptState(100, 10, true);
// When
state.Update(ConsoleKey.UpArrow);
// Then
state.Index.ShouldBe(99);
}
[Fact]
public void Should_Wrap_Index_If_Wrap_And_Page_Up()
{
// Given
var state = CreateListPromptState(10, 100, true);
// When
state.Update(ConsoleKey.PageUp);
// Then
state.Index.ShouldBe(0);
}
[Fact]
public void Should_Wrap_Index_If_Wrap_And_Offset_And_Page_Down()
{
// Given
var state = CreateListPromptState(10, 100, true);
state.Update(ConsoleKey.End);
state.Update(ConsoleKey.UpArrow);
// When
state.Update(ConsoleKey.PageDown);
// Then
state.Index.ShouldBe(8);
}
}