A UI popup showing recent text prompts for the TextToAnimation editor. It lists prompt history with search/filter, shows time/mode/associated clip, and allows reusing a prompt or opening the created animation clip if it still exists.
using System;
using System.Collections.Generic;
using System.Linq;
using Editor;
using Sandbox;
using TextToAnimation.Animation;
using TextToAnimation.Editor.Session;
using TextToAnimation.Editor.Workspace;
namespace TextToAnimation.Editor.UI;
/// <summary>
/// A small popup with the prompts sent for this model, newest first. Clicking one puts it back in the
/// prompt; the arrow opens the animation it made (when it still exists).
/// </summary>
public sealed class PromptHistoryPopup : PopupWidget
{
readonly EditorSession _session;
readonly Action<string> _reuse;
readonly LineEdit _filter;
readonly Widget _list;
public PromptHistoryPopup( Widget parent, EditorSession session, Action<string> reuse ) : base( parent )
{
_session = session;
_reuse = reuse;
FixedWidth = 420;
WindowTitle = "Text to Animation - Recent prompts";
Layout = Layout.Column();
Layout.Margin = 10;
Layout.Spacing = 6;
var header = Layout.AddRow();
header.Spacing = 6;
var title = header.Add( new Label( "Recent prompts", this ) );
title.SetStyles( "font-weight: 600;" );
header.AddStretchCell();
header.Add( TaStyle.Muted( new Label( session.PromptHistory.Count == 0 ? "" : $"{session.PromptHistory.Count}", this ), small: true ) );
_filter = Layout.Add( TaStyle.Framed( new LineEdit( this ) { PlaceholderText = "Search prompts…" } ) );
_filter.TextEdited += _ => Rebuild();
_filter.Visible = session.PromptHistory.Count > 6;
var scroll = Layout.Add( new ScrollArea( this ), 1 );
scroll.HorizontalScrollbarMode = ScrollbarMode.Off;
scroll.SetStyles( "background-color: transparent;" );
_list = new Widget( scroll ) { Layout = Layout.Column() };
_list.SetStyles( "background-color: transparent;" );
_list.Layout.Spacing = 2;
scroll.Canvas = _list;
FixedHeight = Math.Clamp( 74 + session.PromptHistory.Count * 48 + (_filter.Visible ? 32 : 0), 110, 420 );
Rebuild();
}
/// <summary>Opens above <paramref name="anchor"/> (the prompt sits low in the window).</summary>
public void OpenAbove( Widget anchor )
{
var cursor = global::Editor.Application.CursorPosition;
OpenAt( new Vector2( cursor.x - 30, cursor.y - Height - 14 ), animateOffset: new Vector2( 0, 8 ) );
if ( _filter.Visible ) _filter.Focus();
}
void Rebuild()
{
_list.Layout.Clear( true );
var filter = _filter.Text?.Trim() ?? "";
var entries = _session.PromptHistory.Where( e => filter.Length == 0 || e.Prompt.Contains( filter, StringComparison.OrdinalIgnoreCase ) ).ToList();
if ( entries.Count == 0 )
_list.Layout.Add( TaStyle.Muted( new Label( _session.PromptHistory.Count == 0 ? "Prompts you send appear here." : "Nothing matches.", _list ) { Alignment = TextFlag.Center, FixedHeight = 40 } ) );
foreach ( var entry in entries.Take( 200 ) )
{
var clip = entry.ClipIds.Select( id => _session.Workspace?.Find( id ) ).FirstOrDefault( c => c is not null );
var e = entry;
_list.Layout.Add( new Row( _list, entry, clip,
() => { _reuse( e.Prompt ); Close(); },
clip is null ? null : () => { _session.SelectClip( clip ); Close(); } ) );
}
_list.Layout.AddStretchCell();
}
static string Ago( DateTime utc )
{
var span = DateTime.UtcNow - utc;
if ( span.TotalMinutes < 1 ) return "just now";
if ( span.TotalHours < 1 ) return $"{(int)span.TotalMinutes} min ago";
if ( span.TotalDays < 1 ) return $"{(int)span.TotalHours} h ago";
if ( span.TotalDays < 30 ) return $"{(int)span.TotalDays} d ago";
return utc.ToLocalTime().ToString( "d MMM yyyy" );
}
static string ModeName( string mode ) => mode switch
{
"InBetween" => "In-between",
"TextEdit" => "Change",
"Variation" => "Variation",
"Expansion" => "Sequence",
_ => "New",
};
sealed class Row : Widget
{
readonly PromptHistoryEntry _entry;
readonly AnimClip _clip;
readonly Action _reuse;
public Row( Widget parent, PromptHistoryEntry entry, AnimClip clip, Action reuse, Action open ) : base( parent )
{
_entry = entry;
_clip = clip;
_reuse = reuse;
FixedHeight = 46;
Cursor = CursorShape.Finger;
MouseTracking = true;
ToolTip = $"{entry.Prompt}\nClick to use this prompt again";
Layout = Layout.Row();
Layout.Margin = new Sandbox.UI.Margin( 0, 0, 6, 0 );
Layout.AddStretchCell();
if ( open is not null )
Layout.Add( TaStyle.Icon( this, "open_in_new", open, $"Open {clip.Name}", 26 ) );
}
protected override void OnMouseEnter() => Update();
protected override void OnMouseLeave() => Update();
protected override void OnMousePress( MouseEvent e )
{
if ( e.LeftMouseButton ) e.Accepted = true;
}
protected override void OnMouseReleased( MouseEvent e )
{
base.OnMouseReleased( e );
if ( e.LeftMouseButton && LocalRect.IsInside( e.LocalPosition ) ) _reuse();
}
protected override void OnPaint()
{
Paint.Antialiasing = true;
if ( Paint.HasMouseOver )
{
Paint.ClearPen();
Paint.SetBrush( TaStyle.Accent.WithAlpha( .16f ) );
Paint.DrawRect( LocalRect, 4 );
}
Paint.SetPen( TaStyle.AccentLight );
Paint.DrawIcon( new Rect( 6, 6, 18, 18 ), "history", 15, TextFlag.Center );
var width = Width - 40 - (_clip is null ? 8 : 34);
Paint.SetDefaultFont( 9 );
Paint.SetPen( Theme.Text );
Paint.DrawText( new Rect( 30, 5, width, 18 ), _entry.Prompt, TextFlag.LeftCenter | TextFlag.SingleLine );
Paint.SetDefaultFont( 8 );
Paint.SetPen( Theme.TextLight );
var made = _clip is not null ? _clip.Name : string.IsNullOrEmpty( _entry.ClipName ) ? "" : $"{_entry.ClipName} (deleted)";
var meta = string.Join( " · ", new[] { ModeName( _entry.Mode ), made, Ago( _entry.CreatedUtc ) }.Where( s => s.Length > 0 ) );
Paint.DrawText( new Rect( 30, 24, width, 16 ), meta, TextFlag.LeftCenter | TextFlag.SingleLine );
}
}
}