Editor/Panels/EditPanel.cs

Editor UI panel for photo editing inside the SuperShot window. It builds controls for filters, color/detail/effect/transform/frame/watermark sections, handles applying presets, undo push timing, and save button visibility.

File Access
using System;
using System.Collections.Generic;
using Sandbox;

namespace Editor.SuperShot;

public sealed class EditPanel : Widget
{
	readonly SuperShotWindow _window;
	RealTimeSince _sinceUndoPush = 100f;

	static readonly HashSet<string> ColorProps = new()
	{
		nameof( EditSettings.Brightness ), nameof( EditSettings.Contrast ), nameof( EditSettings.Saturation ),
		nameof( EditSettings.Exposure ), nameof( EditSettings.Hue ), nameof( EditSettings.Filter )
	};
	static readonly HashSet<string> DetailProps = new() { nameof( EditSettings.Sharpen ), nameof( EditSettings.Blur ) };
	static readonly HashSet<string> EffectProps = new() { nameof( EditSettings.Vignette ), nameof( EditSettings.Grain ) };
	static readonly HashSet<string> TransformProps = new() { nameof( EditSettings.Rotate ), nameof( EditSettings.FlipH ), nameof( EditSettings.FlipV ) };
	static readonly HashSet<string> FrameProps = new() { nameof( EditSettings.Border ), nameof( EditSettings.BorderColor ) };
	static readonly HashSet<string> WatermarkProps = new()
	{
		nameof( EditSettings.Watermark ), nameof( EditSettings.WatermarkText ), nameof( EditSettings.WatermarkAnchor ),
		nameof( EditSettings.WatermarkSize ), nameof( EditSettings.WatermarkColor )
	};

	Button _saveButton;

	public EditPanel( SuperShotWindow window ) : base( null )
	{
		_window = window;
		Name = "Edit";
		WindowTitle = "Edit";
		SetWindowIcon( "tune" );
		Layout = Layout.Column();
		Layout.Margin = 8;
		Layout.Spacing = 8;

		Build();
		_window.Changed += OnWindowChanged;
	}

	void OnWindowChanged() => UpdateSaveVisibility();

	void UpdateSaveVisibility()
	{
		if ( _saveButton.IsValid() )
			_saveButton.Visible = _window.HasCapture;
	}

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

	protected override void OnVisibilityChanged( bool visible )
	{
		base.OnVisibilityChanged( visible );

		if ( !visible )
			_window.LeaveEditReview();
	}

	void Rebuild()
	{
		Layout.Clear( true );
		Build();
	}

	void Build()
	{
		SuperShotUI.AddBanner( Layout, "Edit", "Non-destructive photo adjustments.", "tune" );

		var actions = Layout.AddRow();
		actions.Spacing = 4;
		actions.Add( new Button( "Reset", "restart_alt" ) { Clicked = _window.ResetEdit } );
		actions.Add( new Button( "Undo", "undo" ) { Clicked = _window.Undo } );
		actions.Add( new Button( "Redo", "redo" ) { Clicked = _window.Redo } );
		actions.AddStretchCell();
		_saveButton = new Button.Primary( "Save", "save" ) { Clicked = () => _window.SaveCurrent() };
		actions.Add( _saveButton );

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

		var filterCard = SuperShotUI.AddCard( body, "Filters", "filter_vintage" );
		var filters = filterCard.Body.AddRow();
		filters.Spacing = 4;
		AddFilter( filters, "Warm", ShotFilter.Warm );
		AddFilter( filters, "Cool", ShotFilter.Cool );
		AddFilter( filters, "Vintage", ShotFilter.Vintage );
		AddFilter( filters, "Cinema", ShotFilter.Cinematic );
		AddFilter( filters, "B&W", ShotFilter.BlackAndWhite );
		filterCard.Body.Add( new Button( "Clear Filter", "clear" ) { Clicked = () => ApplyFilter( ShotFilter.None ) } );

		var so = _window.Settings.Edit.GetSerialized();
		so.OnPropertyChanged += _ =>
		{
			if ( _sinceUndoPush > 0.75f )
			{
				_window.PushEditUndo();
				_sinceUndoPush = 0f;
			}
			_window.Settings.Save();
			_window.NotifyChanged();
		};

		var color = SuperShotUI.AddCard( body, "Color", "palette" );
		color.Body.Add( SuperShotUI.SheetWidget( so, p => ColorProps.Contains( p.Name ) ) );

		SuperShotUI.AddSection( body, "Detail", "deblur", SuperShotUI.SheetWidget( so, p => DetailProps.Contains( p.Name ) ), "supershot.edit.detail" );
		SuperShotUI.AddSection( body, "Effects", "auto_awesome", SuperShotUI.SheetWidget( so, p => EffectProps.Contains( p.Name ) ), "supershot.edit.effects" );
		SuperShotUI.AddSection( body, "Transform", "crop_rotate", SuperShotUI.SheetWidget( so, p => TransformProps.Contains( p.Name ) ), "supershot.edit.transform" );
		SuperShotUI.AddSection( body, "Frame", "crop_din", SuperShotUI.SheetWidget( so, p => FrameProps.Contains( p.Name ) ), "supershot.edit.frame" );
		SuperShotUI.AddSection( body, "Watermark", "branding_watermark", SuperShotUI.SheetWidget( so, p => WatermarkProps.Contains( p.Name ) ), "supershot.edit.watermark" );

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

		UpdateSaveVisibility();
	}

	void AddFilter( Layout row, string label, ShotFilter filter )
	{
		row.Add( new Button( label ) { Clicked = () => ApplyFilter( filter ) } );
	}

	void ApplyFilter( ShotFilter filter )
	{
		_window.PushEditUndo();

		var e = _window.Settings.Edit;

		e.Brightness = 1f; e.Contrast = 1f; e.Saturation = 1f; e.Exposure = 0f; e.Hue = 0f;
		e.Vignette = 0f; e.Grain = 0f;
		e.Filter = filter;

		switch ( filter )
		{
			case ShotFilter.Warm:
				e.Exposure = 0.05f; e.Contrast = 1.05f; e.Saturation = 1.10f;
				break;
			case ShotFilter.Cool:
				e.Contrast = 1.05f; e.Saturation = 1.05f;
				break;
			case ShotFilter.Vintage:
				e.Contrast = 0.92f; e.Saturation = 0.85f; e.Vignette = 0.35f; e.Grain = 0.15f;
				break;
			case ShotFilter.Cinematic:
				e.Contrast = 1.15f; e.Saturation = 1.05f; e.Vignette = 0.25f;
				break;
			case ShotFilter.BlackAndWhite:
				e.Contrast = 1.10f; e.Saturation = 0f;
				break;
			default:
				break;
		}

		_window.Settings.Save();
		Rebuild();
		_window.NotifyChanged();
	}
}