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
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    /// Focus-scope subscriptions registered by builders. Keeping the token in
148    /// the same lifetime bucket as signal dependencies makes unmount and
149    /// rebuild cleanup deterministic rather than leaking callbacks into a
150    /// long-lived scope node.
151    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        // Focus-only builders do not appear in `dependencies`; include their
274        // owner IDs so closing a window drops every subscription token too.
275        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
336/// Restores the previous builder scope even when user code panics.
337pub(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    /// DevTools write counter; always present, only read under the feature.
382    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/// Shared state. A read in a registered builder subscribes that Element.
423#[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    /// Creates single-threaded application state. It attaches to the runtime
464    /// that reads it during a build. Clones may be explicitly shared between
465    /// windows or independent applications on the same UI thread.
466    #[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    /// Attaches a DevTools-visible debug name and registers the signal in
487    /// the DevTools registry. Requires `T: Debug` for value summaries.
488    #[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    /// Marks this named signal development-editable from DevTools. Only the
548    /// supported primitive kinds can round-trip an `EditableValue`.
549    #[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        // Re-register with an explicit, typed conversion closure. The closure
570        // can only create the documented primitive types, then uses Signal's
571        // ordinary UI-thread mutation path and dependency invalidation.
572        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    /// Replaces the value and invalidates dependents when it changed.
597    ///
598    /// Reactive state must be changed from an event callback, effect, or
599    /// action. Mutating a signal while a widget builder is running is
600    /// rejected because builders must remain side-effect free.
601    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    /// Reads the value without registering a dependency.
635    pub fn get_untracked(&self) -> T
636    where
637        T: Clone,
638    {
639        self.inner.value.borrow().clone()
640    }
641    /// Mutates the value once and schedules only the Elements that read it.
642    ///
643    /// See [`Signal::set`] for the builder-side-effect rule.
644    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    /// Mutates the value and invalidates dependents only when the value
670    /// changed. This is the equality-aware counterpart to [`Signal::update`]
671    /// for mutable values that implement [`Clone`] and [`PartialEq`].
672    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    /// Visits a snapshot outside the registry borrow, permitting signal drop,
749    /// rename and edits from the visitor without reentrant RefCell failures.
750    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}