Skip to main content

gpui_rhai/
primitive.rs

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