bevy_input_focus/
gained_and_lost.rs1use super::InputFocus;
5use bevy_ecs::prelude::*;
6#[cfg(feature = "bevy_reflect")]
7use bevy_reflect::Reflect;
8
9#[derive(PartialEq, Eq, Debug, Clone, Copy)]
15#[cfg_attr(feature = "bevy_reflect", derive(Reflect))]
16pub enum FocusCause {
17 Navigated,
19
20 Pressed,
24
25 Auto,
27}
28
29#[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 pub entity: Entity,
42 pub cause: FocusCause,
44}
45
46#[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 pub entity: Entity,
59}
60
61pub fn process_recorded_focus_changes(mut focus: ResMut<InputFocus>, mut commands: Commands) {
65 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 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 #[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 app.update();
142
143 app
144 }
145
146 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 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 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 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 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 app.world_mut().resource_mut::<InputFocus>().clear();
325 app.world_mut().entity_mut(entity).despawn();
326 app.update();
327
328 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}