Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. Dismiss Notice

How does UI event bubbles?

Discussion in 'Editor & General Support' started by bitinn, Jan 25, 2018.

  1. bitinn

    bitinn

    Joined:
    Aug 20, 2016
    Posts:
    958
    Hi all,

    I know Unity bubbles UI events, but I have a hard time trying to write a MonoBehaviour script that can capture events from child elements.

    Say I have a Canvas object with a few child Buttons (with empty OnClick event), can I use event bubbling to listen to ALL of the buttons, on the Canvas object, and differentiate them using rawPointerPress?

    I wrote a simple script that extends IPointerClickHandler, but it seems I have to attach the script to the actual button in order to receive OnPointerClick, why?

    (If you are a frontend engineer, you can think of what I am trying to do as "event delegation": not C#'s delegate, but JavaScript HTML DOM's "parent node will receive clicks from child node".)

    Thx in advance!
     
    Last edited: Jan 25, 2018
    phobos2077 likes this.
  2. bitinn

    bitinn

    Joined:
    Aug 20, 2016
    Posts:
    958
    OK, it looks like the Button script that comes with the default Button UI is a type of event trigger? It stops event bubbling even when OnClick is empty (as a handler is found, event stops bubbling).

    I tried it with an Image and it indeed bubbles as expected.

    So I guess I can't use Button?
     
  3. bitinn

    bitinn

    Joined:
    Aug 20, 2016
    Posts:
    958
    I ended doing what most people do, attach a script onto each button so that it bubbles event (still better than plain onclick solution as it decouples event handlers as well as passing a useful PointerEventData to them)

    Code (CSharp):
    1.         public void OnPointerClick (PointerEventData ev) {
    2.             // propagate event further up
    3.             ExecuteEvents.ExecuteHierarchy(transform.parent.gameObject, ev, ExecuteEvents.pointerClickHandler);
    4.         }
     
  4. _geo__

    _geo__

    Joined:
    Feb 26, 2014
    Posts:
    1,112
    I know it's an old post but just for completeness.
    A Button does not stop all Events from bubbling up. It seems to only affect OnPointerDown, OnPointerUp and OnPointerClick (tested in Unity 2019).

    If you look at the definitions of Button and Selectable you will find:
    Code (CSharp):
    1. public class Button : Selectable, IPointerClickHandler, IEventSystemHandler, ISubmitHandler
    2. public class Selectable : UIBehaviour, IMoveHandler, IEventSystemHandler, IPointerDownHandler, IPointerUpHandler, IPointerEnterHandler, IPointerExitHandler, ISelectHandler, IDeselectHandler
    This may lead you to believe that all these events are caugth by the Button and will therefore not bubble up, but that's not the case.

    Here is a Component which implements @bitinn s suggestion. You can choose if it should do nothing (DoNotBubble), bubble only if there is no EventTrigger on the Button (BubbleWithoutTrigger) or bubble every time (BubbleAlways).
    A demo project with a demo scene can be found on gitlab (code is the same as here): https://gitlab.com/kamgam/unity3d-button-event-bubbling.

    Hope it's useful :)

    Code (CSharp):
    1.  
    2. /// <summary>
    3. /// Controls the event bubbling behaviour of UnityEngine.UI.Button class.
    4. /// Sets PointerEventData.pointerPress so it may be used in OnPointerUp.
    5. ///
    6. /// The Events which are affected are: OnPointerDown, OnPointerUp, OnPointerClick.
    7. /// </summary>
    8. public class UiButtonEventBubbling : MonoBehaviour,
    9.                                        IPointerDownHandler,
    10.                                        IPointerUpHandler,
    11.                                        IPointerClickHandler,
    12.                                        ISubmitHandler
    13. {
    14.    /// <summary>
    15.    /// DoNotBubble = The Unity default behaviour. Buttons catch all events and do not bubble.
    16.    /// BubbleWithoutTrigger = Buttons bubble events if there is no Event Trigger component attached to the button. If there is an Event Trigger then no bubbling will occur.
    17.    /// BubbleAlways = Buttons will always bubble events.
    18.    /// </summary>
    19.    public enum ButtonBubbleBehaviour { DoNotBubble, BubbleWithoutTrigger, BubbleAlways };
    20.  
    21.    [Tooltip("Should the Button let events bubble (they usually don't). Only affects PointerUp, PointerDown and PointerClick events.\n\nDoNotBubble:\nThe Unity default behaviour. Buttons catch all events and do not bubble.\n\nBubbleWithoutTrigger:\nButtons bubble some events if there is no Event Trigger component attached to the button. If there is an Event Trigger then no bubbling will occur.\n\nBubbleAlways:\nButtons will always bubble some events (not all).")]
    22.    public ButtonBubbleBehaviour buttonBubbleBehaviour = ButtonBubbleBehaviour.BubbleWithoutTrigger;
    23.  
    24.    [Tooltip("Should OnSubmit be delayed until a key is released?\nUsually onSubmit is executed on key down.")]
    25.    public bool DelaySubmitTilKeyUp = false;
    26.  
    27.    protected bool hasButton;
    28.    protected bool hasEventTrigger;
    29.    protected Coroutine onSubmitOnKeyUpCoroutine;
    30.  
    31.    private void Awake()
    32.    {
    33.        this.hasButton = this.GetComponent<Button>() != null;
    34.        if (this.hasButton) // tiny optimization, don't ask for Event Trigger if it is not a button
    35.        {
    36.            this.hasEventTrigger = this.GetComponent<EventTrigger>() != null;
    37.        }
    38.    }
    39.  
    40.    protected void HandleEventPropagation<T>(Transform goTransform, BaseEventData eventData, EventFunction<T> callbackFunction) where T : IEventSystemHandler
    41.    {
    42.        if (hasButton && goTransform.parent != null)
    43.        {
    44.            var continueBubblingFromHereObject = goTransform.parent.gameObject;
    45.            switch (buttonBubbleBehaviour)
    46.            {
    47.                case ButtonBubbleBehaviour.BubbleAlways:
    48.                    // propagate event further up
    49.                    ExecuteEvents.ExecuteHierarchy(continueBubblingFromHereObject, eventData, callbackFunction);
    50.                    break;
    51.                case ButtonBubbleBehaviour.BubbleWithoutTrigger:
    52.                    // propagate event further up only if there is no EventTrigger
    53.                    if (this.hasEventTrigger == false)
    54.                    {
    55.                        ExecuteEvents.ExecuteHierarchy(continueBubblingFromHereObject, eventData, callbackFunction);
    56.                    }
    57.                    break;
    58.                case ButtonBubbleBehaviour.DoNotBubble:
    59.                default:
    60.                    // Do Nothing: default unity behaviour. Up, Down and Click events will not bubble.
    61.                    break;
    62.            }
    63.        }
    64.    }
    65.  
    66.    public void OnPointerDown(PointerEventData eventData)
    67.    {
    68.        eventData.pointerPress = this.gameObject; // Set the usually empty pointerPress in OnPointerDown.
    69.        HandleEventPropagation(transform, eventData, ExecuteEvents.pointerDownHandler);
    70.    }
    71.  
    72.    public void OnPointerUp(PointerEventData eventData)
    73.    {
    74.        HandleEventPropagation(transform, eventData, ExecuteEvents.pointerUpHandler);
    75.    }
    76.  
    77.    public void OnPointerClick(PointerEventData eventData)
    78.    {
    79.        HandleEventPropagation(transform, eventData, ExecuteEvents.pointerClickHandler);
    80.    }
    81.  
    82.    public void OnSubmit(BaseEventData eventData)
    83.    {
    84.        if (DelaySubmitTilKeyUp)
    85.        {
    86.            if (onSubmitOnKeyUpCoroutine != null)
    87.            {
    88.                StopCoroutine(onSubmitOnKeyUpCoroutine);
    89.            }
    90.            onSubmitOnKeyUpCoroutine = StartCoroutine(onSubmitOnKeyUp(transform, eventData));
    91.        }
    92.        else
    93.        {
    94.            HandleEventPropagation(transform, eventData, ExecuteEvents.submitHandler);
    95.        }
    96.    }
    97.  
    98.    protected System.Collections.IEnumerator onSubmitOnKeyUp(Transform transform, BaseEventData eventData)
    99.    {
    100.        // Wait for the key(s) to be released.
    101.        yield return new WaitWhile( () => Input.anyKey );
    102.        // Wait until the end of the frame to avoid other key dependent input to get triggered instantly.
    103.        yield return new WaitForEndOfFrame();
    104.        HandleEventPropagation(transform, eventData, ExecuteEvents.submitHandler);
    105.        onSubmitOnKeyUpCoroutine = null;
    106.    }
    107. }
    108.  
    Here is a test Component which also contains some write-up on how to get the desired Button if you use Event Bubbling. Just add it to any parent GameObject and check which events are bubbling up.
    Code (CSharp):
    1.  
    2. using UnityEngine;
    3. using UnityEngine.EventSystems;
    4.  
    5. public class EventBubblingTest : MonoBehaviour,
    6.                                  IPointerDownHandler,
    7.                                  IPointerUpHandler,
    8.                                  IPointerEnterHandler,
    9.                                  IPointerExitHandler,
    10.                                  IPointerClickHandler,
    11.                                  IMoveHandler,
    12.                                  ISelectHandler,
    13.                                  IDeselectHandler
    14. {
    15.     public void OnPointerDown(PointerEventData eventData)
    16.     {
    17.         // Sadly "eventData.pointerPress" is null.
    18.         // If you use UiButtonEventBubbling then it will set pointerPress for you.
    19.         //
    20.         // Be aware that "eventData.pointerCurrentRaycast.gameObject" will return
    21.         // the actual object hit by the ui raycast, which may be the button but it
    22.         // can also be a child of the button. You may want to search up wards in the
    23.         // hierarchy for the button.
    24.         //
    25.         // You may want to use "eventData.selectedObject" which may be null if the object
    26.         // is not a "Selectable" or if anyhting prevents it from  being selected.
    27.         //
    28.         // Do not use "eventData.lastPress"! It returns the result of the previous
    29.         // event, not the current one.
    30.         if (eventData.pointerPress != null)
    31.         {
    32.             Debug.Log("PointerDown on " + eventData.pointerPress.name);
    33.         }
    34.         else
    35.         {
    36.             Debug.Log("PointerDown on " + eventData.pointerCurrentRaycast.gameObject.name);
    37.         }
    38.     }
    39.  
    40.     public void OnPointerUp(PointerEventData eventData)
    41.     {
    42.         // For Buttons "eventData.pointerPress" is a fine choice. It will give you the
    43.         // pressed Button. You can also access the raycast result at down time with
    44.         // "eventData.pointerPressRaycast".
    45.         //
    46.         // It seems "eventData.pointerPress" is the first object up the hierarhy which
    47.         // implements the IPointerUpHandler. The documentation says "The GameObject that
    48.         // received the OnPointerDown."
    49.         //
    50.         // To get the real target you could use "pointerCurrentRaycast.gameObject" but be
    51.         // aware that it may be null if the user releases outside of the object. It may also
    52.         // be another completely unrelated object if the user releases on another object. For
    53.         // example if the user presses down on Button A but releases over an image
    54.         // outside the button then "pointerCurrentRaycast.gameObject" will return that
    55.         // image (current position raycast).
    56.         //
    57.         // Long story short, you may have to do some logic depending on your needs.
    58.         Debug.Log("PointerUp on " + eventData.pointerPress.name + ". Raycast(down) did hit " + eventData.pointerPressRaycast.gameObject.name);
    59.     }
    60.  
    61.     public void OnPointerEnter(PointerEventData eventData)
    62.     {
    63.         Debug.Log("PointerEnter on " + eventData.pointerEnter.gameObject);
    64.     }
    65.  
    66.     public void OnPointerExit(PointerEventData eventData)
    67.     {
    68.         Debug.Log("PointerExit on " + eventData.pointerEnter.gameObject);
    69.     }
    70.  
    71.     public void OnPointerClick(PointerEventData eventData)
    72.     {
    73.         // It seems "eventData.pointerPress" is the first object up the hierarhy which
    74.         // implements the IPointerUpHandler. The documentation says "The GameObject that
    75.         // received the OnPointerDown."
    76.         Debug.Log("PointerClick on " + eventData.pointerPress.gameObject + ". Raycast(down) did hit " + eventData.pointerPressRaycast.gameObject.name);
    77.     }
    78.  
    79.     public virtual void OnDeselect(BaseEventData eventData)
    80.     {
    81.         Debug.Log("Deselect");
    82.     }
    83.  
    84.     public virtual void OnMove(AxisEventData eventData)
    85.     {
    86.         Debug.Log("Move");
    87.     }
    88.  
    89.     public virtual void OnSelect(BaseEventData eventData)
    90.     {
    91.         Debug.Log("Select");
    92.     }
    93. }
    94.  
    Update feb 2021: Added proper handling of OnSubmit(). Also added some extra code which delays the execution of OnSubmit() until the key is released (it is off by default, see "DelaySubmitTilKeyUp" checkbox). Enjoy!
     
    Last edited: Feb 16, 2021
  5. MaxIzrinCubeUX

    MaxIzrinCubeUX

    Joined:
    Jan 13, 2020
    Posts:
    5
    According to the Unity docs, if you register a callback, which you can do for a "VisualElement", the events will bubble up, and propagate, while implementing the interfaces executes the event only on the given object.

    But I couldn't find any example of how to actually do this, and what class implements "VisualElement"?
    It seems to be the same classes, like Trigger, Image, Button, etc... but in a different namespace.
    I can't get it to work though, GetComponent refuses to accept it, while the inspector takes these types without error, but the callback isn't called, at least for me.

    The docs: https://docs.unity3d.com/Manual/UIE-Events-Handling.html
     
    neonblitzer likes this.
  6. _geo__

    _geo__

    Joined:
    Feb 26, 2014
    Posts:
    1,112
    I assume you would have to do it by script as described here:
    https://docs.unity3d.com/ScriptReference/UIElements.VisualElement.html
    https://docs.unity3d.com/ScriptReference/UIElements.CallbackEventHandler.RegisterCallback.html

    But as you have experienced yourself, the hard part is how to get the VisualElement. I assume since it is in the UiElements namespace it is not part of the runtime (update: definitely is not).

    Quote: "UIElements: User Interface Elements (UIElements) is a retained-mode UI toolkit for developing user interfaces in the Unity Editor."

    https://docs.unity3d.com/Manual/UIToolkits.html

    Yes, it is confusing at times :D

    For completeness: the button component above is for RUNTIME, not the editor. It also helps with buttons within ScrollRects. Just add the component to your buttons within the Scroll View and add an EventTrigger to your Scroll View (ScrollRect) and listen there.
     
    Last edited: May 18, 2020
  7. phobos2077

    phobos2077

    Joined:
    Feb 10, 2018
    Posts:
    350
    For even more completeness: UIElements (aka UI Toolkit) is not limited to Editor. It's just a completely separate Retained mode UI system for both Editor and Runtime. Events handling manual page referred to above has nothing to do with Unity UI (aka "Legacy UI system").
     
  8. _geo__

    _geo__

    Joined:
    Feb 26, 2014
    Posts:
    1,112
    I see what you did there ^^. Thanks for the clarification.

    Yes, the initial thread was only about uGUI which has nothing to do with UiElements. Also the Bubble Component is just for uGUI. UIElements runtime features are still listed as "ToBeDone" in the docs. Do you have any experience with it? Is it ready for runtime use by now?
     
    Last edited: Jan 9, 2021
  9. phobos2077

    phobos2077

    Joined:
    Feb 10, 2018
    Posts:
    350
    Just barely. I've seen it integrated into the project by another developer for custom editor tool. I think it's still WIP/experimental for Runtime use, but I think for the future it will be the definitive tool for game UI, replacing UGUI entirely because of it's superior stylesheet system, separation of structure from presentation, other benefits of retained mode UI. Also as I glimpsed from the manual, there is finally a proper event system, not this pile of hacks we use with UGUI.

    PS: I was a web developer for many years, so you can understand my excitement about "modern" UI system.
     
  10. _geo__

    _geo__

    Joined:
    Feb 26, 2014
    Posts:
    1,112
    Ha, yes, same here. I always cringe if I see someone inventing a new ui system instead of just using a solution which has been working and improving over decades (html + css).

    I think Unity will push this quite heavily because those new shareholders will surely want to see some growth. They have now almost saturated the game dev market and to penetrate new markets they need a much better ui and input system, imho. I tested unity once for making a regular app. The power consumption was quite high. There is no need to fully rerender a registration form every frame at 60 fps.

    I hope they'll stick closely to the css syntax and name things accordingly (maybe add sass/less features while they are at it). Imagine copy pasting css templates into unity to bootstrap (pun not intended :D) your ui. Wouldn't that be a thing. Realistically I would settle for ditching all those prefabs and just have decently working stylesheets.
     
  11. phobos2077

    phobos2077

    Joined:
    Feb 10, 2018
    Posts:
    350
    After working with CSS/HTML for 8 years - I hate it with passion. It's powerful, yes, but it's built as layers of legacy crap. Unity does exactly the right thing by doing a new *clean* UI framework, built in modern manner but not based on any bloated browser technology (I hate browsers, even though they do push UI technology for past decade).
     
    neonblitzer likes this.
  12. _geo__

    _geo__

    Joined:
    Feb 26, 2014
    Posts:
    1,112
    Ah okay, I see the validity of your argument, though I only partially agree.

    To me css has weathered quite a lot of storms over time, adapting along the way. I still remember making websites with table layouts and only minimal css. Table layouts are long gone (thank god) and float design is also mostly gone by now thanks to flexbox. You are right in that there is some bloat. I think to keep the web working they had to remain backwards compatible, which came with a lot of downsides. Having multiple browser vendors competing on pushing different syntax and features did not help either (something I think unity can avoid due to its market dominance).

    What I've experienced in the past with new frameworks is that for simple cases they usually do a fine job. Yet, for me it's all about the edge cases which only really work in a battle tested frameworks. To me css is the most tested of all (all those things it is used for: web, apps, desktop). I agree that unity should not implement a full fledged browser rendering engine with all the flaws they come with. But building on the ideas of css and learning from it will surely give a better result than uGui. If they are compatible with css standards (flex, margins, paddings, cascades, .. just the really basic stuff) then they would increase the number of people who can build uis for unity by a lot. Not too many devs will ever learn uGui, but most have had some contact with css. That's why I think it would be a smart move to be compatible, at least on the syntax side.

    Maybe I am too optimistic :)
     
    phobos2077 likes this.
  13. m0nkeybl1tz

    m0nkeybl1tz

    Joined:
    Feb 10, 2013
    Posts:
    25
    Hey, sorry if this is off-topic, but I'm trying to make a draggable window with buttons and am running into a similar issue.

    Basically, the parent has the "Draggable" script that listens for OnBeginDrag, and there are a number of children with a "Button" script that listens for OnPointerClick, but for some reason clicking the buttons only seems to trigger the OnBeginDrag event in the parent, not the OnPointerClick event for the children. Is there any way for the children to intercept the event before it reaches the parent?
     
  14. _geo__

    _geo__

    Joined:
    Feb 26, 2014
    Posts:
    1,112
    Try to implement IBeginDragHandler, IDragHandler, ... on your button and only forward the event to the parent if needed.