Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add event for clicks #10141

Closed
wants to merge 38 commits into from
Closed
Show file tree
Hide file tree
Changes from 36 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
d7c1113
Add API to support publishing click events for UI nodes
Jul 22, 2023
ae0e726
Refactor game_menu example to use Click events
Jul 22, 2023
b63703f
Add LastInteraction into prelude
Jul 22, 2023
615db07
Refactor for LastInteraction In ButtonBundle
Jul 23, 2023
8db5137
Fix PreUpdate systems call order
Jul 24, 2023
bdd29aa
Revert to old name for menu_action
Jul 24, 2023
396d95f
Extend state example to use Click events
Jul 24, 2023
7169033
Remove EventReader<Click> for simpler add_systems call
Jul 24, 2023
68d8f7a
Fix name error for function calls
Jul 24, 2023
7805a07
Combine Update add systems calls
Jul 24, 2023
b62bab1
For reals combine all add_systems calls run on Update
Jul 24, 2023
47b3226
Move add order for consistency
Jul 24, 2023
bb0a31d
Refactor display & visibility example for click events
Sep 2, 2023
020baca
Merge branch 'main' into add-event-for-clicks
Sep 2, 2023
9134ca3
Fix game menu example for changed event reader iterator
Sep 2, 2023
f07e153
Fix event reader iterator call on missed example
Sep 2, 2023
0729791
Minor grammar and syntax changes
Sep 2, 2023
898d202
Refactor size constraints example for click event
Sep 2, 2023
5174150
Remove ButtonActivatedEvent, replace with Click
Sep 2, 2023
bf4655c
Fix failing doc test
Sep 2, 2023
8dd6901
Change scope of event writer in an attempt to fix doc test
Sep 3, 2023
6842ccf
Simplify send of click event
Sep 3, 2023
15abb9c
Remove LastInteraction struct
Sep 3, 2023
94c39d9
Remove unused import
Sep 3, 2023
589d9d0
Simplify tuple unpacking for click events in examples
Sep 3, 2023
86961a3
Change tuple packing back
Sep 3, 2023
95d34b5
Move the click send to incorporate hover
Sep 3, 2023
ad556b2
Amend click event comment
Sep 4, 2023
5f12182
Change Click -> Clicked
Sep 6, 2023
198542a
Refactor iteration to old method
Sep 6, 2023
9f92e48
Fix click typo
Sep 6, 2023
4c9b271
Remove old comment
Sep 7, 2023
84f88a4
Merge remote-tracking branch 'origin/main' into add-event-for-clicks
Shatur Oct 16, 2023
8bb6d3b
Update crates/bevy_ui/src/focus.rs
Shatur Oct 17, 2023
1b5a278
Register click only if the button was pressed before
Shatur Oct 21, 2023
c53a16f
Add import
Shatur Oct 21, 2023
48ad80c
Fix doc link
Shatur Oct 21, 2023
981890a
Update size constraints example
Shatur Oct 21, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion crates/bevy_ui/src/focus.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ use bevy_derive::{Deref, DerefMut};
use bevy_ecs::{
change_detection::DetectChangesMut,
entity::Entity,
prelude::{Component, With},
event::EventWriter,
prelude::{Component, Event, With},
query::WorldQuery,
reflect::ReflectComponent,
system::{Local, Query, Res},
Expand Down Expand Up @@ -55,6 +56,15 @@ impl Default for Interaction {
}
}

/// Used to publish which entity was clicked
///
/// Commonly used by creating a UI node and using [`EventReader<Clicked>`](event::EventReader)
/// to obtain the list of clicked UI nodes.
///
/// **Note:** This captures the full click/press-release action, i.e. it is emitted after release.
#[derive(Event)]
pub struct Clicked(pub Entity);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure "clicked" conveys the right meaning, as the name doesn't imply it's on release. The Web API's click event is only emitted on release just like you implemented it here, though.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think clicks in any UI always works this way. But won't hurt to mention in the docs, of course!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, "click" is the right terminology here. We have "pressed" as a one-phase event, "just pressed" as a two-phase event, and "clicked" as a three-phase event.


/// A component storing the position of the mouse relative to the node, (0., 0.) being the top-left corner and (1., 1.) being the bottom-right
/// If the mouse is not over the node, the value will go beyond the range of (0., 0.) to (1., 1.)
/// A None value means that the cursor position is unknown.
Expand Down Expand Up @@ -142,6 +152,7 @@ pub fn ui_focus_system(
ui_stack: Res<UiStack>,
mut node_query: Query<NodeQuery>,
primary_window: Query<Entity, With<PrimaryWindow>>,
mut click_events: EventWriter<Clicked>,
) {
let primary_window = primary_window.iter().next();

Expand All @@ -152,12 +163,14 @@ pub fn ui_focus_system(
}
}

let mut was_pressed = false;
let mouse_released =
mouse_button_input.just_released(MouseButton::Left) || touches_input.any_just_released();
if mouse_released {
for node in &mut node_query {
if let Some(mut interaction) = node.interaction {
if *interaction == Interaction::Pressed {
was_pressed = true;
*interaction = Interaction::None;
}
}
Expand Down Expand Up @@ -245,6 +258,10 @@ pub fn ui_focus_system(
}

if contains_cursor {
// Emit a click event only if mouse was released under the button
if mouse_released && was_pressed {
click_events.send(Clicked(*entity));
}
Some(*entity)
} else {
if let Some(mut interaction) = node.interaction {
Expand Down
3 changes: 2 additions & 1 deletion crates/bevy_ui/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ pub mod prelude {
#[doc(hidden)]
pub use crate::{
camera_config::*, geometry::*, node_bundles::*, ui_node::*, widget::Button, widget::Label,
Interaction, UiScale,
Clicked, Interaction, UiScale,
};
}

Expand Down Expand Up @@ -129,6 +129,7 @@ impl Plugin for UiPlugin {
.register_type::<widget::Label>()
.register_type::<ZIndex>()
.register_type::<Outline>()
.add_event::<Clicked>()
.add_systems(
PreUpdate,
ui_focus_system.in_set(UiSystem::Focus).after(InputSystem),
Expand Down
17 changes: 10 additions & 7 deletions examples/ecs/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,15 @@ fn main() {
// All systems from the exit schedule of the state we're leaving are run first,
// and then all systems from the enter schedule of the state we're entering are run second.
.add_systems(OnEnter(AppState::Menu), setup_menu)
// By contrast, update systems are stored in the `Update` schedule. They simply
// check the value of the `State<T>` resource to see if they should run each frame.
.add_systems(Update, menu.run_if(in_state(AppState::Menu)))
.add_systems(OnExit(AppState::Menu), cleanup_menu)
.add_systems(OnEnter(AppState::InGame), setup_game)
.add_systems(
Update,
(movement, change_color).run_if(in_state(AppState::InGame)),
(
menu_interaction.run_if(in_state(AppState::Menu)),
menu_action.run_if(on_event::<Clicked>()),
(movement, change_color).run_if(in_state(AppState::InGame)),
),
)
.run();
}
Expand Down Expand Up @@ -95,8 +96,7 @@ fn setup_menu(mut commands: Commands) {
commands.insert_resource(MenuData { button_entity });
}

fn menu(
mut next_state: ResMut<NextState<AppState>>,
fn menu_interaction(
mut interaction_query: Query<
(&Interaction, &mut BackgroundColor),
(Changed<Interaction>, With<Button>),
Expand All @@ -106,7 +106,6 @@ fn menu(
match *interaction {
Interaction::Pressed => {
*color = PRESSED_BUTTON.into();
next_state.set(AppState::InGame);
}
Interaction::Hovered => {
*color = HOVERED_BUTTON.into();
Expand All @@ -118,6 +117,10 @@ fn menu(
}
}

fn menu_action(mut next_state: ResMut<NextState<AppState>>) {
next_state.set(AppState::InGame);
}

fn cleanup_menu(mut commands: Commands, menu_data: Res<MenuData>) {
commands.entity(menu_data.button_entity).despawn_recursive();
}
Expand Down
12 changes: 5 additions & 7 deletions examples/games/game_menu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -793,17 +793,15 @@ mod menu {
}

fn menu_action(
interaction_query: Query<
(&Interaction, &MenuButtonAction),
(Changed<Interaction>, With<Button>),
>,
mut click_events: EventReader<Clicked>,
menu_buttons: Query<&MenuButtonAction>,
mut app_exit_events: EventWriter<AppExit>,
mut menu_state: ResMut<NextState<MenuState>>,
mut game_state: ResMut<NextState<GameState>>,
) {
for (interaction, menu_button_action) in &interaction_query {
if *interaction == Interaction::Pressed {
match menu_button_action {
for event in click_events.read() {
if let Ok(menu_button) = menu_buttons.get(event.0) {
match menu_button {
MenuButtonAction::Quit => app_exit_events.send(AppExit),
MenuButtonAction::Play => {
game_state.set(GameState::Game);
Expand Down
8 changes: 5 additions & 3 deletions examples/ui/display_and_visibility.rs
Original file line number Diff line number Diff line change
Expand Up @@ -431,16 +431,18 @@ where
}

fn buttons_handler<T>(
mut click_events: EventReader<Clicked>,
visibility_button_query: Query<(&Target<T>, &Children)>,
mut left_panel_query: Query<&mut <Target<T> as TargetUpdate>::TargetComponent>,
mut visibility_button_query: Query<(&Target<T>, &Interaction, &Children), Changed<Interaction>>,
mut text_query: Query<&mut Text>,
) where
T: Send + Sync,
Target<T>: TargetUpdate + Component,
{
for (target, interaction, children) in visibility_button_query.iter_mut() {
if matches!(interaction, Interaction::Pressed) {
for event in click_events.read() {
if let Ok((target, children)) = visibility_button_query.get(event.0) {
let mut target_value = left_panel_query.get_mut(target.id).unwrap();

for &child in children {
if let Ok(mut text) = text_query.get_mut(child) {
text.sections[0].value = target.update_target(target_value.as_mut());
Expand Down
87 changes: 40 additions & 47 deletions examples/ui/size_constraints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ use bevy::prelude::*;
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_event::<ButtonActivatedEvent>()
.add_systems(Startup, setup)
.add_systems(Update, (update_buttons, update_radio_buttons_colors))
.run();
Expand Down Expand Up @@ -35,9 +34,6 @@ enum Constraint {
#[derive(Copy, Clone, Component)]
struct ButtonValue(Val);

#[derive(Event)]
struct ButtonActivatedEvent(Entity);

fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
// ui camera
commands.spawn(Camera2dBundle::default());
Expand Down Expand Up @@ -302,28 +298,24 @@ fn update_buttons(
mut bar_query: Query<&mut Style, With<Bar>>,
mut text_query: Query<&mut Text>,
children_query: Query<&Children>,
mut button_activated_event: EventWriter<ButtonActivatedEvent>,
) {
let mut style = bar_query.single_mut();
for (button_id, interaction, constraint, value) in button_query.iter_mut() {
match interaction {
Interaction::Pressed => {
button_activated_event.send(ButtonActivatedEvent(button_id));
match constraint {
Constraint::FlexBasis => {
style.flex_basis = value.0;
}
Constraint::Width => {
style.width = value.0;
}
Constraint::MinWidth => {
style.min_width = value.0;
}
Constraint::MaxWidth => {
style.max_width = value.0;
}
Interaction::Pressed => match constraint {
Constraint::FlexBasis => {
style.flex_basis = value.0;
Shatur marked this conversation as resolved.
Show resolved Hide resolved
}
}
Constraint::Width => {
style.width = value.0;
}
Constraint::MinWidth => {
style.min_width = value.0;
}
Constraint::MaxWidth => {
style.max_width = value.0;
}
},
Interaction::Hovered => {
if let Ok(children) = children_query.get(button_id) {
for &child in children {
Expand Down Expand Up @@ -359,38 +351,39 @@ fn update_buttons(
}

fn update_radio_buttons_colors(
mut event_reader: EventReader<ButtonActivatedEvent>,
mut click_events: EventReader<Clicked>,
button_query: Query<(Entity, &Constraint, &Interaction)>,
mut color_query: Query<&mut BackgroundColor>,
mut text_query: Query<&mut Text>,
children_query: Query<&Children>,
) {
for &ButtonActivatedEvent(button_id) in event_reader.read() {
let target_constraint = button_query.get_component::<Constraint>(button_id).unwrap();
for (id, constraint, interaction) in button_query.iter() {
if target_constraint == constraint {
let (border_color, inner_color, text_color) = if id == button_id {
(ACTIVE_BORDER_COLOR, ACTIVE_INNER_COLOR, ACTIVE_TEXT_COLOR)
} else {
(
INACTIVE_BORDER_COLOR,
INACTIVE_INNER_COLOR,
if matches!(interaction, Interaction::Hovered) {
HOVERED_TEXT_COLOR
} else {
UNHOVERED_TEXT_COLOR
},
)
};
for &Clicked(button_id) in click_events.read() {
if let Ok(target_constraint) = button_query.get_component::<Constraint>(button_id) {
Shatur marked this conversation as resolved.
Show resolved Hide resolved
for (id, constraint, interaction) in button_query.iter() {
if target_constraint == constraint {
let (border_color, inner_color, text_color) = if id == button_id {
(ACTIVE_BORDER_COLOR, ACTIVE_INNER_COLOR, ACTIVE_TEXT_COLOR)
} else {
(
INACTIVE_BORDER_COLOR,
INACTIVE_INNER_COLOR,
if matches!(interaction, Interaction::Hovered) {
HOVERED_TEXT_COLOR
} else {
UNHOVERED_TEXT_COLOR
},
)
};

color_query.get_mut(id).unwrap().0 = border_color;
if let Ok(children) = children_query.get(id) {
for &child in children {
color_query.get_mut(child).unwrap().0 = inner_color;
if let Ok(grand_children) = children_query.get(child) {
for &grandchild in grand_children {
if let Ok(mut text) = text_query.get_mut(grandchild) {
text.sections[0].style.color = text_color;
color_query.get_mut(id).unwrap().0 = border_color;
if let Ok(children) = children_query.get(id) {
for &child in children {
color_query.get_mut(child).unwrap().0 = inner_color;
if let Ok(grand_children) = children_query.get(child) {
for &grandchild in grand_children {
if let Ok(mut text) = text_query.get_mut(grandchild) {
text.sections[0].style.color = text_color;
}
}
}
}
Expand Down
Loading