// Copyright (c) Microsoft. All rights reserved. using Harness.ConsoleReactiveFramework; namespace Harness.ConsoleReactiveComponents; /// /// Props for . /// public record TextPanelProps : ConsoleReactiveProps { /// Gets the items to render in the panel. Each item is a pre-rendered /// console string (may include ANSI escape sequences and newlines). public IReadOnlyList Items { get; init; } = []; } /// /// A component that renders a list of pre-rendered string items vertically. /// Designed for rendering dynamic items in a non-scroll region that may be /// re-rendered on each update. If the component's /// exceeds the number of output lines, leftover lines are erased. /// public class TextPanel : ConsoleReactiveComponent { /// /// Calculates the height (in lines) needed to render all items. /// /// The items to measure. /// The total number of lines all items will occupy. public static int CalculateHeight(IReadOnlyList items) { int total = 0; for (int i = 0; i < items.Count; i++) { total += CountLines(items[i]); } return total; } /// public override void RenderCore(TextPanelProps props, ConsoleReactiveState state) { int currentRow = 0; for (int i = 0; i < props.Items.Count; i++) { string text = props.Items[i]; string[] lines = text.Split('\n'); int lineCount = CountLines(text); for (int j = 0; j < lineCount; j++) { Console.Write(AnsiEscapes.MoveAndEraseLine(props.Y + currentRow)); Console.Write(lines[j]); currentRow++; } } // If the component height exceeds the output, erase leftover lines if (props.Height > currentRow) { for (int i = currentRow; i < props.Height; i++) { Console.Write(AnsiEscapes.MoveAndEraseLine(props.Y + i)); } } } private static int CountLines(string text) { if (string.IsNullOrEmpty(text)) { return 0; } int count = 1; for (int i = 0; i < text.Length; i++) { if (text[i] == '\n') { count++; } } // If text ends with a newline, don't count the trailing empty line if (text[text.Length - 1] == '\n') { count--; } return count; } }