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) { } } }