1use std::{
5 collections::HashSet,
6 sync::{Arc, OnceLock},
7};
8
9use super::{
10 raw::{RawElWrapper, observe, register_system, utils::remove_system_holder_on_remove},
11 utils::clone,
12};
13use apply::Apply;
14use bevy_app::prelude::*;
15use bevy_ecs::{prelude::*, system::SystemParam};
16use bevy_math::prelude::*;
17use bevy_transform::prelude::*;
18use bevy_ui::prelude::*;
19use futures_signals::signal::{Mutable, Signal};
20
21#[derive(Clone, Copy, Default, Debug)]
24pub struct Scene {
25 #[allow(missing_docs)]
26 pub width: f32,
27 #[allow(missing_docs)]
28 pub height: f32,
29}
30
31#[derive(Clone, Copy, Default, Debug)]
33pub struct Viewport {
34 pub offset_x: f32,
36 pub offset_y: f32,
38 #[allow(missing_docs)]
39 pub width: f32,
40 #[allow(missing_docs)]
41 pub height: f32,
42}
43
44#[derive(Component, Event, Default)]
49pub struct MutableViewport {
50 #[allow(missing_docs)]
51 pub scene: Scene,
52 #[allow(missing_docs)]
53 pub viewport: Viewport,
54}
55
56#[derive(Component)]
58pub struct OnViewportLocationChange;
59
60pub enum Axis {
62 #[allow(missing_docs)]
63 Horizontal,
64 #[allow(missing_docs)]
65 Vertical,
66 #[allow(missing_docs)]
67 Both,
68}
69
70#[derive(Component, Default, Debug)]
73struct LastSignalScrollPosition {
74 x: f32,
75 y: f32,
76}
77
78pub trait ViewportMutable: RawElWrapper {
82 fn mutable_viewport(self, axis: Axis) -> Self {
86 self.update_raw_el(move |raw_el| {
87 raw_el
88 .insert(MutableViewport::default())
89 .with_component::<Node>(move |mut node| {
90 node.overflow = match axis {
91 Axis::Horizontal => Overflow::scroll_x(),
92 Axis::Vertical => Overflow::scroll_y(),
93 Axis::Both => Overflow::scroll(),
94 }
95 })
96 })
97 }
98
99 fn on_viewport_location_change_with_system<Marker>(
103 self,
104 handler: impl IntoSystem<In<(Entity, (Scene, Viewport))>, (), Marker> + Send + 'static,
105 ) -> Self {
106 self.update_raw_el(|raw_el| {
107 let system_holder = Arc::new(OnceLock::new());
108 raw_el
109 .insert(OnViewportLocationChange)
110 .on_spawn(clone!((system_holder) move |world, entity| {
111 let system = register_system(world, handler);
112 let _ = system_holder.set(system);
113 observe(world, entity, move |viewport_location_change: Trigger<MutableViewport>, mut commands: Commands| {
114 let &MutableViewport { scene, viewport } = viewport_location_change.event();
115 commands.run_system_with(system, (entity, (scene, viewport)));
116 });
117 }))
118 .apply(remove_system_holder_on_remove(system_holder))
119 })
120 }
121
122 fn on_viewport_location_change(self, mut handler: impl FnMut(Scene, Viewport) + Send + Sync + 'static) -> Self {
125 self.on_viewport_location_change_with_system(move |In((_, (scene, viewport)))| handler(scene, viewport))
126 }
127
128 fn viewport_x_signal<S: Signal<Item = f32> + Send + 'static>(
130 mut self,
131 x_signal_option: impl Into<Option<S>>,
132 ) -> Self {
133 if let Some(x_signal) = x_signal_option.into() {
134 self = self.update_raw_el(|raw_el| {
135 raw_el
136 .insert(LastSignalScrollPosition::default())
137 .on_signal_with_system(
138 x_signal,
139 |In((entity, x)): In<(Entity, f32)>,
140 mut query: Query<(&mut ScrollPosition, &mut LastSignalScrollPosition)>| {
141 if let Ok((mut scroll_pos, mut last_signal_pos)) = query.get_mut(entity)
142 && last_signal_pos.x.to_bits() != x.to_bits()
143 {
144 last_signal_pos.x = x;
145 scroll_pos.offset_x = x;
146 }
147 },
148 )
149 });
150 }
151 self
152 }
153
154 fn viewport_y_signal<S: Signal<Item = f32> + Send + 'static>(
156 mut self,
157 y_signal_option: impl Into<Option<S>>,
158 ) -> Self {
159 if let Some(y_signal) = y_signal_option.into() {
160 self = self.update_raw_el(|raw_el| {
161 raw_el
162 .insert(LastSignalScrollPosition::default())
163 .on_signal_with_system(
164 y_signal,
165 |In((entity, y)): In<(Entity, f32)>,
166 mut query: Query<(&mut ScrollPosition, &mut LastSignalScrollPosition)>| {
167 if let Ok((mut scroll_pos, mut last_signal_pos)) = query.get_mut(entity)
168 && last_signal_pos.y.to_bits() != y.to_bits()
169 {
170 last_signal_pos.y = y;
171 scroll_pos.offset_y = y;
172 }
173 },
174 )
175 });
176 }
177 self
178 }
179
180 fn viewport_x_sync(self, viewport_x: Mutable<f32>) -> Self {
182 self.on_viewport_location_change_with_system(
183 move |In((entity, (_, viewport))): In<(Entity, (Scene, Viewport))>,
184 last_signal_positions: Query<&LastSignalScrollPosition>| {
185 if let Ok(last_signal_pos) = last_signal_positions.get(entity)
186 && last_signal_pos.x.to_bits() != viewport.offset_x.to_bits()
187 {
188 viewport_x.set_neq(viewport.offset_x);
189 }
190 },
191 )
192 }
193
194 fn viewport_y_sync(self, viewport_y: Mutable<f32>) -> Self {
196 self.on_viewport_location_change_with_system(
197 move |In((entity, (_, viewport))): In<(Entity, (Scene, Viewport))>,
198 last_signal_positions: Query<&LastSignalScrollPosition>| {
199 if let Ok(last_signal_pos) = last_signal_positions.get(entity)
200 && last_signal_pos.y.to_bits() != viewport.offset_y.to_bits()
201 {
202 viewport_y.set_neq(viewport.offset_y);
203 }
204 },
205 )
206 }
207}
208
209#[derive(SystemParam)]
211pub struct LogicalRect<'w, 's> {
212 data: Query<'w, 's, (&'static ComputedNode, &'static GlobalTransform)>,
213}
214
215impl LogicalRect<'_, '_> {
216 pub fn get(&self, entity: Entity) -> Option<Rect> {
218 if let Ok((computed_node, global_transform)) = self.data.get(entity) {
219 return Rect::from_center_size(global_transform.translation().xy(), computed_node.size()).apply(Some);
220 }
221 None
222 }
223}
224
225#[derive(SystemParam)]
226struct SceneViewport<'w, 's> {
227 childrens: Query<'w, 's, &'static Children>,
228 logical_rect: LogicalRect<'w, 's>,
229 scroll_positions: Query<'w, 's, &'static ScrollPosition>,
230}
231
232impl SceneViewport<'_, '_> {
233 fn get(&self, entity: Entity) -> Option<(Scene, Viewport)> {
234 if let Some(Vec2 {
235 x: viewport_width,
236 y: viewport_height,
237 }) = self.logical_rect.get(entity).as_ref().map(Rect::size)
238 && let Ok(&ScrollPosition { offset_x, offset_y }) = self.scroll_positions.get(entity)
239 {
240 let mut min = Vec2::MAX;
241 let mut max = Vec2::MIN;
242 for child in self
243 .childrens
244 .get(entity)
245 .ok()
246 .into_iter()
247 .flat_map(|children| children.iter())
248 {
249 if let Some(child_rect) = self.logical_rect.get(child) {
250 min = min.min(child_rect.min);
251 max = max.max(child_rect.max);
252 }
253 }
254 let scene = Scene {
255 width: max.x - min.x,
256 height: max.y - min.y,
257 };
258 let viewport = Viewport {
259 offset_x,
260 offset_y,
261 width: viewport_width,
262 height: viewport_height,
263 };
264 return Some((scene, viewport));
265 }
266 None
267 }
268}
269
270fn dispatch_viewport_location_change(
271 entity: Entity,
272 scene_viewports: &SceneViewport,
273 commands: &mut Commands,
274 checked_viewport_listeners: &mut HashSet<Entity>,
275) {
276 if let Some((scene, viewport)) = scene_viewports.get(entity) {
277 if let Ok(mut entity) = commands.get_entity(entity) {
278 entity.insert(MutableViewport { scene, viewport });
279 }
280 commands.trigger_targets(MutableViewport { scene, viewport }, entity);
281 checked_viewport_listeners.insert(entity);
282 }
283}
284
285#[allow(clippy::type_complexity)]
286fn viewport_location_change_dispatcher(
287 viewports: Query<
288 Entity,
289 (
290 Or<(Changed<ComputedNode>, Changed<ScrollPosition>, Changed<Children>)>,
291 With<OnViewportLocationChange>,
292 ),
293 >,
294 changed_computed_nodes: Query<Entity, Changed<ComputedNode>>,
295 viewport_location_change_listeners: Query<Entity, With<OnViewportLocationChange>>,
296 child_ofs: Query<&ChildOf>,
297 scene_viewports: SceneViewport,
298 mut commands: Commands,
299) {
300 let mut checked_viewport_listeners = HashSet::new();
301 for entity in viewports.iter() {
302 dispatch_viewport_location_change(entity, &scene_viewports, &mut commands, &mut checked_viewport_listeners);
303 }
304 for entity in changed_computed_nodes.iter() {
305 if let Ok(&ChildOf(parent)) = child_ofs.get(entity)
306 && !checked_viewport_listeners.contains(&parent)
307 && viewport_location_change_listeners.contains(parent)
308 {
309 dispatch_viewport_location_change(parent, &scene_viewports, &mut commands, &mut checked_viewport_listeners);
310 }
311 }
312}
313
314pub(super) fn plugin(app: &mut App) {
315 app.add_systems(
316 Update,
317 viewport_location_change_dispatcher.run_if(any_with_component::<OnViewportLocationChange>),
318 );
319}