Skip to main content

incular_runtime/
environment.rs

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/// Truncates a Debug rendering to keep DevTools payloads bounded.
21#[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    /// The runtime environment fields a build may depend on, and the fields a
35    /// platform update changed.
36    #[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        /// The remaining `MediaQueryData` accessibility and text values.
53        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    // Display features are view geometry and change with it.
64    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    /// Focus-scope subscriptions registered by builders. Keeping the token in
178    /// the same lifetime bucket as signal dependencies makes unmount and
179    /// rebuild cleanup deterministic rather than leaking callbacks into a
180    /// long-lived scope node.
181    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        // Focus-only builders do not appear in `dependencies`; include their
304        // owner IDs so closing a window drops every subscription token too.
305        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
366/// Restores the previous builder scope even when user code panics.
367pub(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    /// DevTools write counter; always present, only read under the feature.
412    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/// Shared state. A read in a registered builder subscribes that Element.
453#[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    /// Creates single-threaded application state. It attaches to the runtime
494    /// that reads it during a build. Clones may be explicitly shared between
495    /// windows or independent applications on the same UI thread.
496    #[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    /// Attaches a DevTools-visible debug name and registers the signal in
517    /// the DevTools registry. Requires `T: Debug` for value summaries.
518    #[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    /// Marks this named signal development-editable from DevTools. Only the
578    /// supported primitive kinds can round-trip an `EditableValue`.
579    #[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        // Re-register with an explicit, typed conversion closure. The closure
600        // can only create the documented primitive types, then uses Signal's
601        // ordinary UI-thread mutation path and dependency invalidation.
602        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    /// Replaces the value and invalidates dependents when it changed.
627    ///
628    /// Reactive state must be changed from an event callback, effect, or
629    /// action. Mutating a signal while a widget builder is running is
630    /// rejected because builders must remain side-effect free.
631    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    /// Reads the value without registering a dependency.
665    pub fn get_untracked(&self) -> T
666    where
667        T: Clone,
668    {
669        self.inner.value.borrow().clone()
670    }
671    /// Mutates the value once and schedules only the Elements that read it.
672    ///
673    /// See [`Signal::set`] for the builder-side-effect rule.
674    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    /// Mutates the value and invalidates dependents only when the value
700    /// changed. This is the equality-aware counterpart to [`Signal::update`]
701    /// for mutable values that implement [`Clone`] and [`PartialEq`].
702    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    /// Visits a snapshot outside the registry borrow, permitting signal drop,
779    /// rename and edits from the visitor without reentrant RefCell failures.
780    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}