using System.Globalization; namespace Sandbox.UI { /// /// A variable unit based length. ie, could be a percentage or a pixel length. This is commonly used to express the size of things in UI space, usually coming from style sheets. /// public struct Length : IEquatable { /// /// The meaning of the value is dependent on . /// public float Value; /// /// How to determine the final length. Commonly used with Pixel or Percentage. /// public LengthUnit Unit; /// /// The current root panel size. This is required for vh, vw, vmin and vmax. This is set during PreLayout, Layout and PostLayout. /// internal static Vector2 RootSize; /// /// The current root panel font size. This is required for rem. This is set during PreLayout, Layout and PostLayout. /// internal static Length RootFontSize; /// /// The current panel font size. This is required for em. This is set during PreLayout. /// internal static Length CurrentFontSize; /// /// The current root scale factor. This is required for dpi scaling. This is set during PreLayout, Layout and PostLayout. /// internal static float RootScale = 1.0f; /// /// If the length unit is Expression, this will represent the calc() expression parsed /// private string _expression; /// /// Convert to a pixel value. Use the dimension to work out percentage values. /// public readonly float GetPixels( float dimension ) { if ( Unit == LengthUnit.Percentage ) return dimension * (Value / 100.0f); if ( Unit == LengthUnit.ViewWidth ) return RootSize.x * (Value / 100.0f); if ( Unit == LengthUnit.ViewHeight ) return RootSize.y * (Value / 100.0f); if ( Unit == LengthUnit.ViewMin ) return Math.Min( RootSize.x, RootSize.y ) * (Value / 100.0f); if ( Unit == LengthUnit.ViewMax ) return Math.Max( RootSize.x, RootSize.y ) * (Value / 100.0f); if ( Unit == LengthUnit.Expression ) return UI.Calc.Evaluate( _expression, dimension ); if ( Unit == LengthUnit.RootEm ) return RootFontSize.Value * Value; if ( Unit == LengthUnit.Em ) return CurrentFontSize.Value * Value; return Value; } /// /// Used in situations where the scale couldn't be applied during style computation /// internal readonly float GetScaledPixels( float dimension ) { if ( Unit == LengthUnit.Pixels ) return Value * RootScale; if ( Unit == LengthUnit.RootEm ) return GetPixels( dimension ) * RootScale; return GetPixels( dimension ); } /// /// Get the pixel size but also evaluate content size to support use Start, End, Center /// public readonly float GetPixels( float dimension, float contentSize ) { if ( Unit == LengthUnit.Start ) return 0.0f; if ( Unit == LengthUnit.End ) return dimension - contentSize; if ( Unit == LengthUnit.Center ) return (dimension - contentSize) * 0.5f; return GetPixels( dimension ); } public static implicit operator Length( float value ) { return new Length { Value = value, Unit = LengthUnit.Pixels }; } public readonly override bool Equals( object obj ) => base.Equals( obj ); public readonly override int GetHashCode() => HashCode.Combine( Value, Unit ); /// /// Lerp from one length to another. /// /// Length at delta 0 /// Length at delta 1 /// The interpolation stage /// The width or height of the parent to use when working out percentage lengths /// The interpolated Length internal static Length? Lerp( Length a, Length b, float delta, float dimension ) { var x = a.GetPixels( dimension ); var y = b.GetPixels( dimension ); var diff = (y - x); return x + diff * delta; } /// /// Lerp from one length to another. /// /// Length at delta 0 /// Length at delta 1 /// The interpolation stage /// The width or height of the parent to use when working out percentage lengths /// Evaluate content size to support use Start, End, Center /// The interpolated Length internal static Length? Lerp( Length a, Length b, float delta, float dimension, float contentSize ) { var x = a.GetPixels( dimension, contentSize ); var y = b.GetPixels( dimension, contentSize ); var diff = (y - x); return x + diff * delta; } /// /// Lerp from one length to another. /// /// Length at delta 0 /// Length at delta 1 /// The interpolation stage /// The interpolated Length internal static Length? Lerp( Length a, Length b, float delta ) { if ( a.Unit != b.Unit ) return b; return new Length { Unit = a.Unit, Value = a.Value.LerpTo( b.Value, delta, false ) }; } /// /// Create a length in pixels /// /// The amount of pixels for this length /// A new length public static Length? Pixels( float pixels ) => new Length { Value = pixels, Unit = LengthUnit.Pixels }; /// /// Create a length in percents /// /// The amount of percent for this (0-100) /// A new length public static Length? Percent( float percent ) => new Length { Value = percent, Unit = LengthUnit.Percentage }; /// /// Create a length based on the view height /// /// The amount of percent for this (0-100) /// A new length public static Length? ViewHeight( float percentage ) => new Length { Value = percentage, Unit = LengthUnit.ViewHeight }; /// /// Create a length based on the view width /// /// The amount of percent for this (0-100) /// A new length public static Length? ViewWidth( float percentage ) => new Length { Value = percentage, Unit = LengthUnit.ViewWidth }; /// /// Create a length based on the longest edge of the screen size /// /// The amount of percent for this (0-100) /// A new length public static Length? ViewMax( float percentage ) => new Length { Value = percentage, Unit = LengthUnit.ViewMax }; /// /// Create a length based on the shortest edge of the screen size /// /// The amount of percent for this (0-100) /// A new length public static Length? ViewMin( float percentage ) => new Length { Value = percentage, Unit = LengthUnit.ViewMin }; /// /// Create a length in percents /// /// The fraction of a percent (0 = 0%, 1 = 100%) /// A new length public static Length? Fraction( float fraction ) => Percent( fraction * 100 ); /// /// Create a length based on a css calc expression /// public static Length? Calc( string expression ) => new Length { Unit = LengthUnit.Expression, _expression = expression }; /// /// Create a length based on the font size of the root element. /// /// Value in rem /// A new length public static Length Rem( float value ) => new Length { Value = value, Unit = LengthUnit.RootEm }; /// /// Create a length based on the font size of the current element. /// /// Value in em /// A new length public static Length Em( float value ) => new Length { Value = value, Unit = LengthUnit.Em }; /// /// Quickly create a Length with Unit set to LengthUnit.Auto /// public static Length Auto => new Length { Unit = LengthUnit.Auto }; /// /// Quickly create a Length with Unit set to LengthUnit.Contain /// public static Length Contain => new Length { Unit = LengthUnit.Contain }; /// /// Quickly create a Length with Unit set to LengthUnit.Cover /// public static Length Cover => new Length { Unit = LengthUnit.Cover }; public static Length Undefined => new Length { Unit = LengthUnit.Undefined }; /// /// Parse a length. This is used by the stylesheet parsing system. /// /// A length represented by a string /// Length.Parse( "100px" ) /// Length.Parse( "56%" ) /// public static Length? Parse( string value ) { if ( value == "center" ) return new Length { Unit = LengthUnit.Center }; if ( value == "left" || value == "top" ) return new Length { Unit = LengthUnit.Start }; if ( value == "right" || value == "bottom" ) return new Length { Unit = LengthUnit.End }; if ( value == "cover" ) return Cover; if ( value == "contain" ) return Contain; if ( value == "auto" ) return Auto; // Store this as an expression, defers the actual calculation until we use it if ( value.StartsWith( "calc(" ) ) return Calc( value ); // For keyframes if ( value == "from" ) return Length.Percent( 0 ); if ( value == "to" ) return Length.Percent( 100 ); var pc = value.IndexOf( '%' ); if ( pc > 0 ) { var num = value.Substring( 0, pc ); return Length.Percent( float.Parse( num, CultureInfo.InvariantCulture ) ); } var p = value.IndexOf( 'p' ); var x = value.IndexOf( 'x' ); if ( p > 0 && x == p + 1 ) { var num = value.Substring( 0, p ); return Length.Pixels( float.Parse( num, CultureInfo.InvariantCulture ) ); } var d = value.IndexOf( 'd' ); var e = value.IndexOf( 'e' ); var g = value.IndexOf( 'g' ); if ( d > 0 && e == d + 1 && g == d + 2 ) { var num = value.Substring( 0, d ); return Length.Pixels( float.Parse( num, CultureInfo.InvariantCulture ) ); } var v = value.IndexOf( 'v' ); var h = value.IndexOf( 'h' ); if ( v > 0 && h == v + 1 ) { var num = value.Substring( 0, v ); return Length.ViewHeight( float.Parse( num, CultureInfo.InvariantCulture ) ); } var w = value.IndexOf( 'w' ); if ( v > 0 && w == v + 1 ) { var num = value.Substring( 0, v ); return Length.ViewWidth( float.Parse( num, CultureInfo.InvariantCulture ) ); } var m = value.IndexOf( 'm' ); var i = value.IndexOf( 'i' ); var n = value.IndexOf( 'n' ); if ( v > 0 && m == v + 1 && i == v + 2 && n == v + 3 ) { var num = value.Substring( 0, v ); return Length.ViewMin( float.Parse( num, CultureInfo.InvariantCulture ) ); } var a = value.IndexOf( 'a' ); if ( v > 0 && m == v + 1 && a == v + 2 && x == v + 3 ) { var num = value.Substring( 0, v ); return Length.ViewMax( float.Parse( num, CultureInfo.InvariantCulture ) ); } var r = value.IndexOf( 'r' ); if ( r > 0 && e == r + 1 && m == r + 2 ) { var num = value.Substring( 0, r ); return Length.Rem( float.Parse( num, CultureInfo.InvariantCulture ) ); } if ( e > 0 && m == e + 1 ) { var num = value.Substring( 0, e ); return Length.Em( float.Parse( num, CultureInfo.InvariantCulture ) ); } // // If we can parse it as a float, treat it as pixels // if ( float.TryParse( value, NumberStyles.Float, CultureInfo.InvariantCulture, out float fnum ) ) { return Length.Pixels( fnum ); } return null; } public readonly bool Equals( Length other ) { return (Value, Unit) == (other.Value, other.Unit); } public static bool operator ==( Length lhs, Length rhs ) { return lhs.Equals( rhs ); } public static bool operator !=( Length lhs, Length rhs ) { return !lhs.Equals( rhs ); } public static bool operator ==( Length? lhs, Length? rhs ) { if ( !lhs.HasValue && !rhs.HasValue ) return true; if ( !lhs.HasValue ) return false; if ( !rhs.HasValue ) return false; return lhs.Value.Equals( rhs.Value ); } public static bool operator !=( Length? lhs, Length? rhs ) { if ( !lhs.HasValue && !rhs.HasValue ) return false; if ( !lhs.HasValue ) return true; if ( !rhs.HasValue ) return true; return !lhs.Value.Equals( rhs.Value ); } /// /// If it's a %, will return 0-1. If not it'll return its value. /// internal readonly float GetFraction( float f = 1.0f ) { return GetPixels( 1.0f ) * f; } public readonly override string ToString() { if ( Unit == LengthUnit.Expression ) return $"{_expression}"; if ( Unit == LengthUnit.Pixels ) return $"{Value}px"; if ( Unit == LengthUnit.Percentage ) return $"{Value}%"; if ( Unit == LengthUnit.RootEm ) return $"{Value}rem"; if ( Unit == LengthUnit.Em ) return $"{Value}em"; return $"{Unit}"; } internal static void Scale( ref Length? scale, float amount, bool skipRounding = false ) { if ( scale == null ) return; var s = scale.Value; Scale( ref s, amount, skipRounding ); scale = s; } internal static void Scale( ref Length scale, float amount, bool skipRounding = false ) { if ( scale == null ) return; if ( scale.Unit == LengthUnit.Pixels ) { if ( !skipRounding ) { scale = Pixels( System.MathF.Ceiling( scale.Value * amount ) ).Value; } else { scale = Pixels( scale.Value * amount ).Value; } } if ( scale.Unit == LengthUnit.RootEm || scale.Unit == LengthUnit.Em ) { scale = scale with { Value = scale.Value * amount }; } } } /// /// Possible units for various CSS properties that require length, used by struct. /// public enum LengthUnit : byte { /// /// The layout engine will calculate and select a width for the specified element. /// Auto = 0, /// /// The length is in pixels. /// Pixels, /// /// The length is a percentage (0-100) of the parent's length. (typically) /// Percentage, /// /// The length is a percentage (0-100) of the viewport's height. /// ViewHeight, /// /// The length is a percentage (0-100) of the viewport's width. /// ViewWidth, /// /// The length is a percentage (0-100) of the viewport's smallest side/edge. /// ViewMin, /// /// The length is a percentage (0-100) of the viewport's largest side/edge. /// ViewMax, /// /// Start of the parent at the appropriate axis. /// Start, /// /// For background images, cover the entire element with the image, stretcing and cropping as necessary. /// Cover, /// /// For background images, contain the image within the element bounds. /// Contain, /// /// End of the parent at the appropriate axis. /// End, /// /// In the middle of the parent at the appropriate axis. /// Center, /// /// Similar to CSS 'unset', basically means we don't have a value; should only really be used under certain /// circumstances (e.g. to handle background sizing properly). /// Undefined, /// /// Represents a calc( ... ) expression /// Expression, /// /// Font size of the root element. /// RootEm, /// /// Font size of the current element. /// Em } public static class LengthUnitExtension { /// /// Determine whether this unit type is dynamic (ie. should be updated regularly) or whether it's constant /// public static bool IsDynamic( this LengthUnit unit ) { return unit == LengthUnit.ViewWidth || unit == LengthUnit.ViewHeight /* vw/vh */ || unit == LengthUnit.ViewMin || unit == LengthUnit.ViewMax /* vmin/vmax */ || unit == LengthUnit.Expression /* calc( ... ) */ || unit == LengthUnit.RootEm || unit == LengthUnit.Em; /* em/rem */ } } }