Skip to main content

mittens_engine/engine/ecs/system/
scroll_system.rs

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}