use std::{
collections::HashSet,
sync::{Arc, OnceLock},
};
use super::{
element::BuilderWrapper,
utils::{clone, observe, register_system, remove_system_holder_on_despawn},
};
use apply::Apply;
use bevy_app::prelude::*;
use bevy_ecs::{prelude::*, system::SystemParam};
use bevy_math::prelude::*;
use bevy_ui::prelude::*;
use jonmo::signal::Signal;
#[derive(Clone, Copy, Default, Debug)]
pub struct Scene {
#[allow(missing_docs)]
pub width: f32,
#[allow(missing_docs)]
pub height: f32,
}
#[derive(Clone, Copy, Default, Debug)]
pub struct Viewport {
pub offset_x: f32,
pub offset_y: f32,
#[allow(missing_docs)]
pub width: f32,
#[allow(missing_docs)]
pub height: f32,
}
#[derive(Component, Default)]
pub struct MutableViewport {
#[allow(missing_docs)]
pub scene: Scene,
#[allow(missing_docs)]
pub viewport: Viewport,
}
#[derive(EntityEvent)]
pub struct MutableViewportEvent {
pub entity: Entity,
pub mutable_viewport: MutableViewport,
}
#[derive(Component)]
pub struct OnViewportLocationChange;
#[derive(Component, Default, Debug)]
struct LastSignalScrollPosition {
x: f32,
y: f32,
}
pub trait ViewportMutable: BuilderWrapper {
fn mutable_viewport(self, overflow: Overflow) -> Self {
self.with_builder(move |builder| {
builder
.insert(MutableViewport::default())
.with_component::<Node>(move |mut node| {
node.overflow = overflow;
})
})
}
fn on_viewport_location_change<Marker>(
self,
handler: impl IntoSystem<In<(Entity, (Scene, Viewport))>, (), Marker> + Send + Sync + 'static,
) -> Self {
self.with_builder(|builder| {
let system_holder = Arc::new(OnceLock::new());
builder
.insert(OnViewportLocationChange)
.on_spawn(clone!((system_holder) move |world, entity| {
let system = register_system(world, handler);
let _ = system_holder.set(system);
observe(world, entity, move |viewport_location_change: On<MutableViewportEvent>, mut commands: Commands| {
let &MutableViewportEvent { mutable_viewport: MutableViewport { scene, viewport }, .. } = viewport_location_change.event();
commands.run_system_with(system, (entity, (scene, viewport)));
});
}))
.apply(remove_system_holder_on_despawn(system_holder))
})
}
fn viewport_x_signal<S: Signal<Item = f32> + Send + 'static>(
mut self,
x_signal_option: impl Into<Option<S>>,
) -> Self {
if let Some(x_signal) = x_signal_option.into() {
self = self.with_builder(|builder| {
builder.insert(LastSignalScrollPosition::default()).on_signal(
x_signal,
|In((entity, x)): In<(Entity, f32)>,
mut query: Query<(
&mut ScrollPosition,
&mut LastSignalScrollPosition,
Option<&ComputedNode>,
)>| {
if let Ok((mut scroll_pos, mut last_signal_pos, maybe_node)) = query.get_mut(entity)
&& last_signal_pos.x.to_bits() != x.to_bits()
{
let mut target = x;
target = if let Some(node) = maybe_node {
target.clamp(0.0, max_scroll_offset(node).x)
} else {
target.max(0.0)
};
last_signal_pos.x = target;
scroll_pos.x = target;
}
},
)
});
}
self
}
fn viewport_y_signal<S: Signal<Item = f32> + Send + 'static>(
mut self,
y_signal_option: impl Into<Option<S>>,
) -> Self {
if let Some(y_signal) = y_signal_option.into() {
self = self.with_builder(|builder| {
builder.insert(LastSignalScrollPosition::default()).on_signal(
y_signal,
|In((entity, y)): In<(Entity, f32)>,
mut query: Query<(
&mut ScrollPosition,
&mut LastSignalScrollPosition,
Option<&ComputedNode>,
)>| {
if let Ok((mut scroll_pos, mut last_signal_pos, maybe_node)) = query.get_mut(entity)
&& last_signal_pos.y.to_bits() != y.to_bits()
{
let mut target = y;
target = if let Some(node) = maybe_node {
target.clamp(0.0, max_scroll_offset(node).y)
} else {
target.max(0.0)
};
last_signal_pos.y = target;
scroll_pos.y = target;
}
},
)
});
}
self
}
}
#[derive(SystemParam)]
pub struct LogicalRect<'w, 's> {
data: Query<'w, 's, (&'static ComputedNode, &'static UiGlobalTransform)>,
}
impl LogicalRect<'_, '_> {
pub fn get(&self, entity: Entity) -> Option<Rect> {
if let Ok((computed_node, global_transform)) = self.data.get(entity) {
return Rect::from_center_size(global_transform.translation.xy(), computed_node.size()).apply(Some);
}
None
}
}
#[derive(SystemParam)]
struct SceneViewport<'w, 's> {
childrens: Query<'w, 's, &'static Children>,
logical_rect: LogicalRect<'w, 's>,
scroll_positions: Query<'w, 's, &'static ScrollPosition>,
}
impl SceneViewport<'_, '_> {
fn get(&self, entity: Entity) -> Option<(Scene, Viewport)> {
if let Some(Vec2 {
x: viewport_width,
y: viewport_height,
}) = self.logical_rect.get(entity).as_ref().map(Rect::size)
&& let Ok(&ScrollPosition(Vec2 {
x: offset_x,
y: offset_y,
})) = self.scroll_positions.get(entity)
{
let mut min = Vec2::MAX;
let mut max = Vec2::MIN;
for child in self
.childrens
.get(entity)
.ok()
.into_iter()
.flat_map(|children| children.iter())
{
if let Some(child_rect) = self.logical_rect.get(child) {
min = min.min(child_rect.min);
max = max.max(child_rect.max);
}
}
let scene = Scene {
width: max.x - min.x,
height: max.y - min.y,
};
let viewport = Viewport {
offset_x,
offset_y,
width: viewport_width,
height: viewport_height,
};
return Some((scene, viewport));
}
None
}
}
fn dispatch_viewport_location_change(
entity: Entity,
scene_viewports: &SceneViewport,
commands: &mut Commands,
checked_viewport_listeners: &mut HashSet<Entity>,
) {
if let Some((scene, viewport)) = scene_viewports.get(entity) {
if let Ok(mut entity) = commands.get_entity(entity) {
entity.insert(MutableViewport { scene, viewport });
}
commands.trigger(MutableViewportEvent {
entity,
mutable_viewport: MutableViewport { scene, viewport },
});
checked_viewport_listeners.insert(entity);
}
}
#[allow(clippy::type_complexity)]
fn viewport_location_change_dispatcher(
viewports: Query<
Entity,
(
Or<(Changed<ComputedNode>, Changed<ScrollPosition>, Changed<Children>)>,
With<OnViewportLocationChange>,
),
>,
changed_computed_nodes: Query<Entity, Changed<ComputedNode>>,
viewport_location_change_listeners: Query<Entity, With<OnViewportLocationChange>>,
child_ofs: Query<&ChildOf>,
scene_viewports: SceneViewport,
mut commands: Commands,
) {
let mut checked_viewport_listeners = HashSet::new();
for entity in viewports.iter() {
dispatch_viewport_location_change(entity, &scene_viewports, &mut commands, &mut checked_viewport_listeners);
}
for entity in changed_computed_nodes.iter() {
if let Ok(&ChildOf(parent)) = child_ofs.get(entity)
&& !checked_viewport_listeners.contains(&parent)
&& viewport_location_change_listeners.contains(parent)
{
dispatch_viewport_location_change(parent, &scene_viewports, &mut commands, &mut checked_viewport_listeners);
}
}
}
fn max_scroll_offset(node: &ComputedNode) -> Vec2 {
(node.content_size - node.size() + node.scrollbar_size).max(Vec2::ZERO)
}
pub(super) fn plugin(app: &mut App) {
app.add_systems(
Update,
viewport_location_change_dispatcher.run_if(any_with_component::<OnViewportLocationChange>),
);
}