Add FocusGained and FocusLost entity events#23723
Open
alice-i-cecile wants to merge 5 commits intobevyengine:mainfrom
Open
Add FocusGained and FocusLost entity events#23723alice-i-cecile wants to merge 5 commits intobevyengine:mainfrom
FocusGained and FocusLost entity events#23723alice-i-cecile wants to merge 5 commits intobevyengine:mainfrom
Conversation
alice-i-cecile
commented
Apr 8, 2026
_release-content/migration-guides/input_focus_setting_getting.md
Outdated
Show resolved
Hide resolved
Member
Author
|
I'm adding this to the 0.19 milestone as I think it will be extremely useful for building UI widgets in earnest, but we can cut it if needed. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Objective
When working with more complex widgets, users have repeatedly asked for a way to reliably detect when an entity gains or loses focus. They have not, however, filed an issue about this 🎟️
These are called
focus/focusin+blur/focusouton the web. The former element in these pairs does not bubble, while the latter does.Because Bevy's observer infrastructure allows you to get the original target of a bubbled event, we can create only bubbling variations.
@viridia has suggested that #23707 would benefit from a form of this feature.
Rejected designs
The simplest approach would be to simply add a
previously_focusedfield onInputFocus, and then emit events based on the difference using an ordinary system. Unfortunately, this has a serious flaw: events are lost if this changes state multiple times in the same frame.We can resolve this problem by completely locking down access, and requiring commands or events to be used to change the input focus. This ensures no changes are lost and sends them off semi-immediately, but is a major breaking change to this API and prevents immediate-mode checks of "what is the current input focus".
Solution
We can do better. If we sacrifice "FocusGained and Lost must be emitted immediately", we can track changes inside of
InputFocus, before sending them off in an ordinary system.This is minimally breaking (you have to use the getters/setters now), and ensures no gained/lost events are ever missed. Users who completely overwrite
InputFocus(e.g. by usingfrom_entity) will miss changes, but frankly, you deserve it if you ignore the nice setters and clear warnings in the docs.FocusGainedandFocusLostevents. Split to their own file for cleanliness.InputFocus, forcing users to always go through the existing getters and setters.InputFocusto track changes as they have been made.PostUpdate, converting them intoFocusGainedandFocusLost.PostUpdatethat this needed a relative ordering for.InputFocusPluginto store this new system, stealing some of the setup that was previously inInputDispatchPlugin. Somewhat incidentally, this fixes Dependency coupling between widgets and input focus #19057, by selecting option 1.Testing
I was not very confident that my implementation of this logic was correct, so I wrote a rather aggressive set of mid-level tests, using
App.They pass, so apparently my first implementation was actually good enough.