godot-proposals
godot-proposals copied to clipboard
Rename the Mouse Filter modes and add a Skip mouse filter mode
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:
https://github.com/godotengine/godot/issues/55432 https://github.com/godotengine/godot/issues/55288 and many others.
This is currently not possible.
Additionally other proposals (such as https://github.com/godotengine/godot-proposals/issues/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
Not sure how deeply it is related, but this proposal is worth considering when deciding on the implementation: https://github.com/godotengine/godot-proposals/issues/3272 (it also has a PR ready: https://github.com/godotengine/godot/pull/53075)
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"?
@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.
@pycbouh I suppose we could have an extra mode INHERIT for this and make it default?
Okay @golddotasksquestions @pycbouh Added both your feedback in the proposal.
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?)
@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.
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.
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.
@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.
I find the current names easier to understand than the proposed ones.
How does that relate to https://github.com/godotengine/godot/pull/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.
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.
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
Ignoredoes.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
Passit handles the event and passes it to the one visually sorted next in line until one of them is set toStopor 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
IgnoreorPassmouse filter, the "walk" just carries on, but when one of the walks hits aStopmouse 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.
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
@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
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.
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.
@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)
I still find the current way it's working simpler, so no complaints from me to move it to Godot 5.
@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.
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)
@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.
@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.
https://github.com/godotengine/godot/issues/55432#issuecomment-1172924095
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.
@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.
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.
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.
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...
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?
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.