Skip to content

Instantly share code, notes, and snippets.

@tigregalis
Created April 19, 2023 19:24
Show Gist options
  • Save tigregalis/616099785106630c73dde6557e32b251 to your computer and use it in GitHub Desktop.
Save tigregalis/616099785106630c73dde6557e32b251 to your computer and use it in GitHub Desktop.
bevy-click-handling
[package]
name = "bevy-click-handling"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
bevy = { git = "https://github.com/bevyengine/bevy", rev = "fe852fd0adbce6856f5886d66d20d62cfc936287" }
use bevy::{
ecs::{
query::WorldQuery,
system::{BoxedSystem, SystemState},
},
input::InputSystem,
prelude::*,
render::camera::NormalizedRenderTarget,
ui::{FocusPolicy, RelativeCursorPosition, UiStack, UiSystem},
window::PrimaryWindow,
};
// -- application --
fn main() {
App::new()
.add_plugins(DefaultPlugins)
// add the EventHandlerPlugin
.add_plugin(EventHandlerPlugin)
.add_systems(Startup, setup)
.run();
}
#[derive(Component)]
struct CounterState {
counter: isize,
}
const NORMAL_BUTTON: Color = Color::rgb(0.15, 0.15, 0.15);
fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
// ui camera
commands.spawn(Camera2dBundle::default());
commands
.spawn(NodeBundle {
style: Style {
size: Size::new(Val::Percent(100.0), Val::Percent(100.0)),
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
..default()
},
..default()
})
.with_children(|parent| {
parent
.spawn(ButtonBundle {
style: Style {
size: Size::new(Val::Px(150.0), Val::Px(65.0)),
// horizontally center child text
justify_content: JustifyContent::Center,
// vertically center child text
align_items: AlignItems::Center,
..default()
},
background_color: NORMAL_BUTTON.into(),
..default()
})
// add some entity-specific state with a component
.insert(CounterState { counter: 0 })
// add an On<Click> handler
.insert(On::<Click>::new(
move |In(click_event): In<Click>, // the first parameter is In<Click>
mut this: Query<(&mut CounterState, &Children)>, // any other system params are fine
mut text_children: Query<
&mut Text,
(With<Parent>, Without<CounterState>),
>| {
if let Ok((mut counter_state, children)) = this.get_mut(click_event.target)
{
counter_state.counter += 1;
bevy::log::info!(
"Button {target:?} clicked {count} time(s).",
target = click_event.target,
count = counter_state.counter
);
if let Ok(mut text) =
text_children.get_mut(*children.iter().next().unwrap())
{
text.sections[0].value = format!("{}", counter_state.counter);
}
}
},
))
.insert(On::<Mousedown>::new(
|In(mousedown_event): In<Mousedown>| {
bevy::log::info!(
"Button {target:?} (mousedown at {location:?}).",
target = mousedown_event.target,
location = mousedown_event.location,
)
},
))
.with_children(|parent| {
parent.spawn(TextBundle::from_section(
"0",
TextStyle {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 40.0,
color: Color::rgb(0.9, 0.9, 0.9),
},
));
});
});
}
// -- Plugin --
pub struct EventHandlerPlugin;
impl Plugin for EventHandlerPlugin {
fn build(&self, app: &mut App) {
app
// register events, these must also implement Target
.add_event::<Mousedown>()
.add_event::<Mouseup>()
.add_event::<Click>()
// register a system(s) to emit events
.add_systems(
PreUpdate,
emit_ui_click_events_system
.in_set(UiSystem::Focus)
.after(InputSystem),
)
// register the generic `run_handlers::<Event>` system for each event
.add_systems(PostUpdate, run_handlers::<Mousedown>)
.add_systems(PostUpdate, run_handlers::<Mouseup>)
.add_systems(PostUpdate, run_handlers::<Click>);
}
}
// -- UI mouse interaction events --
/// Mousedown fires when mouse is just pressed and target is under the cursor
#[derive(Clone)]
pub struct Mousedown {
pub target: Entity,
pub location: Vec2,
}
impl Target for Mousedown {
fn target(&self) -> Entity {
self.target
}
}
/// Mouseup fires when mouse is just released and target is under the cursor
#[derive(Clone)]
pub struct Mouseup {
pub target: Entity,
pub location: Vec2,
}
impl Target for Mouseup {
fn target(&self) -> Entity {
self.target
}
}
/// Click fires when mousedown target and mouseup target are the same
#[derive(Clone)]
pub struct Click {
pub target: Entity,
pub location: Vec2,
}
impl Target for Click {
fn target(&self) -> Entity {
self.target
}
}
// -- UI mouse interaction event emitting system --
#[derive(Default)]
pub struct MouseState {
mousedown_target: Option<Entity>,
}
#[derive(WorldQuery)]
#[world_query(mutable)]
pub struct NodeQuery {
entity: Entity,
node: &'static Node,
global_transform: &'static GlobalTransform,
relative_cursor_position: Option<&'static mut RelativeCursorPosition>,
focus_policy: Option<&'static FocusPolicy>,
calculated_clip: Option<&'static CalculatedClip>,
computed_visibility: Option<&'static ComputedVisibility>,
}
/// adapted from [`bevy::ui::ui_focus_system`]
pub fn emit_ui_click_events_system(
mut state: Local<MouseState>,
mut clicks: EventWriter<Click>,
mut mousedowns: EventWriter<Mousedown>,
mut mouseups: EventWriter<Mouseup>,
camera: Query<(&Camera, Option<&UiCameraConfig>)>,
windows: Query<&Window>,
mouse_button_input: Res<Input<MouseButton>>,
touches_input: Res<Touches>,
ui_stack: Res<UiStack>,
mut node_query: Query<NodeQuery>,
primary_window: Query<Entity, With<PrimaryWindow>>,
) {
let primary_window = primary_window.iter().next();
let mouse_released =
mouse_button_input.just_released(MouseButton::Left) || touches_input.any_just_released();
let mouse_pressed =
mouse_button_input.just_pressed(MouseButton::Left) || touches_input.any_just_pressed();
let is_ui_disabled =
|camera_ui| matches!(camera_ui, Some(&UiCameraConfig { show_ui: false, .. }));
let cursor_position = camera
.iter()
.filter(|(_, camera_ui)| !is_ui_disabled(*camera_ui))
.filter_map(|(camera, _)| {
if let Some(NormalizedRenderTarget::Window(window_ref)) =
camera.target.normalize(primary_window)
{
Some(window_ref)
} else {
None
}
})
.find_map(|window_ref| {
windows
.get(window_ref.entity())
.ok()
.and_then(|window| window.cursor_position())
})
.or_else(|| touches_input.first_pressed_position());
// 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.
let mut hovered_nodes = ui_stack
.uinodes
.iter()
// reverse the iterator to traverse the tree from closest nodes to furthest
.rev()
.filter_map(|entity| {
if let Ok(node) = node_query.get_mut(*entity) {
// Nodes that are not rendered should not be interactable
if let Some(computed_visibility) = node.computed_visibility {
if !computed_visibility.is_visible() {
return None;
}
}
let position = node.global_transform.translation();
let ui_position = position.truncate();
let extents = node.node.size() / 2.0;
let mut min = ui_position - extents;
if let Some(clip) = node.calculated_clip {
min = Vec2::max(min, clip.clip.min);
}
// The mouse position relative to the node
// (0., 0.) is the top-left corner, (1., 1.) is the bottom-right corner
let relative_cursor_position = cursor_position.map(|cursor_position| {
Vec2::new(
(cursor_position.x - min.x) / node.node.size().x,
(cursor_position.y - min.y) / node.node.size().y,
)
});
// If the current cursor position is within the bounds of the node, consider it for
// clicking
let relative_cursor_position_component = RelativeCursorPosition {
normalized: relative_cursor_position,
};
let contains_cursor = relative_cursor_position_component.mouse_over();
// Save the relative cursor position to the correct component
if let Some(mut node_relative_cursor_position_component) =
node.relative_cursor_position
{
*node_relative_cursor_position_component = relative_cursor_position_component;
}
if contains_cursor {
Some(*entity)
} else {
None
}
} else {
None
}
})
.collect::<Vec<Entity>>()
.into_iter();
// set Clicked or Hovered on top nodes. 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.by_ref());
while let Some(node) = iter.fetch_next() {
if mouse_pressed {
state.mousedown_target = Some(node.entity);
// send mousedown event
mousedowns.send(Mousedown {
target: node.entity,
location: cursor_position.unwrap(),
});
}
if mouse_released {
// send mouseup event
mouseups.send(Mouseup {
target: node.entity,
location: cursor_position.unwrap(),
});
if state.mousedown_target == Some(node.entity) {
// send click event
clicks.send(Click {
target: node.entity,
location: cursor_position.unwrap(),
});
}
}
// TODO: Focus policy / more than one
match node.focus_policy.unwrap_or(&FocusPolicy::Block) {
FocusPolicy::Block => {
break;
}
FocusPolicy::Pass => { /* allow the next node to be hovered/clicked */ }
}
}
if mouse_released {
// always reset mousedown target to none when released
state.mousedown_target = None;
}
}
// -- Generic event handling logic --
/// This trait must be implemented for event handling types, pointing to the target of the event (the owner of the event handler)
pub trait Target {
fn target(&self) -> Entity;
}
#[derive(Default)]
pub struct Handler {
system: Option<BoxedSystem>,
initialized: bool,
}
impl Handler {
fn new<Ev: Clone + Event + Target, Params>(this: impl IntoSystem<Ev, (), Params>) -> Self {
Self {
system: Some(Box::new(IntoSystem::into_system(
inject_event_into_system.pipe(this),
))),
initialized: false,
}
}
fn run(&mut self, world: &mut World) {
if let Some(this) = &mut self.system {
if !self.initialized {
this.initialize(world);
self.initialized = true;
}
this.run((), world);
};
}
}
#[derive(Resource)]
struct CurrentEvent<Ev>(Ev);
fn inject_event_into_system<Ev: Clone + Event + Target>(current: Res<CurrentEvent<Ev>>) -> Ev {
current.0.clone()
}
/// Register the `On<Event>` component on your entities, with a handler.
/// The handler is a system that runs when an event is fired, where the first parameter is an `In<Event>`
/// and all other parameters are normal Bevy system parameters.
#[derive(Component)]
struct On<Ev: Clone + Event + Target>(Handler, std::marker::PhantomData<Ev>);
impl<Ev: Clone + Event + Target> On<Ev> {
fn new<Params>(this: impl IntoSystem<Ev, (), Params>) -> Self {
Self(Handler::new(this), std::marker::PhantomData)
}
}
impl<Ev: Clone + Event + Target> Default for On<Ev> {
fn default() -> Self {
Self(Handler::default(), std::marker::PhantomData)
}
}
// We need to cache system state, otherwise the event reader will read the resource twice
#[derive(Resource)]
struct CachedSystemState<Ev: Clone + Event + Target> {
state: SystemState<(
EventReader<'static, 'static, Ev>,
Query<'static, 'static, &'static mut On<Ev>>,
)>,
}
pub fn run_handlers<Ev: Clone + Event + Target>(world: &mut World) {
if !world.contains_resource::<CachedSystemState<Ev>>() {
let state = SystemState::<(EventReader<Ev>, Query<&mut On<Ev>>)>::new(world);
world.insert_resource(CachedSystemState { state });
};
// We need to use a resource scope, to pull the cached system state out of the world
world.resource_scope(|world, mut system_state: Mut<CachedSystemState<Ev>>| {
let system_state = &mut system_state.state;
let (mut click_events, mut click_handlers) = system_state.get_mut(world);
let mut handlers_to_run = Vec::with_capacity(click_handlers.iter().count());
// Take the handlers out of the world
for ev in click_events.iter() {
if let Ok(mut handler) = click_handlers.get_mut(ev.target()) {
handlers_to_run.push((ev.clone(), std::mem::take(&mut *handler)));
};
}
// Run the handlers on the world
for (ev, handler) in handlers_to_run.iter_mut() {
{
world.insert_resource(CurrentEvent(ev.clone()));
}
handler.0.run(world);
}
// Put the handlers back into the world
let (_, mut click_handlers) = system_state.get_mut(world);
for (ev, used_handler) in handlers_to_run {
if let Ok(mut handler) = click_handlers.get_mut(ev.target()) {
*handler = used_handler;
}
}
});
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment