Editor/Panels/CapturePanel.cs

An editor UI panel for the SuperShot tool, named Home. It builds the capture/home view with cards and controls to quick-capture, open gallery/editor, show recent shots and output folder, and presents small custom widgets for summary pills, workspace cards and recent shot buttons.

File Access
using System;
using Sandbox;

namespace Editor.SuperShot;

public sealed class CapturePanel : Widget
{
	readonly SuperShotWindow _window;

	public CapturePanel( SuperShotWindow window ) : base( null )
	{
		_window = window;
		Name = "Home";
		WindowTitle = "Home";
		SetWindowIcon( "home" );
		Layout = Layout.Column();
		Layout.Margin = 8;
		Layout.Spacing = 8;

		Build();
	}

	void Build()
	{
		var capture = _window.Settings.Capture;

		SuperShotUI.AddBanner( Layout, "Home", "Capture, edit, filter, share.", "home" );

		var scroll = new ScrollArea( this );
		scroll.Canvas = new Widget( scroll );
		scroll.Canvas.Layout = Layout.Column();
		scroll.Canvas.Layout.Spacing = 10;
		var body = scroll.Canvas.Layout;

		var so = capture.GetSerialized();
		so.OnPropertyChanged += _ =>
		{
			_window.Settings.Save();
			_window.NotifyChanged();
		};

		AddHeroCapture( body );
		AddWorkspaceCards( body );

		var quickRow = body.AddRow();
		quickRow.Spacing = 10;
		AddQuickCaptureCard( quickRow );
		AddPackageCard( quickRow );

		var bottomRow = body.AddRow();
		bottomRow.Spacing = 10;
		AddRecentShotsCard( bottomRow );
		AddOutputCard( bottomRow );

		body.AddStretchCell();
		Layout.Add( scroll, 1 );
	}

	static bool IsEssential( SerializedProperty p )
	{
		return p.Name is nameof( CaptureSettings.Source )
			or nameof( CaptureSettings.Resolution )
			or nameof( CaptureSettings.CustomWidth )
			or nameof( CaptureSettings.CustomHeight );
	}

	void AddHeroCapture( Layout body )
	{
		var card = SuperShotUI.AddCard( body, "Current View", "photo_camera" );
		card.Body.Add( new Label( "Shoot the editor camera using your current resolution and default filter." ) { WordWrap = true } );

		var summary = card.Body.AddRow();
		summary.Spacing = 8;
		AddSummaryPill( summary, "Resolution", ResolutionLabel( _window.Settings.Capture ) );
		AddSummaryPill( summary, "Source", SourceLabel( _window.Settings.Capture.Source ) );
		AddSummaryPill( summary, "Default Filter", DefaultFilterLabel() );
		AddSummaryPill( summary, "Game UI", _window.Settings.Capture.ShowGameUI ? "Shown" : "Hidden" );
		AddSummaryPill( summary, "Format", _window.Settings.Output.Format.ToString().ToUpper() );

		var actions = card.Body.AddRow();
		actions.Spacing = 6;
		actions.Add( new Button.Primary( "Capture Current View", "photo_camera" )
		{
			FixedHeight = Theme.RowHeight * 2,
			Clicked = () => _window.Capture()
		}, 1 );
		actions.Add( new Button( "Capture + Open Editor", "edit" )
		{
			FixedHeight = Theme.RowHeight * 2,
			Clicked = CaptureAndOpenEditor
		} );
		actions.Add( new Button( "Output Folder", "folder_open" )
		{
			FixedHeight = Theme.RowHeight * 2,
			Clicked = () => SuperShotService.RevealInExplorer( _window.Settings.Output.ResolveFolder() )
		} );
	}

	void AddWorkspaceCards( Layout body )
	{
		var row = body.AddRow();
		row.Spacing = 10;
		row.Add( new WorkspaceCard( "Editor", "tune", "Layer, text and compose captured shots.", () => _window.DockManager.RaiseDock( "Edit" ) ), 1 );
		row.Add( new WorkspaceCard( "Gallery", "collections", "Browse, reopen and manage saved shots.", () => _window.DockManager.RaiseDock( "Gallery" ) ), 1 );
		row.Add( new WorkspaceCard( "Settings", "settings", "Output folder, format and naming options.", () => _window.DockManager.RaiseDock( "Settings" ) ), 1 );
	}

	void AddQuickCaptureCard( Layout parent )
	{
		var quick = new Card( "Quick Capture", "bolt" );
		parent.Add( quick, 1 );
		AddPresetRow( quick.Body, clean: false,
			("720p", ShotResolution.HD720),
			("1080p", ShotResolution.HD1080),
			("1440p", ShotResolution.QHD1440) );
		AddPresetRow( quick.Body, clean: false,
			("4K", ShotResolution.UHD4K),
			("8K", ShotResolution.UHD8K),
			("Square", ShotResolution.Square1080) );
	}

	void AddPackageCard( Layout parent )
	{
		var pkg = new Card( "Package Thumbnails", "inventory_2" );
		parent.Add( pkg, 1 );
		pkg.Body.Add( new Label( "Clean captures sized for s&box package artwork." ) { WordWrap = true } );
		AddPresetRow( pkg.Body, clean: true,
			("Square 512", ShotResolution.PackageSquare),
			("Wide 910", ShotResolution.PackageWide),
			("Tall 512", ShotResolution.PackageTall) );
		pkg.Body.Add( new Button( "Capture All Three", "burst_mode" ) { Clicked = () => SuperShotService.CaptureAllPackageThumbnails() } );
	}

	void AddRecentShotsCard( Layout parent )
	{
		var recent = new Card( "Recent Shots", "history" );
		parent.Add( recent, 1 );

		if ( _window.Gallery.Count == 0 )
		{
			recent.Body.Add( new Label( "No shots yet. Capture something and your recent work will appear here." ) { WordWrap = true } );
			return;
		}

		var row = recent.Body.AddRow();
		row.Spacing = 6;
		int shown = 0;
		for ( int i = _window.Gallery.Count - 1; i >= 0 && shown < 5; i--, shown++ )
		{
			row.Add( new RecentShotButton( _window, _window.Gallery[i] ) );
		}
		row.AddStretchCell();
		recent.Body.Add( new Button( "Open Gallery", "collections" ) { Clicked = () => _window.DockManager.RaiseDock( "Gallery" ) } );
	}

	void AddOutputCard( Layout parent )
	{
		var output = new Card( "Output", "folder" );
		parent.Add( output, 1 );
		output.Body.Add( new Label( $"Folder: {_window.Settings.Output.ResolveFolder()}" ) { WordWrap = true } );
		output.Body.Add( new Label( $"Format: {_window.Settings.Output.Format.ToString().ToUpper()}" ) );

		var actions = output.Body.AddRow();
		actions.Spacing = 6;
		actions.Add( new Button( "Open Folder", "folder_open" )
		{
			Clicked = () => SuperShotService.RevealInExplorer( _window.Settings.Output.ResolveFolder() )
		} );
		actions.Add( new Button( "Settings", "settings" )
		{
			Clicked = () => _window.DockManager.RaiseDock( "Settings" )
		} );
	}

	void AddSummaryPill( Layout parent, string label, string value )
	{
		parent.Add( new SummaryPill( label, value ), 1 );
	}

	string DefaultFilterLabel()
	{
		_window.Settings.EnsureBuiltinFilterPresets();
		var id = _window.Settings.Capture.DefaultFilterPresetId;
		return string.IsNullOrEmpty( id )
			? "None"
			: _window.Settings.FindFilterPreset( id )?.Name ?? "None";
	}

	static string ResolutionLabel( CaptureSettings capture )
	{
		var (w, h) = SuperShotContext.ResolveSize( capture );
		return $"{w}x{h}";
	}

	static string SourceLabel( CaptureSource source )
	{
		return source == CaptureSource.SceneMainCamera ? "Scene Camera" : "Editor Freecam";
	}

	void CaptureAndOpenEditor()
	{
		_window.CaptureNow();
		if ( _window.HasCapture )
			_window.DockManager.RaiseDock( "Edit" );
	}

	void AddPresetRow( Layout parent, bool clean, params (string label, ShotResolution res)[] presets )
	{
		var row = parent.AddRow();
		row.Spacing = 4;
		foreach ( var (label, res) in presets )
		{
			var resolution = res;
			row.Add( new Button( label )
			{
				Clicked = clean
					? () => SuperShotService.QuickCapture( resolution )
					: () => _window.QuickCapture( resolution )
			} );
		}
	}

	sealed class SummaryPill : Widget
	{
		readonly string _label;
		readonly string _value;

		public SummaryPill( string label, string value ) : base( null )
		{
			_label = label;
			_value = value;
			MinimumSize = new Vector2( 120, 46 );
		}

		protected override void OnPaint()
		{
			Paint.ClearPen();
			Paint.SetBrush( Theme.ControlBackground.Lighten( 0.08f ) );
			Paint.DrawRect( LocalRect, 5f );
			Paint.SetPen( SuperShotUI.Muted );
			Paint.SetDefaultFont( 7, 500 );
			Paint.DrawText( LocalRect.Shrink( 10, 6 ), _label, TextFlag.LeftTop );
			Paint.SetPen( Theme.Text );
			Paint.SetDefaultFont( 10, 600 );
			Paint.DrawText( new Rect( 10, 21, Width - 20, 18 ), _value, TextFlag.LeftTop );
		}
	}

	sealed class WorkspaceCard : Widget
	{
		readonly string _title;
		readonly string _icon;
		readonly string _description;
		readonly Action _clicked;

		public WorkspaceCard( string title, string icon, string description, Action clicked ) : base( null )
		{
			_title = title;
			_icon = icon;
			_description = description;
			_clicked = clicked;
			Cursor = CursorShape.Finger;
			MinimumSize = new Vector2( 180, 100 );
		}

		protected override void OnMouseClick( MouseEvent e )
		{
			base.OnMouseClick( e );
			if ( e.LeftMouseButton )
				_clicked?.Invoke();
		}

		protected override void OnPaint()
		{
			Paint.ClearPen();
			Paint.SetBrush( SuperShotUI.CardBackground );
			Paint.DrawRect( LocalRect, 6f );
			Paint.ClearBrush();
			Paint.SetPen( SuperShotUI.CardBorder, 1f );
			Paint.DrawRect( LocalRect.Shrink( 0.5f ), 6f );
			Paint.SetPen( SuperShotUI.Accent );
			Paint.DrawIcon( new Rect( 14, 14, 28, 28 ), _icon, 24, TextFlag.Center );
			Paint.SetPen( Theme.Text );
			Paint.SetDefaultFont( 11, 700 );
			Paint.DrawText( new Rect( 52, 14, Width - 66, 22 ), _title, TextFlag.LeftTop );
			Paint.SetPen( SuperShotUI.Muted );
			Paint.SetDefaultFont( 8, 400 );
			Paint.DrawText( new Rect( 14, 48, Width - 28, Height - 58 ), _description, TextFlag.LeftTop | TextFlag.WordWrap );
		}
	}

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

		public RecentShotButton( SuperShotWindow window, CapturedShot shot ) : base( null )
		{
			_window = window;
			_shot = shot;
			Cursor = CursorShape.Finger;
			FixedSize = new Vector2( 120, 76 );
		}

		protected override void OnMouseClick( MouseEvent e )
		{
			base.OnMouseClick( e );
			if ( e.LeftMouseButton )
				_window.OpenInEditor( _shot );
		}

		protected override void OnPaint()
		{
			Paint.ClearPen();
			Paint.SetBrush( Color.Black );
			Paint.DrawRect( LocalRect, 4f );
			if ( _shot.Thumbnail is not null )
			{
				var rect = FitRect( _shot.Thumbnail.Size, LocalRect.Shrink( 4 ).Size );
				rect.Left += 4;
				rect.Top += 4;
				Paint.Draw( rect, _shot.Thumbnail );
			}
			Paint.ClearBrush();
			Paint.SetPen( SuperShotUI.CardBorder, 1f );
			Paint.DrawRect( LocalRect.Shrink( 0.5f ), 4f );
		}

		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 );
		}
	}
}