Skip to main content

gpui_rhai/
primitive.rs

1use std::cell::RefCell;
2use std::collections::{BTreeMap, BTreeSet};
3use std::fmt;
4use std::panic::{AssertUnwindSafe, catch_unwind};
5use std::rc::{Rc, Weak};
6
7use gpui::{AnyElement, App, IntoElement, ParentElement, RenderOnce, Window, div};
8use rhai::{Array, Dynamic, FnPtr, Map};
9use serde::{Deserialize, Serialize};
10use thiserror::Error;
11
12use crate::{
13    AssetId, ColorResolver, ColorValue, ComponentStateSchema, EventSchema, Length,
14    NodeEventDispatcher, ObjectField, RadiusToken, Rgba8, SchemaDefinitionError,
15    SchemaValidationError, ScriptCallback, ScriptGeneration, SpacingToken, Style, UiEventHandler,
16    UiNode, UiValue, UiValueError, ValueSchema,
17};
18
19#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
20#[serde(try_from = "String", into = "String")]
21pub struct PrimitiveId(String);
22
23impl PrimitiveId {
24    /// Parse a namespaced primitive ID such as `my_app.code_editor`.
25    ///
26    /// # Errors
27    ///
28    /// Returns [`PrimitiveError::InvalidId`] for invalid identifiers.
29    pub fn parse(value: impl Into<String>) -> Result<Self, PrimitiveError> {
30        let value = value.into();
31        if value
32            .split_once('.')
33            .is_some_and(|(namespace, name)| is_identifier(namespace) && is_identifier(name))
34        {
35            Ok(Self(value))
36        } else {
37            Err(PrimitiveError::InvalidId(value))
38        }
39    }
40
41    #[must_use]
42    pub fn as_str(&self) -> &str {
43        &self.0
44    }
45
46    #[must_use]
47    pub fn namespace(&self) -> &str {
48        self.0
49            .split_once('.')
50            .map_or("", |(namespace, _)| namespace)
51    }
52}
53
54impl TryFrom<String> for PrimitiveId {
55    type Error = PrimitiveError;
56
57    fn try_from(value: String) -> Result<Self, Self::Error> {
58        Self::parse(value)
59    }
60}
61
62impl From<PrimitiveId> for String {
63    fn from(value: PrimitiveId) -> Self {
64        value.0
65    }
66}
67
68fn is_identifier(value: &str) -> bool {
69    !value.is_empty()
70        && !value.starts_with('_')
71        && !value.ends_with('_')
72        && !value.contains("__")
73        && value.chars().all(|character| {
74            character.is_ascii_lowercase() || character.is_ascii_digit() || character == '_'
75        })
76}
77
78fn is_pascal_case(value: &str) -> bool {
79    value
80        .chars()
81        .next()
82        .is_some_and(|character| character.is_ascii_uppercase())
83        && value
84            .chars()
85            .all(|character| character.is_ascii_alphanumeric())
86}
87
88#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
89pub struct PrimitiveDescriptor {
90    pub id: PrimitiveId,
91    pub export: String,
92    #[serde(default)]
93    pub props: BTreeMap<String, ObjectField>,
94    #[serde(default)]
95    pub events: BTreeMap<String, EventSchema>,
96    #[serde(default)]
97    pub state: ComponentStateSchema,
98    #[serde(default)]
99    pub lifecycle: bool,
100    #[serde(default, skip_serializing_if = "Option::is_none")]
101    pub effect: Option<EffectPrimitiveDescriptor>,
102}
103
104#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
105#[serde(rename_all = "snake_case")]
106pub enum PrimitivePlatform {
107    MacOs,
108    Linux,
109    Windows,
110}
111
112impl PrimitivePlatform {
113    #[must_use]
114    pub const fn current() -> Self {
115        #[cfg(target_os = "macos")]
116        {
117            Self::MacOs
118        }
119        #[cfg(target_os = "linux")]
120        {
121            Self::Linux
122        }
123        #[cfg(target_os = "windows")]
124        {
125            Self::Windows
126        }
127    }
128}
129
130#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
131pub struct EffectPrimitiveDescriptor {
132    pub platforms: BTreeSet<PrimitivePlatform>,
133    pub max_instances: usize,
134    pub max_cost_per_instance: usize,
135    pub reduced_motion: bool,
136    pub quality_tiers: bool,
137}
138
139#[derive(Clone, Debug, PartialEq)]
140pub enum PrimitiveValue {
141    Data(UiValue),
142    Node(Box<UiNode>),
143    Nodes(Vec<UiNode>),
144    Callback(UiEventHandler),
145    Style(Box<Style>),
146    Length(Length),
147    Asset(AssetId),
148    Signal(crate::NativeSignal),
149    Ref(crate::ElementRef),
150    Document(crate::NativeTextDocument),
151    #[cfg(feature = "charts")]
152    ChartData(crate::NativeChartData),
153}
154
155/// Read-only semantic theme values captured for one native primitive render.
156///
157/// Primitive handlers use this snapshot to resolve component-owned paint parts
158/// without receiving a mutable application/theme manager or coupling to a
159/// concrete theme family.
160#[derive(Clone, Debug)]
161pub struct PrimitiveTheme {
162    colors: BTreeMap<String, Rgba8>,
163    spacing: BTreeMap<SpacingToken, Length>,
164    radii: BTreeMap<RadiusToken, Length>,
165    typography: BTreeMap<String, crate::ResolvedTypography>,
166    direction: crate::TextDirection,
167    locale: String,
168    number: Option<crate::NumberMetadata>,
169    motion: crate::ThemeMotion,
170    motion_preference: crate::MotionPreference,
171    motion_quality: crate::MotionQuality,
172    clock: crate::RuntimeClock,
173}
174
175pub(crate) const RUNTIME_THEME_COLOR_TOKENS: &[&str] = &[
176    "surface",
177    "surface_raised",
178    "surface_hover",
179    "text_primary",
180    "text_muted",
181    "accent",
182    "accent_hover",
183    "on_accent",
184    "danger",
185    "on_danger",
186    "warning",
187    "on_warning",
188    "success",
189    "on_success",
190    "border",
191    "focus_ring",
192    "selection",
193    "disabled",
194    "syntax.comment",
195    "syntax.string",
196    "syntax.number",
197    "syntax.keyword",
198    "syntax.function",
199    "syntax.type",
200    "syntax.variable",
201    "syntax.constant",
202    "syntax.operator",
203    "syntax.punctuation",
204    "syntax.tag",
205    "syntax.attribute",
206    "document.search_match",
207    "document.search_current",
208    "diff.left_only",
209    "diff.right_only",
210    "diff.modified",
211    "diff.inline_left",
212    "diff.inline_right",
213    "diff.gutter",
214    "diff.fold",
215    "charts.axis",
216    "charts.grid",
217    "charts.tooltip_surface",
218    "charts.tooltip_text",
219    "charts.positive",
220    "charts.negative",
221    "charts.selection",
222    "charts.map_missing",
223    "charts.crosshair",
224    "charts.palette_1",
225    "charts.palette_2",
226    "charts.palette_3",
227    "charts.palette_4",
228    "charts.palette_5",
229    "charts.palette_6",
230    "charts.palette_7",
231    "charts.palette_8",
232    "table.selection",
233];
234
235pub(crate) const RUNTIME_THEME_SPACING_TOKENS: &[SpacingToken] = &[
236    SpacingToken::Xxs,
237    SpacingToken::Xs,
238    SpacingToken::Sm,
239    SpacingToken::Md,
240    SpacingToken::Lg,
241];
242
243pub(crate) const RUNTIME_THEME_RADIUS_TOKENS: &[RadiusToken] =
244    &[RadiusToken::Sm, RadiusToken::Md, RadiusToken::Lg];
245
246impl Default for PrimitiveTheme {
247    fn default() -> Self {
248        Self {
249            colors: BTreeMap::new(),
250            spacing: BTreeMap::new(),
251            radii: BTreeMap::new(),
252            typography: BTreeMap::new(),
253            direction: crate::TextDirection::LeftToRight,
254            locale: "en".to_owned(),
255            number: None,
256            motion: crate::ThemeMotion::default(),
257            motion_preference: crate::MotionPreference::Normal,
258            motion_quality: crate::MotionQuality::High,
259            clock: crate::RuntimeClock::default(),
260        }
261    }
262}
263
264impl PrimitiveTheme {
265    #[cfg(test)]
266    pub(crate) fn capture(colors: &impl ColorResolver) -> Self {
267        Self::capture_with_direction(colors, crate::TextDirection::LeftToRight)
268    }
269
270    #[cfg(test)]
271    pub(crate) fn capture_with_direction(
272        colors: &impl ColorResolver,
273        direction: crate::TextDirection,
274    ) -> Self {
275        Self::capture_with_motion_policy(
276            colors,
277            direction,
278            crate::MotionPreference::Normal,
279            crate::MotionQuality::High,
280        )
281    }
282
283    #[cfg(test)]
284    pub(crate) fn capture_with_motion_policy(
285        colors: &impl ColorResolver,
286        direction: crate::TextDirection,
287        motion_preference: crate::MotionPreference,
288        motion_quality: crate::MotionQuality,
289    ) -> Self {
290        Self::capture_with_environment(
291            colors,
292            direction,
293            "en",
294            None,
295            crate::RuntimeClock::default(),
296            motion_preference,
297            motion_quality,
298        )
299    }
300
301    #[allow(clippy::too_many_lines)]
302    pub(crate) fn capture_with_environment(
303        colors: &impl ColorResolver,
304        direction: crate::TextDirection,
305        locale: &str,
306        number: Option<&crate::NumberMetadata>,
307        clock: crate::RuntimeClock,
308        motion_preference: crate::MotionPreference,
309        motion_quality: crate::MotionQuality,
310    ) -> Self {
311        Self {
312            colors: colors.color_snapshot(),
313            spacing: RUNTIME_THEME_SPACING_TOKENS
314                .iter()
315                .copied()
316                .filter_map(|token| {
317                    colors
318                        .resolve_length(Length::ThemeSpacing(token))
319                        .map(|value| (token, value))
320                })
321                .collect(),
322            radii: RUNTIME_THEME_RADIUS_TOKENS
323                .iter()
324                .copied()
325                .filter_map(|token| {
326                    colors
327                        .resolve_length(Length::ThemeRadius(token))
328                        .map(|value| (token, value))
329                })
330                .collect(),
331            typography: crate::REQUIRED_TYPOGRAPHY
332                .iter()
333                .filter_map(|role| {
334                    colors
335                        .resolve_typography(role)
336                        .map(|value| ((*role).to_owned(), value))
337                })
338                .collect(),
339            direction,
340            locale: locale.to_owned(),
341            number: number.cloned(),
342            motion: colors.resolve_motion(),
343            motion_preference,
344            motion_quality,
345            clock,
346        }
347    }
348
349    #[must_use]
350    pub fn color(&self, token: &str) -> Option<Rgba8> {
351        self.colors.get(token).copied()
352    }
353
354    #[must_use]
355    pub fn resolve_color(&self, value: &ColorValue) -> Option<Rgba8> {
356        match value {
357            ColorValue::Literal(value) => Some(*value),
358            ColorValue::Token(token) => self.color(token),
359        }
360    }
361
362    #[must_use]
363    pub fn resolve_length(&self, value: Length) -> Option<Length> {
364        match value {
365            Length::ThemeSpacing(token) => self.spacing.get(&token).copied(),
366            Length::ThemeRadius(token) => self.radii.get(&token).copied(),
367            Length::Pixels(_) | Length::Rems(_) | Length::Relative(_) => Some(value),
368        }
369    }
370
371    #[must_use]
372    pub const fn direction(&self) -> crate::TextDirection {
373        self.direction
374    }
375
376    #[must_use]
377    pub fn locale(&self) -> &str {
378        &self.locale
379    }
380
381    #[must_use]
382    pub const fn number_metadata(&self) -> Option<&crate::NumberMetadata> {
383        self.number.as_ref()
384    }
385
386    #[must_use]
387    pub const fn motion_preference(&self) -> crate::MotionPreference {
388        self.motion_preference
389    }
390
391    #[must_use]
392    pub const fn motion_quality(&self) -> crate::MotionQuality {
393        self.motion_quality
394    }
395
396    #[must_use]
397    pub fn now(&self) -> std::time::Instant {
398        self.clock.now()
399    }
400
401    #[must_use]
402    pub const fn motion(&self) -> &crate::ThemeMotion {
403        &self.motion
404    }
405
406    #[must_use]
407    pub fn typography(&self, role: &str) -> Option<crate::ResolvedTypography> {
408        self.typography.get(role).cloned()
409    }
410}
411
412impl ColorResolver for PrimitiveTheme {
413    fn resolve(&self, color: &ColorValue) -> Option<Rgba8> {
414        self.resolve_color(color)
415    }
416
417    fn resolve_length(&self, length: Length) -> Option<Length> {
418        PrimitiveTheme::resolve_length(self, length)
419    }
420
421    fn color_snapshot(&self) -> BTreeMap<String, Rgba8> {
422        self.colors.clone()
423    }
424
425    fn resolve_typography(&self, role: &str) -> Option<crate::ResolvedTypography> {
426        self.typography(role)
427    }
428
429    fn resolve_motion(&self) -> crate::ThemeMotion {
430        self.motion.clone()
431    }
432}
433
434#[derive(Clone, Debug, Default, PartialEq)]
435pub struct PrimitiveProps(BTreeMap<String, PrimitiveValue>);
436
437impl PrimitiveProps {
438    #[must_use]
439    pub fn new() -> Self {
440        Self::default()
441    }
442
443    #[must_use]
444    pub fn get(&self, name: &str) -> Option<&PrimitiveValue> {
445        self.0.get(name)
446    }
447
448    pub fn insert(
449        &mut self,
450        name: impl Into<String>,
451        value: PrimitiveValue,
452    ) -> Option<PrimitiveValue> {
453        self.0.insert(name.into(), value)
454    }
455
456    #[must_use]
457    pub fn with(mut self, name: impl Into<String>, value: PrimitiveValue) -> Self {
458        self.insert(name, value);
459        self
460    }
461
462    pub fn iter(&self) -> impl Iterator<Item = (&str, &PrimitiveValue)> {
463        self.0.iter().map(|(name, value)| (name.as_str(), value))
464    }
465
466    pub(crate) fn iter_mut(&mut self) -> impl Iterator<Item = (&str, &mut PrimitiveValue)> {
467        self.0
468            .iter_mut()
469            .map(|(name, value)| (name.as_str(), value))
470    }
471
472    pub(crate) fn bind_component_scope(
473        &mut self,
474        component: &crate::ComponentInstancePath,
475        incarnation: crate::ComponentIncarnation,
476        events: &BTreeMap<String, EventSchema>,
477        native_context: Option<&crate::invocation::ScriptInvocationContext>,
478    ) {
479        for value in self.0.values_mut() {
480            match value {
481                PrimitiveValue::Callback(callback) => {
482                    if let Some(callback) = callback.as_script_mut() {
483                        callback.bind_component_scope_if_unset(
484                            component,
485                            incarnation,
486                            events.clone(),
487                        );
488                        if let Some(context) = native_context {
489                            callback.bind_native_context_if_unset(context.clone());
490                        }
491                    }
492                }
493                PrimitiveValue::Node(node) => {
494                    node.bind_component_scope(component, incarnation, events, native_context);
495                }
496                PrimitiveValue::Nodes(nodes) => {
497                    for node in nodes {
498                        node.bind_component_scope(component, incarnation, events, native_context);
499                    }
500                }
501                PrimitiveValue::Data(_)
502                | PrimitiveValue::Style(_)
503                | PrimitiveValue::Length(_)
504                | PrimitiveValue::Asset(_)
505                | PrimitiveValue::Signal(_)
506                | PrimitiveValue::Ref(_)
507                | PrimitiveValue::Document(_) => {}
508                #[cfg(feature = "charts")]
509                PrimitiveValue::ChartData(_) => {}
510            }
511        }
512    }
513}
514
515#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
516pub struct PrimitiveInstanceId {
517    primitive: PrimitiveId,
518    key: String,
519    node: crate::NodeId,
520}
521
522impl PrimitiveInstanceId {
523    pub(crate) fn new(primitive: PrimitiveId, key: String, node: crate::NodeId) -> Self {
524        Self {
525            primitive,
526            key,
527            node,
528        }
529    }
530
531    #[must_use]
532    pub const fn primitive(&self) -> &PrimitiveId {
533        &self.primitive
534    }
535
536    #[must_use]
537    pub fn key(&self) -> &str {
538        &self.key
539    }
540
541    #[must_use]
542    pub const fn node(&self) -> crate::NodeId {
543        self.node
544    }
545}
546
547#[derive(Clone, Debug, PartialEq)]
548pub struct PrimitiveNode {
549    pub primitive: PrimitiveId,
550    pub key: Option<String>,
551    pub props: PrimitiveProps,
552}
553
554#[derive(Clone, Debug)]
555pub struct PrimitiveInstance {
556    pub id: Option<PrimitiveInstanceId>,
557    pub node: PrimitiveNode,
558    resources: Option<PrimitiveResourceScope>,
559}
560
561impl PrimitiveInstance {
562    /// Return the runtime-owned cancellation scope for a retained instance.
563    /// Ephemeral primitives have no resource scope and must not start durable
564    /// work from render.
565    #[must_use]
566    pub const fn resources(&self) -> Option<&PrimitiveResourceScope> {
567        self.resources.as_ref()
568    }
569}
570
571#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
572pub struct PrimitiveResourceHandle(u64);
573
574struct PrimitiveResourceEntry {
575    label: String,
576    cleanup: Option<Box<dyn FnOnce()>>,
577}
578
579#[derive(Default)]
580struct PrimitiveResourceState {
581    next_id: u64,
582    entries: BTreeMap<u64, PrimitiveResourceEntry>,
583}
584
585impl Drop for PrimitiveResourceState {
586    fn drop(&mut self) {
587        let entries = std::mem::take(&mut self.entries);
588        for entry in entries.into_values().rev() {
589            let _ = run_resource_cleanup(entry);
590        }
591    }
592}
593
594#[derive(Clone, Default)]
595pub struct PrimitiveResourceScope {
596    inner: Rc<RefCell<PrimitiveResourceState>>,
597}
598
599impl fmt::Debug for PrimitiveResourceScope {
600    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
601        match self.inner.try_borrow() {
602            Ok(state) => formatter
603                .debug_struct("PrimitiveResourceScope")
604                .field(
605                    "active",
606                    &state
607                        .entries
608                        .values()
609                        .map(|entry| entry.label.as_str())
610                        .collect::<Vec<_>>(),
611                )
612                .finish(),
613            Err(_) => formatter.write_str("PrimitiveResourceScope(<borrowed>)"),
614        }
615    }
616}
617
618impl PrimitiveResourceScope {
619    #[must_use]
620    pub fn new() -> Self {
621        Self::default()
622    }
623
624    /// Own one native task/subscription/resource cancellation callback.
625    ///
626    /// # Errors
627    ///
628    /// Returns [`PrimitiveResourceError`] for an unsafe label or conflicting
629    /// scope borrow.
630    pub fn own(
631        &self,
632        label: impl Into<String>,
633        cleanup: impl FnOnce() + 'static,
634    ) -> Result<PrimitiveResourceHandle, PrimitiveResourceError> {
635        let label = label.into();
636        if label.is_empty()
637            || label.len() > 128
638            || !label.chars().all(|character| {
639                character.is_ascii_alphanumeric() || matches!(character, '_' | '-' | '.' | ':')
640            })
641        {
642            return Err(PrimitiveResourceError::InvalidLabel(label));
643        }
644        let mut state = self
645            .inner
646            .try_borrow_mut()
647            .map_err(|_| PrimitiveResourceError::Borrowed)?;
648        let id = state.next_id.max(1);
649        state.next_id = id
650            .checked_add(1)
651            .ok_or(PrimitiveResourceError::IdExhausted)?;
652        state.entries.insert(
653            id,
654            PrimitiveResourceEntry {
655                label,
656                cleanup: Some(Box::new(cleanup)),
657            },
658        );
659        Ok(PrimitiveResourceHandle(id))
660    }
661
662    /// Cancel one owned resource early.
663    ///
664    /// # Errors
665    ///
666    /// Returns a borrow or cleanup-panic diagnostic.
667    pub fn cancel(&self, handle: &PrimitiveResourceHandle) -> Result<bool, PrimitiveResourceError> {
668        let entry = self
669            .inner
670            .try_borrow_mut()
671            .map_err(|_| PrimitiveResourceError::Borrowed)?
672            .entries
673            .remove(&handle.0);
674        let Some(entry) = entry else {
675            return Ok(false);
676        };
677        run_resource_cleanup(entry)?;
678        Ok(true)
679    }
680
681    #[must_use]
682    pub fn active_count(&self) -> usize {
683        self.inner.borrow().entries.len()
684    }
685
686    fn checkpoint(&self) -> Result<u64, PrimitiveResourceError> {
687        self.inner
688            .try_borrow()
689            .map(|state| state.next_id.max(1))
690            .map_err(|_| PrimitiveResourceError::Borrowed)
691    }
692
693    fn rollback(&self, checkpoint: u64) -> Result<(), PrimitiveResourceError> {
694        self.cleanup_where(|id| id >= checkpoint)
695    }
696
697    fn close(&self) -> Result<(), PrimitiveResourceError> {
698        self.cleanup_where(|_| true)
699    }
700
701    fn cleanup_where(&self, predicate: impl Fn(u64) -> bool) -> Result<(), PrimitiveResourceError> {
702        let mut entries = {
703            let mut state = self
704                .inner
705                .try_borrow_mut()
706                .map_err(|_| PrimitiveResourceError::Borrowed)?;
707            let ids = state
708                .entries
709                .keys()
710                .copied()
711                .filter(|id| predicate(*id))
712                .collect::<Vec<_>>();
713            ids.into_iter()
714                .rev()
715                .filter_map(|id| state.entries.remove(&id))
716                .collect::<Vec<_>>()
717        };
718        let mut first_error = None;
719        for entry in entries.drain(..) {
720            if let Err(error) = run_resource_cleanup(entry)
721                && first_error.is_none()
722            {
723                first_error = Some(error);
724            }
725        }
726        first_error.map_or(Ok(()), Err)
727    }
728}
729
730fn run_resource_cleanup(mut entry: PrimitiveResourceEntry) -> Result<(), PrimitiveResourceError> {
731    let label = entry.label;
732    let Some(cleanup) = entry.cleanup.take() else {
733        return Ok(());
734    };
735    catch_unwind(AssertUnwindSafe(cleanup))
736        .map_err(|_| PrimitiveResourceError::CleanupPanic { label })
737}
738
739#[derive(Clone, Debug, Error, Eq, PartialEq)]
740pub enum PrimitiveResourceError {
741    #[error("primitive resource label `{0}` must be 1-128 safe ASCII characters")]
742    InvalidLabel(String),
743    #[error("primitive resource scope is already borrowed")]
744    Borrowed,
745    #[error("primitive resource scope exhausted its handle identity space")]
746    IdExhausted,
747    #[error("primitive resource cleanup `{label}` panicked")]
748    CleanupPanic { label: String },
749}
750
751#[derive(Clone)]
752pub struct PrimitiveEventEmitter {
753    registry: Weak<RefCell<PrimitiveRegistryInner>>,
754    primitive: PrimitiveId,
755    callbacks: BTreeMap<String, UiEventHandler>,
756    dispatcher: Option<NodeEventDispatcher>,
757}
758
759impl PrimitiveEventEmitter {
760    /// Normalize and dispatch a declared native primitive event.
761    ///
762    /// # Errors
763    ///
764    /// Returns schema and event declaration errors. Missing callback props are
765    /// treated as an intentionally unobserved event.
766    pub fn emit(
767        &self,
768        event: &str,
769        payload: UiValue,
770        window: &mut Window,
771        cx: &mut App,
772    ) -> Result<(), PrimitiveError> {
773        let registry = PrimitiveRegistry {
774            inner: self
775                .registry
776                .upgrade()
777                .ok_or(PrimitiveError::RegistryReleased)?,
778        };
779        let payload = registry.normalize_event(&self.primitive, event, payload)?;
780        if let Some(handler) = self.callbacks.get(event) {
781            match handler {
782                UiEventHandler::Script(callback) => {
783                    if let Some(dispatcher) = self.dispatcher.as_ref() {
784                        dispatcher.dispatch(callback.clone(), payload, None, window, cx);
785                    }
786                }
787                UiEventHandler::Host(callback) => {
788                    callback.invoke(payload, window, cx);
789                }
790                UiEventHandler::Native(reference) => {
791                    if let Some(dispatcher) = self.dispatcher.as_ref() {
792                        dispatcher.dispatch_native(
793                            reference.clone(),
794                            event.to_owned(),
795                            payload,
796                            None,
797                            window,
798                            cx,
799                        );
800                    }
801                }
802            }
803        }
804        Ok(())
805    }
806
807    /// Write one primitive-owned native signal without invoking Rhai.
808    ///
809    /// # Errors
810    ///
811    /// Returns a stale or type error when the signal no longer belongs to the
812    /// mounted component instance.
813    pub fn write_signal(
814        &self,
815        signal: &crate::NativeSignal,
816        value: crate::SignalValue,
817        cx: &mut App,
818    ) -> Result<bool, crate::SignalError> {
819        self.dispatcher.as_ref().map_or_else(
820            || Err(crate::SignalError::Stale(signal.id().clone())),
821            |dispatcher| dispatcher.write_signal(signal.clone(), value, cx),
822        )
823    }
824
825    /// Read the last committed layout bounds for a primitive-owned element ref.
826    #[must_use]
827    pub fn element_bounds(
828        &self,
829        reference: &crate::ElementRef,
830        cx: &App,
831    ) -> Option<crate::GeometryBounds> {
832        self.dispatcher
833            .as_ref()
834            .and_then(|dispatcher| dispatcher.element_bounds(reference, cx))
835    }
836}
837
838pub trait PrimitiveHandler {
839    /// Return deterministic work units for this validated effect instance.
840    /// Non-effect primitives ignore this value.
841    fn effect_cost(&self, _instance: &PrimitiveInstance) -> usize {
842        1
843    }
844
845    /// Contribute a bounded semantic summary for the retained primitive node.
846    /// Native internals remain private; accessibility and automation receive
847    /// only durable, already-presented values.
848    fn accessibility(
849        &self,
850        _instance: &PrimitiveInstanceId,
851        _cx: &App,
852    ) -> Option<PrimitiveAccessibilityProjection> {
853        None
854    }
855
856    /// Actions that the native primitive can perform for assistive technology.
857    ///
858    /// The default is intentionally empty: a primitive must not advertise an
859    /// operation merely because its outer Rhai node has a compatible role.
860    fn accessibility_actions(
861        &self,
862        _instance: &PrimitiveInstanceId,
863    ) -> Vec<gpui::AccessibleAction> {
864        Vec::new()
865    }
866
867    /// Perform one previously advertised accessibility action.
868    ///
869    /// # Errors
870    ///
871    /// Returns a bounded diagnostic when the instance is stale, disabled, or
872    /// the platform payload is invalid for the action.
873    fn perform_accessibility_action(
874        &mut self,
875        _instance: &PrimitiveInstanceId,
876        _action: gpui::AccessibleAction,
877        _data: Option<&gpui::accesskit::ActionData>,
878        _window: &mut Window,
879        _cx: &mut App,
880    ) -> Result<(), String> {
881        Err("primitive does not support accessibility actions".to_owned())
882    }
883
884    /// Called once before the first render of a keyed lifecycle primitive.
885    ///
886    /// # Errors
887    ///
888    /// Returns a diagnostic message when native setup fails.
889    fn mount(&mut self, _instance: &PrimitiveInstance) -> Result<(), String> {
890        Ok(())
891    }
892
893    /// Apply a validated prop/style/event snapshot to an existing keyed instance.
894    ///
895    /// # Errors
896    ///
897    /// Returns a diagnostic message when the retained native update fails.
898    fn update(
899        &mut self,
900        _previous: &PrimitiveInstance,
901        _next: &PrimitiveInstance,
902    ) -> Result<(), String> {
903        Ok(())
904    }
905
906    /// Prepare one retained native instance to cross into suspended state.
907    /// Implementations must be idempotent because a failed peer is compensated
908    /// and the whole operation may be retried.
909    fn suspend(&mut self, _instance: &PrimitiveInstanceId, _cx: &mut App) {}
910
911    /// Prepare one suspended native instance to become active. The public view
912    /// is not marked active until every primitive and the script transaction succeed.
913    fn resume(&mut self, _instance: &PrimitiveInstanceId, _cx: &mut App) {}
914
915    /// Commit a successfully prepared resume after the script transaction has
916    /// also succeeded. Activity time and event delivery may begin here.
917    fn commit_resume(&mut self, _instance: &PrimitiveInstanceId, _cx: &mut App) {}
918
919    /// Render the primitive into a native GPUI element.
920    ///
921    /// # Errors
922    ///
923    /// Returns a diagnostic message when native rendering fails.
924    fn render(
925        &mut self,
926        instance: &PrimitiveInstance,
927        events: &PrimitiveEventEmitter,
928        theme: &PrimitiveTheme,
929        window: &mut Window,
930        cx: &mut App,
931    ) -> Result<AnyElement, String>;
932
933    /// Called when a previously mounted keyed primitive is no longer reachable.
934    fn unmount(&mut self, _instance: &PrimitiveInstanceId) {}
935}
936
937#[derive(Clone, Debug, Default, PartialEq)]
938pub struct PrimitiveAccessibilityProjection {
939    pub description: String,
940    pub value: Option<UiValue>,
941}
942
943struct PrimitiveEntry {
944    descriptor: PrimitiveDescriptor,
945    handler: Box<dyn PrimitiveHandler>,
946}
947
948#[derive(Default)]
949struct PrimitiveRegistryInner {
950    entries: BTreeMap<PrimitiveId, PrimitiveEntry>,
951    mounted: BTreeMap<PrimitiveInstanceId, PrimitiveInstance>,
952}
953
954#[derive(Clone, Default)]
955pub struct PrimitiveRegistry {
956    inner: Rc<RefCell<PrimitiveRegistryInner>>,
957}
958
959impl fmt::Debug for PrimitiveRegistry {
960    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
961        match self.inner.try_borrow() {
962            Ok(inner) => formatter
963                .debug_struct("PrimitiveRegistry")
964                .field("registered", &inner.entries.keys().collect::<Vec<_>>())
965                .field("mounted", &inner.mounted.keys().collect::<Vec<_>>())
966                .finish(),
967            Err(_) => formatter.write_str("PrimitiveRegistry(<borrowed>)"),
968        }
969    }
970}
971
972impl PrimitiveRegistry {
973    #[must_use]
974    pub fn new() -> Self {
975        Self::default()
976    }
977
978    /// Register a validated primitive descriptor and native handler.
979    ///
980    /// # Errors
981    ///
982    /// Returns [`PrimitiveError`] for duplicate IDs, invalid exports/defaults,
983    /// or missing callback props for declared events.
984    pub fn register(
985        &self,
986        descriptor: PrimitiveDescriptor,
987        handler: impl PrimitiveHandler + 'static,
988    ) -> Result<(), PrimitiveError> {
989        validate_descriptor(&descriptor)?;
990        let mut inner = self
991            .inner
992            .try_borrow_mut()
993            .map_err(|_| PrimitiveError::Borrowed)?;
994        if inner.entries.contains_key(&descriptor.id) {
995            return Err(PrimitiveError::Duplicate(descriptor.id));
996        }
997        if inner.entries.values().any(|entry| {
998            entry.descriptor.id.namespace() == descriptor.id.namespace()
999                && entry.descriptor.export == descriptor.export
1000        }) {
1001            return Err(PrimitiveError::DuplicateExport {
1002                namespace: descriptor.id.namespace().to_owned(),
1003                export: descriptor.export,
1004            });
1005        }
1006        inner.entries.insert(
1007            descriptor.id.clone(),
1008            PrimitiveEntry {
1009                descriptor,
1010                handler: Box::new(handler),
1011            },
1012        );
1013        Ok(())
1014    }
1015
1016    /// Convert validated Rhai props into a custom `UiNode`.
1017    ///
1018    /// # Errors
1019    ///
1020    /// Returns [`PrimitiveError`] for unknown primitives, invalid props, or a
1021    /// missing key on stateful/lifecycle instances.
1022    pub fn create_node(
1023        &self,
1024        id: &PrimitiveId,
1025        key: Option<String>,
1026        props: &Map,
1027        generation: ScriptGeneration,
1028    ) -> Result<UiNode, PrimitiveError> {
1029        let inner = self
1030            .inner
1031            .try_borrow()
1032            .map_err(|_| PrimitiveError::Borrowed)?;
1033        let descriptor = &inner
1034            .entries
1035            .get(id)
1036            .ok_or_else(|| PrimitiveError::Unknown(id.clone()))?
1037            .descriptor;
1038        if (descriptor.lifecycle || !descriptor.state.is_empty()) && key.is_none() {
1039            return Err(PrimitiveError::MissingKey(id.clone()));
1040        }
1041        let schema = ValueSchema::object(descriptor.props.clone());
1042        schema
1043            .validate(&Dynamic::from_map(props.clone()))
1044            .map_err(|source| PrimitiveError::InvalidProps {
1045                primitive: id.clone(),
1046                source,
1047            })?;
1048        let props = convert_props(&descriptor.props, props, generation)?;
1049        Ok(UiNode::custom(PrimitiveNode {
1050            primitive: id.clone(),
1051            key,
1052            props,
1053        }))
1054    }
1055
1056    /// Validate an event emitted by a native primitive adapter.
1057    ///
1058    /// # Errors
1059    ///
1060    /// Returns [`PrimitiveError`] for unknown events or invalid payloads.
1061    pub fn normalize_event(
1062        &self,
1063        id: &PrimitiveId,
1064        event: &str,
1065        payload: UiValue,
1066    ) -> Result<UiValue, PrimitiveError> {
1067        let inner = self
1068            .inner
1069            .try_borrow()
1070            .map_err(|_| PrimitiveError::Borrowed)?;
1071        let descriptor = &inner
1072            .entries
1073            .get(id)
1074            .ok_or_else(|| PrimitiveError::Unknown(id.clone()))?
1075            .descriptor;
1076        let schema = descriptor
1077            .events
1078            .get(event)
1079            .ok_or_else(|| PrimitiveError::UnknownEvent {
1080                primitive: id.clone(),
1081                event: event.to_owned(),
1082            })?;
1083        schema
1084            .payload
1085            .validate_ui_value(&payload)
1086            .map_err(|source| PrimitiveError::InvalidEvent {
1087                primitive: id.clone(),
1088                event: event.to_owned(),
1089                source,
1090            })?;
1091        Ok(payload)
1092    }
1093
1094    pub(crate) fn accessibility_projections(
1095        &self,
1096        tree: &crate::RetainedUiTree,
1097        cx: &App,
1098    ) -> BTreeMap<crate::NodeId, PrimitiveAccessibilityProjection> {
1099        let Ok(inner) = self.inner.try_borrow() else {
1100            return BTreeMap::new();
1101        };
1102        tree.nodes()
1103            .filter_map(|node| {
1104                let primitive = node.primitive()?.clone();
1105                let instance = PrimitiveInstanceId {
1106                    primitive: primitive.clone(),
1107                    key: node.key()?.to_owned(),
1108                    node: node.id(),
1109                };
1110                let projection = inner
1111                    .entries
1112                    .get(&primitive)?
1113                    .handler
1114                    .accessibility(&instance, cx)?;
1115                Some((node.id(), projection))
1116            })
1117            .collect()
1118    }
1119
1120    pub(crate) fn accessibility_actions(
1121        &self,
1122        instance: &PrimitiveInstanceId,
1123    ) -> Vec<gpui::AccessibleAction> {
1124        let Ok(inner) = self.inner.try_borrow() else {
1125            return Vec::new();
1126        };
1127        inner
1128            .entries
1129            .get(&instance.primitive)
1130            .map_or_else(Vec::new, |entry| {
1131                entry.handler.accessibility_actions(instance)
1132            })
1133    }
1134
1135    pub(crate) fn perform_accessibility_action(
1136        &self,
1137        instance: &PrimitiveInstanceId,
1138        action: gpui::AccessibleAction,
1139        data: Option<&gpui::accesskit::ActionData>,
1140        window: &mut Window,
1141        cx: &mut App,
1142    ) -> Result<(), PrimitiveError> {
1143        let mut inner = self
1144            .inner
1145            .try_borrow_mut()
1146            .map_err(|_| PrimitiveError::Borrowed)?;
1147        if !inner.mounted.contains_key(instance) {
1148            return Err(PrimitiveError::MissingInstance(instance.clone()));
1149        }
1150        let entry = inner
1151            .entries
1152            .get_mut(&instance.primitive)
1153            .ok_or_else(|| PrimitiveError::Unknown(instance.primitive.clone()))?;
1154        guard_primitive_panic(&instance.primitive, "accessibility action", || {
1155            entry
1156                .handler
1157                .perform_accessibility_action(instance, action, data, window, cx)
1158        })?
1159        .map_err(|message| PrimitiveError::Handler {
1160            primitive: instance.primitive.clone(),
1161            message,
1162        })
1163    }
1164
1165    /// Unmount keyed lifecycle instances absent from the successful node tree.
1166    ///
1167    /// # Errors
1168    ///
1169    /// Returns [`PrimitiveError::Borrowed`] during a conflicting render borrow.
1170    pub fn retain_mounted(
1171        &self,
1172        active: &BTreeSet<PrimitiveInstanceId>,
1173    ) -> Result<(), PrimitiveError> {
1174        let mut inner = self
1175            .inner
1176            .try_borrow_mut()
1177            .map_err(|_| PrimitiveError::Borrowed)?;
1178        let removed = inner
1179            .mounted
1180            .keys()
1181            .filter(|instance| !active.contains(*instance))
1182            .cloned()
1183            .collect::<Vec<_>>();
1184        let mut first_error = None;
1185        for instance in &removed {
1186            let resources = inner
1187                .mounted
1188                .get(instance)
1189                .and_then(|mounted| mounted.resources.clone());
1190            if let Some(entry) = inner.entries.get_mut(&instance.primitive)
1191                && let Err(error) = guard_primitive_panic(&instance.primitive, "unmount", || {
1192                    entry.handler.unmount(instance);
1193                })
1194                && first_error.is_none()
1195            {
1196                first_error = Some(error);
1197            }
1198            if let Some(resources) = resources
1199                && let Err(error) = resources.close()
1200                && first_error.is_none()
1201            {
1202                first_error = Some(PrimitiveError::Resource(error));
1203            }
1204            inner.mounted.remove(instance);
1205        }
1206        first_error.map_or(Ok(()), Err)
1207    }
1208
1209    /// Unmount keyed primitive instances absent from the current successful tree.
1210    ///
1211    /// # Errors
1212    ///
1213    /// Returns borrow or panic-boundary errors from native unmount handlers.
1214    pub fn retain_tree(&self, tree: &crate::RetainedUiTree) -> Result<(), PrimitiveError> {
1215        let active = collect_primitive_instances(tree);
1216        self.retain_mounted(&active)
1217    }
1218
1219    pub(crate) fn suspend_mounted(&self, cx: &mut App) -> Result<(), PrimitiveError> {
1220        let mut inner = self
1221            .inner
1222            .try_borrow_mut()
1223            .map_err(|_| PrimitiveError::Borrowed)?;
1224        let instances = inner.mounted.keys().cloned().collect::<Vec<_>>();
1225        let mut first_error = None;
1226        for instance in instances {
1227            if let Some(entry) = inner.entries.get_mut(&instance.primitive)
1228                && let Err(error) = guard_primitive_panic(&instance.primitive, "suspend", || {
1229                    entry.handler.suspend(&instance, cx);
1230                })
1231                && first_error.is_none()
1232            {
1233                first_error = Some(error);
1234            }
1235        }
1236        first_error.map_or(Ok(()), Err)
1237    }
1238
1239    pub(crate) fn resume_mounted(&self, cx: &mut App) -> Result<(), PrimitiveError> {
1240        let mut inner = self
1241            .inner
1242            .try_borrow_mut()
1243            .map_err(|_| PrimitiveError::Borrowed)?;
1244        let instances = inner.mounted.keys().cloned().collect::<Vec<_>>();
1245        let mut first_error = None;
1246        for instance in instances {
1247            if let Some(entry) = inner.entries.get_mut(&instance.primitive)
1248                && let Err(error) = guard_primitive_panic(&instance.primitive, "resume", || {
1249                    entry.handler.resume(&instance, cx);
1250                })
1251                && first_error.is_none()
1252            {
1253                first_error = Some(error);
1254            }
1255        }
1256        first_error.map_or(Ok(()), Err)
1257    }
1258
1259    pub(crate) fn commit_resume_mounted(&self, cx: &mut App) -> Result<(), PrimitiveError> {
1260        let mut inner = self
1261            .inner
1262            .try_borrow_mut()
1263            .map_err(|_| PrimitiveError::Borrowed)?;
1264        let instances = inner.mounted.keys().cloned().collect::<Vec<_>>();
1265        let mut first_error = None;
1266        for instance in instances {
1267            if let Some(entry) = inner.entries.get_mut(&instance.primitive)
1268                && let Err(error) =
1269                    guard_primitive_panic(&instance.primitive, "commit resume", || {
1270                        entry.handler.commit_resume(&instance, cx);
1271                    })
1272                && first_error.is_none()
1273            {
1274                first_error = Some(error);
1275            }
1276        }
1277        first_error.map_or(Ok(()), Err)
1278    }
1279
1280    pub(crate) fn element(
1281        &self,
1282        node: PrimitiveNode,
1283        retained_id: Option<crate::NodeId>,
1284        fallback: Option<UiNode>,
1285        dispatcher: Option<NodeEventDispatcher>,
1286        theme: PrimitiveTheme,
1287    ) -> AnyElement {
1288        RegisteredPrimitiveElement {
1289            registry: self.clone(),
1290            node,
1291            retained_id,
1292            fallback,
1293            dispatcher,
1294            theme,
1295        }
1296        .into_any_element()
1297    }
1298
1299    #[allow(clippy::too_many_lines)]
1300    fn render_instance(
1301        &self,
1302        node: PrimitiveNode,
1303        retained_id: Option<crate::NodeId>,
1304        events: &PrimitiveEventEmitter,
1305        theme: &PrimitiveTheme,
1306        window: &mut Window,
1307        cx: &mut App,
1308    ) -> Result<AnyElement, PrimitiveError> {
1309        let mut inner = self
1310            .inner
1311            .try_borrow_mut()
1312            .map_err(|_| PrimitiveError::Borrowed)?;
1313        let retained_instance = primitive_is_retained(&inner, &node.primitive)?;
1314        if retained_instance && retained_id.is_none() {
1315            return Err(PrimitiveError::MissingRetainedIdentity(node.primitive));
1316        }
1317        let instance_id = retained_instance.then(|| PrimitiveInstanceId {
1318            primitive: node.primitive.clone(),
1319            key: node
1320                .key
1321                .clone()
1322                .expect("retained primitive descriptors require a key"),
1323            node: retained_id.expect("retained primitive renderer supplies NodeId"),
1324        });
1325        let previous = instance_id
1326            .as_ref()
1327            .and_then(|id| inner.mounted.get(id))
1328            .cloned();
1329        let resources = if retained_instance {
1330            Some(
1331                previous
1332                    .as_ref()
1333                    .and_then(|instance| instance.resources.clone())
1334                    .unwrap_or_default(),
1335            )
1336        } else {
1337            None
1338        };
1339        let checkpoint = resources
1340            .as_ref()
1341            .map(PrimitiveResourceScope::checkpoint)
1342            .transpose()?;
1343        let instance = PrimitiveInstance {
1344            id: instance_id.clone(),
1345            node,
1346            resources: resources.clone(),
1347        };
1348        let needs_mount = instance_id.is_some() && previous.is_none();
1349        if needs_mount
1350            && let Some(effect) = inner
1351                .entries
1352                .get(&instance.node.primitive)
1353                .and_then(|entry| entry.descriptor.effect.as_ref())
1354        {
1355            let mounted = inner
1356                .mounted
1357                .keys()
1358                .filter(|id| id.primitive == instance.node.primitive)
1359                .count();
1360            if mounted >= effect.max_instances {
1361                return Err(PrimitiveError::EffectInstanceBudget {
1362                    primitive: instance.node.primitive.clone(),
1363                    actual: mounted.saturating_add(1),
1364                    limit: effect.max_instances,
1365                });
1366            }
1367        }
1368        let entry = inner
1369            .entries
1370            .get_mut(&instance.node.primitive)
1371            .ok_or_else(|| PrimitiveError::Unknown(instance.node.primitive.clone()))?;
1372        if let Some(effect) = &entry.descriptor.effect {
1373            let cost = guard_primitive_panic(&instance.node.primitive, "effect_cost", || {
1374                entry.handler.effect_cost(&instance)
1375            })?;
1376            if cost > effect.max_cost_per_instance {
1377                return Err(PrimitiveError::EffectCostBudget {
1378                    primitive: instance.node.primitive.clone(),
1379                    actual: cost,
1380                    limit: effect.max_cost_per_instance,
1381                });
1382            }
1383        }
1384        let operation = (|| {
1385            if needs_mount {
1386                guard_primitive_panic(&instance.node.primitive, "mount", || {
1387                    entry.handler.mount(&instance)
1388                })?
1389                .map_err(|message| PrimitiveError::Handler {
1390                    primitive: instance.node.primitive.clone(),
1391                    message,
1392                })?;
1393            }
1394            if let Some(previous) = &previous
1395                && previous.node != instance.node
1396            {
1397                guard_primitive_panic(&instance.node.primitive, "update", || {
1398                    entry.handler.update(previous, &instance)
1399                })?
1400                .map_err(|message| PrimitiveError::Handler {
1401                    primitive: instance.node.primitive.clone(),
1402                    message,
1403                })?;
1404            }
1405            guard_primitive_panic(&instance.node.primitive, "render", || {
1406                entry.handler.render(&instance, events, theme, window, cx)
1407            })?
1408            .map_err(|message| PrimitiveError::Handler {
1409                primitive: instance.node.primitive.clone(),
1410                message,
1411            })
1412        })();
1413        let element = match operation {
1414            Ok(element) => element,
1415            Err(error) => {
1416                return Err(rollback_failed_primitive_operation(
1417                    entry,
1418                    &instance,
1419                    needs_mount,
1420                    resources.as_ref(),
1421                    checkpoint,
1422                    error,
1423                ));
1424            }
1425        };
1426        if let Some(id) = instance_id {
1427            inner.mounted.insert(id, instance);
1428        }
1429        Ok(element)
1430    }
1431}
1432
1433fn rollback_failed_primitive_operation(
1434    entry: &mut PrimitiveEntry,
1435    instance: &PrimitiveInstance,
1436    needs_unmount: bool,
1437    resources: Option<&PrimitiveResourceScope>,
1438    checkpoint: Option<u64>,
1439    original: PrimitiveError,
1440) -> PrimitiveError {
1441    let mut rollback_error = None;
1442    if needs_unmount
1443        && let Some(instance_id) = instance.id.as_ref()
1444        && let Err(error) =
1445            guard_primitive_panic(&instance.node.primitive, "failed-mount unmount", || {
1446                entry.handler.unmount(instance_id);
1447            })
1448    {
1449        rollback_error = Some(error);
1450    }
1451    if let (Some(resources), Some(checkpoint)) = (resources, checkpoint)
1452        && let Err(error) = resources.rollback(checkpoint)
1453        && rollback_error.is_none()
1454    {
1455        rollback_error = Some(PrimitiveError::Resource(error));
1456    }
1457    rollback_error.unwrap_or(original)
1458}
1459
1460fn primitive_is_retained(
1461    inner: &PrimitiveRegistryInner,
1462    primitive: &PrimitiveId,
1463) -> Result<bool, PrimitiveError> {
1464    let descriptor = &inner
1465        .entries
1466        .get(primitive)
1467        .ok_or_else(|| PrimitiveError::Unknown(primitive.clone()))?
1468        .descriptor;
1469    Ok(descriptor.lifecycle || !descriptor.state.is_empty())
1470}
1471
1472fn collect_primitive_instances(tree: &crate::RetainedUiTree) -> BTreeSet<PrimitiveInstanceId> {
1473    tree.nodes()
1474        .filter_map(|node| {
1475            Some(PrimitiveInstanceId {
1476                primitive: node.primitive()?.clone(),
1477                key: node.key()?.to_owned(),
1478                node: node.id(),
1479            })
1480        })
1481        .collect()
1482}
1483
1484fn guard_primitive_panic<T>(
1485    primitive: &PrimitiveId,
1486    phase: &'static str,
1487    operation: impl FnOnce() -> T,
1488) -> Result<T, PrimitiveError> {
1489    catch_unwind(AssertUnwindSafe(operation)).map_err(|_| PrimitiveError::Panic {
1490        primitive: primitive.clone(),
1491        phase,
1492    })
1493}
1494
1495#[derive(gpui::IntoElement)]
1496struct RegisteredPrimitiveElement {
1497    registry: PrimitiveRegistry,
1498    node: PrimitiveNode,
1499    retained_id: Option<crate::NodeId>,
1500    fallback: Option<UiNode>,
1501    dispatcher: Option<NodeEventDispatcher>,
1502    theme: PrimitiveTheme,
1503}
1504
1505impl RenderOnce for RegisteredPrimitiveElement {
1506    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
1507        let registry = self.registry;
1508        let callbacks = self
1509            .node
1510            .props
1511            .iter()
1512            .filter_map(|(name, value)| {
1513                name.strip_prefix("on_").and_then(|event| match value {
1514                    PrimitiveValue::Callback(callback) => {
1515                        Some((event.to_owned(), callback.clone()))
1516                    }
1517                    _ => None,
1518                })
1519            })
1520            .collect();
1521        let events = PrimitiveEventEmitter {
1522            registry: Rc::downgrade(&registry.inner),
1523            primitive: self.node.primitive.clone(),
1524            callbacks,
1525            dispatcher: self.dispatcher,
1526        };
1527        match registry.render_instance(
1528            self.node,
1529            self.retained_id,
1530            &events,
1531            &self.theme,
1532            window,
1533            cx,
1534        ) {
1535            Ok(element) => element,
1536            Err(error) => self.fallback.map_or_else(
1537                || {
1538                    div()
1539                        .child(format!("Custom primitive error: {error}"))
1540                        .into_any_element()
1541                },
1542                |fallback| {
1543                    crate::GpuiNodeRenderer::render_with_primitives(
1544                        &fallback,
1545                        &crate::LiteralColorResolver,
1546                        &crate::InteractionState::default(),
1547                        &registry,
1548                    )
1549                },
1550            ),
1551        }
1552    }
1553}
1554
1555fn validate_descriptor(descriptor: &PrimitiveDescriptor) -> Result<(), PrimitiveError> {
1556    if !is_pascal_case(&descriptor.export) {
1557        return Err(PrimitiveError::InvalidExport(descriptor.export.clone()));
1558    }
1559    for (name, field) in &descriptor.props {
1560        if !is_identifier(name) {
1561            return Err(PrimitiveError::InvalidPropName(name.clone()));
1562        }
1563        field
1564            .schema
1565            .validate_definition()
1566            .map_err(|source| PrimitiveError::InvalidSchema {
1567                location: format!("prop `{name}`"),
1568                source,
1569            })?;
1570        if let Some(default) = &field.default {
1571            field.schema.validate_ui_value(default).map_err(|source| {
1572                PrimitiveError::InvalidDefault {
1573                    prop: name.clone(),
1574                    source,
1575                }
1576            })?;
1577        }
1578    }
1579    for (name, event) in &descriptor.events {
1580        event
1581            .payload
1582            .validate_definition()
1583            .map_err(|source| PrimitiveError::InvalidSchema {
1584                location: format!("event `{name}`"),
1585                source,
1586            })?;
1587        let callback = format!("on_{name}");
1588        if !descriptor
1589            .props
1590            .get(&callback)
1591            .is_some_and(|field| schema_accepts_callback(&field.schema))
1592        {
1593            return Err(PrimitiveError::MissingEventCallback {
1594                event: name.clone(),
1595                prop: callback,
1596            });
1597        }
1598    }
1599    ComponentStateSchema::new(descriptor.state.fields().clone())
1600        .map_err(|source| PrimitiveError::InvalidStateSchema(source.to_string()))?;
1601    if let Some(effect) = &descriptor.effect {
1602        if effect.platforms.is_empty() || !effect.platforms.contains(&PrimitivePlatform::current())
1603        {
1604            return Err(PrimitiveError::UnsupportedEffectPlatform {
1605                primitive: descriptor.id.clone(),
1606                platform: PrimitivePlatform::current(),
1607            });
1608        }
1609        if effect.max_instances == 0 || effect.max_cost_per_instance == 0 {
1610            return Err(PrimitiveError::InvalidEffectBudget(descriptor.id.clone()));
1611        }
1612        if !descriptor.lifecycle {
1613            return Err(PrimitiveError::EffectRequiresLifecycle(
1614                descriptor.id.clone(),
1615            ));
1616        }
1617    }
1618    Ok(())
1619}
1620
1621fn schema_accepts_callback(schema: &ValueSchema) -> bool {
1622    matches!(schema, ValueSchema::Callback)
1623        || matches!(schema, ValueSchema::Optional { value } if schema_accepts_callback(value))
1624        || matches!(schema, ValueSchema::OneOf { variants } if variants.iter().any(schema_accepts_callback))
1625}
1626
1627fn convert_props(
1628    schema: &BTreeMap<String, ObjectField>,
1629    values: &Map,
1630    generation: ScriptGeneration,
1631) -> Result<PrimitiveProps, PrimitiveError> {
1632    let mut converted = BTreeMap::new();
1633    for (name, field) in schema {
1634        let value = values
1635            .get(name.as_str())
1636            .cloned()
1637            .or_else(|| field.default.clone().map(UiValue::into_dynamic));
1638        if let Some(value) = value {
1639            converted.insert(
1640                name.clone(),
1641                convert_prop(&field.schema, value, generation).map_err(|source| {
1642                    PrimitiveError::PropConversion {
1643                        prop: name.clone(),
1644                        source,
1645                    }
1646                })?,
1647            );
1648        }
1649    }
1650    Ok(PrimitiveProps(converted))
1651}
1652
1653fn convert_prop(
1654    schema: &ValueSchema,
1655    value: Dynamic,
1656    generation: ScriptGeneration,
1657) -> Result<PrimitiveValue, PrimitivePropConversionError> {
1658    match schema {
1659        ValueSchema::Optional { value: inner } if value.is_unit() => {
1660            Ok(PrimitiveValue::Data(UiValue::Null))
1661        }
1662        ValueSchema::Optional { value: inner } => convert_prop(inner, value, generation),
1663        ValueSchema::OneOf { variants } => {
1664            let branch = variants
1665                .iter()
1666                .find(|variant| variant.validate(&value).is_ok())
1667                .expect("validated primitive one_of prop matches one branch");
1668            convert_prop(branch, value, generation)
1669        }
1670        ValueSchema::Node => Ok(PrimitiveValue::Node(Box::new(value.cast::<UiNode>()))),
1671        ValueSchema::Callback if value.is::<FnPtr>() => {
1672            Ok(PrimitiveValue::Callback(UiEventHandler::Script(
1673                ScriptCallback::try_from_fn_ptr(value.cast::<FnPtr>(), generation)?,
1674            )))
1675        }
1676        ValueSchema::Callback => Ok(PrimitiveValue::Callback(UiEventHandler::Native(
1677            value.cast::<crate::NativeHandlerRef>(),
1678        ))),
1679        ValueSchema::Array { items, .. } if matches!(items.as_ref(), ValueSchema::Node) => {
1680            Ok(PrimitiveValue::Nodes(
1681                value
1682                    .cast::<Array>()
1683                    .into_iter()
1684                    .map(Dynamic::cast::<UiNode>)
1685                    .collect(),
1686            ))
1687        }
1688        ValueSchema::Style => Ok(PrimitiveValue::Style(Box::new(value.cast::<Style>()))),
1689        ValueSchema::Length => Ok(PrimitiveValue::Length(value.cast::<Length>())),
1690        ValueSchema::Asset => Ok(PrimitiveValue::Asset(value.cast::<AssetId>())),
1691        ValueSchema::Signal => Ok(PrimitiveValue::Signal(value.cast::<crate::NativeSignal>())),
1692        ValueSchema::Ref => Ok(PrimitiveValue::Ref(value.cast::<crate::ElementRef>())),
1693        ValueSchema::Document => Ok(PrimitiveValue::Document(
1694            value.cast::<crate::NativeTextDocument>(),
1695        )),
1696        #[cfg(feature = "charts")]
1697        ValueSchema::ChartData => Ok(PrimitiveValue::ChartData(
1698            value.cast::<crate::NativeChartData>(),
1699        )),
1700        _ => UiValue::from_dynamic(value)
1701            .map(PrimitiveValue::Data)
1702            .map_err(Into::into),
1703    }
1704}
1705
1706#[derive(Debug, Error)]
1707pub enum PrimitivePropConversionError {
1708    #[error(transparent)]
1709    Value(#[from] UiValueError),
1710    #[error(transparent)]
1711    Callback(#[from] crate::ScriptCallbackDefinitionError),
1712}
1713
1714#[derive(Debug, Error)]
1715pub enum PrimitiveError {
1716    #[error("primitive ID `{0}` must be `namespace.snake_case_name`")]
1717    InvalidId(String),
1718    #[error("primitive export `{0}` must be PascalCase")]
1719    InvalidExport(String),
1720    #[error("primitive prop `{0}` must be `snake_case`")]
1721    InvalidPropName(String),
1722    #[error("invalid schema definition for primitive {location}: {source}")]
1723    InvalidSchema {
1724        location: String,
1725        source: SchemaDefinitionError,
1726    },
1727    #[error("invalid primitive state schema: {0}")]
1728    InvalidStateSchema(String),
1729    #[error("primitive registry is already borrowed during rendering")]
1730    Borrowed,
1731    #[error("primitive `{0:?}` is already registered")]
1732    Duplicate(PrimitiveId),
1733    #[error("primitive export `{namespace}::{export}` is already registered")]
1734    DuplicateExport { namespace: String, export: String },
1735    #[error("primitive `{0:?}` is not registered")]
1736    Unknown(PrimitiveId),
1737    #[error("primitive `{0:?}` requires a stable key")]
1738    MissingKey(PrimitiveId),
1739    #[error("primitive `{0:?}` requires a retained NodeId renderer")]
1740    MissingRetainedIdentity(PrimitiveId),
1741    #[error("primitive accessibility action targeted stale instance {0:?}")]
1742    MissingInstance(PrimitiveInstanceId),
1743    #[error("primitive event emitter outlived its registry")]
1744    RegistryReleased,
1745    #[error("props for primitive `{primitive:?}` are invalid: {source}")]
1746    InvalidProps {
1747        primitive: PrimitiveId,
1748        source: SchemaValidationError,
1749    },
1750    #[error("default for primitive prop `{prop}` is invalid: {source}")]
1751    InvalidDefault {
1752        prop: String,
1753        source: SchemaValidationError,
1754    },
1755    #[error("primitive prop `{prop}` cannot cross the runtime boundary: {source}")]
1756    PropConversion {
1757        prop: String,
1758        source: PrimitivePropConversionError,
1759    },
1760    #[error("primitive event `{event}` requires callback prop `{prop}`")]
1761    MissingEventCallback { event: String, prop: String },
1762    #[error("primitive `{primitive:?}` does not declare event `{event}`")]
1763    UnknownEvent {
1764        primitive: PrimitiveId,
1765        event: String,
1766    },
1767    #[error("primitive `{primitive:?}` event `{event}` is invalid: {source}")]
1768    InvalidEvent {
1769        primitive: PrimitiveId,
1770        event: String,
1771        source: SchemaValidationError,
1772    },
1773    #[error("primitive `{primitive:?}` handler failed: {message}")]
1774    Handler {
1775        primitive: PrimitiveId,
1776        message: String,
1777    },
1778    #[error("primitive `{primitive:?}` panicked during {phase}")]
1779    Panic {
1780        primitive: PrimitiveId,
1781        phase: &'static str,
1782    },
1783    #[error("effect primitive `{primitive:?}` does not support {platform:?}")]
1784    UnsupportedEffectPlatform {
1785        primitive: PrimitiveId,
1786        platform: PrimitivePlatform,
1787    },
1788    #[error("effect primitive `{0:?}` must declare positive instance and cost budgets")]
1789    InvalidEffectBudget(PrimitiveId),
1790    #[error("effect primitive `{0:?}` must opt into scoped lifecycle")]
1791    EffectRequiresLifecycle(PrimitiveId),
1792    #[error("effect primitive `{primitive:?}` instance budget exceeded: {actual} > {limit}")]
1793    EffectInstanceBudget {
1794        primitive: PrimitiveId,
1795        actual: usize,
1796        limit: usize,
1797    },
1798    #[error("effect primitive `{primitive:?}` cost budget exceeded: {actual} > {limit}")]
1799    EffectCostBudget {
1800        primitive: PrimitiveId,
1801        actual: usize,
1802        limit: usize,
1803    },
1804    #[error(transparent)]
1805    Resource(#[from] PrimitiveResourceError),
1806}
1807
1808#[cfg(test)]
1809mod tests {
1810    use super::*;
1811    use crate::{ObjectField, StateField};
1812    use std::cell::Cell;
1813
1814    struct TestHandler;
1815
1816    struct TestTheme;
1817
1818    impl ColorResolver for TestTheme {
1819        fn resolve(&self, color: &ColorValue) -> Option<Rgba8> {
1820            match color {
1821                ColorValue::Token(token)
1822                    if matches!(token.as_str(), "accent" | "selection" | "table.selection") =>
1823                {
1824                    Some(Rgba8::from_rgba_hex(0x1234_56ff))
1825                }
1826                _ => None,
1827            }
1828        }
1829
1830        fn resolve_length(&self, length: Length) -> Option<Length> {
1831            match length {
1832                Length::ThemeSpacing(SpacingToken::Xxs) => Some(Length::Pixels(2.0)),
1833                Length::ThemeSpacing(SpacingToken::Sm) => Some(Length::Pixels(6.0)),
1834                _ => None,
1835            }
1836        }
1837
1838        fn resolve_typography(&self, role: &str) -> Option<crate::ResolvedTypography> {
1839            (role == "body").then(|| crate::ResolvedTypography {
1840                family: Some("JetBrains Mono".to_owned()),
1841                fallbacks: vec!["PingFang SC".to_owned()],
1842                size: Length::Pixels(12.0),
1843                line_height: Length::Pixels(16.0),
1844                weight: 400,
1845            })
1846        }
1847    }
1848
1849    impl PrimitiveHandler for TestHandler {
1850        fn render(
1851            &mut self,
1852            _: &PrimitiveInstance,
1853            _: &PrimitiveEventEmitter,
1854            _: &PrimitiveTheme,
1855            _: &mut Window,
1856            _: &mut App,
1857        ) -> Result<AnyElement, String> {
1858            Ok(div().into_any_element())
1859        }
1860    }
1861
1862    #[test]
1863    fn primitive_theme_exposes_only_resolved_semantic_snapshot() {
1864        let theme = PrimitiveTheme::capture(&TestTheme);
1865        assert_eq!(
1866            theme.color("accent"),
1867            Some(Rgba8::from_rgba_hex(0x1234_56ff))
1868        );
1869        assert_eq!(theme.color("unknown"), None);
1870        assert_eq!(
1871            theme.color("selection"),
1872            Some(Rgba8::from_rgba_hex(0x1234_56ff))
1873        );
1874        assert_eq!(
1875            theme.color("table.selection"),
1876            Some(Rgba8::from_rgba_hex(0x1234_56ff))
1877        );
1878        assert_eq!(
1879            theme.resolve_color(&ColorValue::Literal(Rgba8::from_rgba_hex(0xaabb_ccdd))),
1880            Some(Rgba8::from_rgba_hex(0xaabb_ccdd))
1881        );
1882        assert_eq!(
1883            theme.resolve_length(Length::ThemeSpacing(SpacingToken::Sm)),
1884            Some(Length::Pixels(6.0))
1885        );
1886        assert_eq!(
1887            theme.resolve_length(Length::ThemeSpacing(SpacingToken::Xxs)),
1888            Some(Length::Pixels(2.0))
1889        );
1890        assert_eq!(
1891            theme.typography("body").unwrap().family.as_deref(),
1892            Some("JetBrains Mono")
1893        );
1894
1895        let engine = crate::RuntimeEngine::new();
1896        let loaded = crate::load_theme_source(
1897            engine.engine(),
1898            "default_light.rhai",
1899            include_str!("../../../registry/themes/default_light.rhai"),
1900        )
1901        .unwrap();
1902        let captured = PrimitiveTheme::capture(&loaded);
1903        assert_eq!(
1904            captured.color("table.selection"),
1905            loaded.tokens.color("table.selection")
1906        );
1907        assert!(captured.color("table.selection").is_some());
1908    }
1909
1910    fn descriptor() -> PrimitiveDescriptor {
1911        PrimitiveDescriptor {
1912            id: PrimitiveId::parse("my_app.code_editor").unwrap(),
1913            export: "CodeEditor".to_owned(),
1914            props: BTreeMap::from([
1915                (
1916                    "value".to_owned(),
1917                    ObjectField::required(ValueSchema::string()),
1918                ),
1919                (
1920                    "on_change".to_owned(),
1921                    ObjectField::optional(ValueSchema::optional(ValueSchema::Callback)),
1922                ),
1923            ]),
1924            events: BTreeMap::from([(
1925                "change".to_owned(),
1926                EventSchema {
1927                    payload: ValueSchema::string(),
1928                },
1929            )]),
1930            state: ComponentStateSchema::new(BTreeMap::from([(
1931                "selection".to_owned(),
1932                StateField::new(ValueSchema::integer(), UiValue::Integer(0)),
1933            )]))
1934            .unwrap(),
1935            lifecycle: true,
1936            effect: None,
1937        }
1938    }
1939
1940    #[test]
1941    fn effect_descriptors_require_current_platform_lifecycle_and_budgets() {
1942        let mut descriptor = descriptor();
1943        descriptor.effect = Some(EffectPrimitiveDescriptor {
1944            platforms: BTreeSet::from([PrimitivePlatform::current()]),
1945            max_instances: 8,
1946            max_cost_per_instance: 4_096,
1947            reduced_motion: true,
1948            quality_tiers: true,
1949        });
1950        validate_descriptor(&descriptor).unwrap();
1951        descriptor.lifecycle = false;
1952        assert!(matches!(
1953            validate_descriptor(&descriptor),
1954            Err(PrimitiveError::EffectRequiresLifecycle(_))
1955        ));
1956    }
1957
1958    #[test]
1959    fn custom_primitive_props_and_keys_are_validated() {
1960        let registry = PrimitiveRegistry::new();
1961        let descriptor = descriptor();
1962        let id = descriptor.id.clone();
1963        registry.register(descriptor, TestHandler).unwrap();
1964        assert!(matches!(
1965            registry.create_node(
1966                &id,
1967                None,
1968                &Map::from_iter([("value".into(), Dynamic::from("source"))]),
1969                ScriptGeneration::initial(),
1970            ),
1971            Err(PrimitiveError::MissingKey(_))
1972        ));
1973        registry
1974            .create_node(
1975                &id,
1976                Some("editor".to_owned()),
1977                &Map::from_iter([("value".into(), Dynamic::from("source"))]),
1978                ScriptGeneration::initial(),
1979            )
1980            .unwrap();
1981    }
1982
1983    #[test]
1984    fn custom_primitive_events_are_normalized() {
1985        let registry = PrimitiveRegistry::new();
1986        let descriptor = descriptor();
1987        let id = descriptor.id.clone();
1988        registry.register(descriptor, TestHandler).unwrap();
1989        assert_eq!(
1990            registry
1991                .normalize_event(&id, "change", UiValue::String("new".to_owned()))
1992                .unwrap(),
1993            UiValue::String("new".to_owned())
1994        );
1995        assert!(matches!(
1996            registry.normalize_event(&id, "change", UiValue::Bool(true)),
1997            Err(PrimitiveError::InvalidEvent { .. })
1998        ));
1999    }
2000
2001    #[test]
2002    fn native_panics_are_converted_to_primitive_errors() {
2003        let id = PrimitiveId::parse("my_app.crash").unwrap();
2004        assert!(matches!(
2005            guard_primitive_panic(&id, "render", || panic!("boom")),
2006            Err(PrimitiveError::Panic {
2007                phase: "render",
2008                ..
2009            })
2010        ));
2011    }
2012
2013    #[test]
2014    fn primitive_resource_scope_rolls_back_and_continues_after_cleanup_panic() {
2015        let scope = PrimitiveResourceScope::new();
2016        let retained = Rc::new(Cell::new(0));
2017        let retained_cleanup = Rc::clone(&retained);
2018        scope
2019            .own("retained", move || retained_cleanup.set(1))
2020            .unwrap();
2021        let checkpoint = scope.checkpoint().unwrap();
2022        let order = Rc::new(RefCell::new(Vec::new()));
2023        let first = Rc::clone(&order);
2024        scope
2025            .own("first", move || first.borrow_mut().push(1))
2026            .unwrap();
2027        scope.own("panic", || panic!("cleanup failed")).unwrap();
2028        let last = Rc::clone(&order);
2029        scope
2030            .own("last", move || last.borrow_mut().push(3))
2031            .unwrap();
2032
2033        assert!(matches!(
2034            scope.rollback(checkpoint),
2035            Err(PrimitiveResourceError::CleanupPanic { ref label }) if label == "panic"
2036        ));
2037        assert_eq!(*order.borrow(), vec![3, 1]);
2038        assert_eq!(scope.active_count(), 1);
2039        scope.close().unwrap();
2040        assert_eq!(retained.get(), 1);
2041    }
2042
2043    #[test]
2044    fn successful_tree_cleanup_unmounts_removed_keyed_instances() {
2045        struct UnmountCounter(Rc<Cell<usize>>);
2046        impl PrimitiveHandler for UnmountCounter {
2047            fn render(
2048                &mut self,
2049                _: &PrimitiveInstance,
2050                _: &PrimitiveEventEmitter,
2051                _: &PrimitiveTheme,
2052                _: &mut Window,
2053                _: &mut App,
2054            ) -> Result<AnyElement, String> {
2055                Ok(div().into_any_element())
2056            }
2057
2058            fn unmount(&mut self, _: &PrimitiveInstanceId) {
2059                self.0.set(self.0.get() + 1);
2060            }
2061        }
2062
2063        let registry = PrimitiveRegistry::new();
2064        let descriptor = descriptor();
2065        let node = PrimitiveNode {
2066            primitive: descriptor.id.clone(),
2067            key: Some("editor".to_owned()),
2068            props: PrimitiveProps::new(),
2069        };
2070        let mut tree = crate::RetainedUiTree::new();
2071        tree.reconcile(UiNode::custom(node.clone())).unwrap();
2072        let instance = collect_primitive_instances(&tree)
2073            .into_iter()
2074            .next()
2075            .unwrap();
2076        let unmounted = Rc::new(Cell::new(0));
2077        let cleaned = Rc::new(Cell::new(0));
2078        let resources = PrimitiveResourceScope::new();
2079        let cleanup = Rc::clone(&cleaned);
2080        resources
2081            .own("watcher", move || cleanup.set(cleanup.get() + 1))
2082            .unwrap();
2083        registry
2084            .register(descriptor, UnmountCounter(Rc::clone(&unmounted)))
2085            .unwrap();
2086        registry.inner.borrow_mut().mounted.insert(
2087            instance.clone(),
2088            PrimitiveInstance {
2089                id: Some(instance.clone()),
2090                node,
2091                resources: Some(resources),
2092            },
2093        );
2094        tree.reconcile(UiNode::text("removed")).unwrap();
2095        registry.retain_tree(&tree).unwrap();
2096        assert_eq!(unmounted.get(), 1);
2097        assert_eq!(cleaned.get(), 1);
2098    }
2099
2100    #[test]
2101    fn primitive_identity_uses_retained_node_not_component_local_key() {
2102        let primitive = PrimitiveId::parse("my_app.editor").unwrap();
2103        let branch = |branch: &str| {
2104            UiNode::box_node(vec![UiNode::custom(PrimitiveNode {
2105                primitive: primitive.clone(),
2106                key: Some("editor".to_owned()),
2107                props: PrimitiveProps::new().with(
2108                    "branch",
2109                    PrimitiveValue::Data(UiValue::String(branch.to_owned())),
2110                ),
2111            })])
2112            .with_key(branch)
2113        };
2114        let mut tree = crate::RetainedUiTree::new();
2115        tree.reconcile(UiNode::box_node(vec![branch("left"), branch("right")]))
2116            .unwrap();
2117        let before = collect_primitive_instances(&tree);
2118        assert_eq!(before.len(), 2);
2119        assert!(before.iter().all(|instance| instance.key() == "editor"));
2120        assert_eq!(
2121            before
2122                .iter()
2123                .map(PrimitiveInstanceId::node)
2124                .collect::<BTreeSet<_>>()
2125                .len(),
2126            2
2127        );
2128
2129        tree.reconcile(UiNode::box_node(vec![branch("right"), branch("left")]))
2130            .unwrap();
2131        assert_eq!(collect_primitive_instances(&tree), before);
2132    }
2133
2134    #[test]
2135    fn primitive_event_emitter_holds_only_a_weak_registry_reference() {
2136        let registry = PrimitiveRegistry::new();
2137        let weak = Rc::downgrade(&registry.inner);
2138        let emitter = PrimitiveEventEmitter {
2139            registry: Rc::downgrade(&registry.inner),
2140            primitive: PrimitiveId::parse("my_app.editor").unwrap(),
2141            callbacks: BTreeMap::new(),
2142            dispatcher: None,
2143        };
2144        assert_eq!(Rc::strong_count(&registry.inner), 1);
2145        drop(registry);
2146        assert!(weak.upgrade().is_none());
2147        drop(emitter);
2148    }
2149}