Editor UI panel for the SuperShot editor. Constructs the Edit tab UI including toolbar, tool rail, canvas, sidebar with recent shots, layers list, arrangement controls, and property sheets; hooks property change events to push undo and refresh the canvas.
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 )
};
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 )
};
static readonly HashSet<string> LayerCommonProps = new()
{
nameof( SuperShotLayer.Name ), nameof( SuperShotLayer.Visible ),
nameof( SuperShotLayer.Opacity ), nameof( SuperShotLayer.BlendMode )
};
static readonly HashSet<string> LayerTransformProps = new()
{
nameof( SuperShotLayer.Position ), nameof( SuperShotLayer.Size )
};
DocumentCanvas _canvas;
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() => Rebuild();
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()
{
AddToolbar();
var workspace = Layout.AddRow();
workspace.Spacing = 6;
AddToolRail( workspace );
_canvas = new DocumentCanvas( this, _window );
workspace.Add( _canvas, 1 );
AddSidebar( workspace );
}
void AddToolbar()
{
var bar = new Widget( this );
bar.MinimumSize = new Vector2( 0, 40 );
bar.Layout = Layout.Row();
bar.Layout.Margin = 4;
bar.Layout.Spacing = 4;
Layout.Add( bar );
var row = bar.Layout;
row.Add( new Button.Primary( "Save", "save" ) { Enabled = _window.HasCapture, Clicked = () => _window.SaveCurrent() } );
row.Add( new Button( "Save As", "save_as" ) { Enabled = _window.HasCapture, Clicked = () => _window.SaveCurrentAsNew() } );
row.AddSpacingCell( 10 );
row.Add( new Button( "", "undo" ) { ToolTip = "Undo", Clicked = _window.Undo } );
row.Add( new Button( "", "redo" ) { ToolTip = "Redo", Clicked = _window.Redo } );
row.Add( new Button( "", "restart_alt" ) { ToolTip = "Reset all edits", Clicked = _window.ResetEdit } );
row.AddStretchCell();
row.Add( new Label( _window.HasCapture ? (_window.ActiveDocument is not null ? "Editable document" : "Flat image") : "No image loaded" ) { Color = SuperShotUI.Muted } );
row.Add( new Button( "Before / After", "compare" )
{
Enabled = _window.HasCapture,
ToolTip = "Hold to compare against the original",
Clicked = () =>
{
if ( _canvas is null )
return;
_canvas.ShowOriginal = !_canvas.ShowOriginal;
_canvas.Refresh();
}
} );
}
void AddToolRail( Layout workspace )
{
var rail = new Widget( this );
rail.MinimumSize = new Vector2( 44, 0 );
rail.MaximumSize = new Vector2( 44, 100000 );
rail.Layout = Layout.Column();
rail.Layout.Margin = 2;
rail.Layout.Spacing = 6;
workspace.Add( rail, 0 );
AddToolButton( rail.Layout, "Add text layer", "title", _window.AddTextLayer );
AddToolButton( rail.Layout, "Add image layer", "add_photo_alternate",
() => new MessagePromptDialog( "Add Image Layer", "Paste an image file path", path => _window.AddImageLayer( path ), "Add" ) );
AddToolButton( rail.Layout, "Add adjustment layer", "tune", _window.AddAdjustmentLayer );
rail.Layout.AddSpacingCell( 10 );
AddToolButton( rail.Layout, "Duplicate selected layer", "content_copy", _window.DuplicateSelectedLayer );
AddToolButton( rail.Layout, "Delete selected layer", "delete", _window.DeleteSelectedLayer );
rail.Layout.AddStretchCell();
}
static void AddToolButton( Layout layout, string tooltip, string icon, Action clicked )
{
layout.Add( new Button( "", icon )
{
ToolTip = tooltip,
FixedSize = new Vector2( 36, 34 ),
Clicked = clicked
} );
}
void AddSidebar( Layout workspace )
{
var right = new ScrollArea( this );
right.FixedWidth = 440;
right.MinimumSize = new Vector2( 440, 0 );
right.MaximumSize = new Vector2( 440, 100000 );
right.Canvas = new Widget( right );
right.Canvas.FixedWidth = 420;
right.Canvas.Layout = Layout.Column();
right.Canvas.Layout.Margin = 0;
right.Canvas.Layout.Spacing = 8;
workspace.Add( right, 0 );
var body = right.Canvas.Layout;
if ( !_window.HasCapture || _window.ActiveDocument is null )
{
var empty = SuperShotUI.AddCard( body, "Nothing loaded", "image" );
empty.Body.Add( new Label( "Capture a shot or open one from the Gallery to start editing. Then add text, images, and adjustment layers, and drag them right on the canvas." ) { WordWrap = true } );
AddRecentShotsUi( body );
body.AddStretchCell();
return;
}
AddArrangeUi( body );
AddLayerUi( body );
if ( _window.SelectedLayer is { } selectedLayer )
AddSelectedLayerProperties( body, selectedLayer );
AddBaseAdjustmentUi( body );
body.AddStretchCell();
}
void AddRecentShotsUi( Layout body )
{
var card = SuperShotUI.AddCard( body, "Recent Shots", "history" );
if ( _window.Gallery.Count == 0 )
{
card.Body.Add( new Label( "No recent shots yet. Capture something from the Home tab and it will show up here." ) { Color = SuperShotUI.Muted, WordWrap = true } );
card.Body.Add( new Button( "Open Home", "home" ) { Clicked = () => _window.DockManager.RaiseDock( "Home" ) } );
return;
}
Layout row = null;
int inRow = 0;
int shown = 0;
for ( int i = _window.Gallery.Count - 1; i >= 0 && shown < 6; i--, shown++ )
{
if ( inRow == 0 )
{
row = card.Body.AddRow();
row.Spacing = 6;
}
row.Add( new RecentShotTile( _window, _window.Gallery[i] ), 1 );
inRow++;
if ( inRow == 2 )
{
inRow = 0;
row = null;
}
}
if ( row is not null )
row.AddStretchCell();
card.Body.Add( new Button( "Open Full Gallery", "collections" ) { Clicked = () => _window.DockManager.RaiseDock( "Gallery" ) } );
}
void AddArrangeUi( Layout body )
{
var layer = _window.SelectedLayer;
bool enabled = layer is not null && layer.HasVisualBox;
var card = SuperShotUI.AddCard( body, "Arrange", "align_horizontal_center" );
card.Body.Add( new Label( enabled ? "Align the selected layer to the canvas." : "Select a text or image layer to align it." ) { Color = SuperShotUI.Muted, WordWrap = true } );
var horizontal = card.Body.AddRow();
horizontal.Spacing = 4;
AddAlignButton( horizontal, "align_horizontal_left", "Align left", enabled, () => _window.AlignSelectedLayer( -1, null ) );
AddAlignButton( horizontal, "align_horizontal_center", "Center horizontally", enabled, () => _window.AlignSelectedLayer( 0, null ) );
AddAlignButton( horizontal, "align_horizontal_right", "Align right", enabled, () => _window.AlignSelectedLayer( 1, null ) );
horizontal.AddStretchCell();
var vertical = card.Body.AddRow();
vertical.Spacing = 4;
AddAlignButton( vertical, "align_vertical_top", "Align top", enabled, () => _window.AlignSelectedLayer( null, -1 ) );
AddAlignButton( vertical, "align_vertical_center", "Center vertically", enabled, () => _window.AlignSelectedLayer( null, 0 ) );
AddAlignButton( vertical, "align_vertical_bottom", "Align bottom", enabled, () => _window.AlignSelectedLayer( null, 1 ) );
vertical.AddStretchCell();
var quick = card.Body.AddRow();
quick.Spacing = 4;
quick.Add( new Button( "Center", "filter_center_focus" ) { Enabled = enabled, Clicked = () => _window.AlignSelectedLayer( 0, 0 ) }, 1 );
quick.Add( new Button( "Fill Canvas", "fit_screen" ) { Enabled = enabled, Clicked = _window.FillCanvasWithSelectedLayer }, 1 );
}
static void AddAlignButton( Layout row, string icon, string tooltip, bool enabled, Action clicked )
{
row.Add( new Button( "", icon )
{
ToolTip = tooltip,
Enabled = enabled,
FixedSize = new Vector2( 32, 28 ),
Clicked = clicked
} );
}
void AddLayerUi( Layout body )
{
var document = _window.ActiveDocument;
var card = SuperShotUI.AddCard( body, "Layers", "layers" );
var actions = card.Body.AddRow();
actions.Spacing = 4;
actions.Add( new Button( "Text", "title" ) { Clicked = _window.AddTextLayer }, 1 );
actions.Add( new Button( "Image", "image" )
{
Clicked = () => new MessagePromptDialog( "Add Image Layer", "Paste an image file path", path => _window.AddImageLayer( path ), "Add" )
}, 1 );
actions.Add( new Button( "Adj", "tune" ) { ToolTip = "Add adjustment layer", Clicked = _window.AddAdjustmentLayer }, 1 );
if ( document.Layers.Count == 0 )
{
card.Body.Add( new Label( "No layers yet. Add text, image, or adjustment layers above." ) { Color = SuperShotUI.Muted, WordWrap = true } );
return;
}
for ( int i = document.Layers.Count - 1; i >= 0; i-- )
{
var layer = document.Layers[i];
if ( layer is null )
continue;
bool selected = document.SelectedLayerId == layer.Id;
var row = card.Body.AddRow();
row.Spacing = 4;
row.Add( new Button( "", layer.Visible ? "visibility" : "visibility_off" )
{
ToolTip = layer.Visible ? "Hide layer" : "Show layer",
FixedSize = new Vector2( 28, 26 ),
Clicked = () => _window.ToggleLayerVisibility( layer )
} );
row.Add( new Button( "", LayerIcon( layer.Kind ) )
{
FixedSize = new Vector2( 28, 26 ),
Clicked = () => _window.SelectLayer( layer )
} );
row.Add( new Button( selected ? $"● {layer.Name}" : layer.Name )
{
Clicked = () => _window.SelectLayer( layer )
}, 1 );
}
var order = card.Body.AddRow();
order.Spacing = 4;
order.Add( new Button( "", "arrow_upward" ) { ToolTip = "Move up", FixedSize = new Vector2( 30, 26 ), Clicked = () => _window.MoveSelectedLayer( 1 ) } );
order.Add( new Button( "", "arrow_downward" ) { ToolTip = "Move down", FixedSize = new Vector2( 30, 26 ), Clicked = () => _window.MoveSelectedLayer( -1 ) } );
order.AddStretchCell();
order.Add( new Button( "", "content_copy" ) { ToolTip = "Duplicate layer", FixedSize = new Vector2( 30, 26 ), Clicked = _window.DuplicateSelectedLayer } );
order.Add( new Button( "", "delete" ) { ToolTip = "Delete layer", FixedSize = new Vector2( 30, 26 ), Clicked = _window.DeleteSelectedLayer } );
}
static string LayerIcon( SuperShotLayerKind kind )
{
return kind switch
{
SuperShotLayerKind.Text => "title",
SuperShotLayerKind.Image => "image",
SuperShotLayerKind.Adjustment => "tune",
_ => "layers"
};
}
void AddSelectedLayerProperties( Layout body, SuperShotLayer layer )
{
var layerSo = layer.GetSerialized();
layerSo.OnPropertyChanged += _ => OnEditChanged();
var card = SuperShotUI.AddCard( body, $"Layer · {layer.Name}", "edit" );
card.Body.Add( SuperShotUI.SheetWidget( layerSo, p => LayerCommonProps.Contains( p.Name ) ) );
if ( layer.HasVisualBox )
SuperShotUI.AddSection( body, "Transform", "open_with", SuperShotUI.SheetWidget( layerSo, p => LayerTransformProps.Contains( p.Name ) ), "supershot.layer.transform", defaultOpen: false );
SerializedObject detail = layer.Kind switch
{
SuperShotLayerKind.Text => layer.Text.GetSerialized(),
SuperShotLayerKind.Image => layer.Image.GetSerialized(),
SuperShotLayerKind.Adjustment => layer.Adjustment.Edit.GetSerialized(),
_ => null
};
if ( detail is null )
return;
detail.OnPropertyChanged += _ => OnEditChanged();
SuperShotUI.AddSection( body, $"{layer.Kind} Settings", "tune", SuperShotUI.SheetWidget( detail ), defaultOpen: true );
}
void AddBaseAdjustmentUi( Layout body )
{
var so = _window.ActiveDocument.BaseEdit.GetSerialized();
so.OnPropertyChanged += _ => OnEditChanged();
var color = SuperShotUI.AddCard( body, "Image 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" );
}
void OnEditChanged()
{
if ( _sinceUndoPush > 0.75f )
{
_window.PushEditUndo();
_sinceUndoPush = 0f;
}
_canvas?.Refresh();
}
sealed class RecentShotTile : Widget
{
readonly SuperShotWindow _window;
readonly CapturedShot _shot;
public RecentShotTile( SuperShotWindow window, CapturedShot shot ) : base( null )
{
_window = window;
_shot = shot;
MinimumSize = new Vector2( 0, 112 );
Cursor = CursorShape.Finger;
ToolTip = $"{shot.Size.x}x{shot.Size.y} {shot.Time:g}";
}
protected override void OnMouseClick( MouseEvent e )
{
base.OnMouseClick( e );
if ( e.LeftMouseButton )
_window.OpenInEditor( _shot );
}
protected override void OnPaint()
{
var bg = IsUnderMouse ? SuperShotUI.CardBorder.WithAlpha( 0.65f ) : SuperShotUI.CardBackground;
Paint.ClearPen();
Paint.SetBrush( bg );
Paint.DrawRect( LocalRect, 6f );
Paint.ClearBrush();
Paint.SetPen( IsUnderMouse ? SuperShotUI.Accent : SuperShotUI.CardBorder, 1f );
Paint.DrawRect( LocalRect.Shrink( 0.5f ), 6f );
var imageRect = new Rect( 6, 6, Width - 12, Height - 30 );
Paint.ClearPen();
Paint.SetBrush( Color.Black );
Paint.DrawRect( imageRect, 4f );
if ( _shot.Thumbnail is not null )
{
var rect = FitRect( _shot.Thumbnail.Size, imageRect.Size );
rect.Left += imageRect.Left;
rect.Top += imageRect.Top;
Paint.Draw( rect, _shot.Thumbnail );
}
else
{
Paint.SetPen( SuperShotUI.Muted );
Paint.DrawIcon( imageRect, "image", 22, TextFlag.Center );
}
Paint.SetDefaultFont( 7, 500 );
Paint.SetPen( Theme.Text.WithAlpha( 0.9f ) );
var label = _shot.SavedPath is not null ? "Saved shot" : "Recent shot";
Paint.DrawText( new Rect( 8, Height - 22, Width - 16, 10 ), label, TextFlag.LeftTop );
Paint.SetPen( SuperShotUI.Muted );
Paint.DrawText( new Rect( 8, Height - 12, Width - 16, 10 ), $"{_shot.Size.x}x{_shot.Size.y}", TextFlag.LeftTop );
}
static Rect FitRect( Vector2 content, Vector2 area )
{
if ( content.x <= 0f || content.y <= 0f )
return new Rect( 0, 0, area.x, area.y );
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 );
}
}
}