using System; using System.Collections.Generic; using UnityEngine; using UnityEngine.UIElements; namespace Uigenx.Actions { /// One row of the compiled event table: element name, event, action and payload. public readonly struct UgxEventEntry { public readonly string ElementName; public readonly UgxUiEvent Event; public readonly string Action; public readonly string NodeId; /// Builds the payload at fire time. Null means an empty payload and no allocation. public readonly Func Payload; public UgxEventEntry(string elementName, UgxUiEvent uiEvent, string action, string nodeId = null, Func payload = null) { ElementName = elementName; Event = uiEvent; Action = action; NodeId = nodeId; Payload = payload; } } /// Wires compiled UI events onto elements and emits them through . public static class UgxActionBinder { /// Milliseconds a pointer must stay down before `long-press` fires. public const long LongPressThresholdMs = 500; public static IDisposable Bind(VisualElement element, UgxUiEvent uiEvent, string action, string screen = null, string nodeId = null, Func payload = null) { if (element == null) throw new ArgumentNullException(nameof(element)); if (string.IsNullOrEmpty(action)) throw new ArgumentException("action name is required", nameof(action)); var emitter = new Emitter(action, screen, nodeId, payload); switch (uiEvent) { case UgxUiEvent.Click: return Register(element, emitter.Fire); case UgxUiEvent.PointerDown: return Register(element, emitter.Fire); case UgxUiEvent.PointerUp: return Register(element, emitter.Fire); case UgxUiEvent.PointerEnter: return Register(element, emitter.Fire); case UgxUiEvent.PointerLeave: return Register(element, emitter.Fire); case UgxUiEvent.Focus: return Register(element, emitter.Fire); case UgxUiEvent.Blur: return Register(element, emitter.Fire); case UgxUiEvent.Submit: case UgxUiEvent.NavigationSubmit: return Register(element, emitter.Fire); case UgxUiEvent.NavigationCancel: return Register(element, emitter.Fire); case UgxUiEvent.LongPress: return BindLongPress(element, emitter); case UgxUiEvent.Change: return BindChange(element, emitter); default: throw new ArgumentOutOfRangeException(nameof(uiEvent)); } } /// /// Binds every row against elements found by name under . Rows whose /// element is missing are reported once and skipped, so one bad name cannot break a screen. /// public static UgxDisposableSet BindTable(VisualElement root, IReadOnlyList table, string screen) { if (root == null) throw new ArgumentNullException(nameof(root)); var set = new UgxDisposableSet(table?.Count ?? 0); if (table == null) return set; 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 '" + screen + "' has an event on missing element '" + entry.ElementName + "', skipped"); continue; } set.Add(Bind(element, entry.Event, entry.Action, screen, entry.NodeId, entry.Payload)); } return set; } static IDisposable Register(VisualElement element, EventCallback callback) where TEvent : EventBase, new() { element.RegisterCallback(callback); return new Registration(element, callback); } /// /// `change` is generic in Unity, so the binder covers the value types the built in controls /// produce. A control with another value type needs a hand written subscription. /// static IDisposable BindChange(VisualElement element, Emitter emitter) { var set = new UgxDisposableSet(4); set.Add(Register>(element, emitter.FireChange)); set.Add(Register>(element, emitter.FireChange)); set.Add(Register>(element, emitter.FireChange)); set.Add(Register>(element, emitter.FireChange)); return set; } static IDisposable BindLongPress(VisualElement element, Emitter emitter) { var manipulator = new LongPressManipulator(emitter); element.AddManipulator(manipulator); return new ManipulatorRegistration(element, manipulator); } /// Payload key holding the new value of a `change` event. public const string ChangeValueKey = "value"; sealed class Emitter { readonly string _action; readonly string _screen; readonly string _nodeId; readonly Func _payload; internal Emitter(string action, string screen, string nodeId, Func payload) { _action = action; _screen = screen; _nodeId = nodeId; _payload = payload; } internal void Fire(EventBase evt) => UgxActionBus.Emit(new UgxAction(_action, Build(), _screen, _nodeId)); internal void FireChange(ChangeEvent evt) => UgxActionBus.Emit(new UgxAction(_action, Build().With(ChangeValueKey, evt.newValue), _screen, _nodeId)); internal void FireDirect() => UgxActionBus.Emit(new UgxAction(_action, Build(), _screen, _nodeId)); UgxActionPayload Build() { if (_payload == null) return default; // A generated payload factory is foreign code from the runtime's point of view. try { return _payload(); } catch (Exception e) { Debug.LogError("[ugx] payload factory for action '" + _action + "' threw: " + e); return default; } } } sealed class Registration : IDisposable where TEvent : EventBase, new() { VisualElement _element; EventCallback _callback; internal Registration(VisualElement element, EventCallback callback) { _element = element; _callback = callback; } public void Dispose() { if (_callback == null) return; _element.UnregisterCallback(_callback); _element = null; _callback = null; } } sealed class ManipulatorRegistration : IDisposable { VisualElement _element; Manipulator _manipulator; internal ManipulatorRegistration(VisualElement element, Manipulator manipulator) { _element = element; _manipulator = manipulator; } public void Dispose() { if (_manipulator == null) return; _element.RemoveManipulator(_manipulator); _element = null; _manipulator = null; } } sealed class LongPressManipulator : PointerManipulator { readonly Emitter _emitter; IVisualElementScheduledItem _pending; int _pointerId = -1; internal LongPressManipulator(Emitter emitter) => _emitter = emitter; protected override void RegisterCallbacksOnTarget() { target.RegisterCallback(OnDown); target.RegisterCallback(OnCancel); target.RegisterCallback(OnCancel); target.RegisterCallback(OnCancel); target.RegisterCallback(OnCancel); } protected override void UnregisterCallbacksFromTarget() { target.UnregisterCallback(OnDown); target.UnregisterCallback(OnCancel); target.UnregisterCallback(OnCancel); target.UnregisterCallback(OnCancel); target.UnregisterCallback(OnCancel); Cancel(); _pending = null; } void OnDown(PointerDownEvent evt) { if (_pointerId >= 0) return; _pointerId = evt.pointerId; // One scheduled item for the manipulator's lifetime, re-armed on each press. if (_pending == null) _pending = target.schedule.Execute(Fire).StartingIn(LongPressThresholdMs); else _pending.ExecuteLater(LongPressThresholdMs); } void OnCancel(EventBase evt) => Cancel(); void Cancel() { _pending?.Pause(); _pointerId = -1; } void Fire() { if (_pointerId < 0) return; Cancel(); _emitter.FireDirect(); } } } }