use rustc_hash::{FxHashMap, FxHashSet};
use smallvec::SmallVec;
use super::state::{ReactiveState, with_reactive};
use crate::app_state::with_app_state;
use crate::jobs::{JobRequest, JobType, request_job};
use crate::reactive::runtime::SignalId;
use crate::tree::WidgetId;
pub(crate) struct SignalTrackingContext {
widget_id: WidgetId,
job_type: JobType,
segment: Option<u32>,
}
pub fn with_signal_tracking<F, R>(widget_id: WidgetId, job_type: JobType, f: F) -> R
where
F: FnOnce() -> R,
{
with_tracking_context(widget_id, job_type, None, f)
}
pub(crate) fn with_segment_tracking<F, R>(widget_id: WidgetId, segment: u32, f: F) -> R
where
F: FnOnce() -> R,
{
with_tracking_context(widget_id, JobType::Reconcile, Some(segment), f)
}
fn with_tracking_context<F, R>(
widget_id: WidgetId,
job_type: JobType,
segment: Option<u32>,
f: F,
) -> R
where
F: FnOnce() -> R,
{
with_reactive(|reactive| {
reactive
.tracking_context
.borrow_mut()
.push(SignalTrackingContext {
widget_id,
job_type,
segment,
});
});
let _guard = crate::reactive::guard::defer(|| {
with_reactive(|reactive| {
reactive.tracking_context.borrow_mut().pop();
});
});
f()
}
pub fn suspend_widget_tracking<F, R>(f: F) -> R
where
F: FnOnce() -> R,
{
let saved: Vec<_> =
with_reactive(|reactive| reactive.tracking_context.borrow_mut().drain(..).collect());
let _guard = crate::reactive::guard::defer(move || {
with_reactive(|reactive| *reactive.tracking_context.borrow_mut() = saved);
});
f()
}
#[cfg(debug_assertions)]
pub(crate) fn widget_tracking_active() -> bool {
with_reactive(|reactive| !reactive.tracking_context.borrow().is_empty())
}
pub fn record_signal_read(signal_id: SignalId) {
with_reactive(|reactive| {
if let Some(tracking) = reactive.tracking_context.borrow().last() {
register_subscriber_in(
reactive,
tracking.widget_id,
signal_id,
tracking.job_type,
tracking.segment,
);
}
});
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
struct Subscriber {
widget_id: WidgetId,
job_type: JobType,
segment: Option<u32>,
}
type SubscriberList = SmallVec<[Subscriber; 2]>;
type SignalList = SmallVec<[SignalId; 4]>;
#[derive(Default)]
pub(crate) struct SubscriberRegistry {
signal_to_widgets: FxHashMap<usize, SubscriberList>,
widget_to_signals: FxHashMap<WidgetId, SignalList>,
active: FxHashSet<(usize, Subscriber)>,
detached: FxHashSet<WidgetId>,
}
impl SubscriberRegistry {
fn is_detached(&self, widget_id: WidgetId) -> bool {
!self.detached.is_empty() && self.detached.contains(&widget_id)
}
}
pub(crate) fn take_dirty_segments(widget_id: WidgetId) -> Option<SmallVec<[u32; 4]>> {
with_app_state(|app| app.dirty_segments.borrow_mut().remove(&widget_id))
}
pub fn register_subscriber(widget_id: WidgetId, signal_id: SignalId, job_type: JobType) {
with_reactive(|reactive| {
register_subscriber_in(reactive, widget_id, signal_id, job_type, None);
});
}
fn register_subscriber_in(
reactive: &ReactiveState,
widget_id: WidgetId,
signal_id: SignalId,
job_type: JobType,
segment: Option<u32>,
) {
let mut reg = reactive.subscribers.borrow_mut();
if reg.is_detached(widget_id) {
return;
}
let sub = Subscriber {
widget_id,
job_type,
segment,
};
if !reg.active.insert((signal_id.index(), sub)) {
return; }
reg.signal_to_widgets
.entry(signal_id.index())
.or_default()
.push(sub);
let signals = reg.widget_to_signals.entry(widget_id).or_default();
if !signals.contains(&signal_id) {
signals.push(signal_id);
}
}
pub fn notify_signal_change(signal_id: SignalId) {
with_reactive(|reactive| {
let reg = reactive.subscribers.borrow();
let Some(subs) = reg.signal_to_widgets.get(&signal_id.index()) else {
return;
};
for sub in subs {
if let Some(segment) = sub.segment {
mark_segment_dirty(sub.widget_id, segment);
}
let request = match sub.job_type {
JobType::Layout => JobRequest::Layout,
JobType::Paint => JobRequest::Paint,
JobType::Reconcile => JobRequest::Reconcile,
JobType::Unregister => JobRequest::Unregister,
JobType::Animation => JobRequest::Animation(crate::jobs::RequiredJob::None),
};
request_job(sub.widget_id, request);
}
});
}
pub fn clear_signal_subscribers(signal_id: SignalId) {
with_reactive(|reactive| {
let mut reg = reactive.subscribers.borrow_mut();
if let Some(subs) = reg.signal_to_widgets.remove(&signal_id.index()) {
for sub in &subs {
reg.active.remove(&(signal_id.index(), *sub));
if let Some(signals) = reg.widget_to_signals.get_mut(&sub.widget_id) {
signals.retain(|&mut s| s != signal_id);
if signals.is_empty() {
reg.widget_to_signals.remove(&sub.widget_id);
}
}
}
}
});
}
pub(crate) fn detach_widget(widget_id: WidgetId) {
clear_widget_subscribers(widget_id);
with_reactive(|reactive| {
reactive.subscribers.borrow_mut().detached.insert(widget_id);
});
}
pub(crate) fn reattach_widget(widget_id: WidgetId) {
with_reactive(|reactive| {
reactive
.subscribers
.borrow_mut()
.detached
.remove(&widget_id);
});
}
pub(crate) fn is_detached(widget_id: WidgetId) -> bool {
with_reactive(|reactive| reactive.subscribers.borrow().is_detached(widget_id))
}
pub(crate) const EVERY_SEGMENT: u32 = u32::MAX;
pub(crate) fn mark_every_segment_dirty(widget_id: WidgetId) {
mark_segment_dirty(widget_id, EVERY_SEGMENT);
}
fn mark_segment_dirty(widget_id: WidgetId, segment: u32) {
with_app_state(|app| {
let mut dirty = app.dirty_segments.borrow_mut();
let segments = dirty.entry(widget_id).or_default();
if !segments.contains(&segment) {
segments.push(segment);
}
});
}
pub fn clear_widget_subscribers(widget_id: WidgetId) {
with_reactive(|reactive| {
let mut reg = reactive.subscribers.borrow_mut();
reg.detached.remove(&widget_id);
if let Some(signal_ids) = reg.widget_to_signals.remove(&widget_id) {
for signal_id in signal_ids {
let Some(subs) = reg.signal_to_widgets.get_mut(&signal_id.index()) else {
continue;
};
let removed: SmallVec<[Subscriber; 2]> = subs
.iter()
.filter(|s| s.widget_id == widget_id)
.copied()
.collect();
subs.retain(|s| s.widget_id != widget_id);
if subs.is_empty() {
reg.signal_to_widgets.remove(&signal_id.index());
}
for sub in removed {
reg.active.remove(&(signal_id.index(), sub));
}
}
}
});
with_app_state(|app| {
app.dirty_segments.borrow_mut().remove(&widget_id);
});
}
#[cfg(test)]
fn subscriber_count() -> usize {
with_reactive(|reactive| reactive.subscribers.borrow().signal_to_widgets.len())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tree::WidgetId;
fn widget_id(n: u64) -> WidgetId {
WidgetId::from_u64(n)
}
fn signal_id(n: u32) -> SignalId {
SignalId::new(n, 0)
}
#[test]
fn the_innermost_scope_owns_the_read() {
let parent = widget_id(300);
let leaf = widget_id(301);
with_signal_tracking(parent, JobType::Layout, || {
with_signal_tracking(leaf, JobType::Layout, || {
record_signal_read(signal_id(70));
});
record_signal_read(signal_id(71));
});
let subscribers = |sig: u32| {
with_reactive(|reactive| {
let reg = reactive.subscribers.borrow();
reg.signal_to_widgets
.get(&(sig as usize))
.map(|s| s.iter().map(|e| e.widget_id).collect::<Vec<_>>())
.unwrap_or_default()
})
};
assert_eq!(subscribers(70), vec![leaf]);
assert_eq!(subscribers(71), vec![parent]);
}
#[test]
fn only_a_signal_a_widget_reads_has_an_entry() {
use crate::reactive::create_signal;
let before = subscriber_count();
let signals: Vec<_> = (0..1000).map(|_| create_signal(0u32)).collect();
assert_eq!(subscriber_count(), before, "nothing has read them");
let last = signals.last().unwrap();
with_signal_tracking(widget_id(600), JobType::Paint, || last.get());
assert_eq!(subscriber_count(), before + 1, "one read, one entry");
}
#[test]
fn test_clear_signal_subscribers_removes_entry() {
let wid = widget_id(100);
register_subscriber(wid, signal_id(42), JobType::Paint);
assert!(subscriber_count() > 0);
clear_signal_subscribers(signal_id(42));
with_reactive(|reactive| {
let reg = reactive.subscribers.borrow();
assert!(!reg.signal_to_widgets.contains_key(&42));
});
}
#[test]
fn test_clear_widget_subscribers_removes_from_all_signals() {
let wid = widget_id(200);
let other = widget_id(201);
register_subscriber(wid, signal_id(10), JobType::Paint);
register_subscriber(wid, signal_id(11), JobType::Layout);
register_subscriber(other, signal_id(10), JobType::Paint);
clear_widget_subscribers(wid);
with_reactive(|reactive| {
let reg = reactive.subscribers.borrow();
let s10 = ®.signal_to_widgets[&10];
assert!(s10.iter().all(|s| s.widget_id != wid));
assert!(s10.iter().any(|s| s.widget_id == other));
assert!(!reg.signal_to_widgets.contains_key(&11));
});
}
#[test]
fn an_untracked_read_of_a_derived_subscribes_nobody() {
use crate::reactive::{create_derived, create_signal};
let subscribed = |wid: WidgetId| {
with_reactive(|reactive| {
reactive
.subscribers
.borrow()
.widget_to_signals
.get(&wid)
.is_some_and(|signals| !signals.is_empty())
})
};
let inner = create_signal(1i32);
let derived = create_derived(move || inner.get() + 1);
let untracked = widget_id(500);
with_signal_tracking(untracked, JobType::Paint, || {
assert_eq!(derived.get_untracked(), 2);
});
assert!(
!subscribed(untracked),
"an untracked read must leave the scope holding nothing"
);
let borrowed = widget_id(502);
with_signal_tracking(borrowed, JobType::Paint, || {
assert_eq!(derived.with_untracked(|v| *v), 2);
});
assert!(
!subscribed(borrowed),
"`with_untracked` says untracked too, and has to mean it"
);
let tracked = widget_id(501);
with_signal_tracking(tracked, JobType::Paint, || {
assert_eq!(derived.get(), 2);
});
assert!(
subscribed(tracked),
"and a tracked one still has to subscribe, or the test proves nothing"
);
}
#[test]
fn test_with_signal_tracking_registers_subscriber() {
let wid = widget_id(300);
let sid = signal_id(99);
with_signal_tracking(wid, JobType::Paint, || {
record_signal_read(sid);
});
with_reactive(|reactive| {
let reg = reactive.subscribers.borrow();
let s = ®.signal_to_widgets[&sid.index()];
assert!(s.contains(&Subscriber {
widget_id: wid,
job_type: JobType::Paint,
segment: None,
}));
});
clear_signal_subscribers(sid);
}
fn subscribed(wid: WidgetId, sid: SignalId) -> bool {
with_reactive(|reactive| {
reactive
.subscribers
.borrow()
.signal_to_widgets
.get(&sid.index())
.is_some_and(|subs| subs.iter().any(|s| s.widget_id == wid))
})
}
#[test]
fn a_detached_widget_reads_without_subscribing() {
let wid = widget_id(410);
let sid = signal_id(60);
let read = || with_signal_tracking(wid, JobType::Paint, || record_signal_read(sid));
read();
assert!(subscribed(wid, sid), "the control: a read subscribes");
detach_widget(wid);
assert!(!subscribed(wid, sid), "the detach clears it");
read();
assert!(
!subscribed(wid, sid),
"and a read while detached registers nothing"
);
assert!(is_detached(wid));
reattach_widget(wid);
read();
assert!(subscribed(wid, sid), "reattached, reading subscribes again");
detach_widget(wid);
clear_widget_subscribers(wid);
assert!(
!is_detached(wid),
"a widget leaving the tree forgets its detach"
);
clear_signal_subscribers(sid);
}
#[test]
fn test_reverse_index_consistency() {
let wid = widget_id(400);
register_subscriber(wid, signal_id(50), JobType::Paint);
register_subscriber(wid, signal_id(51), JobType::Layout);
with_reactive(|reactive| {
let reg = reactive.subscribers.borrow();
let signals = reg.widget_to_signals.get(&wid).unwrap();
assert!(signals.contains(&signal_id(50)));
assert!(signals.contains(&signal_id(51)));
});
clear_widget_subscribers(wid);
with_reactive(|reactive| {
let reg = reactive.subscribers.borrow();
assert!(!reg.widget_to_signals.contains_key(&wid));
});
}
}