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 DisposableBag activeEffects; 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) .Subscribe(); } /// /// Internal configuration of the VisualElement. Called when rootVisualElement is ready. /// private void Configure(IUiElement uiElement, VisualElement visualElement) { this.visualElement = visualElement; uiElement.Configure(visualElement); Raise(UiEvent.OnCreate, cts.Token) .Subscribe() .AddTo(ref activeEffects); } /// /// Waits for the UIDocument's rootVisualElement to be ready. /// private Observable ConfigureWhenReady(IUiElement element) { return Observable.EveryValueChanged(Document, d => d.rootVisualElement) .WhereNotNull() .Take(1) .TakeUntil(cts.Token) .Do(root => Configure(element, root)) .AsUnitObservable(); } /// /// 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; // activeEffects.Dispose(); activeEffects.Clear(); 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(CancellationTokenSource cts = default) { cts ??= new CancellationTokenSource(); return Raise(UiEvent.OnDestroy, cts.Token) .Do(onCompleted: _ => { Dispose(); PooledObject.Release(); }, onErrorResume: _ => cts.Cancel()) .TakeUntil(cts.Token) .AsUnitObservable(); } /// /// Immediately disposes of effects and releases the object to the pool, /// without awaiting OnDestroy effects. /// public void DespawnNow() { Dispose(); PooledObject.Release(); } } }