mirror of
https://github.com/Facepunch/sbox-public.git
synced 2026-01-13 16:59:38 -05:00
This commit imports the C# engine code and game files, excluding C++ source code. [Source-Commit: ceb3d758046e50faa6258bc3b658a30c97743268]
665 lines
15 KiB
C#
665 lines
15 KiB
C#
using Microsoft.AspNetCore.Components;
|
|
using Sandbox.Internal;
|
|
using System.Threading;
|
|
|
|
namespace Sandbox.UI;
|
|
|
|
/// <summary>
|
|
/// A simple User Interface panel. Can be styled with <a href="https://en.wikipedia.org/wiki/CSS">CSS</a>.
|
|
/// </summary>
|
|
[Library( "panel" ), Alias( "div", "span" ), Expose]
|
|
[Title( "Panel" ), Icon( "view_quilt" )]
|
|
public partial class Panel : IPanel, IValid, IComponent
|
|
{
|
|
/// <summary>
|
|
/// 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)
|
|
/// </summary>
|
|
[Property]
|
|
public string ElementName { get; set; }
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public string Id
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
/// <summary>
|
|
/// If this was created by razor, this is the file in which it was created
|
|
/// </summary>
|
|
[Hide]
|
|
public string SourceFile { get; set; }
|
|
|
|
/// <summary>
|
|
/// If this was created by razor, this is the line number in the file
|
|
/// </summary>
|
|
[Hide]
|
|
public int SourceLine { get; set; }
|
|
|
|
/// <summary>
|
|
/// Quick access to timing events, for async/await.
|
|
/// </summary>
|
|
public TaskSource Task = new( 1 );
|
|
|
|
/// <summary>
|
|
/// A collection of stylesheets applied to this panel directly.
|
|
/// </summary>
|
|
public StyleSheetCollection StyleSheet;
|
|
|
|
|
|
//
|
|
// Start with the intro flag on
|
|
//
|
|
PseudoClass _pseudoClass = PseudoClass.Intro;
|
|
|
|
/// <summary>
|
|
/// Special flags used by the styling system for hover, active etc..
|
|
/// </summary>
|
|
[Property]
|
|
public PseudoClass PseudoClass
|
|
{
|
|
get => _pseudoClass;
|
|
set
|
|
{
|
|
if ( _pseudoClass == value )
|
|
return;
|
|
|
|
_pseudoClass = value;
|
|
|
|
StyleSelectorsChanged( true, true );
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Whether this panel has the <c>:focus</c> pseudo class active.
|
|
/// </summary>
|
|
[Hide]
|
|
public bool HasFocus => (PseudoClass & PseudoClass.Focus) != 0;
|
|
|
|
/// <summary>
|
|
/// Whether this panel has the <c>:active</c> pseudo class active.
|
|
/// </summary>
|
|
[Hide]
|
|
public bool HasActive => (PseudoClass & PseudoClass.Active) != 0;
|
|
|
|
/// <summary>
|
|
/// Whether this panel has the <c>:hover</c> pseudo class active.
|
|
/// </summary>
|
|
[Hide]
|
|
public bool HasHovered => (PseudoClass & PseudoClass.Hover) != 0;
|
|
|
|
/// <summary>
|
|
/// Whether this panel has the <c>:intro</c> pseudo class active.
|
|
/// </summary>
|
|
[Hide]
|
|
public bool HasIntro => (PseudoClass & PseudoClass.Intro) != 0;
|
|
|
|
/// <summary>
|
|
/// Whether this panel has the <c>:outro</c> pseudo class active.
|
|
/// </summary>
|
|
[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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called when a hotload happened. (Not necessarily on this panel)
|
|
/// </summary>
|
|
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 );
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// List of all <see cref="UI.StyleSheet"/>s applied to this panel and all its <see cref="AncestorsAndSelf">ancestors</see>.
|
|
/// </summary>
|
|
[Hide]
|
|
public IEnumerable<StyleSheet> AllStyleSheets
|
|
{
|
|
get
|
|
{
|
|
foreach ( var p in AncestorsAndSelf )
|
|
{
|
|
if ( p.StyleSheet.List == null ) continue;
|
|
|
|
foreach ( var sheet in p.StyleSheet.List )
|
|
yield return sheet;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Switch a pseudo class on or off.
|
|
/// </summary>
|
|
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 );
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Return true if this panel isn't hidden by opacity or displaymode.
|
|
/// </summary>
|
|
[Hide]
|
|
public bool IsVisible { get; internal set; } = true;
|
|
|
|
/// <summary>
|
|
/// Return true if this panel isn't hidden by opacity or displaymode.
|
|
/// </summary>
|
|
[Hide]
|
|
public bool IsVisibleSelf { get; internal set; } = true;
|
|
|
|
/// <summary>
|
|
/// Called every frame. This is your "Think" function.
|
|
/// </summary>
|
|
public virtual void Tick()
|
|
{
|
|
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called after the parent of this panel has changed.
|
|
/// </summary>
|
|
public virtual void OnParentChanged()
|
|
{
|
|
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns true if this panel would like the mouse cursor to be visible.
|
|
/// </summary>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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]
|
|
/// </summary>
|
|
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 );
|
|
}
|
|
|
|
/// <summary>
|
|
/// Convert a point from the screen to a position relative to the top left of this panel
|
|
/// </summary>
|
|
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 );
|
|
}
|
|
|
|
/// <summary>
|
|
/// Convert a point from local space to screen space
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Find and return any children of this panel (including self) within the given rect.
|
|
/// </summary>
|
|
/// <param name="box">The area to look for panels in, in screen-space coordinates.</param>
|
|
/// <param name="fullyInside">Whether we want only the panels that are completely within the given bounds.</param>
|
|
public IEnumerable<Panel> 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;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Allow selecting child text
|
|
/// </summary>
|
|
[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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called when the player moves the mouse after "press and holding" (or dragging) the panel.
|
|
/// </summary>
|
|
protected virtual void OnDragSelect( SelectionEvent e )
|
|
{
|
|
if ( AllowChildSelection )
|
|
{
|
|
e.StopPropagation();
|
|
|
|
foreach ( var child in Children )
|
|
{
|
|
UpdateSelection( child, e );
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// If AllowChildSelection is enabled, we'll try to select all children text
|
|
/// </summary>
|
|
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();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Clear any selection in children
|
|
/// </summary>
|
|
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 );
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called when the current language has changed. This allows you to rebuild
|
|
/// anything that might need rebuilding. Tokenized text labels should automatically update.
|
|
/// </summary>
|
|
public virtual void LanguageChanged()
|
|
{
|
|
foreach ( var child in Children )
|
|
{
|
|
child.LanguageChanged();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Invoke a method after a delay. If the panel is deleted before this delay the method will not be called.
|
|
/// </summary>
|
|
public async void Invoke( float seconds, Action action )
|
|
{
|
|
await Task.DelaySeconds( seconds );
|
|
if ( !this.IsValid() ) return;
|
|
|
|
action.InvokeWithWarning();
|
|
}
|
|
|
|
Dictionary<string, CancellationTokenSource> invokes = new();
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Cancel a named invocation
|
|
/// </summary>
|
|
public void CancelInvoke( string name )
|
|
{
|
|
if ( invokes.Remove( name, out var cts ) )
|
|
{
|
|
cts.Cancel();
|
|
}
|
|
}
|
|
|
|
}
|