// 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;