Editor/Prism/Integration/PrismAutosave.cs

Editor-side autosave system for the Prism editor and its recovery UI. It tracks open PrismWindow instances, periodically serializes dirty PrismGraph sessions to per-document folders under the project autosave root, prunes old snapshots, lists and restores snapshots, and provides a recovery window UI with rows for each snapshot.

File Access
using Editor.Prism.Core;
using Editor.Prism.Model;
using Editor.Prism.Serialization;
using Editor.Prism.Ui;
using System.IO;
using System.Text;

namespace Editor.Prism.Integration;

/// <summary>One retained autosave generation of one document.</summary>
public sealed record PrismAutosaveEntry(
	string File, string DocumentPath, string Title, DateTime TimestampUtc, long Size, bool IsSubgraph )
{
	/// <summary>How long ago this snapshot was taken, in words.</summary>
	public string Age
	{
		get
		{
			var span = DateTime.UtcNow - TimestampUtc;

			if ( span.TotalSeconds < 90 ) return "just now";
			if ( span.TotalMinutes < 90 ) return $"{(int)span.TotalMinutes} minutes ago";
			if ( span.TotalHours < 36 ) return $"{(int)span.TotalHours} hours ago";

			return $"{(int)span.TotalDays} days ago";
		}
	}

	/// <summary>True when the original document is gone, so this snapshot is all that is left.</summary>
	public bool OriginalMissing =>
		string.IsNullOrWhiteSpace( DocumentPath ) || !System.IO.File.Exists( DocumentPath );

	/// <summary>True when this snapshot is newer than the document it came from.</summary>
	public bool NewerThanOriginal
	{
		get
		{
			if ( OriginalMissing ) return true;

			try
			{
				return TimestampUtc > System.IO.File.GetLastWriteTimeUtc( DocumentPath ).AddSeconds( 2 );
			}
			catch ( Exception )
			{
				return true;
			}
		}
	}

	/// <inheritdoc/>
	public override string ToString() => $"{Title} — {Age}";
}

/// <summary>
/// Periodic snapshots of unsaved work, and the recovery pass that offers them back after a crash.
/// <para>
/// Snapshots live under <c>&lt;project&gt;/.sbox/prism/autosave/&lt;document&gt;/</c>, one folder per
/// document, newest last, capped by <see cref="PrismCookies.AutosaveRetained"/>. They are never
/// written over the document itself — restoring is always an explicit choice, and it keeps a
/// <c>.bak</c> of whatever it replaced.
/// </para>
/// <para>
/// Tracking is by <b>window</b>, not by session: the session object behind a window is swapped every
/// time a document is opened or an undo restores the graph, so the window is the stable handle and
/// its <c>Session</c> is re-read on every tick.
/// </para>
/// </summary>
public static class PrismAutosave
{
	static readonly List<PrismWindow> s_windows = new();
	static readonly Dictionary<string, DateTime> s_lastSnapshotUtc = new();
	static DateTime s_nextSweepUtc = DateTime.MinValue;

	/// <summary>Raised after a snapshot is written. The absolute path of the snapshot is passed.</summary>
	public static event Action<string> Snapshotted;

	/// <summary>Absolute path of the autosave root, or null when there is no current project.</summary>
	public static string Root => PrismLog.Guard( "Resolving the Prism autosave root", () =>
	{
		var project = Project.Current?.GetRootPath();

		if ( string.IsNullOrWhiteSpace( project ) ) return null;

		return Path.Combine( project, PrismConstants.AutosaveRoot.Replace( '/', Path.DirectorySeparatorChar ) );
	}, null );

	// ---- tracking ----------------------------------------------------------

	/// <summary>Start autosaving whatever document this window is showing. Idempotent.</summary>
	public static void Track( PrismWindow window )
	{
		if ( window is null ) return;

		PrismLog.Guard( "Tracking a Prism window for autosave", () =>
		{
			Prune();

			foreach ( var existing in s_windows )
			{
				if ( ReferenceEquals( existing, window ) ) return;
			}

			s_windows.Add( window );
		} );
	}

	/// <summary>Stop autosaving a window. Called when it closes.</summary>
	public static void Untrack( PrismWindow window )
	{
		if ( window is null ) return;

		PrismLog.Guard( "Untracking a Prism window", () => s_windows.RemoveAll( x => ReferenceEquals( x, window ) ) );
	}

	/// <summary>Windows currently being watched.</summary>
	public static IReadOnlyList<PrismWindow> Tracked
	{
		get
		{
			Prune();

			return s_windows;
		}
	}

	/// <summary>Forget every tracked window and every timer. Used by teardown, not by hotload.</summary>
	public static void Reset()
	{
		s_windows.Clear();
		s_lastSnapshotUtc.Clear();
		s_nextSweepUtc = DateTime.MinValue;
	}

	/// <summary>
	/// What hotload calls. The window instances are migrated by the hotload system rather than
	/// destroyed, so a window that was open before the reload is still open and still worth watching —
	/// clearing the list here would silently stop autosaving it. Only the dead entries and the
	/// throttle timers go.
	/// </summary>
	public static void Revalidate()
	{
		Prune();

		s_lastSnapshotUtc.Clear();
		s_nextSweepUtc = DateTime.MinValue;
	}

	/// <summary>Drop every <see cref="Snapshotted"/> handler — they point into the outgoing assembly.</summary>
	public static void ClearSubscribers()
	{
		Snapshotted = null;
	}

	static void Prune()
	{
		s_windows.RemoveAll( x => x is null || !x.IsValid );
	}

	// ---- the timer ---------------------------------------------------------

	/// <summary>
	/// Runs once a frame and does nothing at all until the interval is up. Everything expensive —
	/// serializing, writing, sweeping old generations — is behind the timestamp check.
	/// </summary>
	[EditorEvent.Frame]
	static void Frame()
	{
		if ( s_windows.Count == 0 ) return;

		var now = DateTime.UtcNow;

		if ( now < s_nextSweepUtc ) return;

		s_nextSweepUtc = now.AddSeconds( 5 );

		if ( !PrismCookies.AutosaveEnabled ) return;

		PrismLog.Guard( "Running the Prism autosave pass", () => Pass( now ) );
	}

	static void Pass( DateTime now )
	{
		Prune();

		if ( s_windows.Count == 0 ) return;

		var interval = TimeSpan.FromSeconds( PrismCookies.AutosaveIntervalSeconds );
		var open = new List<string>();
		var anyDirty = false;

		foreach ( var window in s_windows )
		{
			var session = PrismLog.Guard( "Reading a Prism session", () => window.Session, null );

			if ( session is null ) continue;

			if ( !string.IsNullOrWhiteSpace( session.FilePath ) ) open.Add( session.FilePath );

			if ( !session.IsDirty ) continue;

			anyDirty = true;

			var key = KeyOf( session );

			if ( string.IsNullOrEmpty( key ) ) continue;

			if ( s_lastSnapshotUtc.TryGetValue( key, out var last ) && now - last < interval ) continue;

			s_lastSnapshotUtc[key] = now;

			Snapshot( session );
		}

		PrismCookies.OpenDocuments = open;

		// The marker is what tells the next startup that the editor went down with work in it.
		if ( anyDirty && !PrismCookies.SessionMarker ) PrismCookies.SessionMarker = true;
	}

	// ---- writing -----------------------------------------------------------

	/// <summary>Snapshot a session now, regardless of the timer. Returns the file written, or null.</summary>
	public static string Snapshot( PrismSession session )
	{
		if ( session is null ) return null;

		return Snapshot( session.Graph, session.FilePath );
	}

	/// <summary>
	/// Snapshot a graph now. <paramref name="documentPath"/> may be null for a document that has never
	/// been saved — it is only used to name the folder and to point recovery back at the original.
	/// </summary>
	public static string Snapshot( PrismGraph graph, string documentPath )
	{
		if ( graph is null ) return null;

		return PrismLog.Guard( "Writing a Prism autosave", () =>
		{
			var folder = FolderFor( graph, documentPath );

			if ( string.IsNullOrWhiteSpace( folder ) ) return null;

			Directory.CreateDirectory( folder );

			var text = PrismSerializer.Write( graph );

			if ( string.IsNullOrEmpty( text ) ) return null;

			var extension = graph.IsSubgraph ? PrismConstants.SubgraphExtension : PrismConstants.GraphExtension;
			var file = Path.Combine( folder, $"{DateTime.UtcNow:yyyyMMdd-HHmmss-fff}.{extension}" );

			File.WriteAllText( file, text, Encoding.UTF8 );
			WriteManifest( folder, graph, documentPath );
			Sweep( folder );

			PrismLog.Trace( $"Autosaved to '{file}'" );
			PrismLog.Guard( "Raising PrismAutosave.Snapshotted", () => Snapshotted?.Invoke( file ) );

			return file;
		}, null );
	}

	static void WriteManifest( string folder, PrismGraph graph, string documentPath )
	{
		var manifest = new JsonObject
		{
			["path"] = documentPath ?? string.Empty,
			["title"] = graph.Meta?.Title ?? Path.GetFileNameWithoutExtension( documentPath ?? "untitled" ),
			["documentId"] = graph.DocumentId ?? string.Empty,
			["subgraph"] = graph.IsSubgraph,
			["editorVersion"] = PrismConstants.EditorVersion
		};

		File.WriteAllText( Path.Combine( folder, "manifest.json" ), manifest.ToJsonString(), Encoding.UTF8 );
	}

	static void Sweep( string folder )
	{
		var keep = PrismCookies.AutosaveRetained;

		var files = Directory.GetFiles( folder )
			.Where( x => !x.EndsWith( "manifest.json", StringComparison.OrdinalIgnoreCase ) )
			.OrderBy( x => x, StringComparer.Ordinal )
			.ToList();

		for ( var index = 0; index < files.Count - keep; index++ )
		{
			try
			{
				File.Delete( files[index] );
			}
			catch ( Exception e )
			{
				PrismLog.Trace( $"Could not remove the old autosave '{files[index]}': {e.Message}" );
			}
		}
	}

	// ---- reading -----------------------------------------------------------

	/// <summary>Every document that has at least one retained snapshot, newest snapshot first.</summary>
	public static IReadOnlyList<PrismAutosaveEntry> All()
	{
		return PrismLog.Guard( "Listing Prism autosaves", () =>
		{
			var root = Root;
			var results = new List<PrismAutosaveEntry>();

			if ( string.IsNullOrWhiteSpace( root ) || !Directory.Exists( root ) ) return (IReadOnlyList<PrismAutosaveEntry>)results;

			foreach ( var folder in Directory.GetDirectories( root ) )
			{
				var newest = Newest( folder );

				if ( newest is not null ) results.Add( newest );
			}

			return (IReadOnlyList<PrismAutosaveEntry>)results
				.OrderByDescending( x => x.TimestampUtc )
				.ToList();
		}, Array.Empty<PrismAutosaveEntry>() );
	}

	/// <summary>Every retained snapshot of one document, newest first.</summary>
	public static IReadOnlyList<PrismAutosaveEntry> History( string documentPath )
	{
		return PrismLog.Guard( "Listing the autosave history of a document", () =>
		{
			var folder = FolderFor( null, documentPath );

			if ( string.IsNullOrWhiteSpace( folder ) || !Directory.Exists( folder ) )
			{
				return (IReadOnlyList<PrismAutosaveEntry>)Array.Empty<PrismAutosaveEntry>();
			}

			return (IReadOnlyList<PrismAutosaveEntry>)Entries( folder )
				.OrderByDescending( x => x.TimestampUtc )
				.ToList();
		}, Array.Empty<PrismAutosaveEntry>() );
	}

	static PrismAutosaveEntry Newest( string folder )
	{
		return Entries( folder ).OrderByDescending( x => x.TimestampUtc ).FirstOrDefault();
	}

	static List<PrismAutosaveEntry> Entries( string folder )
	{
		var results = new List<PrismAutosaveEntry>();

		string documentPath = null;
		string title = Path.GetFileName( folder );
		var subgraph = false;

		var manifestPath = Path.Combine( folder, "manifest.json" );

		if ( File.Exists( manifestPath ) )
		{
			PrismLog.Guard( "Reading an autosave manifest", () =>
			{
				var manifest = JsonNode.Parse( File.ReadAllText( manifestPath ) ) as JsonObject;

				if ( manifest is null ) return;

				documentPath = manifest["path"]?.GetValue<string>();
				title = manifest["title"]?.GetValue<string>() ?? title;
				subgraph = manifest["subgraph"]?.GetValue<bool>() ?? false;
			} );
		}

		if ( string.IsNullOrWhiteSpace( documentPath ) ) documentPath = null;

		foreach ( var file in Directory.GetFiles( folder ) )
		{
			if ( file.EndsWith( "manifest.json", StringComparison.OrdinalIgnoreCase ) ) continue;

			var info = new FileInfo( file );

			results.Add( new PrismAutosaveEntry( file, documentPath, title,
				info.LastWriteTimeUtc, info.Length, subgraph ) );
		}

		return results;
	}

	// ---- restoring ---------------------------------------------------------

	/// <summary>
	/// Put a snapshot back over the document it came from, keeping a <c>.bak</c> of whatever was
	/// there. Returns the path that now holds the recovered document.
	/// </summary>
	public static string Restore( PrismAutosaveEntry entry )
	{
		if ( entry is null || !File.Exists( entry.File ) ) return null;

		return PrismLog.Guard( "Restoring a Prism autosave", () =>
		{
			var destination = entry.DocumentPath;

			if ( string.IsNullOrWhiteSpace( destination ) ) return null;

			var directory = Path.GetDirectoryName( destination );

			if ( !string.IsNullOrWhiteSpace( directory ) ) Directory.CreateDirectory( directory );

			if ( File.Exists( destination ) )
			{
				var backup = destination + ".bak";

				try
				{
					File.Copy( destination, backup, true );
				}
				catch ( Exception e )
				{
					PrismLog.Warn( $"Could not back up '{destination}' before restoring: {e.Message}" );
				}
			}

			File.Copy( entry.File, destination, true );

			PrismLog.Guard( "Registering the restored document", () => AssetSystem.RegisterFile( destination ) );
			PrismLog.Info( $"Restored '{Path.GetFileName( destination )}' from the autosave taken {entry.Age}" );

			return destination;
		}, null );
	}

	/// <summary>Delete every snapshot of one document.</summary>
	public static void Discard( PrismAutosaveEntry entry )
	{
		if ( entry is null ) return;

		PrismLog.Guard( "Discarding a Prism autosave", () =>
		{
			var folder = Path.GetDirectoryName( entry.File );

			if ( string.IsNullOrWhiteSpace( folder ) || !Directory.Exists( folder ) ) return;

			Directory.Delete( folder, true );
		} );
	}

	/// <summary>Delete every snapshot of every document.</summary>
	public static void DiscardAll()
	{
		PrismLog.Guard( "Discarding every Prism autosave", () =>
		{
			var root = Root;

			if ( string.IsNullOrWhiteSpace( root ) || !Directory.Exists( root ) ) return;

			Directory.Delete( root, true );
		} );

		PrismCookies.SessionMarker = false;
	}

	// ---- crash recovery ----------------------------------------------------

	/// <summary>
	/// Startup pass. Only offers anything when the previous session left the dirty marker set, so a
	/// clean shutdown never nags — the snapshots are still there for <c>Prism ▸ Recover Autosave</c>.
	/// </summary>
	public static void ScanForRecovery()
	{
		PrismLog.Guard( "Scanning for recoverable Prism documents", () =>
		{
			if ( !PrismCookies.SessionMarker ) return;

			PrismCookies.SessionMarker = false;

			var recoverable = All().Where( x => x.NewerThanOriginal ).ToList();

			if ( recoverable.Count == 0 ) return;

			PrismLog.Warn( $"Prism found {recoverable.Count} document(s) with unsaved changes from the previous session" );

			ShowRecovery( recoverable, true );
		} );
	}

	/// <summary>Open the recovery window. Pass false to skip it when there is nothing to show.</summary>
	public static void ShowRecovery( bool force )
	{
		var entries = All().ToList();

		if ( entries.Count == 0 && !force ) return;

		ShowRecovery( entries, false );
	}

	/// <summary>Open the recovery window filtered to one document's history.</summary>
	public static void ShowRecovery( string documentPath )
	{
		ShowRecovery( History( documentPath ).ToList(), false );
	}

	static void ShowRecovery( List<PrismAutosaveEntry> entries, bool afterCrash )
	{
		PrismLog.Guard( "Showing the Prism recovery window", () =>
		{
			var window = new PrismRecoveryWindow( entries, afterCrash );

			window.Show();
		} );
	}

	// ---- shutdown ----------------------------------------------------------

	/// <summary>
	/// Take a final snapshot of anything still dirty and decide whether the next startup should offer
	/// recovery. Exiting with unsaved work leaves the marker set on purpose.
	/// </summary>
	[Event( "app.exit" )]
	static void OnExit()
	{
		PrismLog.Guard( "Finishing the Prism autosave on exit", () =>
		{
			Prune();

			var dirty = false;

			foreach ( var window in s_windows )
			{
				var session = PrismLog.Guard( "Reading a Prism session on exit", () => window.Session, null );

				if ( session is null || !session.IsDirty ) continue;

				dirty = true;

				Snapshot( session );
			}

			PrismCookies.SessionMarker = dirty;
		} );
	}

	// ---- paths -------------------------------------------------------------

	static string KeyOf( PrismSession session )
	{
		if ( session is null ) return null;
		if ( !string.IsNullOrWhiteSpace( session.FilePath ) ) return session.FilePath.ToLowerInvariant();

		return "untitled:" + ( session.Graph?.DocumentId ?? "0" );
	}

	/// <summary>
	/// The folder that holds one document's snapshots: a readable name plus a hash of the full path,
	/// so two <c>base.prism</c> files in different folders never share a history.
	/// </summary>
	static string FolderFor( PrismGraph graph, string documentPath )
	{
		var root = Root;

		if ( string.IsNullOrWhiteSpace( root ) ) return null;

		string name;
		string discriminator;

		if ( string.IsNullOrWhiteSpace( documentPath ) )
		{
			name = "untitled";
			discriminator = graph?.DocumentId ?? "unknown";
		}
		else
		{
			name = Path.GetFileNameWithoutExtension( documentPath );
			discriminator = ShortHash( documentPath.ToLowerInvariant() );
		}

		return Path.Combine( root, $"{Sanitize( name )}-{Sanitize( discriminator )}" );
	}

	static string Sanitize( string value )
	{
		if ( string.IsNullOrWhiteSpace( value ) ) return "untitled";

		var builder = new StringBuilder( value.Length );

		foreach ( var character in value )
		{
			builder.Append( char.IsLetterOrDigit( character ) || character is '-' or '_' ? character : '_' );
		}

		return builder.ToString();
	}

	static string ShortHash( string value )
	{
		unchecked
		{
			var hash = 2166136261u;

			foreach ( var character in value )
			{
				hash = ( hash ^ character ) * 16777619u;
			}

			return hash.ToString( "x8" );
		}
	}
}

/// <summary>
/// The window that offers autosaved work back.
/// <para>
/// It opens by itself exactly once, on the first startup after the editor went down with unsaved
/// changes. Every other time it is reached deliberately from <c>Prism ▸ Recover Autosave</c> or from
/// a document's context menu, where it doubles as a snapshot history browser.
/// </para>
/// </summary>
public sealed class PrismRecoveryWindow : BaseWindow
{
	readonly List<PrismAutosaveEntry> _entries;
	readonly bool _afterCrash;

	Widget _list;

	/// <summary>Build the window over a fixed set of snapshots.</summary>
	public PrismRecoveryWindow( List<PrismAutosaveEntry> entries, bool afterCrash )
	{
		_entries = entries ?? new List<PrismAutosaveEntry>();
		_afterCrash = afterCrash;

		WindowTitle = afterCrash ? "Prism — Recover Unsaved Work" : "Prism Autosaves";
		SetWindowIcon( afterCrash ? "restore_page" : "history" );

		Size = new Vector2( 760f, 520f );
		MinimumSize = new Vector2( 560f, 320f );

		Layout = Layout.Column();
		Layout.Margin = 20f;
		Layout.Spacing = 12f;

		BuildHeader();
		BuildList();
		BuildFooter();
	}

	void BuildHeader()
	{
		var title = Layout.Add( new Label.Title( _afterCrash
			? "Prism closed with unsaved changes"
			: "Autosave history" ) );

		title.Color = PrismTheme.TextPrimary;

		var body = Layout.Add( new Label.Body( _afterCrash
			? "These documents have a snapshot newer than the file on disk. Restoring writes the snapshot "
				+ "over the document and keeps a .bak of what was there."
			: "Snapshots are taken while a document has unsaved changes, and the newest few are kept per "
				+ "document. Restoring keeps a .bak of the file it replaces." ) );

		body.Color = PrismTheme.TextSecondary;
		body.WordWrap = true;
	}

	void BuildList()
	{
		var scroll = new ScrollArea( this );

		scroll.Canvas = new Widget( scroll );
		scroll.Canvas.Layout = Layout.Column();
		scroll.Canvas.Layout.Spacing = 6f;

		_list = scroll.Canvas;

		Layout.Add( scroll, 1 );

		Populate();
	}

	void Populate()
	{
		if ( _list is null ) return;

		_list.Layout.Clear( true );

		if ( _entries.Count == 0 )
		{
			var empty = _list.Layout.Add( new Label.Body(
				"Nothing has been autosaved yet. Prism snapshots a document only while it has unsaved changes." ) );

			empty.Color = PrismTheme.TextMuted;
			empty.WordWrap = true;
			_list.Layout.AddStretchCell();

			return;
		}

		foreach ( var entry in _entries )
		{
			_list.Layout.Add( new PrismRecoveryRow( _list, entry, Refresh ) );
		}

		_list.Layout.AddStretchCell();
	}

	void Refresh()
	{
		_entries.RemoveAll( x => x is null || !System.IO.File.Exists( x.File ) );

		Populate();
	}

	void BuildFooter()
	{
		var row = Layout.AddRow();

		row.Spacing = 8f;

		var discard = row.Add( new Button.Clear( "Discard All Autosaves", "delete_sweep", this ) );

		discard.Clicked = () =>
		{
			PrismAutosave.DiscardAll();
			_entries.Clear();
			Populate();
		};

		row.AddStretchCell();

		var close = row.Add( new Button.Primary( "Done", "check", this ) );

		close.Clicked = Close;
	}
}

/// <summary>One row of <see cref="PrismRecoveryWindow"/>: a snapshot and what can be done with it.</summary>
internal sealed class PrismRecoveryRow : Widget
{
	readonly PrismAutosaveEntry _entry;
	readonly Action _changed;

	/// <summary>Build a row for one snapshot.</summary>
	public PrismRecoveryRow( Widget parent, PrismAutosaveEntry entry, Action changed ) : base( parent )
	{
		_entry = entry;
		_changed = changed;

		MinimumSize = new Vector2( 0f, 62f );

		Layout = Layout.Row();
		Layout.Margin = 12f;
		Layout.Spacing = 10f;

		var text = Layout.AddColumn( 1 );

		text.Spacing = 2f;

		var title = text.Add( new Label( entry.Title ?? "Untitled", this ) );

		title.Color = PrismTheme.TextPrimary;

		var detail = text.Add( new Label( Detail(), this ) );

		detail.Color = entry.OriginalMissing ? PrismTheme.Warning : PrismTheme.TextMuted;

		Layout.AddStretchCell();

		if ( !entry.OriginalMissing )
		{
			var restore = Layout.Add( new Button( "Restore", "restore_page", this ) );

			restore.Clicked = () =>
			{
				var restored = PrismAutosave.Restore( _entry );

				if ( !string.IsNullOrWhiteSpace( restored ) ) PrismAssetEditor.Open( restored );

				_changed?.Invoke();
			};
		}

		var open = Layout.Add( new Button( "Open Copy", "open_in_new", this ) );

		open.Clicked = () => PrismLauncher.OpenDocument( _entry.File );
		open.StatusTip = "Open the snapshot itself, leaving the document on disk untouched";

		var discard = Layout.Add( new Button.Clear( "", "delete", this ) );

		discard.Clicked = () =>
		{
			PrismAutosave.Discard( _entry );
			_changed?.Invoke();
		};

		discard.StatusTip = "Delete every snapshot of this document";
	}

	string Detail()
	{
		if ( _entry.OriginalMissing )
		{
			return $"{_entry.Age} · the original document is gone — open the copy and save it somewhere";
		}

		return $"{_entry.Age} · {_entry.DocumentPath}";
	}

	/// <summary>Card background, so rows read as separate items rather than a wall of text.</summary>
	protected override void OnPaint()
	{
		Paint.Antialiasing = true;
		Paint.ClearPen();
		Paint.SetBrush( _entry.NewerThanOriginal ? PrismTheme.Elevated : PrismTheme.PanelAlt );
		Paint.DrawRect( LocalRect, PrismTheme.RadiusPanel );

		if ( !_entry.NewerThanOriginal ) return;

		Paint.ClearPen();
		Paint.SetBrush( PrismTheme.Accent );
		Paint.DrawRect( new Rect( LocalRect.Left, LocalRect.Top, PrismTheme.AccentBarWidth, LocalRect.Height ),
			PrismTheme.AccentBarWidth * 0.5f );
	}
}