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
26#[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 pub entity: Entity,
39 pub cause: FocusCause,
41}
42
43#[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 pub entity: Entity,
56}
57
58pub fn process_recorded_focus_changes(mut focus: ResMut<InputFocus>, mut commands: Commands) {
62 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 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 #[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 app.update();
139
140 app
141 }
142
143 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 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 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 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 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 app.world_mut().resource_mut::<InputFocus>().clear();
322 app.world_mut().entity_mut(entity).despawn();
323 app.update();
324
325 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}