Files
sbox-public/engine/Sandbox.Engine/Systems/UI/Panel/Panel.Layout.cs
s&box team 71f266059a Open source release
This commit imports the C# engine code and game files, excluding C++ source code.

[Source-Commit: ceb3d758046e50faa6258bc3b658a30c97743268]
2025-11-24 09:05:18 +00:00

778 lines
20 KiB
C#

using Sandbox.Audio;
namespace Sandbox.UI;
public partial class Panel
{
internal YogaWrapper YogaNode;
/// <summary>
/// Access to various bounding boxes of this panel.
/// </summary>
[Hide]
public Box Box { get; init; } = new Box();
/// <summary>
/// If true, calls <see cref="DrawContent( ref RenderState )"/>.
/// </summary>
[Hide]
public virtual bool HasContent => false;
/// <summary>
/// The velocity of the current scroll
/// </summary>
[Hide]
public Vector2 ScrollVelocity;
/// <summary>
/// Offset of the panel's children position for scrolling purposes.
/// </summary>
[Hide]
public Vector2 ScrollOffset { get; set; }
/// <summary>
/// Scale of the panel on the screen.
/// </summary>
[Hide]
public float ScaleToScreen { get; internal set; } = 1.0f;
/// <summary>
/// Inverse scale of <see cref="ScaleToScreen"/>.
/// </summary>
[Hide]
public float ScaleFromScreen => 1.0f / ScaleToScreen;
int LayoutCount = 0;
/// <summary>
/// If this panel has transforms, they'll be reflected here
/// </summary>
[Hide]
public Matrix? LocalMatrix { get; internal set; }
/// <summary>
/// If this panel or its parents have transforms, they'll be compounded here.
/// </summary>
[Hide]
public Matrix? GlobalMatrix { get; internal set; }
/// <summary>
/// The matrix that is applied as a result of transform: styles
/// </summary>
[Hide]
internal Matrix TransformMatrix { get; set; }
/// <summary>
/// The computed style has a non-default backdrop filter property
/// </summary>
[Hide]
internal bool HasBackdropFilter { get; private set; }
/// <summary>
/// The computed style has a non-default filter property
/// </summary>
[Hide]
internal bool HasFilter { get; private set; }
/// <summary>
/// The computed style has a renderable background
/// </summary>
[Hide]
internal bool HasBackground { get; private set; }
internal void UpdateVisibility()
{
bool old = IsVisible;
IsVisibleSelf = ComputedStyle?.CalcVisible() ?? false;
IsVisibleSelf = IsVisibleSelf || HasActiveTransitions;
IsVisible = IsVisibleSelf && (Parent?.IsVisible ?? true);
if ( old == IsVisible )
return;
if ( Parent != null )
{
Parent.IndexesDirty = true;
}
var c = _children?.Count ?? 0;
for ( int i = 0; i < c; i++ )
{
_children[i].UpdateVisibility();
}
}
bool needsPreLayout = true;
bool needsFinalLayout = true;
internal void SetNeedsPreLayout()
{
if ( needsPreLayout ) return;
needsPreLayout = true;
needsFinalLayout = true;
Parent?.SetNeedsPreLayout();
}
internal virtual void PreLayout( LayoutCascade cascade )
{
if ( YogaNode == null )
return;
if ( !needsPreLayout && !cascade.SelectorChanged && !cascade.ParentChanged )
return;
needsPreLayout = false;
if ( IndexesDirty )
{
UpdateChildrenIndexes();
}
ComputedStyle = Style.BuildFinal( ref cascade, out bool changed );
cascade.ParentStyles = ComputedStyle;
PushLengthValues();
ScaleToScreen = cascade.Scale;
Opacity = ComputedStyle.Opacity.Value * (Parent?.Opacity ?? 1.0f);
UpdateVisibility();
if ( changed || !YogaNode.Initialized )
{
UpdateYoga();
}
if ( changed )
{
backgroundRenderDirty = true;
if ( Parent is not null )
{
Parent._renderChildrenDirty = true;
}
HasBackdropFilter = !ComputedStyle.IsDefault( "backdrop-filter-blur" )
|| !ComputedStyle.IsDefault( "backdrop-filter-contrast" )
|| !ComputedStyle.IsDefault( "backdrop-filter-saturate" )
|| !ComputedStyle.IsDefault( "backdrop-filter-sepia" )
|| !ComputedStyle.IsDefault( "backdrop-filter-invert" )
|| !ComputedStyle.IsDefault( "backdrop-filter-hue-rotate" )
|| !ComputedStyle.IsDefault( "backdrop-filter-brightness" );
HasFilter = !ComputedStyle.IsDefault( "filter-saturate" )
|| !ComputedStyle.IsDefault( "filter-brightness" )
|| !ComputedStyle.IsDefault( "filter-contrast" )
|| !ComputedStyle.IsDefault( "filter-blur" )
|| !ComputedStyle.IsDefault( "filter-sepia" )
|| !ComputedStyle.IsDefault( "filter-hue-rotate" )
|| !ComputedStyle.IsDefault( "filter-invert" )
|| !ComputedStyle.IsDefault( "filter-tint" )
|| !ComputedStyle.IsDefault( "filter-border-width" );
HasBackground = ComputedStyle.BackgroundColor.Value.a > 0f
|| ComputedStyle.BorderImageSource is not null
|| (ComputedStyle.BackgroundImage is not null && ComputedStyle.BackgroundImage != Texture.Invalid)
|| (ComputedStyle.BorderLeftColor.Value.a > 0f && ComputedStyle.BorderLeftWidth.Value.GetPixels( 1.0f ) > 0f)
|| (ComputedStyle.BorderTopColor.Value.a > 0f && ComputedStyle.BorderTopWidth.Value.GetPixels( 1.0f ) > 0f)
|| (ComputedStyle.BorderRightColor.Value.a > 0f && ComputedStyle.BorderRightWidth.Value.GetPixels( 1.0f ) > 0f)
|| (ComputedStyle.BorderBottomColor.Value.a > 0f && ComputedStyle.BorderBottomWidth.Value.GetPixels( 1.0f ) > 0f);
UpdateLayer( ComputedStyle );
}
UpdateOrder();
if ( LayoutCount > 0 && !IsVisibleSelf )
{
return;
}
if ( _children == null || _children.Count == 0 )
return;
// We need to tell the children to force an update if any of the parent's
// cascading styles have changed.
cascade.ParentChanged = cascade.ParentChanged || changed;
for ( int i = 0; i < _children.Count; i++ )
{
_children[i].PreLayout( cascade );
}
//
// Our children's 'order' properties might have changed
// if so, tell yoga about the new order
//
SortChildrenOrder();
}
internal void UpdateYoga()
{
if ( ComputedStyle == null )
return;
YogaNode.Width = ComputedStyle.Width;
YogaNode.Height = ComputedStyle.Height;
YogaNode.MaxWidth = ComputedStyle.MaxWidth;
YogaNode.MaxHeight = ComputedStyle.MaxHeight;
YogaNode.MinWidth = ComputedStyle.MinWidth;
YogaNode.MinHeight = ComputedStyle.MinHeight;
YogaNode.Display = ComputedStyle.Display;
YogaNode.Left = ComputedStyle.Left;
YogaNode.Right = ComputedStyle.Right;
YogaNode.Top = ComputedStyle.Top;
YogaNode.Bottom = ComputedStyle.Bottom;
YogaNode.MarginLeft = ComputedStyle.MarginLeft;
YogaNode.MarginRight = ComputedStyle.MarginRight;
YogaNode.MarginTop = ComputedStyle.MarginTop;
YogaNode.MarginBottom = ComputedStyle.MarginBottom;
YogaNode.PaddingLeft = ComputedStyle.PaddingLeft;
YogaNode.PaddingRight = ComputedStyle.PaddingRight;
YogaNode.PaddingTop = ComputedStyle.PaddingTop;
YogaNode.PaddingBottom = ComputedStyle.PaddingBottom;
YogaNode.BorderLeftWidth = ComputedStyle.BorderLeftWidth;
YogaNode.BorderTopWidth = ComputedStyle.BorderTopWidth;
YogaNode.BorderRightWidth = ComputedStyle.BorderRightWidth;
YogaNode.BorderBottomWidth = ComputedStyle.BorderBottomWidth;
YogaNode.PositionType = ComputedStyle.Position;
YogaNode.AspectRatio = ComputedStyle.AspectRatio;
YogaNode.FlexGrow = ComputedStyle.FlexGrow;
YogaNode.FlexShrink = ComputedStyle.FlexShrink;
YogaNode.FlexBasis = ComputedStyle.FlexBasis;
YogaNode.Wrap = ComputedStyle.FlexWrap;
YogaNode.AlignContent = ComputedStyle.AlignContent;
YogaNode.AlignItems = ComputedStyle.AlignItems;
YogaNode.AlignSelf = ComputedStyle.AlignSelf;
YogaNode.FlexDirection = ComputedStyle.FlexDirection;
YogaNode.JustifyContent = ComputedStyle.JustifyContent;
YogaNode.Overflow = ComputedStyle.Overflow;
YogaNode.RowGap = ComputedStyle.RowGap;
YogaNode.ColumnGap = ComputedStyle.ColumnGap;
YogaNode.Initialized = true;
}
/// <summary>
/// The currently calculated opacity.
/// This is set by multiplying our current style opacity with our parent's opacity.
/// </summary>
[Hide]
public float Opacity { get; private set; } = 1.0f;
/// <summary>
/// This panel has just been laid out. You can modify its position now and it will affect its children.
/// This is a useful place to restrict shit to the screen etc.
/// </summary>
public virtual void OnLayout( ref Rect layoutRect )
{
}
int layoutHash;
/// <summary>
/// Takes a <see cref="LayoutCascade"/> and returns an outer rect
/// </summary>
public virtual void FinalLayout( Vector2 offset )
{
if ( ComputedStyle is null )
return;
if ( YogaNode is null )
return;
PushLengthValues();
var hash = HashCode.Combine( offset, ScrollOffset, ScrollVelocity, ComputedStyle?.Transform, Opacity, ComputedStyle.Display );
if ( layoutHash == hash && !YogaNode.HasNewLayout && !needsFinalLayout ) return;
needsFinalLayout = false;
layoutHash = hash;
//if ( YogaNode.HasNewLayout || parentPos != offset )
{
Box.Rect = YogaNode.YogaRect;
Box.Rect.Position += offset;
OnLayout( ref Box.Rect );
Box.Padding = YogaNode.Padding;
Box.Margin = YogaNode.Margin;
Box.Border = YogaNode.Border;
Box.RectOuter = Box.Rect.Grow( YogaNode.Margin.Left, YogaNode.Margin.Top, YogaNode.Margin.Right, YogaNode.Margin.Bottom );
Box.RectInner = Box.Rect.Shrink( YogaNode.Padding.Left, YogaNode.Padding.Top, YogaNode.Padding.Right, YogaNode.Padding.Bottom );
Box.ClipRect = Box.Rect.Shrink( YogaNode.Border.Left, YogaNode.Border.Top, YogaNode.Border.Right, YogaNode.Border.Bottom );
UpdateLayer( ComputedStyle );
Box.Rect = Box.Rect.Floor();
Box.RectOuter = Box.RectOuter.Floor();
Box.RectInner = Box.RectInner.Floor();
Box.ClipRect = Box.ClipRect.Floor();
// Build the matrix that is generated from "transform" etc. We do this here after we have the size of the
// panel - which should be super duper fine.
TransformMatrix = ComputedStyle.BuildTransformMatrix( Box.Rect.Size );
backgroundRenderDirty = true;
}
//
// If we have an intro flag, we need to turn it off
// because by now it's been on for one frame
//
if ( HasIntro )
{
// A nice optimization here would be to not dirty the
// style selector if none of our styles have a :intro flag
Switch( PseudoClass.Intro, false );
}
if ( ComputedStyle.Display == DisplayMode.None ) return;
if ( LayoutCount > 0 && Opacity <= 0.0f ) return;
// The initial state should be true for these panels
// So there is no need to manually scroll to the bottom for scroll to be pinned there by default
if ( LayoutCount == 0 && PreferScrollToBottom )
{
IsScrollAtBottom = true;
}
bool wasScrollatBottom = IsScrollAtBottom;
offset = Box.Rect.Position - ScrollOffset.SnapToGrid( 1.0f );
FinalLayoutChildren( offset );
if ( wasScrollatBottom )
{
UpdateScrollPin();
}
LayoutCount++;
}
private void PushLengthValues()
{
Length.CurrentFontSize = ComputedStyle.FontSize ?? Length.Pixels( 13 ).Value;
}
/// <summary>
/// If true, we'll try to stay scrolled to the bottom when the panel changes size
/// </summary>
[Hide]
public bool PreferScrollToBottom { get; set; }
/// <summary>
/// Whether the scrolling is currently pinned to the bottom of the panel as dictated by <see cref="PreferScrollToBottom"/>.
/// </summary>
[Hide]
public bool IsScrollAtBottom { get; private set; }
/// <summary>
/// The size of the scrollable area within this panel.
/// </summary>
[Hide]
public Vector2 ScrollSize { get; private set; }
bool IsDragScrolling;
/// <summary>
/// Layout the children of this panel.
/// </summary>
/// <param name="offset">The parent's position.</param>
protected virtual void FinalLayoutChildren( Vector2 offset )
{
if ( !HasChildren )
return;
for ( int i = 0; i < _children.Count; i++ )
{
try
{
_children[i].FinalLayout( offset );
}
catch ( System.Exception e )
{
Log.Warning( e );
}
}
if ( ComputedStyle.Overflow.Value == OverflowMode.Scroll )
{
var rect = Box.Rect;
rect.Position -= ScrollOffset;
for ( int i = 0; i < _children.Count; i++ )
{
var child = _children[i];
if ( child.IsVisible )
{
rect.Add( child.GetLayoutRect() );
}
}
rect.Height += Box.Padding.Bottom;
rect.Right += Box.Padding.Right;
ConstrainScrolling( rect.Size );
}
else
{
ScrollOffset = 0;
}
}
Rect GetLayoutRect()
{
if ( HasChildren && ComputedStyle.Display == DisplayMode.Contents )
{
Rect rect = default;
for ( int i = 0; i < _children.Count; i++ )
{
var child = _children[i];
if ( child.IsVisible )
{
if ( i == 0 ) rect = child.GetLayoutRect();
else rect.Add( child.GetLayoutRect() );
}
}
return rect;
}
return Box.RectOuter;
}
private void UpdateScrollPin()
{
if ( !PreferScrollToBottom )
return;
if ( IsScrollAtBottom )
return;
if ( !ScrollVelocity.y.AlmostEqual( 0, 0.1f ) )
return;
ScrollOffset = new Vector2( ScrollOffset.x, ScrollSize.y );
IsScrollAtBottom = true;
ScrollVelocity.y = 0;
}
bool isScrolling;
Vector2 scrollVelocityVelocity;
protected virtual void AddScrollVelocity()
{
if ( ScrollVelocity.IsNearZeroLength )
{
ScrollVelocity = 0;
return;
}
ScrollVelocity = Vector2.SmoothDamp( ScrollVelocity, 0, ref scrollVelocityVelocity, 0.5f, RealTime.SmoothDelta );
// Bring it to a stop
if ( ScrollVelocity.y.AlmostEqual( 0, 0.01f ) ) ScrollVelocity.y = 0;
if ( ScrollVelocity.x.AlmostEqual( 0, 0.01f ) ) ScrollVelocity.x = 0;
}
/// <summary>
/// Constrain <see cref="ScrollOffset">scrolling</see> to the given size.
/// </summary>
protected virtual void ConstrainScrolling( Vector2 size )
{
if ( IsDragScrolling )
return;
isScrolling = false;
size -= Box.Rect.Size;
var heightChange = size.y - ScrollSize.y;
ScrollSize = size;
ScrollSize = ScrollSize.SnapToGrid( 1.0f );
var overflow = ComputedStyle.Overflow;
if ( overflow == OverflowMode.Visible || overflow == OverflowMode.Hidden )
{
ScrollOffset = 0;
return;
}
var so = ScrollOffset;
// add velocity
so += ScrollVelocity * RealTime.SmoothDelta * 60.0f;
// Reverse the axis if flex-direction: *-reverse or justify-content: flex-end;
var axisReversed = ComputedStyle.JustifyContent == Justify.FlexEnd || ComputedStyle.FlexDirection == FlexDirection.RowReverse || ComputedStyle.FlexDirection == FlexDirection.ColumnReverse;
IsScrollAtBottom = so.y + ScrollVelocity.y >= size.y;
if ( ScrollVelocity.y > 0 && IsScrollAtBottom ) so.y += heightChange;
//
// TODO - a style to let them turn springy mode off ?
//
var constrainSpeed = RealTime.SmoothDelta * 100.0f;
if ( axisReversed )
{
if ( so.y > 0 ) so.y = so.y.LerpTo( 0, constrainSpeed );
if ( so.x > 0 ) so.x = so.x.LerpTo( 0, constrainSpeed );
if ( so.y < -ScrollSize.y ) so.y = so.y.LerpTo( -ScrollSize.y, constrainSpeed );
if ( so.x < -ScrollSize.x ) so.x = so.x.LerpTo( -ScrollSize.x, constrainSpeed );
}
else
{
if ( so.y < 0 ) so.y = so.y.LerpTo( 0, constrainSpeed );
if ( so.x < 0 ) so.x = so.x.LerpTo( 0, constrainSpeed );
if ( so.y > ScrollSize.y ) so.y = so.y.LerpTo( ScrollSize.y, constrainSpeed );
if ( so.x > ScrollSize.x ) so.x = so.x.LerpTo( ScrollSize.x, constrainSpeed );
}
if ( ScrollOffset == so )
return;
ScrollOffset = so;
isScrolling = true;
}
/// <summary>
/// Play a sound from this panel.
/// </summary>
public void PlaySound( string sound )
{
if ( string.IsNullOrEmpty( sound ) )
return;
var h = Sound.Play( sound );
if ( !h.IsValid() )
return;
if ( FindRootPanel() is WorldPanel worldPanel )
{
// Calculate world position of the element, not just the root WorldPanel
var worldPosition = worldPanel.Position;
var panelPosition = Box.Rect.Position;
var worldRotation = worldPanel.Rotation * new Angles( 0, 90, 0 );
var worldOffset = new Vector3( panelPosition.x, panelPosition.y, 0 );
worldOffset = worldRotation * (worldOffset * ScenePanelObject.ScreenToWorldScale);
h.TargetMixer = Mixer.FindMixerByName( "Game" );
h.Position = worldPosition + worldOffset;
}
else
{
var normalizedScreenPosition = Box.Rect.Center / Screen.Size;
normalizedScreenPosition -= 0.5f;
h.TargetMixer = Mixer.FindMixerByName( "UI" );
h.Position = new Vector3( 64.0f, normalizedScreenPosition.x.Clamp( -1, 1 ) * -256.0f, -normalizedScreenPosition.y.Clamp( -1, 1 ) * 64.0f );
h.ListenLocal = true;
}
}
}
/// <summary>
/// Represents position and size of a <see cref="Panel"/> on the screen.
/// </summary>
[SkipHotload]
public class Box
{
/// <summary>
/// Position and size of the element on the screen, <b>including both - its padding AND margin</b>.
/// </summary>
public Rect RectOuter;
/// <summary>
/// Position and size of only the element's inner content on the screen, <i>without padding OR margin</i>.
/// </summary>
public Rect RectInner;
/// <summary>
/// The size of padding.
/// </summary>
public Margin Padding;
/// <summary>
/// The size of border.
/// </summary>
public Margin Border;
/// <summary>
/// The size of border.
/// </summary>
public Margin Margin;
/// <summary>
/// Position and size of the element on the screen, <b>including its padding</b>, <i>but not margin</i>.
/// </summary>
public Rect Rect;
/// <summary>
/// <see cref="Rect"/> minus the border sizes.
/// Used internally to "clip" (hide) everything outside of these bounds, if the panels <see cref="OverflowMode"/> is not set to <see cref="OverflowMode.Visible"/>.
/// </summary>
public Rect ClipRect;
/// <summary>
/// Position of the left edge in screen coordinates.
/// </summary>
public float Left => Rect.Left;
/// <summary>
/// Position of the right edge in screen coordinates.
/// </summary>
public float Right => Rect.Right;
/// <summary>
/// Position of the top edge in screen coordinates.
/// </summary>
public float Top => Rect.Top;
/// <summary>
/// Position of the bottom edge in screen coordinates.
/// </summary>
public float Bottom => Rect.Bottom;
}
internal static class YogaEx
{
public static void SetYoga( this ref Length? self, YGNodeRef _native, Func<float> dimension, Action<YGNodeRef> setAuto, Action<YGNodeRef, float> setUnit, Action<YGNodeRef, float> setPercent )
{
if ( !self.HasValue || self.Value.Unit == LengthUnit.Undefined )
{
setUnit( _native, float.NaN );
return;
}
if ( self.Value.Unit == LengthUnit.Expression )
{
setUnit( _native, self.Value.GetPixels( dimension() ) );
return;
}
if ( self.Value.Unit == LengthUnit.Auto )
{
setAuto?.Invoke( _native );
return;
}
if ( self.Value.Unit == LengthUnit.Pixels )
{
setUnit( _native, self.Value.Value );
return;
}
if ( self.Value.Unit == LengthUnit.Percentage )
{
setPercent( _native, self.Value.Value );
return;
}
if ( self.Value.Unit == LengthUnit.ViewHeight || self.Value.Unit == LengthUnit.ViewWidth || self.Value.Unit == LengthUnit.ViewMin || self.Value.Unit == LengthUnit.ViewMax )
{
setUnit( _native, self.Value.GetPixels( 0.0f ) );
return;
}
if ( self.Value.Unit == LengthUnit.RootEm || self.Value.Unit == LengthUnit.Em )
{
setUnit( _native, self.Value.GetPixels( dimension() ) );
return;
}
}
public static void SetYoga( this ref Length? self, YGNodeRef _native, Func<float> dimension, Action<YGNodeRef, YGEdge> setAuto, Action<YGNodeRef, YGEdge, float> setUnit, Action<YGNodeRef, YGEdge, float> setPercent, YGEdge edge )
{
if ( !self.HasValue || self.Value.Unit == LengthUnit.Undefined )
{
setUnit( _native, edge, float.NaN );
return;
}
if ( self.Value.Unit == LengthUnit.Expression )
{
setUnit( _native, edge, self.Value.GetPixels( dimension() ) );
return;
}
if ( self.Value.Unit == LengthUnit.Auto )
{
setAuto?.Invoke( _native, edge );
return;
}
if ( self.Value.Unit == LengthUnit.Pixels )
{
setUnit( _native, edge, self.Value.Value );
return;
}
if ( self.Value.Unit == LengthUnit.Percentage )
{
if ( setPercent is not null )
{
setPercent( _native, edge, self.Value.Value );
}
else
{
setUnit( _native, edge, self.Value.GetPixels( dimension() ) );
}
return;
}
if ( self.Value.Unit == LengthUnit.ViewHeight || self.Value.Unit == LengthUnit.ViewWidth || self.Value.Unit == LengthUnit.ViewMin || self.Value.Unit == LengthUnit.ViewMax )
{
setUnit( _native, edge, self.Value.GetPixels( 0.0f ) );
return;
}
if ( self.Value.Unit == LengthUnit.RootEm || self.Value.Unit == LengthUnit.Em )
{
setUnit( _native, edge, self.Value.GetPixels( dimension() ) );
return;
}
}
public static float ToFloat( this Length? self, Length? dimension )
{
if ( self == null ) return 0;
if ( self.Value.Unit == LengthUnit.Expression )
return self.Value.GetPixels( dimension?.Value ?? 0f );
if ( self.Value.Unit == LengthUnit.Pixels )
return self.Value.Value;
if ( self.Value.Unit == LengthUnit.RootEm || self.Value.Unit == LengthUnit.Em )
return self.Value.GetPixels( dimension?.Value ?? 0f );
// TODO
return self.Value.Value;
}
}