canto/Assets/Scripts/UI/Orchestration/UiElementInstance.cs
2025-08-14 19:11:32 -04:00

130 lines
3.9 KiB
C#

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