hooking up observers and clicking for ui node (#14695)

Makes the newly merged picking usable for UI elements. 

currently it both triggers the events, as well as sends them as throught
commands.trigger_targets. We should probably figure out if this is
needed for them all.

# Objective

Hooks up obserers and picking for a very simple example

## Solution

upstreamed the UI picking backend from bevy_mod_picking

## Testing

tested with the new example picking/simple_picking.rs


---

---------

Co-authored-by: Lixou <82600264+DasLixou@users.noreply.github.com>
Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
Co-authored-by: Kristoffer Søholm <k.soeholm@gmail.com>
This commit is contained in:
TotalKrill 2024-08-15 16:43:55 +02:00 committed by GitHub
parent 0ea46663b0
commit 6adf31babf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 455 additions and 66 deletions

View file

@ -3352,6 +3352,18 @@ description = "Demonstrates how to rotate the skybox and the environment map sim
category = "3D Rendering"
wasm = false
[[example]]
name = "simple_picking"
path = "examples/picking/simple_picking.rs"
doc-scrape-examples = true
required-features = ["bevy_picking"]
[package.metadata.example.simple_picking]
name = "Showcases simple picking events and usage"
description = "Demonstrates how to use picking events to spawn simple objects"
category = "Picking"
wasm = true
[profile.wasm-release]
inherits = "release"
opt-level = "z"

View file

@ -191,7 +191,7 @@ meshlet_processor = ["bevy_pbr?/meshlet_processor"]
bevy_dev_tools = ["dep:bevy_dev_tools"]
# Provides a picking functionality
bevy_picking = ["dep:bevy_picking"]
bevy_picking = ["dep:bevy_picking", "bevy_ui?/bevy_picking"]
# Enable support for the ios_simulator by downgrading some rendering capabilities
ios_simulator = ["bevy_pbr?/ios_simulator", "bevy_render?/ios_simulator"]

View file

@ -56,6 +56,8 @@ plugin_group! {
bevy_gizmos:::GizmoPlugin,
#[cfg(feature = "bevy_state")]
bevy_state::app:::StatesPlugin,
#[cfg(feature = "bevy_picking")]
bevy_picking:::DefaultPickingPlugins,
#[cfg(feature = "bevy_dev_tools")]
bevy_dev_tools:::DevToolsPlugin,
#[cfg(feature = "bevy_ci_testing")]

View file

@ -66,3 +66,7 @@ pub use crate::state::prelude::*;
#[doc(hidden)]
#[cfg(feature = "bevy_gltf")]
pub use crate::gltf::prelude::*;
#[doc(hidden)]
#[cfg(feature = "bevy_picking")]
pub use crate::picking::prelude::*;

View file

@ -20,6 +20,9 @@ bevy_transform = { path = "../bevy_transform", version = "0.15.0-dev" }
bevy_utils = { path = "../bevy_utils", version = "0.15.0-dev" }
bevy_window = { path = "../bevy_window", version = "0.15.0-dev" }
bevy_time = { path = "../bevy_time", version = "0.15.0-dev" }
bevy_asset = { path = "../bevy_asset", version = "0.15.0-dev" }
uuid = { version = "1.1", features = ["v4"] }
[lints]

View file

@ -205,6 +205,7 @@ pub struct Drop {
/// Generates pointer events from input and focus data
#[allow(clippy::too_many_arguments)]
pub fn pointer_events(
mut commands: Commands,
// Input
mut input_presses: EventReader<InputPress>,
mut input_moves: EventReader<InputMove>,
@ -237,12 +238,14 @@ pub fn pointer_events(
.iter()
.flat_map(|h| h.iter().map(|(entity, data)| (*entity, data.to_owned())))
{
pointer_move.send(Pointer::new(
let event = Pointer::new(
pointer_id,
location.clone(),
hovered_entity,
Move { hit, delta },
));
);
commands.trigger_targets(event.clone(), event.target);
pointer_move.send(event);
}
}
@ -264,12 +267,14 @@ pub fn pointer_events(
);
continue;
};
pointer_up.send(Pointer::new(
let event = Pointer::new(
press_event.pointer_id,
location,
hovered_entity,
Up { button, hit },
));
);
commands.trigger_targets(event.clone(), event.target);
pointer_up.send(event);
}
}
for (hovered_entity, hit) in hover_map
@ -285,12 +290,14 @@ pub fn pointer_events(
);
continue;
};
pointer_down.send(Pointer::new(
let event = Pointer::new(
press_event.pointer_id,
location,
hovered_entity,
Down { button, hit },
));
);
commands.trigger_targets(event.clone(), event.target);
pointer_down.send(event);
}
}
}
@ -313,12 +320,9 @@ pub fn pointer_events(
);
continue;
};
pointer_over.send(Pointer::new(
pointer_id,
location,
hovered_entity,
Over { hit },
));
let event = Pointer::new(pointer_id, location, hovered_entity, Over { hit });
commands.trigger_targets(event.clone(), event.target);
pointer_over.send(event);
}
}
@ -340,12 +344,9 @@ pub fn pointer_events(
);
continue;
};
pointer_out.send(Pointer::new(
pointer_id,
location,
hovered_entity,
Out { hit },
));
let event = Pointer::new(pointer_id, location, hovered_entity, Out { hit });
commands.trigger_targets(event.clone(), event.target);
pointer_out.send(event);
}
}
}
@ -366,6 +367,11 @@ pub struct DragEntry {
/// Uses pointer events to determine when click and drag events occur.
#[allow(clippy::too_many_arguments)]
pub fn send_click_and_drag_events(
// for triggering observers
// - Pointer<Click>
// - Pointer<Drag>
// - Pointer<DragStart>
mut commands: Commands,
// Input
mut pointer_down: EventReader<Pointer<Down>>,
mut pointer_up: EventReader<Pointer<Up>>,
@ -377,12 +383,9 @@ pub fn send_click_and_drag_events(
mut down_map: Local<
HashMap<(PointerId, PointerButton), HashMap<Entity, (Pointer<Down>, Instant)>>,
>,
// Output
// Outputs used for further processing
mut drag_map: ResMut<DragMap>,
mut pointer_click: EventWriter<Pointer<Click>>,
mut pointer_drag_start: EventWriter<Pointer<DragStart>>,
mut pointer_drag_end: EventWriter<Pointer<DragEnd>>,
mut pointer_drag: EventWriter<Pointer<Drag>>,
) {
let pointer_location = |pointer_id: PointerId| {
pointer_map
@ -415,7 +418,7 @@ pub fn send_click_and_drag_events(
latest_pos: down.pointer_location.position,
},
);
pointer_drag_start.send(Pointer::new(
let event = Pointer::new(
pointer_id,
down.pointer_location.clone(),
down.target,
@ -423,7 +426,8 @@ pub fn send_click_and_drag_events(
button,
hit: down.hit.clone(),
},
));
);
commands.trigger_targets(event, down.target);
}
for (dragged_entity, drag) in drag_list.iter_mut() {
@ -433,12 +437,9 @@ pub fn send_click_and_drag_events(
delta: location.position - drag.latest_pos,
};
drag.latest_pos = location.position;
pointer_drag.send(Pointer::new(
pointer_id,
location.clone(),
*dragged_entity,
drag_event,
));
let target = *dragged_entity;
let event = Pointer::new(pointer_id, location.clone(), target, drag_event);
commands.trigger_targets(event, target);
}
}
}
@ -458,7 +459,7 @@ pub fn send_click_and_drag_events(
.and_then(|down| down.get(&target))
{
let duration = now - *down_instant;
pointer_click.send(Pointer::new(
let event = Pointer::new(
pointer_id,
pointer_location,
target,
@ -467,7 +468,8 @@ pub fn send_click_and_drag_events(
hit,
duration,
},
));
);
commands.trigger_targets(event, target);
}
}
@ -501,12 +503,9 @@ pub fn send_click_and_drag_events(
button: press.button,
distance: drag.latest_pos - drag.start_pos,
};
pointer_drag_end.send(Pointer::new(
press.pointer_id,
location.clone(),
drag_target,
drag_end,
));
let event = Pointer::new(press.pointer_id, location.clone(), drag_target, drag_end);
commands.trigger_targets(event.clone(), event.target);
pointer_drag_end.send(event);
}
}
}
@ -514,6 +513,12 @@ pub fn send_click_and_drag_events(
/// Uses pointer events to determine when drag-over events occur
#[allow(clippy::too_many_arguments)]
pub fn send_drag_over_events(
// uses this to trigger the following
// - Pointer<DragEnter>,
// - Pointer<DragOver>,
// - Pointer<DragLeave>,
// - Pointer<Drop>,
mut commands: Commands,
// Input
drag_map: Res<DragMap>,
mut pointer_over: EventReader<Pointer<Over>>,
@ -522,12 +527,6 @@ pub fn send_drag_over_events(
mut pointer_drag_end: EventReader<Pointer<DragEnd>>,
// Local
mut drag_over_map: Local<HashMap<(PointerId, PointerButton), HashMap<Entity, HitData>>>,
// Output
mut pointer_drag_enter: EventWriter<Pointer<DragEnter>>,
mut pointer_drag_over: EventWriter<Pointer<DragOver>>,
mut pointer_drag_leave: EventWriter<Pointer<DragLeave>>,
mut pointer_drop: EventWriter<Pointer<Drop>>,
) {
// Fire PointerDragEnter events.
for Pointer {
@ -548,17 +547,17 @@ pub fn send_drag_over_events(
{
let drag_entry = drag_over_map.entry((pointer_id, button)).or_default();
drag_entry.insert(target, hit.clone());
let event = DragEnter {
button,
dragged: *drag_target,
hit: hit.clone(),
};
pointer_drag_enter.send(Pointer::new(
let event = Pointer::new(
pointer_id,
pointer_location.clone(),
target,
event,
));
DragEnter {
button,
dragged: *drag_target,
hit: hit.clone(),
},
);
commands.trigger_targets(event, target);
}
}
}
@ -580,7 +579,7 @@ pub fn send_drag_over_events(
|&&drag_target| target != drag_target, /* can't drag over itself */
)
{
pointer_drag_over.send(Pointer::new(
let event = Pointer::new(
pointer_id,
pointer_location.clone(),
target,
@ -589,7 +588,8 @@ pub fn send_drag_over_events(
dragged: *drag_target,
hit: hit.clone(),
},
));
);
commands.trigger_targets(event, target);
}
}
}
@ -598,7 +598,7 @@ pub fn send_drag_over_events(
for Pointer {
pointer_id,
pointer_location,
target,
target: drag_end_target,
event: DragEnd {
button,
distance: _,
@ -609,26 +609,30 @@ pub fn send_drag_over_events(
continue;
};
for (dragged_over, hit) in drag_over_set.drain() {
pointer_drag_leave.send(Pointer::new(
let target = dragged_over;
let event = Pointer::new(
pointer_id,
pointer_location.clone(),
dragged_over,
DragLeave {
button,
dragged: target,
dragged: drag_end_target,
hit: hit.clone(),
},
));
pointer_drop.send(Pointer::new(
);
commands.trigger_targets(event, target);
let event = Pointer::new(
pointer_id,
pointer_location.clone(),
dragged_over,
target,
Drop {
button,
dropped: target,
hit: hit.clone(),
},
));
);
commands.trigger_targets(event, target);
}
}
@ -651,7 +655,7 @@ pub fn send_drag_over_events(
continue;
};
for drag_target in drag_list.keys() {
pointer_drag_leave.send(Pointer::new(
let event = Pointer::new(
pointer_id,
pointer_location.clone(),
target,
@ -660,7 +664,8 @@ pub fn send_drag_over_events(
dragged: *drag_target,
hit: hit.clone(),
},
));
);
commands.trigger_targets(event, target);
}
}
}

View file

@ -12,6 +12,15 @@ use bevy_app::prelude::*;
use bevy_ecs::prelude::*;
use bevy_reflect::prelude::*;
/// common exports for picking interaction
pub mod prelude {
#[doc(hidden)]
pub use crate::{
events::*, input::InputPlugin, pointer::PointerButton, DefaultPickingPlugins,
InteractionPlugin, Pickable, PickingPlugin, PickingPluginsSettings,
};
}
/// Used to globally toggle picking features at runtime.
#[derive(Clone, Debug, Resource, Reflect)]
#[reflect(Resource, Default)]
@ -167,8 +176,27 @@ pub enum PickSet {
Last,
}
/// One plugin that contains the [`input::InputPlugin`], [`PickingPlugin`] and the [`InteractionPlugin`],
/// this is probably the plugin that will be most used.
/// Note: for any of these plugins to work, they require a picking backend to be active,
/// The picking backend is responsible to turn an input, into a [`crate::backend::PointerHits`]
/// that [`PickingPlugin`] and [`InteractionPlugin`] will refine into [`bevy_ecs::observer::Trigger`]s.
#[derive(Default)]
pub struct DefaultPickingPlugins;
impl Plugin for DefaultPickingPlugins {
fn build(&self, app: &mut App) {
app.add_plugins((
input::InputPlugin::default(),
PickingPlugin,
InteractionPlugin,
));
}
}
/// This plugin sets up the core picking infrastructure. It receives input events, and provides the shared
/// types used by other picking plugins.
#[derive(Default)]
pub struct PickingPlugin;
impl Plugin for PickingPlugin {
@ -185,11 +213,18 @@ impl Plugin for PickingPlugin {
pointer::update_pointer_map,
pointer::InputMove::receive,
pointer::InputPress::receive,
backend::ray::RayMap::repopulate,
backend::ray::RayMap::repopulate.after(pointer::InputMove::receive),
)
.in_set(PickSet::ProcessInput),
)
.configure_sets(First, (PickSet::Input, PickSet::PostInput).chain())
.configure_sets(
First,
(PickSet::Input, PickSet::PostInput)
.after(bevy_time::TimeSystem)
.ambiguous_with(bevy_asset::handle_internal_asset_events)
.after(bevy_ecs::event::EventUpdates)
.chain(),
)
.configure_sets(
PreUpdate,
(
@ -200,6 +235,7 @@ impl Plugin for PickingPlugin {
// Eventually events will need to be dispatched here
PickSet::Last,
)
.ambiguous_with(bevy_asset::handle_internal_asset_events)
.chain(),
)
.register_type::<pointer::PointerId>()
@ -213,6 +249,7 @@ impl Plugin for PickingPlugin {
}
/// Generates [`Pointer`](events::Pointer) events and handles event bubbling.
#[derive(Default)]
pub struct InteractionPlugin;
impl Plugin for InteractionPlugin {
@ -224,6 +261,12 @@ impl Plugin for InteractionPlugin {
.init_resource::<focus::PreviousHoverMap>()
.init_resource::<DragMap>()
.add_event::<PointerCancel>()
.add_event::<Pointer<Down>>()
.add_event::<Pointer<Up>>()
.add_event::<Pointer<Move>>()
.add_event::<Pointer<Over>>()
.add_event::<Pointer<Out>>()
.add_event::<Pointer<DragEnd>>()
.add_systems(
PreUpdate,
(

View file

@ -26,6 +26,7 @@ bevy_reflect = { path = "../bevy_reflect", version = "0.15.0-dev", features = [
bevy_render = { path = "../bevy_render", version = "0.15.0-dev" }
bevy_sprite = { path = "../bevy_sprite", version = "0.15.0-dev" }
bevy_text = { path = "../bevy_text", version = "0.15.0-dev", optional = true }
bevy_picking = { path = "../bevy_picking", version = "0.15.0-dev", optional = true }
bevy_transform = { path = "../bevy_transform", version = "0.15.0-dev" }
bevy_window = { path = "../bevy_window", version = "0.15.0-dev" }
bevy_utils = { path = "../bevy_utils", version = "0.15.0-dev" }
@ -40,6 +41,7 @@ smallvec = "1.11"
[features]
serialize = ["serde", "smallvec/serde", "bevy_math/serialize"]
bevy_picking = ["dep:bevy_picking"]
[lints]

View file

@ -17,6 +17,9 @@ pub mod ui_material;
pub mod update;
pub mod widget;
#[cfg(feature = "bevy_picking")]
pub mod picking_backend;
use bevy_derive::{Deref, DerefMut};
use bevy_reflect::Reflect;
#[cfg(feature = "bevy_text")]
@ -202,6 +205,9 @@ impl Plugin for UiPlugin {
build_text_interop(app);
build_ui_render(app);
#[cfg(feature = "bevy_picking")]
app.add_plugins(picking_backend::UiPickingBackend);
}
fn finish(&self, app: &mut App) {

View file

@ -0,0 +1,214 @@
//! A picking backend for UI nodes.
//!
//! # Usage
//!
//! This backend does not require markers on cameras or entities to function. It will look for any
//! pointers using the same render target as the UI camera, and run hit tests on the UI node tree.
//!
//! ## Important Note
//!
//! This backend completely ignores [`FocusPolicy`](crate::FocusPolicy). The design of `bevy_ui`'s
//! focus systems and the picking plugin are not compatible. Instead, use the [`Pickable`] component
//! to customize how an entity responds to picking focus. Nodes without the [`Pickable`] component
//! will not trigger events.
//!
//! ## Implementation Notes
//!
//! - `bevy_ui` can only render to the primary window
//! - `bevy_ui` can render on any camera with a flag, it is special, and is not tied to a particular
//! camera.
//! - To correctly sort picks, the order of `bevy_ui` is set to be the camera order plus 0.5.
#![allow(clippy::type_complexity)]
#![allow(clippy::too_many_arguments)]
#![deny(missing_docs)]
use crate::{prelude::*, UiStack};
use bevy_app::prelude::*;
use bevy_ecs::{prelude::*, query::QueryData};
use bevy_math::Vec2;
use bevy_render::prelude::*;
use bevy_transform::prelude::*;
use bevy_utils::hashbrown::HashMap;
use bevy_window::PrimaryWindow;
use bevy_picking::backend::prelude::*;
/// A plugin that adds picking support for UI nodes.
#[derive(Clone)]
pub struct UiPickingBackend;
impl Plugin for UiPickingBackend {
fn build(&self, app: &mut App) {
app.add_systems(PreUpdate, ui_picking.in_set(PickSet::Backend));
}
}
/// Main query from bevy's `ui_focus_system`
#[derive(QueryData)]
#[query_data(mutable)]
pub struct NodeQuery {
entity: Entity,
node: &'static Node,
global_transform: &'static GlobalTransform,
pickable: Option<&'static Pickable>,
calculated_clip: Option<&'static CalculatedClip>,
view_visibility: Option<&'static ViewVisibility>,
target_camera: Option<&'static TargetCamera>,
}
/// Computes the UI node entities under each pointer.
///
/// Bevy's [`UiStack`] orders all nodes in the order they will be rendered, which is the same order
/// we need for determining picking.
pub fn ui_picking(
pointers: Query<(&PointerId, &PointerLocation)>,
camera_query: Query<(Entity, &Camera, Has<IsDefaultUiCamera>)>,
default_ui_camera: DefaultUiCamera,
primary_window: Query<Entity, With<PrimaryWindow>>,
ui_scale: Res<UiScale>,
ui_stack: Res<UiStack>,
mut node_query: Query<NodeQuery>,
mut output: EventWriter<PointerHits>,
) {
// For each camera, the pointer and its position
let mut pointer_pos_by_camera = HashMap::<Entity, HashMap<PointerId, Vec2>>::new();
for (pointer_id, pointer_location) in
pointers.iter().filter_map(|(pointer, pointer_location)| {
Some(*pointer).zip(pointer_location.location().cloned())
})
{
// This pointer is associated with a render target, which could be used by multiple
// cameras. We want to ensure we return all cameras with a matching target.
for camera in camera_query
.iter()
.map(|(entity, camera, _)| {
(
entity,
camera.target.normalize(primary_window.get_single().ok()),
)
})
.filter_map(|(entity, target)| Some(entity).zip(target))
.filter(|(_entity, target)| target == &pointer_location.target)
.map(|(cam_entity, _target)| cam_entity)
{
let Ok((_, camera_data, _)) = camera_query.get(camera) else {
continue;
};
let mut pointer_pos = pointer_location.position;
if let Some(viewport) = camera_data.logical_viewport_rect() {
pointer_pos -= viewport.min;
}
let scaled_pointer_pos = pointer_pos / **ui_scale;
pointer_pos_by_camera
.entry(camera)
.or_default()
.insert(pointer_id, scaled_pointer_pos);
}
}
// The list of node entities hovered for each (camera, pointer) combo
let mut hit_nodes = HashMap::<(Entity, PointerId), Vec<Entity>>::new();
// prepare an iterator that contains all the nodes that have the cursor in their rect,
// from the top node to the bottom one. this will also reset the interaction to `None`
// for all nodes encountered that are no longer hovered.
for node_entity in ui_stack
.uinodes
.iter()
// reverse the iterator to traverse the tree from closest nodes to furthest
.rev()
{
let Ok(node) = node_query.get_mut(*node_entity) else {
continue;
};
// Nodes that are not rendered should not be interactable
if node
.view_visibility
.map(|view_visibility| view_visibility.get())
!= Some(true)
{
continue;
}
let Some(camera_entity) = node
.target_camera
.map(TargetCamera::entity)
.or(default_ui_camera.get())
else {
continue;
};
let node_rect = node.node.logical_rect(node.global_transform);
// Nodes with Display::None have a (0., 0.) logical rect and can be ignored
if node_rect.size() == Vec2::ZERO {
continue;
}
// Intersect with the calculated clip rect to find the bounds of the visible region of the node
let visible_rect = node
.calculated_clip
.map(|clip| node_rect.intersect(clip.clip))
.unwrap_or(node_rect);
let pointers_on_this_cam = pointer_pos_by_camera.get(&camera_entity);
// The mouse position relative to the node
// (0., 0.) is the top-left corner, (1., 1.) is the bottom-right corner
// Coordinates are relative to the entire node, not just the visible region.
for (pointer_id, cursor_position) in pointers_on_this_cam.iter().flat_map(|h| h.iter()) {
let relative_cursor_position = (*cursor_position - node_rect.min) / node_rect.size();
if visible_rect
.normalize(node_rect)
.contains(relative_cursor_position)
{
hit_nodes
.entry((camera_entity, *pointer_id))
.or_default()
.push(*node_entity);
}
}
}
for ((camera, pointer), hovered_nodes) in hit_nodes.iter() {
// As soon as a node with a `Block` focus policy is detected, the iteration will stop on it
// because it "captures" the interaction.
let mut iter = node_query.iter_many_mut(hovered_nodes.iter());
let mut picks = Vec::new();
let mut depth = 0.0;
while let Some(node) = iter.fetch_next() {
let Some(camera_entity) = node
.target_camera
.map(TargetCamera::entity)
.or(default_ui_camera.get())
else {
continue;
};
picks.push((node.entity, HitData::new(camera_entity, depth, None, None)));
if let Some(pickable) = node.pickable {
// If an entity has a `Pickable` component, we will use that as the source of truth.
if pickable.should_block_lower {
break;
}
} else {
// If the Pickable component doesn't exist, default behavior is to block.
break;
}
depth += 0.00001; // keep depth near 0 for precision
}
let order = camera_query
.get(*camera)
.map(|(_, cam, _)| cam.order)
.unwrap_or_default() as f32
+ 0.5; // bevy ui can run on any camera, it's a special case
output.send(PointerHits::new(*pointer, picks, order));
}
}

View file

@ -54,6 +54,7 @@ git checkout v0.4.0
- [Input](#input)
- [Math](#math)
- [Movement](#movement)
- [Picking](#picking)
- [Reflection](#reflection)
- [Scene](#scene)
- [Shaders](#shaders)
@ -350,6 +351,12 @@ Example | Description
--- | ---
[Run physics in a fixed timestep](../examples/movement/physics_in_fixed_timestep.rs) | Handles input, physics, and rendering in an industry-standard way by using a fixed timestep
## Picking
Example | Description
--- | ---
[Showcases simple picking events and usage](../examples/picking/simple_picking.rs) | Demonstrates how to use picking events to spawn simple objects
## Reflection
Example | Description

View file

@ -2,6 +2,7 @@
use std::{
f32::consts::PI,
ops::Drop,
sync::{
atomic::{AtomicBool, AtomicU32, Ordering},
Arc,

View file

@ -0,0 +1,90 @@
//! A simple scene to demonstrate picking events
use bevy::{color::palettes::css::*, prelude::*};
fn main() {
let mut app = App::new();
app.add_plugins(DefaultPlugins);
app.add_systems(Startup, setup);
app.run();
}
/// set up a simple 3D scene
fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
commands
.spawn((
TextBundle {
text: Text::from_section("Click Me to get a box", TextStyle::default()),
style: Style {
position_type: PositionType::Absolute,
top: Val::Percent(10.0),
left: Val::Percent(10.0),
..default()
},
..Default::default()
},
Pickable::default(),
))
.observe(
|_click: Trigger<Pointer<Click>>,
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
mut num: Local<usize>| {
commands.spawn(PbrBundle {
mesh: meshes.add(Cuboid::new(1.0, 1.0, 1.0)),
material: materials.add(Color::srgb_u8(124, 144, 255)),
transform: Transform::from_xyz(0.0, 0.5 + 1.1 * *num as f32, 0.0),
..default()
});
*num += 1;
},
)
.observe(|evt: Trigger<Pointer<Out>>, mut texts: Query<&mut Text>| {
let mut text = texts.get_mut(evt.entity()).unwrap();
let first = text.sections.first_mut().unwrap();
first.style.color = WHITE.into();
})
.observe(|evt: Trigger<Pointer<Over>>, mut texts: Query<&mut Text>| {
let mut text = texts.get_mut(evt.entity()).unwrap();
let first = text.sections.first_mut().unwrap();
first.style.color = BLUE.into();
});
// circular base
commands
.spawn((
PbrBundle {
mesh: meshes.add(Circle::new(4.0)),
material: materials.add(Color::WHITE),
transform: Transform::from_rotation(Quat::from_rotation_x(
-std::f32::consts::FRAC_PI_2,
)),
..default()
},
Pickable::default(),
))
.observe(|click: Trigger<Pointer<Click>>| {
let click = click.event();
println!("{click:?}");
});
// light
commands.spawn(PointLightBundle {
point_light: PointLight {
shadows_enabled: true,
..default()
},
transform: Transform::from_xyz(4.0, 8.0, 4.0),
..default()
});
// camera
commands.spawn(Camera3dBundle {
transform: Transform::from_xyz(-2.5, 4.5, 9.0).looking_at(Vec3::ZERO, Vec3::Y),
..default()
});
}