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
390fn assert_reactive_mutation_allowed() {
391 incular_core::reactivity::assert_mutation_allowed();
392 BUILD_SCOPE.with(|scope| {
393 let current = scope.borrow();
394 let Some(scope) = current.as_ref() else {
395 return;
396 };
397 let owner = scope.element.map_or_else(
398 || "the application root".to_owned(),
399 |id| format!("element {id:?}"),
400 );
401 panic!(
402 "Incular reactive state was mutated while building {owner}; move the mutation to an event callback, Effect, or Action"
403 );
404 });
405}
406
407#[cfg(feature = "devtools")]
408thread_local! { pub(crate) static DEV_TASK_COMPLETION: Cell<bool> = const { Cell::new(false) }; }
409struct SignalInner<T> {
410 value: incular_core::reactivity::ReactiveCell<T>,
411 dev_write_count: std::cell::Cell<u64>,
413 #[cfg(feature = "devtools")]
414 dev_signal_id: std::cell::Cell<Option<u64>>,
415 #[cfg(feature = "devtools")]
416 dev_name: RefCell<Option<String>>,
417 #[cfg(feature = "devtools")]
418 dev_editable_kind: std::cell::Cell<Option<EditableSignalKind>>,
419 #[cfg(feature = "devtools")]
420 dev_last_write: RefCell<(Option<String>, Option<String>)>,
421 #[cfg(feature = "devtools")]
422 dev_summarize: RefCell<Option<DevSummarizer<T>>>,
423}
424impl<T> Drop for SignalInner<T> {
425 fn drop(&mut self) {
426 #[cfg(feature = "devtools")]
427 if let Some(id) = self.dev_signal_id.get() {
428 devtools_registry::unregister(id);
429 }
430 }
431}
432impl<T> Dependency for SignalInner<T> {
433 fn source(&self) -> &incular_core::reactivity::DependencySource {
434 self.value.source()
435 }
436 fn is_signal(&self) -> bool {
437 true
438 }
439 #[cfg(feature = "devtools")]
440 fn cause(&self) -> Option<InvalidationCause> {
441 self.dev_signal_id.get().map(|id| {
442 let (old, new) = self.dev_last_write.borrow().clone();
443 InvalidationCause::Signal {
444 id,
445 name: self.dev_name.borrow().clone(),
446 old,
447 new,
448 }
449 })
450 }
451}
452#[derive(Clone)]
454pub struct Signal<T> {
455 inner: Rc<SignalInner<T>>,
456}
457#[cfg(feature = "devtools")]
458type DevSummarizer<T> = Box<dyn Fn(&T) -> String>;
459
460#[cfg(feature = "devtools")]
461fn editable_signal_value<T: 'static>(
462 value: &incular_devtools_protocol::EditableValue,
463) -> Option<T> {
464 fn cast<T: 'static, U: Any>(value: U) -> Option<T> {
465 let value: Box<dyn Any> = Box::new(value);
466 value.downcast::<T>().ok().map(|value| *value)
467 }
468
469 let target = TypeId::of::<T>();
470 match value {
471 incular_devtools_protocol::EditableValue::Bool(value) if target == TypeId::of::<bool>() => {
472 cast(*value)
473 }
474 incular_devtools_protocol::EditableValue::Int(value) if target == TypeId::of::<i64>() => {
475 cast(*value)
476 }
477 incular_devtools_protocol::EditableValue::Uint(value) if target == TypeId::of::<u64>() => {
478 cast(*value)
479 }
480 incular_devtools_protocol::EditableValue::Float(value) if target == TypeId::of::<f64>() => {
481 cast(*value)
482 }
483 incular_devtools_protocol::EditableValue::Str(value)
484 if target == TypeId::of::<String>() =>
485 {
486 cast(value.clone())
487 }
488 _ => None,
489 }
490}
491
492impl<T: 'static> Signal<T> {
493 #[must_use]
497 pub fn new(value: T) -> Self {
498 Self {
499 inner: Rc::new(SignalInner {
500 value: incular_core::reactivity::ReactiveCell::new(value),
501 dev_write_count: std::cell::Cell::new(0),
502 #[cfg(feature = "devtools")]
503 dev_signal_id: std::cell::Cell::new(None),
504 #[cfg(feature = "devtools")]
505 dev_name: RefCell::new(None),
506 #[cfg(feature = "devtools")]
507 dev_editable_kind: std::cell::Cell::new(None),
508 #[cfg(feature = "devtools")]
509 dev_last_write: RefCell::new((None, None)),
510 #[cfg(feature = "devtools")]
511 dev_summarize: RefCell::new(None),
512 }),
513 }
514 }
515
516 #[cfg(feature = "devtools")]
519 pub fn devtools(self, name: &str) -> Self
520 where
521 T: std::fmt::Debug + 'static,
522 {
523 use std::sync::atomic::{AtomicU64, Ordering};
524 static NEXT_ID: AtomicU64 = AtomicU64::new(1);
525 let id = self.inner.dev_signal_id.get().unwrap_or_else(|| {
526 NEXT_ID
527 .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |id| id.checked_add(1))
528 .expect("DevTools signal identity space exhausted")
529 });
530 self.inner.dev_signal_id.set(Some(id));
531 *self.inner.dev_name.borrow_mut() = Some(name.to_owned());
532 *self.inner.dev_summarize.borrow_mut() = Some(Box::new(|value: &T| truncate_debug(value)));
533
534 let weak_value = Rc::downgrade(&self.inner);
535 let weak_value_count = weak_value.clone();
536 let weak_value_subscribers = weak_value.clone();
537 let weak_value_subscriber_entries = weak_value.clone();
538 let registration = crate::devtools_registry::SignalRegistration {
539 name: Some(name.to_owned()),
540 type_name: std::any::type_name::<T>(),
541 editable_kind: None,
542 write_count: Rc::new(move || {
543 weak_value_count
544 .upgrade()
545 .map(|inner| inner.dev_write_count.get())
546 .unwrap_or_default()
547 }),
548 subscriber_count: Rc::new(move || {
549 weak_value_subscribers
550 .upgrade()
551 .map(|inner| {
552 inner
553 .source()
554 .subscribers::<(ReactiveRootId, ElementId)>()
555 .len()
556 })
557 .unwrap_or_default()
558 }),
559 subscribers: Rc::new(move || {
560 weak_value_subscriber_entries
561 .upgrade()
562 .map(|inner| inner.source().subscribers::<(ReactiveRootId, ElementId)>())
563 .unwrap_or_default()
564 }),
565 last_write: Rc::new(move || {
566 weak_value.upgrade().map_or((None, None), |inner| {
567 let guard = inner.dev_last_write.borrow();
568 (guard.0.clone(), guard.1.clone())
569 })
570 }),
571 apply_edit: Rc::new(move |_| false),
572 };
573 crate::devtools_registry::register(id, registration);
574 self
575 }
576
577 #[cfg(feature = "devtools")]
580 pub fn devtools_editable(self) -> Self
581 where
582 T: std::fmt::Debug + PartialEq + 'static,
583 {
584 let kind = if std::any::TypeId::of::<T>() == std::any::TypeId::of::<bool>() {
585 Some(EditableSignalKind::Bool)
586 } else if std::any::TypeId::of::<T>() == std::any::TypeId::of::<i64>() {
587 Some(EditableSignalKind::Int)
588 } else if std::any::TypeId::of::<T>() == std::any::TypeId::of::<u64>() {
589 Some(EditableSignalKind::Uint)
590 } else if std::any::TypeId::of::<T>() == std::any::TypeId::of::<f64>() {
591 Some(EditableSignalKind::Float)
592 } else if std::any::TypeId::of::<T>() == std::any::TypeId::of::<String>() {
593 Some(EditableSignalKind::Str)
594 } else {
595 None
596 };
597 self.inner.dev_editable_kind.set(kind);
598 let weak_value = Rc::downgrade(&self.inner);
599 crate::devtools_registry::set_editable(
603 self.inner.dev_signal_id.get().unwrap_or_default(),
604 kind,
605 Rc::new(move |value| {
606 let Some(value) = editable_signal_value::<T>(value) else {
607 return false;
608 };
609 let Some(inner) = weak_value.upgrade() else {
610 return false;
611 };
612 Signal { inner }.set(value)
613 }),
614 );
615 self
616 }
617
618 pub fn get(&self) -> T
619 where
620 T: Clone,
621 {
622 scheduler_counters::SIGNAL_READS.fetch_add(1, Ordering::Relaxed);
623 reactive::track_dependency(self.inner.clone());
624 self.inner.value.borrow().clone()
625 }
626 pub fn set(&self, value: T) -> bool
632 where
633 T: PartialEq,
634 {
635 assert_reactive_mutation_allowed();
636 if *self.inner.value.borrow() == value {
637 return false;
638 }
639 #[cfg(feature = "devtools")]
640 let old_summary = self
641 .inner
642 .dev_summarize
643 .borrow()
644 .as_ref()
645 .map(|format| format(&self.inner.value.borrow()));
646 #[cfg(feature = "devtools")]
647 let new_summary = self
648 .inner
649 .dev_summarize
650 .borrow()
651 .as_ref()
652 .map(|format| format(&value));
653 *self.inner.value.borrow_mut() = value;
654 #[cfg(feature = "devtools")]
655 if old_summary.is_some() || new_summary.is_some() {
656 *self.inner.dev_last_write.borrow_mut() = (old_summary, new_summary);
657 }
658 self.enqueue_dependents();
659 true
660 }
661 pub fn dependent_count(&self) -> usize {
662 self.inner.source().subscriber_count()
663 }
664 pub fn get_untracked(&self) -> T
666 where
667 T: Clone,
668 {
669 self.inner.value.borrow().clone()
670 }
671 pub fn update(&self, update: impl FnOnce(&mut T)) {
675 assert_reactive_mutation_allowed();
676 #[cfg(feature = "devtools")]
677 let old_summary = self
678 .inner
679 .dev_summarize
680 .borrow()
681 .as_ref()
682 .map(|format| format(&self.inner.value.borrow()));
683 update(&mut self.inner.value.borrow_mut());
684 #[cfg(feature = "devtools")]
685 {
686 let new_summary = self
687 .inner
688 .dev_summarize
689 .borrow()
690 .as_ref()
691 .map(|format| format(&self.inner.value.borrow()));
692 if old_summary.is_some() || new_summary.is_some() {
693 *self.inner.dev_last_write.borrow_mut() = (old_summary, new_summary);
694 }
695 }
696 self.enqueue_dependents();
697 }
698
699 pub fn update_if_changed(&self, update: impl FnOnce(&mut T)) -> bool
703 where
704 T: Clone + PartialEq,
705 {
706 assert_reactive_mutation_allowed();
707 let previous = self.inner.value.borrow().clone();
708 #[cfg(feature = "devtools")]
709 let old_summary = self
710 .inner
711 .dev_summarize
712 .borrow()
713 .as_ref()
714 .map(|format| format(&previous));
715 update(&mut self.inner.value.borrow_mut());
716 if *self.inner.value.borrow() == previous {
717 return false;
718 }
719 #[cfg(feature = "devtools")]
720 {
721 let new_summary = self
722 .inner
723 .dev_summarize
724 .borrow()
725 .as_ref()
726 .map(|format| format(&self.inner.value.borrow()));
727 if old_summary.is_some() || new_summary.is_some() {
728 *self.inner.dev_last_write.borrow_mut() = (old_summary, new_summary);
729 }
730 }
731 self.enqueue_dependents();
732 true
733 }
734
735 fn enqueue_dependents(&self) {
736 self.inner
737 .dev_write_count
738 .set(self.inner.dev_write_count.get() + 1);
739 scheduler_counters::SIGNAL_WRITES.fetch_add(1, Ordering::Relaxed);
740 self.inner.source().notify();
741 }
742}
743
744#[cfg(feature = "devtools")]
745pub mod devtools_registry {
746 pub use super::SignalRegistration;
747 use std::{cell::RefCell, collections::BTreeMap, rc::Rc};
748 std::thread_local! {
749 static SIGNALS: RefCell<BTreeMap<u64, SignalRegistration>> = const { RefCell::new(BTreeMap::new()) };
750 }
751 pub fn register(id: u64, mut registration: SignalRegistration) {
752 SIGNALS.with(|signals| {
753 let mut signals = signals.borrow_mut();
754 if let Some(previous) = signals.get(&id) {
755 registration.editable_kind = previous.editable_kind;
756 registration.apply_edit = previous.apply_edit.clone();
757 }
758 signals.insert(id, registration);
759 });
760 }
761 pub(super) fn unregister(id: u64) {
762 let _ = SIGNALS.try_with(|signals| {
763 signals.borrow_mut().remove(&id);
764 });
765 }
766 pub fn set_editable(
767 id: u64,
768 kind: Option<super::EditableSignalKind>,
769 apply_edit: Rc<dyn Fn(&incular_devtools_protocol::EditableValue) -> bool>,
770 ) {
771 SIGNALS.with(|signals| {
772 if let Some(registration) = signals.borrow_mut().get_mut(&id) {
773 registration.editable_kind = kind;
774 registration.apply_edit = apply_edit;
775 }
776 });
777 }
778 pub fn with_all<R>(visit: impl FnOnce(&[(u64, SignalRegistration)]) -> R) -> R {
781 let snapshot = SIGNALS.with(|signals| {
782 signals
783 .borrow()
784 .iter()
785 .map(|(id, row)| (*id, row.clone()))
786 .collect::<Vec<_>>()
787 });
788 visit(&snapshot)
789 }
790}
791
792#[derive(Clone, Copy, Debug, PartialEq, Eq)]
793pub enum EditableSignalKind {
794 Bool,
795 Int,
796 Uint,
797 Float,
798 Str,
799}
800
801#[cfg_attr(not(feature = "devtools"), allow(dead_code))]
802#[derive(Clone)]
803pub struct SignalRegistration {
804 pub name: Option<String>,
805 pub type_name: &'static str,
806 pub editable_kind: Option<EditableSignalKind>,
807 pub write_count: Rc<dyn Fn() -> u64>,
808 pub subscriber_count: Rc<dyn Fn() -> usize>,
809 pub subscribers: Rc<dyn Fn() -> Vec<(ReactiveRootId, ElementId)>>,
810 pub last_write: Rc<dyn Fn() -> (Option<String>, Option<String>)>,
811 #[cfg(feature = "devtools")]
812 pub apply_edit: Rc<dyn Fn(&incular_devtools_protocol::EditableValue) -> bool>,
813}