use std::any::{Any, TypeId};
use std::cell::{Cell, RefCell};
use std::collections::HashMap;
use std::rc::Rc;
use std::sync::Arc;
use crate::widget::EventContext;
use crate::window::TeksiloWindowId;
pub trait EventSource: 'static {
type Origin: Clone + 'static;
type Event: Send + 'static;
fn subscribe(
&self,
origin: Self::Origin,
callback: Arc<dyn Fn(Self::Event) + Send + Sync + 'static>,
) -> SubscriptionHandle;
}
pub struct SubscriptionHandle {
_inner: Box<dyn Any>,
}
impl SubscriptionHandle {
pub fn new<T: 'static>(token: T) -> Self {
Self {
_inner: Box::new(token),
}
}
pub fn empty() -> Self {
Self::new(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct SubscriptionId(pub(crate) u64);
pub trait AppEventPoster: Send + Sync + 'static {
fn post_subscription_event(&self, sub_id: SubscriptionId, event: Box<dyn Any + Send>);
fn post_external(&self, _payload: Box<dyn Any + Send>) {}
}
pub struct EventSourceAdapter {
pub(crate) origin_type: TypeId,
pub(crate) origin_type_name: &'static str,
pub(crate) event_type: TypeId,
pub(crate) event_type_name: &'static str,
#[allow(clippy::type_complexity)]
pub(crate) subscribe_fn: Box<
dyn Fn(
Box<dyn Any>,
Arc<dyn Fn(Box<dyn Any + Send>) + Send + Sync + 'static>,
) -> SubscriptionHandle,
>,
}
impl EventSourceAdapter {
pub fn new<S: EventSource>(source: S) -> Self {
let source = Arc::new(source);
let origin_type = TypeId::of::<S::Origin>();
let origin_type_name = std::any::type_name::<S::Origin>();
let event_type = TypeId::of::<S::Event>();
let event_type_name = std::any::type_name::<S::Event>();
let subscribe_fn: Box<
dyn Fn(
Box<dyn Any>,
Arc<dyn Fn(Box<dyn Any + Send>) + Send + Sync + 'static>,
) -> SubscriptionHandle,
> = Box::new(move |erased_origin, framework_wrapper| {
let origin: Box<S::Origin> = erased_origin
.downcast::<S::Origin>()
.expect("origin type mismatch — framework bug");
let typed_callback: Arc<dyn Fn(S::Event) + Send + Sync + 'static> =
Arc::new(move |event: S::Event| {
let erased: Box<dyn Any + Send> = Box::new(event);
framework_wrapper(erased);
});
source.subscribe(*origin, typed_callback)
});
Self {
origin_type,
origin_type_name,
event_type,
event_type_name,
subscribe_fn,
}
}
}
type CtxSubscriptionCallback = Rc<dyn Fn(&dyn Any, &mut EventContext)>;
pub struct TreeAppContext {
pub(crate) poster: Option<Arc<dyn AppEventPoster>>,
pub(crate) event_source: Option<EventSourceAdapter>,
#[allow(clippy::type_complexity)]
pub(crate) subscription_callbacks: RefCell<HashMap<SubscriptionId, Box<dyn Fn(&dyn Any)>>>,
#[allow(clippy::type_complexity)]
pub(crate) subscription_ctx_callbacks:
RefCell<HashMap<SubscriptionId, (Option<TeksiloWindowId>, CtxSubscriptionCallback)>>,
pub(crate) next_subscription_id: Cell<u64>,
pub(crate) app_state: HashMap<TypeId, Box<dyn Any>>,
}
impl TreeAppContext {
pub fn empty() -> Self {
Self {
poster: None,
event_source: None,
subscription_callbacks: RefCell::new(HashMap::new()),
subscription_ctx_callbacks: RefCell::new(HashMap::new()),
next_subscription_id: Cell::new(1),
app_state: HashMap::new(),
}
}
pub fn with_source_and_poster(
event_source: EventSourceAdapter,
poster: Arc<dyn AppEventPoster>,
) -> Self {
Self {
poster: Some(poster),
event_source: Some(event_source),
subscription_callbacks: RefCell::new(HashMap::new()),
subscription_ctx_callbacks: RefCell::new(HashMap::new()),
next_subscription_id: Cell::new(1),
app_state: HashMap::new(),
}
}
pub fn with_app_state(mut self, registry: HashMap<TypeId, Box<dyn Any>>) -> Self {
self.app_state = registry;
self
}
pub fn with_poster(mut self, poster: Arc<dyn AppEventPoster>) -> Self {
self.poster = Some(poster);
self
}
pub fn poster(&self) -> Option<&Arc<dyn AppEventPoster>> {
self.poster.as_ref()
}
pub fn app_state<T: 'static>(&self) -> Option<&T> {
self.app_state
.get(&TypeId::of::<T>())
.and_then(|boxed| boxed.downcast_ref::<T>())
}
pub(crate) fn allocate_subscription_id(&self) -> SubscriptionId {
let id = self.next_subscription_id.get();
self.next_subscription_id.set(id + 1);
SubscriptionId(id)
}
pub fn dispatch_subscription_event(&self, sub_id: SubscriptionId, event: &dyn Any) -> bool {
let callbacks = self.subscription_callbacks.borrow();
if let Some(callback) = callbacks.get(&sub_id) {
callback(event);
true
} else {
false
}
}
pub fn ctx_subscription_window(&self, sub_id: SubscriptionId) -> Option<TeksiloWindowId> {
self.subscription_ctx_callbacks
.borrow()
.get(&sub_id)
.and_then(|(window_id, _)| *window_id)
}
pub fn dispatch_subscription_event_with_ctx(
&self,
sub_id: SubscriptionId,
event: &dyn Any,
ctx: &mut EventContext,
) -> bool {
let callback = self
.subscription_ctx_callbacks
.borrow()
.get(&sub_id)
.map(|(_window_id, callback)| Rc::clone(callback));
match callback {
Some(callback) => {
callback(event, ctx);
true
}
None => false,
}
}
pub fn ctx_subscription_count(&self) -> usize {
self.subscription_ctx_callbacks.borrow().len()
}
pub fn purge_ctx_subscriptions_for_window(&self, window_id: TeksiloWindowId) {
self.subscription_ctx_callbacks
.borrow_mut()
.retain(|_, (win, _)| *win != Some(window_id));
}
pub fn subscription_count(&self) -> usize {
self.subscription_callbacks.borrow().len()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::signal::Signal;
use crate::widget::{LayoutContext, Widget};
use crate::widget_id::WidgetId;
use crate::widget_tree::WidgetTree;
use std::sync::Mutex;
use teksilo_canvas::SizeProposal;
#[derive(Clone, PartialEq, Eq, Hash, Debug)]
enum TestOrigin {
Created,
Updated,
}
#[derive(Clone, Debug, PartialEq)]
struct TestEvent {
id: u64,
message: String,
}
#[derive(Default)]
struct MockEventSource {
#[allow(clippy::type_complexity)]
subscribers: Arc<
Mutex<
Vec<(
u64,
TestOrigin,
Arc<dyn Fn(TestEvent) + Send + Sync + 'static>,
)>,
>,
>,
next_id: std::sync::atomic::AtomicU64,
}
struct MockToken {
#[allow(clippy::type_complexity)]
subscribers: Arc<
Mutex<
Vec<(
u64,
TestOrigin,
Arc<dyn Fn(TestEvent) + Send + Sync + 'static>,
)>,
>,
>,
id: u64,
}
impl Drop for MockToken {
fn drop(&mut self) {
if let Ok(mut subs) = self.subscribers.lock() {
subs.retain(|(id, _, _)| *id != self.id);
}
}
}
impl EventSource for MockEventSource {
type Origin = TestOrigin;
type Event = TestEvent;
fn subscribe(
&self,
origin: Self::Origin,
callback: Arc<dyn Fn(Self::Event) + Send + Sync + 'static>,
) -> SubscriptionHandle {
let id = self
.next_id
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
self.subscribers
.lock()
.unwrap()
.push((id, origin, callback));
SubscriptionHandle::new(MockToken {
subscribers: self.subscribers.clone(),
id,
})
}
}
impl MockEventSource {
fn publish(&self, origin: TestOrigin, event: TestEvent) {
let subs = self.subscribers.lock().unwrap();
for (_id, sub_origin, cb) in subs.iter() {
if *sub_origin == origin {
cb(event.clone());
}
}
}
fn subscriber_count(&self) -> usize {
self.subscribers.lock().unwrap().len()
}
}
#[derive(Default)]
struct TestPoster {
#[allow(clippy::type_complexity)]
queue: Mutex<Vec<(SubscriptionId, Box<dyn Any + Send>)>>,
}
impl AppEventPoster for TestPoster {
fn post_subscription_event(&self, sub_id: SubscriptionId, event: Box<dyn Any + Send>) {
self.queue.lock().unwrap().push((sub_id, event));
}
}
impl TestPoster {
fn drain(&self) -> Vec<(SubscriptionId, Box<dyn Any + Send>)> {
std::mem::take(&mut *self.queue.lock().unwrap())
}
}
#[derive(Debug)]
struct SubscribingWidget {
origin: TestOrigin,
last_message: Signal<String>,
}
impl Widget for SubscribingWidget {
fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
let last_message = self.last_message.clone();
ctx.subscribe_event(self.origin.clone(), move |event: &TestEvent| {
last_message.set(event.message.clone());
});
Vec::new()
}
fn layout_response(
&self,
proposal: SizeProposal,
_ctx: &LayoutContext,
) -> crate::widget::LayoutResponse {
proposal.resolve(0.0, 0.0).into()
}
}
#[derive(Debug)]
struct CtxSubscribingWidget {
origin: TestOrigin,
last_message: Signal<String>,
}
impl Widget for CtxSubscribingWidget {
fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
let last_message = self.last_message.clone();
ctx.subscribe_event_with_ctx(
self.origin.clone(),
move |event: &TestEvent, _ctx: &mut crate::widget::EventContext| {
last_message.set(event.message.clone());
},
);
Vec::new()
}
fn layout_response(
&self,
proposal: SizeProposal,
_ctx: &LayoutContext,
) -> crate::widget::LayoutResponse {
proposal.resolve(0.0, 0.0).into()
}
}
fn install_source(
tree: &mut WidgetTree,
source: MockEventSource,
) -> (Arc<MockEventSource>, Arc<TestPoster>) {
let source = Arc::new(source);
struct SharedSource {
inner: Arc<MockEventSource>,
}
impl EventSource for SharedSource {
type Origin = TestOrigin;
type Event = TestEvent;
fn subscribe(
&self,
origin: Self::Origin,
callback: Arc<dyn Fn(Self::Event) + Send + Sync + 'static>,
) -> SubscriptionHandle {
self.inner.subscribe(origin, callback)
}
}
let adapter = EventSourceAdapter::new(SharedSource {
inner: source.clone(),
});
let poster: Arc<TestPoster> = Arc::new(TestPoster::default());
let poster_dyn: Arc<dyn AppEventPoster> = poster.clone();
let app_context =
std::rc::Rc::new(TreeAppContext::with_source_and_poster(adapter, poster_dyn));
tree.set_app_context(app_context);
(source, poster)
}
fn drain_and_dispatch(tree: &WidgetTree, poster: &TestPoster) {
let events = poster.drain();
for (sub_id, event) in events {
tree.app_context()
.dispatch_subscription_event(sub_id, &*event);
}
}
#[test]
fn subscribe_event_delivers_to_widget_signal() {
let mut tree = WidgetTree::new();
let (source, poster) = install_source(&mut tree, MockEventSource::default());
let signal = Signal::new(String::new());
let _id = tree.add(SubscribingWidget {
origin: TestOrigin::Created,
last_message: signal.clone(),
});
assert_eq!(source.subscriber_count(), 1);
assert_eq!(tree.app_context().subscription_count(), 1);
source.publish(
TestOrigin::Created,
TestEvent {
id: 1,
message: "hello".to_string(),
},
);
drain_and_dispatch(&tree, &poster);
assert_eq!(signal.get(), "hello");
}
#[test]
fn subscribe_event_with_ctx_dispatches_inside_fresh_context() {
use crate::window::{NoopWindowOps, TeksiloWindowId};
let mut tree = WidgetTree::new();
let app_ctx = tree.app_context().clone();
let sub_id = app_ctx.allocate_subscription_id();
let win = TeksiloWindowId::new(1);
let seen = Signal::new(String::new());
let seen_cb = seen.clone();
let stored: std::rc::Rc<dyn Fn(&dyn Any, &mut crate::widget::EventContext)> =
std::rc::Rc::new(move |event_any, _ctx: &mut crate::widget::EventContext| {
let ev = event_any
.downcast_ref::<TestEvent>()
.expect("subscription event downcast failed");
seen_cb.set(ev.message.clone());
});
app_ctx
.subscription_ctx_callbacks
.borrow_mut()
.insert(sub_id, (Some(win), stored));
assert_eq!(app_ctx.ctx_subscription_window(sub_id), Some(win));
assert_eq!(app_ctx.ctx_subscription_window(SubscriptionId(9999)), None);
let event = TestEvent {
id: 9,
message: "progress-42".to_string(),
};
let handled = std::cell::Cell::new(false);
tree.run_with_event_context(&mut NoopWindowOps, |ctx| {
handled.set(app_ctx.dispatch_subscription_event_with_ctx(sub_id, &event, ctx));
});
assert!(
handled.get(),
"context-bearing dispatch must find the callback"
);
assert_eq!(seen.get(), "progress-42");
tree.run_with_event_context(&mut NoopWindowOps, |ctx| {
assert!(!app_ctx.dispatch_subscription_event_with_ctx(
SubscriptionId(9999),
&event,
ctx
));
});
}
#[test]
fn ctx_dispatch_releases_borrow_before_invoking_callback() {
use crate::window::{NoopWindowOps, TeksiloWindowId};
let mut tree = WidgetTree::new();
let app_ctx = tree.app_context().clone();
let sub_id = app_ctx.allocate_subscription_id();
let reenter_ctx = app_ctx.clone();
let reentered = std::rc::Rc::new(std::cell::Cell::new(false));
let flag = reentered.clone();
let cb: std::rc::Rc<dyn Fn(&dyn Any, &mut crate::widget::EventContext)> =
std::rc::Rc::new(move |_ev, _ctx| {
reenter_ctx.subscription_ctx_callbacks.borrow_mut().insert(
SubscriptionId(4242),
(
Some(TeksiloWindowId::new(2)),
std::rc::Rc::new(|_e: &dyn Any, _c: &mut crate::widget::EventContext| {}),
),
);
flag.set(true);
});
app_ctx
.subscription_ctx_callbacks
.borrow_mut()
.insert(sub_id, (Some(TeksiloWindowId::new(1)), cb));
let event = TestEvent {
id: 1,
message: String::new(),
};
tree.run_with_event_context(&mut NoopWindowOps, |ctx| {
assert!(app_ctx.dispatch_subscription_event_with_ctx(sub_id, &event, ctx));
});
assert!(
reentered.get(),
"callback ran and its re-entrant map insert did not panic"
);
assert_eq!(
app_ctx.ctx_subscription_count(),
2,
"original + the re-entrant insert both present"
);
}
#[test]
fn subscribe_event_with_ctx_registers_and_tears_down() {
let mut tree = WidgetTree::new();
let (_source, _poster) = install_source(&mut tree, MockEventSource::default());
let id = tree.add(CtxSubscribingWidget {
origin: TestOrigin::Created,
last_message: Signal::new(String::new()),
});
assert_eq!(tree.app_context().ctx_subscription_count(), 1);
assert_eq!(tree.app_context().subscription_count(), 0);
tree.destroy_subtree(id);
assert_eq!(
tree.app_context().ctx_subscription_count(),
0,
"destroying the widget must remove its context-bearing subscription"
);
}
#[test]
fn unrelated_origin_does_not_fire_callback() {
let mut tree = WidgetTree::new();
let (source, poster) = install_source(&mut tree, MockEventSource::default());
let signal = Signal::new(String::new());
let _id = tree.add(SubscribingWidget {
origin: TestOrigin::Created,
last_message: signal.clone(),
});
source.publish(
TestOrigin::Updated,
TestEvent {
id: 1,
message: "ignored".to_string(),
},
);
drain_and_dispatch(&tree, &poster);
assert_eq!(signal.get(), "");
}
#[test]
fn destroying_widget_removes_ui_callback() {
let mut tree = WidgetTree::new();
let (_source, _poster) = install_source(&mut tree, MockEventSource::default());
let signal = Signal::new(String::new());
let id = tree.add(SubscribingWidget {
origin: TestOrigin::Created,
last_message: signal.clone(),
});
assert_eq!(tree.app_context().subscription_count(), 1);
tree.destroy_subtree(id);
assert_eq!(tree.app_context().subscription_count(), 0);
}
#[test]
fn in_flight_event_after_destroy_is_dropped_not_delivered() {
let mut tree = WidgetTree::new();
let (source, poster) = install_source(&mut tree, MockEventSource::default());
let signal = Signal::new(String::new());
let id = tree.add(SubscribingWidget {
origin: TestOrigin::Created,
last_message: signal.clone(),
});
source.publish(
TestOrigin::Created,
TestEvent {
id: 7,
message: "buffered".to_string(),
},
);
tree.destroy_subtree(id);
drain_and_dispatch(&tree, &poster);
assert_eq!(signal.get(), "");
assert_eq!(tree.app_context().subscription_count(), 0);
}
#[test]
#[should_panic(expected = "no event source was registered")]
fn subscribe_without_event_source_panics() {
let mut tree = WidgetTree::new();
let signal = Signal::new(String::new());
tree.add(SubscribingWidget {
origin: TestOrigin::Created,
last_message: signal,
});
}
use std::rc::Rc;
struct TestGlobals {
greeting: Signal<String>,
}
#[derive(Debug)]
struct AppStateReader {
observed: Signal<String>,
saw_none: Signal<bool>,
}
impl Widget for AppStateReader {
fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
match ctx.app_state::<Rc<TestGlobals>>() {
Some(globals) => self.observed.set(globals.greeting.get()),
None => self.saw_none.set(true),
}
Vec::new()
}
fn layout_response(
&self,
proposal: SizeProposal,
_ctx: &LayoutContext,
) -> crate::widget::LayoutResponse {
proposal.resolve(0.0, 0.0).into()
}
}
#[test]
fn app_state_roundtrip_in_build_context() {
let globals = Rc::new(TestGlobals {
greeting: Signal::new("hello from registry".to_string()),
});
let mut registry: HashMap<TypeId, Box<dyn Any>> = HashMap::new();
registry.insert(TypeId::of::<Rc<TestGlobals>>(), Box::new(globals.clone()));
let mut tree = WidgetTree::new();
tree.set_app_context(Rc::new(TreeAppContext::empty().with_app_state(registry)));
let observed = Signal::new(String::new());
let saw_none = Signal::new(false);
tree.add(AppStateReader {
observed: observed.clone(),
saw_none: saw_none.clone(),
});
assert_eq!(observed.get(), "hello from registry");
assert!(!saw_none.get());
}
#[test]
fn app_state_missing_returns_none() {
let mut tree = WidgetTree::new();
let observed = Signal::new(String::new());
let saw_none = Signal::new(false);
tree.add(AppStateReader {
observed: observed.clone(),
saw_none: saw_none.clone(),
});
assert_eq!(observed.get(), "");
assert!(saw_none.get());
}
#[test]
fn app_state_distinct_types_coexist() {
struct Alpha(u32);
struct Beta(String);
let mut registry: HashMap<TypeId, Box<dyn Any>> = HashMap::new();
registry.insert(TypeId::of::<Rc<Alpha>>(), Box::new(Rc::new(Alpha(42))));
registry.insert(
TypeId::of::<Rc<Beta>>(),
Box::new(Rc::new(Beta("beta!".to_string()))),
);
let ctx = TreeAppContext::empty().with_app_state(registry);
assert_eq!(ctx.app_state::<Rc<Alpha>>().unwrap().0, 42);
assert_eq!(ctx.app_state::<Rc<Beta>>().unwrap().0, "beta!");
assert!(ctx.app_state::<Rc<u64>>().is_none());
}
#[test]
fn an_event_posted_before_a_rebuild_still_reaches_the_widget() {
let mut tree = WidgetTree::new();
let (source, poster) = install_source(&mut tree, MockEventSource::default());
let signal = Signal::new(String::new());
let id = tree.add(SubscribingWidget {
origin: TestOrigin::Created,
last_message: signal.clone(),
});
source.publish(
TestOrigin::Created,
TestEvent {
id: 1,
message: "landed".to_string(),
},
);
tree.arena_mark_needs_rebuild_for_testing(id);
tree.layout(SizeProposal::exact(100.0, 100.0));
drain_and_dispatch(&tree, &poster);
assert_eq!(
signal.get(),
"landed",
"the rebuild must not swallow an event that was already in flight"
);
}
#[test]
fn an_event_posted_before_a_rebuild_still_reaches_a_context_bearing_subscription() {
use crate::window::NoopWindowOps;
let mut tree = WidgetTree::new();
let (source, poster) = install_source(&mut tree, MockEventSource::default());
let signal = Signal::new(String::new());
let id = tree.add(CtxSubscribingWidget {
origin: TestOrigin::Created,
last_message: signal.clone(),
});
source.publish(
TestOrigin::Created,
TestEvent {
id: 1,
message: "landed".to_string(),
},
);
tree.arena_mark_needs_rebuild_for_testing(id);
tree.layout(SizeProposal::exact(100.0, 100.0));
let app_ctx = tree.app_context().clone();
let events = poster.drain();
assert!(!events.is_empty(), "the source must have posted something");
for (sub_id, event) in events {
tree.run_with_event_context(&mut NoopWindowOps, |ctx| {
app_ctx.dispatch_subscription_event_with_ctx(sub_id, &*event, ctx);
});
}
assert_eq!(
signal.get(),
"landed",
"the ctx-bearing path must survive a rebuild too"
);
}
#[test]
fn an_event_posted_before_a_destroy_fires_nothing_and_leaks_nothing() {
let mut tree = WidgetTree::new();
let (source, poster) = install_source(&mut tree, MockEventSource::default());
let signal = Signal::new(String::new());
let id = tree.add(SubscribingWidget {
origin: TestOrigin::Created,
last_message: signal.clone(),
});
source.publish(
TestOrigin::Created,
TestEvent {
id: 1,
message: "too late".to_string(),
},
);
tree.destroy_subtree(id);
drain_and_dispatch(&tree, &poster);
assert_eq!(
signal.get(),
"",
"a destroyed widget's callback must not run"
);
assert_eq!(
tree.app_context().subscription_count(),
0,
"and nothing may be left behind in the callback map"
);
}
#[test]
fn a_rebuild_that_subscribes_more_reuses_what_it_can_and_allocates_the_rest() {
#[derive(Debug)]
struct GrowingWidget {
built: std::rc::Rc<std::cell::Cell<u32>>,
first_message: Signal<String>,
second_message: Signal<String>,
}
impl Widget for GrowingWidget {
fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
let first = self.built.get() == 0;
self.built.set(self.built.get() + 1);
let one = self.first_message.clone();
ctx.subscribe_event(TestOrigin::Created, move |event: &TestEvent| {
one.set(event.message.clone());
});
if !first {
let two = self.second_message.clone();
ctx.subscribe_event(TestOrigin::Updated, move |event: &TestEvent| {
two.set(event.message.clone());
});
}
Vec::new()
}
fn layout_response(
&self,
proposal: SizeProposal,
_ctx: &LayoutContext,
) -> crate::widget::LayoutResponse {
proposal.resolve(0.0, 0.0).into()
}
}
let mut tree = WidgetTree::new();
let (source, poster) = install_source(&mut tree, MockEventSource::default());
let built = std::rc::Rc::new(std::cell::Cell::new(0));
let one = Signal::new(String::new());
let two = Signal::new(String::new());
let id = tree.add(GrowingWidget {
built: built.clone(),
first_message: one.clone(),
second_message: two.clone(),
});
assert_eq!(tree.app_context().subscription_count(), 1);
tree.arena_mark_needs_rebuild_for_testing(id);
tree.layout(SizeProposal::exact(100.0, 100.0));
assert_eq!(
tree.app_context().subscription_count(),
2,
"the re-used slot plus a freshly allocated one"
);
assert_eq!(
source.subscriber_count(),
2,
"and both are registered with the source, not just the re-used one"
);
source.publish(
TestOrigin::Created,
TestEvent {
id: 1,
message: "to the first".to_string(),
},
);
source.publish(
TestOrigin::Updated,
TestEvent {
id: 2,
message: "to the second".to_string(),
},
);
drain_and_dispatch(&tree, &poster);
assert_eq!(one.get(), "to the first");
assert_eq!(
two.get(),
"to the second",
"the newly allocated id must be live"
);
}
#[test]
fn a_rebuild_that_subscribes_less_drops_the_surplus_subscription() {
#[derive(Debug)]
struct ShrinkingWidget {
built: std::rc::Rc<std::cell::Cell<u32>>,
}
impl Widget for ShrinkingWidget {
fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
let first = self.built.get() == 0;
self.built.set(self.built.get() + 1);
ctx.subscribe_event(TestOrigin::Created, |_event: &TestEvent| {});
if first {
ctx.subscribe_event(TestOrigin::Updated, |_event: &TestEvent| {});
}
Vec::new()
}
fn layout_response(
&self,
proposal: SizeProposal,
_ctx: &LayoutContext,
) -> crate::widget::LayoutResponse {
proposal.resolve(0.0, 0.0).into()
}
}
let mut tree = WidgetTree::new();
let (source, _poster) = install_source(&mut tree, MockEventSource::default());
let built = std::rc::Rc::new(std::cell::Cell::new(0));
let id = tree.add(ShrinkingWidget {
built: built.clone(),
});
assert_eq!(tree.app_context().subscription_count(), 2);
assert_eq!(source.subscriber_count(), 2);
tree.arena_mark_needs_rebuild_for_testing(id);
tree.layout(SizeProposal::exact(100.0, 100.0));
assert_eq!(
tree.app_context().subscription_count(),
1,
"the second slot was not re-registered, so it must be gone"
);
assert_eq!(
source.subscriber_count(),
1,
"and the source must not still be holding it"
);
}
}