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