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
390thread_local! {
391    /// Invalidations of signals written while a builder ran, applied at
392    /// the start of the next frame.
393    static DEFERRED_INVALIDATIONS: RefCell<Vec<Box<dyn FnOnce()>>> = const { RefCell::new(Vec::new()) };
394    /// Build owners already reported for a write during their build.
395    static REPORTED_BUILD_WRITES: RefCell<std::collections::HashSet<String>> =
396        RefCell::new(std::collections::HashSet::new());
397}
398
399/// Whether a reactive write happens while a builder runs. Builders must stay
400/// free of side effects, but like Flutter's `setState` during build, a
401/// stray write is reported (once per build owner) instead of closing the
402/// application: the value changes at once and its dependents rebuild in
403/// the next frame.
404fn 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
426/// Queues a deferred invalidation for the next frame.
427fn defer_invalidation(invalidate: impl FnOnce() + 'static) {
428    DEFERRED_INVALIDATIONS.with(|queue| queue.borrow_mut().push(Box::new(invalidate)));
429}
430
431/// Whether writes made during a build still wait to invalidate their
432/// dependents.
433pub(crate) fn has_deferred_invalidations() -> bool {
434    DEFERRED_INVALIDATIONS
435        .try_with(|queue| !queue.borrow().is_empty())
436        .unwrap_or(false)
437}
438
439/// Applies the invalidations deferred from writes made during a build.
440pub(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    /// DevTools write counter; always present, only read under the feature.
454    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/// Shared state. A read in a registered builder subscribes that Element.
495#[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    /// Creates single-threaded application state. It attaches to the runtime
536    /// that reads it during a build. Clones may be explicitly shared between
537    /// windows or independent applications on the same UI thread.
538    #[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    /// Attaches a DevTools-visible debug name and registers the signal in
559    /// the DevTools registry. Requires `T: Debug` for value summaries.
560    #[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    /// Marks this named signal development-editable from DevTools. Only the
620    /// supported primitive kinds can round-trip an `EditableValue`.
621    #[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        // Re-register with an explicit, typed conversion closure. The closure
642        // can only create the documented primitive types, then uses Signal's
643        // ordinary UI-thread mutation path and dependency invalidation.
644        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    /// Replaces the value and invalidates dependents when it changed.
669    ///
670    /// Reactive state must be changed from an event callback, effect, or
671    /// action. Mutating a signal while a widget builder is running is
672    /// rejected because builders must remain side-effect free.
673    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    /// Reads the value without registering a dependency.
707    pub fn get_untracked(&self) -> T
708    where
709        T: Clone,
710    {
711        self.inner.value.borrow().clone()
712    }
713    /// Mutates the value once and schedules only the Elements that read it.
714    ///
715    /// See [`Signal::set`] for the builder-side-effect rule.
716    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    /// Mutates the value and invalidates dependents only when the value
742    /// changed. This is the equality-aware counterpart to [`Signal::update`]
743    /// for mutable values that implement [`Clone`] and [`PartialEq`].
744    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    /// Notifies dependents now, or at the next frame for a write made
778    /// while building.
779    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    /// Visits a snapshot outside the registry borrow, permitting signal drop,
834    /// rename and edits from the visitor without reentrant RefCell failures.
835    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}