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 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#[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 #[must_use]
646 pub const fn resources(&self) -> Option<&PrimitiveResourceScope> {
647 self.resources.as_ref()
648 }
649
650 #[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 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 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
855pub 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 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 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 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 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 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 #[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 fn effect_cost(&self, _instance: &PrimitiveInstance) -> usize {
1174 1
1175 }
1176
1177 fn accessibility(
1181 &self,
1182 _instance: &PrimitiveInstanceId,
1183 _cx: &App,
1184 ) -> Option<PrimitiveAccessibilityProjection> {
1185 None
1186 }
1187
1188 fn accessibility_actions(
1193 &self,
1194 _instance: &PrimitiveInstanceId,
1195 ) -> Vec<gpui::AccessibleAction> {
1196 Vec::new()
1197 }
1198
1199 fn uses_primary_focus(&self) -> bool {
1204 false
1205 }
1206
1207 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 fn perform_key(
1232 &mut self,
1233 _instance: &PrimitiveInstanceId,
1234 _key: &str,
1235 ) -> Result<Option<PrimitiveSemanticProposal>, String> {
1236 Ok(None)
1237 }
1238
1239 fn mount(&mut self, _instance: &PrimitiveInstance) -> Result<(), String> {
1245 Ok(())
1246 }
1247
1248 fn update(
1254 &mut self,
1255 _previous: &PrimitiveInstance,
1256 _next: &PrimitiveInstance,
1257 ) -> Result<(), String> {
1258 Ok(())
1259 }
1260
1261 fn suspend(&mut self, _instance: &PrimitiveInstanceId, _cx: &mut App) {}
1265
1266 fn resume(&mut self, _instance: &PrimitiveInstanceId, _cx: &mut App) {}
1269
1270 fn commit_resume(&mut self, _instance: &PrimitiveInstanceId, _cx: &mut App) {}
1273
1274 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 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 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 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 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 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 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(®istry.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 ®istry,
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(®istry.inner);
2602 let emitter = PrimitiveContext {
2603 registry: Rc::downgrade(®istry.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(®istry.inner), 1);
2615 drop(registry);
2616 assert!(weak.upgrade().is_none());
2617 drop(emitter);
2618 }
2619}