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}
101
102#[derive(Clone, Debug, PartialEq)]
103pub enum PrimitiveValue {
104 Data(UiValue),
105 Node(Box<UiNode>),
106 Nodes(Vec<UiNode>),
107 Callback(UiEventHandler),
108 Style(Box<Style>),
109 Length(Length),
110 Asset(AssetId),
111 Signal(crate::NativeSignal),
112 Ref(crate::ElementRef),
113 Document(crate::NativeTextDocument),
114}
115
116#[derive(Clone, Debug)]
122pub struct PrimitiveTheme {
123 colors: BTreeMap<String, Rgba8>,
124 spacing: BTreeMap<SpacingToken, Length>,
125 radii: BTreeMap<RadiusToken, Length>,
126 typography: BTreeMap<String, crate::ResolvedTypography>,
127 direction: crate::TextDirection,
128}
129
130impl Default for PrimitiveTheme {
131 fn default() -> Self {
132 Self {
133 colors: BTreeMap::new(),
134 spacing: BTreeMap::new(),
135 radii: BTreeMap::new(),
136 typography: BTreeMap::new(),
137 direction: crate::TextDirection::LeftToRight,
138 }
139 }
140}
141
142impl PrimitiveTheme {
143 #[cfg(test)]
144 pub(crate) fn capture(colors: &impl ColorResolver) -> Self {
145 Self::capture_with_direction(colors, crate::TextDirection::LeftToRight)
146 }
147
148 pub(crate) fn capture_with_direction(
149 colors: &impl ColorResolver,
150 direction: crate::TextDirection,
151 ) -> Self {
152 const TOKENS: &[&str] = &[
153 "surface",
154 "surface_raised",
155 "surface_hover",
156 "text_primary",
157 "text_muted",
158 "accent",
159 "accent_hover",
160 "on_accent",
161 "danger",
162 "on_danger",
163 "warning",
164 "on_warning",
165 "success",
166 "on_success",
167 "border",
168 "focus_ring",
169 "disabled",
170 "syntax.comment",
171 "syntax.string",
172 "syntax.number",
173 "syntax.keyword",
174 "syntax.function",
175 "syntax.type",
176 "syntax.variable",
177 "syntax.constant",
178 "syntax.operator",
179 "syntax.punctuation",
180 "syntax.tag",
181 "syntax.attribute",
182 "document.search_match",
183 "document.search_current",
184 "diff.left_only",
185 "diff.right_only",
186 "diff.modified",
187 "diff.inline_left",
188 "diff.inline_right",
189 "diff.gutter",
190 "diff.fold",
191 ];
192 Self {
193 colors: TOKENS
194 .iter()
195 .filter_map(|token| {
196 colors
197 .resolve(&ColorValue::Token((*token).to_owned()))
198 .map(|value| ((*token).to_owned(), value))
199 })
200 .collect(),
201 spacing: [
202 SpacingToken::Xs,
203 SpacingToken::Sm,
204 SpacingToken::Md,
205 SpacingToken::Lg,
206 ]
207 .into_iter()
208 .filter_map(|token| {
209 colors
210 .resolve_length(Length::ThemeSpacing(token))
211 .map(|value| (token, value))
212 })
213 .collect(),
214 radii: [RadiusToken::Sm, RadiusToken::Md, RadiusToken::Lg]
215 .into_iter()
216 .filter_map(|token| {
217 colors
218 .resolve_length(Length::ThemeRadius(token))
219 .map(|value| (token, value))
220 })
221 .collect(),
222 typography: crate::REQUIRED_TYPOGRAPHY
223 .iter()
224 .filter_map(|role| {
225 colors
226 .resolve_typography(role)
227 .map(|value| ((*role).to_owned(), value))
228 })
229 .collect(),
230 direction,
231 }
232 }
233
234 #[must_use]
235 pub fn color(&self, token: &str) -> Option<Rgba8> {
236 self.colors.get(token).copied()
237 }
238
239 #[must_use]
240 pub fn resolve_color(&self, value: &ColorValue) -> Option<Rgba8> {
241 match value {
242 ColorValue::Literal(value) => Some(*value),
243 ColorValue::Token(token) => self.color(token),
244 }
245 }
246
247 #[must_use]
248 pub fn resolve_length(&self, value: Length) -> Option<Length> {
249 match value {
250 Length::ThemeSpacing(token) => self.spacing.get(&token).copied(),
251 Length::ThemeRadius(token) => self.radii.get(&token).copied(),
252 Length::Pixels(_) | Length::Rems(_) | Length::Relative(_) => Some(value),
253 }
254 }
255
256 #[must_use]
257 pub const fn direction(&self) -> crate::TextDirection {
258 self.direction
259 }
260
261 #[must_use]
262 pub fn typography(&self, role: &str) -> Option<crate::ResolvedTypography> {
263 self.typography.get(role).cloned()
264 }
265}
266
267impl ColorResolver for PrimitiveTheme {
268 fn resolve(&self, color: &ColorValue) -> Option<Rgba8> {
269 self.resolve_color(color)
270 }
271
272 fn resolve_length(&self, length: Length) -> Option<Length> {
273 PrimitiveTheme::resolve_length(self, length)
274 }
275
276 fn resolve_typography(&self, role: &str) -> Option<crate::ResolvedTypography> {
277 self.typography(role)
278 }
279}
280
281#[derive(Clone, Debug, Default, PartialEq)]
282pub struct PrimitiveProps(BTreeMap<String, PrimitiveValue>);
283
284impl PrimitiveProps {
285 #[must_use]
286 pub fn new() -> Self {
287 Self::default()
288 }
289
290 #[must_use]
291 pub fn get(&self, name: &str) -> Option<&PrimitiveValue> {
292 self.0.get(name)
293 }
294
295 pub fn insert(
296 &mut self,
297 name: impl Into<String>,
298 value: PrimitiveValue,
299 ) -> Option<PrimitiveValue> {
300 self.0.insert(name.into(), value)
301 }
302
303 #[must_use]
304 pub fn with(mut self, name: impl Into<String>, value: PrimitiveValue) -> Self {
305 self.insert(name, value);
306 self
307 }
308
309 pub fn iter(&self) -> impl Iterator<Item = (&str, &PrimitiveValue)> {
310 self.0.iter().map(|(name, value)| (name.as_str(), value))
311 }
312
313 pub(crate) fn iter_mut(&mut self) -> impl Iterator<Item = (&str, &mut PrimitiveValue)> {
314 self.0
315 .iter_mut()
316 .map(|(name, value)| (name.as_str(), value))
317 }
318
319 pub(crate) fn bind_component_scope(
320 &mut self,
321 component: &crate::ComponentInstancePath,
322 incarnation: crate::ComponentIncarnation,
323 events: &BTreeMap<String, EventSchema>,
324 native_context: Option<&crate::invocation::ScriptInvocationContext>,
325 ) {
326 for value in self.0.values_mut() {
327 match value {
328 PrimitiveValue::Callback(callback) => {
329 if let Some(callback) = callback.as_script_mut() {
330 callback.bind_component_scope_if_unset(
331 component,
332 incarnation,
333 events.clone(),
334 );
335 if let Some(context) = native_context {
336 callback.bind_native_context_if_unset(context.clone());
337 }
338 }
339 }
340 PrimitiveValue::Node(node) => {
341 node.bind_component_scope(component, incarnation, events, native_context);
342 }
343 PrimitiveValue::Nodes(nodes) => {
344 for node in nodes {
345 node.bind_component_scope(component, incarnation, events, native_context);
346 }
347 }
348 PrimitiveValue::Data(_)
349 | PrimitiveValue::Style(_)
350 | PrimitiveValue::Length(_)
351 | PrimitiveValue::Asset(_)
352 | PrimitiveValue::Signal(_)
353 | PrimitiveValue::Ref(_)
354 | PrimitiveValue::Document(_) => {}
355 }
356 }
357 }
358}
359
360#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
361pub struct PrimitiveInstanceId {
362 primitive: PrimitiveId,
363 key: String,
364 node: crate::NodeId,
365}
366
367impl PrimitiveInstanceId {
368 #[must_use]
369 pub const fn primitive(&self) -> &PrimitiveId {
370 &self.primitive
371 }
372
373 #[must_use]
374 pub fn key(&self) -> &str {
375 &self.key
376 }
377
378 #[must_use]
379 pub const fn node(&self) -> crate::NodeId {
380 self.node
381 }
382}
383
384#[derive(Clone, Debug, PartialEq)]
385pub struct PrimitiveNode {
386 pub primitive: PrimitiveId,
387 pub key: Option<String>,
388 pub props: PrimitiveProps,
389}
390
391#[derive(Clone, Debug)]
392pub struct PrimitiveInstance {
393 pub id: Option<PrimitiveInstanceId>,
394 pub node: PrimitiveNode,
395 resources: Option<PrimitiveResourceScope>,
396}
397
398impl PrimitiveInstance {
399 #[must_use]
403 pub const fn resources(&self) -> Option<&PrimitiveResourceScope> {
404 self.resources.as_ref()
405 }
406}
407
408#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
409pub struct PrimitiveResourceHandle(u64);
410
411struct PrimitiveResourceEntry {
412 label: String,
413 cleanup: Option<Box<dyn FnOnce()>>,
414}
415
416#[derive(Default)]
417struct PrimitiveResourceState {
418 next_id: u64,
419 entries: BTreeMap<u64, PrimitiveResourceEntry>,
420}
421
422impl Drop for PrimitiveResourceState {
423 fn drop(&mut self) {
424 let entries = std::mem::take(&mut self.entries);
425 for entry in entries.into_values().rev() {
426 let _ = run_resource_cleanup(entry);
427 }
428 }
429}
430
431#[derive(Clone, Default)]
432pub struct PrimitiveResourceScope {
433 inner: Rc<RefCell<PrimitiveResourceState>>,
434}
435
436impl fmt::Debug for PrimitiveResourceScope {
437 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
438 match self.inner.try_borrow() {
439 Ok(state) => formatter
440 .debug_struct("PrimitiveResourceScope")
441 .field(
442 "active",
443 &state
444 .entries
445 .values()
446 .map(|entry| entry.label.as_str())
447 .collect::<Vec<_>>(),
448 )
449 .finish(),
450 Err(_) => formatter.write_str("PrimitiveResourceScope(<borrowed>)"),
451 }
452 }
453}
454
455impl PrimitiveResourceScope {
456 #[must_use]
457 pub fn new() -> Self {
458 Self::default()
459 }
460
461 pub fn own(
468 &self,
469 label: impl Into<String>,
470 cleanup: impl FnOnce() + 'static,
471 ) -> Result<PrimitiveResourceHandle, PrimitiveResourceError> {
472 let label = label.into();
473 if label.is_empty()
474 || label.len() > 128
475 || !label.chars().all(|character| {
476 character.is_ascii_alphanumeric() || matches!(character, '_' | '-' | '.' | ':')
477 })
478 {
479 return Err(PrimitiveResourceError::InvalidLabel(label));
480 }
481 let mut state = self
482 .inner
483 .try_borrow_mut()
484 .map_err(|_| PrimitiveResourceError::Borrowed)?;
485 let id = state.next_id.max(1);
486 state.next_id = id
487 .checked_add(1)
488 .ok_or(PrimitiveResourceError::IdExhausted)?;
489 state.entries.insert(
490 id,
491 PrimitiveResourceEntry {
492 label,
493 cleanup: Some(Box::new(cleanup)),
494 },
495 );
496 Ok(PrimitiveResourceHandle(id))
497 }
498
499 pub fn cancel(&self, handle: &PrimitiveResourceHandle) -> Result<bool, PrimitiveResourceError> {
505 let entry = self
506 .inner
507 .try_borrow_mut()
508 .map_err(|_| PrimitiveResourceError::Borrowed)?
509 .entries
510 .remove(&handle.0);
511 let Some(entry) = entry else {
512 return Ok(false);
513 };
514 run_resource_cleanup(entry)?;
515 Ok(true)
516 }
517
518 #[must_use]
519 pub fn active_count(&self) -> usize {
520 self.inner.borrow().entries.len()
521 }
522
523 fn checkpoint(&self) -> Result<u64, PrimitiveResourceError> {
524 self.inner
525 .try_borrow()
526 .map(|state| state.next_id.max(1))
527 .map_err(|_| PrimitiveResourceError::Borrowed)
528 }
529
530 fn rollback(&self, checkpoint: u64) -> Result<(), PrimitiveResourceError> {
531 self.cleanup_where(|id| id >= checkpoint)
532 }
533
534 fn close(&self) -> Result<(), PrimitiveResourceError> {
535 self.cleanup_where(|_| true)
536 }
537
538 fn cleanup_where(&self, predicate: impl Fn(u64) -> bool) -> Result<(), PrimitiveResourceError> {
539 let mut entries = {
540 let mut state = self
541 .inner
542 .try_borrow_mut()
543 .map_err(|_| PrimitiveResourceError::Borrowed)?;
544 let ids = state
545 .entries
546 .keys()
547 .copied()
548 .filter(|id| predicate(*id))
549 .collect::<Vec<_>>();
550 ids.into_iter()
551 .rev()
552 .filter_map(|id| state.entries.remove(&id))
553 .collect::<Vec<_>>()
554 };
555 let mut first_error = None;
556 for entry in entries.drain(..) {
557 if let Err(error) = run_resource_cleanup(entry)
558 && first_error.is_none()
559 {
560 first_error = Some(error);
561 }
562 }
563 first_error.map_or(Ok(()), Err)
564 }
565}
566
567fn run_resource_cleanup(mut entry: PrimitiveResourceEntry) -> Result<(), PrimitiveResourceError> {
568 let label = entry.label;
569 let Some(cleanup) = entry.cleanup.take() else {
570 return Ok(());
571 };
572 catch_unwind(AssertUnwindSafe(cleanup))
573 .map_err(|_| PrimitiveResourceError::CleanupPanic { label })
574}
575
576#[derive(Clone, Debug, Error, Eq, PartialEq)]
577pub enum PrimitiveResourceError {
578 #[error("primitive resource label `{0}` must be 1-128 safe ASCII characters")]
579 InvalidLabel(String),
580 #[error("primitive resource scope is already borrowed")]
581 Borrowed,
582 #[error("primitive resource scope exhausted its handle identity space")]
583 IdExhausted,
584 #[error("primitive resource cleanup `{label}` panicked")]
585 CleanupPanic { label: String },
586}
587
588#[derive(Clone)]
589pub struct PrimitiveEventEmitter {
590 registry: Weak<RefCell<PrimitiveRegistryInner>>,
591 primitive: PrimitiveId,
592 callbacks: BTreeMap<String, UiEventHandler>,
593 dispatcher: Option<NodeEventDispatcher>,
594}
595
596impl PrimitiveEventEmitter {
597 pub fn emit(
604 &self,
605 event: &str,
606 payload: UiValue,
607 window: &mut Window,
608 cx: &mut App,
609 ) -> Result<(), PrimitiveError> {
610 let registry = PrimitiveRegistry {
611 inner: self
612 .registry
613 .upgrade()
614 .ok_or(PrimitiveError::RegistryReleased)?,
615 };
616 let payload = registry.normalize_event(&self.primitive, event, payload)?;
617 if let Some(handler) = self.callbacks.get(event) {
618 match handler {
619 UiEventHandler::Script(callback) => {
620 if let Some(dispatcher) = self.dispatcher.as_ref() {
621 dispatcher.dispatch(callback.clone(), payload, None, window, cx);
622 }
623 }
624 UiEventHandler::Host(callback) => {
625 callback.invoke(payload, window, cx);
626 }
627 UiEventHandler::Native(reference) => {
628 if let Some(dispatcher) = self.dispatcher.as_ref() {
629 dispatcher.dispatch_native(
630 reference.clone(),
631 event.to_owned(),
632 payload,
633 None,
634 window,
635 cx,
636 );
637 }
638 }
639 }
640 }
641 Ok(())
642 }
643
644 pub fn write_signal(
651 &self,
652 signal: &crate::NativeSignal,
653 value: crate::SignalValue,
654 cx: &mut App,
655 ) -> Result<bool, crate::SignalError> {
656 self.dispatcher.as_ref().map_or_else(
657 || Err(crate::SignalError::Stale(signal.id().clone())),
658 |dispatcher| dispatcher.write_signal(signal.clone(), value, cx),
659 )
660 }
661
662 #[must_use]
664 pub fn element_bounds(
665 &self,
666 reference: &crate::ElementRef,
667 cx: &App,
668 ) -> Option<crate::GeometryBounds> {
669 self.dispatcher
670 .as_ref()
671 .and_then(|dispatcher| dispatcher.element_bounds(reference, cx))
672 }
673}
674
675pub trait PrimitiveHandler {
676 fn mount(&mut self, _instance: &PrimitiveInstance) -> Result<(), String> {
682 Ok(())
683 }
684
685 fn update(
691 &mut self,
692 _previous: &PrimitiveInstance,
693 _next: &PrimitiveInstance,
694 ) -> Result<(), String> {
695 Ok(())
696 }
697
698 fn render(
704 &mut self,
705 instance: &PrimitiveInstance,
706 events: &PrimitiveEventEmitter,
707 theme: &PrimitiveTheme,
708 window: &mut Window,
709 cx: &mut App,
710 ) -> Result<AnyElement, String>;
711
712 fn unmount(&mut self, _instance: &PrimitiveInstanceId) {}
714}
715
716struct PrimitiveEntry {
717 descriptor: PrimitiveDescriptor,
718 handler: Box<dyn PrimitiveHandler>,
719}
720
721#[derive(Default)]
722struct PrimitiveRegistryInner {
723 entries: BTreeMap<PrimitiveId, PrimitiveEntry>,
724 mounted: BTreeMap<PrimitiveInstanceId, PrimitiveInstance>,
725}
726
727#[derive(Clone, Default)]
728pub struct PrimitiveRegistry {
729 inner: Rc<RefCell<PrimitiveRegistryInner>>,
730}
731
732impl fmt::Debug for PrimitiveRegistry {
733 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
734 match self.inner.try_borrow() {
735 Ok(inner) => formatter
736 .debug_struct("PrimitiveRegistry")
737 .field("registered", &inner.entries.keys().collect::<Vec<_>>())
738 .field("mounted", &inner.mounted.keys().collect::<Vec<_>>())
739 .finish(),
740 Err(_) => formatter.write_str("PrimitiveRegistry(<borrowed>)"),
741 }
742 }
743}
744
745impl PrimitiveRegistry {
746 #[must_use]
747 pub fn new() -> Self {
748 Self::default()
749 }
750
751 pub fn register(
758 &self,
759 descriptor: PrimitiveDescriptor,
760 handler: impl PrimitiveHandler + 'static,
761 ) -> Result<(), PrimitiveError> {
762 validate_descriptor(&descriptor)?;
763 let mut inner = self
764 .inner
765 .try_borrow_mut()
766 .map_err(|_| PrimitiveError::Borrowed)?;
767 if inner.entries.contains_key(&descriptor.id) {
768 return Err(PrimitiveError::Duplicate(descriptor.id));
769 }
770 if inner.entries.values().any(|entry| {
771 entry.descriptor.id.namespace() == descriptor.id.namespace()
772 && entry.descriptor.export == descriptor.export
773 }) {
774 return Err(PrimitiveError::DuplicateExport {
775 namespace: descriptor.id.namespace().to_owned(),
776 export: descriptor.export,
777 });
778 }
779 inner.entries.insert(
780 descriptor.id.clone(),
781 PrimitiveEntry {
782 descriptor,
783 handler: Box::new(handler),
784 },
785 );
786 Ok(())
787 }
788
789 pub fn create_node(
796 &self,
797 id: &PrimitiveId,
798 key: Option<String>,
799 props: &Map,
800 generation: ScriptGeneration,
801 ) -> Result<UiNode, PrimitiveError> {
802 let inner = self
803 .inner
804 .try_borrow()
805 .map_err(|_| PrimitiveError::Borrowed)?;
806 let descriptor = &inner
807 .entries
808 .get(id)
809 .ok_or_else(|| PrimitiveError::Unknown(id.clone()))?
810 .descriptor;
811 if (descriptor.lifecycle || !descriptor.state.is_empty()) && key.is_none() {
812 return Err(PrimitiveError::MissingKey(id.clone()));
813 }
814 let schema = ValueSchema::object(descriptor.props.clone());
815 schema
816 .validate(&Dynamic::from_map(props.clone()))
817 .map_err(|source| PrimitiveError::InvalidProps {
818 primitive: id.clone(),
819 source,
820 })?;
821 let props = convert_props(&descriptor.props, props, generation)?;
822 Ok(UiNode::custom(PrimitiveNode {
823 primitive: id.clone(),
824 key,
825 props,
826 }))
827 }
828
829 pub fn normalize_event(
835 &self,
836 id: &PrimitiveId,
837 event: &str,
838 payload: UiValue,
839 ) -> Result<UiValue, PrimitiveError> {
840 let inner = self
841 .inner
842 .try_borrow()
843 .map_err(|_| PrimitiveError::Borrowed)?;
844 let descriptor = &inner
845 .entries
846 .get(id)
847 .ok_or_else(|| PrimitiveError::Unknown(id.clone()))?
848 .descriptor;
849 let schema = descriptor
850 .events
851 .get(event)
852 .ok_or_else(|| PrimitiveError::UnknownEvent {
853 primitive: id.clone(),
854 event: event.to_owned(),
855 })?;
856 schema
857 .payload
858 .validate_ui_value(&payload)
859 .map_err(|source| PrimitiveError::InvalidEvent {
860 primitive: id.clone(),
861 event: event.to_owned(),
862 source,
863 })?;
864 Ok(payload)
865 }
866
867 pub fn retain_mounted(
873 &self,
874 active: &BTreeSet<PrimitiveInstanceId>,
875 ) -> Result<(), PrimitiveError> {
876 let mut inner = self
877 .inner
878 .try_borrow_mut()
879 .map_err(|_| PrimitiveError::Borrowed)?;
880 let removed = inner
881 .mounted
882 .keys()
883 .filter(|instance| !active.contains(*instance))
884 .cloned()
885 .collect::<Vec<_>>();
886 let mut first_error = None;
887 for instance in &removed {
888 let resources = inner
889 .mounted
890 .get(instance)
891 .and_then(|mounted| mounted.resources.clone());
892 if let Some(entry) = inner.entries.get_mut(&instance.primitive)
893 && let Err(error) = guard_primitive_panic(&instance.primitive, "unmount", || {
894 entry.handler.unmount(instance);
895 })
896 && first_error.is_none()
897 {
898 first_error = Some(error);
899 }
900 if let Some(resources) = resources
901 && let Err(error) = resources.close()
902 && first_error.is_none()
903 {
904 first_error = Some(PrimitiveError::Resource(error));
905 }
906 inner.mounted.remove(instance);
907 }
908 first_error.map_or(Ok(()), Err)
909 }
910
911 pub fn retain_tree(&self, tree: &crate::RetainedUiTree) -> Result<(), PrimitiveError> {
917 let active = collect_primitive_instances(tree);
918 self.retain_mounted(&active)
919 }
920
921 pub(crate) fn element(
922 &self,
923 node: PrimitiveNode,
924 retained_id: Option<crate::NodeId>,
925 fallback: Option<UiNode>,
926 dispatcher: Option<NodeEventDispatcher>,
927 theme: PrimitiveTheme,
928 ) -> AnyElement {
929 RegisteredPrimitiveElement {
930 registry: self.clone(),
931 node,
932 retained_id,
933 fallback,
934 dispatcher,
935 theme,
936 }
937 .into_any_element()
938 }
939
940 fn render_instance(
941 &self,
942 node: PrimitiveNode,
943 retained_id: Option<crate::NodeId>,
944 events: &PrimitiveEventEmitter,
945 theme: &PrimitiveTheme,
946 window: &mut Window,
947 cx: &mut App,
948 ) -> Result<AnyElement, PrimitiveError> {
949 let mut inner = self
950 .inner
951 .try_borrow_mut()
952 .map_err(|_| PrimitiveError::Borrowed)?;
953 let retained_instance = primitive_is_retained(&inner, &node.primitive)?;
954 if retained_instance && retained_id.is_none() {
955 return Err(PrimitiveError::MissingRetainedIdentity(node.primitive));
956 }
957 let instance_id = retained_instance.then(|| PrimitiveInstanceId {
958 primitive: node.primitive.clone(),
959 key: node
960 .key
961 .clone()
962 .expect("retained primitive descriptors require a key"),
963 node: retained_id.expect("retained primitive renderer supplies NodeId"),
964 });
965 let previous = instance_id
966 .as_ref()
967 .and_then(|id| inner.mounted.get(id))
968 .cloned();
969 let resources = if retained_instance {
970 Some(
971 previous
972 .as_ref()
973 .and_then(|instance| instance.resources.clone())
974 .unwrap_or_default(),
975 )
976 } else {
977 None
978 };
979 let checkpoint = resources
980 .as_ref()
981 .map(PrimitiveResourceScope::checkpoint)
982 .transpose()?;
983 let instance = PrimitiveInstance {
984 id: instance_id.clone(),
985 node,
986 resources: resources.clone(),
987 };
988 let needs_mount = instance_id.is_some() && previous.is_none();
989 let entry = inner
990 .entries
991 .get_mut(&instance.node.primitive)
992 .ok_or_else(|| PrimitiveError::Unknown(instance.node.primitive.clone()))?;
993 let operation = (|| {
994 if needs_mount {
995 guard_primitive_panic(&instance.node.primitive, "mount", || {
996 entry.handler.mount(&instance)
997 })?
998 .map_err(|message| PrimitiveError::Handler {
999 primitive: instance.node.primitive.clone(),
1000 message,
1001 })?;
1002 }
1003 if let Some(previous) = &previous
1004 && previous.node != instance.node
1005 {
1006 guard_primitive_panic(&instance.node.primitive, "update", || {
1007 entry.handler.update(previous, &instance)
1008 })?
1009 .map_err(|message| PrimitiveError::Handler {
1010 primitive: instance.node.primitive.clone(),
1011 message,
1012 })?;
1013 }
1014 guard_primitive_panic(&instance.node.primitive, "render", || {
1015 entry.handler.render(&instance, events, theme, window, cx)
1016 })?
1017 .map_err(|message| PrimitiveError::Handler {
1018 primitive: instance.node.primitive.clone(),
1019 message,
1020 })
1021 })();
1022 let element = match operation {
1023 Ok(element) => element,
1024 Err(error) => {
1025 return Err(rollback_failed_primitive_operation(
1026 entry,
1027 &instance,
1028 needs_mount,
1029 resources.as_ref(),
1030 checkpoint,
1031 error,
1032 ));
1033 }
1034 };
1035 if let Some(id) = instance_id {
1036 inner.mounted.insert(id, instance);
1037 }
1038 Ok(element)
1039 }
1040}
1041
1042fn rollback_failed_primitive_operation(
1043 entry: &mut PrimitiveEntry,
1044 instance: &PrimitiveInstance,
1045 needs_unmount: bool,
1046 resources: Option<&PrimitiveResourceScope>,
1047 checkpoint: Option<u64>,
1048 original: PrimitiveError,
1049) -> PrimitiveError {
1050 let mut rollback_error = None;
1051 if needs_unmount
1052 && let Some(instance_id) = instance.id.as_ref()
1053 && let Err(error) =
1054 guard_primitive_panic(&instance.node.primitive, "failed-mount unmount", || {
1055 entry.handler.unmount(instance_id);
1056 })
1057 {
1058 rollback_error = Some(error);
1059 }
1060 if let (Some(resources), Some(checkpoint)) = (resources, checkpoint)
1061 && let Err(error) = resources.rollback(checkpoint)
1062 && rollback_error.is_none()
1063 {
1064 rollback_error = Some(PrimitiveError::Resource(error));
1065 }
1066 rollback_error.unwrap_or(original)
1067}
1068
1069fn primitive_is_retained(
1070 inner: &PrimitiveRegistryInner,
1071 primitive: &PrimitiveId,
1072) -> Result<bool, PrimitiveError> {
1073 let descriptor = &inner
1074 .entries
1075 .get(primitive)
1076 .ok_or_else(|| PrimitiveError::Unknown(primitive.clone()))?
1077 .descriptor;
1078 Ok(descriptor.lifecycle || !descriptor.state.is_empty())
1079}
1080
1081fn collect_primitive_instances(tree: &crate::RetainedUiTree) -> BTreeSet<PrimitiveInstanceId> {
1082 tree.nodes()
1083 .filter_map(|node| {
1084 Some(PrimitiveInstanceId {
1085 primitive: node.primitive()?.clone(),
1086 key: node.key()?.to_owned(),
1087 node: node.id(),
1088 })
1089 })
1090 .collect()
1091}
1092
1093fn guard_primitive_panic<T>(
1094 primitive: &PrimitiveId,
1095 phase: &'static str,
1096 operation: impl FnOnce() -> T,
1097) -> Result<T, PrimitiveError> {
1098 catch_unwind(AssertUnwindSafe(operation)).map_err(|_| PrimitiveError::Panic {
1099 primitive: primitive.clone(),
1100 phase,
1101 })
1102}
1103
1104#[derive(gpui::IntoElement)]
1105struct RegisteredPrimitiveElement {
1106 registry: PrimitiveRegistry,
1107 node: PrimitiveNode,
1108 retained_id: Option<crate::NodeId>,
1109 fallback: Option<UiNode>,
1110 dispatcher: Option<NodeEventDispatcher>,
1111 theme: PrimitiveTheme,
1112}
1113
1114impl RenderOnce for RegisteredPrimitiveElement {
1115 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
1116 let registry = self.registry;
1117 let callbacks = self
1118 .node
1119 .props
1120 .iter()
1121 .filter_map(|(name, value)| {
1122 name.strip_prefix("on_").and_then(|event| match value {
1123 PrimitiveValue::Callback(callback) => {
1124 Some((event.to_owned(), callback.clone()))
1125 }
1126 _ => None,
1127 })
1128 })
1129 .collect();
1130 let events = PrimitiveEventEmitter {
1131 registry: Rc::downgrade(®istry.inner),
1132 primitive: self.node.primitive.clone(),
1133 callbacks,
1134 dispatcher: self.dispatcher,
1135 };
1136 match registry.render_instance(
1137 self.node,
1138 self.retained_id,
1139 &events,
1140 &self.theme,
1141 window,
1142 cx,
1143 ) {
1144 Ok(element) => element,
1145 Err(error) => self.fallback.map_or_else(
1146 || {
1147 div()
1148 .child(format!("Custom primitive error: {error}"))
1149 .into_any_element()
1150 },
1151 |fallback| {
1152 crate::GpuiNodeRenderer::render_with_primitives(
1153 &fallback,
1154 &crate::LiteralColorResolver,
1155 &crate::InteractionState::default(),
1156 ®istry,
1157 )
1158 },
1159 ),
1160 }
1161 }
1162}
1163
1164fn validate_descriptor(descriptor: &PrimitiveDescriptor) -> Result<(), PrimitiveError> {
1165 if !is_pascal_case(&descriptor.export) {
1166 return Err(PrimitiveError::InvalidExport(descriptor.export.clone()));
1167 }
1168 for (name, field) in &descriptor.props {
1169 if !is_identifier(name) {
1170 return Err(PrimitiveError::InvalidPropName(name.clone()));
1171 }
1172 field
1173 .schema
1174 .validate_definition()
1175 .map_err(|source| PrimitiveError::InvalidSchema {
1176 location: format!("prop `{name}`"),
1177 source,
1178 })?;
1179 if let Some(default) = &field.default {
1180 field.schema.validate_ui_value(default).map_err(|source| {
1181 PrimitiveError::InvalidDefault {
1182 prop: name.clone(),
1183 source,
1184 }
1185 })?;
1186 }
1187 }
1188 for (name, event) in &descriptor.events {
1189 event
1190 .payload
1191 .validate_definition()
1192 .map_err(|source| PrimitiveError::InvalidSchema {
1193 location: format!("event `{name}`"),
1194 source,
1195 })?;
1196 let callback = format!("on_{name}");
1197 if !descriptor
1198 .props
1199 .get(&callback)
1200 .is_some_and(|field| schema_accepts_callback(&field.schema))
1201 {
1202 return Err(PrimitiveError::MissingEventCallback {
1203 event: name.clone(),
1204 prop: callback,
1205 });
1206 }
1207 }
1208 ComponentStateSchema::new(descriptor.state.fields().clone())
1209 .map_err(|source| PrimitiveError::InvalidStateSchema(source.to_string()))?;
1210 Ok(())
1211}
1212
1213fn schema_accepts_callback(schema: &ValueSchema) -> bool {
1214 matches!(schema, ValueSchema::Callback)
1215 || matches!(schema, ValueSchema::Optional { value } if schema_accepts_callback(value))
1216 || matches!(schema, ValueSchema::OneOf { variants } if variants.iter().any(schema_accepts_callback))
1217}
1218
1219fn convert_props(
1220 schema: &BTreeMap<String, ObjectField>,
1221 values: &Map,
1222 generation: ScriptGeneration,
1223) -> Result<PrimitiveProps, PrimitiveError> {
1224 let mut converted = BTreeMap::new();
1225 for (name, field) in schema {
1226 let value = values
1227 .get(name.as_str())
1228 .cloned()
1229 .or_else(|| field.default.clone().map(UiValue::into_dynamic));
1230 if let Some(value) = value {
1231 converted.insert(
1232 name.clone(),
1233 convert_prop(&field.schema, value, generation).map_err(|source| {
1234 PrimitiveError::PropConversion {
1235 prop: name.clone(),
1236 source,
1237 }
1238 })?,
1239 );
1240 }
1241 }
1242 Ok(PrimitiveProps(converted))
1243}
1244
1245fn convert_prop(
1246 schema: &ValueSchema,
1247 value: Dynamic,
1248 generation: ScriptGeneration,
1249) -> Result<PrimitiveValue, PrimitivePropConversionError> {
1250 match schema {
1251 ValueSchema::Optional { value: inner } if value.is_unit() => {
1252 Ok(PrimitiveValue::Data(UiValue::Null))
1253 }
1254 ValueSchema::Optional { value: inner } => convert_prop(inner, value, generation),
1255 ValueSchema::OneOf { variants } => {
1256 let branch = variants
1257 .iter()
1258 .find(|variant| variant.validate(&value).is_ok())
1259 .expect("validated primitive one_of prop matches one branch");
1260 convert_prop(branch, value, generation)
1261 }
1262 ValueSchema::Node => Ok(PrimitiveValue::Node(Box::new(value.cast::<UiNode>()))),
1263 ValueSchema::Callback if value.is::<FnPtr>() => {
1264 Ok(PrimitiveValue::Callback(UiEventHandler::Script(
1265 ScriptCallback::try_from_fn_ptr(value.cast::<FnPtr>(), generation)?,
1266 )))
1267 }
1268 ValueSchema::Callback => Ok(PrimitiveValue::Callback(UiEventHandler::Native(
1269 value.cast::<crate::NativeHandlerRef>(),
1270 ))),
1271 ValueSchema::Array { items, .. } if matches!(items.as_ref(), ValueSchema::Node) => {
1272 Ok(PrimitiveValue::Nodes(
1273 value
1274 .cast::<Array>()
1275 .into_iter()
1276 .map(Dynamic::cast::<UiNode>)
1277 .collect(),
1278 ))
1279 }
1280 ValueSchema::Style => Ok(PrimitiveValue::Style(Box::new(value.cast::<Style>()))),
1281 ValueSchema::Length => Ok(PrimitiveValue::Length(value.cast::<Length>())),
1282 ValueSchema::Asset => Ok(PrimitiveValue::Asset(value.cast::<AssetId>())),
1283 ValueSchema::Signal => Ok(PrimitiveValue::Signal(value.cast::<crate::NativeSignal>())),
1284 ValueSchema::Ref => Ok(PrimitiveValue::Ref(value.cast::<crate::ElementRef>())),
1285 ValueSchema::Document => Ok(PrimitiveValue::Document(
1286 value.cast::<crate::NativeTextDocument>(),
1287 )),
1288 _ => UiValue::from_dynamic(value)
1289 .map(PrimitiveValue::Data)
1290 .map_err(Into::into),
1291 }
1292}
1293
1294#[derive(Debug, Error)]
1295pub enum PrimitivePropConversionError {
1296 #[error(transparent)]
1297 Value(#[from] UiValueError),
1298 #[error(transparent)]
1299 Callback(#[from] crate::ScriptCallbackDefinitionError),
1300}
1301
1302#[derive(Debug, Error)]
1303pub enum PrimitiveError {
1304 #[error("primitive ID `{0}` must be `namespace.snake_case_name`")]
1305 InvalidId(String),
1306 #[error("primitive export `{0}` must be PascalCase")]
1307 InvalidExport(String),
1308 #[error("primitive prop `{0}` must be `snake_case`")]
1309 InvalidPropName(String),
1310 #[error("invalid schema definition for primitive {location}: {source}")]
1311 InvalidSchema {
1312 location: String,
1313 source: SchemaDefinitionError,
1314 },
1315 #[error("invalid primitive state schema: {0}")]
1316 InvalidStateSchema(String),
1317 #[error("primitive registry is already borrowed during rendering")]
1318 Borrowed,
1319 #[error("primitive `{0:?}` is already registered")]
1320 Duplicate(PrimitiveId),
1321 #[error("primitive export `{namespace}::{export}` is already registered")]
1322 DuplicateExport { namespace: String, export: String },
1323 #[error("primitive `{0:?}` is not registered")]
1324 Unknown(PrimitiveId),
1325 #[error("primitive `{0:?}` requires a stable key")]
1326 MissingKey(PrimitiveId),
1327 #[error("primitive `{0:?}` requires a retained NodeId renderer")]
1328 MissingRetainedIdentity(PrimitiveId),
1329 #[error("primitive event emitter outlived its registry")]
1330 RegistryReleased,
1331 #[error("props for primitive `{primitive:?}` are invalid: {source}")]
1332 InvalidProps {
1333 primitive: PrimitiveId,
1334 source: SchemaValidationError,
1335 },
1336 #[error("default for primitive prop `{prop}` is invalid: {source}")]
1337 InvalidDefault {
1338 prop: String,
1339 source: SchemaValidationError,
1340 },
1341 #[error("primitive prop `{prop}` cannot cross the runtime boundary: {source}")]
1342 PropConversion {
1343 prop: String,
1344 source: PrimitivePropConversionError,
1345 },
1346 #[error("primitive event `{event}` requires callback prop `{prop}`")]
1347 MissingEventCallback { event: String, prop: String },
1348 #[error("primitive `{primitive:?}` does not declare event `{event}`")]
1349 UnknownEvent {
1350 primitive: PrimitiveId,
1351 event: String,
1352 },
1353 #[error("primitive `{primitive:?}` event `{event}` is invalid: {source}")]
1354 InvalidEvent {
1355 primitive: PrimitiveId,
1356 event: String,
1357 source: SchemaValidationError,
1358 },
1359 #[error("primitive `{primitive:?}` handler failed: {message}")]
1360 Handler {
1361 primitive: PrimitiveId,
1362 message: String,
1363 },
1364 #[error("primitive `{primitive:?}` panicked during {phase}")]
1365 Panic {
1366 primitive: PrimitiveId,
1367 phase: &'static str,
1368 },
1369 #[error(transparent)]
1370 Resource(#[from] PrimitiveResourceError),
1371}
1372
1373#[cfg(test)]
1374mod tests {
1375 use super::*;
1376 use crate::{ObjectField, StateField};
1377 use std::cell::Cell;
1378
1379 struct TestHandler;
1380
1381 struct TestTheme;
1382
1383 impl ColorResolver for TestTheme {
1384 fn resolve(&self, color: &ColorValue) -> Option<Rgba8> {
1385 matches!(color, ColorValue::Token(token) if token == "accent")
1386 .then(|| Rgba8::from_rgba_hex(0x1234_56ff))
1387 }
1388
1389 fn resolve_length(&self, length: Length) -> Option<Length> {
1390 (length == Length::ThemeSpacing(SpacingToken::Sm)).then_some(Length::Pixels(6.0))
1391 }
1392
1393 fn resolve_typography(&self, role: &str) -> Option<crate::ResolvedTypography> {
1394 (role == "body").then(|| crate::ResolvedTypography {
1395 family: Some("JetBrains Mono".to_owned()),
1396 fallbacks: vec!["PingFang SC".to_owned()],
1397 size: Length::Pixels(12.0),
1398 line_height: Length::Pixels(16.0),
1399 weight: 400,
1400 })
1401 }
1402 }
1403
1404 impl PrimitiveHandler for TestHandler {
1405 fn render(
1406 &mut self,
1407 _: &PrimitiveInstance,
1408 _: &PrimitiveEventEmitter,
1409 _: &PrimitiveTheme,
1410 _: &mut Window,
1411 _: &mut App,
1412 ) -> Result<AnyElement, String> {
1413 Ok(div().into_any_element())
1414 }
1415 }
1416
1417 #[test]
1418 fn primitive_theme_exposes_only_resolved_semantic_snapshot() {
1419 let theme = PrimitiveTheme::capture(&TestTheme);
1420 assert_eq!(
1421 theme.color("accent"),
1422 Some(Rgba8::from_rgba_hex(0x1234_56ff))
1423 );
1424 assert_eq!(theme.color("unknown"), None);
1425 assert_eq!(
1426 theme.resolve_color(&ColorValue::Literal(Rgba8::from_rgba_hex(0xaabb_ccdd))),
1427 Some(Rgba8::from_rgba_hex(0xaabb_ccdd))
1428 );
1429 assert_eq!(
1430 theme.resolve_length(Length::ThemeSpacing(SpacingToken::Sm)),
1431 Some(Length::Pixels(6.0))
1432 );
1433 assert_eq!(
1434 theme.typography("body").unwrap().family.as_deref(),
1435 Some("JetBrains Mono")
1436 );
1437 }
1438
1439 fn descriptor() -> PrimitiveDescriptor {
1440 PrimitiveDescriptor {
1441 id: PrimitiveId::parse("my_app.code_editor").unwrap(),
1442 export: "CodeEditor".to_owned(),
1443 props: BTreeMap::from([
1444 (
1445 "value".to_owned(),
1446 ObjectField::required(ValueSchema::string()),
1447 ),
1448 (
1449 "on_change".to_owned(),
1450 ObjectField::optional(ValueSchema::optional(ValueSchema::Callback)),
1451 ),
1452 ]),
1453 events: BTreeMap::from([(
1454 "change".to_owned(),
1455 EventSchema {
1456 payload: ValueSchema::string(),
1457 },
1458 )]),
1459 state: ComponentStateSchema::new(BTreeMap::from([(
1460 "selection".to_owned(),
1461 StateField::new(ValueSchema::integer(), UiValue::Integer(0)),
1462 )]))
1463 .unwrap(),
1464 lifecycle: true,
1465 }
1466 }
1467
1468 #[test]
1469 fn custom_primitive_props_and_keys_are_validated() {
1470 let registry = PrimitiveRegistry::new();
1471 let descriptor = descriptor();
1472 let id = descriptor.id.clone();
1473 registry.register(descriptor, TestHandler).unwrap();
1474 assert!(matches!(
1475 registry.create_node(
1476 &id,
1477 None,
1478 &Map::from_iter([("value".into(), Dynamic::from("source"))]),
1479 ScriptGeneration::initial(),
1480 ),
1481 Err(PrimitiveError::MissingKey(_))
1482 ));
1483 registry
1484 .create_node(
1485 &id,
1486 Some("editor".to_owned()),
1487 &Map::from_iter([("value".into(), Dynamic::from("source"))]),
1488 ScriptGeneration::initial(),
1489 )
1490 .unwrap();
1491 }
1492
1493 #[test]
1494 fn custom_primitive_events_are_normalized() {
1495 let registry = PrimitiveRegistry::new();
1496 let descriptor = descriptor();
1497 let id = descriptor.id.clone();
1498 registry.register(descriptor, TestHandler).unwrap();
1499 assert_eq!(
1500 registry
1501 .normalize_event(&id, "change", UiValue::String("new".to_owned()))
1502 .unwrap(),
1503 UiValue::String("new".to_owned())
1504 );
1505 assert!(matches!(
1506 registry.normalize_event(&id, "change", UiValue::Bool(true)),
1507 Err(PrimitiveError::InvalidEvent { .. })
1508 ));
1509 }
1510
1511 #[test]
1512 fn native_panics_are_converted_to_primitive_errors() {
1513 let id = PrimitiveId::parse("my_app.crash").unwrap();
1514 assert!(matches!(
1515 guard_primitive_panic(&id, "render", || panic!("boom")),
1516 Err(PrimitiveError::Panic {
1517 phase: "render",
1518 ..
1519 })
1520 ));
1521 }
1522
1523 #[test]
1524 fn primitive_resource_scope_rolls_back_and_continues_after_cleanup_panic() {
1525 let scope = PrimitiveResourceScope::new();
1526 let retained = Rc::new(Cell::new(0));
1527 let retained_cleanup = Rc::clone(&retained);
1528 scope
1529 .own("retained", move || retained_cleanup.set(1))
1530 .unwrap();
1531 let checkpoint = scope.checkpoint().unwrap();
1532 let order = Rc::new(RefCell::new(Vec::new()));
1533 let first = Rc::clone(&order);
1534 scope
1535 .own("first", move || first.borrow_mut().push(1))
1536 .unwrap();
1537 scope.own("panic", || panic!("cleanup failed")).unwrap();
1538 let last = Rc::clone(&order);
1539 scope
1540 .own("last", move || last.borrow_mut().push(3))
1541 .unwrap();
1542
1543 assert!(matches!(
1544 scope.rollback(checkpoint),
1545 Err(PrimitiveResourceError::CleanupPanic { ref label }) if label == "panic"
1546 ));
1547 assert_eq!(*order.borrow(), vec![3, 1]);
1548 assert_eq!(scope.active_count(), 1);
1549 scope.close().unwrap();
1550 assert_eq!(retained.get(), 1);
1551 }
1552
1553 #[test]
1554 fn successful_tree_cleanup_unmounts_removed_keyed_instances() {
1555 struct UnmountCounter(Rc<Cell<usize>>);
1556 impl PrimitiveHandler for UnmountCounter {
1557 fn render(
1558 &mut self,
1559 _: &PrimitiveInstance,
1560 _: &PrimitiveEventEmitter,
1561 _: &PrimitiveTheme,
1562 _: &mut Window,
1563 _: &mut App,
1564 ) -> Result<AnyElement, String> {
1565 Ok(div().into_any_element())
1566 }
1567
1568 fn unmount(&mut self, _: &PrimitiveInstanceId) {
1569 self.0.set(self.0.get() + 1);
1570 }
1571 }
1572
1573 let registry = PrimitiveRegistry::new();
1574 let descriptor = descriptor();
1575 let node = PrimitiveNode {
1576 primitive: descriptor.id.clone(),
1577 key: Some("editor".to_owned()),
1578 props: PrimitiveProps::new(),
1579 };
1580 let mut tree = crate::RetainedUiTree::new();
1581 tree.reconcile(UiNode::custom(node.clone())).unwrap();
1582 let instance = collect_primitive_instances(&tree)
1583 .into_iter()
1584 .next()
1585 .unwrap();
1586 let unmounted = Rc::new(Cell::new(0));
1587 let cleaned = Rc::new(Cell::new(0));
1588 let resources = PrimitiveResourceScope::new();
1589 let cleanup = Rc::clone(&cleaned);
1590 resources
1591 .own("watcher", move || cleanup.set(cleanup.get() + 1))
1592 .unwrap();
1593 registry
1594 .register(descriptor, UnmountCounter(Rc::clone(&unmounted)))
1595 .unwrap();
1596 registry.inner.borrow_mut().mounted.insert(
1597 instance.clone(),
1598 PrimitiveInstance {
1599 id: Some(instance.clone()),
1600 node,
1601 resources: Some(resources),
1602 },
1603 );
1604 tree.reconcile(UiNode::text("removed")).unwrap();
1605 registry.retain_tree(&tree).unwrap();
1606 assert_eq!(unmounted.get(), 1);
1607 assert_eq!(cleaned.get(), 1);
1608 }
1609
1610 #[test]
1611 fn primitive_identity_uses_retained_node_not_component_local_key() {
1612 let primitive = PrimitiveId::parse("my_app.editor").unwrap();
1613 let branch = |branch: &str| {
1614 UiNode::box_node(vec![UiNode::custom(PrimitiveNode {
1615 primitive: primitive.clone(),
1616 key: Some("editor".to_owned()),
1617 props: PrimitiveProps::new().with(
1618 "branch",
1619 PrimitiveValue::Data(UiValue::String(branch.to_owned())),
1620 ),
1621 })])
1622 .with_key(branch)
1623 };
1624 let mut tree = crate::RetainedUiTree::new();
1625 tree.reconcile(UiNode::box_node(vec![branch("left"), branch("right")]))
1626 .unwrap();
1627 let before = collect_primitive_instances(&tree);
1628 assert_eq!(before.len(), 2);
1629 assert!(before.iter().all(|instance| instance.key() == "editor"));
1630 assert_eq!(
1631 before
1632 .iter()
1633 .map(PrimitiveInstanceId::node)
1634 .collect::<BTreeSet<_>>()
1635 .len(),
1636 2
1637 );
1638
1639 tree.reconcile(UiNode::box_node(vec![branch("right"), branch("left")]))
1640 .unwrap();
1641 assert_eq!(collect_primitive_instances(&tree), before);
1642 }
1643
1644 #[test]
1645 fn primitive_event_emitter_holds_only_a_weak_registry_reference() {
1646 let registry = PrimitiveRegistry::new();
1647 let weak = Rc::downgrade(®istry.inner);
1648 let emitter = PrimitiveEventEmitter {
1649 registry: Rc::downgrade(®istry.inner),
1650 primitive: PrimitiveId::parse("my_app.editor").unwrap(),
1651 callbacks: BTreeMap::new(),
1652 dispatcher: None,
1653 };
1654 assert_eq!(Rc::strong_count(®istry.inner), 1);
1655 drop(registry);
1656 assert!(weak.upgrade().is_none());
1657 drop(emitter);
1658 }
1659}