This commit is contained in:
Rowan 2025-07-20 01:20:05 -04:00
parent 4456431f25
commit 1eb1fc220e
8 changed files with 104 additions and 83 deletions

View file

@ -1230,7 +1230,7 @@ MonoBehaviour:
root: {fileID: 1281046468} root: {fileID: 1281046468}
panelSettings: {fileID: 11400000, guid: 2bc58aab5867867e5b0feeae2df42fd0, type: 2} panelSettings: {fileID: 11400000, guid: 2bc58aab5867867e5b0feeae2df42fd0, type: 2}
despawnTimeout: despawnTimeout:
duration: 2 duration: 60
unit: 2 unit: 2
prefab: {fileID: 0} prefab: {fileID: 0}
--- !u!1 &1363717994 --- !u!1 &1363717994

View file

@ -3,7 +3,7 @@ using UnityEditor;
using UnityEditor.UIElements; using UnityEditor.UIElements;
using UnityEngine.UIElements; using UnityEngine.UIElements;
[CustomPropertyDrawer(typeof(SerializableDuration))] [CustomPropertyDrawer(typeof(Duration))]
public class DurationPropertyDrawer : PropertyDrawer public class DurationPropertyDrawer : PropertyDrawer
{ {
public override VisualElement CreatePropertyGUI(SerializedProperty property) public override VisualElement CreatePropertyGUI(SerializedProperty property)
@ -12,7 +12,7 @@ public class DurationPropertyDrawer : PropertyDrawer
container.style.flexDirection = FlexDirection.Row; container.style.flexDirection = FlexDirection.Row;
var duration = new PropertyField( var duration = new PropertyField(
property.FindPropertyRelative("duration"), property.FindPropertyRelative("displayValue"),
ObjectNames.NicifyVariableName(property.name) ObjectNames.NicifyVariableName(property.name)
); );

View file

@ -1,4 +1,6 @@
using System; using System;
using NUnit.Framework.Constraints;
using UnityEngine;
using UnityEngine.UIElements; using UnityEngine.UIElements;
namespace KitsuneCafe.System namespace KitsuneCafe.System
@ -13,24 +15,34 @@ namespace KitsuneCafe.System
Days Days
} }
public readonly struct Duration : IComparable, IComparable<Duration>, IComparable<TimeSpan>, IEquatable<Duration>, IEquatable<TimeSpan> [Serializable]
public struct Duration : IComparable, IComparable<Duration>, IComparable<TimeSpan>, IEquatable<Duration>, IEquatable<TimeSpan>, ISerializationCallbackReceiver
{ {
public readonly long Value; [SerializeField]
private readonly TimeUnit unit; private double displayValue;
[SerializeField, HideInInspector]
private long value;
[SerializeField]
private TimeUnit unit;
public Duration(long ticks, TimeUnit unit) public Duration(long ticks, TimeUnit unit)
{ {
Value = ticks; displayValue = ToDisplayValue(ticks, unit);
value = ticks;
this.unit = unit; this.unit = unit;
} }
public Duration(long ticks) : this(ticks, TimeUnit.Ticks) { } 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 TimeSpan(Duration duration) => duration.AsTimeSpan();
public static implicit operator TimeValue(Duration duration) => duration.AsTimeValue(); 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 TimeSpan AsTimeSpan() => new(value);
public readonly TimeValue AsTimeValue() => new(Into(TimeUnit.Seconds)); public readonly TimeValue AsTimeValue() => new((float)Into(TimeUnit.Seconds));
public static Duration From(long value, TimeUnit unit) public static Duration From(long value, TimeUnit unit)
{ {
@ -64,74 +76,107 @@ namespace KitsuneCafe.System
return new Duration(Convert.ToInt64(ticks), unit); return new Duration(Convert.ToInt64(ticks), unit);
} }
public long Into(TimeUnit unit) public static double ToDisplayValue(long ticks, TimeUnit unit)
{ {
return unit switch return unit switch
{ {
TimeUnit.Ticks => Value, TimeUnit.Ticks => ticks,
TimeUnit.Milliseconds => Value / TimeSpan.TicksPerMillisecond, TimeUnit.Milliseconds => ticks / TimeSpan.TicksPerMillisecond,
TimeUnit.Seconds => Value / TimeSpan.TicksPerSecond, TimeUnit.Seconds => ticks / TimeSpan.TicksPerSecond,
TimeUnit.Minutes => Value / TimeSpan.TicksPerMinute, TimeUnit.Minutes => ticks / TimeSpan.TicksPerMinute,
TimeUnit.Hours => Value / TimeSpan.TicksPerHour, TimeUnit.Hours => ticks / TimeSpan.TicksPerHour,
TimeUnit.Days => Value / TimeSpan.TicksPerHour, TimeUnit.Days => ticks / TimeSpan.TicksPerHour,
var x => throw new ArgumentException($"{x} is not a valid TimeUnit.") 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 return obj switch
{ {
Duration d => CompareTo(d), Duration d => CompareTo(d),
TimeSpan ts => CompareTo(ts), 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) 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) 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); return Compare(this, other);
} }
public int CompareTo(TimeSpan other) public readonly int CompareTo(TimeSpan other)
{ {
return TimeSpan.Compare(this, other); return TimeSpan.Compare(this, other);
} }
public bool Equals(Duration other) public readonly bool Equals(Duration other)
{ {
return Equals(this, other); return Equals(this, other);
} }
public bool Equals(TimeSpan other) public readonly bool Equals(TimeSpan other)
{ {
return other.Equals(this); 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)) return (obj is Duration d && Equals(this, d))
|| (obj is TimeSpan ts && Equals(this, ts)); || (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);
} }
} }
} }

View file

@ -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);
}
}

View file

@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 490bd35fe5e91e4489990185d89762ec

View file

@ -6,6 +6,7 @@ using UnityEngine;
using UnityEngine.UIElements; using UnityEngine.UIElements;
using KitsuneCafe.Extension; using KitsuneCafe.Extension;
using Unit = R3.Unit; using Unit = R3.Unit;
using System;
namespace KitsuneCafe.UI namespace KitsuneCafe.UI
{ {
@ -19,13 +20,13 @@ namespace KitsuneCafe.UI
private string propertyName = "opacity"; private string propertyName = "opacity";
[SerializeField, ShowIf("addTransition")] [SerializeField, ShowIf("addTransition")]
private SerializableDuration duration; private Duration duration;
[SerializeField, ShowIf("addTransition")] [SerializeField, ShowIf("addTransition")]
private EasingMode easing; private EasingMode easing;
[SerializeField, ShowIf("addTransition")] [SerializeField, ShowIf("addTransition")]
private SerializableDuration delay; private Duration delay;
[SerializeField, Range(0f, 1f)] [SerializeField, Range(0f, 1f)]
private float from; private float from;
@ -35,7 +36,7 @@ namespace KitsuneCafe.UI
public override IUiEffect Instantiate() public override IUiEffect Instantiate()
{ {
var fade = new FadeEffectInstance(from, to); var fade = new FadeEffectInstance(from, to, duration + delay);
if (addTransition) if (addTransition)
{ {
@ -51,10 +52,13 @@ namespace KitsuneCafe.UI
public readonly float From; public readonly float From;
public readonly float To; public readonly float To;
public FadeEffectInstance(float from, float to) public readonly TimeSpan Timeout;
public FadeEffectInstance(float from, float to, TimeSpan timeout)
{ {
From = from; From = from;
To = to; To = to;
Timeout = timeout;
} }
private Observable<Unit> ObserveGeometryChange(VisualElement target, CancellationToken token) private Observable<Unit> ObserveGeometryChange(VisualElement target, CancellationToken token)
@ -74,18 +78,25 @@ namespace KitsuneCafe.UI
.Take(1); .Take(1);
} }
private bool IsOpacityEvent(TransitionEndEvent evt)
{
return evt.stylePropertyNames.Contains("opacity");
}
public Observable<Unit> Execute(VisualElement target, CancellationToken token) public Observable<Unit> Execute(VisualElement target, CancellationToken token)
{ {
target.style.opacity = From; target.style.opacity = From;
var to = To; var to = To;
return Defer(target, token) return Defer(target, token)
.Do(_ => target.style.opacity = to) .Do(_ => target.style.opacity = to)
.Select(_ => target.ObserveEvent<TransitionEndEvent>()) .Select(_ => target.ObserveEvent<TransitionEndEvent>(token))
.Switch() .Switch()
.Where(evt => evt.stylePropertyNames.Contains("opacity")) .Where(IsOpacityEvent)
.Take(1) .Take(1)
.TakeUntil(token) .TakeUntil(token)
.AsUnitObservable(); .AsUnitObservable()
.Race(Observable.Timer(Timeout));
} }
} }
} }

View file

@ -18,7 +18,6 @@ namespace KitsuneCafe.UI
private IUiElement uiElement; private IUiElement uiElement;
private VisualElement visualElement; private VisualElement visualElement;
private DisposableBag activeEffects;
private IDisposable instance; private IDisposable instance;
private CancellationTokenSource cts; private CancellationTokenSource cts;
@ -41,21 +40,20 @@ namespace KitsuneCafe.UI
Document.panelSettings = settings; Document.panelSettings = settings;
instance = ConfigureWhenReady(element) instance = ConfigureWhenReady(element)
.TakeUntil(cts.Token)
.Subscribe(); .Subscribe();
} }
/// <summary> /// <summary>
/// Internal configuration of the VisualElement. Called when rootVisualElement is ready. /// Internal configuration of the VisualElement. Called when rootVisualElement is ready.
/// </summary> /// </summary>
private void Configure(IUiElement uiElement, VisualElement visualElement) private Observable<Unit> Configure(IUiElement uiElement, VisualElement visualElement)
{ {
this.visualElement = visualElement; this.visualElement = visualElement;
uiElement.Configure(visualElement); uiElement.Configure(visualElement);
Raise(UiEvent.OnCreate, cts.Token) return Raise(UiEvent.OnCreate, cts.Token);
.Subscribe()
.AddTo(ref activeEffects);
} }
/// <summary> /// <summary>
@ -66,9 +64,7 @@ namespace KitsuneCafe.UI
return Observable.EveryValueChanged(Document, d => d.rootVisualElement) return Observable.EveryValueChanged(Document, d => d.rootVisualElement)
.WhereNotNull() .WhereNotNull()
.Take(1) .Take(1)
.TakeUntil(cts.Token) .SelectMany(root => Configure(element, root));
.Do(root => Configure(element, root))
.AsUnitObservable();
} }
/// <summary> /// <summary>
@ -102,9 +98,6 @@ namespace KitsuneCafe.UI
cts?.Dispose(); cts?.Dispose();
cts = null; cts = null;
// activeEffects.Dispose();
activeEffects.Clear();
instance?.Dispose(); instance?.Dispose();
uiElement = default; uiElement = default;
@ -115,18 +108,13 @@ namespace KitsuneCafe.UI
/// Initiates the despawn process: runs OnDestroy effects, waits for them to finish, /// Initiates the despawn process: runs OnDestroy effects, waits for them to finish,
/// then disposes everything and releases the object back to the pool. /// then disposes everything and releases the object back to the pool.
/// </summary> /// </summary>
public Observable<Unit> Despawn(CancellationTokenSource cts = default) public Observable<Unit> Despawn(CancellationToken token = default)
{ {
cts ??= new CancellationTokenSource(); cts.Cancel();
return Raise(UiEvent.OnDestroy, token)
return Raise(UiEvent.OnDestroy, cts.Token) .Do(onDispose: () => Debug.Log("Disposing OnDestroy"))
.Do(onCompleted: _ => .Do(onCompleted: _ => DespawnNow(),
{ onErrorResume: _ => DespawnNow())
Dispose();
PooledObject.Release();
},
onErrorResume: _ => cts.Cancel())
.TakeUntil(cts.Token)
.AsUnitObservable(); .AsUnitObservable();
} }

View file

@ -14,7 +14,7 @@ namespace KitsuneCafe.UI
[SerializeField] private Transform root; [SerializeField] private Transform root;
[SerializeField] private PanelSettings panelSettings; [SerializeField] private PanelSettings panelSettings;
[SerializeField] private SerializableDuration despawnTimeout; [SerializeField] private Duration globalTimeout;
[SerializeField] private GameObject prefab; [SerializeField] private GameObject prefab;
private readonly Dictionary<ElementId, GameObject> activeElements = new(); private readonly Dictionary<ElementId, GameObject> activeElements = new();
@ -141,7 +141,7 @@ namespace KitsuneCafe.UI
if (obj.TryGetComponent<UiElementInstance>(out var instance)) if (obj.TryGetComponent<UiElementInstance>(out var instance))
{ {
instance.Despawn() instance.Despawn()
.Race(Observable.Timer(despawnTimeout).Do(_ => instance.DespawnNow())) .Race(Observable.Timer(globalTimeout).Do(_ => instance.DespawnNow()))
.Do(onCompleted: _ => activeElements.Remove(id)) .Do(onCompleted: _ => activeElements.Remove(id))
.Subscribe() .Subscribe()
.AddTo(obj); .AddTo(obj);