1use crate::engine::ecs::component::{RenderableComponent, StencilClipComponent};
2use crate::engine::ecs::component::{RouterComponent, ScrollingComponent, TransformComponent};
3use crate::engine::ecs::rx::RxWorld;
4use crate::engine::ecs::{ComponentId, EventSignal, IntentValue, SignalEmitter, SignalKind, World};
5use crate::engine::graphics::primitives::TransformMatrix;
6use crate::utils::math;
7
8#[derive(Debug, Default)]
9pub struct ScrollingSystem;
10
11impl ScrollingSystem {
12 const LAYOUT_BG_LABEL: &'static str = "__bg";
13 const OWNED_ROUTER_LABEL: &'static str = "__scroll_router";
14 const OWNED_TRACK_LABEL: &'static str = "__scroll_track";
15
16 pub fn new() -> Self {
17 Self
18 }
19
20 fn mat4_identity() -> TransformMatrix {
21 [
22 [1.0, 0.0, 0.0, 0.0],
23 [0.0, 1.0, 0.0, 0.0],
24 [0.0, 0.0, 1.0, 0.0],
25 [0.0, 0.0, 0.0, 1.0],
26 ]
27 }
28
29 fn mat4_mul_vec4(m: TransformMatrix, v: [f32; 4]) -> [f32; 4] {
30 [
31 m[0][0] * v[0] + m[1][0] * v[1] + m[2][0] * v[2] + m[3][0] * v[3],
32 m[0][1] * v[0] + m[1][1] * v[1] + m[2][1] * v[2] + m[3][1] * v[3],
33 m[0][2] * v[0] + m[1][2] * v[1] + m[2][2] * v[2] + m[3][2] * v[3],
34 m[0][3] * v[0] + m[1][3] * v[1] + m[2][3] * v[2] + m[3][3] * v[3],
35 ]
36 }
37
38 fn immediate_owned_track(world: &World, scroll_component: ComponentId) -> Option<ComponentId> {
39 world
40 .children_of(scroll_component)
41 .iter()
42 .copied()
43 .find(|&child| {
44 world.component_label(child) == Some(Self::OWNED_TRACK_LABEL)
45 && world
46 .get_component_by_id_as::<TransformComponent>(child)
47 .is_some()
48 })
49 }
50
51 fn immediate_owned_router(world: &World, scroll_component: ComponentId) -> Option<ComponentId> {
52 world
53 .children_of(scroll_component)
54 .iter()
55 .copied()
56 .find(|&child| {
57 world.component_label(child) == Some(Self::OWNED_ROUTER_LABEL)
58 && world
59 .get_component_by_id_as::<RouterComponent>(child)
60 .is_some()
61 })
62 }
63
64 fn ensure_owned_router_and_track(
65 world: &mut World,
66 emit: &mut dyn SignalEmitter,
67 scroll_component: ComponentId,
68 ) -> Option<ComponentId> {
69 let track = if let Some(track) = Self::immediate_owned_track(world, scroll_component) {
70 track
71 } else {
72 let track = world.add_component_boxed_named(
73 Self::OWNED_TRACK_LABEL,
74 Box::new(TransformComponent::new()),
75 );
76 let _ = world.add_child(scroll_component, track);
77 world.init_component_tree(track, emit);
78 track
79 };
80
81 if Self::immediate_owned_router(world, scroll_component).is_none() {
82 let router = world.add_component_boxed_named(
83 Self::OWNED_ROUTER_LABEL,
84 Box::new(RouterComponent::new().with_target_name(Self::OWNED_TRACK_LABEL)),
85 );
86 let _ = world.add_child(scroll_component, router);
87 world.init_component_tree(router, emit);
88 }
89
90 Some(track)
91 }
92
93 fn install_drag_forwarding(
94 rx: &mut RxWorld,
95 drag_scope: ComponentId,
96 scroll_component: ComponentId,
97 ) {
98 rx.add_handler_closure(SignalKind::DragMove, drag_scope, move |world, emit, env| {
99 let Some(EventSignal::DragMove { delta_world, .. }) = env.event.as_ref() else {
100 return;
101 };
102
103 let Some(scroll_state) =
104 Self::apply_world_drag(world, emit, scroll_component, *delta_world)
105 else {
106 return;
107 };
108
109 emit.push_event(
110 scroll_component,
111 EventSignal::Scrolling {
112 scroll_component,
113 drag_scope,
114 delta_world: *delta_world,
115 scroll_offset: scroll_state.0,
116 max_scroll: scroll_state.1,
117 viewport_height: scroll_state.2,
118 content_height: scroll_state.3,
119 },
120 );
121 });
122 }
123
124 fn parent_transform_world_matrix(
125 world: &World,
126 transform_cid: ComponentId,
127 ) -> Option<TransformMatrix> {
128 let mut cur = transform_cid;
129 while let Some(parent) = world.parent_of(cur) {
130 if let Some(t) = world.get_component_by_id_as::<TransformComponent>(parent) {
131 return Some(t.transform.matrix_world);
132 }
133 cur = parent;
134 }
135 None
136 }
137
138 fn world_delta_to_track_local(
139 world: &World,
140 track_id: ComponentId,
141 delta_world: [f32; 3],
142 ) -> [f32; 3] {
143 let parent_world = Self::parent_transform_world_matrix(world, track_id)
144 .unwrap_or_else(Self::mat4_identity);
145 let inv_parent_world = math::mat4_inverse(parent_world).unwrap_or_else(Self::mat4_identity);
146 let v = Self::mat4_mul_vec4(
147 inv_parent_world,
148 [delta_world[0], delta_world[1], delta_world[2], 0.0],
149 );
150 [v[0], v[1], v[2]]
151 }
152
153 fn scroll_local_drag_delta_y(
154 world: &World,
155 scroll_component: ComponentId,
156 delta_world: [f32; 3],
157 ) -> f32 {
158 let track_id = world
159 .get_component_by_id_as::<ScrollingComponent>(scroll_component)
160 .and_then(|sc| sc.track)
161 .unwrap_or(scroll_component);
162 Self::world_delta_to_track_local(world, track_id, delta_world)[1]
163 }
164
165 fn apply_world_drag(
166 world: &mut World,
167 emit: &mut dyn SignalEmitter,
168 scroll_component: ComponentId,
169 delta_world: [f32; 3],
170 ) -> Option<(f32, f32, f32, f32)> {
171 let delta_local_y = Self::scroll_local_drag_delta_y(world, scroll_component, delta_world);
172
173 let scroll_state = {
174 let Some(sc) = world.get_component_by_id_as_mut::<ScrollingComponent>(scroll_component)
175 else {
176 return None;
177 };
178 let prev_offset = sc.scroll_offset;
179 let changed = sc.apply_drag(-delta_local_y);
180
181 if !changed {
182 return None;
183 }
184 (
185 sc.scroll_offset,
186 sc.max_scroll(),
187 sc.viewport_height,
188 sc.content_height,
189 )
190 };
191
192 Self::sync_component(world, emit, scroll_component);
193 Some(scroll_state)
194 }
195
196 pub fn deferred_register(
197 &mut self,
198 rx: &mut RxWorld,
199 world: &mut World,
200 emit: &mut dyn SignalEmitter,
201 scroll_component: ComponentId,
202 ) {
203 let existing_track = world
204 .get_component_by_id_as::<ScrollingComponent>(scroll_component)
205 .and_then(|sc| sc.track);
206 let track = existing_track
207 .or_else(|| Self::ensure_owned_router_and_track(world, emit, scroll_component))
208 .or_else(|| Self::nearest_ancestor_transform(world, scroll_component));
209 let drag_scope = Self::nearest_drag_scope(world, scroll_component);
210
211 if let Some(track_id) = track {
212 let base_pos = world
213 .get_component_by_id_as::<TransformComponent>(track_id)
214 .map(|tc| tc.transform.translation)
215 .unwrap_or([0.0, 0.0, 0.0]);
216 if let Some(sc) =
217 world.get_component_by_id_as_mut::<ScrollingComponent>(scroll_component)
218 {
219 if sc.track.is_none() {
220 sc.set_track(track_id, base_pos);
221 }
222 }
223 Self::sync_component(world, emit, scroll_component);
224 }
225
226 if let Some(scope) = drag_scope {
227 let should_install = world
228 .get_component_by_id_as::<ScrollingComponent>(scroll_component)
229 .map(|sc| sc.drag_scope != Some(scope))
230 .unwrap_or(false);
231
232 if should_install {
233 if let Some(sc) =
234 world.get_component_by_id_as_mut::<ScrollingComponent>(scroll_component)
235 {
236 sc.set_drag_scope(scope);
237 }
238 Self::install_drag_forwarding(rx, scope, scroll_component);
239 }
240 }
241 }
242
243 fn nearest_ancestor_transform(world: &World, start: ComponentId) -> Option<ComponentId> {
244 let mut cursor = world.parent_of(start);
245 while let Some(node) = cursor {
246 if world
247 .get_component_by_id_as::<TransformComponent>(node)
248 .is_some()
249 {
250 return Some(node);
251 }
252 cursor = world.parent_of(node);
253 }
254 None
255 }
256
257 fn subtree_first_renderable(world: &World, root: ComponentId) -> Option<ComponentId> {
258 let mut stack = vec![root];
259 while let Some(node) = stack.pop() {
260 if world
261 .get_component_by_id_as::<RenderableComponent>(node)
262 .is_some()
263 {
264 return Some(node);
265 }
266 for &child in world.children_of(node).iter().rev() {
267 stack.push(child);
268 }
269 }
270 None
271 }
272
273 fn sibling_layout_bg_renderable(world: &World, start: ComponentId) -> Option<ComponentId> {
274 let parent = world.parent_of(start)?;
275 let bg = world.children_of(parent).iter().copied().find(|&child| {
276 world.component_label(child) == Some(Self::LAYOUT_BG_LABEL)
277 && world
278 .get_component_by_id_as::<TransformComponent>(child)
279 .is_some()
280 })?;
281 Self::subtree_first_renderable(world, bg)
282 }
283
284 fn nearest_ancestor_clip_scope(world: &World, start: ComponentId) -> Option<ComponentId> {
285 let mut cursor = world.parent_of(start);
286 while let Some(node) = cursor {
287 if world
288 .get_component_by_id_as::<StencilClipComponent>(node)
289 .is_some()
290 {
291 return Some(Self::stencil_drag_scope_root(world, node).unwrap_or(node));
292 }
293 cursor = world.parent_of(node);
294 }
295
296 None
297 }
298
299 fn nearest_ancestor_renderable(world: &World, start: ComponentId) -> Option<ComponentId> {
300 let mut cursor = world.parent_of(start);
301 while let Some(node) = cursor {
302 if world
303 .get_component_by_id_as::<RenderableComponent>(node)
304 .is_some()
305 {
306 return Some(node);
307 }
308 cursor = world.parent_of(node);
309 }
310
311 None
312 }
313
314 fn nearest_drag_scope(world: &World, start: ComponentId) -> Option<ComponentId> {
315 Self::sibling_layout_bg_renderable(world, start)
316 .or_else(|| Self::nearest_ancestor_clip_scope(world, start))
317 .or_else(|| Self::nearest_ancestor_renderable(world, start))
318 .or_else(|| Self::nearest_ancestor_transform(world, start))
319 }
320
321 fn stencil_drag_scope_root(world: &World, stencil_clip: ComponentId) -> Option<ComponentId> {
322 let parent = world.parent_of(stencil_clip)?;
323 if world.component_label(parent) == Some("__bg") {
324 return world.parent_of(parent);
325 }
326 Some(parent)
327 }
328
329 pub fn set_content_height(
330 world: &mut World,
331 emit: &mut dyn SignalEmitter,
332 scroll_component: ComponentId,
333 content_height: f32,
334 ) {
335 {
336 let Some(sc) = world.get_component_by_id_as_mut::<ScrollingComponent>(scroll_component)
337 else {
338 return;
339 };
340 let _ = sc.set_content_height(content_height);
341 }
342
343 Self::sync_component(world, emit, scroll_component);
344 }
345
346 pub fn sync_component(
347 world: &mut World,
348 emit: &mut dyn SignalEmitter,
349 scroll_component: ComponentId,
350 ) {
351 let (track_id, translation, rotation, scale) = {
352 let Some(sc) = world.get_component_by_id_as::<ScrollingComponent>(scroll_component)
353 else {
354 return;
355 };
356 let Some(track_id) = sc.track else {
357 return;
358 };
359 let translation = sc.track_translation();
360 let Some(track_tc) = world.get_component_by_id_as::<TransformComponent>(track_id)
361 else {
362 return;
363 };
364 (
365 track_id,
366 translation,
367 track_tc.transform.rotation,
368 track_tc.transform.scale,
369 )
370 };
371
372 emit.push_intent_now(
373 track_id,
374 IntentValue::UpdateTransform {
375 component_ids: vec![track_id],
376 translation,
377 rotation_quat_xyzw: rotation,
378 scale,
379 },
380 );
381 }
382}
383
384#[cfg(test)]
385mod tests {
386 use super::ScrollingSystem;
387 use crate::engine::ecs::CommandQueue;
388 use crate::engine::ecs::IntentValue;
389 use crate::engine::ecs::SignalEmitter;
390 use crate::engine::ecs::SystemWorld;
391 use crate::engine::ecs::World;
392 use crate::engine::ecs::component::{
393 RenderableComponent, ScrollingComponent, TransformComponent,
394 };
395 use crate::engine::graphics::{RenderAssets, VisualWorld};
396
397 #[test]
398 fn scrolling_without_explicit_track_gets_owned_scroll_track() {
399 let mut world = World::default();
400 let mut visuals = VisualWorld::new();
401 let mut render_assets = RenderAssets::new();
402 let mut queue = CommandQueue::new();
403 let mut systems = SystemWorld::default();
404
405 let scrolling = world.add_component(ScrollingComponent::new(1.0, 10.0));
406 let item = world.add_component(TransformComponent::new().with_position(0.0, 2.0, 0.0));
407 let _ = world.add_child(scrolling, item);
408
409 world.init_component_tree(scrolling, &mut queue);
410 systems.process_commands(&mut world, &mut visuals, &mut render_assets, &mut queue);
411
412 let track = world
413 .get_component_by_id_as::<ScrollingComponent>(scrolling)
414 .and_then(|sc| sc.track)
415 .expect("owned track");
416
417 assert_eq!(world.component_label(track), Some("__scroll_track"));
418 assert_eq!(world.parent_of(track), Some(scrolling));
419 assert_eq!(world.parent_of(item), Some(track));
420 }
421
422 #[test]
423 fn explicit_scroll_track_is_preserved() {
424 let mut world = World::default();
425 let mut visuals = VisualWorld::new();
426 let mut render_assets = RenderAssets::new();
427 let mut queue = CommandQueue::new();
428 let mut systems = SystemWorld::default();
429
430 let scrolling = world.add_component(ScrollingComponent::new(1.0, 10.0));
431 let explicit_track =
432 world.add_component(TransformComponent::new().with_position(3.0, 4.0, 5.0));
433 let child = world.add_component(TransformComponent::new());
434 let _ = world.add_child(scrolling, explicit_track);
435 let _ = world.add_child(explicit_track, child);
436
437 if let Some(sc) = world.get_component_by_id_as_mut::<ScrollingComponent>(scrolling) {
438 sc.set_track(explicit_track, [3.0, 4.0, 5.0]);
439 }
440
441 world.init_component_tree(scrolling, &mut queue);
442 systems.process_commands(&mut world, &mut visuals, &mut render_assets, &mut queue);
443
444 let sc = world
445 .get_component_by_id_as::<ScrollingComponent>(scrolling)
446 .expect("scrolling state");
447 assert_eq!(sc.track, Some(explicit_track));
448 assert!(ScrollingSystem::immediate_owned_track(&world, scrolling).is_none());
449 assert_eq!(world.parent_of(child), Some(explicit_track));
450 }
451
452 #[test]
453 fn scrolling_late_attached_children_route_into_owned_track() {
454 let mut world = World::default();
455 let mut visuals = VisualWorld::new();
456 let mut render_assets = RenderAssets::new();
457 let mut queue = CommandQueue::new();
458 let mut systems = SystemWorld::default();
459
460 let scrolling = world.add_component(ScrollingComponent::new(1.0, 10.0));
461 world.init_component_tree(scrolling, &mut queue);
462 systems.process_commands(&mut world, &mut visuals, &mut render_assets, &mut queue);
463
464 let track = world
465 .get_component_by_id_as::<ScrollingComponent>(scrolling)
466 .and_then(|sc| sc.track)
467 .expect("owned track");
468
469 let late = world.add_component(TransformComponent::new().with_position(0.0, 3.0, 0.0));
470 queue.push_intent_now(
471 late,
472 IntentValue::Attach {
473 parents: vec![scrolling],
474 child: late,
475 },
476 );
477 systems.process_commands(&mut world, &mut visuals, &mut render_assets, &mut queue);
478
479 assert_eq!(world.parent_of(late), Some(track));
480 }
481
482 #[test]
483 fn scrolling_prefers_sibling_layout_bg_renderable_for_drag_scope() {
484 let mut world = World::default();
485 let mut visuals = VisualWorld::new();
486 let mut render_assets = RenderAssets::new();
487 let mut queue = CommandQueue::new();
488 let mut systems = SystemWorld::default();
489
490 let root = world.add_component_boxed_named("root", Box::new(TransformComponent::new()));
491 let bg = world.add_component_boxed_named("__bg", Box::new(TransformComponent::new()));
492 let bg_renderable = world.add_component(RenderableComponent::square());
493 let scrolling = world.add_component_boxed_named(
494 "layout_scroll",
495 Box::new(ScrollingComponent::new(1.0, 10.0)),
496 );
497
498 let _ = world.add_child(root, bg);
499 let _ = world.add_child(bg, bg_renderable);
500 let _ = world.add_child(root, scrolling);
501
502 world.init_component_tree(root, &mut queue);
503 systems.process_commands(&mut world, &mut visuals, &mut render_assets, &mut queue);
504
505 let sc = world
506 .get_component_by_id_as::<ScrollingComponent>(scrolling)
507 .expect("scrolling state");
508 assert_eq!(sc.drag_scope, Some(bg_renderable));
509 }
510
511 #[test]
512 fn world_drag_is_converted_into_scroll_local_y() {
513 let mut world = World::default();
514 let mut visuals = VisualWorld::new();
515 let mut render_assets = RenderAssets::new();
516 let mut queue = CommandQueue::new();
517 let mut systems = SystemWorld::default();
518
519 let parent = world.add_component(
520 TransformComponent::new()
521 .with_position(0.0, 0.0, 0.0)
522 .with_scale(1.0, 2.0, 1.0),
523 );
524 let scrolling = world.add_component(ScrollingComponent::new(1.0, 10.0));
525
526 let _ = world.add_child(parent, scrolling);
527
528 world.init_component_tree(parent, &mut queue);
529 systems.process_commands(&mut world, &mut visuals, &mut render_assets, &mut queue);
530
531 let delta_local_y =
532 ScrollingSystem::scroll_local_drag_delta_y(&world, scrolling, [0.0, 2.0, 0.0]);
533 assert!(
534 (delta_local_y - 1.0).abs() < 1e-5,
535 "expected world delta to divide by parent Y scale"
536 );
537
538 let scroll_state =
539 ScrollingSystem::apply_world_drag(&mut world, &mut queue, scrolling, [0.0, 2.0, 0.0])
540 .expect("scroll should move");
541 systems.process_commands(&mut world, &mut visuals, &mut render_assets, &mut queue);
542
543 let sc = world
544 .get_component_by_id_as::<ScrollingComponent>(scrolling)
545 .expect("scrolling state");
546 assert!((sc.scroll_offset - 1.0).abs() < 1e-5);
547 assert!((scroll_state.0 - 1.0).abs() < 1e-5);
548
549 let track = sc.track.expect("owned track");
550 let track_transform = world
551 .get_component_by_id_as::<TransformComponent>(track)
552 .expect("track transform");
553 assert!((track_transform.transform.translation[1] - 1.0).abs() < 1e-5);
554 }
555}