mirror of
				https://github.com/nsnail/spectre.console.git
				synced 2025-10-31 09:09:25 +08:00 
			
		
		
		
	 Patrik Svensson
					Patrik Svensson
				
			
				
					committed by
					
						 Patrik Svensson
						Patrik Svensson
					
				
			
			
				
	
			
			
			 Patrik Svensson
						Patrik Svensson
					
				
			
						parent
						
							88edfe68ec
						
					
				
				
					commit
					9f8ca6d648
				
			| @@ -83,5 +83,25 @@ namespace Spectre.Console.Tests.Unit | ||||
|                 .NormalizeLineEndings() | ||||
|                 .ShouldBe(expected); | ||||
|         } | ||||
|  | ||||
|         [Theory] | ||||
|         [InlineData(Overflow.Fold, "foo \npneumonoultram\nicroscopicsili\ncovolcanoconio\nsis bar qux")] | ||||
|         [InlineData(Overflow.Crop, "foo \npneumonoultram\nbar qux")] | ||||
|         [InlineData(Overflow.Ellipsis, "foo \npneumonoultra…\nbar qux")] | ||||
|         public void Should_Overflow_Text_Correctly(Overflow overflow, string expected) | ||||
|         { | ||||
|             // Given | ||||
|             var fixture = new PlainConsole(14); | ||||
|             var text = new Text("foo pneumonoultramicroscopicsilicovolcanoconiosis bar qux") | ||||
|                 .SetOverflow(overflow); | ||||
|  | ||||
|             // When | ||||
|             fixture.Render(text); | ||||
|  | ||||
|             // Then | ||||
|             fixture.Output | ||||
|                 .NormalizeLineEndings() | ||||
|                 .ShouldBe(expected); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -7,7 +7,7 @@ namespace Spectre.Console | ||||
|     /// <summary> | ||||
|     /// A renderable piece of markup text. | ||||
|     /// </summary> | ||||
|     public sealed class Markup : Renderable, IAlignable | ||||
|     public sealed class Markup : Renderable, IAlignable, IOverflowable | ||||
|     { | ||||
|         private readonly Paragraph _paragraph; | ||||
|  | ||||
| @@ -18,6 +18,13 @@ namespace Spectre.Console | ||||
|             set => _paragraph.Alignment = value; | ||||
|         } | ||||
|  | ||||
|         /// <inheritdoc/> | ||||
|         public Overflow? Overflow | ||||
|         { | ||||
|             get => _paragraph.Overflow; | ||||
|             set => _paragraph.Overflow = value; | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="Markup"/> class. | ||||
|         /// </summary> | ||||
|   | ||||
							
								
								
									
										24
									
								
								src/Spectre.Console/Rendering/Overflow.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								src/Spectre.Console/Rendering/Overflow.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | ||||
| namespace Spectre.Console | ||||
| { | ||||
|     /// <summary> | ||||
|     /// Represents text overflow. | ||||
|     /// </summary> | ||||
|     public enum Overflow | ||||
|     { | ||||
|         /// <summary> | ||||
|         /// Put any excess characters on the next line. | ||||
|         /// </summary> | ||||
|         Fold = 0, | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Truncates the text at the end of the line. | ||||
|         /// </summary> | ||||
|         Crop = 1, | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Truncates the text at the end of the line and | ||||
|         /// also inserts an ellipsis character. | ||||
|         /// </summary> | ||||
|         Ellipsis = 2, | ||||
|     } | ||||
| } | ||||
| @@ -1,4 +1,4 @@ | ||||
| using System; | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.Diagnostics; | ||||
| using System.Linq; | ||||
| @@ -12,7 +12,7 @@ namespace Spectre.Console | ||||
|     /// of the paragraph can have individual styling. | ||||
|     /// </summary> | ||||
|     [DebuggerDisplay("{_text,nq}")] | ||||
|     public sealed class Paragraph : Renderable, IAlignable | ||||
|     public sealed class Paragraph : Renderable, IAlignable, IOverflowable | ||||
|     { | ||||
|         private readonly List<SegmentLine> _lines; | ||||
|  | ||||
| @@ -21,6 +21,11 @@ namespace Spectre.Console | ||||
|         /// </summary> | ||||
|         public Justify? Alignment { get; set; } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Gets or sets the text overflow strategy. | ||||
|         /// </summary> | ||||
|         public Overflow? Overflow { get; set; } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="Paragraph"/> class. | ||||
|         /// </summary> | ||||
| @@ -197,34 +202,76 @@ namespace Spectre.Console | ||||
|             var line = new SegmentLine(); | ||||
|  | ||||
|             var newLine = true; | ||||
|             using (var iterator = new SegmentLineIterator(_lines)) | ||||
|  | ||||
|             using var iterator = new SegmentLineIterator(_lines); | ||||
|             var queue = new Queue<Segment>(); | ||||
|             while (true) | ||||
|             { | ||||
|                 while (iterator.MoveNext()) | ||||
|                 var current = (Segment?)null; | ||||
|                 if (queue.Count == 0) | ||||
|                 { | ||||
|                     var current = iterator.Current; | ||||
|                     if (current == null) | ||||
|                     if (!iterator.MoveNext()) | ||||
|                     { | ||||
|                         throw new InvalidOperationException("Iterator returned empty segment."); | ||||
|                         break; | ||||
|                     } | ||||
|  | ||||
|                     if (newLine && current.IsWhiteSpace && !current.IsLineBreak) | ||||
|                     { | ||||
|                         newLine = false; | ||||
|                         continue; | ||||
|                     } | ||||
|                     current = iterator.Current; | ||||
|                 } | ||||
|                 else | ||||
|                 { | ||||
|                     current = queue.Dequeue(); | ||||
|                 } | ||||
|  | ||||
|                 if (current == null) | ||||
|                 { | ||||
|                     throw new InvalidOperationException("Iterator returned empty segment."); | ||||
|                 } | ||||
|  | ||||
|                 if (newLine && current.IsWhiteSpace && !current.IsLineBreak) | ||||
|                 { | ||||
|                     newLine = false; | ||||
|                     continue; | ||||
|                 } | ||||
|  | ||||
|                     if (current.IsLineBreak) | ||||
|                 newLine = false; | ||||
|  | ||||
|                 if (current.IsLineBreak) | ||||
|                 { | ||||
|                     line.Add(current); | ||||
|                     lines.Add(line); | ||||
|                     line = new SegmentLine(); | ||||
|                     newLine = true; | ||||
|                     continue; | ||||
|                 } | ||||
|  | ||||
|                 var length = current.CellLength(context.Encoding); | ||||
|                 if (length > maxWidth) | ||||
|                 { | ||||
|                     // The current segment is longer than the width of the console, | ||||
|                     // so we will need to crop it up, into new segments. | ||||
|                     var segments = Segment.SplitOverflow(current, Overflow, context.Encoding, maxWidth); | ||||
|                     if (segments.Count > 0) | ||||
|                     { | ||||
|                         line.Add(current); | ||||
|                         lines.Add(line); | ||||
|                         line = new SegmentLine(); | ||||
|                         newLine = true; | ||||
|                         continue; | ||||
|                     } | ||||
|                         if (line.CellWidth(context.Encoding) + segments[0].CellLength(context.Encoding) > maxWidth) | ||||
|                         { | ||||
|                             lines.Add(line); | ||||
|                             line = new SegmentLine(); | ||||
|                             newLine = true; | ||||
|  | ||||
|                     var length = current.CellLength(context.Encoding); | ||||
|                             segments.ForEach(s => queue.Enqueue(s)); | ||||
|                             continue; | ||||
|                         } | ||||
|                         else | ||||
|                         { | ||||
|                             // Add the segment and push the rest of them to the queue. | ||||
|                             line.Add(segments[0]); | ||||
|                             segments.Skip(1).ForEach(s => queue.Enqueue(s)); | ||||
|                             continue; | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|                 else | ||||
|                 { | ||||
|                     if (line.CellWidth(context.Encoding) + length > maxWidth) | ||||
|                     { | ||||
|                         line.Add(Segment.Empty); | ||||
| @@ -232,16 +279,16 @@ namespace Spectre.Console | ||||
|                         line = new SegmentLine(); | ||||
|                         newLine = true; | ||||
|                     } | ||||
|  | ||||
|                     if (newLine && current.IsWhiteSpace) | ||||
|                     { | ||||
|                         continue; | ||||
|                     } | ||||
|  | ||||
|                     newLine = false; | ||||
|  | ||||
|                     line.Add(current); | ||||
|                 } | ||||
|  | ||||
|                 if (newLine && current.IsWhiteSpace) | ||||
|                 { | ||||
|                     continue; | ||||
|                 } | ||||
|  | ||||
|                 newLine = false; | ||||
|  | ||||
|                 line.Add(current); | ||||
|             } | ||||
|  | ||||
|             // Flush remaining. | ||||
|   | ||||
| @@ -261,6 +261,57 @@ namespace Spectre.Console.Rendering | ||||
|             return result; | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Splits an overflowing segment into several new segments. | ||||
|         /// </summary> | ||||
|         /// <param name="segment">The segment to split.</param> | ||||
|         /// <param name="overflow">The overflow strategy to use.</param> | ||||
|         /// <param name="encoding">The encodign to use.</param> | ||||
|         /// <param name="width">The maxiumum width.</param> | ||||
|         /// <returns>A list of segments that has been split.</returns> | ||||
|         public static List<Segment> SplitOverflow(Segment segment, Overflow? overflow, Encoding encoding, int width) | ||||
|         { | ||||
|             if (segment is null) | ||||
|             { | ||||
|                 throw new ArgumentNullException(nameof(segment)); | ||||
|             } | ||||
|  | ||||
|             if (segment.CellLength(encoding) <= width) | ||||
|             { | ||||
|                 return new List<Segment>(1) { segment }; | ||||
|             } | ||||
|  | ||||
|             // Default to folding | ||||
|             overflow ??= Overflow.Fold; | ||||
|  | ||||
|             var result = new List<Segment>(); | ||||
|  | ||||
|             if (overflow == Overflow.Fold) | ||||
|             { | ||||
|                 var totalLength = segment.Text.CellLength(encoding); | ||||
|                 var lengthLeft = totalLength; | ||||
|                 while (lengthLeft > 0) | ||||
|                 { | ||||
|                     var index = totalLength - lengthLeft; | ||||
|                     var take = Math.Min(width, totalLength - index); | ||||
|  | ||||
|                     result.Add(new Segment(segment.Text.Substring(index, take), segment.Style)); | ||||
|                     lengthLeft -= take; | ||||
|                 } | ||||
|             } | ||||
|             else if (overflow == Overflow.Crop) | ||||
|             { | ||||
|                 result.Add(new Segment(segment.Text.Substring(0, width), segment.Style)); | ||||
|             } | ||||
|             else if (overflow == Overflow.Ellipsis) | ||||
|             { | ||||
|                 result.Add(new Segment(segment.Text.Substring(0, width - 1), segment.Style)); | ||||
|                 result.Add(new Segment("…", segment.Style)); | ||||
|             } | ||||
|  | ||||
|             return result; | ||||
|         } | ||||
|  | ||||
|         internal static List<List<SegmentLine>> MakeSameHeight(int cellHeight, List<List<SegmentLine>> cells) | ||||
|         { | ||||
|             foreach (var cell in cells) | ||||
|   | ||||
| @@ -10,7 +10,7 @@ namespace Spectre.Console | ||||
|     /// </summary> | ||||
|     [DebuggerDisplay("{_text,nq}")] | ||||
|     [SuppressMessage("Naming", "CA1724:Type names should not match namespaces")] | ||||
|     public sealed class Text : Renderable, IAlignable | ||||
|     public sealed class Text : Renderable, IAlignable, IOverflowable | ||||
|     { | ||||
|         private readonly Paragraph _paragraph; | ||||
|  | ||||
| @@ -38,6 +38,15 @@ namespace Spectre.Console | ||||
|             set => _paragraph.Alignment = value; | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Gets or sets the text overflow strategy. | ||||
|         /// </summary> | ||||
|         public Overflow? Overflow | ||||
|         { | ||||
|             get => _paragraph.Overflow; | ||||
|             set => _paragraph.Overflow = value; | ||||
|         } | ||||
|  | ||||
|         /// <inheritdoc/> | ||||
|         protected override Measurement Measure(RenderContext context, int maxWidth) | ||||
|         { | ||||
|   | ||||
| @@ -0,0 +1,80 @@ | ||||
| using System; | ||||
|  | ||||
| namespace Spectre.Console | ||||
| { | ||||
|     /// <summary> | ||||
|     /// Contains extension methods for <see cref="IOverflowable"/>. | ||||
|     /// </summary> | ||||
|     public static class OverflowableExtensions | ||||
|     { | ||||
|         /// <summary> | ||||
|         /// Folds any overflowing text. | ||||
|         /// </summary> | ||||
|         /// <typeparam name="T">An object implementing <see cref="IOverflowable"/>.</typeparam> | ||||
|         /// <param name="obj">The overflowable object instance.</param> | ||||
|         /// <returns>The same instance so that multiple calls can be chained.</returns> | ||||
|         public static T Fold<T>(this T obj) | ||||
|             where T : class, IOverflowable | ||||
|         { | ||||
|             if (obj is null) | ||||
|             { | ||||
|                 throw new ArgumentNullException(nameof(obj)); | ||||
|             } | ||||
|  | ||||
|             return SetOverflow(obj, Overflow.Fold); | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Crops any overflowing text. | ||||
|         /// </summary> | ||||
|         /// <typeparam name="T">An object implementing <see cref="IOverflowable"/>.</typeparam> | ||||
|         /// <param name="obj">The overflowable object instance.</param> | ||||
|         /// <returns>The same instance so that multiple calls can be chained.</returns> | ||||
|         public static T Crop<T>(this T obj) | ||||
|             where T : class, IOverflowable | ||||
|         { | ||||
|             if (obj is null) | ||||
|             { | ||||
|                 throw new ArgumentNullException(nameof(obj)); | ||||
|             } | ||||
|  | ||||
|             return SetOverflow(obj, Overflow.Crop); | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Crops any overflowing text and adds an ellipsis to the end. | ||||
|         /// </summary> | ||||
|         /// <typeparam name="T">An object implementing <see cref="IOverflowable"/>.</typeparam> | ||||
|         /// <param name="obj">The overflowable object instance.</param> | ||||
|         /// <returns>The same instance so that multiple calls can be chained.</returns> | ||||
|         public static T Ellipsis<T>(this T obj) | ||||
|             where T : class, IOverflowable | ||||
|         { | ||||
|             if (obj is null) | ||||
|             { | ||||
|                 throw new ArgumentNullException(nameof(obj)); | ||||
|             } | ||||
|  | ||||
|             return SetOverflow(obj, Overflow.Ellipsis); | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Sets the overflow strategy. | ||||
|         /// </summary> | ||||
|         /// <typeparam name="T">An object implementing <see cref="IOverflowable"/>.</typeparam> | ||||
|         /// <param name="obj">The overflowable object instance.</param> | ||||
|         /// <param name="overflow">The overflow strategy to use.</param> | ||||
|         /// <returns>The same instance so that multiple calls can be chained.</returns> | ||||
|         public static T SetOverflow<T>(this T obj, Overflow overflow) | ||||
|             where T : class, IOverflowable | ||||
|         { | ||||
|             if (obj is null) | ||||
|             { | ||||
|                 throw new ArgumentNullException(nameof(obj)); | ||||
|             } | ||||
|  | ||||
|             obj.Overflow = overflow; | ||||
|             return obj; | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										13
									
								
								src/Spectre.Console/Rendering/Traits/IOverflowable.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/Spectre.Console/Rendering/Traits/IOverflowable.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | ||||
| namespace Spectre.Console | ||||
| { | ||||
|     /// <summary> | ||||
|     /// Represents something that can overflow. | ||||
|     /// </summary> | ||||
|     public interface IOverflowable | ||||
|     { | ||||
|         /// <summary> | ||||
|         /// Gets or sets the text overflow strategy. | ||||
|         /// </summary> | ||||
|         Overflow? Overflow { get; set; } | ||||
|     } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user