using Sandbox; using Editor; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; namespace SFXR.Editor; /// /// A popup dialog to select an entity type /// internal partial class SFXREffectTypeSelector : PopupWidget { public Action OnSelect { get; set; } List Panels { get; set; } = new(); int CurrentPanelId { get; set; } = 0; Widget Main { get; set; } string searchString; const string NoCategoryName = "Uncategorized"; internal LineEdit Search { get; init; } public SFXREffectTypeSelector( Widget parent ) : base( parent ) { Layout = Layout.Column(); var head = Layout.Row(); head.Margin = 6; Layout.Add( head ); Main = new Widget( this ); Main.Layout = Layout.Row(); Layout.Add( Main ); FixedWidth = 200; MaximumHeight = 300; DeleteOnClose = true; Search = new LineEdit( this ); Search.MinimumHeight = 22; Search.PlaceholderText = "Search.."; Search.TextEdited += ( t ) => { searchString = t; ResetSelection(); }; head.Add( Search ); ResetSelection(); Search.Focus(); } /// /// Pushes a new selection to the selector /// /// void PushSelection( EffectSelection selection ) { CurrentPanelId++; // Do we have something at our new index, if so, kill it if ( Panels.Count > CurrentPanelId && Panels.ElementAt( CurrentPanelId ) is var existingObj ) existingObj.Destroy(); Panels.Insert( CurrentPanelId, selection ); Main.Layout.Add( selection, 1 ); if ( !selection.IsManual ) { UpdateSelection( selection ); } AnimateSelection( true, Panels[CurrentPanelId - 1], selection ); selection.Focus(); } /// /// Pops the current selection off /// internal void PopSelection() { // Don't pop while empty if ( CurrentPanelId == 0 ) return; var currentIdx = Panels[CurrentPanelId]; CurrentPanelId--; AnimateSelection( false, currentIdx, Panels[CurrentPanelId] ); Panels[CurrentPanelId].Focus(); } /// /// Runs an animation on the last selection, and the current selection. /// I kinda hate this. A lot. But it's pretty. /// /// /// /// void AnimateSelection( bool forward, EffectSelection prev, EffectSelection selection ) { const string easing = "ease-out"; const float speed = 0.3f; var distance = Width; var prevFrom = prev.Position.x; var prevTo = forward ? prev.Position.x - distance : prev.Position.x + distance; var selectionFrom = forward ? selection.Position.x + distance : selection.Position.x; var selectionTo = forward ? selection.Position.x : selection.Position.x + distance; var func = ( EffectSelection a, float x ) => { a.Position = a.Position.WithX( x ); OnMoved(); }; Animate.Add( prev, speed, prevFrom, prevTo, x => func( prev, x ), easing ); Animate.Add( selection, speed, selectionFrom, selectionTo, x => func( selection, x ), easing ); } /// /// Resets the current selection, useful when setting up / searching /// protected void ResetSelection() { Main.Layout.Clear( true ); Panels.Clear(); var selection = new EffectSelection( this, this ); CurrentPanelId = 0; UpdateSelection( selection ); Panels.Add( selection ); Main.Layout.Add( selection ); } protected override void OnPaint() { Paint.Antialiasing = true; Paint.SetPen( Theme.WidgetBackground.Darken( 0.4f ), 1 ); Paint.SetBrush( Theme.WidgetBackground ); Paint.DrawRect( LocalRect.Shrink( 1 ), 3 ); } /// /// Called when a category is selected /// /// void OnCategorySelected( string category ) { // Push this as a new selection PushSelection( new EffectSelection( this, this, category ) ); } /// /// Called when an individual effect is selected /// /// void OnEffectSelected( TypeDescription type ) { OnSelect( type ); Destroy(); } protected override void OnKeyRelease( KeyEvent e ) { if ( e.Key == KeyCode.Down ) { var selection = Panels[CurrentPanelId]; if ( selection.ItemList.FirstOrDefault() != null ) { selection.Focus(); selection.PostKeyEvent( KeyCode.Down ); e.Accepted = true; } } } /// /// Updates any selection /// /// void UpdateSelection( EffectSelection selection ) { selection.Clear(); selection.ItemList.Add( selection.CategoryHeader ); // entity components var types = EditorTypeLibrary.GetTypes().Where( x => !x.IsAbstract ); if ( !string.IsNullOrWhiteSpace( searchString ) ) { var searchWords = searchString.Split( ' ', StringSplitOptions.RemoveEmptyEntries ); var query = types.Where( x => searchWords.All( word => x.Title.Contains( word, StringComparison.OrdinalIgnoreCase ) ) ); foreach ( var type in query ) { selection.AddEntry( new EffectEntry( selection, type ) { MouseClick = () => OnEffectSelected( type ) } ); } selection.AddStretchCell(); return; } if ( selection.Category == null ) { var categories = types.Select( x => string.IsNullOrWhiteSpace( x.Group ) ? NoCategoryName : x.Group ).Distinct().OrderBy( x => x ).ToArray(); if ( categories.Length > 1 ) { foreach ( var category in categories ) { selection.AddEntry( new EffectCategory( selection ) { Category = category, MouseClick = () => OnCategorySelected( category ), } ); } selection.AddStretchCell(); return; } } else { types = types.Where( x => selection.Category == NoCategoryName ? x.Group == null : x.Group == selection.Category ).OrderBy( x => x.Title ); foreach ( var type in types ) { selection.AddEntry( new EffectEntry( selection, type ) { MouseClick = () => OnEffectSelected( type ) } ); } selection.AddStretchCell(); } } /// /// A widget that contains a given selection - we hold this in a class because more than one can exist. /// partial class EffectSelection : Widget { internal string Category { get; init; } internal Widget CategoryHeader { get; init; } ScrollArea Scroller { get; init; } SFXREffectTypeSelector Selector { get; set; } internal List ItemList { get; private set; } = new(); internal int CurrentItemId { get; private set; } = 0; internal Widget CurrentItem { get; private set; } internal bool IsManual { get; set; } internal EffectSelection( Widget parent, SFXREffectTypeSelector selector, string categoryName = null ) : base( parent ) { Selector = selector; Category = categoryName; FixedWidth = 200; MinimumHeight = 220; Layout = Layout.Column(); CategoryHeader = new Widget( this ); CategoryHeader.FixedHeight = 24; CategoryHeader.OnPaintOverride = PaintHeader; CategoryHeader.MouseClick = Selector.PopSelection; Layout.Add( CategoryHeader ); Scroller = new ScrollArea( this ); Scroller.Layout = Layout.Column(); Scroller.FocusMode = FocusMode.None; Layout.Add( Scroller, 1 ); Scroller.Canvas = new Widget( Scroller ); Scroller.Canvas.Layout = Layout.Column(); } protected bool SelectMoveRow( int delta ) { var selection = Selector.Panels[Selector.CurrentPanelId]; if ( delta == 1 && selection.ItemList.Count - 1 > selection.CurrentItemId ) { selection.CurrentItem = selection.ItemList[++selection.CurrentItemId]; selection.Update(); if ( selection.CurrentItem != null ) { Scroller.MakeVisible( selection.CurrentItem ); } return true; } else if ( delta == -1 ) { if ( selection.CurrentItemId > 0 ) { selection.CurrentItem = selection.ItemList[--selection.CurrentItemId]; selection.Update(); if ( selection.CurrentItem != null ) { Scroller.MakeVisible( selection.CurrentItem ); } return true; } else { selection.Selector.Search.Focus(); selection.CurrentItem = null; selection.Update(); return true; } } return false; } protected bool Enter() { var selection = Selector.Panels[Selector.CurrentPanelId]; if ( selection.ItemList[selection.CurrentItemId] is Widget entry ) { entry.MouseClick?.Invoke(); return true; } return false; } protected override void OnKeyRelease( KeyEvent e ) { // Move down if ( e.Key == KeyCode.Down ) { e.Accepted = true; SelectMoveRow( 1 ); return; } // Move up if ( e.Key == KeyCode.Up ) { e.Accepted = true; SelectMoveRow( -1 ); return; } // Back button while in any selection, goes to previous selction. if ( e.Key == KeyCode.Left ) { e.Accepted = true; Selector.PopSelection(); return; } // Moving right, or hitting the enter key assumes you're trying to select something if ( (e.Key == KeyCode.Return || e.Key == KeyCode.Right) && Enter() ) { e.Accepted = true; return; } } internal bool PaintHeader() { var c = CategoryHeader; var selected = c.IsUnderMouse || CurrentItem == c; Paint.ClearPen(); Paint.SetBrush( selected ? Theme.Selection : Theme.WidgetBackground.WithAlpha( selected ? 0.7f : 0.4f ) ); Paint.DrawRect( c.LocalRect ); var r = c.LocalRect.Shrink( 12, 2 ); Paint.SetPen( Theme.ControlText ); if ( Selector.CurrentPanelId > 0 ) { Paint.DrawIcon( r, "arrow_back", 14, TextFlag.LeftCenter ); } Paint.SetDefaultFont( 8 ); Paint.DrawText( r, string.IsNullOrEmpty( Category ) ? "Effect" : Category, TextFlag.Center ); return true; } /// /// Adds a new entry to the current selection. /// /// internal Widget AddEntry( Widget entry ) { var layoutWidget = Scroller.Canvas.Layout.Add( entry ); ItemList.Add( entry ); if ( entry is EffectEntry e ) e.Selector = this; return layoutWidget; } /// /// Adds a stretch cell to the bottom of the selection - good to call this when you know you're done adding entries. /// internal void AddStretchCell() { Scroller.Canvas.Layout.AddStretchCell( 1 ); Update(); } /// /// Adds a separator cell. /// internal void AddSeparator() { Scroller.Canvas.Layout.AddSeparator( true ); Update(); } /// /// Clears the current selection /// internal void Clear() { Scroller.Canvas.Layout.Clear( true ); ItemList.Clear(); } protected override void OnPaint() { Paint.Antialiasing = true; Paint.SetPen( Theme.WidgetBackground.Darken( 0.8f ), 1 ); Paint.SetBrush( Theme.WidgetBackground.Darken( 0.2f ) ); Paint.DrawRect( LocalRect.Shrink( 0 ), 3 ); } } /// /// An effect entry /// class EffectEntry : Widget { public string Text { get; set; } = "My Effect"; public string Icon { get; set; } = "note_add"; public bool IsSelected { get; set; } = false; internal EffectSelection Selector { get; set; } public TypeDescription Type { get; init; } internal EffectEntry( Widget parent, TypeDescription type = null ) : base( parent ) { FixedHeight = 24; Type = type; if ( type is not null ) { Text = type.Title; Icon = type.Icon; } } protected override void OnPaint() { var r = LocalRect.Shrink( 12, 2 ); var selected = IsUnderMouse || Selector.CurrentItem == this; var opacity = selected ? 1.0f : 0.7f; if ( selected ) { Paint.ClearPen(); Paint.SetBrush( Theme.Selection ); Paint.DrawRect( LocalRect ); } if ( Type is not null && !string.IsNullOrEmpty( Type.Icon ) ) { Type.PaintComponentIcon( new Rect( r.Position, r.Height ).Shrink( 2 ), opacity ); } else { Paint.SetPen( Theme.Green.WithAlpha( opacity ) ); var icon = !string.IsNullOrEmpty( Icon ) ? Icon : "note_add"; Paint.DrawIcon( new Rect( r.Position, r.Height ).Shrink( 2 ), icon, r.Height, TextFlag.Center ); } r.Left += r.Height + 6; Paint.SetDefaultFont( 8 ); Paint.SetPen( Theme.ControlText.WithAlpha( selected ? 1.0f : 0.5f ) ); Paint.DrawText( r, Text, TextFlag.LeftCenter ); } } /// /// A category effect entry /// class EffectCategory : EffectEntry { public string Category { get; set; } public EffectCategory( Widget parent ) : base( parent ) { } protected override void OnPaint() { var selected = IsUnderMouse || Selector.CurrentItem == this; if ( selected ) { Paint.ClearPen(); Paint.SetBrush( Theme.Selection ); Paint.DrawRect( LocalRect ); } var r = LocalRect.Shrink( 12, 2 ); Paint.SetPen( Theme.ControlText.WithAlpha( selected ? 1.0f : 0.5f ) ); Paint.SetDefaultFont( 8 ); Paint.DrawText( r, Category, TextFlag.LeftCenter ); Paint.DrawIcon( r, "arrow_forward", 14, TextFlag.RightCenter ); } } }