Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public override void RenderCore(ListSelectionProps props, ConsoleReactiveState s
{
foreach (string line in props.Title.Split('\n'))
{
Console.Write(AnsiEscapes.MoveCursor(this.Y + row, this.X));
Console.Write(AnsiEscapes.MoveCursor(props.Y + row, props.X));
Console.Write(AnsiEscapes.EraseEntireLine);
Console.Write(line);
row++;
Expand All @@ -51,7 +51,7 @@ public override void RenderCore(ListSelectionProps props, ConsoleReactiveState s

for (int i = 0; i < totalItems; i++)
{
Console.Write(AnsiEscapes.MoveCursor(this.Y + row, this.X));
Console.Write(AnsiEscapes.MoveCursor(props.Y + row, props.X));
Console.Write(AnsiEscapes.EraseEntireLine);

bool isSelected = i == props.SelectedIndex;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,11 @@ public static int CalculateHeight(TextInputProps props, int availableWidth)
public override void RenderCore(TextInputProps props, ConsoleReactiveState state)
{
int promptLength = props.Prompt.Length;
int textWidth = this.Width - promptLength;
int textWidth = props.Width - promptLength;
string indent = new(' ', promptLength);

// First line: prompt + start of text
Console.Write(AnsiEscapes.MoveCursor(this.Y, this.X));
Console.Write(AnsiEscapes.MoveCursor(props.Y, props.X));
Console.Write(AnsiEscapes.EraseEntireLine);
Console.Write(props.Prompt);

Expand Down Expand Up @@ -90,7 +90,7 @@ public override void RenderCore(TextInputProps props, ConsoleReactiveState state
while (offset < props.Text.Length)
{
int chunk = Math.Min(textWidth, props.Text.Length - offset);
Console.Write(AnsiEscapes.MoveCursor(this.Y + row, this.X));
Console.Write(AnsiEscapes.MoveCursor(props.Y + row, props.X));
Console.Write(AnsiEscapes.EraseEntireLine);
Console.Write(indent);
Console.Write(props.Text[offset..(offset + chunk)]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public record TextPanelProps : ConsoleReactiveProps
/// <summary>
/// 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 <see cref="ConsoleReactiveComponent.Height"/>
/// re-rendered on each update. If the component's <see cref="ConsoleReactiveProps.Height"/>
/// exceeds the number of output lines, leftover lines are erased.
/// </summary>
public class TextPanel : ConsoleReactiveComponent<TextPanelProps, ConsoleReactiveState>
Expand Down Expand Up @@ -51,18 +51,18 @@ public override void RenderCore(TextPanelProps props, ConsoleReactiveState state

for (int j = 0; j < lineCount; j++)
{
Console.Write(AnsiEscapes.MoveAndEraseLine(this.Y + currentRow));
Console.Write(AnsiEscapes.MoveAndEraseLine(props.Y + currentRow));
Console.Write(lines[j]);
currentRow++;
}
}

// If the component height exceeds the output, erase leftover lines
if (this.Height > currentRow)
if (props.Height > currentRow)
{
for (int i = currentRow; i < this.Height; i++)
for (int i = currentRow; i < props.Height; i++)
{
Console.Write(AnsiEscapes.MoveAndEraseLine(this.Y + i));
Console.Write(AnsiEscapes.MoveAndEraseLine(props.Y + i));
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ public override void RenderCore(TextScrollPanelProps props, TextScrollPanelState
}

// Move cursor to the bottom of the scroll area
Console.Write(AnsiEscapes.MoveCursor(this.Y + this.Height - 1, this.X));
Console.Write(AnsiEscapes.MoveCursor(props.Y + props.Height - 1, props.X));

// Output only new items since last rendered
for (int i = state.RenderedCount; i < props.Items.Count; i++)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,6 @@ namespace Harness.ConsoleReactiveComponents;
/// </summary>
public record TopBottomRuleProps : ConsoleReactiveProps
{
/// <summary>Gets the width of the horizontal rules in characters.</summary>
public int Width { get; init; }

/// <summary>Gets the foreground color of the horizontal rules. If <c>null</c>, the default terminal color is used.</summary>
public ConsoleColor? Color { get; init; }
}
Expand All @@ -32,7 +29,7 @@ public static int CalculateHeight(TopBottomRuleProps props)
int childrenHeight = 0;
foreach (var child in props.Children)
{
childrenHeight += child.Height;
childrenHeight += child.BaseProps?.Height ?? 0;
}

// Top rule + children + bottom rule
Expand All @@ -51,11 +48,11 @@ public override void RenderCore(TopBottomRuleProps props, ConsoleReactiveState s
}

// Top rule
Console.Write(AnsiEscapes.MoveCursor(this.Y, this.X));
Console.Write(AnsiEscapes.MoveCursor(props.Y, props.X));
Console.Write(rule);

// Render children stacked below the top rule
int currentY = this.Y + 1;
int currentY = props.Y + 1;

if (props.Color.HasValue)
{
Expand All @@ -64,10 +61,9 @@ public override void RenderCore(TopBottomRuleProps props, ConsoleReactiveState s

foreach (var child in props.Children)
{
child.X = this.X;
child.Y = currentY;
child.BaseProps = child.BaseProps! with { X = props.X, Y = currentY };
child.Render();
currentY += child.Height;
currentY += child.BaseProps.Height;
}

if (props.Color.HasValue)
Expand All @@ -76,7 +72,7 @@ public override void RenderCore(TopBottomRuleProps props, ConsoleReactiveState s
}

// Bottom rule
Console.Write(AnsiEscapes.MoveCursor(currentY, this.X));
Console.Write(AnsiEscapes.MoveCursor(currentY, props.X));
Console.Write(rule);

if (props.Color.HasValue)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
namespace Harness.ConsoleReactiveFramework;

/// <summary>
/// Abstract base class for all console UI components. Provides layout properties
/// (position and size) and a <see cref="Render"/> method for drawing to the console.
/// Abstract base class for all console UI components. Provides access to layout
/// through <see cref="BaseProps"/> and a <see cref="Render"/> method for drawing to the console.
/// Derive from <see cref="ConsoleReactiveComponent{TProps, TState}"/> instead of this class directly.
/// </summary>
public abstract class ConsoleReactiveComponent
Expand All @@ -13,20 +13,21 @@ internal ConsoleReactiveComponent()
{
}

/// <summary>Gets or sets the 1-based column position of the component.</summary>
public int X { get; set; }

/// <summary>Gets or sets the 1-based row position of the component.</summary>
public int Y { get; set; }

/// <summary>Gets or sets the width of the component in columns.</summary>
public int Width { get; set; }

/// <summary>Gets or sets the height of the component in rows.</summary>
public int Height { get; set; }
/// <summary>
/// Gets or sets the component's props as the base <see cref="ConsoleReactiveProps"/> type.
/// Used by parent components to set layout (X, Y, Width, Height) on children without
/// knowing the concrete props type.
/// </summary>
public abstract ConsoleReactiveProps? BaseProps { get; set; }

/// <summary>Renders the component to the console at its current position.</summary>
public abstract void Render();

/// <summary>
/// Invalidates the component's cached render state, causing the next <see cref="Render"/> call
/// to proceed even if props and state have not changed. Use after a screen erase to force repaint.
/// </summary>
public abstract void Invalidate();
}

/// <summary>
Expand All @@ -46,6 +47,13 @@ public abstract class ConsoleReactiveComponent<TProps, TState> : ConsoleReactive
/// <summary>Gets or sets the component's props (external configuration).</summary>
public TProps? Props { get; set; }

/// <inheritdoc/>
public override ConsoleReactiveProps? BaseProps
{
get => this.Props;
set => this.Props = (TProps?)value;
Comment thread
westey-m marked this conversation as resolved.
}

/// <summary>Gets or sets the component's internal state.</summary>
protected TState? State { get; set; }

Expand Down Expand Up @@ -73,8 +81,8 @@ public override void Render()
return;
}

if (ReferenceEquals(this.Props, this._lastRenderedProps)
&& ReferenceEquals(this.State, this._lastRenderedState))
if (EqualityComparer<TProps>.Default.Equals(this.Props, this._lastRenderedProps)
&& EqualityComparer<TState>.Default.Equals(this.State, this._lastRenderedState))
{
return;
}
Expand All @@ -86,6 +94,16 @@ public override void Render()
}
}

/// <inheritdoc/>
public override void Invalidate()
{
lock (this._renderLock)
{
this._lastRenderedProps = default;
this._lastRenderedState = default;
}
}

/// <summary>
/// Called by <see cref="Render"/> to perform the actual rendering. Override this in derived classes.
/// </summary>
Expand All @@ -95,11 +113,23 @@ public override void Render()
}

/// <summary>
/// Base record for component props. Provides an optional <see cref="Children"/> collection
/// for composing child components.
/// Base record for component props. Provides layout properties (position and size)
/// and an optional <see cref="Children"/> collection for composing child components.
/// </summary>
public record ConsoleReactiveProps
{
/// <summary>Gets the 1-based column position of the component.</summary>
public int X { get; init; }

/// <summary>Gets the 1-based row position of the component.</summary>
public int Y { get; init; }

/// <summary>Gets the width of the component in columns.</summary>
public int Width { get; init; }

/// <summary>Gets the height of the component in rows.</summary>
public int Height { get; init; }

/// <summary>Gets the child components to render within this component.</summary>
public IReadOnlyList<ConsoleReactiveComponent> Children { get; init; } = [];
Comment thread
westey-m marked this conversation as resolved.
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public override void RenderCore(AgentModeAndHelpProps props, ConsoleReactiveStat
}

System.Console.Write(AnsiEscapes.SaveCursor);
System.Console.Write(AnsiEscapes.MoveAndEraseLine(this.Y));
System.Console.Write(AnsiEscapes.MoveAndEraseLine(props.Y));

bool hasMode = props.Mode is not null;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ public override void RenderCore(AgentStatusProps props, AgentStatusState state)
}

System.Console.Write(AnsiEscapes.SaveCursor);
System.Console.Write(AnsiEscapes.MoveCursor(this.Y, this.X));
System.Console.Write(AnsiEscapes.MoveCursor(props.Y, props.X));
if (props != this._previousProps)
{
System.Console.Write(AnsiEscapes.EraseToEndOfLine);
Expand Down
Loading
Loading