using Microsoft.AspNetCore.Components; using Sandbox.Internal; using System.Threading; namespace Sandbox.UI; /// /// A simple User Interface panel. Can be styled with CSS. /// [Library( "panel" ), Alias( "div", "span" ), Expose] [Title( "Panel" ), Icon( "view_quilt" )] public partial class Panel : IPanel, IValid, IComponent { /// /// The element name. If you've created this Panel via a template this will be whatever the element /// name is on there. If not then it'll be the name of the class (ie Panel, Button) /// [Property] public string ElementName { get; set; } /// /// Works the same as the html id="" attribute. If you set Id to "poop", it'll match any styles /// that define #poop in their selector. /// public string Id { get; set; } /// /// If this was created by razor, this is the file in which it was created /// [Hide] public string SourceFile { get; set; } /// /// If this was created by razor, this is the line number in the file /// [Hide] public int SourceLine { get; set; } /// /// Quick access to timing events, for async/await. /// public TaskSource Task = new( 1 ); /// /// A collection of stylesheets applied to this panel directly. /// public StyleSheetCollection StyleSheet; // // Start with the intro flag on // PseudoClass _pseudoClass = PseudoClass.Intro; /// /// Special flags used by the styling system for hover, active etc.. /// [Property] public PseudoClass PseudoClass { get => _pseudoClass; set { if ( _pseudoClass == value ) return; _pseudoClass = value; StyleSelectorsChanged( true, true ); } } /// /// Whether this panel has the :focus pseudo class active. /// [Hide] public bool HasFocus => (PseudoClass & PseudoClass.Focus) != 0; /// /// Whether this panel has the :active pseudo class active. /// [Hide] public bool HasActive => (PseudoClass & PseudoClass.Active) != 0; /// /// Whether this panel has the :hover pseudo class active. /// [Hide] public bool HasHovered => (PseudoClass & PseudoClass.Hover) != 0; /// /// Whether this panel has the :intro pseudo class active. /// [Hide] public bool HasIntro => (PseudoClass & PseudoClass.Intro) != 0; /// /// Whether this panel has the :outro pseudo class active. /// [Hide] public bool HasOutro => (PseudoClass & PseudoClass.Outro) != 0; public Panel() { InitializeEvents(); YogaNode = new YogaWrapper( this ); Style = new PanelStyle( this ); StyleSheet = new StyleSheetCollection( this ); Transitions = new Transitions( this ); ElementName = GetType().Name.ToLower(); Switch( PseudoClass.Empty, true ); LoadStyleSheet(); } public Panel( Panel parent ) : this() { if ( parent != null ) Parent = parent; } public Panel( Panel parent, string classnames ) : this( parent ) { if ( classnames != null ) AddClass( classnames ); } internal virtual void AddToLists() { Sandbox.Event.Register( this ); } internal virtual void RemoveFromLists() { Sandbox.Event.Unregister( this ); PanelLayer?.Dispose(); PanelLayer = null; } /// /// Called when a hotload happened. (Not necessarily on this panel) /// public virtual void OnHotloaded() { LoadStyleSheet(); InitializeEvents(); // If the checksum changed on our render tree, then we have to assume that everthing // about it changed. Lets destroy it and start from scratch. if ( razorLastTreeChecksum != GetRenderTreeChecksum() ) { razorLastTreeChecksum = GetRenderTreeChecksum(); if ( renderTree != null ) { renderTree?.Clear(); razorTreeDirty = true; } } // // Some of our children may have stopped existing. Like if they deleted the // type - then we'll have nulls here. Lets prune them out. // _children?.RemoveAll( x => x is null ); _renderChildren?.RemoveAll( x => x is null ); if ( _childrenHash is not null && _childrenHash.Any( x => x == null ) ) _childrenHash = _childrenHash.Where( x => x != null ).ToHashSet(); foreach ( var child in Children ) { try { child.OnHotloaded(); } catch ( System.Exception e ) { Log.Error( e ); } } } /// /// List of all s applied to this panel and all its ancestors. /// [Hide] public IEnumerable AllStyleSheets { get { foreach ( var p in AncestorsAndSelf ) { if ( p.StyleSheet.List == null ) continue; foreach ( var sheet in p.StyleSheet.List ) yield return sheet; } } } /// /// Switch a pseudo class on or off. /// public bool Switch( PseudoClass c, bool state ) { if ( state == ((PseudoClass & c) != 0) ) return false; if ( state ) { PseudoClass |= c; } else { PseudoClass &= ~c; } return true; } internal static void Switch( PseudoClass c, bool state, Panel panel, Panel unlessAncestorOf = null ) { if ( panel == null ) return; foreach ( var target in panel.AncestorsAndSelf ) { if ( unlessAncestorOf != null && unlessAncestorOf.IsAncestor( target ) ) continue; target.Switch( c, state ); } } /// /// Return true if this panel isn't hidden by opacity or displaymode. /// [Hide] public bool IsVisible { get; internal set; } = true; /// /// Return true if this panel isn't hidden by opacity or displaymode. /// [Hide] public bool IsVisibleSelf { get; internal set; } = true; /// /// Called every frame. This is your "Think" function. /// public virtual void Tick() { } /// /// Called after the parent of this panel has changed. /// public virtual void OnParentChanged() { } /// /// Returns true if this panel would like the mouse cursor to be visible. /// public virtual bool WantsMouseInput() { if ( ComputedStyle == null ) return false; if ( !IsVisibleSelf ) return false; if ( ComputedStyle.PointerEvents == PointerEvents.All ) return true; if ( _children is null ) return false; foreach ( var child in _children ) { if ( child?.WantsMouseInput() ?? false ) return true; } return false; } internal void TickInternal() { if ( Application.IsHeadless ) return; if ( IsDeleting ) { // we're probably transitioning out // so make sure we keep updating the layout SetNeedsPreLayout(); return; } try { if ( ParentHasChanged ) { ParentHasChanged = false; OnParentChanged(); StyleSelectorsChanged( true, true ); } var didBuildRenderTree = false; var isFirstRender = renderTree == null; if ( HasRenderTree || templateBindsChanged ) { InternalTreeBinds(); if ( templateBindsChanged ) { templateBindsChanged = false; razorTreeDirty = true; ParametersChanged( true ); } if ( razorTreeDirty ) { InternalRenderTree(); didBuildRenderTree = true; } } // keep before and after updated UpdateBeforeAfterElements(); // // If our style is dirty, or we're animating/transitioning/scrolling then make sure we get layed out // if ( Style is not null && (Style.IsDirty || HasActiveTransitions || (ComputedStyle?.HasAnimation ?? false) || ScrollVelocity != 0 || isScrolling || IsDragScrolling) ) { SetNeedsPreLayout(); } // // Don't tick our children if we're not visible // if ( IsVisible && _children is not null && _children.Count > 0 ) { for ( int i = _children.Count - 1; _children != null && i >= 0; i-- ) { _children[i]?.TickInternal(); } } // // Defer OnAfterTreeRender so that children are all processed too // if ( didBuildRenderTree ) { OnAfterTreeRender( isFirstRender ); } RunPendingEvents(); Tick(); RunPendingEvents(); AddScrollVelocity(); RunClassBinds(); } catch ( System.Exception e ) { Log.Error( e ); } } internal int GetRenderOrderIndex() { return (SiblingIndex) + (ComputedStyle?.ZIndex ?? 0); } /// /// Convert a point from the screen to a point representing a delta on this panel where /// the top left is [0,0] and the bottom right is [1,1] /// public Vector2 ScreenPositionToPanelDelta( Vector2 pos ) { pos = ScreenPositionToPanelPosition( pos ); var x = pos.x.LerpInverse( 0, Box.Rect.Width, false ); var y = pos.y.LerpInverse( 0, Box.Rect.Height, false ); return new Vector2( x, y ); } /// /// Convert a point from the screen to a position relative to the top left of this panel /// public Vector2 ScreenPositionToPanelPosition( Vector2 pos ) { if ( GlobalMatrix.HasValue ) { pos = GlobalMatrix.Value.Transform( pos ); } var x = pos.x - Box.Rect.Left; var y = pos.y - Box.Rect.Top; return new Vector2( x, y ); } /// /// Convert a point from local space to screen space /// public Vector2 PanelPositionToScreenPosition( Vector2 pos ) { var screenPos = new Vector2( pos.x + Box.Rect.Left, pos.y + Box.Rect.Top ); if ( GlobalMatrix.HasValue ) { screenPos = GlobalMatrix.Value.Inverted.Transform( screenPos ); } return screenPos; } /// /// Find and return any children of this panel (including self) within the given rect. /// /// The area to look for panels in, in screen-space coordinates. /// Whether we want only the panels that are completely within the given bounds. public IEnumerable FindInRect( Rect box, bool fullyInside ) { if ( !IsVisible ) yield break; if ( !IsInside( box, fullyInside ) ) yield break; yield return this; if ( !HasChildren ) yield break; foreach ( var child in Children ) { foreach ( var found in child.FindInRect( box, fullyInside ) ) { yield return found; } } } /// /// Allow selecting child text /// [Category( "Selection" )] public bool AllowChildSelection { get; set; } [Hide] public bool IsValid => YogaNode is not null; string CollectSelectedChildrenText( Panel p ) { if ( !p.IsVisible ) return null; if ( p is Sandbox.UI.Label label ) { return label.GetSelectedText(); } string selection = null; var lines = p.ComputedStyle.FlexDirection == FlexDirection.Column; foreach ( var child in p.Children ) { var sel = CollectSelectedChildrenText( child ); if ( string.IsNullOrEmpty( sel ) ) continue; if ( selection == null ) selection = sel; else selection = $"{selection}{(lines ? "\n" : " ")}{sel}"; } return selection; } /// /// Called when the player moves the mouse after "press and holding" (or dragging) the panel. /// protected virtual void OnDragSelect( SelectionEvent e ) { if ( AllowChildSelection ) { e.StopPropagation(); foreach ( var child in Children ) { UpdateSelection( child, e ); } } } /// /// If AllowChildSelection is enabled, we'll try to select all children text /// public void SelectAllInChildren() { if ( this is Sandbox.UI.Label label ) { label.ShouldDrawSelection = true; label.SelectionStart = 0; label.SelectionEnd = int.MaxValue; return; } foreach ( var child in Children ) { child.SelectAllInChildren(); } } /// /// Clear any selection in children /// public void UnselectAllInChildren() { if ( this is Sandbox.UI.Label label ) { label.ShouldDrawSelection = false; return; } foreach ( var child in Children ) { child.UnselectAllInChildren(); } } void UpdateSelection( Panel p, SelectionEvent e ) { var rect = e.SelectionRect; // child is outside of selection vertically if ( p.Box.Rect.Bottom < rect.Top || p.Box.Rect.Top > rect.Bottom ) { p.UnselectAllInChildren(); return; } // Selectable if ( p is Sandbox.UI.Label label ) { label.ShouldDrawSelection = true; if ( e.StartPoint.y > e.EndPoint.y ) { (e.EndPoint, e.StartPoint) = (e.StartPoint, e.EndPoint); } var start = p.Box.Rect.Top < rect.Top; var end = p.Box.Rect.Bottom > e.EndPoint.y; var negwidth = (e.EndPoint - e.StartPoint).x < 0; // selectAll if ( start && end ) { label.SelectionStart = label.GetLetterAtScreenPosition( new Vector2( rect.Left, rect.Top ) ); label.SelectionEnd = label.GetLetterAtScreenPosition( new Vector2( rect.Right, rect.Bottom ) ); } else if ( start ) { var from = negwidth ? rect.Right : rect.Left; label.SelectionStart = label.GetLetterAtScreenPosition( new Vector2( from, rect.Top ) ); label.SelectionEnd = int.MaxValue; } else if ( end ) { var to = negwidth ? rect.Left : rect.Right; label.SelectionStart = 0; label.SelectionEnd = label.GetLetterAtScreenPosition( new Vector2( to, rect.Bottom ) ); } else { label.SelectionStart = 0; label.SelectionEnd = int.MaxValue; } return; } foreach ( var child in p.Children ) { UpdateSelection( child, e ); } } /// /// Called when the current language has changed. This allows you to rebuild /// anything that might need rebuilding. Tokenized text labels should automatically update. /// public virtual void LanguageChanged() { foreach ( var child in Children ) { child.LanguageChanged(); } } /// /// Invoke a method after a delay. If the panel is deleted before this delay the method will not be called. /// public async void Invoke( float seconds, Action action ) { await Task.DelaySeconds( seconds ); if ( !this.IsValid() ) return; action.InvokeWithWarning(); } Dictionary invokes = new(); /// /// Invoke a method after a delay. If the panel is deleted before this delay the method will not be called. If the invoke is called /// while the old one is waiting, the old one will be cancelled. /// public async void InvokeOnce( string name, float seconds, Action action ) { CancelInvoke( name ); var tokenSource = new CancellationTokenSource(); invokes[name] = tokenSource; await Task.DelaySeconds( seconds ); if ( !this.IsValid() ) return; if ( tokenSource.IsCancellationRequested ) return; invokes.Remove( name ); action.InvokeWithWarning(); } /// /// Cancel a named invocation /// public void CancelInvoke( string name ) { if ( invokes.Remove( name, out var cts ) ) { cts.Cancel(); } } }