added documentation, fixed some bugs

This commit is contained in:
Sebastian Bularca
2026-04-06 17:30:09 +02:00
parent 333539eb0e
commit f42885830a
67 changed files with 1963 additions and 151 deletions

View File

@@ -2,33 +2,37 @@ using System;
using UnityEngine;
namespace Jovian.PopupSystem {
/// <summary>
/// Default popup animator that fades CanvasGroup alpha. Each category gets its own instance
/// so concurrent show/hide animations don't corrupt each other.
/// </summary>
public sealed class FadePopupAnimator : IPopupAnimator {
private CanvasGroup target;
private float duration;
private float timer;
private float elapsed;
private float startAlpha;
private float endAlpha;
private Action onComplete;
private Action onFinish;
public bool IsAnimating => target != null;
public void Show(CanvasGroup canvasGroup, float duration, Action onComplete) {
target = canvasGroup;
this.duration = Mathf.Max(duration, 0.001f);
timer = Mathf.Max(duration, 0.001f);
elapsed = 0f;
startAlpha = 0f;
endAlpha = 1f;
this.onComplete = onComplete;
onFinish = onComplete;
canvasGroup.alpha = 0f;
}
public void Hide(CanvasGroup canvasGroup, float duration, Action onComplete) {
target = canvasGroup;
this.duration = Mathf.Max(duration, 0.001f);
timer = Mathf.Max(duration, 0.001f);
elapsed = 0f;
startAlpha = canvasGroup.alpha;
endAlpha = 0f;
this.onComplete = onComplete;
onFinish = onComplete;
}
public void Tick(float deltaTime) {
@@ -37,15 +41,16 @@ namespace Jovian.PopupSystem {
}
elapsed += deltaTime;
var t = Mathf.Clamp01(elapsed / duration);
var t = Mathf.Clamp01(elapsed / timer);
target.alpha = Mathf.Lerp(startAlpha, endAlpha, t);
if(t >= 1f) {
var callback = onComplete;
target = null;
onComplete = null;
callback?.Invoke();
if(!(t >= 1f)) {
return;
}
var callback = onFinish;
target = null;
onFinish = null;
callback?.Invoke();
}
}
}

View File

@@ -2,10 +2,36 @@ using System;
using UnityEngine;
namespace Jovian.PopupSystem {
/// <summary>
/// Interface for popup show/hide animations. Each registered category gets its own instance
/// to prevent state corruption during concurrent animations. Driven by float timers in
/// <see cref="Tick"/>, not coroutines.
/// </summary>
public interface IPopupAnimator {
/// <summary>
/// Begins a show animation on the given CanvasGroup. Typically fades alpha from 0 to 1.
/// </summary>
/// <param name="canvasGroup">The CanvasGroup to animate.</param>
/// <param name="duration">Animation duration in seconds.</param>
/// <param name="onComplete">Callback invoked when the animation finishes. May be null.</param>
void Show(CanvasGroup canvasGroup, float duration, Action onComplete);
/// <summary>
/// Begins a hide animation on the given CanvasGroup. Typically fades alpha to 0.
/// </summary>
/// <param name="canvasGroup">The CanvasGroup to animate.</param>
/// <param name="duration">Animation duration in seconds.</param>
/// <param name="onComplete">Callback invoked when the animation finishes. May be null.</param>
void Hide(CanvasGroup canvasGroup, float duration, Action onComplete);
/// <summary>
/// Advances the animation by deltaTime. Call every frame from <see cref="IPopupSystem.Tick"/>.
/// </summary>
void Tick(float deltaTime);
/// <summary>
/// True if an animation is currently in progress.
/// </summary>
bool IsAnimating { get; }
}
}

View File

@@ -1,18 +1,79 @@
using System;
using System.Collections.Generic;
using Jovian.PopupSystem.UI;
using UnityEngine;
namespace Jovian.PopupSystem {
/// <summary>
/// Core interface for the popup system. Created per game state, not as a singleton.
/// Manages category registration, trigger discovery, popup display, and lifecycle.
/// </summary>
public interface IPopupSystem {
void InitializeTriggersInChildren(Transform parent, Action<PopupTrigger> configureTrigger);
/// <summary>
/// Scans all <see cref="PopupTrigger"/> components under the given parent and binds them
/// to this system. Each trigger receives a <see cref="PopupTriggerView"/> for behavior.
/// </summary>
void ScanTriggers(Transform parent);
/// <summary>
/// Scans all <see cref="PopupTrigger"/> components under the given parent, binds them,
/// and invokes the configure callback with both the trigger (for hierarchy queries) and
/// its view (for setting content).
/// </summary>
void InitializeTriggersInChildren(Transform parent, Action<PopupTrigger, PopupTriggerView> configureTrigger);
/// <summary>
/// Returns the <see cref="PopupTriggerView"/> for the first registered trigger whose
/// GameObject name matches. Returns null if not found.
/// </summary>
PopupTriggerView GetTriggerHandler(string gameObjectName);
/// <summary>
/// Returns all <see cref="PopupTriggerView"/> instances registered under the given category.
/// </summary>
IReadOnlyList<PopupTriggerView> GetTriggerHandlers(PopupCategory category);
/// <summary>
/// Registers a popup category. Each category gets its own <see cref="PopupReference"/> instance
/// (lazily created on first show) and its own <see cref="IPopupAnimator"/>.
/// </summary>
/// <param name="category">The category to register.</param>
/// <param name="priority">Fallback priority if not defined in <see cref="PopupSettings"/>. Higher dismisses lower.</param>
void RegisterCategory(PopupCategory category, int priority = 0);
/// <summary>
/// Shows a popup for the given category after the configured delay. The build callback
/// populates content via <see cref="PopupContentBuilder"/>. Optionally anchors to a
/// RectTransform or follows the mouse if no anchor is provided.
/// </summary>
void Show(PopupCategory category, Action<PopupContentBuilder> buildContent,
RectTransform anchor = null, AnchorSide? anchorSide = null);
/// <summary>
/// Shows a popup for the given category at a fixed screen position.
/// </summary>
void ShowAtPosition(PopupCategory category, Action<PopupContentBuilder> buildContent,
Vector2 screenPosition);
/// <summary>
/// Hides the popup for the given category with a fade-out animation.
/// </summary>
void Hide(PopupCategory category);
/// <summary>
/// Hides all visible popups across all categories.
/// </summary>
void HideAll();
/// <summary>
/// Drives delay timers, animations, and follow-mouse positioning. Call every frame.
/// </summary>
void Tick(float deltaTime);
/// <summary>
/// Destroys all popup view GameObjects and clears registered categories.
/// Call when the owning game state exits.
/// </summary>
void Dispose();
}
}

View File

@@ -2,6 +2,10 @@ using System;
using UnityEngine;
namespace Jovian.PopupSystem {
/// <summary>
/// Value type identifying a popup channel. Each category gets its own popup view instance.
/// Compared by string ID using ordinal comparison. Define custom categories as static fields.
/// </summary>
[Serializable]
public struct PopupCategory : IEquatable<PopupCategory> {
[SerializeField] string id;

View File

@@ -2,25 +2,42 @@ using Jovian.PopupSystem.UI;
using UnityEngine;
namespace Jovian.PopupSystem {
/// <summary>
/// Fluent API struct for building popup content. Operates directly on cached elements
/// inside a <see cref="PopupReference"/> — no intermediate allocations. Received in
/// the build callback passed to <see cref="IPopupSystem.Show"/>.
/// </summary>
public struct PopupContentBuilder {
readonly PopupReference view;
/// <summary>
/// Creates a builder targeting the given popup reference.
/// </summary>
public PopupContentBuilder(PopupReference view) {
this.view = view;
}
/// <summary>
/// Adds a header text element (bold, larger font).
/// </summary>
public PopupContentBuilder AddHeader(string text) {
var header = view.GetHeader();
header.text = text;
return this;
}
/// <summary>
/// Adds a body text element.
/// </summary>
public PopupContentBuilder AddText(string text) {
var label = view.GetText();
label.text = text;
return this;
}
/// <summary>
/// Adds a colored body text element. Hex color can be with or without the # prefix.
/// </summary>
public PopupContentBuilder AddText(string text, string hexColor) {
var label = view.GetText();
var prefix = hexColor.Length > 0 && hexColor[0] == '#' ? "" : "#";
@@ -28,6 +45,9 @@ namespace Jovian.PopupSystem {
return this;
}
/// <summary>
/// Adds a stat row with a label and integer value.
/// </summary>
public PopupContentBuilder AddStat(string label, int value) {
var (labelText, valueText) = view.GetStat();
labelText.text = label;
@@ -35,6 +55,9 @@ namespace Jovian.PopupSystem {
return this;
}
/// <summary>
/// Adds a stat row with a label and string value.
/// </summary>
public PopupContentBuilder AddStat(string label, string value) {
var (labelText, valueText) = view.GetStat();
labelText.text = label;
@@ -42,6 +65,9 @@ namespace Jovian.PopupSystem {
return this;
}
/// <summary>
/// Adds an image element with the given sprite and optional height.
/// </summary>
public PopupContentBuilder AddImage(Sprite sprite, float height = 64f) {
var image = view.GetImage();
image.sprite = sprite;
@@ -50,6 +76,9 @@ namespace Jovian.PopupSystem {
return this;
}
/// <summary>
/// Adds a horizontal separator line.
/// </summary>
public PopupContentBuilder AddSeparator() {
view.GetSeparator();
return this;

View File

@@ -3,6 +3,10 @@ using System.Collections.Generic;
using UnityEngine;
namespace Jovian.PopupSystem {
/// <summary>
/// ScriptableObject holding all popup system configuration. Create via
/// Assets > Create > Jovian > Popup System > Popup Settings.
/// </summary>
[CreateAssetMenu(fileName = "PopupSettings", menuName = "Jovian/Popup System/Popup Settings")]
public class PopupSettings : ScriptableObject {
[Header("General")]
@@ -26,6 +30,9 @@ namespace Jovian.PopupSystem {
[Header("Per-Category Overrides")]
public List<CategoryDelay> categoryDelayOverrides = new();
/// <summary>
/// Returns the configured priority for a category, or 0 if not configured.
/// </summary>
public int GetPriority(PopupCategory category) {
foreach(var cp in categoryPriorities) {
if(cp.category == category) {
@@ -35,6 +42,9 @@ namespace Jovian.PopupSystem {
return 0;
}
/// <summary>
/// Returns the configured delay override for a category, or the default popupDelay.
/// </summary>
public float GetDelay(PopupCategory category) {
foreach(var cd in categoryDelayOverrides) {
if(cd.category == category) {

View File

@@ -2,23 +2,44 @@ using System;
using System.Collections.Generic;
using Jovian.PopupSystem.UI;
using UnityEngine;
using UnityEngine.UI;
using Object = UnityEngine.Object;
namespace Jovian.PopupSystem {
/// <summary>
/// Core implementation of <see cref="IPopupSystem"/>. Created per game state, not as a singleton.
/// Manages category registration, trigger discovery, popup lifecycle, priority dismissal, and
/// tick-driven delay timers and animations. Pass a canvasParent to auto-scan triggers on construction.
/// </summary>
public sealed class PopupSystem : IPopupSystem {
readonly PopupSettings settings;
readonly PopupReference viewPrefab;
readonly Func<IPopupAnimator> animatorFactory;
readonly Transform canvasParent;
readonly Dictionary<PopupCategory, ViewState> categories = new();
readonly List<PopupTrigger> registeredTriggers = new();
/// <summary>
/// Creates a new popup system instance.
/// </summary>
/// <param name="settings">Configuration ScriptableObject with delays, priorities, and display settings.</param>
/// <param name="viewPrefab">The PopupReference prefab to instantiate per category.</param>
/// <param name="canvasParent">Optional parent Canvas transform. When provided, popup views are parented
/// here (inheriting CanvasScaler) and all PopupTrigger components are auto-scanned.</param>
/// <param name="animatorFactory">Optional factory for custom animators. Defaults to FadePopupAnimator.</param>
public PopupSystem(PopupSettings settings, PopupReference viewPrefab, Transform canvasParent = null, Func<IPopupAnimator> animatorFactory = null) {
this.settings = settings;
this.viewPrefab = viewPrefab;
this.canvasParent = canvasParent;
this.animatorFactory = animatorFactory ?? (() => new FadePopupAnimator());
// Auto-scan if a parent was provided
if(canvasParent != null) {
ScanTriggers(canvasParent);
}
}
/// <inheritdoc />
public void RegisterCategory(PopupCategory category, int priority = 0) {
if(categories.ContainsKey(category)) {
return;
@@ -34,6 +55,7 @@ namespace Jovian.PopupSystem {
};
}
/// <inheritdoc />
public void Show(PopupCategory category, Action<PopupContentBuilder> buildContent,
RectTransform anchor = null, AnchorSide? anchorSide = null) {
if(!categories.TryGetValue(category, out var state)) {
@@ -50,6 +72,7 @@ namespace Jovian.PopupSystem {
state.isPending = true;
}
/// <inheritdoc />
public void ShowAtPosition(PopupCategory category, Action<PopupContentBuilder> buildContent,
Vector2 screenPosition) {
if(!categories.TryGetValue(category, out var state)) {
@@ -65,6 +88,7 @@ namespace Jovian.PopupSystem {
state.isPending = true;
}
/// <inheritdoc />
public void Hide(PopupCategory category) {
if(!categories.TryGetValue(category, out var state)) {
return;
@@ -78,12 +102,14 @@ namespace Jovian.PopupSystem {
}
}
/// <inheritdoc />
public void HideAll() {
foreach(var kvp in categories) {
Hide(kvp.Key);
}
}
/// <inheritdoc />
public void Tick(float deltaTime) {
foreach(var kvp in categories) {
kvp.Value.animator.Tick(deltaTime);
@@ -112,13 +138,55 @@ namespace Jovian.PopupSystem {
}
}
public void InitializeTriggersInChildren(Transform parent, Action<PopupTrigger> configureTrigger) {
/// <inheritdoc />
public void ScanTriggers(Transform parent) {
var triggers = parent.GetComponentsInChildren<PopupTrigger>(true);
foreach(var trigger in triggers) {
configureTrigger(trigger);
if(!registeredTriggers.Contains(trigger)) {
BindTrigger(trigger);
}
}
}
/// <inheritdoc />
public PopupTriggerView GetTriggerHandler(string gameObjectName) {
foreach(var trigger in registeredTriggers) {
if(trigger != null && trigger.gameObject.name == gameObjectName) {
return trigger.Handler;
}
}
return null;
}
/// <inheritdoc />
public IReadOnlyList<PopupTriggerView> GetTriggerHandlers(PopupCategory category) {
var result = new List<PopupTriggerView>();
foreach(var trigger in registeredTriggers) {
if(trigger != null && trigger.Category == category && trigger.Handler != null) {
result.Add(trigger.Handler);
}
}
return result;
}
/// <inheritdoc />
public void InitializeTriggersInChildren(Transform parent, Action<PopupTrigger, PopupTriggerView> configureTrigger) {
var triggers = parent.GetComponentsInChildren<PopupTrigger>(true);
foreach(var trigger in triggers) {
if(!registeredTriggers.Contains(trigger)) {
BindTrigger(trigger);
}
configureTrigger(trigger, trigger.Handler);
}
}
private void BindTrigger(PopupTrigger trigger) {
var handler = new PopupTriggerView(this);
trigger.Bind(handler);
registeredTriggers.Add(trigger);
}
/// <inheritdoc />
public void Dispose() {
foreach(var kvp in categories) {
if(kvp.Value.view != null) {
@@ -135,6 +203,15 @@ namespace Jovian.PopupSystem {
var builder = new PopupContentBuilder(state.view);
state.pendingBuild?.Invoke(builder);
// Activate before layout rebuild so Unity has an active hierarchy to calculate
state.view.CanvasGroup.alpha = 0f;
state.view.SetVisible(true);
// Force full layout rebuild so positioning has correct size on first show
Canvas.ForceUpdateCanvases();
LayoutRebuilder.ForceRebuildLayoutImmediate(state.view.Content);
LayoutRebuilder.ForceRebuildLayoutImmediate((RectTransform)state.view.transform);
if(state.pendingScreenPos.HasValue) {
state.view.SetFixedPosition(state.pendingScreenPos.Value, settings.screenEdgePadding);
state.isFollowMouse = false;
@@ -148,7 +225,6 @@ namespace Jovian.PopupSystem {
state.isFollowMouse = true;
}
state.view.SetVisible(true);
state.view.UpdatePosition();
state.animator.Show(state.view.CanvasGroup, settings.fadeDuration, null);
}

View File

@@ -0,0 +1,54 @@
using System;
using Jovian.PopupSystem.UI;
using UnityEngine;
namespace Jovian.PopupSystem {
/// <summary>
/// Handles popup behavior for a <see cref="PopupTrigger"/>. Holds the content callback and
/// forwards pointer events to the <see cref="IPopupSystem"/>. This is the behavior layer;
/// the MonoBehaviour trigger is the reference holder that forwards events here.
/// </summary>
public sealed class PopupTriggerView {
readonly IPopupSystem popupSystem;
Action<PopupContentBuilder> contentCallback;
/// <summary>
/// Creates a new trigger view bound to the given popup system.
/// </summary>
public PopupTriggerView(IPopupSystem popupSystem) {
this.popupSystem = popupSystem;
}
/// <summary>
/// Sets the content builder callback that will be invoked when the popup is shown.
/// </summary>
public void SetContent(Action<PopupContentBuilder> callback) {
contentCallback = callback;
}
/// <summary>
/// Called by <see cref="PopupTrigger"/> when the pointer enters. Reads the trigger's
/// category, anchor side, and position mode to show the popup.
/// </summary>
public void OnPointerEnter(PopupTrigger trigger) {
if(popupSystem == null || contentCallback == null) {
return;
}
if(trigger.PositionMode == PopupPositionMode.AnchorToElement) {
popupSystem.Show(trigger.Category, contentCallback, (RectTransform)trigger.transform, trigger.AnchorSide);
}
else {
popupSystem.Show(trigger.Category, contentCallback);
}
}
/// <summary>
/// Called by <see cref="PopupTrigger"/> when the pointer exits. Hides the popup
/// for the trigger's category.
/// </summary>
public void OnPointerExit(PopupTrigger trigger) {
popupSystem?.Hide(trigger.Category);
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 1e2351b6a4b8bf046923790d4d09141c

View File

@@ -5,6 +5,10 @@ using UnityEngine.InputSystem;
using UnityEngine.UI;
namespace Jovian.PopupSystem.UI {
/// <summary>
/// MonoBehaviour reference holder for a popup view. Manages the grow-only element cache,
/// screen positioning, and visibility. One instance per registered <see cref="PopupCategory"/>.
/// </summary>
public class PopupReference : MonoBehaviour {
[SerializeField] RectTransform content;
[SerializeField] CanvasGroup canvasGroup;
@@ -52,6 +56,7 @@ namespace Jovian.PopupSystem.UI {
public RectTransform Content => content;
public bool IsVisible => isVisible;
/// <summary>Constrains the popup's horizontal size to the given maximum width in pixels.</summary>
public void SetMaxWidth(float maxPopupWidth) {
maxWidth = maxPopupWidth;
if(maxWidth > 0f) {
@@ -62,6 +67,7 @@ namespace Jovian.PopupSystem.UI {
}
}
/// <summary>Shows or hides the popup GameObject. Resets alpha to 0 when hiding.</summary>
public void SetVisible(bool visible) {
isVisible = visible;
gameObject.SetActive(visible);
@@ -70,6 +76,7 @@ namespace Jovian.PopupSystem.UI {
}
}
/// <summary>Deactivates all cached content elements and marks layout as dirty.</summary>
public void ClearContent() {
DeactivateRange(headerCache, ref headerIndex);
DeactivateRange(textCache, ref textIndex);
@@ -91,14 +98,17 @@ namespace Jovian.PopupSystem.UI {
// --- Element access (grow-only) ---
/// <summary>Returns the next available header element from the cache, or creates one.</summary>
public TMP_Text GetHeader() {
return GetOrCreate(headerCache, headerPrefab, ref headerIndex);
}
/// <summary>Returns the next available text element from the cache, or creates one.</summary>
public TMP_Text GetText() {
return GetOrCreate(textCache, textPrefab, ref textIndex);
}
/// <summary>Returns the next available stat row (label + value pair) from the cache, or creates one.</summary>
public (TMP_Text label, TMP_Text value) GetStat() {
if(statIndex < statCache.Count) {
var existing = statCache[statIndex];
@@ -118,10 +128,12 @@ namespace Jovian.PopupSystem.UI {
return (entry.label, entry.value);
}
/// <summary>Returns the next available image element from the cache, or creates one.</summary>
public Image GetImage() {
return GetOrCreate(imageCache, imagePrefab, ref imageIndex);
}
/// <summary>Returns the next available separator element from the cache, or creates one.</summary>
public Image GetSeparator() {
return GetOrCreate(separatorCache, separatorPrefab, ref separatorIndex);
}
@@ -145,6 +157,7 @@ namespace Jovian.PopupSystem.UI {
// --- Positioning ---
/// <summary>Configures the popup to anchor to a target element on the specified side.</summary>
public void SetAnchorMode(RectTransform target, AnchorSide side, float edgePadding) {
positionMode = PopupPositionMode.AnchorToElement;
anchorTarget = target;
@@ -152,12 +165,14 @@ namespace Jovian.PopupSystem.UI {
screenEdgePadding = edgePadding;
}
/// <summary>Configures the popup to follow the mouse cursor with the given offset.</summary>
public void SetFollowMouseMode(Vector2 offset, float edgePadding) {
positionMode = PopupPositionMode.FollowMouse;
followOffset = offset;
screenEdgePadding = edgePadding;
}
/// <summary>Positions the popup at a fixed screen coordinate with edge clamping.</summary>
public void SetFixedPosition(Vector2 screenPos, float edgePadding) {
positionMode = PopupPositionMode.AnchorToElement;
anchorTarget = null;
@@ -165,6 +180,7 @@ namespace Jovian.PopupSystem.UI {
PositionAtScreenPoint(screenPos);
}
/// <summary>Updates the popup position based on the current mode (follow mouse or anchored).</summary>
public void UpdatePosition() {
if(positionMode == PopupPositionMode.FollowMouse) {
PositionAtScreenPoint(Mouse.current.position.ReadValue() + followOffset);

View File

@@ -1,54 +1,48 @@
using System;
using UnityEngine;
using UnityEngine.EventSystems;
namespace Jovian.PopupSystem.UI {
/// <summary>
/// Reference holder MonoBehaviour for popup triggers. Attach to any UI element with a
/// Graphic component (Image, TMP_Text, etc.) that has Raycast Target enabled.
/// Configure category, anchor side, and position mode in the Inspector.
/// Forwards pointer events to the bound <see cref="PopupTriggerView"/>.
/// </summary>
public class PopupTrigger : MonoBehaviour, IPointerEnterHandler, IPointerExitHandler {
[SerializeField] PopupCategory category;
[SerializeField] AnchorSide anchorSide = AnchorSide.Below;
[SerializeField] PopupPositionMode positionMode = PopupPositionMode.AnchorToElement;
IPopupSystem popupSystem;
Action<PopupContentBuilder> contentCallback;
bool initialized;
PopupTriggerView handler;
/// <summary>The popup category this trigger belongs to.</summary>
public PopupCategory Category => category;
public void Initialize(IPopupSystem popupSystem, Action<PopupContentBuilder> contentCallback) {
this.popupSystem = popupSystem;
this.contentCallback = contentCallback;
initialized = true;
}
public void Initialize(IPopupSystem popupSystem, PopupCategory category, Action<PopupContentBuilder> contentCallback) {
this.popupSystem = popupSystem;
this.category = category;
this.contentCallback = contentCallback;
initialized = true;
}
public void UpdateContent(Action<PopupContentBuilder> contentCallback) {
this.contentCallback = contentCallback;
/// <summary>Which side of this element the popup anchors to.</summary>
public AnchorSide AnchorSide => anchorSide;
/// <summary>Whether the popup anchors to this element or follows the mouse.</summary>
public PopupPositionMode PositionMode => positionMode;
/// <summary>The bound behavior handler. Null until <see cref="Bind"/> is called.</summary>
public PopupTriggerView Handler => handler;
/// <summary>
/// Binds a <see cref="PopupTriggerView"/> to this trigger. Called automatically
/// by <see cref="IPopupSystem.ScanTriggers"/> or <see cref="IPopupSystem.InitializeTriggersInChildren"/>.
/// </summary>
public void Bind(PopupTriggerView view) {
handler = view;
}
/// <summary>Forwards pointer enter to the bound handler.</summary>
public void OnPointerEnter(PointerEventData eventData) {
if(!initialized || popupSystem == null || contentCallback == null) {
return;
}
if(positionMode == PopupPositionMode.AnchorToElement) {
popupSystem.Show(category, contentCallback, (RectTransform)transform, anchorSide);
}
else {
popupSystem.Show(category, contentCallback);
}
handler?.OnPointerEnter(this);
}
/// <summary>Forwards pointer exit to the bound handler.</summary>
public void OnPointerExit(PointerEventData eventData) {
if(!initialized || popupSystem == null) {
return;
}
popupSystem.Hide(category);
handler?.OnPointerExit(this);
}
}
}