mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
0099a6e2fa
* HarnessConsole: Improve rendering perf / reduce flickering * Address PR comments
113 lines
3.4 KiB
C#
113 lines
3.4 KiB
C#
// Copyright (c) Microsoft. All rights reserved.
|
|
|
|
using Harness.ConsoleReactiveFramework;
|
|
|
|
namespace Harness.ConsoleReactiveComponents;
|
|
|
|
/// <summary>
|
|
/// Props for <see cref="TextScrollPanel"/>.
|
|
/// </summary>
|
|
public record TextScrollPanelProps : ConsoleReactiveProps
|
|
{
|
|
/// <summary>Gets the items to render in the scroll panel. Each item is a pre-rendered
|
|
/// console string (may include ANSI escape sequences and newlines).</summary>
|
|
public IReadOnlyList<string> Items { get; init; } = [];
|
|
}
|
|
|
|
/// <summary>
|
|
/// State for <see cref="TextScrollPanel"/>.
|
|
/// </summary>
|
|
public record TextScrollPanelState : ConsoleReactiveState;
|
|
|
|
/// <summary>
|
|
/// A component that renders pre-rendered string items within a scroll area.
|
|
/// The last rendered item is considered dynamic and will be re-rendered on each call.
|
|
/// All prior items are considered finalized and are not re-rendered.
|
|
/// Use <see cref="Invalidate"/> to force a full re-render.
|
|
/// </summary>
|
|
public class TextScrollPanel : ConsoleReactiveComponent<TextScrollPanelProps, TextScrollPanelState>
|
|
{
|
|
private int _renderedCount;
|
|
private int _lastItemOffsetFromBottom;
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="TextScrollPanel"/> class.
|
|
/// </summary>
|
|
public TextScrollPanel()
|
|
{
|
|
this.State = new TextScrollPanelState();
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override void Invalidate()
|
|
{
|
|
this._renderedCount = 0;
|
|
this._lastItemOffsetFromBottom = 0;
|
|
base.Invalidate();
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override void RenderCore(TextScrollPanelProps props, TextScrollPanelState state)
|
|
{
|
|
if (props.Items.Count == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
int bottomRow = props.Y + props.Height - 1;
|
|
|
|
// Determine the first item to render. If we previously rendered items,
|
|
// re-render the last one (it may have changed/grown) from its stored position.
|
|
int startIndex = this._renderedCount > 0 ? this._renderedCount - 1 : 0;
|
|
|
|
if (this._renderedCount > 0 && this._lastItemOffsetFromBottom > 0)
|
|
{
|
|
// Reposition cursor to where the last rendered item began
|
|
Console.Write(AnsiEscapes.MoveCursor(bottomRow - this._lastItemOffsetFromBottom, props.X));
|
|
}
|
|
else
|
|
{
|
|
// First render — position at the bottom of the scroll area
|
|
Console.Write(AnsiEscapes.MoveCursor(bottomRow, props.X));
|
|
}
|
|
|
|
// Render from startIndex onwards
|
|
for (int i = startIndex; i < props.Items.Count; i++)
|
|
{
|
|
Console.Write(props.Items[i]);
|
|
}
|
|
|
|
// Calculate the offset from bottom for the start of the new last item
|
|
int lastItemLines = CountLines(props.Items[^1]);
|
|
this._lastItemOffsetFromBottom = lastItemLines > 0 ? lastItemLines - 1 : 0;
|
|
|
|
// Update rendered count
|
|
this._renderedCount = props.Items.Count;
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|