Skip to main content

bevy_input_focus/
gained_and_lost.rs

1//! Contains [`FocusGained`] and [`FocusLost`] events,
2//! as well as [`process_recorded_focus_changes`] to send them when the focused entity changes.
3
4use super::InputFocus;
5use bevy_ecs::prelude::*;
6#[cfg(feature = "bevy_reflect")]
7use bevy_reflect::Reflect;
8
9/// The cause for a [`FocusGained`]
10///
11/// Sometimes widgets would like to know how their focus was gained so they can act accordingly.
12///
13/// For example, a text input may want to select all text when navigated into, but not when pressed.
14#[derive(PartialEq, Eq, Debug, Clone, Copy)]
15#[cfg_attr(feature = "bevy_reflect", derive(Reflect))]
16pub enum FocusCause {
17    /// The input was navigated into by the keyboard, gamepad, or default behavior when unknown.
18    Navigated,
19
20    /// The input was pressed into with the mouse or touchpad
21    ///
22    /// This is only sent for primary mouse presses. Focus gained from other mouse buttons or gestures will be `Navigated`.
23    Pressed,
24}
25
26/// An [`EntityEvent`] that is sent when an entity gains [`InputFocus`].
27///
28/// This event bubbles up the entity hierarchy, so if a child entity gains focus, its parents will also receive this event.
29#[derive(EntityEvent, Debug, Clone)]
30#[entity_event(auto_propagate)]
31#[cfg_attr(
32    feature = "bevy_reflect",
33    derive(Reflect),
34    reflect(Event, Debug, Clone)
35)]
36pub struct FocusGained {
37    /// The entity that gained focus
38    pub entity: Entity,
39    /// What caused this focus
40    pub cause: FocusCause,
41}
42
43/// An [`EntityEvent`] that is sent when an entity loses [`InputFocus`].
44///
45/// This event bubbles up the entity hierarchy, so if a child entity loses focus, its parents will also receive this event.
46#[derive(EntityEvent, Debug, Clone)]
47#[entity_event(auto_propagate)]
48#[cfg_attr(
49    feature = "bevy_reflect",
50    derive(Reflect),
51    reflect(Event, Debug, Clone)
52)]
53pub struct FocusLost {
54    /// The entity that lost focus.
55    pub entity: Entity,
56}
57
58/// Reads the recorded focus changes from the [`InputFocus`] resource and sends the appropriate [`FocusGained`] and [`FocusLost`] events.
59///
60/// This system is part of [`InputFocusPlugin`](super::InputFocusPlugin).
61pub fn process_recorded_focus_changes(mut focus: ResMut<InputFocus>, mut commands: Commands) {
62    // This function does not actually mutate the `focus.current_focus`, which is
63    // what is exposed to the user via `InputFocus::get`. Other fields are not exposed.
64    // So, we `bypass_change_detection` when accessing `focus` to avoid false signaling
65    // that we changed the `current_focus`. That is what users would care about if
66    // they were to be checking `focus.is_changed()`.
67
68    // We need to track the previous focus as we go,
69    // so we can send the correct FocusLost events when focus changes.
70    let mut previous_focus = focus.original_focus;
71    for change in focus.bypass_change_detection().recorded_changes.drain(..) {
72        let changed_ent = {
73            if let Some((changed_ent, _cause)) = change {
74                Some(changed_ent)
75            } else {
76                None
77            }
78        };
79        // Only send focus change events if the focused entity actually changed.
80        if changed_ent == previous_focus {
81            continue;
82        }
83        match change {
84            Some((new_focus, cause)) => {
85                if let Some(old_focus) = previous_focus {
86                    commands.trigger(FocusLost { entity: old_focus });
87                }
88                commands.trigger(FocusGained {
89                    entity: new_focus,
90                    cause,
91                });
92                previous_focus = Some(new_focus);
93            }
94            None => {
95                if let Some(old_focus) = previous_focus {
96                    commands.trigger(FocusLost { entity: old_focus });
97                }
98                previous_focus = None;
99            }
100        }
101    }
102
103    focus.bypass_change_detection().original_focus = focus.current_focus;
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109    use alloc::vec;
110    use alloc::vec::Vec;
111    use bevy_app::App;
112    use bevy_ecs::observer::On;
113    use bevy_input::InputPlugin;
114
115    /// Tracks the sequence of [`FocusGained`] and [`FocusLost`] events for assertions.
116    #[derive(Debug, Clone, PartialEq)]
117    enum FocusEvent {
118        Gained(Entity),
119        Lost(Entity),
120    }
121
122    #[derive(Resource, Default)]
123    struct FocusEventLog(Vec<FocusEvent>);
124
125    fn setup_app() -> App {
126        let mut app = App::new();
127        app.add_plugins((InputPlugin, super::super::InputFocusPlugin));
128        app.init_resource::<FocusEventLog>();
129
130        app.add_observer(|trigger: On<FocusGained>, mut log: ResMut<FocusEventLog>| {
131            log.0.push(FocusEvent::Gained(trigger.entity));
132        });
133        app.add_observer(|trigger: On<FocusLost>, mut log: ResMut<FocusEventLog>| {
134            log.0.push(FocusEvent::Lost(trigger.entity));
135        });
136
137        // Run once to finish startup
138        app.update();
139
140        app
141    }
142
143    // Convenience method to extract and clear the log values for assertions
144    fn take_log(app: &mut App) -> Vec<FocusEvent> {
145        core::mem::take(&mut app.world_mut().resource_mut::<FocusEventLog>().0)
146    }
147
148    #[test]
149    fn no_changes_no_events() {
150        let mut app = setup_app();
151
152        app.update();
153        assert!(take_log(&mut app).is_empty());
154    }
155
156    #[test]
157    fn gain_focus_from_none() {
158        let mut app = setup_app();
159
160        let entity = app.world_mut().spawn_empty().id();
161        app.world_mut()
162            .resource_mut::<InputFocus>()
163            .set(entity, FocusCause::Navigated);
164        app.update();
165
166        assert_eq!(take_log(&mut app), vec![FocusEvent::Gained(entity)]);
167    }
168
169    #[test]
170    fn lose_focus_to_none() {
171        let mut app = setup_app();
172        let entity = app.world_mut().spawn_empty().id();
173
174        // Establish initial focus.
175        app.world_mut()
176            .resource_mut::<InputFocus>()
177            .set(entity, FocusCause::Navigated);
178        app.update();
179        take_log(&mut app);
180
181        app.world_mut().resource_mut::<InputFocus>().clear();
182        app.update();
183
184        assert_eq!(take_log(&mut app), vec![FocusEvent::Lost(entity)]);
185    }
186
187    #[test]
188    fn switch_focus_between_entities() {
189        let mut app = setup_app();
190        let a = app.world_mut().spawn_empty().id();
191        let b = app.world_mut().spawn_empty().id();
192
193        app.world_mut()
194            .resource_mut::<InputFocus>()
195            .set(a, FocusCause::Navigated);
196        app.update();
197        take_log(&mut app);
198
199        app.world_mut()
200            .resource_mut::<InputFocus>()
201            .set(b, FocusCause::Navigated);
202        app.update();
203
204        assert_eq!(
205            take_log(&mut app),
206            vec![FocusEvent::Lost(a), FocusEvent::Gained(b)]
207        );
208    }
209
210    #[test]
211    fn multiple_changes_in_single_frame() {
212        let mut app = setup_app();
213        take_log(&mut app);
214
215        let a = app.world_mut().spawn_empty().id();
216        let b = app.world_mut().spawn_empty().id();
217        let c = app.world_mut().spawn_empty().id();
218
219        let mut focus = app.world_mut().resource_mut::<InputFocus>();
220        focus.set(a, FocusCause::Navigated);
221        focus.set(b, FocusCause::Navigated);
222        focus.clear();
223        focus.set(c, FocusCause::Navigated);
224
225        app.update();
226
227        assert_eq!(
228            take_log(&mut app),
229            vec![
230                FocusEvent::Gained(a),
231                FocusEvent::Lost(a),
232                FocusEvent::Gained(b),
233                FocusEvent::Lost(b),
234                FocusEvent::Gained(c),
235            ]
236        );
237    }
238
239    #[test]
240    fn clear_when_already_none() {
241        let mut app = setup_app();
242        take_log(&mut app);
243
244        app.world_mut().resource_mut::<InputFocus>().clear();
245        app.update();
246
247        // No entity was focused, so no FocusLost should fire.
248        assert!(take_log(&mut app).is_empty());
249    }
250
251    #[test]
252    fn double_clear() {
253        let mut app = setup_app();
254        let entity = app.world_mut().spawn_empty().id();
255
256        app.world_mut()
257            .resource_mut::<InputFocus>()
258            .set(entity, FocusCause::Navigated);
259        app.update();
260        take_log(&mut app);
261
262        // Clear twice — only one FocusLost should fire (the second clear has no previous focus).
263        let mut focus = app.world_mut().resource_mut::<InputFocus>();
264        focus.clear();
265        focus.clear();
266        app.update();
267
268        assert_eq!(take_log(&mut app), vec![FocusEvent::Lost(entity)]);
269    }
270
271    #[test]
272    fn events_propagate_to_parent() {
273        let mut app = setup_app();
274        take_log(&mut app);
275
276        let child = app.world_mut().spawn_empty().id();
277        let parent = app.world_mut().spawn_empty().add_child(child).id();
278
279        app.world_mut()
280            .resource_mut::<InputFocus>()
281            .set(child, FocusCause::Navigated);
282        app.update();
283
284        // The event fires on the child, then bubbles to the parent.
285        let log = take_log(&mut app);
286        assert!(
287            log.contains(&FocusEvent::Gained(child)),
288            "child should receive FocusGained"
289        );
290        assert!(
291            log.contains(&FocusEvent::Gained(parent)),
292            "parent should receive FocusGained via propagation"
293        );
294
295        app.world_mut().resource_mut::<InputFocus>().clear();
296        app.update();
297
298        let log = take_log(&mut app);
299        assert!(
300            log.contains(&FocusEvent::Lost(child)),
301            "child should receive FocusLost"
302        );
303        assert!(
304            log.contains(&FocusEvent::Lost(parent)),
305            "parent should receive FocusLost via propagation"
306        );
307    }
308
309    #[test]
310    fn focus_lost_on_despawned_entity() {
311        let mut app = setup_app();
312        let entity = app.world_mut().spawn_empty().id();
313
314        app.world_mut()
315            .resource_mut::<InputFocus>()
316            .set(entity, FocusCause::Navigated);
317        app.update();
318        take_log(&mut app);
319
320        // Record a focus change away from the entity, then despawn it before processing.
321        app.world_mut().resource_mut::<InputFocus>().clear();
322        app.world_mut().entity_mut(entity).despawn();
323        app.update();
324
325        // FocusLost should still fire (and not panic).
326        let log = take_log(&mut app);
327        assert_eq!(log, vec![FocusEvent::Lost(entity)]);
328    }
329
330    #[test]
331    fn from_entity_fires_gained_event() {
332        let mut app = setup_app();
333        take_log(&mut app);
334
335        let entity = app.world_mut().spawn_empty().id();
336        app.world_mut()
337            .insert_resource(InputFocus::from_entity(entity));
338        app.update();
339
340        let log = take_log(&mut app);
341        assert!(
342            log.contains(&FocusEvent::Gained(entity)),
343            "from_entity should record a change that fires FocusGained"
344        );
345    }
346}