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    focus_handle: Option<gpui::FocusHandle>,
560}
561
562impl PrimitiveInstance {
563    /// Return the runtime-owned cancellation scope for a retained instance.
564    /// Ephemeral primitives have no resource scope and must not start durable
565    /// work from render.
566    #[must_use]
567    pub const fn resources(&self) -> Option<&PrimitiveResourceScope> {
568        self.resources.as_ref()
569    }
570
571    /// Return the runtime-owned focus identity for this native control.
572    ///
573    /// A focus-owning primitive must use this handle instead of allocating a
574    /// second identity so its declarative wrapper, automation target, and
575    /// native editor all observe the same focus state.
576    #[must_use]
577    pub const fn focus_handle(&self) -> Option<&gpui::FocusHandle> {
578        self.focus_handle.as_ref()
579    }
580}
581
582#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
583pub struct PrimitiveResourceHandle(u64);
584
585struct PrimitiveResourceEntry {
586    label: String,
587    cleanup: Option<Box<dyn FnOnce()>>,
588}
589
590#[derive(Default)]
591struct PrimitiveResourceState {
592    next_id: u64,
593    entries: BTreeMap<u64, PrimitiveResourceEntry>,
594}
595
596impl Drop for PrimitiveResourceState {
597    fn drop(&mut self) {
598        let entries = std::mem::take(&mut self.entries);
599        for entry in entries.into_values().rev() {
600            let _ = run_resource_cleanup(entry);
601        }
602    }
603}
604
605#[derive(Clone, Default)]
606pub struct PrimitiveResourceScope {
607    inner: Rc<RefCell<PrimitiveResourceState>>,
608}
609
610impl fmt::Debug for PrimitiveResourceScope {
611    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
612        match self.inner.try_borrow() {
613            Ok(state) => formatter
614                .debug_struct("PrimitiveResourceScope")
615                .field(
616                    "active",
617                    &state
618                        .entries
619                        .values()
620                        .map(|entry| entry.label.as_str())
621                        .collect::<Vec<_>>(),
622                )
623                .finish(),
624            Err(_) => formatter.write_str("PrimitiveResourceScope(<borrowed>)"),
625        }
626    }
627}
628
629impl PrimitiveResourceScope {
630    #[must_use]
631    pub fn new() -> Self {
632        Self::default()
633    }
634
635    /// Own one native task/subscription/resource cancellation callback.
636    ///
637    /// # Errors
638    ///
639    /// Returns [`PrimitiveResourceError`] for an unsafe label or conflicting
640    /// scope borrow.
641    pub fn own(
642        &self,
643        label: impl Into<String>,
644        cleanup: impl FnOnce() + 'static,
645    ) -> Result<PrimitiveResourceHandle, PrimitiveResourceError> {
646        let label = label.into();
647        if label.is_empty()
648            || label.len() > 128
649            || !label.chars().all(|character| {
650                character.is_ascii_alphanumeric() || matches!(character, '_' | '-' | '.' | ':')
651            })
652        {
653            return Err(PrimitiveResourceError::InvalidLabel(label));
654        }
655        let mut state = self
656            .inner
657            .try_borrow_mut()
658            .map_err(|_| PrimitiveResourceError::Borrowed)?;
659        let id = state.next_id.max(1);
660        state.next_id = id
661            .checked_add(1)
662            .ok_or(PrimitiveResourceError::IdExhausted)?;
663        state.entries.insert(
664            id,
665            PrimitiveResourceEntry {
666                label,
667                cleanup: Some(Box::new(cleanup)),
668            },
669        );
670        Ok(PrimitiveResourceHandle(id))
671    }
672
673    /// Cancel one owned resource early.
674    ///
675    /// # Errors
676    ///
677    /// Returns a borrow or cleanup-panic diagnostic.
678    pub fn cancel(&self, handle: &PrimitiveResourceHandle) -> Result<bool, PrimitiveResourceError> {
679        let entry = self
680            .inner
681            .try_borrow_mut()
682            .map_err(|_| PrimitiveResourceError::Borrowed)?
683            .entries
684            .remove(&handle.0);
685        let Some(entry) = entry else {
686            return Ok(false);
687        };
688        run_resource_cleanup(entry)?;
689        Ok(true)
690    }
691
692    #[must_use]
693    pub fn active_count(&self) -> usize {
694        self.inner.borrow().entries.len()
695    }
696
697    fn checkpoint(&self) -> Result<u64, PrimitiveResourceError> {
698        self.inner
699            .try_borrow()
700            .map(|state| state.next_id.max(1))
701            .map_err(|_| PrimitiveResourceError::Borrowed)
702    }
703
704    fn rollback(&self, checkpoint: u64) -> Result<(), PrimitiveResourceError> {
705        self.cleanup_where(|id| id >= checkpoint)
706    }
707
708    fn close(&self) -> Result<(), PrimitiveResourceError> {
709        self.cleanup_where(|_| true)
710    }
711
712    fn cleanup_where(&self, predicate: impl Fn(u64) -> bool) -> Result<(), PrimitiveResourceError> {
713        let mut entries = {
714            let mut state = self
715                .inner
716                .try_borrow_mut()
717                .map_err(|_| PrimitiveResourceError::Borrowed)?;
718            let ids = state
719                .entries
720                .keys()
721                .copied()
722                .filter(|id| predicate(*id))
723                .collect::<Vec<_>>();
724            ids.into_iter()
725                .rev()
726                .filter_map(|id| state.entries.remove(&id))
727                .collect::<Vec<_>>()
728        };
729        let mut first_error = None;
730        for entry in entries.drain(..) {
731            if let Err(error) = run_resource_cleanup(entry)
732                && first_error.is_none()
733            {
734                first_error = Some(error);
735            }
736        }
737        first_error.map_or(Ok(()), Err)
738    }
739}
740
741fn run_resource_cleanup(mut entry: PrimitiveResourceEntry) -> Result<(), PrimitiveResourceError> {
742    let label = entry.label;
743    let Some(cleanup) = entry.cleanup.take() else {
744        return Ok(());
745    };
746    catch_unwind(AssertUnwindSafe(cleanup))
747        .map_err(|_| PrimitiveResourceError::CleanupPanic { label })
748}
749
750#[derive(Clone, Debug, Error, Eq, PartialEq)]
751pub enum PrimitiveResourceError {
752    #[error("primitive resource label `{0}` must be 1-128 safe ASCII characters")]
753    InvalidLabel(String),
754    #[error("primitive resource scope is already borrowed")]
755    Borrowed,
756    #[error("primitive resource scope exhausted its handle identity space")]
757    IdExhausted,
758    #[error("primitive resource cleanup `{label}` panicked")]
759    CleanupPanic { label: String },
760}
761
762#[derive(Clone)]
763pub struct PrimitiveEventEmitter {
764    registry: Weak<RefCell<PrimitiveRegistryInner>>,
765    primitive: PrimitiveId,
766    callbacks: BTreeMap<String, UiEventHandler>,
767    dispatcher: Option<NodeEventDispatcher>,
768}
769
770impl PrimitiveEventEmitter {
771    /// Normalize and dispatch a declared native primitive event.
772    ///
773    /// # Errors
774    ///
775    /// Returns schema and event declaration errors. Missing callback props are
776    /// treated as an intentionally unobserved event.
777    pub fn emit(
778        &self,
779        event: &str,
780        payload: UiValue,
781        window: &mut Window,
782        cx: &mut App,
783    ) -> Result<(), PrimitiveError> {
784        let registry = PrimitiveRegistry {
785            inner: self
786                .registry
787                .upgrade()
788                .ok_or(PrimitiveError::RegistryReleased)?,
789        };
790        let payload = registry.normalize_event(&self.primitive, event, payload)?;
791        if let Some(handler) = self.callbacks.get(event) {
792            match handler {
793                UiEventHandler::Script(callback) => {
794                    if let Some(dispatcher) = self.dispatcher.as_ref() {
795                        dispatcher.dispatch(callback.clone(), payload, None, window, cx);
796                    }
797                }
798                UiEventHandler::Host(callback) => {
799                    callback.invoke(payload, window, cx);
800                }
801                UiEventHandler::Native(reference) => {
802                    if let Some(dispatcher) = self.dispatcher.as_ref() {
803                        dispatcher.dispatch_native(
804                            reference.clone(),
805                            event.to_owned(),
806                            payload,
807                            None,
808                            window,
809                            cx,
810                        );
811                    }
812                }
813            }
814        }
815        Ok(())
816    }
817
818    /// Write one primitive-owned native signal without invoking Rhai.
819    ///
820    /// # Errors
821    ///
822    /// Returns a stale or type error when the signal no longer belongs to the
823    /// mounted component instance.
824    pub fn write_signal(
825        &self,
826        signal: &crate::NativeSignal,
827        value: crate::SignalValue,
828        cx: &mut App,
829    ) -> Result<bool, crate::SignalError> {
830        self.dispatcher.as_ref().map_or_else(
831            || Err(crate::SignalError::Stale(signal.id().clone())),
832            |dispatcher| dispatcher.write_signal(signal.clone(), value, cx),
833        )
834    }
835
836    /// Read the last committed layout bounds for a primitive-owned element ref.
837    #[must_use]
838    pub fn element_bounds(
839        &self,
840        reference: &crate::ElementRef,
841        cx: &App,
842    ) -> Option<crate::GeometryBounds> {
843        self.dispatcher
844            .as_ref()
845            .and_then(|dispatcher| dispatcher.element_bounds(reference, cx))
846    }
847}
848
849pub trait PrimitiveHandler {
850    /// Return deterministic work units for this validated effect instance.
851    /// Non-effect primitives ignore this value.
852    fn effect_cost(&self, _instance: &PrimitiveInstance) -> usize {
853        1
854    }
855
856    /// Contribute a bounded semantic summary for the retained primitive node.
857    /// Native internals remain private; accessibility and automation receive
858    /// only durable, already-presented values.
859    fn accessibility(
860        &self,
861        _instance: &PrimitiveInstanceId,
862        _cx: &App,
863    ) -> Option<PrimitiveAccessibilityProjection> {
864        None
865    }
866
867    /// Actions that the native primitive can perform for assistive technology.
868    ///
869    /// The default is intentionally empty: a primitive must not advertise an
870    /// operation merely because its outer Rhai node has a compatible role.
871    fn accessibility_actions(
872        &self,
873        _instance: &PrimitiveInstanceId,
874    ) -> Vec<gpui::AccessibleAction> {
875        Vec::new()
876    }
877
878    /// Whether this primitive owns one primary native focus target.
879    ///
880    /// The runtime allocates and retains that identity before rendering so the
881    /// declarative wrapper and native control share it from their first frame.
882    fn uses_primary_focus(&self) -> bool {
883        false
884    }
885
886    /// Perform one previously advertised accessibility action.
887    ///
888    /// # Errors
889    ///
890    /// Returns a bounded diagnostic when the instance is stale, disabled, or
891    /// the platform payload is invalid for the action.
892    fn perform_accessibility_action(
893        &mut self,
894        _instance: &PrimitiveInstanceId,
895        _action: gpui::AccessibleAction,
896        _data: Option<&gpui::accesskit::ActionData>,
897        _window: &mut Window,
898        _cx: &mut App,
899    ) -> Result<(), String> {
900        Err("primitive does not support accessibility actions".to_owned())
901    }
902
903    /// Called once before the first render of a keyed lifecycle primitive.
904    ///
905    /// # Errors
906    ///
907    /// Returns a diagnostic message when native setup fails.
908    fn mount(&mut self, _instance: &PrimitiveInstance) -> Result<(), String> {
909        Ok(())
910    }
911
912    /// Apply a validated prop/style/event snapshot to an existing keyed instance.
913    ///
914    /// # Errors
915    ///
916    /// Returns a diagnostic message when the retained native update fails.
917    fn update(
918        &mut self,
919        _previous: &PrimitiveInstance,
920        _next: &PrimitiveInstance,
921    ) -> Result<(), String> {
922        Ok(())
923    }
924
925    /// Prepare one retained native instance to cross into suspended state.
926    /// Implementations must be idempotent because a failed peer is compensated
927    /// and the whole operation may be retried.
928    fn suspend(&mut self, _instance: &PrimitiveInstanceId, _cx: &mut App) {}
929
930    /// Prepare one suspended native instance to become active. The public view
931    /// is not marked active until every primitive and the script transaction succeed.
932    fn resume(&mut self, _instance: &PrimitiveInstanceId, _cx: &mut App) {}
933
934    /// Commit a successfully prepared resume after the script transaction has
935    /// also succeeded. Activity time and event delivery may begin here.
936    fn commit_resume(&mut self, _instance: &PrimitiveInstanceId, _cx: &mut App) {}
937
938    /// Render the primitive into a native GPUI element.
939    ///
940    /// # Errors
941    ///
942    /// Returns a diagnostic message when native rendering fails.
943    fn render(
944        &mut self,
945        instance: &PrimitiveInstance,
946        events: &PrimitiveEventEmitter,
947        theme: &PrimitiveTheme,
948        window: &mut Window,
949        cx: &mut App,
950    ) -> Result<AnyElement, String>;
951
952    /// Called when a previously mounted keyed primitive is no longer reachable.
953    fn unmount(&mut self, _instance: &PrimitiveInstanceId) {}
954}
955
956#[derive(Clone, Debug, Default, PartialEq)]
957pub struct PrimitiveAccessibilityProjection {
958    pub description: String,
959    pub value: Option<UiValue>,
960}
961
962struct PrimitiveEntry {
963    descriptor: PrimitiveDescriptor,
964    handler: Box<dyn PrimitiveHandler>,
965}
966
967#[derive(Default)]
968struct PrimitiveRegistryInner {
969    entries: BTreeMap<PrimitiveId, PrimitiveEntry>,
970    mounted: BTreeMap<PrimitiveInstanceId, PrimitiveInstance>,
971}
972
973#[derive(Clone, Default)]
974pub struct PrimitiveRegistry {
975    inner: Rc<RefCell<PrimitiveRegistryInner>>,
976}
977
978impl fmt::Debug for PrimitiveRegistry {
979    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
980        match self.inner.try_borrow() {
981            Ok(inner) => formatter
982                .debug_struct("PrimitiveRegistry")
983                .field("registered", &inner.entries.keys().collect::<Vec<_>>())
984                .field("mounted", &inner.mounted.keys().collect::<Vec<_>>())
985                .finish(),
986            Err(_) => formatter.write_str("PrimitiveRegistry(<borrowed>)"),
987        }
988    }
989}
990
991impl PrimitiveRegistry {
992    #[must_use]
993    pub fn new() -> Self {
994        Self::default()
995    }
996
997    /// Register a validated primitive descriptor and native handler.
998    ///
999    /// # Errors
1000    ///
1001    /// Returns [`PrimitiveError`] for duplicate IDs, invalid exports/defaults,
1002    /// or missing callback props for declared events.
1003    pub fn register(
1004        &self,
1005        descriptor: PrimitiveDescriptor,
1006        handler: impl PrimitiveHandler + 'static,
1007    ) -> Result<(), PrimitiveError> {
1008        validate_descriptor(&descriptor)?;
1009        let mut inner = self
1010            .inner
1011            .try_borrow_mut()
1012            .map_err(|_| PrimitiveError::Borrowed)?;
1013        if inner.entries.contains_key(&descriptor.id) {
1014            return Err(PrimitiveError::Duplicate(descriptor.id));
1015        }
1016        if inner.entries.values().any(|entry| {
1017            entry.descriptor.id.namespace() == descriptor.id.namespace()
1018                && entry.descriptor.export == descriptor.export
1019        }) {
1020            return Err(PrimitiveError::DuplicateExport {
1021                namespace: descriptor.id.namespace().to_owned(),
1022                export: descriptor.export,
1023            });
1024        }
1025        inner.entries.insert(
1026            descriptor.id.clone(),
1027            PrimitiveEntry {
1028                descriptor,
1029                handler: Box::new(handler),
1030            },
1031        );
1032        Ok(())
1033    }
1034
1035    /// Convert validated Rhai props into a custom `UiNode`.
1036    ///
1037    /// # Errors
1038    ///
1039    /// Returns [`PrimitiveError`] for unknown primitives, invalid props, or a
1040    /// missing key on stateful/lifecycle instances.
1041    pub fn create_node(
1042        &self,
1043        id: &PrimitiveId,
1044        key: Option<String>,
1045        props: &Map,
1046        generation: ScriptGeneration,
1047    ) -> Result<UiNode, PrimitiveError> {
1048        let inner = self
1049            .inner
1050            .try_borrow()
1051            .map_err(|_| PrimitiveError::Borrowed)?;
1052        let descriptor = &inner
1053            .entries
1054            .get(id)
1055            .ok_or_else(|| PrimitiveError::Unknown(id.clone()))?
1056            .descriptor;
1057        if (descriptor.lifecycle || !descriptor.state.is_empty()) && key.is_none() {
1058            return Err(PrimitiveError::MissingKey(id.clone()));
1059        }
1060        let schema = ValueSchema::object(descriptor.props.clone());
1061        schema
1062            .validate(&Dynamic::from_map(props.clone()))
1063            .map_err(|source| PrimitiveError::InvalidProps {
1064                primitive: id.clone(),
1065                source,
1066            })?;
1067        let props = convert_props(&descriptor.props, props, generation)?;
1068        Ok(UiNode::custom(PrimitiveNode {
1069            primitive: id.clone(),
1070            key,
1071            props,
1072        }))
1073    }
1074
1075    /// Validate an event emitted by a native primitive adapter.
1076    ///
1077    /// # Errors
1078    ///
1079    /// Returns [`PrimitiveError`] for unknown events or invalid payloads.
1080    pub fn normalize_event(
1081        &self,
1082        id: &PrimitiveId,
1083        event: &str,
1084        payload: UiValue,
1085    ) -> Result<UiValue, PrimitiveError> {
1086        let inner = self
1087            .inner
1088            .try_borrow()
1089            .map_err(|_| PrimitiveError::Borrowed)?;
1090        let descriptor = &inner
1091            .entries
1092            .get(id)
1093            .ok_or_else(|| PrimitiveError::Unknown(id.clone()))?
1094            .descriptor;
1095        let schema = descriptor
1096            .events
1097            .get(event)
1098            .ok_or_else(|| PrimitiveError::UnknownEvent {
1099                primitive: id.clone(),
1100                event: event.to_owned(),
1101            })?;
1102        schema
1103            .payload
1104            .validate_ui_value(&payload)
1105            .map_err(|source| PrimitiveError::InvalidEvent {
1106                primitive: id.clone(),
1107                event: event.to_owned(),
1108                source,
1109            })?;
1110        Ok(payload)
1111    }
1112
1113    pub(crate) fn accessibility_projections(
1114        &self,
1115        tree: &crate::RetainedUiTree,
1116        cx: &App,
1117    ) -> BTreeMap<crate::NodeId, PrimitiveAccessibilityProjection> {
1118        let Ok(inner) = self.inner.try_borrow() else {
1119            return BTreeMap::new();
1120        };
1121        tree.nodes()
1122            .filter_map(|node| {
1123                let primitive = node.primitive()?.clone();
1124                let instance = PrimitiveInstanceId {
1125                    primitive: primitive.clone(),
1126                    key: node.key()?.to_owned(),
1127                    node: node.id(),
1128                };
1129                let projection = inner
1130                    .entries
1131                    .get(&primitive)?
1132                    .handler
1133                    .accessibility(&instance, cx)?;
1134                Some((node.id(), projection))
1135            })
1136            .collect()
1137    }
1138
1139    pub(crate) fn accessibility_actions(
1140        &self,
1141        instance: &PrimitiveInstanceId,
1142    ) -> Vec<gpui::AccessibleAction> {
1143        let Ok(inner) = self.inner.try_borrow() else {
1144            return Vec::new();
1145        };
1146        inner
1147            .entries
1148            .get(&instance.primitive)
1149            .map_or_else(Vec::new, |entry| {
1150                entry.handler.accessibility_actions(instance)
1151            })
1152    }
1153
1154    pub(crate) fn uses_primary_focus(&self, primitive: &PrimitiveId) -> bool {
1155        self.inner.try_borrow().ok().is_some_and(|inner| {
1156            inner
1157                .entries
1158                .get(primitive)
1159                .is_some_and(|entry| entry.handler.uses_primary_focus())
1160        })
1161    }
1162
1163    pub(crate) fn perform_accessibility_action(
1164        &self,
1165        instance: &PrimitiveInstanceId,
1166        action: gpui::AccessibleAction,
1167        data: Option<&gpui::accesskit::ActionData>,
1168        window: &mut Window,
1169        cx: &mut App,
1170    ) -> Result<(), PrimitiveError> {
1171        let mut inner = self
1172            .inner
1173            .try_borrow_mut()
1174            .map_err(|_| PrimitiveError::Borrowed)?;
1175        if !inner.mounted.contains_key(instance) {
1176            return Err(PrimitiveError::MissingInstance(instance.clone()));
1177        }
1178        let entry = inner
1179            .entries
1180            .get_mut(&instance.primitive)
1181            .ok_or_else(|| PrimitiveError::Unknown(instance.primitive.clone()))?;
1182        guard_primitive_panic(&instance.primitive, "accessibility action", || {
1183            entry
1184                .handler
1185                .perform_accessibility_action(instance, action, data, window, cx)
1186        })?
1187        .map_err(|message| PrimitiveError::Handler {
1188            primitive: instance.primitive.clone(),
1189            message,
1190        })
1191    }
1192
1193    /// Unmount keyed lifecycle instances absent from the successful node tree.
1194    ///
1195    /// # Errors
1196    ///
1197    /// Returns [`PrimitiveError::Borrowed`] during a conflicting render borrow.
1198    pub fn retain_mounted(
1199        &self,
1200        active: &BTreeSet<PrimitiveInstanceId>,
1201    ) -> Result<(), PrimitiveError> {
1202        let mut inner = self
1203            .inner
1204            .try_borrow_mut()
1205            .map_err(|_| PrimitiveError::Borrowed)?;
1206        let removed = inner
1207            .mounted
1208            .keys()
1209            .filter(|instance| !active.contains(*instance))
1210            .cloned()
1211            .collect::<Vec<_>>();
1212        let mut first_error = None;
1213        for instance in &removed {
1214            let resources = inner
1215                .mounted
1216                .get(instance)
1217                .and_then(|mounted| mounted.resources.clone());
1218            if let Some(entry) = inner.entries.get_mut(&instance.primitive)
1219                && let Err(error) = guard_primitive_panic(&instance.primitive, "unmount", || {
1220                    entry.handler.unmount(instance);
1221                })
1222                && first_error.is_none()
1223            {
1224                first_error = Some(error);
1225            }
1226            if let Some(resources) = resources
1227                && let Err(error) = resources.close()
1228                && first_error.is_none()
1229            {
1230                first_error = Some(PrimitiveError::Resource(error));
1231            }
1232            inner.mounted.remove(instance);
1233        }
1234        first_error.map_or(Ok(()), Err)
1235    }
1236
1237    /// Unmount keyed primitive instances absent from the current successful tree.
1238    ///
1239    /// # Errors
1240    ///
1241    /// Returns borrow or panic-boundary errors from native unmount handlers.
1242    pub fn retain_tree(&self, tree: &crate::RetainedUiTree) -> Result<(), PrimitiveError> {
1243        let active = collect_primitive_instances(tree);
1244        self.retain_mounted(&active)
1245    }
1246
1247    pub(crate) fn suspend_mounted(&self, cx: &mut App) -> Result<(), PrimitiveError> {
1248        let mut inner = self
1249            .inner
1250            .try_borrow_mut()
1251            .map_err(|_| PrimitiveError::Borrowed)?;
1252        let instances = inner.mounted.keys().cloned().collect::<Vec<_>>();
1253        let mut first_error = None;
1254        for instance in instances {
1255            if let Some(entry) = inner.entries.get_mut(&instance.primitive)
1256                && let Err(error) = guard_primitive_panic(&instance.primitive, "suspend", || {
1257                    entry.handler.suspend(&instance, cx);
1258                })
1259                && first_error.is_none()
1260            {
1261                first_error = Some(error);
1262            }
1263        }
1264        first_error.map_or(Ok(()), Err)
1265    }
1266
1267    pub(crate) fn resume_mounted(&self, cx: &mut App) -> Result<(), PrimitiveError> {
1268        let mut inner = self
1269            .inner
1270            .try_borrow_mut()
1271            .map_err(|_| PrimitiveError::Borrowed)?;
1272        let instances = inner.mounted.keys().cloned().collect::<Vec<_>>();
1273        let mut first_error = None;
1274        for instance in instances {
1275            if let Some(entry) = inner.entries.get_mut(&instance.primitive)
1276                && let Err(error) = guard_primitive_panic(&instance.primitive, "resume", || {
1277                    entry.handler.resume(&instance, cx);
1278                })
1279                && first_error.is_none()
1280            {
1281                first_error = Some(error);
1282            }
1283        }
1284        first_error.map_or(Ok(()), Err)
1285    }
1286
1287    pub(crate) fn commit_resume_mounted(&self, cx: &mut App) -> Result<(), PrimitiveError> {
1288        let mut inner = self
1289            .inner
1290            .try_borrow_mut()
1291            .map_err(|_| PrimitiveError::Borrowed)?;
1292        let instances = inner.mounted.keys().cloned().collect::<Vec<_>>();
1293        let mut first_error = None;
1294        for instance in instances {
1295            if let Some(entry) = inner.entries.get_mut(&instance.primitive)
1296                && let Err(error) =
1297                    guard_primitive_panic(&instance.primitive, "commit resume", || {
1298                        entry.handler.commit_resume(&instance, cx);
1299                    })
1300                && first_error.is_none()
1301            {
1302                first_error = Some(error);
1303            }
1304        }
1305        first_error.map_or(Ok(()), Err)
1306    }
1307
1308    pub(crate) fn element(
1309        &self,
1310        node: PrimitiveNode,
1311        retained_id: Option<crate::NodeId>,
1312        focus_handle: Option<gpui::FocusHandle>,
1313        fallback: Option<UiNode>,
1314        dispatcher: Option<NodeEventDispatcher>,
1315        theme: PrimitiveTheme,
1316    ) -> AnyElement {
1317        RegisteredPrimitiveElement {
1318            registry: self.clone(),
1319            node,
1320            retained_id,
1321            focus_handle,
1322            fallback,
1323            dispatcher,
1324            theme,
1325        }
1326        .into_any_element()
1327    }
1328
1329    #[allow(clippy::too_many_lines)]
1330    fn render_instance(
1331        &self,
1332        node: PrimitiveNode,
1333        identity: PrimitiveRenderIdentity,
1334        events: &PrimitiveEventEmitter,
1335        theme: &PrimitiveTheme,
1336        window: &mut Window,
1337        cx: &mut App,
1338    ) -> Result<AnyElement, PrimitiveError> {
1339        let mut inner = self
1340            .inner
1341            .try_borrow_mut()
1342            .map_err(|_| PrimitiveError::Borrowed)?;
1343        let retained_instance = primitive_is_retained(&inner, &node.primitive)?;
1344        if retained_instance && identity.retained_id.is_none() {
1345            return Err(PrimitiveError::MissingRetainedIdentity(node.primitive));
1346        }
1347        let instance_id = retained_instance.then(|| PrimitiveInstanceId {
1348            primitive: node.primitive.clone(),
1349            key: node
1350                .key
1351                .clone()
1352                .expect("retained primitive descriptors require a key"),
1353            node: identity
1354                .retained_id
1355                .expect("retained primitive renderer supplies NodeId"),
1356        });
1357        let previous = instance_id
1358            .as_ref()
1359            .and_then(|id| inner.mounted.get(id))
1360            .cloned();
1361        let resources = if retained_instance {
1362            Some(
1363                previous
1364                    .as_ref()
1365                    .and_then(|instance| instance.resources.clone())
1366                    .unwrap_or_default(),
1367            )
1368        } else {
1369            None
1370        };
1371        let checkpoint = resources
1372            .as_ref()
1373            .map(PrimitiveResourceScope::checkpoint)
1374            .transpose()?;
1375        let instance = PrimitiveInstance {
1376            id: instance_id.clone(),
1377            node,
1378            resources: resources.clone(),
1379            focus_handle: identity.focus_handle,
1380        };
1381        let needs_mount = instance_id.is_some() && previous.is_none();
1382        if needs_mount
1383            && let Some(effect) = inner
1384                .entries
1385                .get(&instance.node.primitive)
1386                .and_then(|entry| entry.descriptor.effect.as_ref())
1387        {
1388            let mounted = inner
1389                .mounted
1390                .keys()
1391                .filter(|id| id.primitive == instance.node.primitive)
1392                .count();
1393            if mounted >= effect.max_instances {
1394                return Err(PrimitiveError::EffectInstanceBudget {
1395                    primitive: instance.node.primitive.clone(),
1396                    actual: mounted.saturating_add(1),
1397                    limit: effect.max_instances,
1398                });
1399            }
1400        }
1401        let entry = inner
1402            .entries
1403            .get_mut(&instance.node.primitive)
1404            .ok_or_else(|| PrimitiveError::Unknown(instance.node.primitive.clone()))?;
1405        if let Some(effect) = &entry.descriptor.effect {
1406            let cost = guard_primitive_panic(&instance.node.primitive, "effect_cost", || {
1407                entry.handler.effect_cost(&instance)
1408            })?;
1409            if cost > effect.max_cost_per_instance {
1410                return Err(PrimitiveError::EffectCostBudget {
1411                    primitive: instance.node.primitive.clone(),
1412                    actual: cost,
1413                    limit: effect.max_cost_per_instance,
1414                });
1415            }
1416        }
1417        let operation = (|| {
1418            if needs_mount {
1419                guard_primitive_panic(&instance.node.primitive, "mount", || {
1420                    entry.handler.mount(&instance)
1421                })?
1422                .map_err(|message| PrimitiveError::Handler {
1423                    primitive: instance.node.primitive.clone(),
1424                    message,
1425                })?;
1426            }
1427            if let Some(previous) = &previous
1428                && previous.node != instance.node
1429            {
1430                guard_primitive_panic(&instance.node.primitive, "update", || {
1431                    entry.handler.update(previous, &instance)
1432                })?
1433                .map_err(|message| PrimitiveError::Handler {
1434                    primitive: instance.node.primitive.clone(),
1435                    message,
1436                })?;
1437            }
1438            guard_primitive_panic(&instance.node.primitive, "render", || {
1439                entry.handler.render(&instance, events, theme, window, cx)
1440            })?
1441            .map_err(|message| PrimitiveError::Handler {
1442                primitive: instance.node.primitive.clone(),
1443                message,
1444            })
1445        })();
1446        let element = match operation {
1447            Ok(element) => element,
1448            Err(error) => {
1449                return Err(rollback_failed_primitive_operation(
1450                    entry,
1451                    &instance,
1452                    needs_mount,
1453                    resources.as_ref(),
1454                    checkpoint,
1455                    error,
1456                ));
1457            }
1458        };
1459        if let Some(id) = instance_id {
1460            inner.mounted.insert(id, instance);
1461        }
1462        Ok(element)
1463    }
1464}
1465
1466fn rollback_failed_primitive_operation(
1467    entry: &mut PrimitiveEntry,
1468    instance: &PrimitiveInstance,
1469    needs_unmount: bool,
1470    resources: Option<&PrimitiveResourceScope>,
1471    checkpoint: Option<u64>,
1472    original: PrimitiveError,
1473) -> PrimitiveError {
1474    let mut rollback_error = None;
1475    if needs_unmount
1476        && let Some(instance_id) = instance.id.as_ref()
1477        && let Err(error) =
1478            guard_primitive_panic(&instance.node.primitive, "failed-mount unmount", || {
1479                entry.handler.unmount(instance_id);
1480            })
1481    {
1482        rollback_error = Some(error);
1483    }
1484    if let (Some(resources), Some(checkpoint)) = (resources, checkpoint)
1485        && let Err(error) = resources.rollback(checkpoint)
1486        && rollback_error.is_none()
1487    {
1488        rollback_error = Some(PrimitiveError::Resource(error));
1489    }
1490    rollback_error.unwrap_or(original)
1491}
1492
1493fn primitive_is_retained(
1494    inner: &PrimitiveRegistryInner,
1495    primitive: &PrimitiveId,
1496) -> Result<bool, PrimitiveError> {
1497    let descriptor = &inner
1498        .entries
1499        .get(primitive)
1500        .ok_or_else(|| PrimitiveError::Unknown(primitive.clone()))?
1501        .descriptor;
1502    Ok(descriptor.lifecycle || !descriptor.state.is_empty())
1503}
1504
1505fn collect_primitive_instances(tree: &crate::RetainedUiTree) -> BTreeSet<PrimitiveInstanceId> {
1506    tree.nodes()
1507        .filter_map(|node| {
1508            Some(PrimitiveInstanceId {
1509                primitive: node.primitive()?.clone(),
1510                key: node.key()?.to_owned(),
1511                node: node.id(),
1512            })
1513        })
1514        .collect()
1515}
1516
1517fn guard_primitive_panic<T>(
1518    primitive: &PrimitiveId,
1519    phase: &'static str,
1520    operation: impl FnOnce() -> T,
1521) -> Result<T, PrimitiveError> {
1522    catch_unwind(AssertUnwindSafe(operation)).map_err(|_| PrimitiveError::Panic {
1523        primitive: primitive.clone(),
1524        phase,
1525    })
1526}
1527
1528#[derive(gpui::IntoElement)]
1529struct RegisteredPrimitiveElement {
1530    registry: PrimitiveRegistry,
1531    node: PrimitiveNode,
1532    retained_id: Option<crate::NodeId>,
1533    focus_handle: Option<gpui::FocusHandle>,
1534    fallback: Option<UiNode>,
1535    dispatcher: Option<NodeEventDispatcher>,
1536    theme: PrimitiveTheme,
1537}
1538
1539struct PrimitiveRenderIdentity {
1540    retained_id: Option<crate::NodeId>,
1541    focus_handle: Option<gpui::FocusHandle>,
1542}
1543
1544impl RenderOnce for RegisteredPrimitiveElement {
1545    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
1546        let registry = self.registry;
1547        let callbacks = self
1548            .node
1549            .props
1550            .iter()
1551            .filter_map(|(name, value)| {
1552                name.strip_prefix("on_").and_then(|event| match value {
1553                    PrimitiveValue::Callback(callback) => {
1554                        Some((event.to_owned(), callback.clone()))
1555                    }
1556                    _ => None,
1557                })
1558            })
1559            .collect();
1560        let events = PrimitiveEventEmitter {
1561            registry: Rc::downgrade(&registry.inner),
1562            primitive: self.node.primitive.clone(),
1563            callbacks,
1564            dispatcher: self.dispatcher,
1565        };
1566        match registry.render_instance(
1567            self.node,
1568            PrimitiveRenderIdentity {
1569                retained_id: self.retained_id,
1570                focus_handle: self.focus_handle,
1571            },
1572            &events,
1573            &self.theme,
1574            window,
1575            cx,
1576        ) {
1577            Ok(element) => element,
1578            Err(error) => self.fallback.map_or_else(
1579                || {
1580                    div()
1581                        .child(format!("Custom primitive error: {error}"))
1582                        .into_any_element()
1583                },
1584                |fallback| {
1585                    crate::GpuiNodeRenderer::render_with_primitives(
1586                        &fallback,
1587                        &crate::LiteralColorResolver,
1588                        &crate::InteractionState::default(),
1589                        &registry,
1590                    )
1591                },
1592            ),
1593        }
1594    }
1595}
1596
1597fn validate_descriptor(descriptor: &PrimitiveDescriptor) -> Result<(), PrimitiveError> {
1598    if !is_pascal_case(&descriptor.export) {
1599        return Err(PrimitiveError::InvalidExport(descriptor.export.clone()));
1600    }
1601    for (name, field) in &descriptor.props {
1602        if !is_identifier(name) {
1603            return Err(PrimitiveError::InvalidPropName(name.clone()));
1604        }
1605        field
1606            .schema
1607            .validate_definition()
1608            .map_err(|source| PrimitiveError::InvalidSchema {
1609                location: format!("prop `{name}`"),
1610                source,
1611            })?;
1612        if let Some(default) = &field.default {
1613            field.schema.validate_ui_value(default).map_err(|source| {
1614                PrimitiveError::InvalidDefault {
1615                    prop: name.clone(),
1616                    source,
1617                }
1618            })?;
1619        }
1620    }
1621    for (name, event) in &descriptor.events {
1622        event
1623            .payload
1624            .validate_definition()
1625            .map_err(|source| PrimitiveError::InvalidSchema {
1626                location: format!("event `{name}`"),
1627                source,
1628            })?;
1629        let callback = format!("on_{name}");
1630        if !descriptor
1631            .props
1632            .get(&callback)
1633            .is_some_and(|field| schema_accepts_callback(&field.schema))
1634        {
1635            return Err(PrimitiveError::MissingEventCallback {
1636                event: name.clone(),
1637                prop: callback,
1638            });
1639        }
1640    }
1641    ComponentStateSchema::new(descriptor.state.fields().clone())
1642        .map_err(|source| PrimitiveError::InvalidStateSchema(source.to_string()))?;
1643    if let Some(effect) = &descriptor.effect {
1644        if effect.platforms.is_empty() || !effect.platforms.contains(&PrimitivePlatform::current())
1645        {
1646            return Err(PrimitiveError::UnsupportedEffectPlatform {
1647                primitive: descriptor.id.clone(),
1648                platform: PrimitivePlatform::current(),
1649            });
1650        }
1651        if effect.max_instances == 0 || effect.max_cost_per_instance == 0 {
1652            return Err(PrimitiveError::InvalidEffectBudget(descriptor.id.clone()));
1653        }
1654        if !descriptor.lifecycle {
1655            return Err(PrimitiveError::EffectRequiresLifecycle(
1656                descriptor.id.clone(),
1657            ));
1658        }
1659    }
1660    Ok(())
1661}
1662
1663fn schema_accepts_callback(schema: &ValueSchema) -> bool {
1664    matches!(schema, ValueSchema::Callback)
1665        || matches!(schema, ValueSchema::Optional { value } if schema_accepts_callback(value))
1666        || matches!(schema, ValueSchema::OneOf { variants } if variants.iter().any(schema_accepts_callback))
1667}
1668
1669fn convert_props(
1670    schema: &BTreeMap<String, ObjectField>,
1671    values: &Map,
1672    generation: ScriptGeneration,
1673) -> Result<PrimitiveProps, PrimitiveError> {
1674    let mut converted = BTreeMap::new();
1675    for (name, field) in schema {
1676        let value = values
1677            .get(name.as_str())
1678            .cloned()
1679            .or_else(|| field.default.clone().map(UiValue::into_dynamic));
1680        if let Some(value) = value {
1681            converted.insert(
1682                name.clone(),
1683                convert_prop(&field.schema, value, generation).map_err(|source| {
1684                    PrimitiveError::PropConversion {
1685                        prop: name.clone(),
1686                        source,
1687                    }
1688                })?,
1689            );
1690        }
1691    }
1692    Ok(PrimitiveProps(converted))
1693}
1694
1695fn convert_prop(
1696    schema: &ValueSchema,
1697    value: Dynamic,
1698    generation: ScriptGeneration,
1699) -> Result<PrimitiveValue, PrimitivePropConversionError> {
1700    match schema {
1701        ValueSchema::Optional { value: inner } if value.is_unit() => {
1702            Ok(PrimitiveValue::Data(UiValue::Null))
1703        }
1704        ValueSchema::Optional { value: inner } => convert_prop(inner, value, generation),
1705        ValueSchema::OneOf { variants } => {
1706            let branch = variants
1707                .iter()
1708                .find(|variant| variant.validate(&value).is_ok())
1709                .expect("validated primitive one_of prop matches one branch");
1710            convert_prop(branch, value, generation)
1711        }
1712        ValueSchema::Node => Ok(PrimitiveValue::Node(Box::new(value.cast::<UiNode>()))),
1713        ValueSchema::Callback if value.is::<FnPtr>() => {
1714            Ok(PrimitiveValue::Callback(UiEventHandler::Script(
1715                ScriptCallback::try_from_fn_ptr(value.cast::<FnPtr>(), generation)?,
1716            )))
1717        }
1718        ValueSchema::Callback => Ok(PrimitiveValue::Callback(UiEventHandler::Native(
1719            value.cast::<crate::NativeHandlerRef>(),
1720        ))),
1721        ValueSchema::Array { items, .. } if matches!(items.as_ref(), ValueSchema::Node) => {
1722            Ok(PrimitiveValue::Nodes(
1723                value
1724                    .cast::<Array>()
1725                    .into_iter()
1726                    .map(Dynamic::cast::<UiNode>)
1727                    .collect(),
1728            ))
1729        }
1730        ValueSchema::Style => Ok(PrimitiveValue::Style(Box::new(value.cast::<Style>()))),
1731        ValueSchema::Length => Ok(PrimitiveValue::Length(value.cast::<Length>())),
1732        ValueSchema::Asset => Ok(PrimitiveValue::Asset(value.cast::<AssetId>())),
1733        ValueSchema::Signal => Ok(PrimitiveValue::Signal(value.cast::<crate::NativeSignal>())),
1734        ValueSchema::Ref => Ok(PrimitiveValue::Ref(value.cast::<crate::ElementRef>())),
1735        ValueSchema::Document => Ok(PrimitiveValue::Document(
1736            value.cast::<crate::NativeTextDocument>(),
1737        )),
1738        #[cfg(feature = "charts")]
1739        ValueSchema::ChartData => Ok(PrimitiveValue::ChartData(
1740            value.cast::<crate::NativeChartData>(),
1741        )),
1742        _ => UiValue::from_dynamic(value)
1743            .map(PrimitiveValue::Data)
1744            .map_err(Into::into),
1745    }
1746}
1747
1748#[derive(Debug, Error)]
1749pub enum PrimitivePropConversionError {
1750    #[error(transparent)]
1751    Value(#[from] UiValueError),
1752    #[error(transparent)]
1753    Callback(#[from] crate::ScriptCallbackDefinitionError),
1754}
1755
1756#[derive(Debug, Error)]
1757pub enum PrimitiveError {
1758    #[error("primitive ID `{0}` must be `namespace.snake_case_name`")]
1759    InvalidId(String),
1760    #[error("primitive export `{0}` must be PascalCase")]
1761    InvalidExport(String),
1762    #[error("primitive prop `{0}` must be `snake_case`")]
1763    InvalidPropName(String),
1764    #[error("invalid schema definition for primitive {location}: {source}")]
1765    InvalidSchema {
1766        location: String,
1767        source: SchemaDefinitionError,
1768    },
1769    #[error("invalid primitive state schema: {0}")]
1770    InvalidStateSchema(String),
1771    #[error("primitive registry is already borrowed during rendering")]
1772    Borrowed,
1773    #[error("primitive `{0:?}` is already registered")]
1774    Duplicate(PrimitiveId),
1775    #[error("primitive export `{namespace}::{export}` is already registered")]
1776    DuplicateExport { namespace: String, export: String },
1777    #[error("primitive `{0:?}` is not registered")]
1778    Unknown(PrimitiveId),
1779    #[error("primitive `{0:?}` requires a stable key")]
1780    MissingKey(PrimitiveId),
1781    #[error("primitive `{0:?}` requires a retained NodeId renderer")]
1782    MissingRetainedIdentity(PrimitiveId),
1783    #[error("primitive accessibility action targeted stale instance {0:?}")]
1784    MissingInstance(PrimitiveInstanceId),
1785    #[error("primitive event emitter outlived its registry")]
1786    RegistryReleased,
1787    #[error("props for primitive `{primitive:?}` are invalid: {source}")]
1788    InvalidProps {
1789        primitive: PrimitiveId,
1790        source: SchemaValidationError,
1791    },
1792    #[error("default for primitive prop `{prop}` is invalid: {source}")]
1793    InvalidDefault {
1794        prop: String,
1795        source: SchemaValidationError,
1796    },
1797    #[error("primitive prop `{prop}` cannot cross the runtime boundary: {source}")]
1798    PropConversion {
1799        prop: String,
1800        source: PrimitivePropConversionError,
1801    },
1802    #[error("primitive event `{event}` requires callback prop `{prop}`")]
1803    MissingEventCallback { event: String, prop: String },
1804    #[error("primitive `{primitive:?}` does not declare event `{event}`")]
1805    UnknownEvent {
1806        primitive: PrimitiveId,
1807        event: String,
1808    },
1809    #[error("primitive `{primitive:?}` event `{event}` is invalid: {source}")]
1810    InvalidEvent {
1811        primitive: PrimitiveId,
1812        event: String,
1813        source: SchemaValidationError,
1814    },
1815    #[error("primitive `{primitive:?}` handler failed: {message}")]
1816    Handler {
1817        primitive: PrimitiveId,
1818        message: String,
1819    },
1820    #[error("primitive `{primitive:?}` panicked during {phase}")]
1821    Panic {
1822        primitive: PrimitiveId,
1823        phase: &'static str,
1824    },
1825    #[error("effect primitive `{primitive:?}` does not support {platform:?}")]
1826    UnsupportedEffectPlatform {
1827        primitive: PrimitiveId,
1828        platform: PrimitivePlatform,
1829    },
1830    #[error("effect primitive `{0:?}` must declare positive instance and cost budgets")]
1831    InvalidEffectBudget(PrimitiveId),
1832    #[error("effect primitive `{0:?}` must opt into scoped lifecycle")]
1833    EffectRequiresLifecycle(PrimitiveId),
1834    #[error("effect primitive `{primitive:?}` instance budget exceeded: {actual} > {limit}")]
1835    EffectInstanceBudget {
1836        primitive: PrimitiveId,
1837        actual: usize,
1838        limit: usize,
1839    },
1840    #[error("effect primitive `{primitive:?}` cost budget exceeded: {actual} > {limit}")]
1841    EffectCostBudget {
1842        primitive: PrimitiveId,
1843        actual: usize,
1844        limit: usize,
1845    },
1846    #[error(transparent)]
1847    Resource(#[from] PrimitiveResourceError),
1848}
1849
1850#[cfg(test)]
1851mod tests {
1852    use super::*;
1853    use crate::{ObjectField, StateField};
1854    use std::cell::Cell;
1855
1856    struct TestHandler;
1857
1858    struct TestTheme;
1859
1860    impl ColorResolver for TestTheme {
1861        fn resolve(&self, color: &ColorValue) -> Option<Rgba8> {
1862            match color {
1863                ColorValue::Token(token)
1864                    if matches!(token.as_str(), "accent" | "selection" | "table.selection") =>
1865                {
1866                    Some(Rgba8::from_rgba_hex(0x1234_56ff))
1867                }
1868                _ => None,
1869            }
1870        }
1871
1872        fn resolve_length(&self, length: Length) -> Option<Length> {
1873            match length {
1874                Length::ThemeSpacing(SpacingToken::Xxs) => Some(Length::Pixels(2.0)),
1875                Length::ThemeSpacing(SpacingToken::Sm) => Some(Length::Pixels(6.0)),
1876                _ => None,
1877            }
1878        }
1879
1880        fn resolve_typography(&self, role: &str) -> Option<crate::ResolvedTypography> {
1881            (role == "body").then(|| crate::ResolvedTypography {
1882                family: Some("JetBrains Mono".to_owned()),
1883                fallbacks: vec!["PingFang SC".to_owned()],
1884                size: Length::Pixels(12.0),
1885                line_height: Length::Pixels(16.0),
1886                weight: 400,
1887            })
1888        }
1889    }
1890
1891    impl PrimitiveHandler for TestHandler {
1892        fn render(
1893            &mut self,
1894            _: &PrimitiveInstance,
1895            _: &PrimitiveEventEmitter,
1896            _: &PrimitiveTheme,
1897            _: &mut Window,
1898            _: &mut App,
1899        ) -> Result<AnyElement, String> {
1900            Ok(div().into_any_element())
1901        }
1902    }
1903
1904    #[test]
1905    fn primitive_theme_exposes_only_resolved_semantic_snapshot() {
1906        let theme = PrimitiveTheme::capture(&TestTheme);
1907        assert_eq!(
1908            theme.color("accent"),
1909            Some(Rgba8::from_rgba_hex(0x1234_56ff))
1910        );
1911        assert_eq!(theme.color("unknown"), None);
1912        assert_eq!(
1913            theme.color("selection"),
1914            Some(Rgba8::from_rgba_hex(0x1234_56ff))
1915        );
1916        assert_eq!(
1917            theme.color("table.selection"),
1918            Some(Rgba8::from_rgba_hex(0x1234_56ff))
1919        );
1920        assert_eq!(
1921            theme.resolve_color(&ColorValue::Literal(Rgba8::from_rgba_hex(0xaabb_ccdd))),
1922            Some(Rgba8::from_rgba_hex(0xaabb_ccdd))
1923        );
1924        assert_eq!(
1925            theme.resolve_length(Length::ThemeSpacing(SpacingToken::Sm)),
1926            Some(Length::Pixels(6.0))
1927        );
1928        assert_eq!(
1929            theme.resolve_length(Length::ThemeSpacing(SpacingToken::Xxs)),
1930            Some(Length::Pixels(2.0))
1931        );
1932        assert_eq!(
1933            theme.typography("body").unwrap().family.as_deref(),
1934            Some("JetBrains Mono")
1935        );
1936
1937        let engine = crate::RuntimeEngine::new();
1938        let loaded = crate::load_theme_source(
1939            engine.engine(),
1940            "default_light.rhai",
1941            include_str!("../../../registry/themes/default_light.rhai"),
1942        )
1943        .unwrap();
1944        let captured = PrimitiveTheme::capture(&loaded);
1945        assert_eq!(
1946            captured.color("table.selection"),
1947            loaded.tokens.color("table.selection")
1948        );
1949        assert!(captured.color("table.selection").is_some());
1950    }
1951
1952    fn descriptor() -> PrimitiveDescriptor {
1953        PrimitiveDescriptor {
1954            id: PrimitiveId::parse("my_app.code_editor").unwrap(),
1955            export: "CodeEditor".to_owned(),
1956            props: BTreeMap::from([
1957                (
1958                    "value".to_owned(),
1959                    ObjectField::required(ValueSchema::string()),
1960                ),
1961                (
1962                    "on_change".to_owned(),
1963                    ObjectField::optional(ValueSchema::optional(ValueSchema::Callback)),
1964                ),
1965            ]),
1966            events: BTreeMap::from([(
1967                "change".to_owned(),
1968                EventSchema {
1969                    payload: ValueSchema::string(),
1970                },
1971            )]),
1972            state: ComponentStateSchema::new(BTreeMap::from([(
1973                "selection".to_owned(),
1974                StateField::new(ValueSchema::integer(), UiValue::Integer(0)),
1975            )]))
1976            .unwrap(),
1977            lifecycle: true,
1978            effect: None,
1979        }
1980    }
1981
1982    #[test]
1983    fn effect_descriptors_require_current_platform_lifecycle_and_budgets() {
1984        let mut descriptor = descriptor();
1985        descriptor.effect = Some(EffectPrimitiveDescriptor {
1986            platforms: BTreeSet::from([PrimitivePlatform::current()]),
1987            max_instances: 8,
1988            max_cost_per_instance: 4_096,
1989            reduced_motion: true,
1990            quality_tiers: true,
1991        });
1992        validate_descriptor(&descriptor).unwrap();
1993        descriptor.lifecycle = false;
1994        assert!(matches!(
1995            validate_descriptor(&descriptor),
1996            Err(PrimitiveError::EffectRequiresLifecycle(_))
1997        ));
1998    }
1999
2000    #[test]
2001    fn custom_primitive_props_and_keys_are_validated() {
2002        let registry = PrimitiveRegistry::new();
2003        let descriptor = descriptor();
2004        let id = descriptor.id.clone();
2005        registry.register(descriptor, TestHandler).unwrap();
2006        assert!(matches!(
2007            registry.create_node(
2008                &id,
2009                None,
2010                &Map::from_iter([("value".into(), Dynamic::from("source"))]),
2011                ScriptGeneration::initial(),
2012            ),
2013            Err(PrimitiveError::MissingKey(_))
2014        ));
2015        registry
2016            .create_node(
2017                &id,
2018                Some("editor".to_owned()),
2019                &Map::from_iter([("value".into(), Dynamic::from("source"))]),
2020                ScriptGeneration::initial(),
2021            )
2022            .unwrap();
2023    }
2024
2025    #[test]
2026    fn custom_primitive_events_are_normalized() {
2027        let registry = PrimitiveRegistry::new();
2028        let descriptor = descriptor();
2029        let id = descriptor.id.clone();
2030        registry.register(descriptor, TestHandler).unwrap();
2031        assert_eq!(
2032            registry
2033                .normalize_event(&id, "change", UiValue::String("new".to_owned()))
2034                .unwrap(),
2035            UiValue::String("new".to_owned())
2036        );
2037        assert!(matches!(
2038            registry.normalize_event(&id, "change", UiValue::Bool(true)),
2039            Err(PrimitiveError::InvalidEvent { .. })
2040        ));
2041    }
2042
2043    #[test]
2044    fn native_panics_are_converted_to_primitive_errors() {
2045        let id = PrimitiveId::parse("my_app.crash").unwrap();
2046        assert!(matches!(
2047            guard_primitive_panic(&id, "render", || panic!("boom")),
2048            Err(PrimitiveError::Panic {
2049                phase: "render",
2050                ..
2051            })
2052        ));
2053    }
2054
2055    #[test]
2056    fn primitive_resource_scope_rolls_back_and_continues_after_cleanup_panic() {
2057        let scope = PrimitiveResourceScope::new();
2058        let retained = Rc::new(Cell::new(0));
2059        let retained_cleanup = Rc::clone(&retained);
2060        scope
2061            .own("retained", move || retained_cleanup.set(1))
2062            .unwrap();
2063        let checkpoint = scope.checkpoint().unwrap();
2064        let order = Rc::new(RefCell::new(Vec::new()));
2065        let first = Rc::clone(&order);
2066        scope
2067            .own("first", move || first.borrow_mut().push(1))
2068            .unwrap();
2069        scope.own("panic", || panic!("cleanup failed")).unwrap();
2070        let last = Rc::clone(&order);
2071        scope
2072            .own("last", move || last.borrow_mut().push(3))
2073            .unwrap();
2074
2075        assert!(matches!(
2076            scope.rollback(checkpoint),
2077            Err(PrimitiveResourceError::CleanupPanic { ref label }) if label == "panic"
2078        ));
2079        assert_eq!(*order.borrow(), vec![3, 1]);
2080        assert_eq!(scope.active_count(), 1);
2081        scope.close().unwrap();
2082        assert_eq!(retained.get(), 1);
2083    }
2084
2085    #[test]
2086    fn successful_tree_cleanup_unmounts_removed_keyed_instances() {
2087        struct UnmountCounter(Rc<Cell<usize>>);
2088        impl PrimitiveHandler for UnmountCounter {
2089            fn render(
2090                &mut self,
2091                _: &PrimitiveInstance,
2092                _: &PrimitiveEventEmitter,
2093                _: &PrimitiveTheme,
2094                _: &mut Window,
2095                _: &mut App,
2096            ) -> Result<AnyElement, String> {
2097                Ok(div().into_any_element())
2098            }
2099
2100            fn unmount(&mut self, _: &PrimitiveInstanceId) {
2101                self.0.set(self.0.get() + 1);
2102            }
2103        }
2104
2105        let registry = PrimitiveRegistry::new();
2106        let descriptor = descriptor();
2107        let node = PrimitiveNode {
2108            primitive: descriptor.id.clone(),
2109            key: Some("editor".to_owned()),
2110            props: PrimitiveProps::new(),
2111        };
2112        let mut tree = crate::RetainedUiTree::new();
2113        tree.reconcile(UiNode::custom(node.clone())).unwrap();
2114        let instance = collect_primitive_instances(&tree)
2115            .into_iter()
2116            .next()
2117            .unwrap();
2118        let unmounted = Rc::new(Cell::new(0));
2119        let cleaned = Rc::new(Cell::new(0));
2120        let resources = PrimitiveResourceScope::new();
2121        let cleanup = Rc::clone(&cleaned);
2122        resources
2123            .own("watcher", move || cleanup.set(cleanup.get() + 1))
2124            .unwrap();
2125        registry
2126            .register(descriptor, UnmountCounter(Rc::clone(&unmounted)))
2127            .unwrap();
2128        registry.inner.borrow_mut().mounted.insert(
2129            instance.clone(),
2130            PrimitiveInstance {
2131                id: Some(instance.clone()),
2132                node,
2133                resources: Some(resources),
2134                focus_handle: None,
2135            },
2136        );
2137        tree.reconcile(UiNode::text("removed")).unwrap();
2138        registry.retain_tree(&tree).unwrap();
2139        assert_eq!(unmounted.get(), 1);
2140        assert_eq!(cleaned.get(), 1);
2141    }
2142
2143    #[test]
2144    fn primitive_identity_uses_retained_node_not_component_local_key() {
2145        let primitive = PrimitiveId::parse("my_app.editor").unwrap();
2146        let branch = |branch: &str| {
2147            UiNode::box_node(vec![UiNode::custom(PrimitiveNode {
2148                primitive: primitive.clone(),
2149                key: Some("editor".to_owned()),
2150                props: PrimitiveProps::new().with(
2151                    "branch",
2152                    PrimitiveValue::Data(UiValue::String(branch.to_owned())),
2153                ),
2154            })])
2155            .with_key(branch)
2156        };
2157        let mut tree = crate::RetainedUiTree::new();
2158        tree.reconcile(UiNode::box_node(vec![branch("left"), branch("right")]))
2159            .unwrap();
2160        let before = collect_primitive_instances(&tree);
2161        assert_eq!(before.len(), 2);
2162        assert!(before.iter().all(|instance| instance.key() == "editor"));
2163        assert_eq!(
2164            before
2165                .iter()
2166                .map(PrimitiveInstanceId::node)
2167                .collect::<BTreeSet<_>>()
2168                .len(),
2169            2
2170        );
2171
2172        tree.reconcile(UiNode::box_node(vec![branch("right"), branch("left")]))
2173            .unwrap();
2174        assert_eq!(collect_primitive_instances(&tree), before);
2175    }
2176
2177    #[test]
2178    fn primitive_event_emitter_holds_only_a_weak_registry_reference() {
2179        let registry = PrimitiveRegistry::new();
2180        let weak = Rc::downgrade(&registry.inner);
2181        let emitter = PrimitiveEventEmitter {
2182            registry: Rc::downgrade(&registry.inner),
2183            primitive: PrimitiveId::parse("my_app.editor").unwrap(),
2184            callbacks: BTreeMap::new(),
2185            dispatcher: None,
2186        };
2187        assert_eq!(Rc::strong_count(&registry.inner), 1);
2188        drop(registry);
2189        assert!(weak.upgrade().is_none());
2190        drop(emitter);
2191    }
2192}