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 */
}
}
}