using System; using System.Collections.Generic; using System.Linq; using System.Threading; using KitsuneCafe.System; using R3; using UnityEngine; using UnityEngine.UIElements; using Unit = R3.Unit; namespace KitsuneCafe.UI { public class UiElementInstance : MonoBehaviour, IDisposable { public UIDocument Document; public PooledObject PooledObject; private IUiElement uiElement; private VisualElement visualElement; private IDisposable instance; private CancellationTokenSource cts; private void OnDisable() { Dispose(); } /// /// Initial configuration of the UIDocument. Called by UISceneManager on spawn/reuse. /// public void Configure(IUiElement element, PanelSettings settings) { uiElement = element; cts = new(); Document.visualTreeAsset = element.VisualTreeAsset; Document.panelSettings = settings; instance = ConfigureWhenReady(element) .TakeUntil(cts.Token) .Subscribe(); } /// /// Internal configuration of the VisualElement. Called when rootVisualElement is ready. /// private Observable Configure(IUiElement uiElement, VisualElement visualElement) { this.visualElement = visualElement; uiElement.Configure(visualElement); return Raise(UiEvent.OnCreate, cts.Token); } /// /// Waits for the UIDocument's rootVisualElement to be ready. /// private Observable ConfigureWhenReady(IUiElement element) { return Observable.EveryValueChanged(Document, d => d.rootVisualElement) .WhereNotNull() .Take(1) .SelectMany(root => Configure(element, root)); } /// /// Raises effects for a given UI event, returning an Observable that completes when all effects are done. /// private Observable Raise(UiEvent evt, CancellationToken token = default) { var effects = uiElement.GetEffectsFor(evt).ToArray(); var obs = new List>(effects.Length); foreach (var effect in effects) { if (effect != null) { obs.Add(effect .Instantiate() .Execute(visualElement, token)); } } return Observable.Merge(obs); } /// /// Forcefully disposes of all active effects and instance lifecycle subscriptions. /// This is for internal cleanup. It does NOT release the object to the pool. /// public void Dispose() { cts?.Cancel(); cts?.Dispose(); cts = null; instance?.Dispose(); uiElement = default; visualElement = default; } /// /// 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(CancellationToken token = default) { cts.Cancel(); return Raise(UiEvent.OnDestroy, token) .Do(onDispose: () => Debug.Log("Disposing OnDestroy")) .Do(onCompleted: _ => DespawnNow(), onErrorResume: _ => DespawnNow()) .AsUnitObservable(); } /// /// Immediately disposes of effects and releases the object to the pool, /// without awaiting OnDestroy effects. /// public void DespawnNow() { Dispose(); PooledObject.Release(); } } }