mirror of
https://github.com/nsnail/spectre.console.git
synced 2025-04-16 08:52:50 +08:00
Allow selections to wrap around
This commit is contained in:
parent
eb02c3d534
commit
4f0ec87522
@ -15,7 +15,8 @@ internal sealed class ListPrompt<T>
|
|||||||
public async Task<ListPromptState<T>> Show(
|
public async Task<ListPromptState<T>> Show(
|
||||||
ListPromptTree<T> tree,
|
ListPromptTree<T> tree,
|
||||||
CancellationToken cancellationToken,
|
CancellationToken cancellationToken,
|
||||||
int requestedPageSize = 15)
|
int requestedPageSize = 15,
|
||||||
|
bool wrapAround = false)
|
||||||
{
|
{
|
||||||
if (tree is null)
|
if (tree is null)
|
||||||
{
|
{
|
||||||
@ -37,7 +38,7 @@ internal sealed class ListPrompt<T>
|
|||||||
}
|
}
|
||||||
|
|
||||||
var nodes = tree.Traverse().ToList();
|
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));
|
var hook = new ListPromptRenderHook<T>(_console, () => BuildRenderable(state));
|
||||||
|
|
||||||
using (new RenderHookScope(_console, hook))
|
using (new RenderHookScope(_console, hook))
|
||||||
|
@ -6,15 +6,17 @@ internal sealed class ListPromptState<T>
|
|||||||
public int Index { get; private set; }
|
public int Index { get; private set; }
|
||||||
public int ItemCount => Items.Count;
|
public int ItemCount => Items.Count;
|
||||||
public int PageSize { get; }
|
public int PageSize { get; }
|
||||||
|
public bool WrapAround { get; }
|
||||||
public IReadOnlyList<ListPromptItem<T>> Items { get; }
|
public IReadOnlyList<ListPromptItem<T>> Items { get; }
|
||||||
|
|
||||||
public ListPromptItem<T> Current => Items[Index];
|
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;
|
Index = 0;
|
||||||
Items = items;
|
Items = items;
|
||||||
PageSize = pageSize;
|
PageSize = pageSize;
|
||||||
|
WrapAround = wrapAround;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool Update(ConsoleKey key)
|
public bool Update(ConsoleKey key)
|
||||||
@ -30,7 +32,9 @@ internal sealed class ListPromptState<T>
|
|||||||
_ => Index,
|
_ => Index,
|
||||||
};
|
};
|
||||||
|
|
||||||
index = index.Clamp(0, ItemCount - 1);
|
index = WrapAround
|
||||||
|
? (ItemCount + (index % ItemCount)) % ItemCount
|
||||||
|
: index.Clamp(0, ItemCount - 1);
|
||||||
if (index != Index)
|
if (index != Index)
|
||||||
{
|
{
|
||||||
Index = index;
|
Index = index;
|
||||||
|
@ -18,6 +18,12 @@ public sealed class MultiSelectionPrompt<T> : IPrompt<List<T>>, IListPromptStrat
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public int PageSize { get; set; } = 10;
|
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>
|
/// <summary>
|
||||||
/// Gets or sets the highlight style of the selected choice.
|
/// Gets or sets the highlight style of the selected choice.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -88,7 +94,7 @@ public sealed class MultiSelectionPrompt<T> : IPrompt<List<T>>, IListPromptStrat
|
|||||||
{
|
{
|
||||||
// Create the list prompt
|
// Create the list prompt
|
||||||
var prompt = new ListPrompt<T>(console, this);
|
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)
|
if (Mode == SelectionMode.Leaf)
|
||||||
{
|
{
|
||||||
|
@ -211,6 +211,25 @@ public static class MultiSelectionPromptExtensions
|
|||||||
return obj;
|
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>
|
/// <summary>
|
||||||
/// Sets the highlight style of the selected choice.
|
/// Sets the highlight style of the selected choice.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -20,6 +20,12 @@ public sealed class SelectionPrompt<T> : IPrompt<T>, IListPromptStrategy<T>
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public int PageSize { get; set; } = 10;
|
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>
|
/// <summary>
|
||||||
/// Gets or sets the highlight style of the selected choice.
|
/// Gets or sets the highlight style of the selected choice.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -78,7 +84,7 @@ public sealed class SelectionPrompt<T> : IPrompt<T>, IListPromptStrategy<T>
|
|||||||
{
|
{
|
||||||
// Create the list prompt
|
// Create the list prompt
|
||||||
var prompt = new ListPrompt<T>(console, this);
|
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 the selected item
|
||||||
return result.Items[result.Index].Data;
|
return result.Items[result.Index].Data;
|
||||||
|
@ -163,6 +163,25 @@ public static class SelectionPromptExtensions
|
|||||||
return obj;
|
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>
|
/// <summary>
|
||||||
/// Sets the highlight style of the selected choice.
|
/// Sets the highlight style of the selected choice.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
120
test/Spectre.Console.Tests/Unit/Prompts/ListPromptStateTests.cs
Normal file
120
test/Spectre.Console.Tests/Unit/Prompts/ListPromptStateTests.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user