using System;
using System.Collections.Generic;
using Uigenx.Actions;
using Uigenx.Animation;
using Uigenx.Binding;
using Uigenx.Effects;
using UnityEngine;
using UnityEngine.UIElements;
namespace Uigenx
{
///
/// Base class for a generated screen. The compiler emits a partial subclass that overrides
/// , ,
/// and , and forwards the four lifecycle hooks to its own
/// partial void methods. Everything runs on the main thread.
///
public abstract class UgxScreen
{
readonly UgxDisposableSet _subscriptions = new UgxDisposableSet(8);
/// Stable key the registry, the factory and the studio all use. Generated as `Key`.
public abstract string ScreenKey { get; }
public VisualElement Root { get; private set; }
public IUgxDataSource DataSource { get; private set; }
public bool IsAttached => Root != null;
public bool IsVisible { get; private set; }
public UgxAnimator Animator { get; private set; }
public UgxAnimationTriggers Triggers { get; private set; }
/// Subscriptions disposed on . Generated code adds to it.
protected UgxDisposableSet Subscriptions => _subscriptions;
protected UgxBindingSet Bindings { get; private set; }
/// Clips this screen owns. The compiler emits `protected override UgxClip[] Clips`.
protected virtual UgxClip[] Clips => UgxClip.None;
public IReadOnlyList ClipList => Clips;
public void Attach(VisualElement root, IUgxDataSource source)
{
if (root == null) throw new ArgumentNullException(nameof(root));
if (Root != null) Detach();
Root = root;
DataSource = source;
Animator = new UgxAnimator(root);
Animator.Register(Clips);
Triggers = new UgxAnimationTriggers(Animator, root);
AttachElements(root);
OnScreenCreate();
ConnectBindings(source);
OnScreenBind(source);
ConnectActions();
ConnectEffects();
}
public void Show()
{
if (Root == null) throw new InvalidOperationException("screen '" + ScreenKey + "' is not attached");
if (IsVisible) return;
IsVisible = true;
Root.style.display = DisplayStyle.Flex;
Bindings?.PushNow();
OnScreenShow();
Triggers?.FireShow();
UgxActionBus.Emit(UgxSystemActions.ScreenShown,
UgxActionPayload.Of(UgxSystemActions.ScreenKey, ScreenKey), ScreenKey);
}
///
/// Hides immediately. Hide clips start playing but the element is already hidden, so a
/// screen that needs its exit animation to finish should play the clip and hide on its
/// completion callback instead.
///
public void Hide()
{
if (Root == null || !IsVisible) return;
IsVisible = false;
Triggers?.FireHide();
OnScreenHide();
Root.style.display = DisplayStyle.None;
UgxActionBus.Emit(UgxSystemActions.ScreenHidden,
UgxActionPayload.Of(UgxSystemActions.ScreenKey, ScreenKey), ScreenKey);
}
public void Detach()
{
if (Root == null) return;
if (IsVisible) Hide();
Triggers?.Dispose();
Triggers = null;
Animator?.Dispose();
Animator = null;
Bindings?.Dispose();
Bindings = null;
_subscriptions.Dispose();
var root = Root;
Root = null;
DataSource = null;
OnScreenDetach(root);
}
public void Emit(string action, UgxActionPayload payload = default)
=> UgxActionBus.Emit(new UgxAction(action, payload, ScreenKey));
/// Resolves a named element, throwing a message that names the screen when it is missing.
protected T Q(string name) where T : VisualElement
{
var element = Root?.Q(name);
if (element == null) throw new UgxElementNotFoundException(ScreenKey, name, typeof(T));
return element;
}
protected T QOptional(string name) where T : VisualElement => Root?.Q(name);
protected void ApplyBindings(IReadOnlyList table, IUgxDataSource source)
{
Bindings?.Dispose();
Bindings = UgxBindingConnector.Apply(Root, table, source, ScreenKey);
}
protected void BindActions(IReadOnlyList table)
=> _subscriptions.Add(UgxActionBinder.BindTable(Root, table, ScreenKey));
protected void InstallTriggers(IReadOnlyList triggers)
=> Triggers?.Install(triggers, DataSource);
///
/// Drives data bound material parameters. The material itself comes from USS
/// `-unity-material`; each binder adopts what the element resolved.
///
protected void ApplyMaterials(IReadOnlyList table)
{
if (table == null || Root == null) return;
for (int i = 0; i < table.Count; i++)
{
var entry = table[i];
var element = string.IsNullOrEmpty(entry.ElementName)
? Root
: Root.Q(entry.ElementName);
if (element == null)
{
Debug.LogWarning("[ugx] screen '" + ScreenKey + "' binds material parameters on missing " +
"element '" + entry.ElementName + "', skipped");
continue;
}
var binder = UgxMaterialBinder.Adopt(element);
for (int p = 0; p < entry.Params.Length; p++)
{
var parameter = entry.Params[p];
binder.Bind(parameter.Name, parameter.Path, parameter.Kind, DataSource);
}
_subscriptions.Add(binder);
}
}
/// Resolves the typed element properties. Generated.
protected virtual void AttachElements(VisualElement root)
{
}
/// Applies the compiled binding table. Generated.
protected virtual void ConnectBindings(IUgxDataSource source)
{
}
/// Applies the compiled event table. Generated.
protected virtual void ConnectActions()
{
}
/// Applies materials, filters and animation triggers. Generated.
protected virtual void ConnectEffects()
{
}
/// Generated code forwards this to its `partial void OnCreate()` hook.
protected virtual void OnScreenCreate()
{
}
/// Generated code forwards this to its `partial void OnBind(IUgxDataSource)` hook.
protected virtual void OnScreenBind(IUgxDataSource source)
{
}
/// Generated code forwards this to its `partial void OnShow()` hook.
protected virtual void OnScreenShow()
{
}
/// Generated code forwards this to its `partial void OnHide()` hook.
protected virtual void OnScreenHide()
{
}
/// Last chance to release anything the screen owns outside .
protected virtual void OnScreenDetach(VisualElement root)
{
}
}
}