bevy_simple_scroll_view/src/lib.rs

235 lines
7.0 KiB
Rust

#![doc = include_str!("../README.md")]
use bevy::{
input::mouse::{MouseMotion, MouseWheel},
prelude::*,
};
/// A `Plugin` providing the systems and components required to make a ScrollView work.
///
/// # Example
/// ```no_run
/// use bevy::prelude::*;
/// use bevy_simple_scroll_view::*;
///
/// App::new()
/// .add_plugins((DefaultPlugins,ScrollViewPlugin))
/// .run();
/// ```
pub struct ScrollViewPlugin;
impl Plugin for ScrollViewPlugin {
fn build(&self, app: &mut App) {
app.register_type::<ScrollView>()
.register_type::<ScrollableContent>()
.add_systems(
Update,
(
create_scroll_view,
update_size,
input_mouse_pressed_move,
input_touch_pressed_move,
scroll_events,
scroll_update,
)
.chain(),
);
}
}
/// Root component of scroll, it should have clipped style.
#[derive(Component, Debug, Reflect)]
#[require(Interaction, Node(scroll_view_node))]
pub struct ScrollView {
/// Field which control speed of the scrolling.
/// Could be negative number to implement invert scroll
pub scroll_speed: f32,
}
impl Default for ScrollView {
fn default() -> Self {
Self {
scroll_speed: 1200.0,
}
}
}
/// Component containing offset value of the scroll container to the parent.
/// It is possible to update the field `pos_y` manually to move scrollview to desired location.
#[derive(Component, Debug, Reflect, Default)]
#[require(Node(scroll_content_node))]
pub struct ScrollableContent {
/// Scroll container offset to the `ScrollView`.
pub pos_y: f32,
/// Maximum value for the scroll. It is updated automatically based on the size of the children nodes.
pub max_scroll: f32,
}
impl ScrollableContent {
/// Scrolls to the top of the scroll view.
pub fn scroll_to_top(&mut self) {
self.pos_y = 0.0;
}
/// Scrolls to the bottom of the scroll view.
pub fn scroll_to_bottom(&mut self) {
self.pos_y = -self.max_scroll;
}
/// Scrolls by a specified amount.
///
/// # Parameters
/// - `value`: The amount to scroll vertically. Positive values scroll down,
/// and negative values scroll up.
///
/// Ensures the new position is clamped between the valid scroll range.
pub fn scroll_by(&mut self, value: f32) {
self.pos_y += value;
self.pos_y = self.pos_y.clamp(-self.max_scroll, 0.);
}
}
/// Creates a default scroll view node.
///
/// This function defines the visual and layout properties of a scrollable container.
pub fn scroll_view_node() -> Node {
Node {
overflow: Overflow::clip(),
align_items: AlignItems::Start,
align_self: AlignSelf::Stretch,
flex_direction: FlexDirection::Row,
..default()
}
}
/// Creates a default scroll content node.
pub fn scroll_content_node() -> Node {
Node {
flex_direction: bevy::ui::FlexDirection::Column,
width: Val::Percent(100.0),
..default()
}
}
/// Applies the default scroll view style to newly added `ScrollView` components.
///
/// This function updates the style of all new `ScrollView` nodes with the default
/// properties defined in `scroll_view_node`.
pub fn create_scroll_view(mut q: Query<&mut Node, Added<ScrollView>>) {
let Node {
overflow,
align_items,
align_self,
flex_direction,
..
} = scroll_view_node();
for mut style in q.iter_mut() {
style.overflow = overflow;
style.align_items = align_items;
style.align_self = align_self;
style.flex_direction = flex_direction;
}
}
fn input_mouse_pressed_move(
mut motion_evr: EventReader<MouseMotion>,
mut q: Query<(&Children, &Interaction), With<ScrollView>>,
mut content_q: Query<&mut ScrollableContent>,
) {
for evt in motion_evr.read() {
for (children, &interaction) in q.iter_mut() {
if interaction != Interaction::Pressed {
continue;
}
for &child in children.iter() {
let Ok(mut scroll) = content_q.get_mut(child) else {
continue;
};
scroll.scroll_by(evt.delta.y);
}
}
}
}
fn update_size(
mut q: Query<(&Children, &ComputedNode), With<ScrollView>>,
mut content_q: Query<(&mut ScrollableContent, &ComputedNode), Changed<ComputedNode>>,
) {
for (children, scroll_view_node) in q.iter_mut() {
let container_height = scroll_view_node.size().y * scroll_view_node.inverse_scale_factor();
for &child in children.iter() {
let Ok((mut scroll, node)) = content_q.get_mut(child) else {
continue;
};
scroll.max_scroll =
(node.size().y * node.inverse_scale_factor() - container_height).max(0.0);
#[cfg(feature = "extra_logs")]
info!(
"CONTAINER {}, max_scroll: {}",
container_height, scroll.max_scroll
);
}
}
}
fn input_touch_pressed_move(
touches: Res<Touches>,
mut q: Query<(&Children, &Interaction), With<ScrollView>>,
mut content_q: Query<&mut ScrollableContent>,
) {
for t in touches.iter() {
let Some(touch) = touches.get_pressed(t.id()) else {
continue;
};
for (children, &interaction) in q.iter_mut() {
if interaction != Interaction::Pressed {
continue;
}
for &child in children.iter() {
let Ok(mut scroll) = content_q.get_mut(child) else {
continue;
};
scroll.scroll_by(touch.delta().y);
}
}
}
}
fn scroll_events(
mut scroll_evr: EventReader<MouseWheel>,
mut q: Query<(&Children, &Interaction, &ScrollView), With<ScrollView>>,
time: Res<Time>,
mut content_q: Query<&mut ScrollableContent>,
) {
use bevy::input::mouse::MouseScrollUnit;
for ev in scroll_evr.read() {
for (children, &interaction, scroll_view) in q.iter_mut() {
if interaction != Interaction::Hovered {
continue;
}
let y = match ev.unit {
MouseScrollUnit::Line => {
ev.y * time.delta().as_secs_f32() * scroll_view.scroll_speed
}
MouseScrollUnit::Pixel => ev.y,
};
#[cfg(feature = "extra_logs")]
info!("Scroolling by {:#?}: {} movement", ev.unit, y);
for &child in children.iter() {
let Ok(mut scroll) = content_q.get_mut(child) else {
continue;
};
scroll.scroll_by(y);
}
}
}
}
fn scroll_update(mut q: Query<(&ScrollableContent, &mut Node), Changed<ScrollableContent>>) {
for (scroll, mut style) in q.iter_mut() {
style.top = Val::Px(scroll.pos_y);
}
}