use bevy::{
ecs::{lifecycle::HookContext, world::DeferredWorld},
input::mouse::MouseScrollUnit,
prelude::*,
};
use log::{debug, warn};
use crate::{ScrollSpeed, Scrollable, ScrollableLineHeight};
#[derive(Component, Clone, Reflect, Debug)]
#[relationship(relationship_target = Scrollable)]
#[require(Node, ThumbColor, DragSpeed)]
#[component(immutable)]
#[component(on_add = spawn_thumb_and_observers)]
pub struct Scrollbar {
pub scrollable: Entity,
}
#[derive(Component, Default, Copy, Clone, Reflect, Debug)]
#[component(immutable)]
pub struct ThumbColor(pub Color);
#[derive(Component, Copy, Clone, Reflect, Debug)]
pub struct DragSpeed(pub f32);
impl Default for DragSpeed {
fn default() -> Self {
Self(Self::DEFAULT)
}
}
impl DragSpeed {
pub const DEFAULT: f32 = 4.0;
}
fn spawn_thumb_and_observers(mut world: DeferredWorld, HookContext { entity, .. }: HookContext) {
let &Scrollbar { scrollable } = world.get::<Scrollbar>(entity).unwrap();
world.commands().queue(move |world: &mut World| {
let Ok(mut scrollable) = world.get_entity_mut(scrollable) else {
warn!(
"Scrollbar setup aborted. Scrollable entity {} does not exist.",
scrollable.index()
);
return;
};
let Some(mut node) = scrollable.get_mut::<Node>() else {
warn!(
"Scrollbar setup aborted. Scrollable entity {} is missing the Node component.",
scrollable.id().index()
);
return;
};
enum ScrollDirection {
Vertical,
Horizontal,
}
let direction = match (node.overflow.x, node.overflow.y) {
(_, OverflowAxis::Scroll) => ScrollDirection::Vertical,
(OverflowAxis::Scroll, _) => ScrollDirection::Horizontal,
(_, _) => {
node.overflow = Overflow::scroll_y();
ScrollDirection::Vertical
}
};
if matches!(direction, ScrollDirection::Vertical)
&& !scrollable.contains::<ScrollableLineHeight>()
{
scrollable.insert(ScrollableLineHeight::default());
}
scrollable.observe(scroll_content_on_mouse_scroll);
let Ok(mut scrollbar) = world.get_entity_mut(entity) else {
warn!(
"Scrollbar setup aborted. Scrollbar entity {} does not exist.",
entity.index()
);
return;
};
scrollbar.observe(jump_content_on_trough_click);
let border_radius = scrollbar.get::<Node>().unwrap().border_radius;
let node = match direction {
ScrollDirection::Vertical => Node {
width: Val::Percent(100.0),
height: Val::ZERO,
border_radius,
..default()
},
ScrollDirection::Horizontal => Node {
width: Val::ZERO,
height: Val::Percent(100.0),
border_radius,
..default()
},
};
let thumb_color = scrollbar.get::<ThumbColor>().unwrap().0;
world
.spawn((node, ChildOf(entity), BackgroundColor(thumb_color)))
.observe(scroll_content_on_thumb_drag);
});
}
fn scroll_content_on_mouse_scroll(
scroll: On<Pointer<Scroll>>,
mut q_scrollable: Query<(
&mut ScrollPosition,
&Node,
&ScrollSpeed,
Option<&ScrollableLineHeight>,
)>,
) -> Result {
let scrollable = scroll.entity;
let (mut scroll_position, node, scroll_speed, line_height) =
q_scrollable.get_mut(scrollable)?;
let mouse_scroll = match (scroll.unit, line_height) {
(MouseScrollUnit::Line, Some(line_height)) => scroll.y * line_height.px(),
_ => scroll.y,
};
let scroll = scroll_speed.0 * mouse_scroll;
if node.overflow.y == OverflowAxis::Scroll {
scroll_position.y -= scroll;
} else if node.overflow.x == OverflowAxis::Scroll {
scroll_position.x -= scroll;
};
Ok(())
}
fn scroll_content_on_thumb_drag(
drag: On<Pointer<Drag>>,
q_child_of: Query<&ChildOf>,
q_scrollbar: Query<(&Scrollbar, &DragSpeed)>,
mut q_scrollable: Query<(&mut ScrollPosition, &Node)>,
) -> Result {
let thumb = drag.entity;
let scrollbar = q_child_of.get(thumb)?.parent();
let (&Scrollbar { scrollable }, drag_speed) = q_scrollbar.get(scrollbar)?;
let (mut scroll_position, node) = q_scrollable.get_mut(scrollable)?;
if node.overflow.y == OverflowAxis::Scroll {
scroll_position.y += drag_speed.0 * drag.delta.y;
} else if node.overflow.x == OverflowAxis::Scroll {
scroll_position.x += drag_speed.0 * drag.delta.x;
};
Ok(())
}
fn jump_content_on_trough_click(
click: On<Pointer<Click>>,
q_scrollbar: Query<(&Scrollbar, &ComputedNode, &Children)>,
q_node: Query<(&Node, &ComputedNode)>,
mut q_scroll_position: Query<&mut ScrollPosition>,
) -> Result {
let scrollbar = click.entity;
if scrollbar != click.original_event_target() {
return Ok(());
}
let Some(click_position) = click.hit.position else {
warn!("Scrollbar Click observed but hit position is missing to move the thumb");
return Ok(());
};
let (&Scrollbar { scrollable }, track_cnode, children) = q_scrollbar.get(scrollbar)?;
let thumb = children[0];
let (_, thumb_cnode) = q_node.get(thumb)?;
let (scrollable_node, scrollable_cnode) = q_node.get(scrollable)?;
let mut scroll_position = q_scroll_position.get_mut(scrollable)?;
if scrollable_node.overflow.y == OverflowAxis::Scroll {
let offset_y = ((0.5 + click_position.y) * track_cnode.size.y).clamp(
thumb_cnode.size.y / 2.0,
track_cnode.size.y - thumb_cnode.size.y / 2.0,
);
let ratio =
(offset_y - thumb_cnode.size.y / 2.0) / (track_cnode.size.y - thumb_cnode.size.y);
scroll_position.y = track_cnode.inverse_scale_factor
* ratio
* (scrollable_cnode.content_size.y - scrollable_cnode.size.y);
debug!("click_position.y: {}", click_position.y);
debug!("offset_y: {offset_y}");
debug!("ratio: {}\n", click_position.y);
} else if scrollable_node.overflow.x == OverflowAxis::Scroll {
let offset_x = ((0.5 + click_position.x) * track_cnode.size.x).clamp(
thumb_cnode.size.x / 2.0,
track_cnode.size.x - thumb_cnode.size.x / 2.0,
);
let ratio =
(offset_x - thumb_cnode.size.x / 2.0) / (track_cnode.size.x - thumb_cnode.size.x);
scroll_position.x = track_cnode.inverse_scale_factor
* ratio
* (scrollable_cnode.content_size.x - scrollable_cnode.size.x);
};
Ok(())
}