use bevy::ecs::entity::EntityHashSet;
use bevy::ecs::system::SystemChangeTick;
use bevy::input::mouse::{AccumulatedMouseScroll, MouseScrollUnit};
use bevy::input::InputSystems;
use bevy::picking::pointer::{PointerId, PointerInteraction};
use bevy::picking::PickingSystems;
use bevy::prelude::*;
use bevy::reflect::ReflectMut;
use bevy::ui::UiSystems;
use bevy_cobweb::prelude::*;
use smol_str::SmolStr;
use crate::builtin::widgets::slider::*;
use crate::prelude::*;
use crate::sickle::*;
fn get_content_size(
view_entity: Entity,
children: &Query<&Children>,
shims: &Query<&ComputedNode, With<ScrollShim>>,
) -> Option<Vec2>
{
let view_children = children.get(view_entity).ok()?;
view_children
.iter()
.find_map(|child| shims.get(child).ok())
.map(|shim_node| shim_node.size())
}
fn cleanup_dead_bases(mut c: Commands, dying: Query<Entity, With<ScrollBaseDying>>)
{
for entity in dying.iter() {
c.entity(entity)
.queue(RemoveDeadScrollBase)
.remove::<ScrollBaseDying>();
}
}
fn consume_scroll_delta(
c: &mut Commands,
slider_vals: &mut ReactiveMut<SliderValue>,
entity: Entity,
correction_factor: f32,
scroll_size: f32,
mut unconsumed_delta: f32,
) -> Option<f32>
{
if unconsumed_delta == 0.0 || scroll_size <= 0.0 {
return None;
}
let Some(val) = slider_vals.get(entity).ok().and_then(|val| val.single()) else { return None };
if unconsumed_delta > 0.0 && val < 1.0 {
let available = (1. - val) * scroll_size;
let val_mut = slider_vals.get_mut(c, entity).unwrap();
if available >= unconsumed_delta * correction_factor {
let remaining = available - unconsumed_delta * correction_factor;
*val_mut = SliderValue::Single(1. - (remaining / scroll_size));
val_mut.normalize();
unconsumed_delta = 0.;
} else {
*val_mut = SliderValue::Single(1.);
let consumed = if correction_factor != 1.0 {
available / correction_factor
} else {
available
};
unconsumed_delta -= consumed;
}
} else if unconsumed_delta < 0.0 && val > 0.0 {
let available = val * scroll_size;
let val_mut = slider_vals.get_mut(c, entity).unwrap();
if available >= -unconsumed_delta * correction_factor {
let remaining = available + unconsumed_delta * correction_factor;
*val_mut = SliderValue::Single(remaining / scroll_size);
val_mut.normalize();
unconsumed_delta = 0.;
} else {
*val_mut = SliderValue::Single(0.);
let consumed = if correction_factor != 1.0 {
available / correction_factor
} else {
available
};
unconsumed_delta += consumed;
}
}
c.react().entity_event(entity, MouseScroll);
Some(unconsumed_delta)
}
#[derive(Default)]
struct MouseScrollEventTracker
{
active_id: Option<u32>,
unconsumed_delta: Vec2,
seen_entities: EntityHashSet,
block: bool,
}
impl MouseScrollEventTracker
{
fn update(&mut self, event: &On<MouseScrollEvent>) -> bool
{
if self.active_id != Some(event.event().id) {
self.active_id = Some(event.event().id);
self.unconsumed_delta = event.event().unconsumed_delta;
self.seen_entities.clear();
self.block = false;
}
if self.block {
return false;
}
let is_new = self.seen_entities.insert(event.entity);
is_new
}
fn block_propagation(&mut self)
{
self.block = true;
}
fn unconsumed_delta(&mut self) -> &mut Vec2
{
&mut self.unconsumed_delta
}
}
fn handle_mouse_scroll_event(
mut event: On<MouseScrollEvent>,
mut event_tracker: Local<MouseScrollEventTracker>,
mut c: Commands,
mut iter_children: ResMut<IterChildren>,
children: Query<&Children>,
bases: Query<(Entity, &ScrollBase, &ComputedScrollBase)>,
views: Query<(Entity, &ComputedNode), With<ScrollView>>,
shims: Query<&ComputedNode, With<ScrollShim>>,
mut slider_vals: ReactiveMut<SliderValue>,
)
{
if !event_tracker.update(&event) {
event.propagate(false);
return;
}
let mouse_scroll_unit = event.event().mouse_unit;
let hit_entity = event.entity;
let Ok((base_entity, scroll_base, computed_base)) = bases.get(hit_entity) else { return };
if !scroll_base.allow_multiscroll {
event.propagate(false);
event_tracker.block_propagation();
}
let unconsumed_delta = event_tracker.unconsumed_delta();
if *unconsumed_delta == Vec2::default() {
event.propagate(false);
event_tracker.block_propagation();
return;
}
let Some((view_entity, view_node)) =
iter_children.search(base_entity, &children, |entity| views.get(entity).ok())
else {
return;
};
let view_size = view_node.size();
let Some(content_size) = get_content_size(view_entity, &children, &shims) else { return };
let scroll_size = (content_size - view_size).max(Vec2::default());
let correction_factor = match mouse_scroll_unit {
MouseScrollUnit::Pixel => 1.0,
MouseScrollUnit::Line => scroll_base.line_size.max(1.0),
};
if let Some(horizontal) = computed_base.horizontal {
if let Some(new) = consume_scroll_delta(
&mut c,
&mut slider_vals,
horizontal,
correction_factor,
scroll_size.x,
-unconsumed_delta.x,
) {
unconsumed_delta.x = -new;
}
}
if let Some(vertical) = computed_base.vertical {
if let Some(new) = consume_scroll_delta(
&mut c,
&mut slider_vals,
vertical,
correction_factor,
scroll_size.y,
-unconsumed_delta.y,
) {
unconsumed_delta.y = -new;
}
}
}
fn apply_mouse_scroll(
change_tick: SystemChangeTick,
mut c: Commands,
mouse_scroll: Res<AccumulatedMouseScroll>,
pointers: Query<(&PointerId, &PointerInteraction)>,
)
{
if mouse_scroll.delta == Vec2::default() {
return;
}
let Some((_, ptr_interaction)) = pointers.iter().find(|(id, _)| **id == PointerId::Mouse) else { return };
for (entity, _) in ptr_interaction.iter() {
if let Ok(mut ec) = c.get_entity(*entity) {
ec.trigger(|entity| MouseScrollEvent {
unconsumed_delta: mouse_scroll.delta,
mouse_unit: mouse_scroll.unit,
id: change_tick.this_run().get(),
entity,
});
}
}
}
fn refresh_scroll_position(
bases: Query<&ComputedScrollBase>,
mut views: Query<(Entity, &mut ScrollPosition, &ComputedNode), With<ScrollView>>,
shims: Query<&ComputedNode, With<ScrollShim>>,
parents: Query<&ChildOf>,
children: Query<&Children>,
slider_vals: Reactive<SliderValue>,
)
{
for (view_entity, mut scroll_pos, view_node) in views.iter_mut() {
let view_size = view_node.size();
let inverse_scale_factor = view_node.inverse_scale_factor();
let Some(content_size) = get_content_size(view_entity, &children, &shims) else { continue };
let scroll_size = (content_size - view_size).max(Vec2::default());
let mut current = view_entity;
let res = loop {
if let Ok(res) = bases.get(current) {
break Some(res);
}
let Ok(child_of) = parents.get(current) else { break None };
current = child_of.parent();
};
let Some(computed_base) = res else { continue };
if let Some(horizontal) = computed_base.horizontal {
let mut slider_val = slider_vals.get(horizontal).copied().unwrap_or_default();
slider_val.normalize();
let val = slider_val.single().unwrap_or_default();
let computed_x_offset = val * scroll_size.x * inverse_scale_factor;
if scroll_pos.x != computed_x_offset {
scroll_pos.x = computed_x_offset;
}
}
if let Some(vertical) = computed_base.vertical {
let mut slider_val = slider_vals.get(vertical).copied().unwrap_or_default();
slider_val.normalize();
let val = slider_val.single().unwrap_or_default();
let computed_y_offset = val * scroll_size.y * inverse_scale_factor;
if scroll_pos.y != computed_y_offset {
scroll_pos.y = computed_y_offset;
}
}
}
}
fn update_scrollbar_handle_size(
base_entity: Entity,
bar_entity: Entity,
c: &mut Commands,
ps: &PseudoStateParam,
bars: &Query<(&ComputedNode, &Children), With<ScrollBar>>,
iter_children: &mut IterChildren,
children: &Query<&Children>,
bar_handles: &Query<Entity, (With<SliderHandle>, With<ScrollHandle>)>,
handles: &mut Query<&mut ComputedNode, (Without<ScrollView>, Without<ScrollShim>, Without<ScrollBar>)>,
mut transforms: &mut Query<&mut UiGlobalTransform>,
content_dim: f32,
view_dim: f32,
pseudo_state: PseudoState,
get_dim_fn: impl Fn(&ComputedNode) -> f32,
get_unrounded_size_fn: impl FnOnce(f32, &ComputedNode) -> Vec2,
get_rounded_size_fn: impl FnOnce(f32, &ComputedNode) -> Vec2,
update_transform_fn: impl Fn(&mut Vec2, f32) + Copy,
variant: &str,
)
{
let Ok((bar_node, bar_children)) = bars.get(bar_entity) else { return };
let Some(handle_entity) =
iter_children.search_descendants(bar_children, &children, |entity| bar_handles.get(entity).ok())
else {
return;
};
let Ok(mut handle_node) = handles.get_mut(handle_entity) else { return };
let proportion = if content_dim > 0.0 {
view_dim / content_dim
} else {
1.0
};
let proportion = proportion.clamp(0.0, 1.0);
if proportion == 1.0 {
ps.try_remove(c, base_entity, pseudo_state.clone());
} else {
ps.try_insert(c, base_entity, pseudo_state.clone());
}
let bar_dim = (get_dim_fn)(bar_node);
let dim_unrounded = bar_dim * proportion;
let dim_rounded = dim_unrounded.round().clamp(0., bar_dim);
let new_size_unrounded = (get_unrounded_size_fn)(dim_unrounded, &handle_node);
let new_size_rounded = (get_rounded_size_fn)(dim_rounded, &handle_node);
let handle_dim = (get_dim_fn)(&handle_node);
let adjustment = (dim_rounded - handle_dim) / 2.;
update_handle_transform_recursive(
handle_entity,
adjustment,
&mut transforms,
update_transform_fn,
children,
);
let ReflectMut::Struct(handle_reflect) = handle_node.as_partial_reflect_mut().reflect_mut() else {
unreachable!()
};
if let Err(err) = handle_reflect
.field_mut("unrounded_size")
.unwrap()
.try_apply(new_size_unrounded.as_partial_reflect())
{
error_once!("failed updating scrollbar handle unrounded {variant} for {bar_entity:?}: {err:?} (this \
error only prints once; this is a bug)");
}
if let Err(err) = handle_reflect
.field_mut("size")
.unwrap()
.try_apply(new_size_rounded.as_partial_reflect())
{
error_once!("failed updating scrollbar handle {variant} for {bar_entity:?}: {err:?} (this error only \
prints once; this is a bug)");
}
}
fn update_handle_transform_recursive(
entity: Entity,
adjustment: f32,
transforms: &mut Query<&mut UiGlobalTransform>,
update_transform_fn: impl Fn(&mut Vec2, f32) + Copy,
children_q: &Query<&Children>,
)
{
let Ok(mut transform) = transforms.get_mut(entity) else { return };
let mut temp = **transform;
(update_transform_fn)(&mut temp.translation, adjustment);
*transform = temp.into();
let Ok(children) = children_q.get(entity) else { return };
for child in children.iter() {
update_handle_transform_recursive(child, adjustment, transforms, update_transform_fn, children_q);
}
}
fn refresh_scroll_handles(
mut c: Commands,
ps: PseudoStateParam,
mut iter_children: ResMut<IterChildren>,
parents: Query<&ChildOf>,
children: Query<&Children>,
bases: Query<(Entity, &ComputedScrollBase, &Node)>,
bars: Query<(&ComputedNode, &Children), With<ScrollBar>>,
views: Query<(Entity, &ComputedNode), With<ScrollView>>,
shims: Query<&ComputedNode, With<ScrollShim>>,
bar_handles: Query<Entity, (With<SliderHandle>, With<ScrollHandle>)>,
mut handles: Query<&mut ComputedNode, (Without<ScrollView>, Without<ScrollShim>, Without<ScrollBar>)>,
mut transforms: Query<&mut UiGlobalTransform>,
)
{
for (view_entity, view_node) in views.iter() {
let view_size = view_node.size();
let content_size = get_content_size(view_entity, &children, &shims).unwrap_or_default();
let mut current = view_entity;
let res = loop {
if let Ok(res) = bases.get(current) {
break Some(res);
}
let Ok(child_of) = parents.get(current) else { break None };
current = child_of.parent();
};
let Some((base_entity, computed_base, base_node)) = res else { continue };
if base_node.display == Display::None {
continue;
}
if let Some(horizontal) = computed_base.horizontal {
update_scrollbar_handle_size(
base_entity,
horizontal,
&mut c,
&ps,
&bars,
&mut iter_children,
&children,
&bar_handles,
&mut handles,
&mut transforms,
content_size.x,
view_size.x,
HORIZONTAL_SCROLL_PSEUDO_STATE.clone(),
|node| node.size().x,
|w_unrounded, handle_node| Vec2::new(w_unrounded, handle_node.unrounded_size().y),
|w_rounded, handle_node| Vec2::new(w_rounded, handle_node.size().y),
|transform: &mut Vec2, adjustment| {
transform.x += adjustment;
},
"width",
);
}
if let Some(vertical) = computed_base.vertical {
update_scrollbar_handle_size(
base_entity,
vertical,
&mut c,
&ps,
&bars,
&mut iter_children,
&children,
&bar_handles,
&mut handles,
&mut transforms,
content_size.y,
view_size.y,
VERTICAL_SCROLL_PSEUDO_STATE.clone(),
|node| node.size().y,
|h_unrounded, handle_node| Vec2::new(handle_node.unrounded_size().x, h_unrounded),
|h_rounded, handle_node| Vec2::new(handle_node.size().x, h_rounded),
|transform: &mut Vec2, adjustment| {
transform.y += adjustment;
},
"height",
);
}
}
}
#[derive(Component)]
struct ScrollBaseDying;
struct RemoveDeadScrollBase;
impl EntityCommand for RemoveDeadScrollBase
{
fn apply(self, mut emut: EntityWorldMut)
{
let Some(old_scroll_base) = emut.take::<ComputedScrollBase>() else { return };
let world = emut.into_world_mut();
old_scroll_base.reapply_bars(world);
}
}
#[derive(Component, Default, Clone, Debug)]
struct ComputedScrollBase
{
horizontal: Option<Entity>,
vertical: Option<Entity>,
dangling: Vec<Entity>,
}
impl ComputedScrollBase
{
fn add_bar(&mut self, entity: Entity, axis: ScrollAxis)
{
match axis {
ScrollAxis::X => {
if let Some(prev) = self.horizontal.take() {
if prev != entity {
tracing::warn!("overwriting tracked horizontal scroll bar {prev:?} with {entity:?}; you may have \
an extra scrollbar");
self.dangling.push(entity);
}
}
self.horizontal = Some(entity);
}
ScrollAxis::Y => {
if let Some(prev) = self.vertical.take() {
if prev != entity {
tracing::warn!("overwriting tracked vertical scroll bar {prev:?} with {entity:?}; you may have \
an extra scrollbar");
self.dangling.push(entity);
}
}
self.vertical = Some(entity);
}
}
}
fn reapply_bars(self, world: &mut World)
{
if let Some(horizontal) = self.horizontal {
if let Some(bar) = world.get::<ScrollBar>(horizontal) {
bar.clone().apply(horizontal, world);
}
}
if let Some(vertical) = self.vertical {
if let Some(bar) = world.get::<ScrollBar>(vertical) {
bar.clone().apply(vertical, world);
}
}
for dangling in self.dangling {
if let Some(bar) = world.get::<ScrollBar>(dangling) {
bar.clone().apply(dangling, world);
}
}
}
}
pub const HORIZONTAL_SCROLL_PSEUDO_STATE: PseudoState =
PseudoState::Custom(SmolStr::new_static("HorizontalScroll"));
pub const VERTICAL_SCROLL_PSEUDO_STATE: PseudoState = PseudoState::Custom(SmolStr::new_static("VerticalScroll"));
#[derive(Reflect, Component, PartialEq, Copy, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ScrollBase
{
#[reflect(default)]
pub allow_multiscroll: bool,
#[reflect(default = "ScrollBase::default_line_size")]
pub line_size: f32,
}
impl ScrollBase
{
fn default_line_size() -> f32
{
16.0
}
}
impl Instruction for ScrollBase
{
fn apply(self, entity: Entity, world: &mut World)
{
let Ok(mut emut) = world.get_entity_mut(entity) else { return };
emut.insert(self);
if emut.contains::<ComputedScrollBase>() {
emut.remove::<ScrollBaseDying>();
} else {
emut.insert(ComputedScrollBase::default());
#[cfg(feature = "hot_reload")]
if emut.contains::<Children>() {
if let Some((_, computed_base)) = get_ancestor_mut::<ComputedScrollBase>(world, entity) {
let other_computed_base = std::mem::take(computed_base);
other_computed_base.reapply_bars(world);
}
let mut dangling = vec![];
iter_descendants_filtered(
world,
entity,
|world, entity| world.get::<ComputedScrollBase>(entity).is_none(),
|world, entity| {
if let Some(bar) = world.get::<ScrollBar>(entity) {
dangling.push((entity, bar.clone()));
}
},
);
for (entity, bar) in dangling {
bar.apply(entity, world);
}
}
}
}
fn revert(entity: Entity, world: &mut World)
{
let Ok(mut emut) = world.get_entity_mut(entity) else { return };
emut.remove::<ScrollBase>();
emut.insert(ScrollBaseDying);
}
}
impl Default for ScrollBase
{
fn default() -> Self
{
Self {
allow_multiscroll: false,
line_size: Self::default_line_size(),
}
}
}
#[derive(Reflect, Component, Default, PartialEq, Clone)]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
reflect(Serialize, Deserialize)
)]
#[require(ScrollPosition)]
pub struct ScrollView;
#[derive(Reflect, Component, Default, PartialEq, Clone)]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
reflect(Serialize, Deserialize)
)]
pub struct ScrollShim;
#[derive(Reflect, Default, PartialEq, Copy, Clone)]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
reflect(Serialize, Deserialize)
)]
pub enum ScrollAxis
{
#[default]
X,
Y,
}
impl Into<SliderAxis> for ScrollAxis
{
fn into(self: Self) -> SliderAxis
{
match self {
Self::X => SliderAxis::X,
Self::Y => SliderAxis::Y,
}
}
}
#[derive(Reflect, Component, Default, PartialEq, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ScrollBar
{
#[reflect(default)]
pub axis: ScrollAxis,
#[reflect(default)]
pub bar_press: SliderPress,
}
impl Instruction for ScrollBar
{
fn apply(self, entity: Entity, world: &mut World)
{
let Ok(mut emut) = world.get_entity_mut(entity) else { return };
emut.insert(self.clone());
let direction = match self.axis {
ScrollAxis::X => SliderDirection::Standard,
ScrollAxis::Y => SliderDirection::Reverse,
};
Slider {
axis: self.axis.into(),
direction,
bar_press: self.bar_press.clone(),
}
.apply(entity, world);
if let Some((_, computed_base)) = get_ancestor_mut::<ComputedScrollBase>(world, entity) {
computed_base.add_bar(entity, self.axis);
} else {
tracing::warn!("failed adding ScrollBar {entity:?} to scroll widget; no ancestor has ScrollBase \
(fixing this requires a restart)");
}
}
fn revert(entity: Entity, world: &mut World)
{
let Ok(mut emut) = world.get_entity_mut(entity) else { return };
emut.remove::<Self>();
Slider::revert(entity, world);
if let Some((_, computed_base)) = get_ancestor_mut::<ComputedScrollBase>(world, entity) {
let other_computed_base = std::mem::take(computed_base);
other_computed_base.reapply_bars(world);
}
}
}
#[derive(Reflect, Component, Default, PartialEq, Clone)]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
reflect(Serialize, Deserialize)
)]
#[require(SliderHandle)]
pub struct ScrollHandle;
pub struct MouseScroll;
#[derive(Component, EntityEvent)]
#[entity_event(propagate = &'static ChildOf, auto_propagate)]
pub struct MouseScrollEvent
{
pub unconsumed_delta: Vec2,
pub mouse_unit: MouseScrollUnit,
id: u32,
entity: Entity,
}
#[derive(SystemSet, Debug, Hash, Eq, PartialEq, Copy, Clone)]
pub struct ScrollUpdateSet;
#[derive(SystemSet, Debug, Hash, Eq, PartialEq, Copy, Clone)]
pub struct ScrollHandleUpdateSet;
pub(crate) struct CobwebScrollPlugin;
impl Plugin for CobwebScrollPlugin
{
fn build(&self, app: &mut App)
{
app.register_instruction_type::<ScrollBase>()
.register_component_type::<ScrollView>()
.register_component_type::<ScrollShim>()
.register_instruction_type::<ScrollBar>()
.register_component_type::<ScrollHandle>()
.configure_sets(
PreUpdate,
ScrollUpdateSet
.after(InputSystems)
.in_set(PickingSystems::Hover)
.after(bevy::picking::hover::update_interactions)
.before(bevy::picking::events::pointer_events),
)
.configure_sets(
PostUpdate,
ScrollUpdateSet
.after(FileProcessingSet)
.after(DynamicStylePostUpdate)
.before(UiSystems::Prepare),
)
.configure_sets(
PostUpdate,
ScrollHandleUpdateSet
.in_set(UiSystems::PostLayout)
.before(SliderUpdateSet),
)
.add_observer(handle_mouse_scroll_event)
.add_systems(First, cleanup_dead_bases.after(FileProcessingSet))
.add_systems(
PreUpdate,
apply_mouse_scroll.in_set(ScrollUpdateSet),
)
.add_systems(
PostUpdate,
(cleanup_dead_bases, refresh_scroll_position)
.chain()
.in_set(ScrollUpdateSet),
)
.add_systems(PostUpdate, refresh_scroll_handles.in_set(ScrollHandleUpdateSet));
}
}