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