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

Interaction component refactor #7257

Closed
wants to merge 62 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
71056a3
made resource_scope work for non-send resources
Pietrek14 Sep 27, 2022
d607e6b
Merge branch 'bevyengine:main' into main
Pietrek14 Sep 27, 2022
83f2417
fixed the non-send resource scope tests
Pietrek14 Sep 27, 2022
6da0e41
formatting
Pietrek14 Sep 27, 2022
3500039
simplified panic in a non-send resource scope test
Pietrek14 Sep 28, 2022
d823b1b
Merge branch 'bevyengine:main' into main
Pietrek14 Sep 28, 2022
f68eb33
changed the name of non-send struct used for testing
Pietrek14 Sep 28, 2022
6ed303e
Merge branch 'bevyengine:main' into main
Pietrek14 Oct 6, 2022
23a22ae
Merge branch 'bevyengine:main' into main
Pietrek14 Jan 7, 2023
a27f8b1
Merge branch 'main' of https://github.com/bevyengine/bevy into main
Pietrek14 Jan 14, 2023
f3acce2
Merge branch 'bevyengine:main' into main
Pietrek14 Jan 15, 2023
e2d0104
Merge branch 'main' of https://github.com/Pietrek14/bevy into main
Pietrek14 Jan 18, 2023
736177e
Merge branch 'bevyengine:main' into main
Pietrek14 Jan 18, 2023
b41e200
Merge branch 'main' of https://github.com/Pietrek14/bevy into main
Pietrek14 Jan 18, 2023
6b176d5
added interaction policy
Pietrek14 Jan 18, 2023
cd4695a
added interactionpolicy to the buttonbundle
Pietrek14 Jan 18, 2023
b06d6b7
added an interactionpolicy example
Pietrek14 Jan 18, 2023
9ed9aa1
formatting
Pietrek14 Jan 18, 2023
b80c598
example pages
Pietrek14 Jan 18, 2023
2ff084c
added the backticks
Pietrek14 Jan 18, 2023
8604044
derive default for interaction policy
Pietrek14 Jan 18, 2023
c2db4f4
comment explaining the logic behind the workings of interactionpolicy
Pietrek14 Jan 18, 2023
0dd29bd
clarified the interactionpolicy example
Pietrek14 Jan 18, 2023
6c34abf
formatting
Pietrek14 Jan 18, 2023
d96afc8
clarified the interaction policy doc-comment
Pietrek14 Jan 18, 2023
fbd41c7
formatting
Pietrek14 Jan 18, 2023
c8d546c
simplified some code
Pietrek14 Jan 18, 2023
48bf944
example pages
Pietrek14 Jan 18, 2023
0f09586
formatting
Pietrek14 Jan 18, 2023
53def6c
Merge branch 'main' of https://github.com/bevyengine/bevy into intera…
Pietrek14 Jan 18, 2023
60cd02d
text fix
Pietrek14 Jan 18, 2023
b3d2544
doc comment for InteractionPolicy
Pietrek14 Jan 21, 2023
1637f9a
grammar fix
Pietrek14 Jan 21, 2023
dfe0b13
Merge branch 'main' of https://github.com/bevyengine/bevy into intera…
Pietrek14 Mar 16, 2023
a22810a
decoupled hovered from pressed
Pietrek14 Mar 31, 2023
a106115
Merge branch 'interaction-policy' of https://github.com/Pietrek14/bev…
Pietrek14 Mar 31, 2023
608da59
Merge branch 'main' of https://github.com/bevyengine/bevy into intera…
Pietrek14 Mar 31, 2023
d288b69
collapsed an if statement
Pietrek14 Mar 31, 2023
60d191d
example pages
Pietrek14 Mar 31, 2023
9e4a8b7
added some doc-comments
Pietrek14 Apr 1, 2023
84bf151
Merge branch 'main' of https://github.com/bevyengine/bevy into intera…
Pietrek14 May 1, 2023
3ded408
fix
Pietrek14 May 1, 2023
57365c7
formatting
Pietrek14 May 1, 2023
ebdb17a
fixed the size constraints example
Pietrek14 May 1, 2023
4b1a847
Merge branch 'main' of https://github.com/bevyengine/bevy into intera…
Pietrek14 May 13, 2023
af3a318
Merge branch 'main' of https://github.com/bevyengine/bevy into intera…
Pietrek14 May 18, 2023
42eedb0
Merge branch 'main' of https://github.com/bevyengine/bevy into intera…
Pietrek14 Jun 3, 2023
68aa676
example pages
Pietrek14 Jun 3, 2023
44f80d1
fixed the example
Pietrek14 Jun 3, 2023
062ed69
Merge branch 'main' of https://github.com/bevyengine/bevy into intera…
Pietrek14 Jun 27, 2023
221ce33
formatting
Pietrek14 Jun 27, 2023
cd97783
fixed the display and visibility example
Pietrek14 Jun 27, 2023
d6f5f7c
formatting again
Pietrek14 Jun 27, 2023
0936e36
fixed a doc example
Pietrek14 Jun 28, 2023
fa95eea
fixed the example again
Pietrek14 Jun 28, 2023
8c4007c
updated pressed docs
Pietrek14 Jun 28, 2023
17e5152
Merge branch 'main' of https://github.com/bevyengine/bevy into intera…
Pietrek14 Jun 29, 2023
ba242a8
Merge branch 'main' of https://github.com/bevyengine/bevy into intera…
Pietrek14 Jul 1, 2023
2c39e68
Merge branch 'main' of https://github.com/bevyengine/bevy into intera…
Pietrek14 Jul 6, 2023
90560a7
Merge branch 'main' of https://github.com/bevyengine/bevy into intera…
Pietrek14 Sep 11, 2023
c500a71
formatting
Pietrek14 Sep 11, 2023
0771c31
don't import bevy_internal
Pietrek14 Sep 11, 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
10 changes: 10 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2019,6 +2019,16 @@ description = "Illustrates how FontAtlases are populated (used to optimize text
category = "UI (User Interface)"
wasm = true

[[example]]
name = "press_policy"
path = "examples/ui/press_policy.rs"

[package.metadata.example.press_policy]
name = "Press Policy"
description = "Illustrates how the PressPolicy works with the Pressed component. Adds two buttons to the scene, one having a `Hold` press policy, and the other having a `Release` press policy."
category = "UI (User Interface)"
wasm = true

[[example]]
name = "overflow"
path = "examples/ui/overflow.rs"
Expand Down
175 changes: 126 additions & 49 deletions crates/bevy_ui/src/focus.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use bevy_ecs::{
change_detection::DetectChangesMut,
entity::Entity,
prelude::{Component, With},
query::WorldQuery,
query::{Changed, Or, WorldQuery},
reflect::ReflectComponent,
system::{Local, Query, Res},
};
Expand All @@ -18,41 +18,54 @@ use bevy_window::{PrimaryWindow, Window};
use serde::{Deserialize, Serialize};
use smallvec::SmallVec;

/// Describes what type of input interaction has occurred for a UI node.
///
/// This is commonly queried with a `Changed<Interaction>` filter.
/// Describes if a UI node has been pressed.
///
/// Updated in [`ui_focus_system`].
///
/// If a UI node has both [`Interaction`] and [`ViewVisibility`] components,
/// [`Interaction`] will always be [`Interaction::None`]
/// If a UI node has both [`Pressed`] and [`ViewVisibility`] components,
/// the node will be considered not pressed
/// when [`ViewVisibility::get()`] is false.
/// This ensures that hidden UI nodes are not interactable,
/// and do not end up stuck in an active state if hidden at the wrong time.
///
/// Note that you can also control the visibility of a node using the [`Display`](crate::ui_node::Display) property,
/// which fully collapses it during layout calculations.
#[derive(Component, Copy, Clone, Eq, PartialEq, Debug, Reflect, Serialize, Deserialize)]
#[derive(
Component, Copy, Clone, Eq, PartialEq, Debug, Default, Reflect, Serialize, Deserialize,
)]
#[reflect(Component, Serialize, Deserialize, PartialEq)]
pub enum Interaction {
/// The node has been pressed.
///
/// Note: This does not capture click/press-release action.
Pressed,
/// The node has been hovered over
Hovered,
/// Nothing has happened
None,
pub struct Pressed {
/// Is the node currently pressed
pub pressed: bool,
/// Describes whether the component should remain in the pressed state after
/// the cursor stops hovering over the node.
pub press_policy: PressPolicy,
}

impl Interaction {
const DEFAULT: Self = Self::None;
impl Pressed {
pub fn new(press_policy: PressPolicy) -> Self {
Self {
pressed: false,
press_policy,
}
}
}

impl Default for Interaction {
fn default() -> Self {
Self::DEFAULT
}
/// Describes whether the [`Pressed`] component should remain in the pressed state after
/// the cursor stops hovering over the node.
///
/// When the user clicks this node and the `PressPolicy` is set to `Hold`, the `Pressed` component will remain in the clicked state after the cursor leaves the node, until the user releases the interaction button. (the default behaviour)
///
/// If instead the `PressPolicy` is set to `Release`, the `Pressed` component will be considered not clicked
/// as soon as the cursor leaves the node, even if the user still is pressing down the interaction button.
#[derive(Copy, Clone, Default, Eq, PartialEq, Debug, Reflect, Serialize, Deserialize)]
#[reflect(Serialize, Deserialize, PartialEq)]
pub enum PressPolicy {
/// Keep the node clicked after it stopped being hovered
Pietrek14 marked this conversation as resolved.
Show resolved Hide resolved
#[default]
Hold,
/// Release the node if the cursor stops hovering
Release,
}

/// 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
Expand Down Expand Up @@ -88,6 +101,61 @@ impl RelativeCursorPosition {
}
}

/// A simplified interaction state calculated using the [`Pressed`] and [`RelativeCursorPosition`] components.
///
/// To see how to use this, see the [`InteractionStateHandler`] trait.
#[derive(Copy, Clone, Eq, PartialEq, Debug, Reflect)]
pub enum InteractionState {
None,
Hovered,
Pressed,
}

/// Simplified way to get the [`InteractionState`] of the node.
///
/// Example usage:
/// ```rust
/// use bevy_ecs::system::Query;
/// use bevy_ui::InteractionStateHandler;
/// use bevy_ui::InteractionState;
/// use bevy_ui::Pressed;
/// use bevy_ui::RelativeCursorPosition;
///
/// fn button_system(button_query: Query<(&Pressed, &RelativeCursorPosition)>) {
/// let button = button_query.single();
///
/// match button.interaction_state() {
/// InteractionState::None => (),
/// InteractionState::Hovered => {
/// println!("The button is being hovered over");
/// },
/// InteractionState::Pressed => {
/// println!("The button is being pressed");
/// },
/// }
/// }
/// ```
pub trait InteractionStateHandler {
fn interaction_state(&self) -> InteractionState;
}

impl InteractionStateHandler for (&Pressed, &RelativeCursorPosition) {
/// Get the [`InteractionState`] of the node
fn interaction_state(&self) -> InteractionState {
if self.0.pressed {
return InteractionState::Pressed;
}

if self.1.mouse_over() {
return InteractionState::Hovered;
}

InteractionState::None
}
}

pub type InteractionStateChangedFilter = Or<(Changed<Pressed>, Changed<RelativeCursorPosition>)>;

/// Describes whether the node should block interactions with lower nodes
#[derive(Component, Copy, Clone, Eq, PartialEq, Debug, Reflect, Serialize, Deserialize)]
#[reflect(Component, Serialize, Deserialize, PartialEq)]
Expand Down Expand Up @@ -121,7 +189,7 @@ pub struct NodeQuery {
entity: Entity,
node: &'static Node,
global_transform: &'static GlobalTransform,
interaction: Option<&'static mut Interaction>,
pressed_state: Option<&'static mut Pressed>,
relative_cursor_position: Option<&'static mut RelativeCursorPosition>,
focus_policy: Option<&'static FocusPolicy>,
calculated_clip: Option<&'static CalculatedClip>,
Expand All @@ -147,18 +215,18 @@ pub fn ui_focus_system(

// reset entities that were both clicked and released in the last frame
for entity in state.entities_to_reset.drain(..) {
if let Ok(mut interaction) = node_query.get_component_mut::<Interaction>(entity) {
*interaction = Interaction::None;
if let Ok(mut pressed_state) = node_query.get_component_mut::<Pressed>(entity) {
pressed_state.pressed = false;
}
}

let mouse_released =
mouse_button_input.just_released(MouseButton::Left) || touches_input.any_just_released();
if mouse_released {
for node in node_query.iter_mut() {
if let Some(mut interaction) = node.interaction {
if *interaction == Interaction::Pressed {
*interaction = Interaction::None;
if let Some(mut pressed_state) = node.pressed_state {
if pressed_state.pressed {
pressed_state.pressed = false;
}
}
}
Expand Down Expand Up @@ -207,9 +275,12 @@ pub fn ui_focus_system(
if let Some(view_visibility) = node.view_visibility {
if !view_visibility.get() {
// Reset their interaction to None to avoid strange stuck state
if let Some(mut interaction) = node.interaction {
if let Some(mut pressed_state) = node.pressed_state {
// We cannot simply set the interaction to None, as that will trigger change detection repeatedly
interaction.set_if_neq(Interaction::None);
pressed_state.set_if_neq(Pressed {
pressed: false,
..*pressed_state
});
}

return None;
Expand Down Expand Up @@ -247,9 +318,17 @@ pub fn ui_focus_system(
if contains_cursor {
Some(*entity)
} else {
if let Some(mut interaction) = node.interaction {
if *interaction == Interaction::Hovered || (cursor_position.is_none()) {
interaction.set_if_neq(Interaction::None);
if let Some(mut pressed_state) = node.pressed_state {
// If the InteractionPolicy is Release, we should set the interaction to None
// The entity might just as well not have the InteractionPolicy component
// in which case we should use the default behaviour
let interaction_policy = pressed_state.press_policy;

if cursor_position.is_none() || interaction_policy == PressPolicy::Release {
pressed_state.set_if_neq(Pressed {
pressed: false,
..*pressed_state
});
}
}
None
Expand All @@ -265,19 +344,14 @@ pub fn ui_focus_system(
// the iteration will stop on it because it "captures" the interaction.
let mut iter = node_query.iter_many_mut(hovered_nodes.by_ref());
while let Some(node) = iter.fetch_next() {
if let Some(mut interaction) = node.interaction {
if mouse_clicked {
// only consider nodes with Interaction "pressed"
if *interaction != Interaction::Pressed {
*interaction = Interaction::Pressed;
// if the mouse was simultaneously released, reset this Interaction in the next
// frame
if mouse_released {
state.entities_to_reset.push(node.entity);
}
if let Some(mut pressed_state) = node.pressed_state {
if mouse_clicked && !pressed_state.pressed {
pressed_state.pressed = true;
// if the mouse was simultaneously released, reset this Interaction in the next
// frame
if mouse_released {
state.entities_to_reset.push(node.entity);
}
} else if *interaction == Interaction::None {
*interaction = Interaction::Hovered;
}
}

Expand All @@ -292,10 +366,13 @@ pub fn ui_focus_system(
// `moused_over_nodes` after the previous loop is exited.
let mut iter = node_query.iter_many_mut(hovered_nodes);
while let Some(node) = iter.fetch_next() {
if let Some(mut interaction) = node.interaction {
// don't reset pressed nodes because they're handled separately
if *interaction != Interaction::Pressed {
interaction.set_if_neq(Interaction::None);
if let Some(mut pressed_state) = node.pressed_state {
// don't reset clicked nodes because they're handled separately
if !pressed_state.pressed {
pressed_state.set_if_neq(Pressed {
pressed: false,
..*pressed_state
});
}
}
}
Expand Down
7 changes: 5 additions & 2 deletions crates/bevy_ui/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ pub mod prelude {
#[doc(hidden)]
pub use crate::{
camera_config::*, geometry::*, node_bundles::*, ui_node::*, widget::Button, widget::Label,
Interaction, UiScale,
Pressed, UiScale,
};
}

Expand Down Expand Up @@ -98,7 +98,10 @@ impl Plugin for UiPlugin {
.register_type::<GridAutoFlow>()
.register_type::<GridPlacement>()
.register_type::<GridTrack>()
.register_type::<Interaction>()
.register_type::<RepeatedGridTrack>()
.register_type::<FocusPolicy>()
.register_type::<Pressed>()
.register_type::<PressPolicy>()
.register_type::<JustifyContent>()
.register_type::<JustifyItems>()
.register_type::<JustifySelf>()
Expand Down
15 changes: 10 additions & 5 deletions crates/bevy_ui/src/node_bundles.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
use crate::widget::TextFlags;
use crate::{
widget::{Button, UiImageSize},
BackgroundColor, BorderColor, ContentSize, FocusPolicy, Interaction, Node, Style, UiImage,
UiTextureAtlasImage, ZIndex,
BackgroundColor, BorderColor, ContentSize, FocusPolicy, Node, Pressed, RelativeCursorPosition,
Style, UiImage, UiTextureAtlasImage, ZIndex,
};
use bevy_asset::Handle;
use bevy_ecs::bundle::Bundle;
Expand Down Expand Up @@ -278,8 +278,12 @@ pub struct ButtonBundle {
/// Styles which control the layout (size and position) of the node and it's children
/// In some cases these styles also affect how the node drawn/painted.
pub style: Style,
/// Describes whether and how the button has been interacted with by the input
pub interaction: Interaction,
/// Describes whether the button is pressed
pub pressed: Pressed,
/// The position of the cursor relative to the node
///
/// Used for checking if the cursor is hovering over the node
pub relative_cursor_position: RelativeCursorPosition,
/// Whether this node should block interaction with lower nodes
pub focus_policy: FocusPolicy,
/// The background color, which serves as a "fill" for this node
Expand Down Expand Up @@ -317,7 +321,8 @@ impl Default for ButtonBundle {
button: Default::default(),
style: Default::default(),
border_color: BorderColor(Color::NONE),
interaction: Default::default(),
pressed: Default::default(),
relative_cursor_position: Default::default(),
background_color: Default::default(),
image: Default::default(),
transform: Default::default(),
Expand Down
1 change: 1 addition & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,7 @@ Example | Description
[Font Atlas Debug](../examples/ui/font_atlas_debug.rs) | Illustrates how FontAtlases are populated (used to optimize text rendering internally)
[Overflow](../examples/ui/overflow.rs) | Simple example demonstrating overflow behavior
[Overflow and Clipping Debug](../examples/ui/overflow_debug.rs) | An example to debug overflow and clipping behavior
[Press Policy](../examples/ui/press_policy.rs) | Illustrates how the PressPolicy works with the Pressed component. Adds two buttons to the scene, one having a `Hold` press policy, and the other having a `Release` press policy.
[Relative Cursor Position](../examples/ui/relative_cursor_position.rs) | Showcases the RelativeCursorPosition component
[Size Constraints](../examples/ui/size_constraints.rs) | Demonstrates how the to use the size constraints to control the size of a UI node.
[Text](../examples/ui/text.rs) | Illustrates creating and updating text
Expand Down
22 changes: 14 additions & 8 deletions examples/ecs/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,13 @@
//!
//! In this case, we're transitioning from a `Menu` state to an `InGame` state.

use bevy::prelude::*;
use bevy::{
prelude::*,
ui::{
InteractionState, InteractionStateChangedFilter, InteractionStateHandler,
RelativeCursorPosition,
},
};

fn main() {
App::new()
Expand Down Expand Up @@ -93,20 +99,20 @@ fn setup_menu(mut commands: Commands) {
fn menu(
mut next_state: ResMut<NextState<AppState>>,
mut interaction_query: Query<
(&Interaction, &mut BackgroundColor),
(Changed<Interaction>, With<Button>),
(&Pressed, &RelativeCursorPosition, &mut BackgroundColor),
(InteractionStateChangedFilter, With<Button>),
>,
) {
for (interaction, mut color) in &mut interaction_query {
match *interaction {
Interaction::Pressed => {
for (interaction, relative_cursor_position, mut color) in &mut interaction_query {
match (interaction, relative_cursor_position).interaction_state() {
InteractionState::Pressed => {
*color = PRESSED_BUTTON.into();
next_state.set(AppState::InGame);
}
Interaction::Hovered => {
InteractionState::Hovered => {
*color = HOVERED_BUTTON.into();
}
Interaction::None => {
InteractionState::None => {
*color = NORMAL_BUTTON.into();
}
}
Expand Down
Loading
Loading