Skip to main content

nexus_rt/
callback.rs

1//! Context-owning handler dispatch.
2//!
3//! [`Callback`] is the unified dispatch type in nexus-rt. Context-free
4//! handlers use `Callback<(), CtxFree<F>, P>` (via [`IntoHandler`](crate::IntoHandler));
5//! context-owning handlers use `Callback<C, F, P>` (via [`IntoCallback`]).
6//!
7//! The function convention for context-owning callbacks is
8//! `fn handler(ctx: &mut C, params: Res<T>..., event: E)` — context first,
9//! [`SystemParam`]-resolved resources in the middle, event last.
10//!
11//! Same HRTB double-bound pattern, same macro generation, same ~2-cycle
12//! dispatch. Named functions only (same closure limitation as
13//! [`IntoHandler`](crate::IntoHandler)).
14
15use crate::Handler;
16use crate::system::SystemParam;
17use crate::world::{Registry, World};
18
19// =============================================================================
20// Callback<C, F, Params>
21// =============================================================================
22
23/// Unified dispatch type. Stores per-callback context alongside
24/// pre-resolved resource access.
25///
26/// - Context-free handlers: `Callback<(), CtxFree<F>, P>` — created via
27///   [`IntoHandler::into_handler`](crate::IntoHandler::into_handler).
28/// - Context-owning handlers: `Callback<C, F, P>` — created via
29///   [`IntoCallback::into_callback`].
30///
31/// Both implement [`Handler<E>`].
32///
33/// # Examples
34///
35/// ```
36/// use nexus_rt::{WorldBuilder, ResMut, IntoCallback, Handler};
37///
38/// struct Ctx { count: u64 }
39///
40/// fn handler(ctx: &mut Ctx, mut val: ResMut<u64>, event: u32) {
41///     *val += event as u64;
42///     ctx.count += 1;
43/// }
44///
45/// let mut builder = WorldBuilder::new();
46/// builder.register::<u64>(0);
47/// let mut world = builder.build();
48///
49/// let mut cb = handler.into_callback(Ctx { count: 0 }, world.registry_mut());
50/// cb.run(&mut world, 10u32);
51///
52/// assert_eq!(cb.ctx.count, 1);
53/// assert_eq!(*world.resource::<u64>(), 10);
54/// ```
55pub struct Callback<C, F, Params: SystemParam> {
56    /// Per-callback owned state. Accessible outside dispatch.
57    pub ctx: C,
58    pub(crate) f: F,
59    pub(crate) state: Params::State,
60    pub(crate) name: &'static str,
61}
62
63// =============================================================================
64// IntoCallback
65// =============================================================================
66
67/// Converts a named function into a [`Callback`].
68///
69/// Identical to [`IntoHandler`](crate::IntoHandler) but injects `&mut C` as
70/// the first parameter. [`ResourceId`](crate::ResourceId)s resolved via
71/// `registry.id::<T>()` at call time — panics if any resource is not
72/// registered.
73///
74/// # Named functions only
75///
76/// Closures do not work with `IntoCallback` due to Rust's HRTB inference
77/// limitations with GATs. Use named `fn` items instead.
78///
79/// # Panics
80///
81/// Panics if any [`SystemParam`] resource is not registered.
82pub trait IntoCallback<C, E, Params> {
83    /// The concrete Callback type produced.
84    type Callback: Handler<E>;
85
86    /// Convert this function + context into a Callback.
87    fn into_callback(self, ctx: C, registry: &mut Registry) -> Self::Callback;
88}
89
90// =============================================================================
91// Arity 0: fn(ctx: &mut C, E) — context + event only, no SystemParam
92// =============================================================================
93
94impl<C: 'static, E, F: FnMut(&mut C, E) + 'static> IntoCallback<C, E, ()> for F {
95    type Callback = Callback<C, F, ()>;
96
97    fn into_callback(self, ctx: C, registry: &mut Registry) -> Self::Callback {
98        Callback {
99            ctx,
100            f: self,
101            state: <() as SystemParam>::init(registry),
102            name: std::any::type_name::<F>(),
103        }
104    }
105}
106
107impl<C: 'static, E, F: FnMut(&mut C, E) + 'static> Handler<E> for Callback<C, F, ()> {
108    fn run(&mut self, _world: &mut World, event: E) {
109        (self.f)(&mut self.ctx, event);
110    }
111
112    fn inputs_changed(&self, _world: &World) -> bool {
113        // Context-only callback — no resource dependencies to check.
114        // Always returns true so the drivers never skip it.
115        true
116    }
117
118    fn name(&self) -> &'static str {
119        self.name
120    }
121}
122
123// =============================================================================
124// Macro-generated impls (arities 1-8)
125// =============================================================================
126
127macro_rules! impl_into_callback {
128    ($($P:ident),+) => {
129        impl<C: 'static, E, F: 'static, $($P: SystemParam + 'static),+>
130            IntoCallback<C, E, ($($P,)+)> for F
131        where
132            for<'a> &'a mut F:
133                FnMut(&mut C, $($P,)+ E) +
134                FnMut(&mut C, $($P::Item<'a>,)+ E),
135        {
136            type Callback = Callback<C, F, ($($P,)+)>;
137
138            fn into_callback(self, ctx: C, registry: &mut Registry) -> Self::Callback {
139                let state = <($($P,)+) as SystemParam>::init(registry);
140                {
141                    #[allow(non_snake_case)]
142                    let ($($P,)+) = &state;
143                    registry.check_access(&[
144                        $(
145                            (<$P as SystemParam>::resource_id($P),
146                             std::any::type_name::<$P>()),
147                        )+
148                    ]);
149                }
150                Callback { ctx, f: self, state, name: std::any::type_name::<F>() }
151            }
152        }
153
154        impl<C: 'static, E, F: 'static, $($P: SystemParam + 'static),+>
155            Handler<E> for Callback<C, F, ($($P,)+)>
156        where
157            for<'a> &'a mut F:
158                FnMut(&mut C, $($P,)+ E) +
159                FnMut(&mut C, $($P::Item<'a>,)+ E),
160        {
161            #[allow(non_snake_case)]
162            fn run(&mut self, world: &mut World, event: E) {
163                #[allow(clippy::too_many_arguments)]
164                fn call_inner<Ctx, $($P,)+ Ev>(
165                    mut f: impl FnMut(&mut Ctx, $($P,)+ Ev),
166                    ctx: &mut Ctx,
167                    $($P: $P,)+
168                    event: Ev,
169                ) {
170                    f(ctx, $($P,)+ event);
171                }
172
173                // SAFETY: state was produced by init() on the same registry
174                // that built this world. Single-threaded sequential dispatch
175                // ensures no mutable aliasing across params.
176                let ($($P,)+) = unsafe {
177                    <($($P,)+) as SystemParam>::fetch(world, &mut self.state)
178                };
179                call_inner(&mut self.f, &mut self.ctx, $($P,)+ event);
180            }
181
182            fn inputs_changed(&self, world: &World) -> bool {
183                <($($P,)+) as SystemParam>::any_changed(&self.state, world)
184            }
185
186            fn name(&self) -> &'static str {
187                self.name
188            }
189        }
190    };
191}
192
193// Reuse all_tuples — re-declared here since macros are module-scoped.
194macro_rules! all_tuples {
195    ($m:ident) => {
196        $m!(P0);
197        $m!(P0, P1);
198        $m!(P0, P1, P2);
199        $m!(P0, P1, P2, P3);
200        $m!(P0, P1, P2, P3, P4);
201        $m!(P0, P1, P2, P3, P4, P5);
202        $m!(P0, P1, P2, P3, P4, P5, P6);
203        $m!(P0, P1, P2, P3, P4, P5, P6, P7);
204    };
205}
206
207all_tuples!(impl_into_callback);
208
209// =============================================================================
210// Tests
211// =============================================================================
212
213#[cfg(test)]
214mod tests {
215    use super::*;
216    use crate::{Local, Res, ResMut, WorldBuilder};
217
218    // -- Helper types ---------------------------------------------------------
219
220    struct TimerCtx {
221        order_id: u64,
222        call_count: u64,
223    }
224
225    struct OrderCache {
226        expired: Vec<u64>,
227    }
228
229    // -- Core dispatch --------------------------------------------------------
230
231    fn ctx_only_handler(ctx: &mut TimerCtx, _event: u32) {
232        ctx.call_count += 1;
233    }
234
235    #[test]
236    fn ctx_only_no_params() {
237        let mut world = WorldBuilder::new().build();
238        let mut cb = ctx_only_handler.into_callback(
239            TimerCtx {
240                order_id: 1,
241                call_count: 0,
242            },
243            world.registry_mut(),
244        );
245        cb.run(&mut world, 42u32);
246        assert_eq!(cb.ctx.call_count, 1);
247    }
248
249    fn ctx_one_res_handler(ctx: &mut TimerCtx, cache: Res<OrderCache>, _event: u32) {
250        ctx.call_count += cache.expired.len() as u64;
251    }
252
253    #[test]
254    fn ctx_one_res() {
255        let mut builder = WorldBuilder::new();
256        builder.register::<OrderCache>(OrderCache {
257            expired: vec![1, 2, 3],
258        });
259        let mut world = builder.build();
260
261        let mut cb = ctx_one_res_handler.into_callback(
262            TimerCtx {
263                order_id: 1,
264                call_count: 0,
265            },
266            world.registry_mut(),
267        );
268        cb.run(&mut world, 0u32);
269        assert_eq!(cb.ctx.call_count, 3);
270    }
271
272    fn ctx_one_res_mut_handler(ctx: &mut TimerCtx, mut cache: ResMut<OrderCache>, _event: u32) {
273        cache.expired.push(ctx.order_id);
274        ctx.call_count += 1;
275    }
276
277    #[test]
278    fn ctx_one_res_mut() {
279        let mut builder = WorldBuilder::new();
280        builder.register::<OrderCache>(OrderCache { expired: vec![] });
281        let mut world = builder.build();
282
283        let mut cb = ctx_one_res_mut_handler.into_callback(
284            TimerCtx {
285                order_id: 42,
286                call_count: 0,
287            },
288            world.registry_mut(),
289        );
290        cb.run(&mut world, 0u32);
291        assert_eq!(cb.ctx.call_count, 1);
292        assert_eq!(world.resource::<OrderCache>().expired, vec![42]);
293    }
294
295    fn ctx_two_params_handler(
296        ctx: &mut TimerCtx,
297        counter: Res<u64>,
298        mut cache: ResMut<OrderCache>,
299        _event: u32,
300    ) {
301        cache.expired.push(*counter);
302        ctx.call_count += 1;
303    }
304
305    #[test]
306    fn ctx_two_params() {
307        let mut builder = WorldBuilder::new();
308        builder.register::<u64>(99);
309        builder.register::<OrderCache>(OrderCache { expired: vec![] });
310        let mut world = builder.build();
311
312        let mut cb = ctx_two_params_handler.into_callback(
313            TimerCtx {
314                order_id: 0,
315                call_count: 0,
316            },
317            world.registry_mut(),
318        );
319        cb.run(&mut world, 0u32);
320        assert_eq!(cb.ctx.call_count, 1);
321        assert_eq!(world.resource::<OrderCache>().expired, vec![99]);
322    }
323
324    fn ctx_three_params_handler(
325        ctx: &mut TimerCtx,
326        a: Res<u64>,
327        b: Res<bool>,
328        mut c: ResMut<OrderCache>,
329        _event: u32,
330    ) {
331        if *b {
332            c.expired.push(*a);
333        }
334        ctx.call_count += 1;
335    }
336
337    #[test]
338    fn ctx_three_params() {
339        let mut builder = WorldBuilder::new();
340        builder.register::<u64>(7);
341        builder.register::<bool>(true);
342        builder.register::<OrderCache>(OrderCache { expired: vec![] });
343        let mut world = builder.build();
344
345        let mut cb = ctx_three_params_handler.into_callback(
346            TimerCtx {
347                order_id: 0,
348                call_count: 0,
349            },
350            world.registry_mut(),
351        );
352        cb.run(&mut world, 0u32);
353        assert_eq!(cb.ctx.call_count, 1);
354        assert_eq!(world.resource::<OrderCache>().expired, vec![7]);
355    }
356
357    // -- Context ownership ----------------------------------------------------
358
359    #[test]
360    fn ctx_mutated_persists() {
361        let mut world = WorldBuilder::new().build();
362        let mut cb = ctx_only_handler.into_callback(
363            TimerCtx {
364                order_id: 1,
365                call_count: 0,
366            },
367            world.registry_mut(),
368        );
369        cb.run(&mut world, 0u32);
370        cb.run(&mut world, 0u32);
371        cb.run(&mut world, 0u32);
372        assert_eq!(cb.ctx.call_count, 3);
373    }
374
375    #[test]
376    fn ctx_accessible_outside_dispatch() {
377        let mut world = WorldBuilder::new().build();
378        let mut cb = ctx_only_handler.into_callback(
379            TimerCtx {
380                order_id: 42,
381                call_count: 0,
382            },
383            world.registry_mut(),
384        );
385        assert_eq!(cb.ctx.order_id, 42);
386        assert_eq!(cb.ctx.call_count, 0);
387        cb.run(&mut world, 0u32);
388        assert_eq!(cb.ctx.call_count, 1);
389    }
390
391    #[test]
392    fn ctx_mutated_outside_dispatch() {
393        let mut world = WorldBuilder::new().build();
394        let mut cb = ctx_only_handler.into_callback(
395            TimerCtx {
396                order_id: 1,
397                call_count: 0,
398            },
399            world.registry_mut(),
400        );
401        cb.ctx.order_id = 99;
402        cb.run(&mut world, 0u32);
403        assert_eq!(cb.ctx.order_id, 99);
404        assert_eq!(cb.ctx.call_count, 1);
405    }
406
407    // -- Safety validation ----------------------------------------------------
408
409    #[test]
410    #[should_panic(expected = "not registered")]
411    fn panics_on_missing_resource() {
412        let mut world = WorldBuilder::new().build();
413
414        fn needs_cache(_ctx: &mut TimerCtx, _cache: Res<OrderCache>, _e: u32) {}
415
416        let _cb = needs_cache.into_callback(
417            TimerCtx {
418                order_id: 0,
419                call_count: 0,
420            },
421            world.registry_mut(),
422        );
423    }
424
425    #[test]
426    #[should_panic(expected = "not registered")]
427    fn panics_on_second_missing() {
428        let mut builder = WorldBuilder::new();
429        builder.register::<u64>(0);
430        let mut world = builder.build();
431
432        fn needs_two(_ctx: &mut TimerCtx, _a: Res<u64>, _b: Res<OrderCache>, _e: u32) {}
433
434        let _cb = needs_two.into_callback(
435            TimerCtx {
436                order_id: 0,
437                call_count: 0,
438            },
439            world.registry_mut(),
440        );
441    }
442
443    // -- Access conflict detection --------------------------------------------
444
445    #[test]
446    #[should_panic(expected = "conflicting access")]
447    fn callback_duplicate_access_panics() {
448        let mut builder = WorldBuilder::new();
449        builder.register::<u64>(0);
450        let mut world = builder.build();
451
452        fn bad(_ctx: &mut u64, a: Res<u64>, b: ResMut<u64>, _e: ()) {
453            let _ = (*a, &*b);
454        }
455
456        let _cb = bad.into_callback(0u64, world.registry_mut());
457    }
458
459    // -- Change detection -----------------------------------------------------
460
461    fn reads_resource(_ctx: &mut u64, _val: Res<u64>, _e: ()) {}
462
463    #[test]
464    fn inputs_changed_delegates() {
465        let mut builder = WorldBuilder::new();
466        builder.register::<u64>(0);
467        let mut world = builder.build();
468
469        let cb = reads_resource.into_callback(0u64, world.registry_mut());
470
471        // Seq 0: changed_at=0, current_sequence=0 → changed
472        assert!(cb.inputs_changed(&world));
473
474        world.next_sequence();
475        assert!(!cb.inputs_changed(&world));
476
477        *world.resource_mut::<u64>() = 42;
478        assert!(cb.inputs_changed(&world));
479    }
480
481    #[test]
482    fn inputs_changed_true_no_params() {
483        let mut world = WorldBuilder::new().build();
484        let cb = ctx_only_handler.into_callback(
485            TimerCtx {
486                order_id: 0,
487                call_count: 0,
488            },
489            world.registry_mut(),
490        );
491        // Context-only callbacks always run — scheduler must not skip them.
492        assert!(cb.inputs_changed(&world));
493    }
494
495    fn stamps_writer(_ctx: &mut u64, mut val: ResMut<u64>, _e: ()) {
496        *val = 99;
497    }
498
499    #[test]
500    fn mut_stamps_changed_at() {
501        let mut builder = WorldBuilder::new();
502        builder.register::<u64>(0);
503        let mut world = builder.build();
504
505        let mut cb = stamps_writer.into_callback(0u64, world.registry_mut());
506
507        world.next_sequence(); // tick=1
508        let id = world.id::<u64>();
509        unsafe {
510            assert_eq!(world.changed_at(id), crate::Sequence(0));
511        }
512
513        cb.run(&mut world, ());
514        unsafe {
515            assert_eq!(world.changed_at(id), crate::Sequence(1));
516        }
517    }
518
519    // -- Handler<E> interface -------------------------------------------------
520
521    #[test]
522    fn box_dyn_handler() {
523        let mut builder = WorldBuilder::new();
524        builder.register::<u64>(0);
525        let mut world = builder.build();
526
527        fn add_ctx(ctx: &mut u64, mut val: ResMut<u64>, event: u64) {
528            *val += event + *ctx;
529        }
530
531        let cb = add_ctx.into_callback(10u64, world.registry_mut());
532        let mut boxed: Box<dyn Handler<u64>> = Box::new(cb);
533        boxed.run(&mut world, 5u64);
534        // 0 + 5 + 10 = 15
535        assert_eq!(*world.resource::<u64>(), 15);
536    }
537
538    #[test]
539    fn callback_in_vec_dyn() {
540        let mut builder = WorldBuilder::new();
541        builder.register::<u64>(0);
542        let mut world = builder.build();
543
544        fn add(ctx: &mut u64, mut val: ResMut<u64>, _e: ()) {
545            *val += *ctx;
546        }
547        fn mul(ctx: &mut u64, mut val: ResMut<u64>, _e: ()) {
548            *val *= *ctx;
549        }
550
551        let cb_add = add.into_callback(3u64, world.registry_mut());
552        let cb_mul = mul.into_callback(2u64, world.registry_mut());
553
554        let mut handlers: Vec<Box<dyn Handler<()>>> = vec![Box::new(cb_add), Box::new(cb_mul)];
555
556        for h in handlers.iter_mut() {
557            h.run(&mut world, ());
558        }
559        // 0 + 3 = 3, then 3 * 2 = 6
560        assert_eq!(*world.resource::<u64>(), 6);
561    }
562
563    fn with_local(_ctx: &mut u64, mut local: Local<u64>, mut val: ResMut<u64>, _e: ()) {
564        *local += 1;
565        *val = *local;
566    }
567
568    #[test]
569    fn callback_with_local() {
570        let mut builder = WorldBuilder::new();
571        builder.register::<u64>(0);
572        let mut world = builder.build();
573
574        let mut cb = with_local.into_callback(0u64, world.registry_mut());
575        cb.run(&mut world, ());
576        cb.run(&mut world, ());
577        cb.run(&mut world, ());
578        assert_eq!(*world.resource::<u64>(), 3);
579    }
580
581    // -- Integration ----------------------------------------------------------
582
583    #[test]
584    fn callback_interop_with_handler() {
585        use crate::IntoHandler;
586
587        let mut builder = WorldBuilder::new();
588        builder.register::<u64>(0);
589        let mut world = builder.build();
590
591        fn sys_add(mut val: ResMut<u64>, event: u64) {
592            *val += event;
593        }
594        fn cb_mul(ctx: &mut u64, mut val: ResMut<u64>, _e: u64) {
595            *val *= *ctx;
596        }
597
598        let sys = sys_add.into_handler(world.registry_mut());
599        let cb = cb_mul.into_callback(3u64, world.registry_mut());
600
601        let mut handlers: Vec<Box<dyn Handler<u64>>> = vec![Box::new(sys), Box::new(cb)];
602
603        for h in handlers.iter_mut() {
604            h.run(&mut world, 5u64);
605        }
606        // 0 + 5 = 5, then 5 * 3 = 15
607        assert_eq!(*world.resource::<u64>(), 15);
608    }
609}