1use crate::reactive;
2use crate::scheduler_counters;
3use crate::tasks;
4use incular_config::RuntimeEnvironment;
5use incular_widgets::internal::ElementId;
6#[cfg(feature = "devtools")]
7use incular_widgets::internal::InvalidationCause;
8use incular_widgets::{FocusScopeNode, FocusScopeSubscription};
9#[cfg(feature = "devtools")]
10use std::any::{Any, TypeId};
11#[cfg(feature = "devtools")]
12use std::cell::Cell;
13use std::{
14 cell::RefCell,
15 collections::{HashMap, HashSet, VecDeque},
16 rc::{Rc, Weak},
17 sync::atomic::{AtomicU64, Ordering},
18};
19
20#[cfg(feature = "devtools")]
22pub(crate) fn truncate_debug<T: std::fmt::Debug>(value: &T) -> String {
23 let rendered = format!("{value:?}");
24 if rendered.chars().count() <= 80 {
25 rendered
26 } else {
27 let mut out: String = rendered.chars().take(77).collect();
28 out.push('…');
29 out
30 }
31}
32
33pub(crate) const ENV_VIEWPORT: u16 = 1 << 0;
34pub(crate) const ENV_SCALE: u16 = 1 << 1;
35pub(crate) const ENV_BRIGHTNESS: u16 = 1 << 2;
36pub(crate) const ENV_TEXT_SCALE: u16 = 1 << 3;
37pub(crate) const ENV_SAFE_INSETS: u16 = 1 << 4;
38pub(crate) const ENV_VIEW_PADDING: u16 = 1 << 5;
39pub(crate) const ENV_VIEW_INSETS: u16 = 1 << 6;
40pub(crate) const ENV_LOCALE: u16 = 1 << 7;
41pub(crate) const ENV_DIRECTION: u16 = 1 << 8;
42pub(crate) const ENV_REDUCED_MOTION: u16 = 1 << 9;
43pub(crate) const ENV_INPUT: u16 = 1 << 10;
44pub(crate) const ENV_WINDOW_FOCUS: u16 = 1 << 11;
45pub(crate) const ENV_HIGH_CONTRAST: u16 = 1 << 12;
46pub(crate) const ENV_WINDOW_OCCLUSION: u16 = 1 << 13;
47pub(crate) const ENV_ALL: u16 = (1 << 14) - 1;
48
49pub(crate) fn environment_change_mask(
50 previous: &RuntimeEnvironment,
51 next: &RuntimeEnvironment,
52) -> u16 {
53 let mut mask = 0;
54 if previous.viewport != next.viewport
55 || previous.physical_width != next.physical_width
56 || previous.physical_height != next.physical_height
57 {
58 mask |= ENV_VIEWPORT;
59 }
60 if previous.scale_factor != next.scale_factor {
61 mask |= ENV_SCALE;
62 }
63 if previous.brightness != next.brightness {
64 mask |= ENV_BRIGHTNESS;
65 }
66 if previous.text_scale != next.text_scale {
67 mask |= ENV_TEXT_SCALE;
68 }
69 if previous.safe_insets != next.safe_insets {
70 mask |= ENV_SAFE_INSETS;
71 }
72 if previous.view_padding != next.view_padding {
73 mask |= ENV_VIEW_PADDING;
74 }
75 if previous.view_insets != next.view_insets {
76 mask |= ENV_VIEW_INSETS;
77 }
78 if previous.locales != next.locales {
79 mask |= ENV_LOCALE;
80 }
81 if previous.text_direction != next.text_direction {
82 mask |= ENV_DIRECTION;
83 }
84 if previous.reduced_motion != next.reduced_motion {
85 mask |= ENV_REDUCED_MOTION;
86 }
87 if previous.input != next.input {
88 mask |= ENV_INPUT;
89 }
90 if previous.window_focused != next.window_focused {
91 mask |= ENV_WINDOW_FOCUS;
92 }
93 if previous.high_contrast != next.high_contrast {
94 mask |= ENV_HIGH_CONTRAST;
95 }
96 if previous.window_occluded != next.window_occluded {
97 mask |= ENV_WINDOW_OCCLUSION;
98 }
99 mask
100}
101
102pub(crate) type ReactiveRootId = u64;
103pub(crate) type ReactiveNodeId = u64;
104pub(crate) struct TrackedDependency {
105 pub(crate) source_id: u64,
106 pub(crate) source: incular_core::reactivity::DependencySource,
107 pub(crate) dependency: Weak<dyn Dependency>,
108 pub(crate) _subscription: incular_core::reactivity::Subscription,
109}
110
111pub(crate) static NEXT_REACTIVE_ROOT: AtomicU64 = AtomicU64::new(1);
112pub(crate) static NEXT_REACTIVE_NODE: AtomicU64 = AtomicU64::new(1);
113
114pub(crate) trait ReactiveNode {
115 fn id(&self) -> ReactiveNodeId;
116 fn mark_dirty(&self);
117 fn run(&self, root: ReactiveRootId);
118 fn record_dependency(&self, dependency: TrackedDependency);
119 fn dispose(&self, root: ReactiveRootId);
120 fn owns_element(&self, _root: ReactiveRootId, _element: ElementId) -> bool {
121 false
122 }
123}
124
125pub(crate) trait Dependency {
126 fn skips_current_consumer(&self) -> bool {
127 false
128 }
129 fn source(&self) -> &incular_core::reactivity::DependencySource;
130 fn prepare_subscription(&self, _queue: &Rc<RefCell<ReactiveQueue>>) {}
131 fn is_signal(&self) -> bool {
132 false
133 }
134 #[cfg(feature = "devtools")]
135 fn cause(&self) -> Option<InvalidationCause> {
136 None
137 }
138}
139pub(crate) struct ReactiveQueue {
140 pub(crate) root: ReactiveRootId,
141 pub(crate) queued: HashSet<ElementId>,
142 pub(crate) order: VecDeque<ElementId>,
143 pub(crate) queued_nodes: HashSet<ReactiveNodeId>,
144 pub(crate) node_order: VecDeque<ReactiveNodeId>,
145 pub(crate) nodes: HashMap<ReactiveNodeId, Weak<dyn ReactiveNode>>,
146 pub(crate) dependencies: HashMap<ElementId, Vec<TrackedDependency>>,
147 pub(crate) focus_scopes: HashMap<ElementId, Vec<FocusScopeSubscription>>,
152 #[cfg(feature = "devtools")]
153 pub(crate) causes: HashMap<ElementId, Vec<InvalidationCause>>,
154}
155impl Drop for ReactiveQueue {
156 fn drop(&mut self) {
157 self.clear();
158 }
159}
160
161impl ReactiveQueue {
162 pub(crate) fn new() -> Rc<RefCell<Self>> {
163 Rc::new(RefCell::new(Self {
164 root: NEXT_REACTIVE_ROOT
165 .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |id| id.checked_add(1))
166 .expect("reactive root identity space exhausted"),
167 queued: HashSet::new(),
168 order: VecDeque::new(),
169 queued_nodes: HashSet::new(),
170 node_order: VecDeque::new(),
171 nodes: HashMap::new(),
172 dependencies: HashMap::new(),
173 focus_scopes: HashMap::new(),
174 #[cfg(feature = "devtools")]
175 causes: HashMap::new(),
176 }))
177 }
178
179 pub(crate) fn enqueue(&mut self, id: ElementId) -> bool {
180 if self.queued.insert(id) {
181 self.order.push_back(id);
182 true
183 } else {
184 false
185 }
186 }
187 pub(crate) fn enqueue_node(
188 &mut self,
189 id: ReactiveNodeId,
190 node: Weak<dyn ReactiveNode>,
191 ) -> bool {
192 self.register_node(id, node);
193 if self.queued_nodes.insert(id) {
194 self.node_order.push_back(id);
195 true
196 } else {
197 false
198 }
199 }
200 pub(crate) fn register_node(&mut self, id: ReactiveNodeId, node: Weak<dyn ReactiveNode>) {
201 self.nodes.insert(id, node);
202 }
203 #[cfg(feature = "devtools")]
204 pub(crate) fn note_cause(&mut self, id: ElementId, cause: InvalidationCause) {
205 const MAX_CAUSES: usize = 8;
206 let causes = self.causes.entry(id).or_default();
207 if causes.len() == MAX_CAUSES {
208 causes.remove(0);
209 }
210 causes.push(cause);
211 }
212 #[cfg(feature = "devtools")]
213 pub(crate) fn take_causes(&mut self, id: ElementId) -> Vec<InvalidationCause> {
214 self.causes.remove(&id).unwrap_or_default()
215 }
216 pub(crate) fn take(&mut self) -> Option<ElementId> {
217 let id = self.order.pop_front()?;
218 self.queued.remove(&id);
219 Some(id)
220 }
221 pub(crate) fn take_node(&mut self) -> Option<(ReactiveRootId, Weak<dyn ReactiveNode>)> {
222 let id = self.node_order.pop_front()?;
223 self.queued_nodes.remove(&id);
224 self.nodes.get(&id).cloned().map(|node| (self.root, node))
225 }
226 pub(crate) fn has_work(&self) -> bool {
227 !self.queued.is_empty() || !self.queued_nodes.is_empty()
228 }
229 pub(crate) fn refresh(&mut self, id: ElementId) {
230 self.dependencies.remove(&id);
231 self.focus_scopes.remove(&id);
232 }
233 pub(crate) fn record(&mut self, id: ElementId, dep: TrackedDependency) {
234 let entries = self.dependencies.entry(id).or_default();
235 if !entries
236 .iter()
237 .any(|current| current.source_id == dep.source_id)
238 {
239 entries.push(dep);
240 }
241 }
242
243 pub(crate) fn watch_focus_scope(
244 &mut self,
245 id: ElementId,
246 subscription: FocusScopeSubscription,
247 ) {
248 self.focus_scopes.entry(id).or_default().push(subscription);
249 }
250 pub(crate) fn forget(&mut self, id: ElementId) {
251 self.refresh(id);
252 let owned = self
253 .nodes
254 .iter()
255 .filter_map(|(node_id, node)| {
256 node.upgrade()
257 .filter(|node| node.owns_element(self.root, id))
258 .map(|node| (*node_id, node))
259 })
260 .collect::<Vec<_>>();
261 for (node_id, node) in owned {
262 node.dispose(self.root);
263 self.nodes.remove(&node_id);
264 self.queued_nodes.remove(&node_id);
265 self.node_order.retain(|queued| *queued != node_id);
266 }
267 self.queued.remove(&id);
268 #[cfg(feature = "devtools")]
269 self.causes.remove(&id);
270 }
271
272 pub(crate) fn clear(&mut self) {
273 let mut ids: HashSet<_> = self.dependencies.keys().copied().collect();
276 ids.extend(self.focus_scopes.keys().copied());
277 let ids: Vec<_> = ids.into_iter().collect();
278 for id in ids {
279 self.refresh(id);
280 }
281 self.queued.clear();
282 self.order.clear();
283 let nodes: Vec<_> = self.nodes.values().filter_map(Weak::upgrade).collect();
284 self.queued_nodes.clear();
285 self.node_order.clear();
286 self.nodes.clear();
287 for node in nodes {
288 node.dispose(self.root);
289 }
290 #[cfg(feature = "devtools")]
291 self.causes.clear();
292 }
293}
294pub(crate) fn install_focus_scope_watch(
295 queue: &Rc<RefCell<ReactiveQueue>>,
296 element: ElementId,
297 scope: &FocusScopeNode,
298) {
299 let weak_queue = Rc::downgrade(queue);
300 let subscription = scope.observe(move || {
301 if let Some(queue) = weak_queue.upgrade() {
302 let mut queue = queue.borrow_mut();
303 queue.enqueue(element);
304 #[cfg(feature = "devtools")]
305 queue.note_cause(element, InvalidationCause::Manual);
306 }
307 });
308 queue.borrow_mut().watch_focus_scope(element, subscription);
309}
310#[derive(Default)]
311pub(crate) struct InitialBuildDependencies {
312 pub(crate) signals: Vec<Rc<dyn Dependency>>,
313 pub(crate) focus_scopes: Vec<FocusScopeNode>,
314}
315impl InitialBuildDependencies {
316 pub(crate) fn record_signal(&mut self, dependency: Rc<dyn Dependency>) {
317 if !self
318 .signals
319 .iter()
320 .any(|current| current.source().id() == dependency.source().id())
321 {
322 self.signals.push(dependency);
323 }
324 }
325}
326pub(crate) struct BuildScope {
327 pub(crate) root: ReactiveRootId,
328 pub(crate) element: Option<ElementId>,
329 pub(crate) queue: Weak<RefCell<ReactiveQueue>>,
330 pub(crate) initial_dependencies: Option<Rc<RefCell<InitialBuildDependencies>>>,
331 pub(crate) spawner: tasks::RuntimeSpawner,
332 pub(crate) owner_scope: tasks::TaskScope,
333}
334thread_local! { pub(crate) static BUILD_SCOPE: RefCell<Option<BuildScope>> = const { RefCell::new(None) }; }
335
336pub(crate) struct BuildScopeGuard {
338 previous: Option<BuildScope>,
339 _tracking: incular_core::reactivity::TrackingGuard,
340}
341
342impl BuildScopeGuard {
343 pub(crate) fn enter(next: BuildScope) -> Self {
344 let previous = BUILD_SCOPE.with(|scope| scope.replace(Some(next)));
345 Self {
346 previous,
347 _tracking: incular_core::reactivity::TrackingGuard::enter(reactive::track_source),
348 }
349 }
350}
351
352impl Drop for BuildScopeGuard {
353 fn drop(&mut self) {
354 BUILD_SCOPE.with(|scope| {
355 scope.replace(self.previous.take());
356 });
357 }
358}
359
360fn assert_reactive_mutation_allowed() {
361 incular_core::reactivity::assert_mutation_allowed();
362 BUILD_SCOPE.with(|scope| {
363 let current = scope.borrow();
364 let Some(scope) = current.as_ref() else {
365 return;
366 };
367 let owner = scope.element.map_or_else(
368 || "the application root".to_owned(),
369 |id| format!("element {id:?}"),
370 );
371 panic!(
372 "Incular reactive state was mutated while building {owner}; move the mutation to an event callback, Effect, or Action"
373 );
374 });
375}
376
377#[cfg(feature = "devtools")]
378thread_local! { pub(crate) static DEV_TASK_COMPLETION: Cell<bool> = const { Cell::new(false) }; }
379struct SignalInner<T> {
380 value: incular_core::reactivity::ReactiveCell<T>,
381 dev_write_count: std::cell::Cell<u64>,
383 #[cfg(feature = "devtools")]
384 dev_signal_id: std::cell::Cell<Option<u64>>,
385 #[cfg(feature = "devtools")]
386 dev_name: RefCell<Option<String>>,
387 #[cfg(feature = "devtools")]
388 dev_editable_kind: std::cell::Cell<Option<EditableSignalKind>>,
389 #[cfg(feature = "devtools")]
390 dev_last_write: RefCell<(Option<String>, Option<String>)>,
391 #[cfg(feature = "devtools")]
392 dev_summarize: RefCell<Option<DevSummarizer<T>>>,
393}
394impl<T> Drop for SignalInner<T> {
395 fn drop(&mut self) {
396 #[cfg(feature = "devtools")]
397 if let Some(id) = self.dev_signal_id.get() {
398 devtools_registry::unregister(id);
399 }
400 }
401}
402impl<T> Dependency for SignalInner<T> {
403 fn source(&self) -> &incular_core::reactivity::DependencySource {
404 self.value.source()
405 }
406 fn is_signal(&self) -> bool {
407 true
408 }
409 #[cfg(feature = "devtools")]
410 fn cause(&self) -> Option<InvalidationCause> {
411 self.dev_signal_id.get().map(|id| {
412 let (old, new) = self.dev_last_write.borrow().clone();
413 InvalidationCause::Signal {
414 id,
415 name: self.dev_name.borrow().clone(),
416 old,
417 new,
418 }
419 })
420 }
421}
422#[derive(Clone)]
424pub struct Signal<T> {
425 inner: Rc<SignalInner<T>>,
426}
427#[cfg(feature = "devtools")]
428type DevSummarizer<T> = Box<dyn Fn(&T) -> String>;
429
430#[cfg(feature = "devtools")]
431fn editable_signal_value<T: 'static>(
432 value: &incular_devtools_protocol::EditableValue,
433) -> Option<T> {
434 fn cast<T: 'static, U: Any>(value: U) -> Option<T> {
435 let value: Box<dyn Any> = Box::new(value);
436 value.downcast::<T>().ok().map(|value| *value)
437 }
438
439 let target = TypeId::of::<T>();
440 match value {
441 incular_devtools_protocol::EditableValue::Bool(value) if target == TypeId::of::<bool>() => {
442 cast(*value)
443 }
444 incular_devtools_protocol::EditableValue::Int(value) if target == TypeId::of::<i64>() => {
445 cast(*value)
446 }
447 incular_devtools_protocol::EditableValue::Uint(value) if target == TypeId::of::<u64>() => {
448 cast(*value)
449 }
450 incular_devtools_protocol::EditableValue::Float(value) if target == TypeId::of::<f64>() => {
451 cast(*value)
452 }
453 incular_devtools_protocol::EditableValue::Str(value)
454 if target == TypeId::of::<String>() =>
455 {
456 cast(value.clone())
457 }
458 _ => None,
459 }
460}
461
462impl<T: 'static> Signal<T> {
463 #[must_use]
467 pub fn new(value: T) -> Self {
468 Self {
469 inner: Rc::new(SignalInner {
470 value: incular_core::reactivity::ReactiveCell::new(value),
471 dev_write_count: std::cell::Cell::new(0),
472 #[cfg(feature = "devtools")]
473 dev_signal_id: std::cell::Cell::new(None),
474 #[cfg(feature = "devtools")]
475 dev_name: RefCell::new(None),
476 #[cfg(feature = "devtools")]
477 dev_editable_kind: std::cell::Cell::new(None),
478 #[cfg(feature = "devtools")]
479 dev_last_write: RefCell::new((None, None)),
480 #[cfg(feature = "devtools")]
481 dev_summarize: RefCell::new(None),
482 }),
483 }
484 }
485
486 #[cfg(feature = "devtools")]
489 pub fn devtools(self, name: &str) -> Self
490 where
491 T: std::fmt::Debug + 'static,
492 {
493 use std::sync::atomic::{AtomicU64, Ordering};
494 static NEXT_ID: AtomicU64 = AtomicU64::new(1);
495 let id = self.inner.dev_signal_id.get().unwrap_or_else(|| {
496 NEXT_ID
497 .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |id| id.checked_add(1))
498 .expect("DevTools signal identity space exhausted")
499 });
500 self.inner.dev_signal_id.set(Some(id));
501 *self.inner.dev_name.borrow_mut() = Some(name.to_owned());
502 *self.inner.dev_summarize.borrow_mut() = Some(Box::new(|value: &T| truncate_debug(value)));
503
504 let weak_value = Rc::downgrade(&self.inner);
505 let weak_value_count = weak_value.clone();
506 let weak_value_subscribers = weak_value.clone();
507 let weak_value_subscriber_entries = weak_value.clone();
508 let registration = crate::devtools_registry::SignalRegistration {
509 name: Some(name.to_owned()),
510 type_name: std::any::type_name::<T>(),
511 editable_kind: None,
512 write_count: Rc::new(move || {
513 weak_value_count
514 .upgrade()
515 .map(|inner| inner.dev_write_count.get())
516 .unwrap_or_default()
517 }),
518 subscriber_count: Rc::new(move || {
519 weak_value_subscribers
520 .upgrade()
521 .map(|inner| {
522 inner
523 .source()
524 .subscribers::<(ReactiveRootId, ElementId)>()
525 .len()
526 })
527 .unwrap_or_default()
528 }),
529 subscribers: Rc::new(move || {
530 weak_value_subscriber_entries
531 .upgrade()
532 .map(|inner| inner.source().subscribers::<(ReactiveRootId, ElementId)>())
533 .unwrap_or_default()
534 }),
535 last_write: Rc::new(move || {
536 weak_value.upgrade().map_or((None, None), |inner| {
537 let guard = inner.dev_last_write.borrow();
538 (guard.0.clone(), guard.1.clone())
539 })
540 }),
541 apply_edit: Rc::new(move |_| false),
542 };
543 crate::devtools_registry::register(id, registration);
544 self
545 }
546
547 #[cfg(feature = "devtools")]
550 pub fn devtools_editable(self) -> Self
551 where
552 T: std::fmt::Debug + PartialEq + 'static,
553 {
554 let kind = if std::any::TypeId::of::<T>() == std::any::TypeId::of::<bool>() {
555 Some(EditableSignalKind::Bool)
556 } else if std::any::TypeId::of::<T>() == std::any::TypeId::of::<i64>() {
557 Some(EditableSignalKind::Int)
558 } else if std::any::TypeId::of::<T>() == std::any::TypeId::of::<u64>() {
559 Some(EditableSignalKind::Uint)
560 } else if std::any::TypeId::of::<T>() == std::any::TypeId::of::<f64>() {
561 Some(EditableSignalKind::Float)
562 } else if std::any::TypeId::of::<T>() == std::any::TypeId::of::<String>() {
563 Some(EditableSignalKind::Str)
564 } else {
565 None
566 };
567 self.inner.dev_editable_kind.set(kind);
568 let weak_value = Rc::downgrade(&self.inner);
569 crate::devtools_registry::set_editable(
573 self.inner.dev_signal_id.get().unwrap_or_default(),
574 kind,
575 Rc::new(move |value| {
576 let Some(value) = editable_signal_value::<T>(value) else {
577 return false;
578 };
579 let Some(inner) = weak_value.upgrade() else {
580 return false;
581 };
582 Signal { inner }.set(value)
583 }),
584 );
585 self
586 }
587
588 pub fn get(&self) -> T
589 where
590 T: Clone,
591 {
592 scheduler_counters::SIGNAL_READS.fetch_add(1, Ordering::Relaxed);
593 reactive::track_dependency(self.inner.clone());
594 self.inner.value.borrow().clone()
595 }
596 pub fn set(&self, value: T) -> bool
602 where
603 T: PartialEq,
604 {
605 assert_reactive_mutation_allowed();
606 if *self.inner.value.borrow() == value {
607 return false;
608 }
609 #[cfg(feature = "devtools")]
610 let old_summary = self
611 .inner
612 .dev_summarize
613 .borrow()
614 .as_ref()
615 .map(|format| format(&self.inner.value.borrow()));
616 #[cfg(feature = "devtools")]
617 let new_summary = self
618 .inner
619 .dev_summarize
620 .borrow()
621 .as_ref()
622 .map(|format| format(&value));
623 *self.inner.value.borrow_mut() = value;
624 #[cfg(feature = "devtools")]
625 if old_summary.is_some() || new_summary.is_some() {
626 *self.inner.dev_last_write.borrow_mut() = (old_summary, new_summary);
627 }
628 self.enqueue_dependents();
629 true
630 }
631 pub fn dependent_count(&self) -> usize {
632 self.inner.source().subscriber_count()
633 }
634 pub fn get_untracked(&self) -> T
636 where
637 T: Clone,
638 {
639 self.inner.value.borrow().clone()
640 }
641 pub fn update(&self, update: impl FnOnce(&mut T)) {
645 assert_reactive_mutation_allowed();
646 #[cfg(feature = "devtools")]
647 let old_summary = self
648 .inner
649 .dev_summarize
650 .borrow()
651 .as_ref()
652 .map(|format| format(&self.inner.value.borrow()));
653 update(&mut self.inner.value.borrow_mut());
654 #[cfg(feature = "devtools")]
655 {
656 let new_summary = self
657 .inner
658 .dev_summarize
659 .borrow()
660 .as_ref()
661 .map(|format| format(&self.inner.value.borrow()));
662 if old_summary.is_some() || new_summary.is_some() {
663 *self.inner.dev_last_write.borrow_mut() = (old_summary, new_summary);
664 }
665 }
666 self.enqueue_dependents();
667 }
668
669 pub fn update_if_changed(&self, update: impl FnOnce(&mut T)) -> bool
673 where
674 T: Clone + PartialEq,
675 {
676 assert_reactive_mutation_allowed();
677 let previous = self.inner.value.borrow().clone();
678 #[cfg(feature = "devtools")]
679 let old_summary = self
680 .inner
681 .dev_summarize
682 .borrow()
683 .as_ref()
684 .map(|format| format(&previous));
685 update(&mut self.inner.value.borrow_mut());
686 if *self.inner.value.borrow() == previous {
687 return false;
688 }
689 #[cfg(feature = "devtools")]
690 {
691 let new_summary = self
692 .inner
693 .dev_summarize
694 .borrow()
695 .as_ref()
696 .map(|format| format(&self.inner.value.borrow()));
697 if old_summary.is_some() || new_summary.is_some() {
698 *self.inner.dev_last_write.borrow_mut() = (old_summary, new_summary);
699 }
700 }
701 self.enqueue_dependents();
702 true
703 }
704
705 fn enqueue_dependents(&self) {
706 self.inner
707 .dev_write_count
708 .set(self.inner.dev_write_count.get() + 1);
709 scheduler_counters::SIGNAL_WRITES.fetch_add(1, Ordering::Relaxed);
710 self.inner.source().notify();
711 }
712}
713
714#[cfg(feature = "devtools")]
715pub mod devtools_registry {
716 pub use super::SignalRegistration;
717 use std::{cell::RefCell, collections::BTreeMap, rc::Rc};
718 std::thread_local! {
719 static SIGNALS: RefCell<BTreeMap<u64, SignalRegistration>> = const { RefCell::new(BTreeMap::new()) };
720 }
721 pub fn register(id: u64, mut registration: SignalRegistration) {
722 SIGNALS.with(|signals| {
723 let mut signals = signals.borrow_mut();
724 if let Some(previous) = signals.get(&id) {
725 registration.editable_kind = previous.editable_kind;
726 registration.apply_edit = previous.apply_edit.clone();
727 }
728 signals.insert(id, registration);
729 });
730 }
731 pub(super) fn unregister(id: u64) {
732 let _ = SIGNALS.try_with(|signals| {
733 signals.borrow_mut().remove(&id);
734 });
735 }
736 pub fn set_editable(
737 id: u64,
738 kind: Option<super::EditableSignalKind>,
739 apply_edit: Rc<dyn Fn(&incular_devtools_protocol::EditableValue) -> bool>,
740 ) {
741 SIGNALS.with(|signals| {
742 if let Some(registration) = signals.borrow_mut().get_mut(&id) {
743 registration.editable_kind = kind;
744 registration.apply_edit = apply_edit;
745 }
746 });
747 }
748 pub fn with_all<R>(visit: impl FnOnce(&[(u64, SignalRegistration)]) -> R) -> R {
751 let snapshot = SIGNALS.with(|signals| {
752 signals
753 .borrow()
754 .iter()
755 .map(|(id, row)| (*id, row.clone()))
756 .collect::<Vec<_>>()
757 });
758 visit(&snapshot)
759 }
760}
761
762#[derive(Clone, Copy, Debug, PartialEq, Eq)]
763pub enum EditableSignalKind {
764 Bool,
765 Int,
766 Uint,
767 Float,
768 Str,
769}
770
771#[cfg_attr(not(feature = "devtools"), allow(dead_code))]
772#[derive(Clone)]
773pub struct SignalRegistration {
774 pub name: Option<String>,
775 pub type_name: &'static str,
776 pub editable_kind: Option<EditableSignalKind>,
777 pub write_count: Rc<dyn Fn() -> u64>,
778 pub subscriber_count: Rc<dyn Fn() -> usize>,
779 pub subscribers: Rc<dyn Fn() -> Vec<(ReactiveRootId, ElementId)>>,
780 pub last_write: Rc<dyn Fn() -> (Option<String>, Option<String>)>,
781 #[cfg(feature = "devtools")]
782 pub apply_edit: Rc<dyn Fn(&incular_devtools_protocol::EditableValue) -> bool>,
783}