Skip to content

Commit 4c1c545

Browse files
tim-blackbirdJMS55
andauthored
Improve selection (#230)
* Improve selection * Update crates/bevy_editor_core/src/selection.rs Co-authored-by: JMS55 <[email protected]> --------- Co-authored-by: JMS55 <[email protected]>
1 parent b2023bd commit 4c1c545

File tree

7 files changed

+177
-42
lines changed

7 files changed

+177
-42
lines changed

bevy_editor_panes/bevy_3d_viewport/src/selection_box.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use bevy::ecs::system::SystemParam;
22
use bevy::prelude::*;
3-
use bevy_editor_core::SelectedEntity;
3+
use bevy_editor_core::selection::SelectedEntity;
44
use bevy_render::primitives::Aabb;
55

66
#[derive(SystemParam)]

bevy_editor_panes/bevy_properties_pane/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
//! Data can be viewed and modified in real-time, with changes being reflected in the application.
44
55
use bevy::{color::palettes::tailwind, prelude::*, reflect::*};
6-
use bevy_editor_core::SelectedEntity;
6+
use bevy_editor_core::selection::SelectedEntity;
77
use bevy_i_cant_believe_its_not_bsn::{Template, TemplateEntityCommandsExt, template};
88
use bevy_pane_layout::prelude::{PaneAppExt, PaneStructure};
99

bevy_editor_panes/bevy_scene_tree/src/lib.rs

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
//! An interactive, collapsible tree view for hierarchical ECS data in Bevy.
22
33
use bevy::{app::Plugin, color::palettes::tailwind, prelude::*};
4-
use bevy_editor_core::SelectedEntity;
4+
use bevy_editor_core::selection::{
5+
SelectedEntity, common_handlers::toggle_select_on_click_for_entity,
6+
};
57
use bevy_i_cant_believe_its_not_bsn::{Template, TemplateEntityCommandsExt, on, template};
68
use bevy_pane_layout::prelude::{PaneAppExt, PaneStructure};
79

@@ -62,16 +64,6 @@ fn scene_tree_row_for_entity(
6264
name: &Name,
6365
selected_entity: &SelectedEntity,
6466
) -> Template {
65-
let set_selected_entity_on_click =
66-
move |mut trigger: On<Pointer<Click>>, mut selected_entity: ResMut<SelectedEntity>| {
67-
if selected_entity.0 == Some(entity) {
68-
selected_entity.0 = None;
69-
} else {
70-
selected_entity.0 = Some(entity);
71-
}
72-
trigger.propagate(false);
73-
};
74-
7567
template! {
7668
{entity}: (
7769
Node {
@@ -82,7 +74,7 @@ fn scene_tree_row_for_entity(
8274
BorderRadius::all(Val::Px(4.0)),
8375
BackgroundColor(if selected_entity.0 == Some(entity) { tailwind::NEUTRAL_700.into() } else { Color::NONE }),
8476
) => [
85-
on(set_selected_entity_on_click);
77+
on(toggle_select_on_click_for_entity(entity));
8678
(
8779
Text(name.into()),
8880
TextFont::from_font_size(11.0),

crates/bevy_editor/src/lib.rs

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ pub use bevy;
1919

2020
use bevy_context_menu::ContextMenuPlugin;
2121
use bevy_editor_core::EditorCorePlugin;
22+
use bevy_editor_core::selection::common_handlers::toggle_select_on_click;
2223
use bevy_editor_styles::StylesPlugin;
2324

2425
// Panes
@@ -112,12 +113,14 @@ fn dummy_setup(
112113
Name::new("Circle"),
113114
));
114115

115-
commands.spawn((
116-
Mesh3d(meshes.add(Plane3d::new(Vec3::Y, Vec2::splat(1.5)))),
117-
MeshMaterial3d(materials_3d.add(Color::WHITE)),
118-
Name::new("Plane"),
119-
Pickable::default(),
120-
));
116+
commands
117+
.spawn((
118+
Mesh3d(meshes.add(Plane3d::new(Vec3::Y, Vec2::splat(1.5)))),
119+
MeshMaterial3d(materials_3d.add(Color::WHITE)),
120+
Name::new("Plane"),
121+
Pickable::default(),
122+
))
123+
.observe(toggle_select_on_click);
121124

122125
commands.spawn((
123126
DirectionalLight {

crates/bevy_editor_core/src/lib.rs

Lines changed: 9 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,18 @@
11
//! This crate provides core functionality for the Bevy Engine Editor.
22
3-
use bevy::{ecs::entity::Entities, prelude::*};
3+
pub mod selection;
4+
pub mod utils;
45

5-
/// Plugin for the editor scene tree pane.
6+
use bevy::prelude::*;
7+
8+
use crate::{selection::SelectionPlugin, utils::CoreUtilsPlugin};
9+
10+
/// Core plugin for the editor.
11+
#[derive(Default)]
612
pub struct EditorCorePlugin;
713

814
impl Plugin for EditorCorePlugin {
915
fn build(&self, app: &mut App) {
10-
app.init_resource::<SelectedEntity>()
11-
.register_type::<SelectedEntity>()
12-
.add_systems(PostUpdate, reset_selected_entity_if_entity_despawned);
13-
}
14-
}
15-
16-
/// The currently selected entity in the scene.
17-
#[derive(Resource, Default, Reflect)]
18-
#[reflect(Resource, Default)]
19-
pub struct SelectedEntity(pub Option<Entity>);
20-
21-
/// System to reset [`SelectedEntity`] when the entity is despawned.
22-
pub fn reset_selected_entity_if_entity_despawned(
23-
mut selected_entity: ResMut<SelectedEntity>,
24-
entities: &Entities,
25-
) {
26-
if let Some(e) = selected_entity.0 {
27-
if !entities.contains(e) {
28-
selected_entity.0 = None;
29-
}
16+
app.add_plugins((SelectionPlugin, CoreUtilsPlugin));
3017
}
3118
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
//! Editor selection module.
2+
3+
use bevy::{ecs::entity::Entities, prelude::*};
4+
5+
/// Editor selection plugin.
6+
#[derive(Default)]
7+
pub struct SelectionPlugin;
8+
9+
impl Plugin for SelectionPlugin {
10+
fn build(&self, app: &mut App) {
11+
app.init_resource::<SelectedEntity>()
12+
.register_type::<SelectedEntity>()
13+
.add_systems(PostUpdate, reset_selected_entity_if_entity_despawned);
14+
}
15+
}
16+
17+
/// The currently selected entity in the scene.
18+
#[derive(Resource, Default, Reflect)]
19+
#[reflect(Resource, Default)]
20+
pub struct SelectedEntity(pub Option<Entity>);
21+
22+
impl SelectedEntity {
23+
/// Toggle selection for an entity.
24+
pub fn toggle(&mut self, entity: Entity) {
25+
debug_assert_ne!(entity, Entity::PLACEHOLDER);
26+
if self.0 == Some(entity) {
27+
self.0 = None;
28+
} else {
29+
self.0 = Some(entity);
30+
}
31+
}
32+
33+
/// Set an entity as selected.
34+
pub fn set(&mut self, entity: Entity) {
35+
debug_assert_ne!(entity, Entity::PLACEHOLDER);
36+
self.0 = Some(entity);
37+
}
38+
39+
/// Empty the selection.
40+
pub fn reset(&mut self) {
41+
self.0 = None;
42+
}
43+
}
44+
45+
/// System to reset [`SelectedEntity`] when the entity is despawned.
46+
pub fn reset_selected_entity_if_entity_despawned(
47+
mut selected_entity: ResMut<SelectedEntity>,
48+
entities: &Entities,
49+
) {
50+
if let Some(e) = selected_entity.0 {
51+
if !entities.contains(e) {
52+
selected_entity.reset();
53+
}
54+
}
55+
}
56+
57+
/// Common handler observer systems for entity selection behavior.
58+
pub mod common_handlers {
59+
use crate::utils::DragCancelClick;
60+
61+
use super::*;
62+
63+
/// Toggles selection for this entity when it is clicked.
64+
pub fn toggle_select_on_click(
65+
mut trigger: On<Pointer<DragCancelClick>>,
66+
mut selected_entity: ResMut<SelectedEntity>,
67+
) {
68+
if trigger.button == PointerButton::Primary {
69+
selected_entity.toggle(trigger.target());
70+
trigger.propagate(false);
71+
}
72+
}
73+
74+
/// Toggles selection for an entity when this entity is clicked.
75+
pub fn toggle_select_on_click_for_entity(
76+
entity: Entity,
77+
) -> impl FnMut(On<Pointer<DragCancelClick>>, ResMut<SelectedEntity>) {
78+
move |mut trigger: On<Pointer<DragCancelClick>>,
79+
mut selected_entity: ResMut<SelectedEntity>| {
80+
if trigger.button == PointerButton::Primary {
81+
selected_entity.toggle(entity);
82+
trigger.propagate(false);
83+
}
84+
}
85+
}
86+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
//! Editor core utils.
2+
3+
use std::time::Duration;
4+
5+
use bevy::{
6+
picking::backend::HitData, platform::collections::HashMap, platform::time::Instant, prelude::*,
7+
};
8+
9+
/// Editor core utils plugin.
10+
#[derive(Default)]
11+
pub struct CoreUtilsPlugin;
12+
13+
impl Plugin for CoreUtilsPlugin {
14+
fn build(&self, app: &mut App) {
15+
app.init_resource::<DragCancelClickState>()
16+
.add_event::<Pointer<DragCancelClick>>()
17+
.register_type::<Pointer<DragCancelClick>>()
18+
.add_observer(on_press)
19+
.add_observer(on_drag_start)
20+
.add_observer(on_release);
21+
}
22+
}
23+
24+
fn on_press(trigger: On<Pointer<Press>>, mut state: ResMut<DragCancelClickState>) {
25+
state.0.insert(trigger.target(), Instant::now());
26+
}
27+
28+
fn on_drag_start(trigger: On<Pointer<DragStart>>, mut state: ResMut<DragCancelClickState>) {
29+
state.0.remove(&trigger.target());
30+
}
31+
32+
fn on_release(
33+
trigger: On<Pointer<Release>>,
34+
mut state: ResMut<DragCancelClickState>,
35+
mut commands: Commands,
36+
) {
37+
let now = Instant::now();
38+
if let Some(instant) = state.remove(&trigger.target()) {
39+
let event = Pointer::new(
40+
trigger.pointer_id,
41+
trigger.pointer_location.clone(),
42+
DragCancelClick {
43+
button: trigger.button,
44+
hit: trigger.hit.clone(),
45+
duration: now - instant,
46+
},
47+
);
48+
commands.trigger_targets(event.clone(), trigger.target());
49+
commands.send_event(event);
50+
}
51+
}
52+
53+
/// Fires when a pointer sends a pointer pressed event followed by a pointer released event, with the same
54+
/// `target` entity for both events and without a drag start event in between.
55+
#[derive(Clone, PartialEq, Debug, Reflect)]
56+
#[reflect(Clone, PartialEq)]
57+
pub struct DragCancelClick {
58+
/// Pointer button pressed and lifted to trigger this event.
59+
pub button: PointerButton,
60+
/// Information about the picking intersection.
61+
pub hit: HitData,
62+
/// Duration between the pointer pressed and lifted for this click
63+
pub duration: Duration,
64+
}
65+
66+
#[derive(Resource, Deref, DerefMut, Default)]
67+
struct DragCancelClickState(HashMap<Entity, Instant>);

0 commit comments

Comments
 (0)