// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using Microsoft.MixedReality.Toolkit.Utilities; using System; using System.Collections.Generic; using UnityEngine; namespace Microsoft.MixedReality.Toolkit.UI { /// /// Base abstract class for all Theme Engines. Extend to create custom Theme logic /// public abstract class InteractableThemeBase { /// /// Types of component this Theme Engine will target on the initialized GameObject or related GameObjects /// public Type[] Types { get; protected set; } = Array.Empty(); /// /// Name of Theme Engine /// public string Name { get; protected set; } = "Base Theme"; /// /// List of Properties with values per state /// public List StateProperties { get; set; } = new List(); /// /// List of global Theme Engine properties /// public List Properties { get; set; } = new List(); /// /// GameObject initialized with this ThemeEngine and being targeted based on state changes /// public GameObject Host { get; set; } /// /// Defines how to ease between values during state changes /// public Easing Ease { get; set; } = new Easing(); /// /// True if Theme Engine has been initialized, false otherwise /// public bool Loaded { get; protected set; } = false; /// /// Indicates whether the current Theme engine implementation supports easing between state values /// public virtual bool IsEasingSupported => true; /// /// Indicates whether the current Theme engine implementation supports shader targeting on state properties /// public virtual bool AreShadersSupported => false; /// /// Instruct theme to set value for current property with given index state and at given lerp percentage /// /// property to update value /// index of state to access array of values /// percentage transition between values public abstract void SetValue(ThemeStateProperty property, int index, float percentage); /// /// Get the current property value for the provided state property /// /// state property to access /// Value currently for given state property public abstract ThemePropertyValue GetProperty(ThemeStateProperty property); /// /// Generates the default theme definition configuration for the current theme implementation /// /// Default ThemeDefinition to initialize with the current theme engine implementation public abstract ThemeDefinition GetDefaultThemeDefinition(); /// /// Instruct theme to set value for current property with ThemePropertyValue value provided /// /// property to update value /// Value for theme to set protected abstract void SetValue(ThemeStateProperty property, ThemePropertyValue value); protected Dictionary originalStateValues = new Dictionary(); private bool hasFirstState = false; private int lastState = -1; /// /// Helper method to instantiate a Theme Engine of provided type. Type must extend InteractableThemeBase /// /// Type of ThemeEngine to create /// Instance of ThemeEngine of given type public static InteractableThemeBase CreateTheme(Type themeType) { if (!themeType.IsSubclassOf(typeof(InteractableThemeBase))) { Debug.LogError($"Trying to initialize theme of type {themeType} but type does not extend {typeof(InteractableThemeBase)}"); return null; } return (InteractableThemeBase)Activator.CreateInstance(themeType); } /// /// Helper method to create and initialize a Theme Engine for given configuration and targeted GameObject /// /// Theme configuration with type information and properties to initialize ThemeEngine with /// GameObject for Theme Engine to target /// Instance of Theme Engine initialized public static InteractableThemeBase CreateAndInitTheme(ThemeDefinition definition, GameObject host = null) { var theme = CreateTheme(definition.ThemeType); theme.Init(host, definition); return theme; } /// /// Initialize current Theme Engine with given configuration and target the provided GameObject /// /// GameObject to target changes against /// Configuration information to initialize Theme Engine public virtual void Init(GameObject host, ThemeDefinition definition) { Host = host; StateProperties = new List(); foreach (ThemeStateProperty stateProp in definition.StateProperties) { // This is a temporary workaround to support backward compatible themes // If the current state properties is one we know supports shaders, try to migrate data // See ThemeStateProperty class for more details if (ThemeStateProperty.IsShaderPropertyType(stateProp.Type)) { stateProp.MigrateShaderData(); } StateProperties.Add(new ThemeStateProperty() { Name = stateProp.Name, Type = stateProp.Type, Values = stateProp.Values, Default = stateProp.Default, TargetShader = stateProp.TargetShader, ShaderPropertyName = stateProp.ShaderPropertyName, }); originalStateValues.Add(stateProp, GetProperty(stateProp)); } Properties = new List(); foreach (ThemeProperty prop in definition.CustomProperties) { Properties.Add(new ThemeProperty() { Name = prop.Name, Tooltip = prop.Tooltip, Type = prop.Type, Value = prop.Value, }); } Debug.Assert(GetDefaultThemeDefinition().StateProperties.Count == StateProperties.Count, $"{Name} state properties inconsistency with default theme definition, consider reserializing."); Debug.Assert(GetDefaultThemeDefinition().CustomProperties.Count == Properties.Count, $"{Name} custom properties inconsistency with default theme definition, consider reserializing."); if (definition.Easing != null) { Ease = definition.Easing.Copy(); Ease.Stop(); } Loaded = true; } /// /// Resets properties on Host GameObject to their original values when Init() was called for this theme engine. /// Useful for reverting changes done by this theme engine. /// public virtual void Reset() { foreach (var originalState in originalStateValues) { SetValue(originalState.Key, originalState.Value); } } /// /// Update ThemeEngine for given state based on Theme logic. Check, sets, and possibly eases values based on given state /// /// current state to target /// force update call even if state is not new public virtual void OnUpdate(int state, bool force = false) { if (state != lastState || force) { int themePropCount = StateProperties.Count; for (int i = 0; i < themePropCount; i++) { ThemeStateProperty current = StateProperties[i]; current.StartValue = GetProperty(current); if (hasFirstState || force) { Ease.Start(); SetValue(current, state, Ease.GetCurved()); hasFirstState = true; } else { SetValue(current, state, 1); if (i >= themePropCount - 1) { hasFirstState = true; } } StateProperties[i] = current; } lastState = state; } else if (Ease.Enabled && Ease.IsPlaying()) { Ease.OnUpdate(); int themePropCount = StateProperties.Count; for (int i = 0; i < themePropCount; i++) { ThemeStateProperty current = StateProperties[i]; SetValue(current, state, Ease.GetCurved()); } } lastState = state; } protected float LerpFloat(float s, float e, float t) { return (e - s) * t + s; } protected int LerpInt(int s, int e, float t) { return Mathf.RoundToInt((e - s) * t) + s; } protected ThemeProperty GetThemeProperty(int index) { if (index >= 0) { if (Properties != null && Properties.Count > index) { return Properties[index]; } // If Theme's data does not have desired property, return defaults. // If index is not in range, throw exception to fail fast. return GetDefaultThemeDefinition().CustomProperties[index]; } return null; } } }