Event propagation seems to suddenly stop

Hi,

For the sake of discussion, I have a background plane where you can click to move a cursor. Then I have a popup dialog, which has (a) a cover-plane that covers the screen so I know when the user clicked outside, and (b) the actual popup dialog.

The problem is that I’m unable to get any events to click through the cover-plane. Normally, if I don’t stop propagation on something higher up, it clicks through to things behind it. But here, nothing clicks through this cover-plane, whether I have an event-handler attached or not.

The popup cover plane is simply this USS:

7444043--912629--upload_2021-8-24_10-47-11.png

And this UXML.

This is simply added to the UI root element, which already holds the background plane:

As soon as the cover-plane is present, nothing will click through it, whether or not I even have an event handler on the cover-plane.

Is this normal and expected? I have other situations where it doesn’t happen. For example, inside the popup, if I have unhandled areas, they click through to the cover-plane. So it works fine within the popup’s own hierarchy. But it doesn’t work between the popup and the things that are behind it in the root UI element.

This really doesn’t add up. Here is the UI Debugger. Both #clickPlane and #popupCover cover 100% of the screen area. #popupCover is added last.

7444127--912647--upload_2021-8-24_11-31-5.png

An unhandled click from #popupCover will never reach #clickPlane. It doesn’t matter whether #popupCover has an event handler. If I log from a #popupCover handler, “bubbling” is true, and “isPropagationStopped” is false. None of the items have had their pickingMode changed.

The only thing that sticks out as a pattern here is that maybe events have trouble propagation across as TemplateContainer. I’ll try to add these UXML hierarchies in a way where I transplant the children directly into the root VisualElement, which would confirm or deny that hypothesis.

The hypothesis was false. Even on the same level, unhandled events from #popupCover don’t reach #clickPlane.

7444169--912656--upload_2021-8-24_11-44-53.png

As a sanity check, I’ve disabled the picking mode on #popupCover, and of course #clickPlane now receives all clicks. But the mystery remains why #popupCover eats all events, even without an event handler attached. Just being there, nothing is allowed to click through it. I’ve normally been fighting the opposite problem, that everything is click-through, so I’m confused.

hello perholmes,
have you tried moving #clickPlane lower than #popupCover?

But #clickPlane is already lower than #popupCover. #clickPlane is added before #popupCover is even brought in.

They key evidence is that with a Debug.Log(“bla bla”) event handler on both #clickPlane and #popupCover, only #popupCover ever receives any events at all. The event handlers for #clickPlane fire zero times when #popupCover is on top. Even when I attack no event handler at all to #popupCover, #clickPlane still doesn’t fire. The very presence of #popupCover blocks events to anything underneath, even if the events aren’t processed.

But the question is whether there’s a wrong assumption here. Because apparently display order isn’t strictly event order then. It’s starting to look like things literally have to be children of each other in the hierarchy in order to participate in trickle-down-bubble-up events. Merely being in front of or behind each other on the screen isn’t enough.

This would also mean that Bring To Front and Send To Back methods are actually meaningless.

It really does seem like UI Elements uses hierarchy, and not stacking order, to determine event processing order.

I’ve made a reproduction project, which shows the issue. In summary, the issue is:

Layered UI objects will click through when they’re children of each other, but not when they’re siblings of each other, even if the UI layering appears identical.

I can’t tell if it’s a bug or by design (or by accident), but it’s definitely a surprise. Basically, the VisualElement hierarchy, and not the display order is being used to propagate events.

Here, I’ve created two setups. On the left, the red box is in front of the green box, but they’re siblings (added to the same root). When you click the red box in the overlapping area but don’t stop propagation, green nevertheless does not get click through events.

On the right, yellow is nested inside blue. When you click yellow, you get both and event for yellow and blue if you don’t stop propagation. This is in my mind the correct behavior for all visually layered objects.

7445738--913007--upload_2021-8-24_20-29-10.png

Yes, it’s possible to hack around this, but is this the right behavior? It makes it impossible to load HUDs or panels or gizmos and give them the ability to click through. They will always eat the events, simply by being siblings. The display order is ignored.

It also forces HUDs into the awkward situation of having to be children of the objects they’re supposed to float on top of. And since absolute positioning is relative to the parent and not the screen, it forces you to always show overflow on the hierarchy you want to be on top of, and to compensate the coordinates in order to position the HUD absolutely. The alternative is that all HUDs eat all pointer events, or that you’re forced to unilaterally disable picking on HUD elements in order to start/stop them from reacting.

Here is the test code. You’ll also need to drop any PanelSettings configuration in Resources. Create an Empty game object in the scene and attach this script:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UIElements;

public class Main : MonoBehaviour
{
    void Start()
    {
        var uiGameObj = new GameObject();
        uiGameObj.name = "UI Container";

        var rootDoc = uiGameObj.AddComponent<UIDocument>();
        var root = rootDoc.rootVisualElement;
        root.StretchToParentSize();
   
        // Siblings, don't allow click through

        var green = new VisualElement();
        green.style.left = Pixels(200);
        green.style.top = Pixels(200);
        green.style.width = Pixels(200);
        green.style.height = Pixels(200);
        green.style.backgroundColor = new Color(0, 1, 0);
        green.style.position = Position.Absolute;
        green.RegisterCallback<PointerDownEvent>(GreenDown);
        green.name = "Green";
        root.Add(green);

        var red = new VisualElement();
        red.style.left = Pixels(300);
        red.style.top = Pixels(300);
        red.style.width = Pixels(200);
        red.style.height = Pixels(200);
        red.style.backgroundColor = new Color(1, 0, 0);
        red.style.position = Position.Absolute;
        red.RegisterCallback<PointerDownEvent>(RedDown);
        red.name = "Red";
        root.Add(red);
   
        // Children, do allow clickthrough

        var blue = new VisualElement();
        blue.style.left = Pixels(600);
        blue.style.top = Pixels(200);
        blue.style.width = Pixels(200);
        blue.style.height = Pixels(200);
        blue.style.backgroundColor = new Color(0, 0, 1);
        blue.style.position = Position.Absolute;
        blue.RegisterCallback<PointerDownEvent>(BlueDown);
        blue.name = "Blue";
        root.Add(blue);

        var yellow = new VisualElement();
        yellow.style.left = Pixels(50);
        yellow.style.top = Pixels(50);
        yellow.style.width = Pixels(100);
        yellow.style.height = Pixels(100);
        yellow.style.backgroundColor = new Color(1, 1, 0);
        yellow.style.position = Position.Absolute;
        yellow.RegisterCallback<PointerDownEvent>(YellowDown);
        yellow.name = "Yellow";
        blue.Add(yellow);

        // PanelSettings
        var panelSettings = Resources.Load<PanelSettings>("PanelSettings");
   
        if (panelSettings == null) {
            Debug.Log("No panel settings found");
        }

        rootDoc.panelSettings = panelSettings;
 
    }

    public void GreenDown(PointerDownEvent evt)
    {
        Debug.Log("Green down");
    }

    public void RedDown(PointerDownEvent evt)
    {
        Debug.Log("Red down");
    }

    public void BlueDown(PointerDownEvent evt)
    {
        Debug.Log("Blue down");
    }

    public void YellowDown(PointerDownEvent evt)
    {
        Debug.Log("Yellow down");
    }

    public static Length Pixels(float pixels)
    {
        return new Length(pixels, LengthUnit.Pixel);
    }
}

Desired Resolution: The same click-through rules should apply to siblings as they do to children, because the UI layering is identical.

1 Like

No, it does seem that UI Elements follows standard DOM event propagation. It just has some undesirable side-effects that you have to hack around, and people are hacking around it in JavaScript too. It’s especially awkward around absolutely positioned objects, because they follow event propagation that might be very different from what you’re seeing on the screen.

I will have to make a root UI element that receives events in the trickle down phase, so that I can decide at the very top whether a click through is allowed, and then it’s not actually a cover-plane that decides whether click throughs are allowed, but an uber-parent plane that becomes responsible for canceling dialogs and menus. It’s awkward but it can be done.

I’m unsure why I’ve never managed to confront this, neither in JS/CSS or in Flash.

In case it helps someone else, here is the approach I’m landing on.

Basically, I have a root UI element that everything else is attached to. Then I create an event handler on the root for PointerDownEvent, TrickleDown mode, which makes it the first to hear about any events:

{
        uiGameObj = new GameObject();
        uiGameObj.name = "UI Container";

        rootDoc = uiGameObj.AddComponent<UIDocument>();
        root = rootDoc.rootVisualElement;
        root.StretchToParentSize();
        root.RegisterCallback<PointerDownEvent>(PointerDownEarlyWarning, TrickleDown.TrickleDown);
}

    private static void PointerDownEarlyWarning(PointerDownEvent evt)
    {
        Debug.Log("Pointer down early warning");
    }

This early warning handler then lets me decide whether to cancel any open menus, or let the click go through to another area while keeping the menu open. For example, I often want to allow panning on the canvas while keeping a callout open. But actually clicking other objects or menu buttons need to cancel the dialog before showing a new one.

It’ll work.

1 Like