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();
}
}
}
}