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, Rgba8, SchemaDefinitionError, SchemaValidationError,
15    ScriptCallback, ScriptGeneration, Style, UiEventHandler, UiNode, UiValue, UiValueError,
16    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    tokens: Option<std::sync::Arc<crate::ThemeTokens>>,
163    colors: BTreeMap<String, Rgba8>,
164    environment: crate::Environment,
165    inherited_disabled: bool,
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
175impl Default for PrimitiveTheme {
176    fn default() -> Self {
177        Self {
178            tokens: None,
179            colors: BTreeMap::new(),
180            environment: crate::Environment::EMPTY,
181            inherited_disabled: false,
182            direction: crate::TextDirection::LeftToRight,
183            locale: "en".to_owned(),
184            number: None,
185            motion: crate::ThemeMotion::default(),
186            motion_preference: crate::MotionPreference::Normal,
187            motion_quality: crate::MotionQuality::High,
188            clock: crate::RuntimeClock::default(),
189        }
190    }
191}
192
193impl PrimitiveTheme {
194    #[cfg(test)]
195    pub(crate) fn capture(colors: &impl ColorResolver) -> Self {
196        Self::capture_with_direction(colors, crate::TextDirection::LeftToRight)
197    }
198
199    #[cfg(test)]
200    pub(crate) fn capture_with_direction(
201        colors: &impl ColorResolver,
202        direction: crate::TextDirection,
203    ) -> Self {
204        Self::capture_with_motion_policy(
205            colors,
206            direction,
207            crate::MotionPreference::Normal,
208            crate::MotionQuality::High,
209        )
210    }
211
212    #[cfg(test)]
213    pub(crate) fn capture_with_motion_policy(
214        colors: &impl ColorResolver,
215        direction: crate::TextDirection,
216        motion_preference: crate::MotionPreference,
217        motion_quality: crate::MotionQuality,
218    ) -> Self {
219        Self::capture_with_environment(
220            colors,
221            direction,
222            "en",
223            None,
224            crate::RuntimeClock::default(),
225            motion_preference,
226            motion_quality,
227        )
228    }
229
230    pub(crate) fn capture_with_environment(
231        colors: &(impl ColorResolver + ?Sized),
232        direction: crate::TextDirection,
233        locale: &str,
234        number: Option<&crate::NumberMetadata>,
235        clock: crate::RuntimeClock,
236        motion_preference: crate::MotionPreference,
237        motion_quality: crate::MotionQuality,
238    ) -> Self {
239        let tokens = colors.token_set();
240        Self {
241            colors: if tokens.is_some() {
242                BTreeMap::new()
243            } else {
244                colors.color_snapshot()
245            },
246            tokens,
247            environment: colors.environment(),
248            inherited_disabled: false,
249            direction,
250            locale: locale.to_owned(),
251            number: number.cloned(),
252            motion: colors.resolve_motion(),
253            motion_preference,
254            motion_quality,
255            clock,
256        }
257    }
258
259    #[must_use]
260    pub(crate) const fn with_inherited_disabled(mut self, disabled: bool) -> Self {
261        self.inherited_disabled = disabled;
262        self
263    }
264
265    #[must_use]
266    pub fn color(&self, token: &str) -> Option<Rgba8> {
267        match &self.tokens {
268            Some(tokens) => tokens.color(token),
269            None => self.colors.get(token).copied(),
270        }
271    }
272
273    #[must_use]
274    pub fn resolve_color(&self, value: &ColorValue) -> Option<Rgba8> {
275        value.resolve_with(&mut |token| self.color(token))
276    }
277
278    #[must_use]
279    pub fn resolve_length(&self, value: Length) -> Option<Length> {
280        match &self.tokens {
281            Some(tokens) => tokens.resolve_length(value, &self.environment),
282            None => (!value.is_theme_token()).then_some(value),
283        }
284    }
285
286    /// The environment inherited by this primitive.
287    #[must_use]
288    pub const fn environment(&self) -> crate::Environment {
289        self.environment
290    }
291
292    /// Whether an ancestor disabled this subtree. Primitives treat it like
293    /// their own `disabled` prop.
294    #[must_use]
295    pub const fn inherited_disabled(&self) -> bool {
296        self.inherited_disabled
297    }
298
299    #[must_use]
300    pub const fn direction(&self) -> crate::TextDirection {
301        self.direction
302    }
303
304    #[must_use]
305    pub fn locale(&self) -> &str {
306        &self.locale
307    }
308
309    #[must_use]
310    pub const fn number_metadata(&self) -> Option<&crate::NumberMetadata> {
311        self.number.as_ref()
312    }
313
314    #[must_use]
315    pub const fn motion_preference(&self) -> crate::MotionPreference {
316        self.motion_preference
317    }
318
319    #[must_use]
320    pub const fn motion_quality(&self) -> crate::MotionQuality {
321        self.motion_quality
322    }
323
324    #[must_use]
325    pub fn now(&self) -> std::time::Instant {
326        self.clock.now()
327    }
328
329    #[must_use]
330    pub const fn motion(&self) -> &crate::ThemeMotion {
331        &self.motion
332    }
333
334    #[must_use]
335    pub fn typography(&self, role: &str) -> Option<crate::ResolvedTypography> {
336        self.tokens
337            .as_ref()
338            .and_then(|tokens| tokens.resolve_typography(role, &self.environment))
339    }
340}
341
342impl ColorResolver for PrimitiveTheme {
343    fn resolve_token(&self, token: &str) -> Option<Rgba8> {
344        self.color(token)
345    }
346
347    fn color_snapshot(&self) -> BTreeMap<String, Rgba8> {
348        match &self.tokens {
349            Some(tokens) => tokens.color_snapshot(),
350            None => self.colors.clone(),
351        }
352    }
353
354    fn environment(&self) -> crate::Environment {
355        self.environment
356    }
357
358    fn resolve_length_in(
359        &self,
360        length: Length,
361        environment: &crate::Environment,
362    ) -> Option<Length> {
363        match &self.tokens {
364            Some(tokens) => tokens.resolve_length(length, environment),
365            None => (!length.is_theme_token()).then_some(length),
366        }
367    }
368
369    fn resolve_typography_in(
370        &self,
371        role: &str,
372        environment: &crate::Environment,
373    ) -> Option<crate::ResolvedTypography> {
374        self.tokens
375            .as_ref()
376            .and_then(|tokens| tokens.resolve_typography(role, environment))
377    }
378
379    fn token_set(&self) -> Option<std::sync::Arc<crate::ThemeTokens>> {
380        self.tokens.clone()
381    }
382
383    fn resolve_motion(&self) -> crate::ThemeMotion {
384        self.motion.clone()
385    }
386}
387
388#[derive(Clone, Debug, Default, PartialEq)]
389pub struct PrimitiveProps(BTreeMap<String, PrimitiveValue>);
390
391impl PrimitiveProps {
392    #[must_use]
393    pub fn new() -> Self {
394        Self::default()
395    }
396
397    #[must_use]
398    pub fn get(&self, name: &str) -> Option<&PrimitiveValue> {
399        self.0.get(name)
400    }
401
402    #[must_use]
403    pub fn data(&self, name: &str) -> Option<&UiValue> {
404        match self.get(name) {
405            Some(PrimitiveValue::Data(value)) => Some(value),
406            _ => None,
407        }
408    }
409
410    #[must_use]
411    pub fn number(&self, name: &str) -> Option<f64> {
412        match self.data(name) {
413            Some(UiValue::Float(value)) => Some(*value),
414            Some(UiValue::Integer(value)) => value.to_string().parse().ok(),
415            _ => None,
416        }
417    }
418
419    #[must_use]
420    pub fn integer(&self, name: &str) -> Option<i64> {
421        match self.data(name) {
422            Some(UiValue::Integer(value)) => Some(*value),
423            _ => None,
424        }
425    }
426
427    #[must_use]
428    pub fn usize(&self, name: &str) -> Option<usize> {
429        self.integer(name)
430            .and_then(|value| usize::try_from(value).ok())
431    }
432
433    #[must_use]
434    pub fn string(&self, name: &str) -> Option<&str> {
435        match self.data(name) {
436            Some(UiValue::String(value)) => Some(value),
437            _ => None,
438        }
439    }
440
441    #[must_use]
442    pub fn boolean(&self, name: &str) -> Option<bool> {
443        match self.data(name) {
444            Some(UiValue::Bool(value)) => Some(*value),
445            _ => None,
446        }
447    }
448
449    #[must_use]
450    pub fn style(&self, name: &str) -> Option<&Style> {
451        match self.get(name) {
452            Some(PrimitiveValue::Style(style)) => Some(style),
453            _ => None,
454        }
455    }
456
457    #[must_use]
458    pub fn signal(&self, name: &str) -> Option<&crate::NativeSignal> {
459        match self.get(name) {
460            Some(PrimitiveValue::Signal(signal)) => Some(signal),
461            _ => None,
462        }
463    }
464
465    #[must_use]
466    pub fn element_ref(&self, name: &str) -> Option<&crate::ElementRef> {
467        match self.get(name) {
468            Some(PrimitiveValue::Ref(reference)) => Some(reference),
469            _ => None,
470        }
471    }
472
473    #[must_use]
474    pub fn document(&self, name: &str) -> Option<&crate::NativeTextDocument> {
475        match self.get(name) {
476            Some(PrimitiveValue::Document(document)) => Some(document),
477            _ => None,
478        }
479    }
480
481    pub fn insert(
482        &mut self,
483        name: impl Into<String>,
484        value: PrimitiveValue,
485    ) -> Option<PrimitiveValue> {
486        self.0.insert(name.into(), value)
487    }
488
489    #[must_use]
490    pub fn with(mut self, name: impl Into<String>, value: PrimitiveValue) -> Self {
491        self.insert(name, value);
492        self
493    }
494
495    pub fn iter(&self) -> impl Iterator<Item = (&str, &PrimitiveValue)> {
496        self.0.iter().map(|(name, value)| (name.as_str(), value))
497    }
498
499    pub(crate) fn iter_mut(&mut self) -> impl Iterator<Item = (&str, &mut PrimitiveValue)> {
500        self.0
501            .iter_mut()
502            .map(|(name, value)| (name.as_str(), value))
503    }
504
505    pub(crate) fn bind_component_scope(
506        &mut self,
507        component: &crate::ComponentInstancePath,
508        incarnation: crate::ComponentIncarnation,
509        events: &BTreeMap<String, EventSchema>,
510        native_context: Option<&crate::invocation::ScriptInvocationContext>,
511    ) {
512        for value in self.0.values_mut() {
513            match value {
514                PrimitiveValue::Callback(callback) => {
515                    if let Some(callback) = callback.as_script_mut() {
516                        callback.bind_component_scope_if_unset(
517                            component,
518                            incarnation,
519                            events.clone(),
520                        );
521                        if let Some(context) = native_context {
522                            callback.bind_native_context_if_unset(context.clone());
523                        }
524                    }
525                }
526                PrimitiveValue::Node(node) => {
527                    node.bind_component_scope(component, incarnation, events, native_context);
528                }
529                PrimitiveValue::Nodes(nodes) => {
530                    for node in nodes {
531                        node.bind_component_scope(component, incarnation, events, native_context);
532                    }
533                }
534                PrimitiveValue::Data(_)
535                | PrimitiveValue::Style(_)
536                | PrimitiveValue::Length(_)
537                | PrimitiveValue::Asset(_)
538                | PrimitiveValue::Signal(_)
539                | PrimitiveValue::Ref(_)
540                | PrimitiveValue::Document(_) => {}
541                #[cfg(feature = "charts")]
542                PrimitiveValue::ChartData(_) => {}
543            }
544        }
545    }
546}
547
548#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
549pub struct PrimitiveInstanceId {
550    primitive: PrimitiveId,
551    key: String,
552    node: crate::NodeId,
553}
554
555impl PrimitiveInstanceId {
556    pub(crate) fn new(primitive: PrimitiveId, key: String, node: crate::NodeId) -> Self {
557        Self {
558            primitive,
559            key,
560            node,
561        }
562    }
563
564    #[must_use]
565    pub const fn primitive(&self) -> &PrimitiveId {
566        &self.primitive
567    }
568
569    #[must_use]
570    pub fn key(&self) -> &str {
571        &self.key
572    }
573
574    #[must_use]
575    pub const fn node(&self) -> crate::NodeId {
576        self.node
577    }
578}
579
580#[derive(Clone, Debug, PartialEq)]
581pub struct PrimitiveNode {
582    pub primitive: PrimitiveId,
583    pub key: Option<String>,
584    pub props: PrimitiveProps,
585}
586
587#[derive(Clone, Debug)]
588pub struct PrimitiveInstance {
589    pub id: Option<PrimitiveInstanceId>,
590    pub node: PrimitiveNode,
591    resources: Option<PrimitiveResourceScope>,
592    focus_handle: Option<gpui::FocusHandle>,
593}
594
595impl PrimitiveInstance {
596    /// Return the runtime-owned cancellation scope for a retained instance.
597    /// Ephemeral primitives have no resource scope and must not start durable
598    /// work from render.
599    #[must_use]
600    pub const fn resources(&self) -> Option<&PrimitiveResourceScope> {
601        self.resources.as_ref()
602    }
603
604    /// Return the runtime-owned focus identity for this native control.
605    ///
606    /// A focus-owning primitive must use this handle instead of allocating a
607    /// second identity so its declarative wrapper, automation target, and
608    /// native editor all observe the same focus state.
609    #[must_use]
610    pub const fn focus_handle(&self) -> Option<&gpui::FocusHandle> {
611        self.focus_handle.as_ref()
612    }
613}
614
615#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
616pub struct PrimitiveResourceHandle(u64);
617
618struct PrimitiveResourceEntry {
619    label: String,
620    cleanup: Option<Box<dyn FnOnce()>>,
621}
622
623#[derive(Default)]
624struct PrimitiveResourceState {
625    next_id: u64,
626    entries: BTreeMap<u64, PrimitiveResourceEntry>,
627}
628
629impl Drop for PrimitiveResourceState {
630    fn drop(&mut self) {
631        let entries = std::mem::take(&mut self.entries);
632        for entry in entries.into_values().rev() {
633            let _ = run_resource_cleanup(entry);
634        }
635    }
636}
637
638#[derive(Clone, Default)]
639pub struct PrimitiveResourceScope {
640    inner: Rc<RefCell<PrimitiveResourceState>>,
641}
642
643impl fmt::Debug for PrimitiveResourceScope {
644    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
645        match self.inner.try_borrow() {
646            Ok(state) => formatter
647                .debug_struct("PrimitiveResourceScope")
648                .field(
649                    "active",
650                    &state
651                        .entries
652                        .values()
653                        .map(|entry| entry.label.as_str())
654                        .collect::<Vec<_>>(),
655                )
656                .finish(),
657            Err(_) => formatter.write_str("PrimitiveResourceScope(<borrowed>)"),
658        }
659    }
660}
661
662impl PrimitiveResourceScope {
663    #[must_use]
664    pub fn new() -> Self {
665        Self::default()
666    }
667
668    /// Own one native task/subscription/resource cancellation callback.
669    ///
670    /// # Errors
671    ///
672    /// Returns [`PrimitiveResourceError`] for an unsafe label or conflicting
673    /// scope borrow.
674    pub fn own(
675        &self,
676        label: impl Into<String>,
677        cleanup: impl FnOnce() + 'static,
678    ) -> Result<PrimitiveResourceHandle, PrimitiveResourceError> {
679        let label = label.into();
680        if label.is_empty()
681            || label.len() > 128
682            || !label.chars().all(|character| {
683                character.is_ascii_alphanumeric() || matches!(character, '_' | '-' | '.' | ':')
684            })
685        {
686            return Err(PrimitiveResourceError::InvalidLabel(label));
687        }
688        let mut state = self
689            .inner
690            .try_borrow_mut()
691            .map_err(|_| PrimitiveResourceError::Borrowed)?;
692        let id = state.next_id.max(1);
693        state.next_id = id
694            .checked_add(1)
695            .ok_or(PrimitiveResourceError::IdExhausted)?;
696        state.entries.insert(
697            id,
698            PrimitiveResourceEntry {
699                label,
700                cleanup: Some(Box::new(cleanup)),
701            },
702        );
703        Ok(PrimitiveResourceHandle(id))
704    }
705
706    /// Cancel one owned resource early.
707    ///
708    /// # Errors
709    ///
710    /// Returns a borrow or cleanup-panic diagnostic.
711    pub fn cancel(&self, handle: &PrimitiveResourceHandle) -> Result<bool, PrimitiveResourceError> {
712        let entry = self
713            .inner
714            .try_borrow_mut()
715            .map_err(|_| PrimitiveResourceError::Borrowed)?
716            .entries
717            .remove(&handle.0);
718        let Some(entry) = entry else {
719            return Ok(false);
720        };
721        run_resource_cleanup(entry)?;
722        Ok(true)
723    }
724
725    #[must_use]
726    pub fn active_count(&self) -> usize {
727        self.inner.borrow().entries.len()
728    }
729
730    fn checkpoint(&self) -> Result<u64, PrimitiveResourceError> {
731        self.inner
732            .try_borrow()
733            .map(|state| state.next_id.max(1))
734            .map_err(|_| PrimitiveResourceError::Borrowed)
735    }
736
737    fn rollback(&self, checkpoint: u64) -> Result<(), PrimitiveResourceError> {
738        self.cleanup_where(|id| id >= checkpoint)
739    }
740
741    fn close(&self) -> Result<(), PrimitiveResourceError> {
742        self.cleanup_where(|_| true)
743    }
744
745    fn cleanup_where(&self, predicate: impl Fn(u64) -> bool) -> Result<(), PrimitiveResourceError> {
746        let mut entries = {
747            let mut state = self
748                .inner
749                .try_borrow_mut()
750                .map_err(|_| PrimitiveResourceError::Borrowed)?;
751            let ids = state
752                .entries
753                .keys()
754                .copied()
755                .filter(|id| predicate(*id))
756                .collect::<Vec<_>>();
757            ids.into_iter()
758                .rev()
759                .filter_map(|id| state.entries.remove(&id))
760                .collect::<Vec<_>>()
761        };
762        let mut first_error = None;
763        for entry in entries.drain(..) {
764            if let Err(error) = run_resource_cleanup(entry)
765                && first_error.is_none()
766            {
767                first_error = Some(error);
768            }
769        }
770        first_error.map_or(Ok(()), Err)
771    }
772}
773
774fn run_resource_cleanup(mut entry: PrimitiveResourceEntry) -> Result<(), PrimitiveResourceError> {
775    let label = entry.label;
776    let Some(cleanup) = entry.cleanup.take() else {
777        return Ok(());
778    };
779    catch_unwind(AssertUnwindSafe(cleanup))
780        .map_err(|_| PrimitiveResourceError::CleanupPanic { label })
781}
782
783#[derive(Clone, Debug, Error, Eq, PartialEq)]
784pub enum PrimitiveResourceError {
785    #[error("primitive resource label `{0}` must be 1-128 safe ASCII characters")]
786    InvalidLabel(String),
787    #[error("primitive resource scope is already borrowed")]
788    Borrowed,
789    #[error("primitive resource scope exhausted its handle identity space")]
790    IdExhausted,
791    #[error("primitive resource cleanup `{label}` panicked")]
792    CleanupPanic { label: String },
793}
794
795#[derive(Clone)]
796pub struct PrimitiveContext {
797    registry: Weak<RefCell<PrimitiveRegistryInner>>,
798    primitive: PrimitiveId,
799    event_schemas: BTreeMap<String, EventSchema>,
800    callbacks: BTreeMap<String, UiEventHandler>,
801    dispatcher: Option<NodeEventDispatcher>,
802    interactions: crate::interaction::WindowInteractionCoordinator,
803    scroll_handles: Vec<gpui::ScrollHandle>,
804    view_id: String,
805    instance: Option<PrimitiveInstanceId>,
806    retained_node: Option<crate::NodeId>,
807    /// The focus handle of the node that hosts the primitive, if it has one.
808    focus_handle: Option<gpui::FocusHandle>,
809}
810
811/// A schema-checked semantic proposal produced by a native primitive policy.
812///
813/// The runtime executes this value at the owning `ScriptView` boundary so
814/// synchronous automation can observe the real callback result.
815pub struct PrimitiveSemanticProposal {
816    pub(crate) event: String,
817    pub(crate) payload: UiValue,
818    pub(crate) handler: Option<UiEventHandler>,
819}
820
821impl PrimitiveContext {
822    pub(crate) fn observes(&self, event: &str) -> bool {
823        self.callbacks.contains_key(event)
824    }
825
826    /// Prepare a semantic proposal without dispatching it yet.
827    ///
828    /// # Errors
829    ///
830    /// Returns declaration, schema, or released-registry errors.
831    pub fn prepare_proposal(
832        &self,
833        event: impl Into<String>,
834        payload: UiValue,
835    ) -> Result<PrimitiveSemanticProposal, PrimitiveError> {
836        let event = event.into();
837        let schema =
838            self.event_schemas
839                .get(&event)
840                .ok_or_else(|| PrimitiveError::UnknownEvent {
841                    primitive: self.primitive.clone(),
842                    event: event.clone(),
843                })?;
844        schema
845            .payload
846            .validate_ui_value(&payload)
847            .map_err(|source| PrimitiveError::InvalidEvent {
848                primitive: self.primitive.clone(),
849                event: event.clone(),
850                source,
851            })?;
852        Ok(PrimitiveSemanticProposal {
853            handler: self.callbacks.get(&event).cloned(),
854            event,
855            payload,
856        })
857    }
858
859    /// Normalize and dispatch a declared native primitive event.
860    ///
861    /// # Errors
862    ///
863    /// Returns schema and event declaration errors. Missing callback props are
864    /// treated as an intentionally unobserved event.
865    pub fn emit(
866        &self,
867        event: &str,
868        payload: UiValue,
869        window: &mut Window,
870        cx: &mut App,
871    ) -> Result<(), PrimitiveError> {
872        let registry = PrimitiveRegistry {
873            inner: self
874                .registry
875                .upgrade()
876                .ok_or(PrimitiveError::RegistryReleased)?,
877        };
878        let payload = registry.normalize_event(&self.primitive, event, payload)?;
879        if let Some(handler) = self.callbacks.get(event) {
880            match handler {
881                UiEventHandler::Script(callback) => {
882                    if let Some(dispatcher) = self.dispatcher.as_ref() {
883                        dispatcher.dispatch(callback.clone(), event, payload, None, window, cx);
884                    }
885                }
886                UiEventHandler::Host(callback) => {
887                    callback.invoke(payload, window, cx);
888                }
889                UiEventHandler::Native(reference) => {
890                    if let Some(dispatcher) = self.dispatcher.as_ref() {
891                        dispatcher.dispatch_native(
892                            reference.clone(),
893                            event.to_owned(),
894                            payload,
895                            None,
896                            window,
897                            cx,
898                        );
899                    }
900                }
901            }
902        }
903        Ok(())
904    }
905
906    /// Queue one low-frequency semantic proposal after the active native input
907    /// callback returns.
908    ///
909    /// Hot preview remains in native signals; Rhai or Host code observes only
910    /// the final schema-checked proposal.
911    pub fn propose(
912        &self,
913        event: impl Into<String>,
914        payload: UiValue,
915        window: &mut Window,
916        cx: &mut App,
917    ) {
918        let context = self.clone();
919        let event = event.into();
920        window.defer(cx, move |window, cx| {
921            let _ = context.emit(&event, payload, window, cx);
922        });
923    }
924
925    /// Write one primitive-owned native signal without invoking Rhai.
926    ///
927    /// # Errors
928    ///
929    /// Returns a stale or type error when the signal no longer belongs to the
930    /// mounted component instance.
931    pub fn write_signal(
932        &self,
933        signal: &crate::NativeSignal,
934        value: crate::SignalValue,
935        cx: &mut App,
936    ) -> Result<bool, crate::SignalError> {
937        self.dispatcher.as_ref().map_or_else(
938            || Err(crate::SignalError::Stale(signal.id().clone())),
939            |dispatcher| dispatcher.write_signal(signal.clone(), value, cx),
940        )
941    }
942
943    pub(crate) fn read_signal(
944        &self,
945        signal: &crate::NativeSignal,
946        cx: &App,
947    ) -> Result<crate::SignalValue, crate::SignalError> {
948        self.dispatcher.as_ref().map_or_else(
949            || Err(crate::SignalError::Stale(signal.id().clone())),
950            |dispatcher| dispatcher.read_signal(signal, cx),
951        )
952    }
953
954    /// Atomically apply one related native-preview patch and request at most
955    /// one repaint.
956    ///
957    /// # Errors
958    ///
959    /// Returns without changing any signal when a member is stale, duplicated,
960    /// non-finite, or type-incompatible.
961    pub fn write_signals(
962        &self,
963        updates: impl IntoIterator<Item = (crate::NativeSignal, crate::SignalValue)>,
964        cx: &mut App,
965    ) -> Result<bool, crate::SignalError> {
966        let updates = updates.into_iter().collect::<Vec<_>>();
967        let Some(dispatcher) = self.dispatcher.as_ref() else {
968            return updates.first().map_or(Ok(false), |(signal, _)| {
969                Err(crate::SignalError::Stale(signal.id().clone()))
970            });
971        };
972        dispatcher.write_signals(updates, cx)
973    }
974
975    pub(crate) fn interaction_owner(&self, key: &str) -> crate::interaction::InteractionOwner {
976        let owner = crate::interaction::InteractionOwner::new(
977            self.view_id.clone(),
978            format!("{}:{key}", self.primitive.as_str()),
979        );
980        if let Some(node) = self
981            .retained_node
982            .or_else(|| self.instance.as_ref().map(PrimitiveInstanceId::node))
983        {
984            owner.with_retained(node)
985        } else {
986            owner
987        }
988    }
989
990    pub(crate) fn begin_interaction(
991        &self,
992        gesture: crate::interaction::NativeGesture,
993        window: &mut Window,
994        cx: &mut App,
995    ) {
996        self.interactions.begin(gesture, window, cx);
997    }
998
999    pub(crate) fn interaction_is_active(
1000        &self,
1001        owner: &crate::interaction::InteractionOwner,
1002    ) -> bool {
1003        self.interactions.is_active(owner)
1004    }
1005
1006    pub(crate) fn present_interaction(&self, owner: crate::interaction::InteractionOwner) {
1007        self.interactions.present(owner);
1008    }
1009
1010    pub(crate) fn cancel_interaction(
1011        &self,
1012        owner: &crate::interaction::InteractionOwner,
1013        window: &mut Window,
1014        cx: &mut App,
1015    ) -> bool {
1016        self.interactions.cancel_owner(owner, window, cx)
1017    }
1018
1019    pub(crate) fn register_interaction_cancellation(
1020        &self,
1021        owner: crate::interaction::InteractionOwner,
1022        notify: gpui::EntityId,
1023        cancel: impl Fn(&mut Window, &mut App) + 'static,
1024        window: &mut Window,
1025        cx: &mut App,
1026    ) {
1027        self.interactions
1028            .register_auxiliary(owner, notify, cancel, window, cx);
1029    }
1030
1031    pub(crate) fn clear_interaction_cancellation(
1032        &self,
1033        owner: &crate::interaction::InteractionOwner,
1034    ) {
1035        self.interactions.clear_auxiliary(owner);
1036    }
1037
1038    pub(crate) fn register_drop_target(&self, target: crate::interaction::DropTargetRegistration) {
1039        self.interactions.register_drop_target(target);
1040    }
1041
1042    pub(crate) fn drop_target_state(
1043        &self,
1044        owner: &crate::interaction::InteractionOwner,
1045    ) -> crate::interaction::DropTargetState {
1046        self.interactions.drop_target_state(owner)
1047    }
1048
1049    // A drag source passes its press, threshold and both endings through unchanged.
1050    #[allow(clippy::too_many_arguments)]
1051    pub(crate) fn begin_application_drag(
1052        &self,
1053        spec: crate::interaction::ApplicationDragSpec,
1054        position: gpui::Point<gpui::Pixels>,
1055        threshold: f64,
1056        on_end: impl Fn(crate::interaction::ApplicationDropResult, bool, &mut Window, &mut App)
1057        + 'static,
1058        on_tap: Option<crate::interaction::TapHandler>,
1059        window: &mut Window,
1060        cx: &mut App,
1061    ) {
1062        let gesture = crate::interaction::application_drag_gesture(
1063            self.interactions.clone(),
1064            position,
1065            spec.notify(),
1066            spec,
1067            threshold,
1068            on_end,
1069            on_tap,
1070        );
1071        self.interactions.begin(gesture, window, cx);
1072    }
1073
1074    pub(crate) fn perform_keyboard_drop(
1075        &self,
1076        spec: &crate::interaction::ApplicationDragSpec,
1077        target_id: &str,
1078        window: &mut Window,
1079        cx: &mut App,
1080    ) -> crate::interaction::ApplicationDropResult {
1081        self.interactions
1082            .perform_keyboard_drop(spec, target_id, window, cx)
1083    }
1084
1085    pub(crate) fn app_drag_source_active(
1086        &self,
1087        owner: &crate::interaction::InteractionOwner,
1088    ) -> bool {
1089        self.interactions.app_drag_source_active(owner)
1090    }
1091
1092    pub(crate) fn ancestor_scroll_handles(&self) -> Vec<gpui::ScrollHandle> {
1093        self.scroll_handles.clone()
1094    }
1095
1096    /// Whether the node hosting the primitive has keyboard focus.
1097    #[must_use]
1098    pub fn is_focused(&self, window: &Window) -> bool {
1099        self.focus_handle
1100            .as_ref()
1101            .is_some_and(|handle| handle.is_focused(window))
1102    }
1103
1104    /// Read the last committed layout bounds for a primitive-owned element ref.
1105    #[must_use]
1106    pub fn element_bounds(
1107        &self,
1108        reference: &crate::ElementRef,
1109        cx: &App,
1110    ) -> Option<crate::GeometryBounds> {
1111        self.dispatcher
1112            .as_ref()
1113            .and_then(|dispatcher| dispatcher.element_bounds(reference, cx))
1114    }
1115
1116    /// This frame's hitbox of the node holding `reference`: hovered only where
1117    /// the node is visible and nothing occluding sits above it.
1118    pub(crate) fn element_hitbox(
1119        &self,
1120        reference: &crate::ElementRef,
1121        cx: &App,
1122    ) -> Option<gpui::HitboxId> {
1123        self.dispatcher
1124            .as_ref()
1125            .and_then(|dispatcher| dispatcher.element_hitbox(reference, cx))
1126    }
1127
1128    pub(crate) fn canvas_local_point(
1129        &self,
1130        reference: &crate::ElementRef,
1131        point: gpui::Point<gpui::Pixels>,
1132        cx: &App,
1133    ) -> Option<(f64, f64)> {
1134        self.dispatcher.as_ref().and_then(|dispatcher| {
1135            dispatcher.canvas_local_point(reference, (f64::from(point.x), f64::from(point.y)), cx)
1136        })
1137    }
1138
1139    pub(crate) fn canvas_bounds(
1140        &self,
1141        reference: &crate::ElementRef,
1142        cx: &App,
1143    ) -> Option<crate::GeometryBounds> {
1144        self.dispatcher
1145            .as_ref()
1146            .and_then(|dispatcher| dispatcher.canvas_bounds(reference, cx))
1147    }
1148}
1149
1150pub trait PrimitiveHandler {
1151    /// Return deterministic work units for this validated effect instance.
1152    /// Non-effect primitives ignore this value.
1153    fn effect_cost(&self, _instance: &PrimitiveInstance) -> usize {
1154        1
1155    }
1156
1157    /// Contribute a bounded semantic summary for the retained primitive node.
1158    /// Native internals remain private; accessibility and automation receive
1159    /// only durable, already-presented values.
1160    fn accessibility(
1161        &self,
1162        _instance: &PrimitiveInstanceId,
1163        _cx: &App,
1164    ) -> Option<PrimitiveAccessibilityProjection> {
1165        None
1166    }
1167
1168    /// Actions that the native primitive can perform for assistive technology.
1169    ///
1170    /// The default is intentionally empty: a primitive must not advertise an
1171    /// operation merely because its outer Rhai node has a compatible role.
1172    fn accessibility_actions(
1173        &self,
1174        _instance: &PrimitiveInstanceId,
1175    ) -> Vec<gpui::AccessibleAction> {
1176        Vec::new()
1177    }
1178
1179    /// Whether this primitive owns one primary native focus target.
1180    ///
1181    /// The runtime allocates and retains that identity before rendering so the
1182    /// declarative wrapper and native control share it from their first frame.
1183    fn uses_primary_focus(&self) -> bool {
1184        false
1185    }
1186
1187    /// Perform one previously advertised accessibility action.
1188    ///
1189    /// # Errors
1190    ///
1191    /// Returns a bounded diagnostic when the instance is stale, disabled, or
1192    /// the platform payload is invalid for the action.
1193    fn perform_accessibility_action(
1194        &mut self,
1195        _instance: &PrimitiveInstanceId,
1196        _action: gpui::AccessibleAction,
1197        _data: Option<&gpui::accesskit::ActionData>,
1198        _window: &mut Window,
1199        _cx: &mut App,
1200    ) -> Result<(), String> {
1201        Err("primitive does not support accessibility actions".to_owned())
1202    }
1203
1204    /// Perform a native key semantic for automation using the same policy as
1205    /// the real focused control.
1206    ///
1207    /// # Errors
1208    ///
1209    /// Returns a bounded primitive diagnostic when the native semantic cannot
1210    /// be evaluated.
1211    fn perform_key(
1212        &mut self,
1213        _instance: &PrimitiveInstanceId,
1214        _key: &str,
1215    ) -> Result<Option<PrimitiveSemanticProposal>, String> {
1216        Ok(None)
1217    }
1218
1219    /// Called once before the first render of a keyed lifecycle primitive.
1220    ///
1221    /// # Errors
1222    ///
1223    /// Returns a diagnostic message when native setup fails.
1224    fn mount(&mut self, _instance: &PrimitiveInstance) -> Result<(), String> {
1225        Ok(())
1226    }
1227
1228    /// Apply a validated prop/style/event snapshot to an existing keyed instance.
1229    ///
1230    /// # Errors
1231    ///
1232    /// Returns a diagnostic message when the retained native update fails.
1233    fn update(
1234        &mut self,
1235        _previous: &PrimitiveInstance,
1236        _next: &PrimitiveInstance,
1237    ) -> Result<(), String> {
1238        Ok(())
1239    }
1240
1241    /// Prepare one retained native instance to cross into suspended state.
1242    /// Implementations must be idempotent because a failed peer is compensated
1243    /// and the whole operation may be retried.
1244    fn suspend(&mut self, _instance: &PrimitiveInstanceId, _cx: &mut App) {}
1245
1246    /// Prepare one suspended native instance to become active. The public view
1247    /// is not marked active until every primitive and the script transaction succeed.
1248    fn resume(&mut self, _instance: &PrimitiveInstanceId, _cx: &mut App) {}
1249
1250    /// Commit a successfully prepared resume after the script transaction has
1251    /// also succeeded. Activity time and event delivery may begin here.
1252    fn commit_resume(&mut self, _instance: &PrimitiveInstanceId, _cx: &mut App) {}
1253
1254    /// Render the primitive into a native GPUI element.
1255    ///
1256    /// # Errors
1257    ///
1258    /// Returns a diagnostic message when native rendering fails.
1259    fn render(
1260        &mut self,
1261        instance: &PrimitiveInstance,
1262        events: &PrimitiveContext,
1263        theme: &PrimitiveTheme,
1264        window: &mut Window,
1265        cx: &mut App,
1266    ) -> Result<AnyElement, String>;
1267
1268    /// Called when a previously mounted keyed primitive is no longer reachable.
1269    fn unmount(&mut self, _instance: &PrimitiveInstanceId) {}
1270}
1271
1272#[derive(Clone, Debug, Default, PartialEq)]
1273pub struct PrimitiveAccessibilityProjection {
1274    pub description: String,
1275    pub value: Option<UiValue>,
1276}
1277
1278struct PrimitiveEntry {
1279    descriptor: PrimitiveDescriptor,
1280    handler: Box<dyn PrimitiveHandler>,
1281}
1282
1283#[derive(Default)]
1284struct PrimitiveRegistryInner {
1285    entries: BTreeMap<PrimitiveId, PrimitiveEntry>,
1286    mounted: BTreeMap<PrimitiveInstanceId, PrimitiveInstance>,
1287}
1288
1289#[derive(Clone, Default)]
1290pub struct PrimitiveRegistry {
1291    inner: Rc<RefCell<PrimitiveRegistryInner>>,
1292}
1293
1294impl fmt::Debug for PrimitiveRegistry {
1295    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1296        match self.inner.try_borrow() {
1297            Ok(inner) => formatter
1298                .debug_struct("PrimitiveRegistry")
1299                .field("registered", &inner.entries.keys().collect::<Vec<_>>())
1300                .field("mounted", &inner.mounted.keys().collect::<Vec<_>>())
1301                .finish(),
1302            Err(_) => formatter.write_str("PrimitiveRegistry(<borrowed>)"),
1303        }
1304    }
1305}
1306
1307impl PrimitiveRegistry {
1308    /// The descriptors of every registered primitive, in id order.
1309    #[must_use]
1310    pub fn descriptors(&self) -> Vec<PrimitiveDescriptor> {
1311        self.inner
1312            .borrow()
1313            .entries
1314            .values()
1315            .map(|entry| entry.descriptor.clone())
1316            .collect()
1317    }
1318
1319    #[must_use]
1320    pub fn new() -> Self {
1321        Self::default()
1322    }
1323
1324    /// Register a validated primitive descriptor and native handler.
1325    ///
1326    /// # Errors
1327    ///
1328    /// Returns [`PrimitiveError`] for duplicate IDs, invalid exports/defaults,
1329    /// or missing callback props for declared events.
1330    pub fn register(
1331        &self,
1332        descriptor: PrimitiveDescriptor,
1333        handler: impl PrimitiveHandler + 'static,
1334    ) -> Result<(), PrimitiveError> {
1335        validate_descriptor(&descriptor)?;
1336        let mut inner = self
1337            .inner
1338            .try_borrow_mut()
1339            .map_err(|_| PrimitiveError::Borrowed)?;
1340        if inner.entries.contains_key(&descriptor.id) {
1341            return Err(PrimitiveError::Duplicate(descriptor.id));
1342        }
1343        if inner.entries.values().any(|entry| {
1344            entry.descriptor.id.namespace() == descriptor.id.namespace()
1345                && entry.descriptor.export == descriptor.export
1346        }) {
1347            return Err(PrimitiveError::DuplicateExport {
1348                namespace: descriptor.id.namespace().to_owned(),
1349                export: descriptor.export,
1350            });
1351        }
1352        inner.entries.insert(
1353            descriptor.id.clone(),
1354            PrimitiveEntry {
1355                descriptor,
1356                handler: Box::new(handler),
1357            },
1358        );
1359        Ok(())
1360    }
1361
1362    /// Convert validated Rhai props into a custom `UiNode`.
1363    ///
1364    /// # Errors
1365    ///
1366    /// Returns [`PrimitiveError`] for unknown primitives, invalid props, or a
1367    /// missing key on stateful/lifecycle instances.
1368    pub fn create_node(
1369        &self,
1370        id: &PrimitiveId,
1371        key: Option<String>,
1372        props: &Map,
1373        generation: ScriptGeneration,
1374    ) -> Result<UiNode, PrimitiveError> {
1375        let inner = self
1376            .inner
1377            .try_borrow()
1378            .map_err(|_| PrimitiveError::Borrowed)?;
1379        let descriptor = &inner
1380            .entries
1381            .get(id)
1382            .ok_or_else(|| PrimitiveError::Unknown(id.clone()))?
1383            .descriptor;
1384        if (descriptor.lifecycle || !descriptor.state.is_empty()) && key.is_none() {
1385            return Err(PrimitiveError::MissingKey(id.clone()));
1386        }
1387        let schema = ValueSchema::object(descriptor.props.clone());
1388        schema
1389            .validate(&Dynamic::from_map(props.clone()))
1390            .map_err(|source| PrimitiveError::InvalidProps {
1391                primitive: id.clone(),
1392                source,
1393            })?;
1394        let props = convert_props(&descriptor.props, props, generation)?;
1395        Ok(UiNode::custom(PrimitiveNode {
1396            primitive: id.clone(),
1397            key,
1398            props,
1399        }))
1400    }
1401
1402    /// Validate an event emitted by a native primitive adapter.
1403    ///
1404    /// # Errors
1405    ///
1406    /// Returns [`PrimitiveError`] for unknown events or invalid payloads.
1407    pub fn normalize_event(
1408        &self,
1409        id: &PrimitiveId,
1410        event: &str,
1411        payload: UiValue,
1412    ) -> Result<UiValue, PrimitiveError> {
1413        let inner = self
1414            .inner
1415            .try_borrow()
1416            .map_err(|_| PrimitiveError::Borrowed)?;
1417        let descriptor = &inner
1418            .entries
1419            .get(id)
1420            .ok_or_else(|| PrimitiveError::Unknown(id.clone()))?
1421            .descriptor;
1422        let schema = descriptor
1423            .events
1424            .get(event)
1425            .ok_or_else(|| PrimitiveError::UnknownEvent {
1426                primitive: id.clone(),
1427                event: event.to_owned(),
1428            })?;
1429        schema
1430            .payload
1431            .validate_ui_value(&payload)
1432            .map_err(|source| PrimitiveError::InvalidEvent {
1433                primitive: id.clone(),
1434                event: event.to_owned(),
1435                source,
1436            })?;
1437        Ok(payload)
1438    }
1439
1440    pub(crate) fn accessibility_projections(
1441        &self,
1442        tree: &crate::RetainedUiTree,
1443        cx: &App,
1444    ) -> BTreeMap<crate::NodeId, PrimitiveAccessibilityProjection> {
1445        let Ok(inner) = self.inner.try_borrow() else {
1446            return BTreeMap::new();
1447        };
1448        tree.nodes()
1449            .filter_map(|node| {
1450                let primitive = node.primitive()?.clone();
1451                let instance = PrimitiveInstanceId {
1452                    primitive: primitive.clone(),
1453                    key: node.key()?.to_owned(),
1454                    node: node.id(),
1455                };
1456                let projection = inner
1457                    .entries
1458                    .get(&primitive)?
1459                    .handler
1460                    .accessibility(&instance, cx)?;
1461                Some((node.id(), projection))
1462            })
1463            .collect()
1464    }
1465
1466    pub(crate) fn accessibility_actions(
1467        &self,
1468        instance: &PrimitiveInstanceId,
1469    ) -> Vec<gpui::AccessibleAction> {
1470        let Ok(inner) = self.inner.try_borrow() else {
1471            return Vec::new();
1472        };
1473        inner
1474            .entries
1475            .get(&instance.primitive)
1476            .map_or_else(Vec::new, |entry| {
1477                entry.handler.accessibility_actions(instance)
1478            })
1479    }
1480
1481    pub(crate) fn uses_primary_focus(&self, primitive: &PrimitiveId) -> bool {
1482        self.inner.try_borrow().is_ok_and(|inner| {
1483            inner
1484                .entries
1485                .get(primitive)
1486                .is_some_and(|entry| entry.handler.uses_primary_focus())
1487        })
1488    }
1489
1490    pub(crate) fn perform_accessibility_action(
1491        &self,
1492        instance: &PrimitiveInstanceId,
1493        action: gpui::AccessibleAction,
1494        data: Option<&gpui::accesskit::ActionData>,
1495        window: &mut Window,
1496        cx: &mut App,
1497    ) -> Result<(), PrimitiveError> {
1498        let mut inner = self
1499            .inner
1500            .try_borrow_mut()
1501            .map_err(|_| PrimitiveError::Borrowed)?;
1502        if !inner.mounted.contains_key(instance) {
1503            return Err(PrimitiveError::MissingInstance(instance.clone()));
1504        }
1505        let entry = inner
1506            .entries
1507            .get_mut(&instance.primitive)
1508            .ok_or_else(|| PrimitiveError::Unknown(instance.primitive.clone()))?;
1509        guard_primitive_panic(&instance.primitive, "accessibility action", || {
1510            entry
1511                .handler
1512                .perform_accessibility_action(instance, action, data, window, cx)
1513        })?
1514        .map_err(|message| PrimitiveError::Handler {
1515            primitive: instance.primitive.clone(),
1516            message,
1517        })
1518    }
1519
1520    pub(crate) fn perform_key(
1521        &self,
1522        instance: &PrimitiveInstanceId,
1523        key: &str,
1524    ) -> Result<Option<PrimitiveSemanticProposal>, PrimitiveError> {
1525        let mut inner = self
1526            .inner
1527            .try_borrow_mut()
1528            .map_err(|_| PrimitiveError::Borrowed)?;
1529        if !inner.mounted.contains_key(instance) {
1530            return Err(PrimitiveError::MissingInstance(instance.clone()));
1531        }
1532        let entry = inner
1533            .entries
1534            .get_mut(&instance.primitive)
1535            .ok_or_else(|| PrimitiveError::Unknown(instance.primitive.clone()))?;
1536        guard_primitive_panic(&instance.primitive, "key semantic", || {
1537            entry.handler.perform_key(instance, key)
1538        })?
1539        .map_err(|message| PrimitiveError::Handler {
1540            primitive: instance.primitive.clone(),
1541            message,
1542        })
1543    }
1544
1545    /// Unmount keyed lifecycle instances absent from the successful node tree.
1546    ///
1547    /// # Errors
1548    ///
1549    /// Returns [`PrimitiveError::Borrowed`] during a conflicting render borrow.
1550    pub fn retain_mounted(
1551        &self,
1552        active: &BTreeSet<PrimitiveInstanceId>,
1553    ) -> Result<(), PrimitiveError> {
1554        let mut inner = self
1555            .inner
1556            .try_borrow_mut()
1557            .map_err(|_| PrimitiveError::Borrowed)?;
1558        let removed = inner
1559            .mounted
1560            .keys()
1561            .filter(|instance| !active.contains(*instance))
1562            .cloned()
1563            .collect::<Vec<_>>();
1564        let mut first_error = None;
1565        for instance in &removed {
1566            let resources = inner
1567                .mounted
1568                .get(instance)
1569                .and_then(|mounted| mounted.resources.clone());
1570            if let Some(entry) = inner.entries.get_mut(&instance.primitive)
1571                && let Err(error) = guard_primitive_panic(&instance.primitive, "unmount", || {
1572                    entry.handler.unmount(instance);
1573                })
1574                && first_error.is_none()
1575            {
1576                first_error = Some(error);
1577            }
1578            if let Some(resources) = resources
1579                && let Err(error) = resources.close()
1580                && first_error.is_none()
1581            {
1582                first_error = Some(PrimitiveError::Resource(error));
1583            }
1584            inner.mounted.remove(instance);
1585        }
1586        first_error.map_or(Ok(()), Err)
1587    }
1588
1589    /// Unmount keyed primitive instances absent from the current successful tree.
1590    ///
1591    /// # Errors
1592    ///
1593    /// Returns borrow or panic-boundary errors from native unmount handlers.
1594    pub fn retain_tree(&self, tree: &crate::RetainedUiTree) -> Result<(), PrimitiveError> {
1595        let active = collect_primitive_instances(tree);
1596        self.retain_mounted(&active)
1597    }
1598
1599    pub(crate) fn suspend_mounted(&self, cx: &mut App) -> Result<(), PrimitiveError> {
1600        let mut inner = self
1601            .inner
1602            .try_borrow_mut()
1603            .map_err(|_| PrimitiveError::Borrowed)?;
1604        let instances = inner.mounted.keys().cloned().collect::<Vec<_>>();
1605        let mut first_error = None;
1606        for instance in instances {
1607            if let Some(entry) = inner.entries.get_mut(&instance.primitive)
1608                && let Err(error) = guard_primitive_panic(&instance.primitive, "suspend", || {
1609                    entry.handler.suspend(&instance, cx);
1610                })
1611                && first_error.is_none()
1612            {
1613                first_error = Some(error);
1614            }
1615        }
1616        first_error.map_or(Ok(()), Err)
1617    }
1618
1619    pub(crate) fn resume_mounted(&self, cx: &mut App) -> Result<(), PrimitiveError> {
1620        let mut inner = self
1621            .inner
1622            .try_borrow_mut()
1623            .map_err(|_| PrimitiveError::Borrowed)?;
1624        let instances = inner.mounted.keys().cloned().collect::<Vec<_>>();
1625        let mut first_error = None;
1626        for instance in instances {
1627            if let Some(entry) = inner.entries.get_mut(&instance.primitive)
1628                && let Err(error) = guard_primitive_panic(&instance.primitive, "resume", || {
1629                    entry.handler.resume(&instance, cx);
1630                })
1631                && first_error.is_none()
1632            {
1633                first_error = Some(error);
1634            }
1635        }
1636        first_error.map_or(Ok(()), Err)
1637    }
1638
1639    pub(crate) fn commit_resume_mounted(&self, cx: &mut App) -> Result<(), PrimitiveError> {
1640        let mut inner = self
1641            .inner
1642            .try_borrow_mut()
1643            .map_err(|_| PrimitiveError::Borrowed)?;
1644        let instances = inner.mounted.keys().cloned().collect::<Vec<_>>();
1645        let mut first_error = None;
1646        for instance in instances {
1647            if let Some(entry) = inner.entries.get_mut(&instance.primitive)
1648                && let Err(error) =
1649                    guard_primitive_panic(&instance.primitive, "commit resume", || {
1650                        entry.handler.commit_resume(&instance, cx);
1651                    })
1652                && first_error.is_none()
1653            {
1654                first_error = Some(error);
1655            }
1656        }
1657        first_error.map_or(Ok(()), Err)
1658    }
1659
1660    pub(crate) fn element(
1661        &self,
1662        node: PrimitiveNode,
1663        retained: Option<(crate::NodeId, String)>,
1664        focus_handle: Option<gpui::FocusHandle>,
1665        fallback: Option<UiNode>,
1666        runtime: PrimitiveWindowContext,
1667        theme: PrimitiveTheme,
1668    ) -> AnyElement {
1669        RegisteredPrimitiveElement {
1670            registry: self.clone(),
1671            node,
1672            retained,
1673            focus_handle,
1674            fallback,
1675            runtime,
1676            theme,
1677        }
1678        .into_any_element()
1679    }
1680
1681    #[allow(clippy::too_many_lines)]
1682    fn render_instance(
1683        &self,
1684        node: PrimitiveNode,
1685        identity: PrimitiveRenderIdentity,
1686        events: &PrimitiveContext,
1687        theme: &PrimitiveTheme,
1688        window: &mut Window,
1689        cx: &mut App,
1690    ) -> Result<AnyElement, PrimitiveError> {
1691        let mut inner = self
1692            .inner
1693            .try_borrow_mut()
1694            .map_err(|_| PrimitiveError::Borrowed)?;
1695        let retained_instance = primitive_is_retained(&inner, &node.primitive)?;
1696        if retained_instance && identity.retained_id.is_none() {
1697            return Err(PrimitiveError::MissingRetainedIdentity(node.primitive));
1698        }
1699        let instance_id = retained_instance.then(|| PrimitiveInstanceId {
1700            primitive: node.primitive.clone(),
1701            key: identity
1702                .retained_key
1703                .clone()
1704                .expect("retained primitive descriptors require a retained key"),
1705            node: identity
1706                .retained_id
1707                .expect("retained primitive renderer supplies NodeId"),
1708        });
1709        let previous = instance_id
1710            .as_ref()
1711            .and_then(|id| inner.mounted.get(id))
1712            .cloned();
1713        let resources = if retained_instance {
1714            Some(
1715                previous
1716                    .as_ref()
1717                    .and_then(|instance| instance.resources.clone())
1718                    .unwrap_or_default(),
1719            )
1720        } else {
1721            None
1722        };
1723        let checkpoint = resources
1724            .as_ref()
1725            .map(PrimitiveResourceScope::checkpoint)
1726            .transpose()?;
1727        let instance = PrimitiveInstance {
1728            id: instance_id.clone(),
1729            node,
1730            resources: resources.clone(),
1731            focus_handle: identity.focus_handle,
1732        };
1733        let needs_mount = instance_id.is_some() && previous.is_none();
1734        if needs_mount
1735            && let Some(effect) = inner
1736                .entries
1737                .get(&instance.node.primitive)
1738                .and_then(|entry| entry.descriptor.effect.as_ref())
1739        {
1740            let mounted = inner
1741                .mounted
1742                .keys()
1743                .filter(|id| id.primitive == instance.node.primitive)
1744                .count();
1745            if mounted >= effect.max_instances {
1746                return Err(PrimitiveError::EffectInstanceBudget {
1747                    primitive: instance.node.primitive.clone(),
1748                    actual: mounted.saturating_add(1),
1749                    limit: effect.max_instances,
1750                });
1751            }
1752        }
1753        let entry = inner
1754            .entries
1755            .get_mut(&instance.node.primitive)
1756            .ok_or_else(|| PrimitiveError::Unknown(instance.node.primitive.clone()))?;
1757        if let Some(effect) = &entry.descriptor.effect {
1758            let cost = guard_primitive_panic(&instance.node.primitive, "effect_cost", || {
1759                entry.handler.effect_cost(&instance)
1760            })?;
1761            if cost > effect.max_cost_per_instance {
1762                return Err(PrimitiveError::EffectCostBudget {
1763                    primitive: instance.node.primitive.clone(),
1764                    actual: cost,
1765                    limit: effect.max_cost_per_instance,
1766                });
1767            }
1768        }
1769        let operation = (|| {
1770            if needs_mount {
1771                guard_primitive_panic(&instance.node.primitive, "mount", || {
1772                    entry.handler.mount(&instance)
1773                })?
1774                .map_err(|message| PrimitiveError::Handler {
1775                    primitive: instance.node.primitive.clone(),
1776                    message,
1777                })?;
1778            }
1779            if let Some(previous) = &previous
1780                && previous.node != instance.node
1781            {
1782                guard_primitive_panic(&instance.node.primitive, "update", || {
1783                    entry.handler.update(previous, &instance)
1784                })?
1785                .map_err(|message| PrimitiveError::Handler {
1786                    primitive: instance.node.primitive.clone(),
1787                    message,
1788                })?;
1789            }
1790            let mut scoped_events = events.clone();
1791            scoped_events.instance.clone_from(&instance_id);
1792            guard_primitive_panic(&instance.node.primitive, "render", || {
1793                entry
1794                    .handler
1795                    .render(&instance, &scoped_events, theme, window, cx)
1796            })?
1797            .map_err(|message| PrimitiveError::Handler {
1798                primitive: instance.node.primitive.clone(),
1799                message,
1800            })
1801        })();
1802        let element = match operation {
1803            Ok(element) => element,
1804            Err(error) => {
1805                return Err(rollback_failed_primitive_operation(
1806                    entry,
1807                    &instance,
1808                    needs_mount,
1809                    resources.as_ref(),
1810                    checkpoint,
1811                    error,
1812                ));
1813            }
1814        };
1815        if let Some(id) = instance_id {
1816            inner.mounted.insert(id, instance);
1817        }
1818        Ok(element)
1819    }
1820}
1821
1822fn rollback_failed_primitive_operation(
1823    entry: &mut PrimitiveEntry,
1824    instance: &PrimitiveInstance,
1825    needs_unmount: bool,
1826    resources: Option<&PrimitiveResourceScope>,
1827    checkpoint: Option<u64>,
1828    original: PrimitiveError,
1829) -> PrimitiveError {
1830    let mut rollback_error = None;
1831    if needs_unmount
1832        && let Some(instance_id) = instance.id.as_ref()
1833        && let Err(error) =
1834            guard_primitive_panic(&instance.node.primitive, "failed-mount unmount", || {
1835                entry.handler.unmount(instance_id);
1836            })
1837    {
1838        rollback_error = Some(error);
1839    }
1840    if let (Some(resources), Some(checkpoint)) = (resources, checkpoint)
1841        && let Err(error) = resources.rollback(checkpoint)
1842        && rollback_error.is_none()
1843    {
1844        rollback_error = Some(PrimitiveError::Resource(error));
1845    }
1846    rollback_error.unwrap_or(original)
1847}
1848
1849fn primitive_is_retained(
1850    inner: &PrimitiveRegistryInner,
1851    primitive: &PrimitiveId,
1852) -> Result<bool, PrimitiveError> {
1853    let descriptor = &inner
1854        .entries
1855        .get(primitive)
1856        .ok_or_else(|| PrimitiveError::Unknown(primitive.clone()))?
1857        .descriptor;
1858    Ok(descriptor.lifecycle || !descriptor.state.is_empty())
1859}
1860
1861fn collect_primitive_instances(tree: &crate::RetainedUiTree) -> BTreeSet<PrimitiveInstanceId> {
1862    tree.nodes()
1863        .filter_map(|node| {
1864            Some(PrimitiveInstanceId {
1865                primitive: node.primitive()?.clone(),
1866                key: node.key()?.to_owned(),
1867                node: node.id(),
1868            })
1869        })
1870        .collect()
1871}
1872
1873fn guard_primitive_panic<T>(
1874    primitive: &PrimitiveId,
1875    phase: &'static str,
1876    operation: impl FnOnce() -> T,
1877) -> Result<T, PrimitiveError> {
1878    catch_unwind(AssertUnwindSafe(operation)).map_err(|_| PrimitiveError::Panic {
1879        primitive: primitive.clone(),
1880        phase,
1881    })
1882}
1883
1884#[derive(gpui::IntoElement)]
1885struct RegisteredPrimitiveElement {
1886    registry: PrimitiveRegistry,
1887    node: PrimitiveNode,
1888    retained: Option<(crate::NodeId, String)>,
1889    focus_handle: Option<gpui::FocusHandle>,
1890    fallback: Option<UiNode>,
1891    runtime: PrimitiveWindowContext,
1892    theme: PrimitiveTheme,
1893}
1894
1895#[derive(Clone)]
1896pub(crate) struct PrimitiveWindowContext {
1897    dispatcher: Option<NodeEventDispatcher>,
1898    interactions: crate::interaction::WindowInteractionCoordinator,
1899    scroll_handles: Vec<gpui::ScrollHandle>,
1900    view_id: String,
1901}
1902
1903impl PrimitiveWindowContext {
1904    pub(crate) fn new(
1905        dispatcher: Option<NodeEventDispatcher>,
1906        interactions: crate::interaction::WindowInteractionCoordinator,
1907        scroll_handles: Vec<gpui::ScrollHandle>,
1908        view_id: impl Into<String>,
1909    ) -> Self {
1910        Self {
1911            dispatcher,
1912            interactions,
1913            scroll_handles,
1914            view_id: view_id.into(),
1915        }
1916    }
1917}
1918
1919struct PrimitiveRenderIdentity {
1920    retained_id: Option<crate::NodeId>,
1921    retained_key: Option<String>,
1922    focus_handle: Option<gpui::FocusHandle>,
1923}
1924
1925impl RenderOnce for RegisteredPrimitiveElement {
1926    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
1927        let registry = self.registry;
1928        let callbacks = self
1929            .node
1930            .props
1931            .iter()
1932            .filter_map(|(name, value)| {
1933                name.strip_prefix("on_").and_then(|event| match value {
1934                    PrimitiveValue::Callback(callback) => {
1935                        Some((event.to_owned(), callback.clone()))
1936                    }
1937                    _ => None,
1938                })
1939            })
1940            .collect();
1941        let events = PrimitiveContext {
1942            registry: Rc::downgrade(&registry.inner),
1943            primitive: self.node.primitive.clone(),
1944            event_schemas: registry
1945                .inner
1946                .borrow()
1947                .entries
1948                .get(&self.node.primitive)
1949                .map(|entry| entry.descriptor.events.clone())
1950                .unwrap_or_default(),
1951            callbacks,
1952            dispatcher: self.runtime.dispatcher,
1953            interactions: self.runtime.interactions,
1954            scroll_handles: self.runtime.scroll_handles,
1955            view_id: self.runtime.view_id,
1956            instance: None,
1957            retained_node: self.retained.as_ref().map(|(node, _)| *node),
1958            focus_handle: self.focus_handle.clone(),
1959        };
1960        match registry.render_instance(
1961            self.node,
1962            PrimitiveRenderIdentity {
1963                retained_id: self.retained.as_ref().map(|(node, _)| *node),
1964                retained_key: self.retained.map(|(_, key)| key),
1965                focus_handle: self.focus_handle,
1966            },
1967            &events,
1968            &self.theme,
1969            window,
1970            cx,
1971        ) {
1972            Ok(element) => element,
1973            Err(error) => self.fallback.map_or_else(
1974                || {
1975                    div()
1976                        .child(format!("Custom primitive error: {error}"))
1977                        .into_any_element()
1978                },
1979                |fallback| {
1980                    crate::GpuiNodeRenderer::render_with_primitives(
1981                        &fallback,
1982                        &crate::LiteralColorResolver,
1983                        &crate::InteractionState::default(),
1984                        &registry,
1985                    )
1986                },
1987            ),
1988        }
1989    }
1990}
1991
1992fn validate_descriptor(descriptor: &PrimitiveDescriptor) -> Result<(), PrimitiveError> {
1993    if !is_pascal_case(&descriptor.export) {
1994        return Err(PrimitiveError::InvalidExport(descriptor.export.clone()));
1995    }
1996    for (name, field) in &descriptor.props {
1997        if !is_identifier(name) {
1998            return Err(PrimitiveError::InvalidPropName(name.clone()));
1999        }
2000        field
2001            .schema
2002            .validate_definition()
2003            .map_err(|source| PrimitiveError::InvalidSchema {
2004                location: format!("prop `{name}`"),
2005                source,
2006            })?;
2007        if let Some(default) = &field.default {
2008            field.schema.validate_ui_value(default).map_err(|source| {
2009                PrimitiveError::InvalidDefault {
2010                    prop: name.clone(),
2011                    source,
2012                }
2013            })?;
2014        }
2015    }
2016    for (name, event) in &descriptor.events {
2017        event
2018            .payload
2019            .validate_definition()
2020            .map_err(|source| PrimitiveError::InvalidSchema {
2021                location: format!("event `{name}`"),
2022                source,
2023            })?;
2024        let callback = format!("on_{name}");
2025        if !descriptor
2026            .props
2027            .get(&callback)
2028            .is_some_and(|field| schema_accepts_callback(&field.schema))
2029        {
2030            return Err(PrimitiveError::MissingEventCallback {
2031                event: name.clone(),
2032                prop: callback,
2033            });
2034        }
2035    }
2036    ComponentStateSchema::new(descriptor.state.fields().clone())
2037        .map_err(|source| PrimitiveError::InvalidStateSchema(source.to_string()))?;
2038    if let Some(effect) = &descriptor.effect {
2039        if effect.platforms.is_empty() || !effect.platforms.contains(&PrimitivePlatform::current())
2040        {
2041            return Err(PrimitiveError::UnsupportedEffectPlatform {
2042                primitive: descriptor.id.clone(),
2043                platform: PrimitivePlatform::current(),
2044            });
2045        }
2046        if effect.max_instances == 0 || effect.max_cost_per_instance == 0 {
2047            return Err(PrimitiveError::InvalidEffectBudget(descriptor.id.clone()));
2048        }
2049        if !descriptor.lifecycle {
2050            return Err(PrimitiveError::EffectRequiresLifecycle(
2051                descriptor.id.clone(),
2052            ));
2053        }
2054    }
2055    Ok(())
2056}
2057
2058fn schema_accepts_callback(schema: &ValueSchema) -> bool {
2059    matches!(schema, ValueSchema::Callback)
2060        || matches!(schema, ValueSchema::Optional { value } if schema_accepts_callback(value))
2061        || matches!(schema, ValueSchema::OneOf { variants } if variants.iter().any(schema_accepts_callback))
2062}
2063
2064fn convert_props(
2065    schema: &BTreeMap<String, ObjectField>,
2066    values: &Map,
2067    generation: ScriptGeneration,
2068) -> Result<PrimitiveProps, PrimitiveError> {
2069    let mut converted = BTreeMap::new();
2070    for (name, field) in schema {
2071        let value = values
2072            .get(name.as_str())
2073            .cloned()
2074            .or_else(|| field.default.clone().map(UiValue::into_dynamic));
2075        if let Some(value) = value {
2076            converted.insert(
2077                name.clone(),
2078                convert_prop(&field.schema, value, generation).map_err(|source| {
2079                    PrimitiveError::PropConversion {
2080                        prop: name.clone(),
2081                        source,
2082                    }
2083                })?,
2084            );
2085        }
2086    }
2087    Ok(PrimitiveProps(converted))
2088}
2089
2090fn convert_prop(
2091    schema: &ValueSchema,
2092    value: Dynamic,
2093    generation: ScriptGeneration,
2094) -> Result<PrimitiveValue, PrimitivePropConversionError> {
2095    match schema {
2096        ValueSchema::Optional { value: inner } if value.is_unit() => {
2097            Ok(PrimitiveValue::Data(UiValue::Null))
2098        }
2099        ValueSchema::Optional { value: inner } => convert_prop(inner, value, generation),
2100        ValueSchema::OneOf { variants } => {
2101            let branch = variants
2102                .iter()
2103                .find(|variant| variant.validate(&value).is_ok())
2104                .expect("validated primitive one_of prop matches one branch");
2105            convert_prop(branch, value, generation)
2106        }
2107        ValueSchema::Node => Ok(PrimitiveValue::Node(Box::new(value.cast::<UiNode>()))),
2108        ValueSchema::Callback if value.is::<FnPtr>() => {
2109            Ok(PrimitiveValue::Callback(UiEventHandler::Script(
2110                ScriptCallback::try_from_fn_ptr(value.cast::<FnPtr>(), generation)?,
2111            )))
2112        }
2113        ValueSchema::Callback => Ok(PrimitiveValue::Callback(UiEventHandler::Native(
2114            value.cast::<crate::NativeHandlerRef>(),
2115        ))),
2116        ValueSchema::Array { items, .. } if matches!(items.as_ref(), ValueSchema::Node) => {
2117            Ok(PrimitiveValue::Nodes(
2118                value
2119                    .cast::<Array>()
2120                    .into_iter()
2121                    .map(Dynamic::cast::<UiNode>)
2122                    .collect(),
2123            ))
2124        }
2125        ValueSchema::Style => Ok(PrimitiveValue::Style(Box::new(value.cast::<Style>()))),
2126        ValueSchema::Length => Ok(PrimitiveValue::Length(value.cast::<Length>())),
2127        ValueSchema::Asset => Ok(PrimitiveValue::Asset(value.cast::<AssetId>())),
2128        ValueSchema::Signal => Ok(PrimitiveValue::Signal(value.cast::<crate::NativeSignal>())),
2129        ValueSchema::Ref => Ok(PrimitiveValue::Ref(value.cast::<crate::ElementRef>())),
2130        ValueSchema::Document => Ok(PrimitiveValue::Document(
2131            value.cast::<crate::NativeTextDocument>(),
2132        )),
2133        #[cfg(feature = "charts")]
2134        ValueSchema::ChartData => Ok(PrimitiveValue::ChartData(
2135            value.cast::<crate::NativeChartData>(),
2136        )),
2137        _ => UiValue::from_dynamic(value)
2138            .map(PrimitiveValue::Data)
2139            .map_err(Into::into),
2140    }
2141}
2142
2143#[derive(Debug, Error)]
2144pub enum PrimitivePropConversionError {
2145    #[error(transparent)]
2146    Value(#[from] UiValueError),
2147    #[error(transparent)]
2148    Callback(#[from] crate::ScriptCallbackDefinitionError),
2149}
2150
2151#[derive(Debug, Error)]
2152pub enum PrimitiveError {
2153    #[error("primitive ID `{0}` must be `namespace.snake_case_name`")]
2154    InvalidId(String),
2155    #[error("primitive export `{0}` must be PascalCase")]
2156    InvalidExport(String),
2157    #[error("primitive prop `{0}` must be `snake_case`")]
2158    InvalidPropName(String),
2159    #[error("invalid schema definition for primitive {location}: {source}")]
2160    InvalidSchema {
2161        location: String,
2162        source: SchemaDefinitionError,
2163    },
2164    #[error("invalid primitive state schema: {0}")]
2165    InvalidStateSchema(String),
2166    #[error("primitive registry is already borrowed during rendering")]
2167    Borrowed,
2168    #[error("primitive `{0:?}` is already registered")]
2169    Duplicate(PrimitiveId),
2170    #[error("primitive export `{namespace}::{export}` is already registered")]
2171    DuplicateExport { namespace: String, export: String },
2172    #[error("primitive `{0:?}` is not registered")]
2173    Unknown(PrimitiveId),
2174    #[error("primitive `{0:?}` requires a stable key")]
2175    MissingKey(PrimitiveId),
2176    #[error("primitive `{0:?}` requires a retained NodeId renderer")]
2177    MissingRetainedIdentity(PrimitiveId),
2178    #[error("primitive accessibility action targeted stale instance {0:?}")]
2179    MissingInstance(PrimitiveInstanceId),
2180    #[error("primitive event emitter outlived its registry")]
2181    RegistryReleased,
2182    #[error("props for primitive `{primitive:?}` are invalid: {source}")]
2183    InvalidProps {
2184        primitive: PrimitiveId,
2185        source: SchemaValidationError,
2186    },
2187    #[error("default for primitive prop `{prop}` is invalid: {source}")]
2188    InvalidDefault {
2189        prop: String,
2190        source: SchemaValidationError,
2191    },
2192    #[error("primitive prop `{prop}` cannot cross the runtime boundary: {source}")]
2193    PropConversion {
2194        prop: String,
2195        source: PrimitivePropConversionError,
2196    },
2197    #[error("primitive event `{event}` requires callback prop `{prop}`")]
2198    MissingEventCallback { event: String, prop: String },
2199    #[error("primitive `{primitive:?}` does not declare event `{event}`")]
2200    UnknownEvent {
2201        primitive: PrimitiveId,
2202        event: String,
2203    },
2204    #[error("primitive `{primitive:?}` event `{event}` is invalid: {source}")]
2205    InvalidEvent {
2206        primitive: PrimitiveId,
2207        event: String,
2208        source: SchemaValidationError,
2209    },
2210    #[error("primitive `{primitive:?}` handler failed: {message}")]
2211    Handler {
2212        primitive: PrimitiveId,
2213        message: String,
2214    },
2215    #[error("primitive `{primitive:?}` panicked during {phase}")]
2216    Panic {
2217        primitive: PrimitiveId,
2218        phase: &'static str,
2219    },
2220    #[error("effect primitive `{primitive:?}` does not support {platform:?}")]
2221    UnsupportedEffectPlatform {
2222        primitive: PrimitiveId,
2223        platform: PrimitivePlatform,
2224    },
2225    #[error("effect primitive `{0:?}` must declare positive instance and cost budgets")]
2226    InvalidEffectBudget(PrimitiveId),
2227    #[error("effect primitive `{0:?}` must opt into scoped lifecycle")]
2228    EffectRequiresLifecycle(PrimitiveId),
2229    #[error("effect primitive `{primitive:?}` instance budget exceeded: {actual} > {limit}")]
2230    EffectInstanceBudget {
2231        primitive: PrimitiveId,
2232        actual: usize,
2233        limit: usize,
2234    },
2235    #[error("effect primitive `{primitive:?}` cost budget exceeded: {actual} > {limit}")]
2236    EffectCostBudget {
2237        primitive: PrimitiveId,
2238        actual: usize,
2239        limit: usize,
2240    },
2241    #[error(transparent)]
2242    Resource(#[from] PrimitiveResourceError),
2243}
2244
2245#[cfg(test)]
2246mod tests {
2247    use super::*;
2248    use crate::{ObjectField, StateField};
2249    use std::cell::Cell;
2250
2251    struct TestHandler;
2252
2253    struct TestTheme;
2254
2255    fn test_tokens() -> std::sync::Arc<crate::ThemeTokens> {
2256        static TOKENS: std::sync::OnceLock<std::sync::Arc<crate::ThemeTokens>> =
2257            std::sync::OnceLock::new();
2258        TOKENS
2259            .get_or_init(|| {
2260                let engine = crate::RuntimeEngine::new();
2261                crate::load_theme_source(
2262                    engine.engine(),
2263                    "test.rhai",
2264                    r#"fn theme() { #{ family: "T", name: "T", mode: "dark", tokens: #{
2265                        colors: #{ accent: 0x123456ff, selection: 0x123456ff,
2266                            "table.selection": 0x123456ff },
2267                        spacing: #{ xxs: px(2), sm: px(6) },
2268                        typography: #{ roles: #{ body: #{ size: px(12), line_height: px(16),
2269                            weight: 400, family: "JetBrains Mono", fallbacks: ["PingFang SC"] } } },
2270                    } } }"#,
2271                )
2272                .expect("test theme")
2273                .tokens
2274            })
2275            .clone()
2276    }
2277
2278    impl ColorResolver for TestTheme {
2279        fn resolve_token(&self, token: &str) -> Option<Rgba8> {
2280            test_tokens().color(token)
2281        }
2282
2283        fn token_set(&self) -> Option<std::sync::Arc<crate::ThemeTokens>> {
2284            Some(test_tokens())
2285        }
2286    }
2287
2288    impl PrimitiveHandler for TestHandler {
2289        fn render(
2290            &mut self,
2291            _: &PrimitiveInstance,
2292            _: &PrimitiveContext,
2293            _: &PrimitiveTheme,
2294            _: &mut Window,
2295            _: &mut App,
2296        ) -> Result<AnyElement, String> {
2297            Ok(div().into_any_element())
2298        }
2299    }
2300
2301    #[test]
2302    fn primitive_theme_exposes_only_resolved_semantic_snapshot() {
2303        let theme = PrimitiveTheme::capture(&TestTheme);
2304        assert_eq!(
2305            theme.color("accent"),
2306            Some(Rgba8::from_rgba_hex(0x1234_56ff))
2307        );
2308        assert_eq!(theme.color("unknown"), None);
2309        assert_eq!(
2310            theme.color("selection"),
2311            Some(Rgba8::from_rgba_hex(0x1234_56ff))
2312        );
2313        assert_eq!(
2314            theme.color("table.selection"),
2315            Some(Rgba8::from_rgba_hex(0x1234_56ff))
2316        );
2317        assert_eq!(
2318            theme.resolve_color(&ColorValue::Literal(Rgba8::from_rgba_hex(0xaabb_ccdd))),
2319            Some(Rgba8::from_rgba_hex(0xaabb_ccdd))
2320        );
2321        assert_eq!(
2322            theme.resolve_length(Length::theme_spacing("sm").unwrap()),
2323            Some(Length::Pixels(6.0))
2324        );
2325        assert_eq!(
2326            theme.resolve_length(Length::theme_spacing("xxs").unwrap()),
2327            Some(Length::Pixels(2.0))
2328        );
2329        assert_eq!(
2330            theme.typography("body").unwrap().family.as_deref(),
2331            Some("JetBrains Mono")
2332        );
2333
2334        let engine = crate::RuntimeEngine::new();
2335        let loaded = crate::load_theme_source(
2336            engine.engine(),
2337            "default_light.rhai",
2338            include_str!("../../../registry/themes/default_light.rhai"),
2339        )
2340        .unwrap();
2341        let captured = PrimitiveTheme::capture(&loaded);
2342        assert_eq!(captured.color("surface"), loaded.tokens.color("surface"));
2343        assert!(captured.color("surface").is_some());
2344    }
2345
2346    fn descriptor() -> PrimitiveDescriptor {
2347        PrimitiveDescriptor {
2348            id: PrimitiveId::parse("my_app.code_editor").unwrap(),
2349            export: "CodeEditor".to_owned(),
2350            props: BTreeMap::from([
2351                (
2352                    "value".to_owned(),
2353                    ObjectField::required(ValueSchema::string()),
2354                ),
2355                (
2356                    "on_change".to_owned(),
2357                    ObjectField::optional(ValueSchema::optional(ValueSchema::Callback)),
2358                ),
2359            ]),
2360            events: BTreeMap::from([(
2361                "change".to_owned(),
2362                EventSchema {
2363                    doc: None,
2364                    payload: ValueSchema::string(),
2365                },
2366            )]),
2367            state: ComponentStateSchema::new(BTreeMap::from([(
2368                "selection".to_owned(),
2369                StateField::new(ValueSchema::integer(), UiValue::Integer(0)),
2370            )]))
2371            .unwrap(),
2372            lifecycle: true,
2373            effect: None,
2374        }
2375    }
2376
2377    #[test]
2378    fn effect_descriptors_require_current_platform_lifecycle_and_budgets() {
2379        let mut descriptor = descriptor();
2380        descriptor.effect = Some(EffectPrimitiveDescriptor {
2381            platforms: BTreeSet::from([PrimitivePlatform::current()]),
2382            max_instances: 8,
2383            max_cost_per_instance: 4_096,
2384            reduced_motion: true,
2385            quality_tiers: true,
2386        });
2387        validate_descriptor(&descriptor).unwrap();
2388        descriptor.lifecycle = false;
2389        assert!(matches!(
2390            validate_descriptor(&descriptor),
2391            Err(PrimitiveError::EffectRequiresLifecycle(_))
2392        ));
2393    }
2394
2395    #[test]
2396    fn custom_primitive_props_and_keys_are_validated() {
2397        let registry = PrimitiveRegistry::new();
2398        let descriptor = descriptor();
2399        let id = descriptor.id.clone();
2400        registry.register(descriptor, TestHandler).unwrap();
2401        assert!(matches!(
2402            registry.create_node(
2403                &id,
2404                None,
2405                &Map::from_iter([("value".into(), Dynamic::from("source"))]),
2406                ScriptGeneration::initial(),
2407            ),
2408            Err(PrimitiveError::MissingKey(_))
2409        ));
2410        registry
2411            .create_node(
2412                &id,
2413                Some("editor".to_owned()),
2414                &Map::from_iter([("value".into(), Dynamic::from("source"))]),
2415                ScriptGeneration::initial(),
2416            )
2417            .unwrap();
2418    }
2419
2420    #[test]
2421    fn custom_primitive_events_are_normalized() {
2422        let registry = PrimitiveRegistry::new();
2423        let descriptor = descriptor();
2424        let id = descriptor.id.clone();
2425        registry.register(descriptor, TestHandler).unwrap();
2426        assert_eq!(
2427            registry
2428                .normalize_event(&id, "change", UiValue::String("new".to_owned()))
2429                .unwrap(),
2430            UiValue::String("new".to_owned())
2431        );
2432        assert!(matches!(
2433            registry.normalize_event(&id, "change", UiValue::Bool(true)),
2434            Err(PrimitiveError::InvalidEvent { .. })
2435        ));
2436    }
2437
2438    #[test]
2439    fn native_panics_are_converted_to_primitive_errors() {
2440        let id = PrimitiveId::parse("my_app.crash").unwrap();
2441        assert!(matches!(
2442            guard_primitive_panic(&id, "render", || panic!("boom")),
2443            Err(PrimitiveError::Panic {
2444                phase: "render",
2445                ..
2446            })
2447        ));
2448    }
2449
2450    #[test]
2451    fn primitive_resource_scope_rolls_back_and_continues_after_cleanup_panic() {
2452        let scope = PrimitiveResourceScope::new();
2453        let retained = Rc::new(Cell::new(0));
2454        let retained_cleanup = Rc::clone(&retained);
2455        scope
2456            .own("retained", move || retained_cleanup.set(1))
2457            .unwrap();
2458        let checkpoint = scope.checkpoint().unwrap();
2459        let order = Rc::new(RefCell::new(Vec::new()));
2460        let first = Rc::clone(&order);
2461        scope
2462            .own("first", move || first.borrow_mut().push(1))
2463            .unwrap();
2464        scope.own("panic", || panic!("cleanup failed")).unwrap();
2465        let last = Rc::clone(&order);
2466        scope
2467            .own("last", move || last.borrow_mut().push(3))
2468            .unwrap();
2469
2470        assert!(matches!(
2471            scope.rollback(checkpoint),
2472            Err(PrimitiveResourceError::CleanupPanic { ref label }) if label == "panic"
2473        ));
2474        assert_eq!(*order.borrow(), vec![3, 1]);
2475        assert_eq!(scope.active_count(), 1);
2476        scope.close().unwrap();
2477        assert_eq!(retained.get(), 1);
2478    }
2479
2480    #[test]
2481    fn successful_tree_cleanup_unmounts_removed_keyed_instances() {
2482        struct UnmountCounter(Rc<Cell<usize>>);
2483        impl PrimitiveHandler for UnmountCounter {
2484            fn render(
2485                &mut self,
2486                _: &PrimitiveInstance,
2487                _: &PrimitiveContext,
2488                _: &PrimitiveTheme,
2489                _: &mut Window,
2490                _: &mut App,
2491            ) -> Result<AnyElement, String> {
2492                Ok(div().into_any_element())
2493            }
2494
2495            fn unmount(&mut self, _: &PrimitiveInstanceId) {
2496                self.0.set(self.0.get() + 1);
2497            }
2498        }
2499
2500        let registry = PrimitiveRegistry::new();
2501        let descriptor = descriptor();
2502        let node = PrimitiveNode {
2503            primitive: descriptor.id.clone(),
2504            key: Some("editor".to_owned()),
2505            props: PrimitiveProps::new(),
2506        };
2507        let mut tree = crate::RetainedUiTree::new();
2508        tree.reconcile(UiNode::custom(node.clone())).unwrap();
2509        let instance = collect_primitive_instances(&tree)
2510            .into_iter()
2511            .next()
2512            .unwrap();
2513        let unmounted = Rc::new(Cell::new(0));
2514        let cleaned = Rc::new(Cell::new(0));
2515        let resources = PrimitiveResourceScope::new();
2516        let cleanup = Rc::clone(&cleaned);
2517        resources
2518            .own("watcher", move || cleanup.set(cleanup.get() + 1))
2519            .unwrap();
2520        registry
2521            .register(descriptor, UnmountCounter(Rc::clone(&unmounted)))
2522            .unwrap();
2523        registry.inner.borrow_mut().mounted.insert(
2524            instance.clone(),
2525            PrimitiveInstance {
2526                id: Some(instance.clone()),
2527                node,
2528                resources: Some(resources),
2529                focus_handle: None,
2530            },
2531        );
2532        tree.reconcile(UiNode::text("removed")).unwrap();
2533        registry.retain_tree(&tree).unwrap();
2534        assert_eq!(unmounted.get(), 1);
2535        assert_eq!(cleaned.get(), 1);
2536    }
2537
2538    #[test]
2539    fn primitive_identity_uses_retained_node_not_component_local_key() {
2540        let primitive = PrimitiveId::parse("my_app.editor").unwrap();
2541        let branch = |branch: &str| {
2542            UiNode::box_node(vec![UiNode::custom(PrimitiveNode {
2543                primitive: primitive.clone(),
2544                key: Some("editor".to_owned()),
2545                props: PrimitiveProps::new().with(
2546                    "branch",
2547                    PrimitiveValue::Data(UiValue::String(branch.to_owned())),
2548                ),
2549            })])
2550            .with_key(branch)
2551        };
2552        let mut tree = crate::RetainedUiTree::new();
2553        tree.reconcile(UiNode::box_node(vec![branch("left"), branch("right")]))
2554            .unwrap();
2555        let before = collect_primitive_instances(&tree);
2556        assert_eq!(before.len(), 2);
2557        assert!(before.iter().all(|instance| instance.key() == "editor"));
2558        assert_eq!(
2559            before
2560                .iter()
2561                .map(PrimitiveInstanceId::node)
2562                .collect::<BTreeSet<_>>()
2563                .len(),
2564            2
2565        );
2566
2567        tree.reconcile(UiNode::box_node(vec![branch("right"), branch("left")]))
2568            .unwrap();
2569        assert_eq!(collect_primitive_instances(&tree), before);
2570    }
2571
2572    #[test]
2573    fn primitive_identity_uses_the_presented_retained_key() {
2574        let primitive = PrimitiveId::parse("my_app.editor").unwrap();
2575        let node = UiNode::custom(PrimitiveNode {
2576            primitive,
2577            key: Some("constructor-key".to_owned()),
2578            props: PrimitiveProps::new(),
2579        })
2580        .with_key("presented-key");
2581        let mut tree = crate::RetainedUiTree::new();
2582        tree.reconcile(node).unwrap();
2583        let instance = collect_primitive_instances(&tree)
2584            .into_iter()
2585            .next()
2586            .unwrap();
2587        assert_eq!(instance.key(), "presented-key");
2588    }
2589
2590    #[test]
2591    fn primitive_context_holds_only_a_weak_registry_reference() {
2592        let registry = PrimitiveRegistry::new();
2593        let weak = Rc::downgrade(&registry.inner);
2594        let emitter = PrimitiveContext {
2595            registry: Rc::downgrade(&registry.inner),
2596            primitive: PrimitiveId::parse("my_app.editor").unwrap(),
2597            event_schemas: BTreeMap::new(),
2598            callbacks: BTreeMap::new(),
2599            dispatcher: None,
2600            interactions: crate::interaction::WindowInteractionCoordinator::default(),
2601            scroll_handles: Vec::new(),
2602            view_id: "test".to_owned(),
2603            instance: None,
2604            retained_node: None,
2605            focus_handle: None,
2606        };
2607        assert_eq!(Rc::strong_count(&registry.inner), 1);
2608        drop(registry);
2609        assert!(weak.upgrade().is_none());
2610        drop(emitter);
2611    }
2612}