bevy_immediate/ui/
activated.rs

1use bevy_ecs::{
2    entity::Entity,
3    lifecycle,
4    observer::On,
5    query::Has,
6    system::{Commands, Query, ResMut},
7};
8use bevy_input::{
9    ButtonState,
10    keyboard::{KeyCode, KeyboardInput},
11};
12use bevy_input_focus::FocusedInput;
13use bevy_picking::events::{Click, Pointer};
14use bevy_platform::collections::HashSet;
15use bevy_ui::InteractionDisabled;
16
17use crate::{CapSet, ImmCapAccessRequests, ImmCapability, ImmEntity, ImplCap};
18
19/// Immediate mode capability for `.clicked()`
20#[derive(Clone, Copy, PartialEq, Eq, Hash)]
21pub struct CapabilityUiActivated;
22
23impl ImmCapability for CapabilityUiActivated {
24    fn build<CM: CapSet>(app: &mut bevy_app::App, cap_req: &mut ImmCapAccessRequests<CM>) {
25        if !app.is_plugin_added::<TrackClickedPlugin>() {
26            app.add_plugins(TrackClickedPlugin);
27        }
28
29        cap_req.request_component_read::<TrackActivated>(app.world_mut());
30        cap_req.request_resource_read::<TrackActivetedEntitiesResource>(app.world_mut());
31    }
32}
33
34/// Implements support for `.clicked()`
35pub trait ImmUiActivated {
36    /// Was button activated
37    fn activated(&mut self) -> bool;
38}
39
40impl<Cap: CapSet> ImmUiActivated for ImmEntity<'_, '_, '_, Cap>
41where
42    Cap: ImplCap<CapabilityUiActivated>,
43{
44    fn activated(&mut self) -> bool {
45        'correct: {
46            if !self.cap_entity_contains::<TrackActivated>() {
47                break 'correct;
48            }
49
50            let activated = self
51                .cap_get_resource::<TrackActivetedEntitiesResource>()
52                .expect("Capability should be available")
53                .into_inner()
54                .activated
55                .contains(&self.entity());
56            return activated;
57        }
58
59        // Fallback
60        self.entity_commands().insert_if_new(TrackActivated);
61        false
62    }
63}
64
65////////////////////////////////////////////////////////////////////////////////
66
67/// Add click tracking related logic
68pub struct TrackClickedPlugin;
69
70impl bevy_app::Plugin for TrackClickedPlugin {
71    fn build(&self, app: &mut bevy_app::App) {
72        app.insert_resource(TrackActivetedEntitiesResource::default());
73        app.add_systems(bevy_app::First, reset_activated);
74        app.add_observer(track_clicked_insert);
75    }
76}
77
78// Insert on_click picking observer only once
79fn track_clicked_insert(trigger: On<lifecycle::Add, TrackActivated>, mut commands: Commands) {
80    let entity = trigger.event().entity;
81    commands
82        .entity(entity)
83        .observe(button_on_key_event)
84        .observe(button_on_pointer_click);
85}
86
87/// Tracks if entity has been clicked in this frame.
88#[derive(bevy_ecs::component::Component, Default)]
89#[component(storage = "SparseSet")]
90pub struct TrackActivated;
91
92#[derive(bevy_ecs::resource::Resource, Default)]
93struct TrackActivetedEntitiesResource {
94    pub activated: HashSet<Entity>,
95}
96
97fn reset_activated(mut res: ResMut<TrackActivetedEntitiesResource>) {
98    res.activated.clear();
99}
100
101// Code duplicated from
102// https://docs.rs/bevy_ui_widgets/latest/src/bevy_ui_widgets/button.rs.html#26-30
103//
104// Hopefully there will be Activated trigger in future
105
106fn button_on_key_event(
107    event: On<FocusedInput<KeyboardInput>>,
108    q_state: Query<Has<InteractionDisabled>>,
109    mut activated: ResMut<TrackActivetedEntitiesResource>,
110) {
111    if let Ok(disabled) = q_state.get(event.focused_entity)
112        && !disabled
113    {
114        let input_event = &event.input;
115        if !input_event.repeat
116            && input_event.state == ButtonState::Pressed
117            && (input_event.key_code == KeyCode::Enter || input_event.key_code == KeyCode::Space)
118        {
119            activated.activated.insert(event.focused_entity);
120        }
121    }
122}
123
124fn button_on_pointer_click(
125    mut click: On<Pointer<Click>>,
126    mut q_state: Query<Has<InteractionDisabled>>,
127    mut activated: ResMut<TrackActivetedEntitiesResource>,
128) {
129    // WARN: Check for pressed removed
130    if let Ok(disabled) = q_state.get_mut(click.entity) {
131        click.propagate(false);
132        if !disabled {
133            activated.activated.insert(click.entity);
134        }
135    }
136}