Editor/Panels/PreviewPanel.cs

Editor UI panel for the SuperShot tool. It builds toolbar buttons for capture/save/copy/post and a live preview canvas that renders either captured images or a live freecam preview, applies edits, and draws UI overlays like thirds grid.

File AccessHttp Calls
using System;
using Sandbox;

namespace Editor.SuperShot;

public sealed class PreviewPanel : Widget
{
	readonly SuperShotWindow _window;
	PreviewCanvas _canvas;

	public PreviewPanel( SuperShotWindow window ) : base( null )
	{
		_window = window;
		Name = "Preview";
		WindowTitle = "Preview";
		SetWindowIcon( "image" );
		Layout = Layout.Column();
		Layout.Margin = 0;

		Build();
	}

	Button _liveButton;
	Button _saveButton;
	Button _copyButton;
	Button _postButton;

	void Build()
	{
		var toolbar = Layout.AddRow();
		toolbar.Margin = 6;
		toolbar.Spacing = 4;

		toolbar.Add( new Button.Primary( "Capture 1K", "photo_camera" )
		{
			ToolTip = "Quickly capture the current view at 1K (1024x576)",
			Clicked = () => _window.CaptureAt( ShotResolution.OneK )
		} );
		_saveButton = new Button( "Save", "save" ) { Clicked = () => _window.SaveCurrent() };
		toolbar.Add( _saveButton );
		_copyButton = new Button( "Copy Path", "content_copy" ) { ToolTip = "Save the shot and copy its file path", Clicked = CopyPath };
		toolbar.Add( _copyButton );
		_postButton = new Button( "Post to All", "send" ) { Clicked = () => _ = _window.PostCurrentToAll() };
		toolbar.Add( _postButton );
		toolbar.AddStretchCell();

		_liveButton = new Button( LiveLabel(), _window.Settings.LivePreview ? "videocam" : "videocam_off" )
		{
			ToolTip = "Toggle the live freecam preview (off saves performance)",
			Clicked = ToggleLive
		};
		toolbar.Add( _liveButton );

		var thirds = new Button( "Thirds", "grid_3x3" );
		thirds.Clicked = () => { _canvas.ShowThirds = !_canvas.ShowThirds; _canvas.Update(); };
		toolbar.Add( thirds );

		var beforeAfter = new Button( "Before/After", "compare" );
		beforeAfter.Clicked = () => { _canvas.ShowOriginal = !_canvas.ShowOriginal; _canvas.Refresh(); };
		toolbar.Add( beforeAfter );

		_canvas = new PreviewCanvas( this, _window );
		Layout.Add( _canvas, 1 );

		_window.Changed += OnChanged;
		UpdateActionVisibility();
	}

	void UpdateActionVisibility()
	{
		var has = _window.HasCapture;
		if ( _saveButton.IsValid() ) _saveButton.Visible = has;
		if ( _copyButton.IsValid() ) _copyButton.Visible = has;
		if ( _postButton.IsValid() ) _postButton.Visible = has;
	}

	string LiveLabel() => _window.Settings.LivePreview ? "Live: On" : "Live: Off";

	void ToggleLive()
	{
		_window.Settings.LivePreview = !_window.Settings.LivePreview;
		_window.Settings.Save();

		if ( _liveButton.IsValid() )
		{
			_liveButton.Text = LiveLabel();
			_liveButton.Icon = _window.Settings.LivePreview ? "videocam" : "videocam_off";
			_liveButton.Update();
		}

		_canvas?.Refresh();
	}

	void CopyPath()
	{
		var path = _window.SaveCurrent();
		SuperShotService.CopyPathToClipboard( path );
	}

	void OnChanged()
	{
		UpdateActionVisibility();
		_canvas?.Refresh();
	}

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

	sealed class PreviewCanvas : Widget
	{
		readonly SuperShotWindow _window;
		Pixmap _pixmap;
		Bitmap _live;
		RealTimeSince _sinceRender = 100f;
		bool _dirty = true;

		public bool ShowThirds { get; set; } = true;
		public bool ShowOriginal { get; set; } = false;

		public PreviewCanvas( Widget parent, SuperShotWindow window ) : base( parent )
		{
			_window = window;
			MinimumSize = 64;
		}

		public void Refresh()
		{
			_dirty = true;
			Update();
		}

		[EditorEvent.Frame]
		public void Frame()
		{
			if ( _window.HasCapture )
			{
				if ( _dirty )
				{
					BuildFromCapture();
					_dirty = false;
					Update();
				}
				return;
			}

			if ( !_window.Settings.LivePreview )
			{
				// Drop the last live frame immediately so the empty state shows without reopening.
				if ( _pixmap != null )
				{
					_pixmap = null;
					_live?.Dispose();
					_live = null;
					Update();
				}
				return;
			}

			if ( _sinceRender > 0.1f )
			{
				_sinceRender = 0f;
				if ( RenderLive() )
					Update();
			}
		}

		bool RenderLive()
		{
			var (w, h) = SuperShotContext.ResolveSize( _window.Settings.Capture );
			float aspect = w / (float)h;
			int pw = 960;
			int ph = Math.Max( 1, (int)(pw / aspect) );

			if ( _live is null || _live.Width != pw || _live.Height != ph )
			{
				_live?.Dispose();
				_live = new Bitmap( pw, ph );
			}

			if ( !SuperShotCapture.RenderPreview( _live, _window.Settings.Capture ) )
				return false;

			if ( ShowOriginal )
			{
				EnsurePixmap( _live.Width, _live.Height );
				_pixmap.UpdateFromPixels( _live );
				return true;
			}

			using var edited = SuperShotEdit.Apply( _live, _window.Settings.Edit );
			var display = edited is not null && edited.IsValid ? edited : _live;
			EnsurePixmap( display.Width, display.Height );
			_pixmap.UpdateFromPixels( display );
			return true;
		}

		void BuildFromCapture()
		{
			Bitmap source;
			bool ownsSource = false;

			if ( ShowOriginal )
			{
				source = _window.RawCapture;
			}
			else
			{
				source = _window.BuildFinished();
				ownsSource = true;
			}

			if ( source is null || !source.IsValid )
				return;

			EnsurePixmap( source.Width, source.Height );
			_pixmap.UpdateFromPixels( source );

			if ( ownsSource )
				source.Dispose();
		}

		void EnsurePixmap( int w, int h )
		{
			if ( _pixmap is null || _pixmap.Width != w || _pixmap.Height != h )
				_pixmap = new Pixmap( w, h );
		}

		protected override void OnPaint()
		{
			Paint.ClearPen();
			Paint.SetBrush( Theme.ControlBackground.Darken( 0.3f ) );
			Paint.DrawRect( LocalRect );

			if ( _pixmap is null )
			{
				var message = _window.Settings.LivePreview
					? "Move the editor scene view (or activate the Supershot tool), then press Capture.\nPick a scene camera in the Capture tab to shoot that view instead."
					: "Live preview is off.\nPress Capture to shoot, or turn Live back on in the toolbar above.";

				Paint.SetPen( Theme.TextControl.WithAlpha( 0.6f ) );
				Paint.DrawText( LocalRect.Shrink( 24 ), message, TextFlag.Center | TextFlag.WordWrap );
				return;
			}

			var rect = FitRect( _pixmap.Size, Size );
			Paint.Draw( rect, _pixmap );
			DrawOverlays( rect );
		}

		void DrawOverlays( Rect rect )
		{
			if ( !ShowThirds )
				return;

			Paint.SetPen( Color.White.WithAlpha( 0.35f ), 1f );

			float x1 = rect.Left + rect.Width / 3f;
			float x2 = rect.Left + rect.Width * 2f / 3f;
			float y1 = rect.Top + rect.Height / 3f;
			float y2 = rect.Top + rect.Height * 2f / 3f;

			Paint.DrawLine( new Vector2( x1, rect.Top ), new Vector2( x1, rect.Bottom ) );
			Paint.DrawLine( new Vector2( x2, rect.Top ), new Vector2( x2, rect.Bottom ) );
			Paint.DrawLine( new Vector2( rect.Left, y1 ), new Vector2( rect.Right, y1 ) );
			Paint.DrawLine( new Vector2( rect.Left, y2 ), new Vector2( rect.Right, y2 ) );
		}

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

		public override void OnDestroyed()
		{
			base.OnDestroyed();
			_live?.Dispose();
		}
	}
}