1use std::any::{Any, TypeId};
19use std::cell::{Cell, RefCell};
20use std::collections::HashMap;
21use std::rc::Rc;
22use std::sync::Arc;
23
24use crate::widget::EventContext;
25use crate::window::TeksiloWindowId;
26
27pub trait EventSource: 'static {
33 type Origin: Clone + 'static;
36
37 type Event: Send + 'static;
41
42 fn subscribe(
47 &self,
48 origin: Self::Origin,
49 callback: Arc<dyn Fn(Self::Event) + Send + Sync + 'static>,
50 ) -> SubscriptionHandle;
51}
52
53pub struct SubscriptionHandle {
60 _inner: Box<dyn Any>,
61}
62
63impl SubscriptionHandle {
64 pub fn new<T: 'static>(token: T) -> Self {
68 Self {
69 _inner: Box::new(token),
70 }
71 }
72
73 pub fn empty() -> Self {
77 Self::new(())
78 }
79}
80
81#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
87pub struct SubscriptionId(pub(crate) u64);
88
89pub trait AppEventPoster: Send + Sync + 'static {
106 fn post_subscription_event(&self, sub_id: SubscriptionId, event: Box<dyn Any + Send>);
107
108 fn post_external(&self, _payload: Box<dyn Any + Send>) {}
113}
114
115pub struct EventSourceAdapter {
122 pub(crate) origin_type: TypeId,
123 pub(crate) origin_type_name: &'static str,
124 pub(crate) event_type: TypeId,
125 pub(crate) event_type_name: &'static str,
126 #[allow(clippy::type_complexity)]
127 pub(crate) subscribe_fn: Box<
128 dyn Fn(
129 Box<dyn Any>,
130 Arc<dyn Fn(Box<dyn Any + Send>) + Send + Sync + 'static>,
131 ) -> SubscriptionHandle,
132 >,
133}
134
135impl EventSourceAdapter {
136 pub fn new<S: EventSource>(source: S) -> Self {
139 let source = Arc::new(source);
140 let origin_type = TypeId::of::<S::Origin>();
141 let origin_type_name = std::any::type_name::<S::Origin>();
142 let event_type = TypeId::of::<S::Event>();
143 let event_type_name = std::any::type_name::<S::Event>();
144
145 let subscribe_fn: Box<
146 dyn Fn(
147 Box<dyn Any>,
148 Arc<dyn Fn(Box<dyn Any + Send>) + Send + Sync + 'static>,
149 ) -> SubscriptionHandle,
150 > = Box::new(move |erased_origin, framework_wrapper| {
151 let origin: Box<S::Origin> = erased_origin
152 .downcast::<S::Origin>()
153 .expect("origin type mismatch — framework bug");
154
155 let typed_callback: Arc<dyn Fn(S::Event) + Send + Sync + 'static> =
159 Arc::new(move |event: S::Event| {
160 let erased: Box<dyn Any + Send> = Box::new(event);
161 framework_wrapper(erased);
162 });
163
164 source.subscribe(*origin, typed_callback)
165 });
166
167 Self {
168 origin_type,
169 origin_type_name,
170 event_type,
171 event_type_name,
172 subscribe_fn,
173 }
174 }
175}
176
177type CtxSubscriptionCallback = Rc<dyn Fn(&dyn Any, &mut EventContext)>;
192
193pub struct TreeAppContext {
201 pub(crate) poster: Option<Arc<dyn AppEventPoster>>,
202 pub(crate) event_source: Option<EventSourceAdapter>,
203 #[allow(clippy::type_complexity)]
204 pub(crate) subscription_callbacks: RefCell<HashMap<SubscriptionId, Box<dyn Fn(&dyn Any)>>>,
205 #[allow(clippy::type_complexity)]
216 pub(crate) subscription_ctx_callbacks:
217 RefCell<HashMap<SubscriptionId, (Option<TeksiloWindowId>, CtxSubscriptionCallback)>>,
218 pub(crate) next_subscription_id: Cell<u64>,
219 pub(crate) app_state: HashMap<TypeId, Box<dyn Any>>,
222}
223
224impl TreeAppContext {
225 pub fn empty() -> Self {
228 Self {
229 poster: None,
230 event_source: None,
231 subscription_callbacks: RefCell::new(HashMap::new()),
232 subscription_ctx_callbacks: RefCell::new(HashMap::new()),
233 next_subscription_id: Cell::new(1),
234 app_state: HashMap::new(),
235 }
236 }
237
238 pub fn with_source_and_poster(
242 event_source: EventSourceAdapter,
243 poster: Arc<dyn AppEventPoster>,
244 ) -> Self {
245 Self {
246 poster: Some(poster),
247 event_source: Some(event_source),
248 subscription_callbacks: RefCell::new(HashMap::new()),
249 subscription_ctx_callbacks: RefCell::new(HashMap::new()),
250 next_subscription_id: Cell::new(1),
251 app_state: HashMap::new(),
252 }
253 }
254
255 pub fn with_app_state(mut self, registry: HashMap<TypeId, Box<dyn Any>>) -> Self {
260 self.app_state = registry;
261 self
262 }
263
264 pub fn with_poster(mut self, poster: Arc<dyn AppEventPoster>) -> Self {
272 self.poster = Some(poster);
273 self
274 }
275
276 pub fn poster(&self) -> Option<&Arc<dyn AppEventPoster>> {
280 self.poster.as_ref()
281 }
282
283 pub fn app_state<T: 'static>(&self) -> Option<&T> {
286 self.app_state
287 .get(&TypeId::of::<T>())
288 .and_then(|boxed| boxed.downcast_ref::<T>())
289 }
290
291 pub(crate) fn allocate_subscription_id(&self) -> SubscriptionId {
292 let id = self.next_subscription_id.get();
293 self.next_subscription_id.set(id + 1);
294 SubscriptionId(id)
295 }
296
297 pub fn dispatch_subscription_event(&self, sub_id: SubscriptionId, event: &dyn Any) -> bool {
300 let callbacks = self.subscription_callbacks.borrow();
301 if let Some(callback) = callbacks.get(&sub_id) {
302 callback(event);
303 true
304 } else {
305 false
306 }
307 }
308
309 pub fn ctx_subscription_window(&self, sub_id: SubscriptionId) -> Option<TeksiloWindowId> {
314 self.subscription_ctx_callbacks
315 .borrow()
316 .get(&sub_id)
317 .and_then(|(window_id, _)| *window_id)
318 }
319
320 pub fn dispatch_subscription_event_with_ctx(
331 &self,
332 sub_id: SubscriptionId,
333 event: &dyn Any,
334 ctx: &mut EventContext,
335 ) -> bool {
336 let callback = self
337 .subscription_ctx_callbacks
338 .borrow()
339 .get(&sub_id)
340 .map(|(_window_id, callback)| Rc::clone(callback));
341 match callback {
342 Some(callback) => {
343 callback(event, ctx);
344 true
345 }
346 None => false,
347 }
348 }
349
350 pub fn ctx_subscription_count(&self) -> usize {
354 self.subscription_ctx_callbacks.borrow().len()
355 }
356
357 pub fn purge_ctx_subscriptions_for_window(&self, window_id: TeksiloWindowId) {
363 self.subscription_ctx_callbacks
364 .borrow_mut()
365 .retain(|_, (win, _)| *win != Some(window_id));
366 }
367
368 pub fn subscription_count(&self) -> usize {
371 self.subscription_callbacks.borrow().len()
372 }
373}
374
375#[cfg(test)]
376mod tests {
377 use super::*;
378 use crate::signal::Signal;
379 use crate::widget::{LayoutContext, Widget};
380 use crate::widget_id::WidgetId;
381 use crate::widget_tree::WidgetTree;
382 use std::sync::Mutex;
383 use teksilo_canvas::SizeProposal;
384
385 #[derive(Clone, PartialEq, Eq, Hash, Debug)]
388 enum TestOrigin {
389 Created,
390 Updated,
391 }
392
393 #[derive(Clone, Debug, PartialEq)]
394 struct TestEvent {
395 id: u64,
396 message: String,
397 }
398
399 #[derive(Default)]
411 struct MockEventSource {
412 #[allow(clippy::type_complexity)]
413 subscribers: Arc<
414 Mutex<
415 Vec<(
416 u64,
417 TestOrigin,
418 Arc<dyn Fn(TestEvent) + Send + Sync + 'static>,
419 )>,
420 >,
421 >,
422 next_id: std::sync::atomic::AtomicU64,
423 }
424
425 struct MockToken {
428 #[allow(clippy::type_complexity)]
429 subscribers: Arc<
430 Mutex<
431 Vec<(
432 u64,
433 TestOrigin,
434 Arc<dyn Fn(TestEvent) + Send + Sync + 'static>,
435 )>,
436 >,
437 >,
438 id: u64,
439 }
440
441 impl Drop for MockToken {
442 fn drop(&mut self) {
443 if let Ok(mut subs) = self.subscribers.lock() {
444 subs.retain(|(id, _, _)| *id != self.id);
445 }
446 }
447 }
448
449 impl EventSource for MockEventSource {
450 type Origin = TestOrigin;
451 type Event = TestEvent;
452
453 fn subscribe(
454 &self,
455 origin: Self::Origin,
456 callback: Arc<dyn Fn(Self::Event) + Send + Sync + 'static>,
457 ) -> SubscriptionHandle {
458 let id = self
459 .next_id
460 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
461 self.subscribers
462 .lock()
463 .unwrap()
464 .push((id, origin, callback));
465 SubscriptionHandle::new(MockToken {
466 subscribers: self.subscribers.clone(),
467 id,
468 })
469 }
470 }
471
472 impl MockEventSource {
473 fn publish(&self, origin: TestOrigin, event: TestEvent) {
474 let subs = self.subscribers.lock().unwrap();
475 for (_id, sub_origin, cb) in subs.iter() {
476 if *sub_origin == origin {
477 cb(event.clone());
478 }
479 }
480 }
481
482 fn subscriber_count(&self) -> usize {
483 self.subscribers.lock().unwrap().len()
484 }
485 }
486
487 #[derive(Default)]
491 struct TestPoster {
492 #[allow(clippy::type_complexity)]
493 queue: Mutex<Vec<(SubscriptionId, Box<dyn Any + Send>)>>,
494 }
495
496 impl AppEventPoster for TestPoster {
497 fn post_subscription_event(&self, sub_id: SubscriptionId, event: Box<dyn Any + Send>) {
498 self.queue.lock().unwrap().push((sub_id, event));
499 }
500 }
501
502 impl TestPoster {
503 fn drain(&self) -> Vec<(SubscriptionId, Box<dyn Any + Send>)> {
504 std::mem::take(&mut *self.queue.lock().unwrap())
505 }
506 }
507
508 #[derive(Debug)]
511 struct SubscribingWidget {
512 origin: TestOrigin,
513 last_message: Signal<String>,
514 }
515
516 impl Widget for SubscribingWidget {
517 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
518 let last_message = self.last_message.clone();
519 ctx.subscribe_event(self.origin.clone(), move |event: &TestEvent| {
520 last_message.set(event.message.clone());
521 });
522 Vec::new()
523 }
524
525 fn layout_response(
526 &self,
527 proposal: SizeProposal,
528 _ctx: &LayoutContext,
529 ) -> crate::widget::LayoutResponse {
530 proposal.resolve(0.0, 0.0).into()
531 }
532 }
533
534 #[derive(Debug)]
538 struct CtxSubscribingWidget {
539 origin: TestOrigin,
540 last_message: Signal<String>,
541 }
542
543 impl Widget for CtxSubscribingWidget {
544 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
545 let last_message = self.last_message.clone();
546 ctx.subscribe_event_with_ctx(
547 self.origin.clone(),
548 move |event: &TestEvent, _ctx: &mut crate::widget::EventContext| {
549 last_message.set(event.message.clone());
550 },
551 );
552 Vec::new()
553 }
554
555 fn layout_response(
556 &self,
557 proposal: SizeProposal,
558 _ctx: &LayoutContext,
559 ) -> crate::widget::LayoutResponse {
560 proposal.resolve(0.0, 0.0).into()
561 }
562 }
563
564 fn install_source(
567 tree: &mut WidgetTree,
568 source: MockEventSource,
569 ) -> (Arc<MockEventSource>, Arc<TestPoster>) {
570 let source = Arc::new(source);
571 struct SharedSource {
574 inner: Arc<MockEventSource>,
575 }
576 impl EventSource for SharedSource {
577 type Origin = TestOrigin;
578 type Event = TestEvent;
579 fn subscribe(
580 &self,
581 origin: Self::Origin,
582 callback: Arc<dyn Fn(Self::Event) + Send + Sync + 'static>,
583 ) -> SubscriptionHandle {
584 self.inner.subscribe(origin, callback)
585 }
586 }
587
588 let adapter = EventSourceAdapter::new(SharedSource {
589 inner: source.clone(),
590 });
591 let poster: Arc<TestPoster> = Arc::new(TestPoster::default());
592 let poster_dyn: Arc<dyn AppEventPoster> = poster.clone();
593 let app_context =
594 std::rc::Rc::new(TreeAppContext::with_source_and_poster(adapter, poster_dyn));
595 tree.set_app_context(app_context);
596 (source, poster)
597 }
598
599 fn drain_and_dispatch(tree: &WidgetTree, poster: &TestPoster) {
600 let events = poster.drain();
601 for (sub_id, event) in events {
602 tree.app_context()
603 .dispatch_subscription_event(sub_id, &*event);
604 }
605 }
606
607 #[test]
610 fn subscribe_event_delivers_to_widget_signal() {
611 let mut tree = WidgetTree::new();
612 let (source, poster) = install_source(&mut tree, MockEventSource::default());
613
614 let signal = Signal::new(String::new());
615 let _id = tree.add(SubscribingWidget {
616 origin: TestOrigin::Created,
617 last_message: signal.clone(),
618 });
619
620 assert_eq!(source.subscriber_count(), 1);
621 assert_eq!(tree.app_context().subscription_count(), 1);
622
623 source.publish(
624 TestOrigin::Created,
625 TestEvent {
626 id: 1,
627 message: "hello".to_string(),
628 },
629 );
630 drain_and_dispatch(&tree, &poster);
631
632 assert_eq!(signal.get(), "hello");
633 }
634
635 #[test]
636 fn subscribe_event_with_ctx_dispatches_inside_fresh_context() {
637 use crate::window::{NoopWindowOps, TeksiloWindowId};
638
639 let mut tree = WidgetTree::new();
640 let app_ctx = tree.app_context().clone();
645 let sub_id = app_ctx.allocate_subscription_id();
646 let win = TeksiloWindowId::new(1);
647 let seen = Signal::new(String::new());
648 let seen_cb = seen.clone();
649 let stored: std::rc::Rc<dyn Fn(&dyn Any, &mut crate::widget::EventContext)> =
650 std::rc::Rc::new(move |event_any, _ctx: &mut crate::widget::EventContext| {
651 let ev = event_any
652 .downcast_ref::<TestEvent>()
653 .expect("subscription event downcast failed");
654 seen_cb.set(ev.message.clone());
655 });
656 app_ctx
657 .subscription_ctx_callbacks
658 .borrow_mut()
659 .insert(sub_id, (Some(win), stored));
660
661 assert_eq!(app_ctx.ctx_subscription_window(sub_id), Some(win));
664 assert_eq!(app_ctx.ctx_subscription_window(SubscriptionId(9999)), None);
665
666 let event = TestEvent {
669 id: 9,
670 message: "progress-42".to_string(),
671 };
672 let handled = std::cell::Cell::new(false);
673 tree.run_with_event_context(&mut NoopWindowOps, |ctx| {
674 handled.set(app_ctx.dispatch_subscription_event_with_ctx(sub_id, &event, ctx));
675 });
676 assert!(
677 handled.get(),
678 "context-bearing dispatch must find the callback"
679 );
680 assert_eq!(seen.get(), "progress-42");
681
682 tree.run_with_event_context(&mut NoopWindowOps, |ctx| {
685 assert!(!app_ctx.dispatch_subscription_event_with_ctx(
686 SubscriptionId(9999),
687 &event,
688 ctx
689 ));
690 });
691 }
692
693 #[test]
698 fn ctx_dispatch_releases_borrow_before_invoking_callback() {
699 use crate::window::{NoopWindowOps, TeksiloWindowId};
700
701 let mut tree = WidgetTree::new();
702 let app_ctx = tree.app_context().clone();
703 let sub_id = app_ctx.allocate_subscription_id();
704
705 let reenter_ctx = app_ctx.clone();
706 let reentered = std::rc::Rc::new(std::cell::Cell::new(false));
707 let flag = reentered.clone();
708 let cb: std::rc::Rc<dyn Fn(&dyn Any, &mut crate::widget::EventContext)> =
709 std::rc::Rc::new(move |_ev, _ctx| {
710 reenter_ctx.subscription_ctx_callbacks.borrow_mut().insert(
713 SubscriptionId(4242),
714 (
715 Some(TeksiloWindowId::new(2)),
716 std::rc::Rc::new(|_e: &dyn Any, _c: &mut crate::widget::EventContext| {}),
717 ),
718 );
719 flag.set(true);
720 });
721 app_ctx
722 .subscription_ctx_callbacks
723 .borrow_mut()
724 .insert(sub_id, (Some(TeksiloWindowId::new(1)), cb));
725
726 let event = TestEvent {
727 id: 1,
728 message: String::new(),
729 };
730 tree.run_with_event_context(&mut NoopWindowOps, |ctx| {
731 assert!(app_ctx.dispatch_subscription_event_with_ctx(sub_id, &event, ctx));
732 });
733
734 assert!(
735 reentered.get(),
736 "callback ran and its re-entrant map insert did not panic"
737 );
738 assert_eq!(
739 app_ctx.ctx_subscription_count(),
740 2,
741 "original + the re-entrant insert both present"
742 );
743 }
744
745 #[test]
750 fn subscribe_event_with_ctx_registers_and_tears_down() {
751 let mut tree = WidgetTree::new();
752 let (_source, _poster) = install_source(&mut tree, MockEventSource::default());
753
754 let id = tree.add(CtxSubscribingWidget {
755 origin: TestOrigin::Created,
756 last_message: Signal::new(String::new()),
757 });
758 assert_eq!(tree.app_context().ctx_subscription_count(), 1);
760 assert_eq!(tree.app_context().subscription_count(), 0);
761
762 tree.destroy_subtree(id);
763 assert_eq!(
764 tree.app_context().ctx_subscription_count(),
765 0,
766 "destroying the widget must remove its context-bearing subscription"
767 );
768 }
769
770 #[test]
771 fn unrelated_origin_does_not_fire_callback() {
772 let mut tree = WidgetTree::new();
773 let (source, poster) = install_source(&mut tree, MockEventSource::default());
774
775 let signal = Signal::new(String::new());
776 let _id = tree.add(SubscribingWidget {
777 origin: TestOrigin::Created,
778 last_message: signal.clone(),
779 });
780
781 source.publish(
782 TestOrigin::Updated,
783 TestEvent {
784 id: 1,
785 message: "ignored".to_string(),
786 },
787 );
788 drain_and_dispatch(&tree, &poster);
789
790 assert_eq!(signal.get(), "");
791 }
792
793 #[test]
794 fn destroying_widget_removes_ui_callback() {
795 let mut tree = WidgetTree::new();
796 let (_source, _poster) = install_source(&mut tree, MockEventSource::default());
797
798 let signal = Signal::new(String::new());
799 let id = tree.add(SubscribingWidget {
800 origin: TestOrigin::Created,
801 last_message: signal.clone(),
802 });
803
804 assert_eq!(tree.app_context().subscription_count(), 1);
805 tree.destroy_subtree(id);
806 assert_eq!(tree.app_context().subscription_count(), 0);
807 }
808
809 #[test]
810 fn in_flight_event_after_destroy_is_dropped_not_delivered() {
811 let mut tree = WidgetTree::new();
818 let (source, poster) = install_source(&mut tree, MockEventSource::default());
819
820 let signal = Signal::new(String::new());
821 let id = tree.add(SubscribingWidget {
822 origin: TestOrigin::Created,
823 last_message: signal.clone(),
824 });
825
826 source.publish(
828 TestOrigin::Created,
829 TestEvent {
830 id: 7,
831 message: "buffered".to_string(),
832 },
833 );
834
835 tree.destroy_subtree(id);
836 drain_and_dispatch(&tree, &poster);
837
838 assert_eq!(signal.get(), "");
839 assert_eq!(tree.app_context().subscription_count(), 0);
840 }
841
842 #[test]
843 #[should_panic(expected = "no event source was registered")]
844 fn subscribe_without_event_source_panics() {
845 let mut tree = WidgetTree::new();
846 let signal = Signal::new(String::new());
847 tree.add(SubscribingWidget {
849 origin: TestOrigin::Created,
850 last_message: signal,
851 });
852 }
853
854 use std::rc::Rc;
857
858 struct TestGlobals {
859 greeting: Signal<String>,
860 }
861
862 #[derive(Debug)]
865 struct AppStateReader {
866 observed: Signal<String>,
867 saw_none: Signal<bool>,
868 }
869
870 impl Widget for AppStateReader {
871 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
872 match ctx.app_state::<Rc<TestGlobals>>() {
873 Some(globals) => self.observed.set(globals.greeting.get()),
874 None => self.saw_none.set(true),
875 }
876 Vec::new()
877 }
878
879 fn layout_response(
880 &self,
881 proposal: SizeProposal,
882 _ctx: &LayoutContext,
883 ) -> crate::widget::LayoutResponse {
884 proposal.resolve(0.0, 0.0).into()
885 }
886 }
887
888 #[test]
889 fn app_state_roundtrip_in_build_context() {
890 let globals = Rc::new(TestGlobals {
891 greeting: Signal::new("hello from registry".to_string()),
892 });
893
894 let mut registry: HashMap<TypeId, Box<dyn Any>> = HashMap::new();
895 registry.insert(TypeId::of::<Rc<TestGlobals>>(), Box::new(globals.clone()));
896
897 let mut tree = WidgetTree::new();
898 tree.set_app_context(Rc::new(TreeAppContext::empty().with_app_state(registry)));
899
900 let observed = Signal::new(String::new());
901 let saw_none = Signal::new(false);
902 tree.add(AppStateReader {
903 observed: observed.clone(),
904 saw_none: saw_none.clone(),
905 });
906
907 assert_eq!(observed.get(), "hello from registry");
908 assert!(!saw_none.get());
909 }
910
911 #[test]
912 fn app_state_missing_returns_none() {
913 let mut tree = WidgetTree::new();
914 let observed = Signal::new(String::new());
917 let saw_none = Signal::new(false);
918 tree.add(AppStateReader {
919 observed: observed.clone(),
920 saw_none: saw_none.clone(),
921 });
922
923 assert_eq!(observed.get(), "");
924 assert!(saw_none.get());
925 }
926
927 #[test]
928 fn app_state_distinct_types_coexist() {
929 struct Alpha(u32);
930 struct Beta(String);
931
932 let mut registry: HashMap<TypeId, Box<dyn Any>> = HashMap::new();
933 registry.insert(TypeId::of::<Rc<Alpha>>(), Box::new(Rc::new(Alpha(42))));
934 registry.insert(
935 TypeId::of::<Rc<Beta>>(),
936 Box::new(Rc::new(Beta("beta!".to_string()))),
937 );
938
939 let ctx = TreeAppContext::empty().with_app_state(registry);
940 assert_eq!(ctx.app_state::<Rc<Alpha>>().unwrap().0, 42);
941 assert_eq!(ctx.app_state::<Rc<Beta>>().unwrap().0, "beta!");
942 assert!(ctx.app_state::<Rc<u64>>().is_none());
943 }
944
945 #[test]
964 fn an_event_posted_before_a_rebuild_still_reaches_the_widget() {
965 let mut tree = WidgetTree::new();
966 let (source, poster) = install_source(&mut tree, MockEventSource::default());
967
968 let signal = Signal::new(String::new());
969 let id = tree.add(SubscribingWidget {
970 origin: TestOrigin::Created,
971 last_message: signal.clone(),
972 });
973
974 source.publish(
976 TestOrigin::Created,
977 TestEvent {
978 id: 1,
979 message: "landed".to_string(),
980 },
981 );
982
983 tree.arena_mark_needs_rebuild_for_testing(id);
986 tree.layout(SizeProposal::exact(100.0, 100.0));
987
988 drain_and_dispatch(&tree, &poster);
989
990 assert_eq!(
991 signal.get(),
992 "landed",
993 "the rebuild must not swallow an event that was already in flight"
994 );
995 }
996
997 #[test]
1004 fn an_event_posted_before_a_rebuild_still_reaches_a_context_bearing_subscription() {
1005 use crate::window::NoopWindowOps;
1006
1007 let mut tree = WidgetTree::new();
1008 let (source, poster) = install_source(&mut tree, MockEventSource::default());
1009
1010 let signal = Signal::new(String::new());
1011 let id = tree.add(CtxSubscribingWidget {
1012 origin: TestOrigin::Created,
1013 last_message: signal.clone(),
1014 });
1015
1016 source.publish(
1017 TestOrigin::Created,
1018 TestEvent {
1019 id: 1,
1020 message: "landed".to_string(),
1021 },
1022 );
1023
1024 tree.arena_mark_needs_rebuild_for_testing(id);
1025 tree.layout(SizeProposal::exact(100.0, 100.0));
1026
1027 let app_ctx = tree.app_context().clone();
1030 let events = poster.drain();
1031 assert!(!events.is_empty(), "the source must have posted something");
1032 for (sub_id, event) in events {
1033 tree.run_with_event_context(&mut NoopWindowOps, |ctx| {
1034 app_ctx.dispatch_subscription_event_with_ctx(sub_id, &*event, ctx);
1035 });
1036 }
1037
1038 assert_eq!(
1039 signal.get(),
1040 "landed",
1041 "the ctx-bearing path must survive a rebuild too"
1042 );
1043 }
1044
1045 #[test]
1049 fn an_event_posted_before_a_destroy_fires_nothing_and_leaks_nothing() {
1050 let mut tree = WidgetTree::new();
1051 let (source, poster) = install_source(&mut tree, MockEventSource::default());
1052
1053 let signal = Signal::new(String::new());
1054 let id = tree.add(SubscribingWidget {
1055 origin: TestOrigin::Created,
1056 last_message: signal.clone(),
1057 });
1058
1059 source.publish(
1060 TestOrigin::Created,
1061 TestEvent {
1062 id: 1,
1063 message: "too late".to_string(),
1064 },
1065 );
1066 tree.destroy_subtree(id);
1067 drain_and_dispatch(&tree, &poster);
1068
1069 assert_eq!(
1070 signal.get(),
1071 "",
1072 "a destroyed widget's callback must not run"
1073 );
1074 assert_eq!(
1075 tree.app_context().subscription_count(),
1076 0,
1077 "and nothing may be left behind in the callback map"
1078 );
1079 }
1080
1081 #[test]
1089 fn a_rebuild_that_subscribes_more_reuses_what_it_can_and_allocates_the_rest() {
1090 #[derive(Debug)]
1092 struct GrowingWidget {
1093 built: std::rc::Rc<std::cell::Cell<u32>>,
1094 first_message: Signal<String>,
1095 second_message: Signal<String>,
1096 }
1097
1098 impl Widget for GrowingWidget {
1099 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
1100 let first = self.built.get() == 0;
1101 self.built.set(self.built.get() + 1);
1102 let one = self.first_message.clone();
1103 ctx.subscribe_event(TestOrigin::Created, move |event: &TestEvent| {
1104 one.set(event.message.clone());
1105 });
1106 if !first {
1107 let two = self.second_message.clone();
1108 ctx.subscribe_event(TestOrigin::Updated, move |event: &TestEvent| {
1109 two.set(event.message.clone());
1110 });
1111 }
1112 Vec::new()
1113 }
1114
1115 fn layout_response(
1116 &self,
1117 proposal: SizeProposal,
1118 _ctx: &LayoutContext,
1119 ) -> crate::widget::LayoutResponse {
1120 proposal.resolve(0.0, 0.0).into()
1121 }
1122 }
1123
1124 let mut tree = WidgetTree::new();
1125 let (source, poster) = install_source(&mut tree, MockEventSource::default());
1126
1127 let built = std::rc::Rc::new(std::cell::Cell::new(0));
1128 let one = Signal::new(String::new());
1129 let two = Signal::new(String::new());
1130 let id = tree.add(GrowingWidget {
1131 built: built.clone(),
1132 first_message: one.clone(),
1133 second_message: two.clone(),
1134 });
1135 assert_eq!(tree.app_context().subscription_count(), 1);
1136
1137 tree.arena_mark_needs_rebuild_for_testing(id);
1138 tree.layout(SizeProposal::exact(100.0, 100.0));
1139 assert_eq!(
1140 tree.app_context().subscription_count(),
1141 2,
1142 "the re-used slot plus a freshly allocated one"
1143 );
1144 assert_eq!(
1145 source.subscriber_count(),
1146 2,
1147 "and both are registered with the source, not just the re-used one"
1148 );
1149
1150 source.publish(
1152 TestOrigin::Created,
1153 TestEvent {
1154 id: 1,
1155 message: "to the first".to_string(),
1156 },
1157 );
1158 source.publish(
1159 TestOrigin::Updated,
1160 TestEvent {
1161 id: 2,
1162 message: "to the second".to_string(),
1163 },
1164 );
1165 drain_and_dispatch(&tree, &poster);
1166
1167 assert_eq!(one.get(), "to the first");
1168 assert_eq!(
1169 two.get(),
1170 "to the second",
1171 "the newly allocated id must be live"
1172 );
1173 }
1174
1175 #[test]
1179 fn a_rebuild_that_subscribes_less_drops_the_surplus_subscription() {
1180 #[derive(Debug)]
1182 struct ShrinkingWidget {
1183 built: std::rc::Rc<std::cell::Cell<u32>>,
1184 }
1185
1186 impl Widget for ShrinkingWidget {
1187 fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
1188 let first = self.built.get() == 0;
1189 self.built.set(self.built.get() + 1);
1190 ctx.subscribe_event(TestOrigin::Created, |_event: &TestEvent| {});
1191 if first {
1192 ctx.subscribe_event(TestOrigin::Updated, |_event: &TestEvent| {});
1193 }
1194 Vec::new()
1195 }
1196
1197 fn layout_response(
1198 &self,
1199 proposal: SizeProposal,
1200 _ctx: &LayoutContext,
1201 ) -> crate::widget::LayoutResponse {
1202 proposal.resolve(0.0, 0.0).into()
1203 }
1204 }
1205
1206 let mut tree = WidgetTree::new();
1207 let (source, _poster) = install_source(&mut tree, MockEventSource::default());
1208
1209 let built = std::rc::Rc::new(std::cell::Cell::new(0));
1210 let id = tree.add(ShrinkingWidget {
1211 built: built.clone(),
1212 });
1213 assert_eq!(tree.app_context().subscription_count(), 2);
1214 assert_eq!(source.subscriber_count(), 2);
1215
1216 tree.arena_mark_needs_rebuild_for_testing(id);
1217 tree.layout(SizeProposal::exact(100.0, 100.0));
1218
1219 assert_eq!(
1220 tree.app_context().subscription_count(),
1221 1,
1222 "the second slot was not re-registered, so it must be gone"
1223 );
1224 assert_eq!(
1225 source.subscriber_count(),
1226 1,
1227 "and the source must not still be holding it"
1228 );
1229 }
1230}