mirror of
				https://github.com/nsnail/spectre.console.git
				synced 2025-10-31 00:59:26 +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( | ||||
|         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)) | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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) | ||||
|         { | ||||
|   | ||||
| @@ -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> | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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> | ||||
|   | ||||
							
								
								
									
										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