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