Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rename the Mouse Filter modes and add a Skip mouse filter mode #3613

Open
reduz opened this issue Nov 29, 2021 · 41 comments
Open

Rename the Mouse Filter modes and add a Skip mouse filter mode #3613

reduz opened this issue Nov 29, 2021 · 41 comments
Labels
breaks compat Proposal will inevitably break compatibility topic:gui topic:input
Milestone

Comments

@reduz
Copy link
Member

reduz commented Nov 29, 2021

Describe the project you are working on

Godot

Describe the problem or limitation you are having in your project

Godot GUI controls have something called the Mouse Filter, which determines how mouse events are handled by the controls.

To give a bit more insight on how mouse filtering work, the following logic is used: The mouse input will always reach a single control. This is expected in any GUI library. Once the input reaches a control, this control will decide on what to do based on the Mouse Filter, which can be any of the following:

  • STOP: Will consume the event. End of story.
  • PASS: Will also consume the event, but will also pass it to the parent control for consumption. This is useful in cases like a texture rect or label over a button, or controls over a scroll container that can scroll using touch on mobile regardless of the children control. For this to work, passing the control to the parent will work.
  • IGNORE: Does not consume the event and discard it. The control acts like a black hole where events are lost.

This works for the most part, but there are situations where users want the input to be ignored and passed to a sibling control instead, which can be seen in issues like:

godotengine/godot#55432
godotengine/godot#55288
and many others.

This is currently not possible.

Additionally other proposals (such as #3272) make the need to propagate the mode in tree fashion (like we do with other options like the process mode) easier.

Describe the feature / enhancement and how it helps to overcome the problem or limitation

This proposal aims to do three different things:

  • Add an extra mouse filter model that acts as if the control was not there. Similar to it being hidden. The control is simply skipped. This will still allow passing to sibling controls.
  • Rename the mouse filter modes because they are confusing.
  • Add an inherit option and make it default.

Describe how your proposal will work, with code, pseudo-code, mock-ups, and/or diagrams

The new proposed mouse filter modes would be:

  • MOUSE_FILTER_INHERIT: Use the same mode as the parent control.
  • MOUSE_FILTER_HANDLE: Handle the event
  • MOUSE_FILTER_HANDLE_WITH_PARENT: Handle the event and pass it to the parent node.
  • MOUSE_FILTER_IGNORE: Ignore the event (as if the control was hidden), the next available control will handle it. Unlike the old ignore mode (which just absorbs and discards the event), I think this makes more sense to users.

If this enhancement will not be used often, can it be worked around with a few lines of script?

n/a

Is there a reason why this should be core and not an add-on in the asset library?

n/a

@YuriSizov
Copy link
Contributor

Not sure how deeply it is related, but this proposal is worth considering when deciding on the implementation: #3272 (it also has a PR ready: godotengine/godot#53075)

@golddotasksquestions
Copy link

golddotasksquestions commented Nov 29, 2021

I'm even more confused by this than the current behavior.

MOUSE_FILTER_IGNORE: Ignore the event (as if the control was hidden)
MOUSE_FILTER_DISCARD: Discard the event, nobody else will get it.

How is "Discard" and "Ignore" any different?

Neither of them seem to do or imply the behavior I would currently expect from "Pass" as a user: To handle the Input and pass it on to any other node at the current mouse position.

If "Discard" is used to pass the signal to other Nodes at the same mouse location, why is it not called "Pass"?

@reduz
Copy link
Member Author

reduz commented Nov 29, 2021

@golddotasksquestions Better wording welcome, but the general idea is that the first should make it as if the control did not exist for the mouse. The second will do the entire opposite, it gets the event but completely discards it. Its a way to simply disable input for a control (imagine you don't want a mouse to get input for some reason, but you dont want to pass it to something else).

We might simply merge both as the same thing to be honest and have a single IGNORE or SKIP mode.

@reduz
Copy link
Member Author

reduz commented Nov 29, 2021

@pycbouh I suppose we could have an extra mode INHERIT for this and make it default?

@Calinou Calinou changed the title Rename the Mouse Filter modes and add a skip one. Rename the Mouse Filter modes and add a Skip mouse filter mode Nov 29, 2021
@Calinou Calinou added this to the 4.0 milestone Nov 29, 2021
@reduz
Copy link
Member Author

reduz commented Nov 29, 2021

Okay @golddotasksquestions @pycbouh Added both your feedback in the proposal.

@golddotasksquestions
Copy link

golddotasksquestions commented Nov 29, 2021

Adding INHERIT is great, that makes a lot of sense and will save time clicking.

However I still don't understand at all what you are planning to replace "PASS" with for sibling Controls.

MOUSE_FILTER_IGNORE: Ignore the event (as if the control was hidden), the next available control will handle it. Unlike the old ignore mode (which just absorbs and discards the event), I think this makes more sense to users.

The current implementation of Ignore does not discard the mouse event. The mouse event will still get handled by any Control node sorted underneath at the same mouse position.

What we need is a true Pass mouse filter setting: Control handles the event, but "passes" it on to any other Control at the same mouse position.
This would mean if you would have a stack of Controls and all of them are set to mouse filter Pass, they all will handle the event.

Which one of your proposed Mouse filters would do this, regardless of scene tree composition?
(I know MOUSE_FILTER_HANDLE_WITH_PARENT would do this if all Controls are nested in a single chain, but the point of these issues and consequently this proposal is to find a solution for any arbitrary tree composition. Since the current Pass already seems to be identical to MOUSE_FILTER_HANDLE_WITH_PARENT. So MOUSE_FILTER_HANDLE_WITH_PARENT is a change in name alone, right?)

@reduz
Copy link
Member Author

reduz commented Nov 29, 2021

@golddotasksquestions

However I still don't understand at all what you are planning to replace "PASS" with for sibling Controls.

Because there can be several other situations where you still want it to reach another control, but that control is not a sibling but something else, so you would need to make it work for literally everything. Additionally if you do that and you pass to any other control under the mouse it becomes very confusing because a) you don't really know in which order it happens. b) controls covered by other controls may still get the input and you would be unaware.

The pass functionality is only intended to work with parents because its for very specific use cases. It means that the control will handle the input and then pass it to the parent.

What you (and others) actually want is not this, you simply want the control to do nothing (as if it was hidden) and let the next available one take the input. Hacking the pass functionality for this is complex and does not really make sense anyway, when all you want is an "ignore this control" mode.

So, the solution is to add a new mode that actually does what you want, not hack an existing one you were not even aware it worked the way it does.

@reduz
Copy link
Member Author

reduz commented Nov 29, 2021

So, the solution is to add a new mode that actually does what you want, not hack an existing one you were not even aware it worked the way it does.

And to clarify this is not your fault, it is mine for not making it clear enough.

@golddotasksquestions
Copy link

golddotasksquestions commented Nov 29, 2021

What you (and others) actually want is not this, you simply want the control to do nothing (as if it was hidden) and let the next available one take the input.

No, that's definitely not what I or others want. We already have this. This is already what the current implementation of Ignore does.

What I and I think many other expect is what you would matches the visual experience, matches the 2D sorting of the Control nodes in the viewport, regardless of their position in the scene tree.

I have no idea if that's feasible to implement, but I know this is what is intuitively expected: You click on a Control, and if it has mouse filter set to Pass it handles the event and passes it to the one visually sorted next in line until one of them is set to Stop or there are no Control left anymore.

The way I imagined it to work is to first check which Controls are at a given location, and then, starting from the one furthest from the root, simultaneously "walk" the scene tree on multiple branches only on the nodes at this location towards the root. Whenever one of the simultainous "walk" hits a Ignore or Pass mouse filter, the "walk" just carries on, but when one of the walks hits a Stop mouse filter, this particular walk stops.

I'll see if I can create an animation to visualize this.

@reduz
Copy link
Member Author

reduz commented Nov 29, 2021

@golddotasksquestions If you want more than one control to handle a single event that's not going to happen. As I mentioned in the proposal:

The mouse input will always reach a single control.

Breaking this convention would make the entire UI code extremely complex for no good reason because many behaviours (click dragging specially, which is needed for drag and drop as well as scrolling) would just not work. No UI library that I am aware of works this way either.

It's better that you work around what you want to do in a different way.

@Whimfoome
Copy link

I find the current names easier to understand than the proposed ones.

@BimDav
Copy link

BimDav commented Dec 1, 2021

How does that relate to godotengine/godot#54656 ?
I think that in this PR we still have the invariant "The mouse input will always reach a single control.". Conversely, if an input is not handled by any Control node, it makes sense that it continues its path towards _unhandled_input().

But if this PR is still OK then the new naming is worse than the old one since MOUSE_FILTER_HANDLE_WITH_PARENT (the new PASS) does not guarantee that the input will be handled.

@fire-forge
Copy link

IGNORE: Does not consume the event and discard it. The control acts like a black hole where events are lost.

Are you sure that's how Ignore works? This sounds more like Stop, although Stop doesn't ignore the input. In my experience, Ignore makes input events behave as if the node isn't even there, allowing input to pass to other nodes that are below it.

MOUSE_FILTER_IGNORE: Ignore the event (as if the control was hidden), the next available control will handle it.

This sounds exactly like how Ignore currently works, based on personal experience and the way it's described in the docs page for Control.

MOUSE_FILTER_IGNORE = 2 --- The control will not receive mouse button input events through _gui_input. The control will also not receive the mouse_entered nor mouse_exited signals. This will not block other controls from receiving these events or firing the signals. Ignored events will not be handled automatically.

@Szesan
Copy link

Szesan commented Jan 8, 2022

What you (and others) actually want is not this, you simply want the control to do nothing (as if it was hidden) and let the next available one take the input.

No, that's definitely not what I or others want. We already have this. This is already what the current implementation of Ignore does.

What I and I think many other expect is what you would matches the visual experience, matches the 2D sorting of the Control nodes in the viewport, regardless of their position in the scene tree.

I have no idea if that's feasible to implement, but I know this is what is intuitively expected: You click on a Control, and if it has mouse filter set to Pass it handles the event and passes it to the one visually sorted next in line until one of them is set to Stop or there are no Control left anymore.

The way I imagined it to work is to first check which Controls are at a given location, and then, starting from the one furthest from the root, simultaneously "walk" the scene tree on multiple branches only on the nodes at this location towards the root. Whenever one of the simultainous "walk" hits a Ignore or Pass mouse filter, the "walk" just carries on, but when one of the walks hits a Stop mouse filter, this particular walk stops.

I'll see if I can create an animation to visualize this.

Couldn't agree more.

I understand that the current behavior makes sense to the developers, but it makes no sense and it's not at all intuitive for the users of the product.

If the current behavior of the pass filter is correct for some feature the developers envisioned, that's fine. But the users desperately want another feature then, a feature that the name "pass" implies. One that passes the input to the node bellow in rendering order, not in scene hierarchical order.

Name this new filter option "pass4realz" or whatever, but this is what would be an actually useful feature for the vast majority of the userbase.

@Nukiloco
Copy link

Nukiloco commented Jul 2, 2022

To be honest, I like the current UI system but I feel like this might make it a little bit more complicated for people to pick up, especially beginners.

Here is my recommendation on how mouse filters should work:

Inherit should inherit the parent mouse_filter.
Stop allows you to handle the events yourself, but if not handled then its discarded.
Ignore should just pass events down.

When the mouse filter is set to Stop we can control it via the virtual function _gui_input. When overriding _gui_input, if we don't call accept_event then it will pass it to the next node.

# When _gui_input is not overridden, the event gets discarded or handled by the Control node we are extending
# func _gui_input(events):
# accept_event()

# The same as the first function above where the Control node we are extending handles the _gui_input
func _gui_input(events):
  super._gui_input(events)

# This is what _gui_input on mouse_filter Stop does by default
func _gui_input(events):
  accept_event()

# When accept_event is not called, we pass the event to the next node
func _gui_input(events):
# accept_event()
  pass

@golddotasksquestions
Copy link

golddotasksquestions commented Jul 2, 2022

@Nukiloco

I find having to add a script and override a built in function to be faaaaaar less beginner friendly than just setting mouse filter to "pass" in the Inspector.

Even for myself as an intermediate/experienced user this would feel like a downgrade in user friendliness, not an improvement.

@Nukiloco
Copy link

Nukiloco commented Jul 3, 2022

@Nukiloco

I find having to add a script and override a built in function to be faaaaaar less beginner friendly than just setting mouse filter to "pass" in the Inspector.

Even for myself as an intermediate/experienced user this would feel like a downgrade in user friendliness, not an improvement.

Aka Ignore would work like pass as ignore shouldn't discard events at so there is still Pass functionality though it would be moved to Ignore instead.

@aaronfranke
Copy link
Member

Kicked to 4.x instead of 5.0 because the proposed Skip mode does not require renaming the existing mouse filter modes, but the renaming part of this proposal breaks compat so it will have to wait for Godot 5.0 in a decade or so.

@Zireael07
Copy link

@aaronfranke This has been mostly agreed on for 2 years and wasn't touched, and now you're saying we won't see it fixed until ten years from now? Seriously? Some usability things need to win over strict "no breaking compatibility" (or Godot 5 needs to come earlier)

@Whimfoome
Copy link

I still find the current way it's working simpler, so no complaints from me to move it to Godot 5.

@aaronfranke
Copy link
Member

aaronfranke commented Feb 24, 2023

@Zireael07 We can't work on every proposal for Godot 4.0, or else Godot 4.0 itself would take ten years. It's much better to actually get the features like Vulkan that have been waiting since 2019 out the door.

The actual issue can be fixed by adding a new Skip mode, only the rename part is kicked to 5.0, it's a cosmetic change.

@Zireael07
Copy link

There were two years to get this done, this is not something huge like Vulkan itself, though. That's why I'm confused, because it looks like the proposal was just forgotten/swept to the side and then now you're like "oh no too late to do anything oh noes".

Why the cosmetic rename has to wait a decade? (Thanks for clarifying the actual issue can be fixed earlier)

@aaronfranke
Copy link
Member

aaronfranke commented Feb 25, 2023

@Zireael07 Because it breaks compatibility. Users are generally unhappy when they upgrade their Godot projects to a new minor release and their project is broken.

This issue is not 2 years old, it's 1 year and 3 months old. Even if it was 2 years old, that does not impact how likely it is to get done. There are tons of proposals from 2018 and 2019. We can't get to all of them. The simple fact is that nobody has stepped up to do the work in the last year, so it has not gotten done. In order for something to get done, somebody has to do it, and that time is then not spent on doing other things (again, we can't get to everything).

This is not a vital or release-blocking change. The mouse filter is working fine for the vast majority of users. For example, see @Whimfoome's comment. Additionally, if you read the discussion above, there is not even a clear conclusion to the discussion of what the API should look like.

@golddotasksquestions
Copy link

@aaronfranke

The mouse filter is working fine for the vast majority of users.

I don't care at all about the rename since I think the issue lies much deeper. But the matter of fact is mouse filtering is definitely NOT working fine for the vast majority of users. I actually doubt the majority of users even understands what "pass" does. I know I and many others have been trying to understand it for years and I still fail to use it reliably.

godotengine/godot#55432 (comment)

@Daniel-Chin
Copy link

Daniel-Chin commented Mar 7, 2023

For now, I'm using a custom propogate function:

public static void PropogateMouseFilter(Control c, Control.MouseFilterEnum e) {
    c.MouseFilter = e;
    foreach (Node n in c.GetChildren()) {
        PropogateMouseFilter(n as Control, e);
    }
}

Ideally, I believe a "Skip"/"Inherit" MouseFilter mode should be in Godot Core. I mean, did the Godot editor itself actually never needed such a functionality? ;-)

Edit:
Eh oh, the propogation method doesn't affect newly added children.

@Szesan
Copy link

Szesan commented Mar 7, 2023

@aaronfranke

The mouse filter is working fine for the vast majority of users.

I don't care at all about the rename since I think the issue lies much deeper. But the matter of fact is mouse filtering is definitely NOT working fine for the vast majority of users. I actually doubt the majority of users even understands what "pass" does. I know I and many others have been trying to understand it for years and I still fail to use it reliably.

godotengine/godot#55432 (comment)

I have been actively working with Godot for more than 2 years. I already have a relatively successful game on steam store which was built in godot, and I'm currently working on my second game, so I'd like to think I'm pretty efficient with the engine...

Nevertheless, I NEVER use the pass filter. Obviously when I started out I tested it and as it turned out it doesn't do what I expected it to do, I moved on. It's sort of a noob trap, but I don't think a game engine needs a noob trap.

Maybe I'm the lazy one for not trying to fully grasp to "usefulness" of this filter, but this is a genuine question to other developers:

Do you actually ever use the pass filter, do you ever feel the need to use this feature? Because after implementing probably hundreds of custom controls, I surely don't.

@golddotasksquestions
Copy link

golddotasksquestions commented Mar 11, 2023

Do you actually ever use the pass filter, do you ever feel the need to use this feature?

If it would actually do what it I think it should do (allow to handle the mouse action within the control with the pass filter, then pass on or trigger it in the next Control node below, located at the same screen position), I would use it all the time.

@Szesan
Copy link

Szesan commented Mar 11, 2023

Do you actually ever use the pass filter, do you ever feel the need to use this feature?

If it would actually do what it I think it should do (allow to handle the mouse action within the control with the pass filter, then pass on or trigger it in the next Control node belo, located at the same screen position), I would use it all the time.

That was my point as well, I don't use it either because it doesn't do what I expected it to do. And it seems this is a trend and and probably lots of developers feel exactly the same...

@Jowan-Spooner
Copy link

Hey, anyone.
I'm really confused rn as I just realized (after years of working with godot UI nodes) that mouse_filter PASS does absolutely not what I would expect it to do (pass it to any other "lower" nodes). And that there is pretty much no way of achieving what I want it seems.
My Goal:
One Control node is supposed to use _gui_input() to activate something (on an InputEventMouse). I want to use _gui_input() instead of _input() because I want any buttons/UI in FRONT of this node to block this activation (if set to block obviously).
However I would like for the control NOT to consume this (and thus ALL input), but still allow "lower" controls to use input, e.g. for hover, etc.

I always thought that the mouse_filter PASS was the right way to do this, but realized that NO, pass doesn't propagate the input to all the lower controls, it just passes it to it's parent. Like. WHAT?

Now if pass is just not what I'm looking for, then okay. but. What would be the correct way to solve this? Anyone know?
I could use _input() but then I would loose the whole point of using a Control (it can be covered by other controls). Any ideas?

@kitbdev
Copy link

kitbdev commented Nov 14, 2023

Godot uses Event Bubbling for input events.
MOUSE_FILTER_PASS just means the default bubbling behavior should be used.

Most other gui mouse input systems use Event Bubbling (Unity, Unreal, HTML, Windows, Qt, etc). Some also have a capturing phase, which is basically reverse order bubbling.
Some have no propagation at all (such as GameMaker). Events are sent if the mouse overlaps with the bounds.
Phaser also has no propagation but a scene level topOnly flag can be used to switch between only the top-most object receiving mouse events, and all objects that overlap the pointer receiving mouse events.
In godot, you can handle mouse events even if it is overlapped by another Control with:

func _input(event: InputEvent) -> void:
    if event is InputEventMouse:
        if Rect2(Vector2.ZERO, size).has_point(get_global_transform_with_canvas().affine_inverse() * event.position):
            # The mouse event is in this Control.
            pass

And since mouse_entered happens before mouse move events and mouse_exited happens after, you can use those to determine if the mouse is underneath something else.

I could not find any event systems that send gui mouse events to a Control visually behind them in a controlled way (not just a bounds test).
It is possible to implement such a feature, but it would probably be confusing to users having multiple overlapping mouse events and may break existing ui systems since it breaks conventions. It's especially problematic regarding click and drag.

I feel like the existing system makes sense and is sufficient for the vast majority of users. If it is not sufficient, then why is it not a problem in other UI systems? What do they have that would solve the issue that godot does not?

I think the cause of confusion is the name of MOUSE_FILTER_PASS which can imply functionality it doesn't have.
I think we should rename it to MOUSE_FILTER_PROPAGATE_UP to be more clear.
Or possibly MOUSE_FILTER_BUBBLE_UP?

If mouse event passthrough functionality is still desired a separate proposal can be used for it (such as #7167 or a new one), though it needs more details to how it will work with the current system, how existing UIs may be affected by other Controls also receiving mouse events, and use cases.

Edit: I linked to the wrong proposal.

@kitbdev
Copy link

kitbdev commented Nov 14, 2023

I don't think MOUSE_FILTER_INHERIT is a good idea.

  1. Which Controls would use INHERIT by default? Not all of them, since some Controls should be STOP by default (Buttons and other interactables, Panel), some should be PASS by default (Containers, TextureRect), some should be IGNORE by default (Label). See here for a list of the current defaults.
    • If we do set all of them to INHERIT, then they would all just use whatever it defaults to, probably PASS. In this case we would get input going through where we don't want it to, like behind ScrollBars or Panels. We could get around this by being more aggressive with accept_event(), but that makes Controls less flexible and you couldn't get some Controls to propagate inputs if you wanted them to (Panel especially).
  2. It's not immediately clear what the behavior is if there is no parent to inherit from.
  3. Reparenting a Control to another can change the behavior, and its not obvious enough why and how. Especially since they don't all default to INHERIT, reparenting would only sometimes change the behavior and it can happen in different ways. It also depends what exactly is being reparented.
  4. If you child an INHERIT Control to one that is set to STOP, then it would block events from getting to the parent. For default, this is not the behavior we want. For example, parenting a ColorRect(INHERIT) to a Button(STOP) would block clicks from reaching the Button.
  5. If you want to mark many Controls as MOUSE_FILTER_IGNORE, you may try to rely on INHERIT and only set the top Control. However, since not all Controls use INHERIT by default, this wouldn't work and you would then need to check each one individually anyways.
  6. If you want to manually set a Control to INHERIT, then you probably know which mouse filter you actually want it to be and can use that instead. Only if you know that you will be changing the parent's mouse filter (probably between PASS and IGNORE) and want the children to follow it, then it may be useful. I think there are be better options for this use case though, such as a simple set_mouse_filter_recursively() method, or an ignore_input flag in a CanvasLayer, or a flag that forces behavior from the parent like in Add a flag to propagate mouse_filter to children of Control #3272.

@ghsoares
Copy link

ghsoares commented Dec 4, 2023

Just adding my cents to the mouse_filter enum names, I think that renaming MOUSE_FILTER_PASS to MOUSE_FILTER_BUBBLE would make it more explicit of what the behaviour does, by telling it will propagate the event up to the tree from leaf to root node (event bubbling).

I also think that the current MOUSE_FILTER_PASS name would make more sense being implemented in the proposal I've made #8566, where I described an GUI event handling implementation where instead of searching in the tree the single Control node that should receive the event and bubble the event up from there, the event is propagated first from root to leaf nodes to "pre-handle" it (inverse of bubbling, where each parent node would process the event first without actually marking it as handled, making it optional to mark it as handled or not) and when it reaches the single correct Control node to receive the event, bubble the event up if mouse_filter is set to MOUSE_FILTER_BUBBLE, or continue propagating to the siblings if the mouse_filter is set to MOUSE_FILTER_PASS, while it still process the event. It work the same as the current event handling implementation with default settings, but allow more flexibility to the input handling.

In the end we would have the enums:

  • MOUSE_FILTER_BUBBLE (same as current MOUSE_FILTER_PASS): Receive the event and pass it to the parent node, travelling back to the root node (event bubbling behaviour);
  • MOUSE_FILTER_PASS (new behaviour): Receive the event but doesn't actually mark it as handled, making it possible to previous sibling nodes to handle the event;
  • MOUSE_FILTER_STOP (same behaviour): Receive the event and stop propagation and bubbling, no other node receives the event;
  • MOUSE_FILTER_IGNORE (same behaviour): Doesn't receive the event and completely ignore it, making it possible to other nodes which is obscured by this node to receive the event.

With this implementation, it could also be used in #3272, so the parent pre-handle the event, mark it as handled if needed so the event can be propagated to the children or not.

In the HTML/JS event handling as example, you can pass an optional flag to add an event listener to callback when the event is propagated from the root to the leaf nodes (inverse of bubbling) or when it's propagated from the leaf to the root nodes (default, bubbling).

@FeralBytes
Copy link

For anyone looking for a work around, and it may not be perfect for your needs, but hopefully it helps any ways.
My use case was overlapping Texturebuttons, technically the bitmask did not overlap, but Godot does not check to that level of fidelity and thus had a problem with not passing the events along.

extends TextureButton

var __texture_scale: Vector2 = Vector2.ONE
var __mouse_entered: bool = false
var texture_click_mask_size: Vector2 = Vector2.ZERO

# Called when the node enters the scene tree for the first time.
func _ready() -> void:
    texture_click_mask_size = Vector2(texture_click_mask.get_size())
    __texture_scale = texture_click_mask_size / Vector2(self.size)


# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(_delta: float) -> void:
    pass

func _input(event: InputEvent) -> void:
    if event is InputEventMouse:
        var mouse_pos_local_scaled = (get_global_transform_with_canvas().affine_inverse() 
            * event.position * Vector2(self.__texture_scale)
        )
        if (mouse_pos_local_scaled.x < texture_click_mask_size.x and 
            mouse_pos_local_scaled.y < texture_click_mask_size.y and 
            mouse_pos_local_scaled.x > Vector2.ZERO.x and mouse_pos_local_scaled.y > Vector2.ZERO.y
        ):
            if texture_click_mask.get_bitv(mouse_pos_local_scaled):
                if self.__mouse_entered == false:
                    self.__mouse_entered = true
                    emit_signal("mouse_entered")
                print("Mouse is in ", name, ".")
            else:
                if self.__mouse_entered == true:
                    self.__mouse_entered = false
                    emit_signal("mouse_exited")
        else:
                if self.__mouse_entered == true:
                    self.__mouse_entered = false
                    emit_signal("mouse_exited")

@MarkhorDev
Copy link

I agree with this comment about INHERIT being a bad idea.

I feel like the solution here is better documentation. Quite a few people here, including the original post, are just misunderstanding the way that certain modes work. I have been confused by some of the behaviour, although I do actually think it's a good system. I understand that if a lot of people are misunderstanding, it may be a problem with the name, but renaming breaks compatability so it isn't really possible for now.

I'm thinking a dedicated tutorial for mouse_filter and its quirks, including common issues that people run into, would be a good solution. Then there is a dedicate page that can be found, or pointed to, when people are having issues.

I think its also worth explicitly stating that setting the visible property to false effectively sets a node and all its children to IGNORE until made visible again. I also made the mistake of just animating the modulate property for fading control nodes, which does not affect their behaviour with the mouse, so they were still blocking input, but that's just me messing up.

@L4Vo5
Copy link

L4Vo5 commented Jul 21, 2024

It is possible to implement such a feature, but it would probably be confusing to users having multiple overlapping mouse events and may break existing ui systems since it breaks conventions. It's especially problematic regarding click and drag.

Breaking this convention would make the entire UI code extremely complex for no good reason because many behaviours (click dragging specially, which is needed for drag and drop as well as scrolling) would just not work. No UI library that I am aware of works this way either.

I actually came upon this proposal (or really, the related complaints about MOUSE_FILTER_PASS) while trying to implement click+drag for panning.

Looking at some of the code for the editor, that feature is handled by a RefCounted ViewPanner class, owned by the parent object, that handles the panning (this class isn't exposed to users :(). This relies on most children having the pass behaviour, to bubble up to the parent the gui inputs required for dragging. This also means that things like buttons (which are set to stop) will block the dragging even though they don't react to the mouse wheel button at all, as pointed out in ghosares' proposal #8566.
It's also probably not applicable in my case, because there's some Node2Ds in between in the hierarchy, so probably the passed input would be lost.

Meanwhile, the more "intuitive" pass behaviour that sends unhandled events to nodes visually behind, disregarding hierarchy, would allow implementing click+drag in the way that I was attempting: having a Control node that's visually on top, to let it capture the dragging events, but have any other event go right through it. This'd let you drag even through buttons. Or, if you do want the buttons to be able to capture middle click - just put the Control below it, and have the buttons set to the intuitive pass, so unhandled middle mouse input goes right through them. I'm even reusing that exact same Control node, putting it over elements that can be individually dragged around, without needing to implement custom code for dragging those elements. It allows for a more "composition" based approach to dragging stuff. Of course, right now it's half-broken because input propagation doesn't work how I expected.

My point is, making it work the way users expect might break some existing conventions, but I think it has legitimate use cases. And even things that would seemingly "break" can benefit from it on the user's side. Plus, making it not rely on Control parents like the current pass, will allow the behaviour to work even in games that aren't made fully of Control nodes.

@Ariorick
Copy link

Ariorick commented Aug 29, 2024

I'd go with this combination:

  • MOUSE_FILTER_HANDLE_AND_DISCARD: Handle the event
  • MOUSE_FILTER_HANDLE_AND_PROPAGATE_UP: Handle the event and propagate it to the parent node.
  • MOUSE_FILTER_IGNORE: (or PASS) Ignore the event (as if the control was hidden).
  • MOUSE_FILTER_DISCARD: Do not handle and block event from propagating (how IGNORE works right now)

I come from Android development, and if nothing clickable is in the way, I'd prefer to pass the event to all the Controls, that are UNDER mouse event position instead of passing it up to the parents. I would REALLY like to have

  • MOUSE_FILTER_HANDLE_AND_PASS: Handle the event and let underlying control handle it next

^ WIth these names, it's obvious when Control handles the event and what happens to it next ^

An example of how not having real ignore option is a problem for me:
I'd like to create a separate scene using the anchors_preset in the root Control set to Full Rect and positioning elements with Top Left, Top Right anchors. However, this setup causes any mouse events to be consumed by the root Control, preventing them from reaching underlying controls. (Any buttons under this control are not clickable, for example)

I think that it's total nonsense, that filter named IGNORE doesn't let events through and there is no mode to actually ignore any mouse events. I came here quite annoyed at Godot

@Ariorick
Copy link

Ariorick commented Aug 29, 2024

@kitbdev

I could not find any event systems that send gui mouse events to a Control visually behind them in a controlled way (not just a bounds test).

This is how it works in Android, and it's analog of mouse_filter is bool clickable. System will check every view that intersects with click position front to back. View will consume the event if clickable or let other views handle it otherwise.

I don't see any reason why transparent Control in Godot can't have a mouse_filter that allows it to just not interact with the mouse in any way

@Ariorick
Copy link

Total rename is probably for 5.0, but in 4.x we could just add MOUSE_FILTER_SKIP.
@reduz Should I (or you) create a separate proposal for this? Current proposal has break_compat label and probably won't be addressed for decades

@kitbdev
Copy link

kitbdev commented Aug 29, 2024

@Ariorick
MOUSE_FILTER_IGNORE does ignore mouse events and allows the control behind it to get the event. It does not discard the event. If you are going based on the information in the OP, it is incorrect. See https://docs.godotengine.org/en/latest/classes/class_control.html#class-control-constant-mouse-filter-ignore for up to date info.

The part about Android input handling is interesting, I'll look into it.
You can make a separate proposal about a new MOUSE_FILTER_HANDLE_AND_PASS if you wish, that is what most comments here are about. Other proposals have mostly been out of confusion so more implementation details and concrete reasons for it would be helpful (#8566, #7167, godotengine/godot#55432, etc).

@Ariorick
Copy link

Ariorick commented Aug 29, 2024

@kitbdev yep turns out IGNORE does ignore clicks... I thought I tested it, but something went wrong, I guess, and docs are also weird about it

MOUSE_FILTER_IGNORE
The control will also not receive the mouse_entered nor mouse_exited signals. This will not block other controls from receiving these events or firing the signals. Ignored events will not be handled automatically.
https://docs.godotengine.org/en/stable/classes/class_control.html#enum-control-mousefilter

Is this an error in docs? Or what

@Ariorick
Copy link

WHAT
I MISSED THE "NOT" in the docs
I guess it's time to sleep

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
breaks compat Proposal will inevitably break compatibility topic:gui topic:input
Projects
None yet
Development

No branches or pull requests