Editor/Panels/GalleryPanel.cs

An editor UI panel that displays a gallery of captured screenshots. It lays out thumbnail widgets in a responsive grid, handles resizing, rebuilds when the gallery changes, and provides context menu actions to open, save, post, reveal, or delete shots.

File Access
using System;
using Sandbox;

namespace Editor.SuperShot;

public sealed class GalleryPanel : Widget
{
	readonly SuperShotWindow _window;
	Widget _grid;
	int _columns;

	const int ThumbWidthPx = 156;
	const int ThumbHeightPx = 100;
	const int GridSpacing = 6;

	public GalleryPanel( SuperShotWindow window ) : base( null )
	{
		_window = window;
		Name = "Gallery";
		WindowTitle = "Gallery";
		SetWindowIcon( "collections" );
		Layout = Layout.Column();
		Layout.Margin = 6;
		Layout.Spacing = 4;

		var toolbar = Layout.AddRow();
		toolbar.Spacing = 4;
		toolbar.Add( new Button( "Refresh", "refresh" )
		{
			ToolTip = "Re-scan the output folder for saved shots",
			Clicked = () => _window.RefreshGalleryFromDisk()
		} );
		toolbar.AddStretchCell();

		var scroll = new ScrollArea( this );
		scroll.Canvas = new Widget( scroll );
		scroll.Canvas.Layout = Layout.Column();
		scroll.Canvas.Layout.Spacing = GridSpacing;
		scroll.Canvas.Layout.Margin = 4;
		_grid = scroll.Canvas;

		Layout.Add( scroll, 1 );

		_window.Changed += Rebuild;
		Rebuild();
	}

	int ComputeColumns()
	{
		float avail = _grid.Width;
		if ( avail <= 1 )
			return Math.Max( 1, _columns );

		return Math.Max( 1, (int)((avail - 8 + GridSpacing) / (ThumbWidthPx + GridSpacing)) );
	}

	[EditorEvent.Frame]
	void OnFrame()
	{
		if ( _window.Gallery.Count == 0 )
			return;

		if ( ComputeColumns() != _columns )
			Rebuild();
	}

	void Rebuild()
	{
		_grid.Layout.Clear( true );

		if ( _window.Gallery.Count == 0 )
		{
			_columns = 0;
			_grid.Layout.Add( new Label( "No shots yet - press Capture to start. Saved shots land here; click a thumbnail to open it in the Edit tab, right-click for save/post/delete." ) );
			return;
		}

		_columns = ComputeColumns();

		Layout row = null;
		int inRow = 0;

		for ( int i = _window.Gallery.Count - 1; i >= 0; i-- )
		{
			if ( inRow == 0 )
			{
				row = _grid.Layout.AddRow();
				row.Spacing = GridSpacing;
			}

			row.Add( new ThumbWidget( _window, _window.Gallery[i] ) );
			inRow++;

			if ( inRow >= _columns )
			{
				row.AddStretchCell();
				inRow = 0;
			}
		}

		if ( inRow > 0 )
			row.AddStretchCell();

		_grid.Layout.AddStretchCell();
	}

	public override void OnDestroyed()
	{
		base.OnDestroyed();
		_window.Changed -= Rebuild;
	}

	sealed class ThumbWidget : Widget
	{
		readonly SuperShotWindow _window;
		readonly CapturedShot _shot;

		public ThumbWidget( SuperShotWindow window, CapturedShot shot ) : base( null )
		{
			_window = window;
			_shot = shot;
			FixedSize = new Vector2( ThumbWidthPx, ThumbHeightPx );
			Cursor = CursorShape.Finger;
			ToolTip = $"{shot.Size.x}x{shot.Size.y}  {shot.Time:HH:mm:ss}";
		}

		protected override void OnPaint()
		{
			Paint.ClearPen();
			Paint.SetBrush( Color.Black );
			Paint.DrawRect( LocalRect );

			if ( _shot.Thumbnail is not null )
			{
				var rect = FitRect( _shot.Thumbnail.Size, new Vector2( Width, Height - 16 ) );
				Paint.Draw( rect, _shot.Thumbnail );
			}

			Paint.SetPen( Theme.TextControl.WithAlpha( 0.8f ) );
			var label = _shot.SavedPath is not null ? $"{_shot.Size.x}x{_shot.Size.y} (saved)" : $"{_shot.Size.x}x{_shot.Size.y}";
			Paint.DrawText( new Rect( 0, Height - 16, Width, 16 ), label, TextFlag.Center );
		}

		protected override void OnMouseClick( MouseEvent e )
		{
			base.OnMouseClick( e );

			if ( e.LeftMouseButton )
			{
				_window.OpenInEditor( _shot );
			}
			else if ( e.RightMouseButton )
			{
				var menu = new Menu();
				menu.AddOption( "Open in Edit Tab", "edit", () => _window.OpenInEditor( _shot ) );
				menu.AddOption( "Save", "save", () =>
				{
					_window.LoadFromGallery( _shot );
					_window.SaveCurrent();
				} );
				menu.AddOption( "Post to All Channels", "send", () =>
				{
					_window.LoadFromGallery( _shot );
					_ = _window.PostCurrentToAll();
				} );
				if ( _shot.SavedPath is not null )
					menu.AddOption( "Open File Location", "folder", () => SuperShotService.RevealInExplorer( _shot.SavedPath ) );
				menu.AddSeparator();
				menu.AddOption( "Delete", "delete", () => _window.RemoveFromGallery( _shot ) );
				menu.OpenAtCursor();
			}
		}

		static Rect FitRect( Vector2 content, Vector2 area )
		{
			float scale = Math.Min( area.x / content.x, area.y / content.y );
			var size = content * scale;
			return new Rect( (area.x - size.x) / 2f, (area.y - size.y) / 2f, size.x, size.y );
		}
	}
}