Path: blob/main/crates/bevy_input_focus/src/gained_and_lost.rs
30635 views
//! Contains [`FocusGained`] and [`FocusLost`] events,1//! as well as [`process_recorded_focus_changes`] to send them when the focused entity changes.23use super::InputFocus;4use bevy_ecs::prelude::*;5#[cfg(feature = "bevy_reflect")]6use bevy_reflect::Reflect;78/// The cause for a [`FocusGained`]9///10/// Sometimes widgets would like to know how their focus was gained so they can act accordingly.11///12/// For example, a text input may want to select all text when navigated into, but not when pressed.13#[derive(PartialEq, Eq, Debug, Clone, Copy)]14#[cfg_attr(feature = "bevy_reflect", derive(Reflect))]15pub enum FocusCause {16/// The input was navigated into by the keyboard, gamepad, or default behavior when unknown.17Navigated,1819/// The input was pressed into with the mouse or touchpad20///21/// This is only sent for primary mouse presses. Focus gained from other mouse buttons or gestures will be `Navigated`.22Pressed,23}2425/// An [`EntityEvent`] that is sent when an entity gains [`InputFocus`].26///27/// This event bubbles up the entity hierarchy, so if a child entity gains focus, its parents will also receive this event.28#[derive(EntityEvent, Debug, Clone)]29#[entity_event(auto_propagate)]30pub struct FocusGained {31/// The entity that gained focus32pub entity: Entity,33/// What caused this focus34pub cause: FocusCause,35}3637/// An [`EntityEvent`] that is sent when an entity loses [`InputFocus`].38///39/// This event bubbles up the entity hierarchy, so if a child entity loses focus, its parents will also receive this event.40#[derive(EntityEvent, Debug, Clone)]41#[entity_event(auto_propagate)]42pub struct FocusLost {43/// The entity that lost focus.44pub entity: Entity,45}4647/// Reads the recorded focus changes from the [`InputFocus`] resource and sends the appropriate [`FocusGained`] and [`FocusLost`] events.48///49/// This system is part of [`InputFocusPlugin`](super::InputFocusPlugin).50pub fn process_recorded_focus_changes(mut focus: ResMut<InputFocus>, mut commands: Commands) {51// This function does not actually mutate the `focus.current_focus`, which is52// what is exposed to the user via `InputFocus::get`. Other fields are not exposed.53// So, we `bypass_change_detection` when accessing `focus` to avoid false signaling54// that we changed the `current_focus`. That is what users would care about if55// they were to be checking `focus.is_changed()`.5657// We need to track the previous focus as we go,58// so we can send the correct FocusLost events when focus changes.59let mut previous_focus = focus.original_focus;60for change in focus.bypass_change_detection().recorded_changes.drain(..) {61let changed_ent = {62if let Some((changed_ent, _cause)) = change {63Some(changed_ent)64} else {65None66}67};68// Only send focus change events if the focused entity actually changed.69if changed_ent == previous_focus {70continue;71}72match change {73Some((new_focus, cause)) => {74if let Some(old_focus) = previous_focus {75commands.trigger(FocusLost { entity: old_focus });76}77commands.trigger(FocusGained {78entity: new_focus,79cause,80});81previous_focus = Some(new_focus);82}83None => {84if let Some(old_focus) = previous_focus {85commands.trigger(FocusLost { entity: old_focus });86}87previous_focus = None;88}89}90}9192focus.bypass_change_detection().original_focus = focus.current_focus;93}9495#[cfg(test)]96mod tests {97use super::*;98use alloc::vec;99use alloc::vec::Vec;100use bevy_app::App;101use bevy_ecs::observer::On;102use bevy_input::InputPlugin;103104/// Tracks the sequence of [`FocusGained`] and [`FocusLost`] events for assertions.105#[derive(Debug, Clone, PartialEq)]106enum FocusEvent {107Gained(Entity),108Lost(Entity),109}110111#[derive(Resource, Default)]112struct FocusEventLog(Vec<FocusEvent>);113114fn setup_app() -> App {115let mut app = App::new();116app.add_plugins((InputPlugin, super::super::InputFocusPlugin));117app.init_resource::<FocusEventLog>();118119app.add_observer(|trigger: On<FocusGained>, mut log: ResMut<FocusEventLog>| {120log.0.push(FocusEvent::Gained(trigger.entity));121});122app.add_observer(|trigger: On<FocusLost>, mut log: ResMut<FocusEventLog>| {123log.0.push(FocusEvent::Lost(trigger.entity));124});125126// Run once to finish startup127app.update();128129app130}131132// Convenience method to extract and clear the log values for assertions133fn take_log(app: &mut App) -> Vec<FocusEvent> {134core::mem::take(&mut app.world_mut().resource_mut::<FocusEventLog>().0)135}136137#[test]138fn no_changes_no_events() {139let mut app = setup_app();140141app.update();142assert!(take_log(&mut app).is_empty());143}144145#[test]146fn gain_focus_from_none() {147let mut app = setup_app();148149let entity = app.world_mut().spawn_empty().id();150app.world_mut()151.resource_mut::<InputFocus>()152.set(entity, FocusCause::Navigated);153app.update();154155assert_eq!(take_log(&mut app), vec![FocusEvent::Gained(entity)]);156}157158#[test]159fn lose_focus_to_none() {160let mut app = setup_app();161let entity = app.world_mut().spawn_empty().id();162163// Establish initial focus.164app.world_mut()165.resource_mut::<InputFocus>()166.set(entity, FocusCause::Navigated);167app.update();168take_log(&mut app);169170app.world_mut().resource_mut::<InputFocus>().clear();171app.update();172173assert_eq!(take_log(&mut app), vec![FocusEvent::Lost(entity)]);174}175176#[test]177fn switch_focus_between_entities() {178let mut app = setup_app();179let a = app.world_mut().spawn_empty().id();180let b = app.world_mut().spawn_empty().id();181182app.world_mut()183.resource_mut::<InputFocus>()184.set(a, FocusCause::Navigated);185app.update();186take_log(&mut app);187188app.world_mut()189.resource_mut::<InputFocus>()190.set(b, FocusCause::Navigated);191app.update();192193assert_eq!(194take_log(&mut app),195vec![FocusEvent::Lost(a), FocusEvent::Gained(b)]196);197}198199#[test]200fn multiple_changes_in_single_frame() {201let mut app = setup_app();202take_log(&mut app);203204let a = app.world_mut().spawn_empty().id();205let b = app.world_mut().spawn_empty().id();206let c = app.world_mut().spawn_empty().id();207208let mut focus = app.world_mut().resource_mut::<InputFocus>();209focus.set(a, FocusCause::Navigated);210focus.set(b, FocusCause::Navigated);211focus.clear();212focus.set(c, FocusCause::Navigated);213214app.update();215216assert_eq!(217take_log(&mut app),218vec![219FocusEvent::Gained(a),220FocusEvent::Lost(a),221FocusEvent::Gained(b),222FocusEvent::Lost(b),223FocusEvent::Gained(c),224]225);226}227228#[test]229fn clear_when_already_none() {230let mut app = setup_app();231take_log(&mut app);232233app.world_mut().resource_mut::<InputFocus>().clear();234app.update();235236// No entity was focused, so no FocusLost should fire.237assert!(take_log(&mut app).is_empty());238}239240#[test]241fn double_clear() {242let mut app = setup_app();243let entity = app.world_mut().spawn_empty().id();244245app.world_mut()246.resource_mut::<InputFocus>()247.set(entity, FocusCause::Navigated);248app.update();249take_log(&mut app);250251// Clear twice — only one FocusLost should fire (the second clear has no previous focus).252let mut focus = app.world_mut().resource_mut::<InputFocus>();253focus.clear();254focus.clear();255app.update();256257assert_eq!(take_log(&mut app), vec![FocusEvent::Lost(entity)]);258}259260#[test]261fn events_propagate_to_parent() {262let mut app = setup_app();263take_log(&mut app);264265let child = app.world_mut().spawn_empty().id();266let parent = app.world_mut().spawn_empty().add_child(child).id();267268app.world_mut()269.resource_mut::<InputFocus>()270.set(child, FocusCause::Navigated);271app.update();272273// The event fires on the child, then bubbles to the parent.274let log = take_log(&mut app);275assert!(276log.contains(&FocusEvent::Gained(child)),277"child should receive FocusGained"278);279assert!(280log.contains(&FocusEvent::Gained(parent)),281"parent should receive FocusGained via propagation"282);283284app.world_mut().resource_mut::<InputFocus>().clear();285app.update();286287let log = take_log(&mut app);288assert!(289log.contains(&FocusEvent::Lost(child)),290"child should receive FocusLost"291);292assert!(293log.contains(&FocusEvent::Lost(parent)),294"parent should receive FocusLost via propagation"295);296}297298#[test]299fn focus_lost_on_despawned_entity() {300let mut app = setup_app();301let entity = app.world_mut().spawn_empty().id();302303app.world_mut()304.resource_mut::<InputFocus>()305.set(entity, FocusCause::Navigated);306app.update();307take_log(&mut app);308309// Record a focus change away from the entity, then despawn it before processing.310app.world_mut().resource_mut::<InputFocus>().clear();311app.world_mut().entity_mut(entity).despawn();312app.update();313314// FocusLost should still fire (and not panic).315let log = take_log(&mut app);316assert_eq!(log, vec![FocusEvent::Lost(entity)]);317}318319#[test]320fn from_entity_fires_gained_event() {321let mut app = setup_app();322take_log(&mut app);323324let entity = app.world_mut().spawn_empty().id();325app.world_mut()326.insert_resource(InputFocus::from_entity(entity));327app.update();328329let log = take_log(&mut app);330assert!(331log.contains(&FocusEvent::Gained(entity)),332"from_entity should record a change that fires FocusGained"333);334}335}336337338