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