use super::{
pointer_event_aware::{HoverData, PointerEventAware, disableable_signal_setup, disableable_signal_system},
utils::{clone, observe, register_system, remove_system_holder_on_despawn},
viewport_mutable::ViewportMutable,
};
use apply::Apply;
use bevy_app::prelude::*;
use bevy_ecs::prelude::*;
use bevy_input::{mouse::*, prelude::*};
use bevy_math::Vec2;
use bevy_platform::sync::{Arc, OnceLock};
use bevy_ui::prelude::*;
use jonmo::signal::Signal;
#[derive(Component, Default, Clone)]
pub struct ScrollDisabled;
#[derive(Component)]
struct ScrollEnabled;
pub trait MouseWheelScrollable: ViewportMutable {
fn on_scroll_disableable<Disabled: Component, Marker>(
self,
handler: impl IntoSystem<In<(Entity, MouseWheel)>, (), Marker> + Send + Sync + 'static,
) -> Self {
self.with_builder(|builder| {
let system_holder = Arc::new(OnceLock::new());
builder
.insert(ScrollEnabled)
.observe(|event: On<Add, Disabled>, mut commands: Commands| {
if let Ok(mut entity) = commands.get_entity(event.event().entity) {
entity.remove::<ScrollEnabled>();
}
})
.observe(move |event: On<Remove, Disabled>, mut commands: Commands| {
if let Ok(mut entity) = commands.get_entity(event.event().entity) {
entity.try_insert(ScrollEnabled);
}
})
.on_spawn(clone!((system_holder) move |world, entity| {
let system = register_system(world, handler);
let _ = system_holder.set(system);
observe(world, entity, move |mouse_wheel: On<MouseWheelEvent>, mut commands: Commands| {
let MouseWheelEvent { entity, mouse_wheel } = *mouse_wheel.event();
commands.run_system_with(system, (entity, mouse_wheel));
});
}))
.apply(remove_system_holder_on_despawn(system_holder))
})
}
fn on_scroll<Marker>(
self,
handler: impl IntoSystem<In<(Entity, MouseWheel)>, (), Marker> + Send + Sync + 'static,
) -> Self {
self.on_scroll_disableable::<ScrollDisabled, Marker>(handler)
}
fn on_scroll_disableable_signal<Marker>(
self,
handler: impl IntoSystem<In<(Entity, MouseWheel)>, (), Marker> + Send + Sync + 'static,
disabled: impl Signal<Item = bool> + 'static,
) -> Self {
let system_holder = Arc::new(OnceLock::new());
let state_index = Arc::new(OnceLock::new());
self.with_builder(disableable_signal_setup(
handler,
disabled,
system_holder.clone(),
state_index.clone(),
))
.on_scroll_disableable::<ScrollDisabled, _>(disableable_signal_system(system_holder, state_index))
}
}
pub trait OnHoverMouseWheelScrollable: MouseWheelScrollable + PointerEventAware {
fn on_scroll_on_hover_disableable<Disabled: Component + Default, Marker>(
self,
handler: impl IntoSystem<In<(Entity, MouseWheel)>, (), Marker> + Send + Sync + 'static,
) -> Self {
self.on_hovered_change(|In((entity, data)): In<(Entity, HoverData)>, mut commands: Commands| {
if let Ok(mut entity) = commands.get_entity(entity) {
if data.hovered {
entity.remove::<Disabled>();
} else {
entity.try_insert(Disabled::default());
}
}
})
.on_scroll_disableable::<Disabled, _>(handler)
.with_builder(|builder| builder.insert(Disabled::default()))
}
fn on_scroll_on_hover<Marker>(
self,
handler: impl IntoSystem<In<(Entity, MouseWheel)>, (), Marker> + Send + Sync + 'static,
) -> Self {
self.on_scroll_on_hover_disableable::<ScrollDisabled, _>(handler)
}
fn on_scroll_on_hover_disableable_signal<Marker>(
self,
handler: impl IntoSystem<In<(Entity, MouseWheel)>, (), Marker> + Send + Sync + 'static,
disabled: impl Signal<Item = bool> + 'static,
) -> Self {
let system_holder = Arc::new(OnceLock::new());
let state_index = Arc::new(OnceLock::new());
self.with_builder(disableable_signal_setup(
handler,
disabled,
system_holder.clone(),
state_index.clone(),
))
.on_scroll_on_hover_disableable::<ScrollDisabled, _>(disableable_signal_system(system_holder, state_index))
}
}
impl<T: PointerEventAware + MouseWheelScrollable> OnHoverMouseWheelScrollable for T {}
#[derive(EntityEvent)]
pub struct MouseWheelEvent {
entity: Entity,
mouse_wheel: MouseWheel,
}
fn scroll_system(
mut mouse_wheel_events: MessageReader<MouseWheel>,
scroll_listeners: Query<Entity, With<ScrollEnabled>>,
mut commands: Commands,
) {
let listeners = scroll_listeners.iter().collect::<Vec<_>>();
for &event in mouse_wheel_events.read() {
for &entity in &listeners {
commands.trigger(MouseWheelEvent {
entity,
mouse_wheel: event,
});
}
}
}
#[allow(missing_docs)]
#[derive(Clone, Copy, PartialEq)]
pub enum ScrollDirection {
Horizontal,
Vertical,
Both,
}
impl Default for ScrollDirection {
fn default() -> Self {
DEFAULT_SCROLL_DIRECTION
}
}
#[derive(Component, Clone, Copy, Default)]
pub struct ScrollMagnitude(pub f32);
#[derive(Default)]
pub struct BasicScrollHandler {
direction: ScrollDirection,
magnitude: f32,
}
const DEFAULT_SCROLL_DIRECTION: ScrollDirection = ScrollDirection::Vertical;
const DEFAULT_SCROLL_MAGNITUDE: f32 = 10.;
pub fn scroll_normalizer(unit: MouseScrollUnit, scroll: f32, magnitude: f32) -> f32 {
match unit {
MouseScrollUnit::Line => scroll * magnitude,
MouseScrollUnit::Pixel => scroll.abs().min(magnitude) * scroll.signum(),
}
}
impl BasicScrollHandler {
#[allow(missing_docs)]
pub fn new() -> Self {
Self {
direction: DEFAULT_SCROLL_DIRECTION,
magnitude: DEFAULT_SCROLL_MAGNITUDE,
}
}
pub fn direction(mut self, direction: ScrollDirection) -> Self {
self.direction = direction;
self
}
pub fn pixels(mut self, pixels: f32) -> Self {
self.magnitude = pixels;
self
}
#[allow(clippy::type_complexity)]
pub fn into_system(
self,
) -> Box<
dyn FnMut(In<(Entity, MouseWheel)>, Res<ButtonInput<KeyCode>>, Query<&mut ScrollPosition>, Query<&ComputedNode>)
+ Send
+ Sync
+ 'static,
> {
let BasicScrollHandler { direction, magnitude } = self;
let f = move |In((entity, mouse_wheel)): In<(Entity, MouseWheel)>,
keys: Res<ButtonInput<KeyCode>>,
mut scroll_positions: Query<&mut ScrollPosition>,
computed_nodes: Query<&ComputedNode>| {
let dy = scroll_normalizer(mouse_wheel.unit, mouse_wheel.y, magnitude);
if let Ok(mut scroll_position) = scroll_positions.get_mut(entity) {
if matches!(direction, ScrollDirection::Vertical)
|| matches!(direction, ScrollDirection::Both)
&& !(keys.pressed(KeyCode::ShiftLeft) || keys.pressed(KeyCode::ShiftRight))
{
scroll_position.y -= dy;
} else if matches!(direction, ScrollDirection::Horizontal)
|| matches!(direction, ScrollDirection::Both)
&& (keys.pressed(KeyCode::ShiftLeft) || keys.pressed(KeyCode::ShiftRight))
{
scroll_position.x -= dy;
}
let clamped = clamp_scroll_position(
Vec2::new(scroll_position.x, scroll_position.y),
computed_nodes.get(entity).ok(),
);
scroll_position.x = clamped.x;
scroll_position.y = clamped.y;
}
};
Box::new(f)
}
}
fn max_scroll_offset(node: &ComputedNode) -> Vec2 {
(node.content_size - node.size() + node.scrollbar_size).max(Vec2::ZERO)
}
fn clamp_scroll_position(position: Vec2, node: Option<&ComputedNode>) -> Vec2 {
match node {
Some(node) => position.clamp(Vec2::ZERO, max_scroll_offset(node)),
None => position.max(Vec2::ZERO),
}
}
pub(super) fn plugin(app: &mut App) {
app.add_systems(Update, scroll_system.run_if(any_with_component::<ScrollEnabled>));
}