Fixed bevy_ui touch input (#4099)

# Objective

`bevy_ui` doesn't support correctly touch inputs because of two problems in the focus system:
- It attempts to retrieve touch input with a specific `0` id
- It doesn't retrieve touch positions and bases its focus solely on mouse position, absent from mobile devices

## Solution

I added a few methods to the `Touches` resource, allowing to check if **any** touch input was pressed, released or cancelled and to retrieve the *position* of the first pressed touch input and adapted the focus system.

I added a test button to the *iOS* example and it works correclty on emulator. I did not test on a real touch device as:
- Android is not working (https://github.com/bevyengine/bevy/issues/3249)
- I don't have an iOS device
This commit is contained in:
Félix Lescaudey de Maneville 2022-06-20 20:32:19 +00:00
parent 30ca97e287
commit c4fc5d88f0
3 changed files with 89 additions and 7 deletions

View file

@ -224,6 +224,11 @@ impl Touches {
self.pressed.get(&id)
}
/// Checks if any touch input was just pressed.
pub fn any_just_pressed(&self) -> bool {
!self.just_pressed.is_empty()
}
/// Returns `true` if the input corresponding to the `id` has just been pressed.
pub fn just_pressed(&self, id: u64) -> bool {
self.just_pressed.contains_key(&id)
@ -239,6 +244,11 @@ impl Touches {
self.just_released.get(&id)
}
/// Checks if any touch input was just released.
pub fn any_just_released(&self) -> bool {
!self.just_released.is_empty()
}
/// Returns `true` if the input corresponding to the `id` has just been released.
pub fn just_released(&self, id: u64) -> bool {
self.just_released.contains_key(&id)
@ -249,6 +259,11 @@ impl Touches {
self.just_released.values()
}
/// Checks if any touch input was just cancelled.
pub fn any_just_cancelled(&self) -> bool {
!self.just_cancelled.is_empty()
}
/// Returns `true` if the input corresponding to the `id` has just been cancelled.
pub fn just_cancelled(&self, id: u64) -> bool {
self.just_cancelled.contains_key(&id)
@ -259,6 +274,11 @@ impl Touches {
self.just_cancelled.values()
}
/// Retrieves the position of the first currently pressed touch, if any
pub fn first_pressed_position(&self) -> Option<Vec2> {
self.pressed.values().next().map(|t| t.position)
}
/// Processes a [`TouchInput`] event by updating the `pressed`, `just_pressed`,
/// `just_released`, and `just_cancelled` collections.
fn process_touch_event(&mut self, event: &TouchInput) {

View file

@ -71,10 +71,6 @@ pub fn ui_focus_system(
Option<&CalculatedClip>,
)>,
) {
let cursor_position = windows
.get_primary()
.and_then(|window| window.cursor_position());
// 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) {
@ -83,7 +79,7 @@ pub fn ui_focus_system(
}
let mouse_released =
mouse_button_input.just_released(MouseButton::Left) || touches_input.just_released(0);
mouse_button_input.just_released(MouseButton::Left) || touches_input.any_just_released();
if mouse_released {
for (_entity, _node, _global_transform, interaction, _focus_policy, _clip) in
node_query.iter_mut()
@ -97,7 +93,12 @@ pub fn ui_focus_system(
}
let mouse_clicked =
mouse_button_input.just_pressed(MouseButton::Left) || touches_input.just_pressed(0);
mouse_button_input.just_pressed(MouseButton::Left) || touches_input.any_just_pressed();
let cursor_position = windows
.get_primary()
.and_then(|window| window.cursor_position())
.or_else(|| touches_input.first_pressed_position());
let mut moused_over_z_sorted_nodes = node_query
.iter_mut()

View file

@ -13,13 +13,14 @@ fn main() {
.add_startup_system(setup_scene)
.add_startup_system(setup_music)
.add_system(touch_camera)
.add_system(button_handler)
.run();
}
fn touch_camera(
windows: ResMut<Windows>,
mut touches: EventReader<TouchInput>,
mut camera: Query<&mut Transform, With<Camera>>,
mut camera: Query<&mut Transform, With<Camera3d>>,
mut last_position: Local<Option<Vec2>>,
) {
for touch in touches.iter() {
@ -47,6 +48,7 @@ fn setup_scene(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
asset_server: Res<AssetServer>,
) {
// plane
commands.spawn_bundle(PbrBundle {
@ -86,6 +88,65 @@ fn setup_scene(
transform: Transform::from_xyz(-2.0, 2.5, 5.0).looking_at(Vec3::ZERO, Vec3::Y),
..default()
});
// Test ui
commands.spawn_bundle(Camera2dBundle::default());
commands
.spawn_bundle(ButtonBundle {
style: Style {
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
position_type: PositionType::Absolute,
position: UiRect {
left: Val::Px(50.0),
right: Val::Px(50.0),
top: Val::Auto,
bottom: Val::Px(50.0),
},
..default()
},
..default()
})
.with_children(|b| {
b.spawn_bundle(TextBundle {
text: Text {
sections: vec![TextSection {
value: "Test Button".to_string(),
style: TextStyle {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 30.0,
color: Color::BLACK,
},
}],
alignment: TextAlignment {
vertical: VerticalAlign::Center,
horizontal: HorizontalAlign::Center,
},
},
..default()
});
});
}
fn button_handler(
mut interaction_query: Query<
(&Interaction, &mut UiColor),
(Changed<Interaction>, With<Button>),
>,
) {
for (interaction, mut color) in interaction_query.iter_mut() {
match *interaction {
Interaction::Clicked => {
*color = Color::BLUE.into();
}
Interaction::Hovered => {
*color = Color::GRAY.into();
}
Interaction::None => {
*color = Color::WHITE.into();
}
}
}
}
fn setup_music(asset_server: Res<AssetServer>, audio: Res<Audio>) {