mirror of
				https://github.com/nsnail/spectre.console.git
				synced 2025-10-31 09:09:25 +08:00 
			
		
		
		
	Allow selections to wrap around
This commit is contained in:
		| @@ -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); | ||||||
|  |     } | ||||||
|  | } | ||||||
		Reference in New Issue
	
	Block a user
	 Salvage
					Salvage