diff --git a/Assets/Scenes/SampleScene.unity b/Assets/Scenes/SampleScene.unity index f976dc7..195c0e6 100644 --- a/Assets/Scenes/SampleScene.unity +++ b/Assets/Scenes/SampleScene.unity @@ -1230,7 +1230,7 @@ MonoBehaviour: root: {fileID: 1281046468} panelSettings: {fileID: 11400000, guid: 2bc58aab5867867e5b0feeae2df42fd0, type: 2} despawnTimeout: - duration: 2 + duration: 60 unit: 2 prefab: {fileID: 0} --- !u!1 &1363717994 diff --git a/Assets/Scripts/Editor/PropertyDrawers/DurationPropertyDrawer.cs b/Assets/Scripts/Editor/PropertyDrawers/DurationPropertyDrawer.cs index 86d10ef..094388a 100644 --- a/Assets/Scripts/Editor/PropertyDrawers/DurationPropertyDrawer.cs +++ b/Assets/Scripts/Editor/PropertyDrawers/DurationPropertyDrawer.cs @@ -3,7 +3,7 @@ using UnityEditor; using UnityEditor.UIElements; using UnityEngine.UIElements; -[CustomPropertyDrawer(typeof(SerializableDuration))] +[CustomPropertyDrawer(typeof(Duration))] public class DurationPropertyDrawer : PropertyDrawer { public override VisualElement CreatePropertyGUI(SerializedProperty property) @@ -12,7 +12,7 @@ public class DurationPropertyDrawer : PropertyDrawer container.style.flexDirection = FlexDirection.Row; var duration = new PropertyField( - property.FindPropertyRelative("duration"), + property.FindPropertyRelative("displayValue"), ObjectNames.NicifyVariableName(property.name) ); diff --git a/Assets/Scripts/System/Duration.cs b/Assets/Scripts/System/Duration.cs index ed13470..382d9ff 100644 --- a/Assets/Scripts/System/Duration.cs +++ b/Assets/Scripts/System/Duration.cs @@ -1,4 +1,6 @@ using System; +using NUnit.Framework.Constraints; +using UnityEngine; using UnityEngine.UIElements; namespace KitsuneCafe.System @@ -13,24 +15,34 @@ namespace KitsuneCafe.System Days } - public readonly struct Duration : IComparable, IComparable, IComparable, IEquatable, IEquatable + [Serializable] + public struct Duration : IComparable, IComparable, IComparable, IEquatable, IEquatable, ISerializationCallbackReceiver { - public readonly long Value; - private readonly TimeUnit unit; + [SerializeField] + private double displayValue; + + [SerializeField, HideInInspector] + private long value; + + [SerializeField] + private TimeUnit unit; public Duration(long ticks, TimeUnit unit) { - Value = ticks; + displayValue = ToDisplayValue(ticks, unit); + value = ticks; this.unit = unit; } public Duration(long ticks) : this(ticks, TimeUnit.Ticks) { } + public static implicit operator Duration(TimeSpan ts) => new(ts.Ticks, TimeUnit.Ticks); public static implicit operator TimeSpan(Duration duration) => duration.AsTimeSpan(); public static implicit operator TimeValue(Duration duration) => duration.AsTimeValue(); + public static Duration operator +(Duration lhs, Duration rhs) => lhs.AsTimeSpan() + rhs.AsTimeSpan(); - public readonly TimeSpan AsTimeSpan() => new(Value); - public readonly TimeValue AsTimeValue() => new(Into(TimeUnit.Seconds)); + public readonly TimeSpan AsTimeSpan() => new(value); + public readonly TimeValue AsTimeValue() => new((float)Into(TimeUnit.Seconds)); public static Duration From(long value, TimeUnit unit) { @@ -64,74 +76,107 @@ namespace KitsuneCafe.System return new Duration(Convert.ToInt64(ticks), unit); } - public long Into(TimeUnit unit) + public static double ToDisplayValue(long ticks, TimeUnit unit) { return unit switch { - TimeUnit.Ticks => Value, - TimeUnit.Milliseconds => Value / TimeSpan.TicksPerMillisecond, - TimeUnit.Seconds => Value / TimeSpan.TicksPerSecond, - TimeUnit.Minutes => Value / TimeSpan.TicksPerMinute, - TimeUnit.Hours => Value / TimeSpan.TicksPerHour, - TimeUnit.Days => Value / TimeSpan.TicksPerHour, + TimeUnit.Ticks => ticks, + TimeUnit.Milliseconds => ticks / TimeSpan.TicksPerMillisecond, + TimeUnit.Seconds => ticks / TimeSpan.TicksPerSecond, + TimeUnit.Minutes => ticks / TimeSpan.TicksPerMinute, + TimeUnit.Hours => ticks / TimeSpan.TicksPerHour, + TimeUnit.Days => ticks / TimeSpan.TicksPerHour, var x => throw new ArgumentException($"{x} is not a valid TimeUnit.") }; } - public int CompareTo(object obj) + public static long FromDisplayValue(double value, TimeUnit unit) + { + var ticks = unit switch + { + TimeUnit.Ticks => value, + TimeUnit.Milliseconds => TimeSpan.TicksPerMillisecond * value, + TimeUnit.Seconds => TimeSpan.TicksPerSecond * value, + TimeUnit.Minutes => TimeSpan.TicksPerMinute * value, + TimeUnit.Hours => TimeSpan.TicksPerHour * value, + TimeUnit.Days => TimeSpan.TicksPerHour * value, + var x => throw new ArgumentException($"{x} is not a valid TimeUnit.") + }; + + return Convert.ToInt64(ticks); + } + + public readonly double Into() => Into(unit); + public readonly double Into(TimeUnit unit) + { + return ToDisplayValue(value, unit); + } + + public readonly int CompareTo(object obj) { return obj switch { Duration d => CompareTo(d), TimeSpan ts => CompareTo(ts), - var value => throw new ArgumentException($"{value} is not a Duration or TimeSpan") + TimeValue tv => CompareTo(tv), + var value => throw new ArgumentException($"{value} is not comparable to Duration") }; } public static bool Equals(Duration a, Duration b) { - return a.Value == b.Value; + return a.value == b.value; } public static int Compare(Duration a, Duration b) { - return a.Value.CompareTo(b.Value); + return a.value.CompareTo(b.value); } - public int CompareTo(Duration other) + public readonly int CompareTo(Duration other) { return Compare(this, other); } - public int CompareTo(TimeSpan other) + public readonly int CompareTo(TimeSpan other) { return TimeSpan.Compare(this, other); } - public bool Equals(Duration other) + public readonly bool Equals(Duration other) { return Equals(this, other); } - public bool Equals(TimeSpan other) + public readonly bool Equals(TimeSpan other) { return other.Equals(this); } - public override bool Equals(object obj) + public override readonly bool Equals(object obj) { return (obj is Duration d && Equals(this, d)) || (obj is TimeSpan ts && Equals(this, ts)); } - public override int GetHashCode() + public override readonly int GetHashCode() { - return HashCode.Combine(Value); + return HashCode.Combine(value); } - public override string ToString() + public override readonly string ToString() { - return $"{Value} {nameof(unit)}"; + return $"{value} {nameof(unit)}"; + } + + public void OnBeforeSerialize() + { + displayValue = Into(); + } + + public void OnAfterDeserialize() + { + value = FromDisplayValue(displayValue, unit); } } } diff --git a/Assets/Scripts/System/SerializableDuration.cs b/Assets/Scripts/System/SerializableDuration.cs deleted file mode 100644 index 790fc1a..0000000 --- a/Assets/Scripts/System/SerializableDuration.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; -using UnityEngine; -using UnityEngine.UIElements; - -namespace KitsuneCafe.System -{ - [Serializable] - public class SerializableDuration - { - [SerializeField] - private float duration; - - [SerializeField] - private TimeUnit unit = TimeUnit.Seconds; - - - public static implicit operator Duration(SerializableDuration d) => Duration.From(d.duration, d.unit); - public static implicit operator TimeSpan(SerializableDuration d) => Duration.From(d.duration, d.unit); - public static implicit operator TimeValue(SerializableDuration d) => Duration.From(d.duration, d.unit); - } -} diff --git a/Assets/Scripts/System/SerializableDuration.cs.meta b/Assets/Scripts/System/SerializableDuration.cs.meta deleted file mode 100644 index 9b319aa..0000000 --- a/Assets/Scripts/System/SerializableDuration.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 490bd35fe5e91e4489990185d89762ec \ No newline at end of file diff --git a/Assets/Scripts/UI/Effect/FadeEffect.cs b/Assets/Scripts/UI/Effect/FadeEffect.cs index 270e352..59b8be9 100644 --- a/Assets/Scripts/UI/Effect/FadeEffect.cs +++ b/Assets/Scripts/UI/Effect/FadeEffect.cs @@ -6,6 +6,7 @@ using UnityEngine; using UnityEngine.UIElements; using KitsuneCafe.Extension; using Unit = R3.Unit; +using System; namespace KitsuneCafe.UI { @@ -19,13 +20,13 @@ namespace KitsuneCafe.UI private string propertyName = "opacity"; [SerializeField, ShowIf("addTransition")] - private SerializableDuration duration; + private Duration duration; [SerializeField, ShowIf("addTransition")] private EasingMode easing; [SerializeField, ShowIf("addTransition")] - private SerializableDuration delay; + private Duration delay; [SerializeField, Range(0f, 1f)] private float from; @@ -35,7 +36,7 @@ namespace KitsuneCafe.UI public override IUiEffect Instantiate() { - var fade = new FadeEffectInstance(from, to); + var fade = new FadeEffectInstance(from, to, duration + delay); if (addTransition) { @@ -51,10 +52,13 @@ namespace KitsuneCafe.UI public readonly float From; public readonly float To; - public FadeEffectInstance(float from, float to) + public readonly TimeSpan Timeout; + + public FadeEffectInstance(float from, float to, TimeSpan timeout) { From = from; To = to; + Timeout = timeout; } private Observable ObserveGeometryChange(VisualElement target, CancellationToken token) @@ -74,18 +78,25 @@ namespace KitsuneCafe.UI .Take(1); } + private bool IsOpacityEvent(TransitionEndEvent evt) + { + return evt.stylePropertyNames.Contains("opacity"); + } + public Observable Execute(VisualElement target, CancellationToken token) { target.style.opacity = From; + var to = To; return Defer(target, token) .Do(_ => target.style.opacity = to) - .Select(_ => target.ObserveEvent()) + .Select(_ => target.ObserveEvent(token)) .Switch() - .Where(evt => evt.stylePropertyNames.Contains("opacity")) + .Where(IsOpacityEvent) .Take(1) .TakeUntil(token) - .AsUnitObservable(); + .AsUnitObservable() + .Race(Observable.Timer(Timeout)); } } } diff --git a/Assets/Scripts/UI/Orchestration/UiElementInstance.cs b/Assets/Scripts/UI/Orchestration/UiElementInstance.cs index d159821..abd8100 100644 --- a/Assets/Scripts/UI/Orchestration/UiElementInstance.cs +++ b/Assets/Scripts/UI/Orchestration/UiElementInstance.cs @@ -18,7 +18,6 @@ namespace KitsuneCafe.UI private IUiElement uiElement; private VisualElement visualElement; - private DisposableBag activeEffects; private IDisposable instance; private CancellationTokenSource cts; @@ -41,21 +40,20 @@ namespace KitsuneCafe.UI Document.panelSettings = settings; instance = ConfigureWhenReady(element) + .TakeUntil(cts.Token) .Subscribe(); } /// /// Internal configuration of the VisualElement. Called when rootVisualElement is ready. /// - private void Configure(IUiElement uiElement, VisualElement visualElement) + private Observable Configure(IUiElement uiElement, VisualElement visualElement) { this.visualElement = visualElement; uiElement.Configure(visualElement); - Raise(UiEvent.OnCreate, cts.Token) - .Subscribe() - .AddTo(ref activeEffects); + return Raise(UiEvent.OnCreate, cts.Token); } /// @@ -66,9 +64,7 @@ namespace KitsuneCafe.UI return Observable.EveryValueChanged(Document, d => d.rootVisualElement) .WhereNotNull() .Take(1) - .TakeUntil(cts.Token) - .Do(root => Configure(element, root)) - .AsUnitObservable(); + .SelectMany(root => Configure(element, root)); } /// @@ -102,9 +98,6 @@ namespace KitsuneCafe.UI cts?.Dispose(); cts = null; - // activeEffects.Dispose(); - activeEffects.Clear(); - instance?.Dispose(); uiElement = default; @@ -115,18 +108,13 @@ namespace KitsuneCafe.UI /// Initiates the despawn process: runs OnDestroy effects, waits for them to finish, /// then disposes everything and releases the object back to the pool. /// - public Observable Despawn(CancellationTokenSource cts = default) + public Observable Despawn(CancellationToken token = default) { - cts ??= new CancellationTokenSource(); - - return Raise(UiEvent.OnDestroy, cts.Token) - .Do(onCompleted: _ => - { - Dispose(); - PooledObject.Release(); - }, - onErrorResume: _ => cts.Cancel()) - .TakeUntil(cts.Token) + cts.Cancel(); + return Raise(UiEvent.OnDestroy, token) + .Do(onDispose: () => Debug.Log("Disposing OnDestroy")) + .Do(onCompleted: _ => DespawnNow(), + onErrorResume: _ => DespawnNow()) .AsUnitObservable(); } diff --git a/Assets/Scripts/UI/Orchestration/UiSceneManager.cs b/Assets/Scripts/UI/Orchestration/UiSceneManager.cs index 1c02bc9..374185e 100644 --- a/Assets/Scripts/UI/Orchestration/UiSceneManager.cs +++ b/Assets/Scripts/UI/Orchestration/UiSceneManager.cs @@ -14,7 +14,7 @@ namespace KitsuneCafe.UI [SerializeField] private Transform root; [SerializeField] private PanelSettings panelSettings; - [SerializeField] private SerializableDuration despawnTimeout; + [SerializeField] private Duration globalTimeout; [SerializeField] private GameObject prefab; private readonly Dictionary activeElements = new(); @@ -141,7 +141,7 @@ namespace KitsuneCafe.UI if (obj.TryGetComponent(out var instance)) { instance.Despawn() - .Race(Observable.Timer(despawnTimeout).Do(_ => instance.DespawnNow())) + .Race(Observable.Timer(globalTimeout).Do(_ => instance.DespawnNow())) .Do(onCompleted: _ => activeElements.Remove(id)) .Subscribe() .AddTo(obj);