using Godot; using Rokojori; using System.Collections.Generic; using System.Linq; namespace Rokojori { [Tool] [GlobalClass] public partial class UINumber : Resource { [Export] public float value = 0; [Export] public string unit = ""; [ExportGroup("Animation")] [Export] public bool isAnimated; [Export] public Curve animationCurve; [Export] public float animationDuration; [Export] public float animationOffset; [Export] public TimeLine timeLine; public bool IsNone => unit == "none"; public UINumber GetMultiplied( float scale ) { var uiNumber = new UINumber(); uiNumber.CopyFrom( this ); uiNumber.value = value * scale; return uiNumber; } public static UINumber Create( float value, string unit = "" ) { var uiNumber = new UINumber(); uiNumber.unit = unit; uiNumber.value = value; return uiNumber; } public static UINumber EM( float em ) { return Create( em, "em" ); } public static UINumber PW( float pw ) { return Create( pw, "pw" ); } public static UINumber PH( float ph ) { return Create( ph, "ph" ); } public float ComputeRaw( Control control, float alternative ) { return UINumber.Compute( control, this, alternative ); } public static bool IsNullOrNone( UIStylePropertyContainer container, UIStyleNumberProperty property ) { return IsNullOrNone( UIStyle.GetUINumberProperty( container, property, "", container ) ); } public static bool IsNullOrNone( UINumber number ) { if ( number == null ) { return true; } return number.IsNone; } public static int ComputeInt( Control control, UINumber number, float alternative = 0, float relative = 100 ) { return Mathf.RoundToInt( Compute( control, number, alternative, relative ) ); } public static int ComputeInt( Control control, UIStyleNumberProperty property, float alternative = 0, float relative = 100 ) { return ComputeInt( control, property, "", alternative, relative ); } public static int ComputeInt( Control control, UIStyleNumberProperty property, string shaderPropertyName = "", float alternative = 0, float relative = 100 ) { return Mathf.RoundToInt( Compute( control, property, shaderPropertyName, alternative, relative ) ); } public bool Equals( UINumber other ) { if ( other == this ) { return true; } if ( other == null ) { return false; } if ( other.value != value ) { return false; } if ( other.unit != unit ) { return false; } if ( other.isAnimated != isAnimated ) { return false; } if ( other.animationCurve != animationCurve ) { return false; } if ( other.animationDuration != animationDuration ) { return false; } if ( other.animationOffset != animationOffset ) { return false; } if ( other.timeLine != timeLine ) { return false; } return true; } public void CopyFrom( UINumber other ) { if ( other == this ) { return; } value = other.value; unit = other.unit; isAnimated = other.isAnimated; animationCurve = other.animationCurve; animationDuration = other.animationDuration; animationOffset = other.animationOffset; timeLine = other.timeLine; } public static float Compute( Control control, UIStyleNumberProperty property, float alternative = 0, float relative = 100 ) { return Compute( control, property, "", alternative, relative ); } public static float Compute( Control control, UIStyleNumberProperty property, string shaderPropertyName = "", float alternative = 0, float relative = 100 ) { var container = control as UIStylePropertyContainerNode; // Get selected ui number var uiNumber = UIStyle.GetUINumberProperty( container, property, shaderPropertyName, container ); var computedNumber = Compute( control, uiNumber, alternative, relative ); if ( uiNumber != null && uiNumber.isAnimated ) { container.SetAnimatedFlag(); } var transition = UIStyle.GetTransition( container, property, shaderPropertyName ); var transitionAll = UIStyle.GetTransitionSettingsAll( container ); var usesTransition = transition != null || transitionAll != null && transitionAll.transitionAllProperties; if ( ! usesTransition ) { return computedNumber; } var activeNumberTransitions = container.GetActiveUINumberTransitions(); var propertyTransition = activeNumberTransitions.Find( t => t != null && t.propertyType.Matches( property, shaderPropertyName ) ); propertyTransition = EnsureTransitionObject( propertyTransition, control, uiNumber, property, shaderPropertyName ); var endValueChanged = uiNumber != propertyTransition.value; if ( endValueChanged ) { var transitionSettings = UIStyle.GetTransitionSettings( container, property, shaderPropertyName ); propertyTransition.StartTransition( control, uiNumber, transitionSettings ); } if ( ! propertyTransition.transitioning ) { return computedNumber; } var computedTransitionValue = 0f; for ( int i = 0; i < propertyTransition.lastValues.Count; i++ ) { var lastValue = Compute( control, propertyTransition.lastValues[ i ], 0f ); computedTransitionValue += lastValue * propertyTransition.lastWeights[ i ]; } var phase = propertyTransition.GetUnclampedTransitionPhase(); var lerpWeight = propertyTransition.ComputeTransitionWeight( phase ); if ( phase >= 1f ) { propertyTransition.EndTransition(); } var result = Mathf.Lerp( computedTransitionValue, computedNumber, lerpWeight ); return result; // if ( propertyTransition == null ) // { // propertyTransition = new ActiveStyleTransition(); // propertyTransition.propertyType = property; // propertyTransition.value = number; // propertyTransition.transitioning = false; // activeNumberTransitions.Add( propertyTransition ); // return computedValue; // } // else // { // if ( propertyTransition.value != number && ! propertyTransition.transitioning && UIStyle.GetTransitionSettings( container, property ) != null ) // { // var transitionSettings = UIStyle.GetTransitionSettings( container, property ); // propertyTransition.timeLine = UI.GetTimeLine( control, transitionSettings.timeLine ); // propertyTransition.start = propertyTransition.timeLine.position; // propertyTransition.end = propertyTransition.start + transitionSettings.duration; // propertyTransition.transitioning = true; // propertyTransition.curve = transitionSettings.curve; // } // } // if ( propertyTransition.value == number ) // { // propertyTransition.transitioning = false; // return computedValue; // } // var computedTransitionValue = Compute( control, propertyTransition.value, alternative, relative ); // var transitionPhase = propertyTransition.timeLine.ComputeRange( propertyTransition.start, propertyTransition.end ); // if ( transitionPhase >= 1 ) // { // activeNumberTransitions.Remove( propertyTransition ); // } // var amount = MathX.Clamp01( transitionPhase ); // var curveAmount = amount; // if ( propertyTransition.curve != null ) // { // curveAmount = propertyTransition.curve.Sample( curveAmount ); // } // return Mathf.Lerp( computedTransitionValue, computedValue, curveAmount ); } static ActiveStyleTransition EnsureTransitionObject( ActiveStyleTransition propertyTransition, Control control, UINumber uiNUmber, UIStyleNumberProperty property, string shaderPropertyName ) { if ( propertyTransition != null ) { return propertyTransition; } var container = control as UIStylePropertyContainer; propertyTransition = new ActiveStyleTransition(); propertyTransition.propertyType = UIStyleNumberPropertyAndName.Create( property, shaderPropertyName ); var transitionSettings = UIStyle.GetTransitionSettings( container, property, shaderPropertyName ); propertyTransition.value = uiNUmber; propertyTransition.timeLine = UI.GetTimeLine( control, transitionSettings.timeLine ); propertyTransition.transitioning = false; container.GetActiveUINumberTransitions().Add( propertyTransition ); return propertyTransition; } public static float Compute( Control control, UINumber number, float alternative = 0, float relative = 100 ) { if ( number == null || control == null || number.IsNone ) { return alternative; } var width = UI.GetWindowWidth( control ); var height = UI.GetWindowHeight( control ); return Compute( control, number, width, height, relative ); } public static float em( Control control ) { var ui = UIHolder.GetUI( control ); return ui == null ? 12 : ui.X_computedFontSizePixels; } public static float uiw( Control control ) { var ui = UIHolder.GetUI( control ); return ui == null ? 1920 : ui.Size.X; } public static float uih( Control control ) { var ui = UIHolder.GetUI( control ); return ui == null ? 1080 : ui.Size.Y; } public static float Compute( Control control, UINumber number, float width, float height, float relative ) { var value = ComputeFormulaValue( control, number, width, height, relative ); if ( ! number.isAnimated || number.animationCurve == null ) { return value; } var timeLine = UI.GetTimeLine( control, number.timeLine ); var phase = timeLine.ComputePhase( number.animationDuration, number.animationOffset ); return number.animationCurve.Sample( phase ) * value; } static readonly string[] _variables = new string[] { "em", "vw", "vh", "pw", "ph", "cw","ch", "cx","cy", "uiw", "uih", "relative", "value" }; static MapList cached = new MapList(); class CachedExpressionData { public string formula; public Godot.Collections.Array inputs; public float result; public bool Matches( UINumber number, Godot.Collections.Array inputs ) { if ( number.unit != formula ) { return false; } for ( int i = 0; i < inputs.Count; i++ ) { if ( (float)inputs.ElementAt( i ) != (float)this.inputs.ElementAt( i ) ) { return false; } } return true; } } static List cachedExpressions = new List(); static int maxCachedExpressions = 50; static int GetCacheIndex( UINumber number, Godot.Collections.Array inputs ) { return cachedExpressions.FindIndex( c => c.Matches( number, inputs ) ); } static float ComputeFormulaValue( Control control, UINumber number, float width, float height, float relative ) { if ( number == null ) { return 0; } switch ( number.unit ) { case "em": { return number.value * em( control ); } case "vw": { return number.value * width / 100f; } case "vh": { return number.value * height / 100f; } case "pw": { var parent = control.GetParent() as Control; return number.value / 100f * ( parent == null ? width : parent.Size.X ); } case "ph": { var parent = control.GetParent() as Control; return number.value / 100f * ( parent == null ? height : parent.Size.Y ); } case "cw": { var parent = control.GetParent() as Control; return number.value / 100f * ( parent == null ? width : UILayouting.GetContentSize( parent ).X ); } case "ch": { var parent = control.GetParent() as Control; return number.value / 100f * ( parent == null ? height : UILayouting.GetContentSize( parent ).Y ); } case "cx": { var parent = control.GetParent() as Control; return number.value / 100f * ( parent == null ? 0 : UILayouting.GetContentOffset( parent ).X ); } case "cy": { var parent = control.GetParent() as Control; return number.value / 100f * ( parent == null ? 0 : UILayouting.GetContentOffset( parent ).Y ); } case "uiw": { return number.value * uiw( control ); } case "uih": { return number.value * uih( control ); } case "": { return number.value; } case "%": { return number.value * relative / 100f; } } var parentControl = control.GetParent() as Control;; var inputs = GetInputs( control, number, parentControl, width, height, relative ); var cacheIndex = GetCacheIndex( number, inputs ); if ( cacheIndex != -1 ) { var cachedResult = cachedExpressions[ cacheIndex ].result; cachedExpressions.SwapTowardsBegin( cacheIndex ); // control.LogInfo( "Using cached value", cachedExpressions[ cacheIndex ].formula, ">", cachedResult ); return cachedResult; } var expression = new Expression(); var expressionText = number.unit == null ? "" : RegexUtility.Replace( number.unit, "%", " * relative " ); expressionText = RegexUtility.Replace( expressionText, @"\b(\d+)\s*(em|v[wh]|p[wh]|c[wh]|c[xy]|ui[wh])\b", "$1*$2" ); if ( ! IsValid( expressionText ) ) { return 0; } var parseResult = expression.Parse( expressionText, _variables ); if ( Error.Ok != parseResult ) { return 0; } var scale = number.unit.IndexOf( "value" ) == -1 ? number.value : 1; var result = (float) ( scale * expression.Execute( inputs ).AsDouble() ); Cache( number.unit, inputs, result ); return result; } static void Cache( string formula, Godot.Collections.Array inputs, float result ) { CachedExpressionData data = null; if ( cachedExpressions.Count < maxCachedExpressions ) { data = new CachedExpressionData(); cachedExpressions.Add( data ); } else { data = GodotRandom.Get().From( cachedExpressions, 4 * maxCachedExpressions / 5, maxCachedExpressions - 1 ); } data.formula = formula; data.inputs = inputs; data.result = result; } static Godot.Collections.Array GetInputs( Control control, UINumber number, Control parentControl, float width, float height, float relative ) { var inputs = new Godot.Collections.Array(); // em inputs.Add( em( control ) ); // vw, vh inputs.Add( width / 100f ); inputs.Add( height / 100f ); // pw, ph inputs.Add( ( parentControl == null ? width : parentControl.Size.X ) / 100f ); inputs.Add( ( parentControl == null ? height : parentControl.Size.Y ) / 100f ); // cw, ch inputs.Add( ( parentControl == null ? width : UILayouting.GetContentSize( parentControl ).X ) / 100f ); inputs.Add( ( parentControl == null ? height : UILayouting.GetContentSize( parentControl ).Y ) / 100f ); // cx, cy inputs.Add( ( parentControl == null ? 0 : UILayouting.GetContentOffset( parentControl ).X ) / 100f ); inputs.Add( ( parentControl == null ? 0 : UILayouting.GetContentOffset( parentControl ).Y ) / 100f ); // uiw, uih inputs.Add( uiw( control ) ); inputs.Add( uih( control ) ); // "relative" inputs.Add( relative / 100f ); // value inputs.Add( number.value ); return inputs; } static bool IsValid( string expressionText ) { if ( expressionText == null || expressionText == "" ) { return false; } var lexer = new CSharpLexer(); var list = lexer.LexToList( expressionText ); if ( lexer.hasError ) { return false; } lexer.GrabMatches( list, expressionText ); for ( int i = 0; i < list.Count; i++ ) { var token = list[ i ]; if ( token.Is( LexerMatcherLibrary.CwordMatcher ) ) { var value = token.match; if ( ! Arrays.Contains( _variables, value ) ) { // RJLog.Log( "Unknown variable:", "'" + value + "'", " in: ", "'" + expressionText + "'" ); return false; } } } return true; } } }