
# Objective Currently, the observer API looks like this: ```rust app.add_observer(|trigger: Trigger<Explode>| { info!("Entity {} exploded!", trigger.target()); }); ``` Future plans for observers also include "multi-event observers" with a trigger that looks like this (see [Cart's example](https://github.com/bevyengine/bevy/issues/14649#issuecomment-2960402508)): ```rust trigger: Trigger<( OnAdd<Pressed>, OnRemove<Pressed>, OnAdd<InteractionDisabled>, OnRemove<InteractionDisabled>, OnInsert<Hovered>, )>, ``` In scenarios like this, there is a lot of repetition of `On`. These are expected to be very high-traffic APIs especially in UI contexts, so ergonomics and readability are critical. By renaming `Trigger` to `On`, we can make these APIs read more cleanly and get rid of the repetition: ```rust app.add_observer(|trigger: On<Explode>| { info!("Entity {} exploded!", trigger.target()); }); ``` ```rust trigger: On<( Add<Pressed>, Remove<Pressed>, Add<InteractionDisabled>, Remove<InteractionDisabled>, Insert<Hovered>, )>, ``` Names like `On<Add<Pressed>>` emphasize the actual event listener nature more than `Trigger<OnAdd<Pressed>>`, and look cleaner. This *also* frees up the `Trigger` name if we want to use it for the observer event type, splitting them out from buffered events (bikeshedding this is out of scope for this PR though). For prior art: [`bevy_eventlistener`](https://github.com/aevyrie/bevy_eventlistener) used [`On`](https://docs.rs/bevy_eventlistener/latest/bevy_eventlistener/event_listener/struct.On.html) for its event listener type. Though in our case, the observer is the event listener, and `On` is just a type containing information about the triggered event. ## Solution Steal from `bevy_event_listener` by @aevyrie and use `On`. - Rename `Trigger` to `On` - Rename `OnAdd` to `Add` - Rename `OnInsert` to `Insert` - Rename `OnReplace` to `Replace` - Rename `OnRemove` to `Remove` - Rename `OnDespawn` to `Despawn` ## Discussion ### Naming Conflicts?? Using a name like `Add` might initially feel like a very bad idea, since it risks conflict with `core::ops::Add`. However, I don't expect this to be a big problem in practice. - You rarely need to actually implement the `Add` trait, especially in modules that would use the Bevy ECS. - In the rare cases where you *do* get a conflict, it is very easy to fix by just disambiguating, for example using `ops::Add`. - The `Add` event is a struct while the `Add` trait is a trait (duh), so the compiler error should be very obvious. For the record, renaming `OnAdd` to `Add`, I got exactly *zero* errors or conflicts within Bevy itself. But this is of course not entirely representative of actual projects *using* Bevy. You might then wonder, why not use `Added`? This would conflict with the `Added` query filter, so it wouldn't work. Additionally, the current naming convention for observer events does not use past tense. ### Documentation This does make documentation slightly more awkward when referring to `On` or its methods. Previous docs often referred to `Trigger::target` or "sends a `Trigger`" (which is... a bit strange anyway), which would now be `On::target` and "sends an observer `Event`". You can see the diff in this PR to see some of the effects. I think it should be fine though, we may just need to reword more documentation to read better.
203 lines
6.8 KiB
Rust
203 lines
6.8 KiB
Rust
//! Components to customize winit cursor
|
|
|
|
use crate::{
|
|
converters::convert_system_cursor_icon,
|
|
state::{CursorSource, PendingCursor},
|
|
};
|
|
#[cfg(feature = "custom_cursor")]
|
|
use crate::{
|
|
custom_cursor::{
|
|
calculate_effective_rect, extract_and_transform_rgba_pixels, extract_rgba_pixels,
|
|
transform_hotspot, CustomCursorPlugin,
|
|
},
|
|
state::{CustomCursorCache, CustomCursorCacheKey},
|
|
WinitCustomCursor,
|
|
};
|
|
use bevy_app::{App, Last, Plugin};
|
|
#[cfg(feature = "custom_cursor")]
|
|
use bevy_asset::Assets;
|
|
#[cfg(feature = "custom_cursor")]
|
|
use bevy_ecs::system::Res;
|
|
use bevy_ecs::{
|
|
change_detection::DetectChanges,
|
|
component::Component,
|
|
entity::Entity,
|
|
lifecycle::Remove,
|
|
observer::On,
|
|
query::With,
|
|
reflect::ReflectComponent,
|
|
system::{Commands, Local, Query},
|
|
world::Ref,
|
|
};
|
|
#[cfg(feature = "custom_cursor")]
|
|
use bevy_image::{Image, TextureAtlasLayout};
|
|
use bevy_platform::collections::HashSet;
|
|
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
|
|
use bevy_window::{SystemCursorIcon, Window};
|
|
#[cfg(feature = "custom_cursor")]
|
|
use tracing::warn;
|
|
|
|
#[cfg(feature = "custom_cursor")]
|
|
pub use crate::custom_cursor::{CustomCursor, CustomCursorImage};
|
|
|
|
#[cfg(all(
|
|
feature = "custom_cursor",
|
|
target_family = "wasm",
|
|
target_os = "unknown"
|
|
))]
|
|
pub use crate::custom_cursor::CustomCursorUrl;
|
|
|
|
pub(crate) struct CursorPlugin;
|
|
|
|
impl Plugin for CursorPlugin {
|
|
fn build(&self, app: &mut App) {
|
|
#[cfg(feature = "custom_cursor")]
|
|
app.add_plugins(CustomCursorPlugin);
|
|
|
|
app.register_type::<CursorIcon>()
|
|
.add_systems(Last, update_cursors);
|
|
|
|
app.add_observer(on_remove_cursor_icon);
|
|
}
|
|
}
|
|
|
|
/// Insert into a window entity to set the cursor for that window.
|
|
#[derive(Component, Debug, Clone, Reflect, PartialEq, Eq)]
|
|
#[reflect(Component, Debug, Default, PartialEq, Clone)]
|
|
pub enum CursorIcon {
|
|
#[cfg(feature = "custom_cursor")]
|
|
/// Custom cursor image.
|
|
Custom(CustomCursor),
|
|
/// System provided cursor icon.
|
|
System(SystemCursorIcon),
|
|
}
|
|
|
|
impl Default for CursorIcon {
|
|
fn default() -> Self {
|
|
CursorIcon::System(Default::default())
|
|
}
|
|
}
|
|
|
|
impl From<SystemCursorIcon> for CursorIcon {
|
|
fn from(icon: SystemCursorIcon) -> Self {
|
|
CursorIcon::System(icon)
|
|
}
|
|
}
|
|
|
|
fn update_cursors(
|
|
mut commands: Commands,
|
|
windows: Query<(Entity, Ref<CursorIcon>), With<Window>>,
|
|
#[cfg(feature = "custom_cursor")] cursor_cache: Res<CustomCursorCache>,
|
|
#[cfg(feature = "custom_cursor")] images: Res<Assets<Image>>,
|
|
#[cfg(feature = "custom_cursor")] texture_atlases: Res<Assets<TextureAtlasLayout>>,
|
|
mut queue: Local<HashSet<Entity>>,
|
|
) {
|
|
for (entity, cursor) in windows.iter() {
|
|
if !(queue.remove(&entity) || cursor.is_changed()) {
|
|
continue;
|
|
}
|
|
|
|
let cursor_source = match cursor.as_ref() {
|
|
#[cfg(feature = "custom_cursor")]
|
|
CursorIcon::Custom(CustomCursor::Image(c)) => {
|
|
let CustomCursorImage {
|
|
handle,
|
|
texture_atlas,
|
|
flip_x,
|
|
flip_y,
|
|
rect,
|
|
hotspot,
|
|
} = c;
|
|
|
|
let cache_key = CustomCursorCacheKey::Image {
|
|
id: handle.id(),
|
|
texture_atlas_layout_id: texture_atlas.as_ref().map(|a| a.layout.id()),
|
|
texture_atlas_index: texture_atlas.as_ref().map(|a| a.index),
|
|
flip_x: *flip_x,
|
|
flip_y: *flip_y,
|
|
rect: *rect,
|
|
};
|
|
|
|
if cursor_cache.0.contains_key(&cache_key) {
|
|
CursorSource::CustomCached(cache_key)
|
|
} else {
|
|
let Some(image) = images.get(handle) else {
|
|
warn!(
|
|
"Cursor image {handle:?} is not loaded yet and couldn't be used. Trying again next frame."
|
|
);
|
|
queue.insert(entity);
|
|
continue;
|
|
};
|
|
|
|
let (rect, needs_sub_image) =
|
|
calculate_effective_rect(&texture_atlases, image, texture_atlas, rect);
|
|
|
|
let (maybe_rgba, hotspot) = if *flip_x || *flip_y || needs_sub_image {
|
|
(
|
|
extract_and_transform_rgba_pixels(image, *flip_x, *flip_y, rect),
|
|
transform_hotspot(*hotspot, *flip_x, *flip_y, rect),
|
|
)
|
|
} else {
|
|
(extract_rgba_pixels(image), *hotspot)
|
|
};
|
|
|
|
let Some(rgba) = maybe_rgba else {
|
|
warn!("Cursor image {handle:?} not accepted because it's not rgba8 or rgba32float format");
|
|
continue;
|
|
};
|
|
|
|
let source = match WinitCustomCursor::from_rgba(
|
|
rgba,
|
|
rect.width() as u16,
|
|
rect.height() as u16,
|
|
hotspot.0,
|
|
hotspot.1,
|
|
) {
|
|
Ok(source) => source,
|
|
Err(err) => {
|
|
warn!("Cursor image {handle:?} is invalid: {err}");
|
|
continue;
|
|
}
|
|
};
|
|
|
|
CursorSource::Custom((cache_key, source))
|
|
}
|
|
}
|
|
#[cfg(all(
|
|
feature = "custom_cursor",
|
|
target_family = "wasm",
|
|
target_os = "unknown"
|
|
))]
|
|
CursorIcon::Custom(CustomCursor::Url(c)) => {
|
|
let cache_key = CustomCursorCacheKey::Url(c.url.clone());
|
|
|
|
if cursor_cache.0.contains_key(&cache_key) {
|
|
CursorSource::CustomCached(cache_key)
|
|
} else {
|
|
use crate::CustomCursorExtWebSys;
|
|
let source =
|
|
WinitCustomCursor::from_url(c.url.clone(), c.hotspot.0, c.hotspot.1);
|
|
CursorSource::Custom((cache_key, source))
|
|
}
|
|
}
|
|
CursorIcon::System(system_cursor_icon) => {
|
|
CursorSource::System(convert_system_cursor_icon(*system_cursor_icon))
|
|
}
|
|
};
|
|
|
|
commands
|
|
.entity(entity)
|
|
.insert(PendingCursor(Some(cursor_source)));
|
|
}
|
|
}
|
|
|
|
/// Resets the cursor to the default icon when `CursorIcon` is removed.
|
|
fn on_remove_cursor_icon(trigger: On<Remove, CursorIcon>, mut commands: Commands) {
|
|
// Use `try_insert` to avoid panic if the window is being destroyed.
|
|
commands
|
|
.entity(trigger.target().unwrap())
|
|
.try_insert(PendingCursor(Some(CursorSource::System(
|
|
convert_system_cursor_icon(SystemCursorIcon::Default),
|
|
))));
|
|
}
|