// Copyright (c) Microsoft. All rights reserved.
namespace Harness.ConsoleReactiveFramework;
///
/// Abstract base class for all console UI components. Provides access to layout
/// through and a method for drawing to the console.
/// Derive from instead of this class directly.
///
public abstract class ConsoleReactiveComponent
{
internal ConsoleReactiveComponent()
{
}
///
/// Gets or sets the component's props as the base type.
/// Used by parent components to set layout (X, Y, Width, Height) on children without
/// knowing the concrete props type.
///
public abstract ConsoleReactiveProps? BaseProps { get; set; }
/// Renders the component to the console at its current position.
public abstract void Render();
///
/// Invalidates the component's cached render state, causing the next call
/// to proceed even if props and state have not changed. Use after a screen erase to force repaint.
///
public abstract void Invalidate();
}
///
/// Generic base class for console UI components with typed props and state.
/// Props represent externally supplied configuration; state represents internal mutable data.
///
/// The type of the component's props (external configuration).
/// The type of the component's internal state.
public abstract class ConsoleReactiveComponent : ConsoleReactiveComponent
where TProps : ConsoleReactiveProps
where TState : ConsoleReactiveState
{
private readonly object _renderLock = new();
private TProps? _lastRenderedProps;
private TState? _lastRenderedState;
/// Gets or sets the component's props (external configuration).
public TProps? Props { get; set; }
///
public override ConsoleReactiveProps? BaseProps
{
get => this.Props;
set => this.Props = (TProps?)value;
}
/// Gets or sets the component's internal state.
protected TState? State { get; set; }
///
/// Updates the component's state and triggers a re-render.
///
/// The new state value.
public void SetState(TState newState)
{
this.State = newState;
this.Render();
}
///
/// Renders the component using the current props and state.
/// Uses a lock to prevent concurrent renders from multiple sources.
/// Skips rendering if neither props nor state have changed since the last render.
///
public override void Render()
{
lock (this._renderLock)
{
if (this.Props is null)
{
return;
}
if (EqualityComparer.Default.Equals(this.Props, this._lastRenderedProps)
&& EqualityComparer.Default.Equals(this.State, this._lastRenderedState))
{
return;
}
this.RenderCore(this.Props, this.State!);
this._lastRenderedProps = this.Props;
this._lastRenderedState = this.State;
}
}
///
public override void Invalidate()
{
lock (this._renderLock)
{
this._lastRenderedProps = default;
this._lastRenderedState = default;
}
}
///
/// Called by to perform the actual rendering. Override this in derived classes.
///
/// The current props.
/// The current state.
public abstract void RenderCore(TProps props, TState state);
}
///
/// Base record for component props. Provides layout properties (position and size)
/// and an optional collection for composing child components.
///
public record ConsoleReactiveProps
{
/// Gets the 1-based column position of the component.
public int X { get; init; }
/// Gets the 1-based row position of the component.
public int Y { get; init; }
/// Gets the width of the component in columns.
public int Width { get; init; }
/// Gets the height of the component in rows.
public int Height { get; init; }
/// Gets the child components to render within this component.
public IReadOnlyList Children { get; init; } = [];
}
///
/// Base record for component state.
///
public record ConsoleReactiveState;