Skip to main content

gpui_rhai/
element_ref.rs

1use std::collections::{BTreeMap, BTreeSet};
2
3use rhai::{CustomType, ImmutableString, TypeBuilder};
4use thiserror::Error;
5
6use crate::{ComponentInstancePath, NodeId};
7
8#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
9pub struct ElementRefId {
10    component: ComponentInstancePath,
11    key: String,
12}
13
14impl ElementRefId {
15    /// Construct a stable component-local reference identity.
16    ///
17    /// # Errors
18    ///
19    /// Returns [`ElementRefError::InvalidKey`] for unsafe keys.
20    pub fn new(
21        component: ComponentInstancePath,
22        key: impl Into<String>,
23    ) -> Result<Self, ElementRefError> {
24        let key = key.into();
25        if key.is_empty()
26            || key.len() > 128
27            || !key.chars().all(|character| {
28                character.is_ascii_alphanumeric() || matches!(character, '_' | '-' | '.' | ':')
29            })
30        {
31            return Err(ElementRefError::InvalidKey(key));
32        }
33        Ok(Self { component, key })
34    }
35
36    #[must_use]
37    pub const fn component(&self) -> &ComponentInstancePath {
38        &self.component
39    }
40
41    #[must_use]
42    pub fn key(&self) -> &str {
43        &self.key
44    }
45}
46
47/// Non-owning reference to a retained node in one formal component scope.
48#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
49pub struct ElementRef {
50    id: ElementRefId,
51}
52
53impl ElementRef {
54    #[must_use]
55    pub const fn new(id: ElementRefId) -> Self {
56        Self { id }
57    }
58
59    #[must_use]
60    pub const fn id(&self) -> &ElementRefId {
61        &self.id
62    }
63}
64
65impl CustomType for ElementRef {
66    fn build(mut builder: TypeBuilder<Self>) {
67        builder
68            .with_name("ElementRef")
69            .with_get("key", |reference: &mut Self| {
70                ImmutableString::from(reference.id.key.as_str())
71            })
72            .with_get("scope", |reference: &mut Self| {
73                ImmutableString::from(format!(
74                    "{}:{}",
75                    reference.id.component(),
76                    reference.id.key()
77                ))
78            });
79    }
80}
81
82#[derive(Clone, Debug, Default)]
83pub struct ElementRefRegistry {
84    active: BTreeMap<ElementRefId, NodeId>,
85    // The logical subscription survives absence and replacement of its node.
86    geometry_readers: BTreeMap<ElementRefId, BTreeSet<crate::read_dependency::ReadDependency>>,
87    dirty: BTreeSet<ComponentInstancePath>,
88}
89
90pub(crate) type ElementRefGeometryBindings =
91    BTreeMap<NodeId, BTreeMap<ElementRefId, BTreeSet<crate::read_dependency::ReadDependency>>>;
92
93impl ElementRefRegistry {
94    #[must_use]
95    pub fn new() -> Self {
96        Self::default()
97    }
98
99    #[must_use]
100    pub fn len(&self) -> usize {
101        self.active.len()
102    }
103
104    #[must_use]
105    pub fn is_empty(&self) -> bool {
106        self.active.is_empty()
107    }
108
109    pub fn iter(&self) -> impl ExactSizeIterator<Item = (&ElementRefId, &NodeId)> {
110        self.active.iter()
111    }
112
113    /// Resolve the last committed retained identity.
114    ///
115    /// # Errors
116    ///
117    /// Returns [`ElementRefError::Stale`] after unmount or before commit.
118    pub fn resolve(&self, reference: &ElementRef) -> Result<NodeId, ElementRefError> {
119        self.active
120            .get(reference.id())
121            .copied()
122            .ok_or_else(|| ElementRefError::Stale(reference.id().clone()))
123    }
124
125    /// Resolve a mounted component-local ref key.
126    ///
127    /// # Errors
128    ///
129    /// Returns [`ElementRefError::UnknownKey`] when no binding is committed.
130    pub fn resolve_key(
131        &self,
132        component: &ComponentInstancePath,
133        key: &str,
134    ) -> Result<ElementRef, ElementRefError> {
135        self.active
136            .keys()
137            .find(|id| id.component() == component && id.key() == key)
138            .cloned()
139            .map(ElementRef::new)
140            .ok_or_else(|| ElementRefError::UnknownKey {
141                component: component.clone(),
142                key: key.to_owned(),
143            })
144    }
145
146    pub(crate) fn resolve_and_track_geometry(
147        &mut self,
148        reference: &ElementRef,
149        reader: impl Into<crate::read_dependency::ReadDependency>,
150    ) -> Option<NodeId> {
151        self.geometry_readers
152            .entry(reference.id().clone())
153            .or_default()
154            .insert(reader.into());
155        self.active.get(reference.id()).copied()
156    }
157
158    pub(crate) fn reconcile(
159        &mut self,
160        root: &ComponentInstancePath,
161        candidate: BTreeMap<ElementRefId, NodeId>,
162    ) {
163        let previous = self.active.clone();
164        self.active.retain(|id, _| !id.component.is_within(root));
165        self.active.extend(candidate);
166        for (id, readers) in &self.geometry_readers {
167            if id.component.is_within(root) && previous.get(id) != self.active.get(id) {
168                self.dirty
169                    .extend(readers.iter().map(|reader| reader.owner.clone()));
170            }
171        }
172    }
173
174    /// The committed logical bindings, grouped by their current geometry node.
175    /// Unbound references remain subscribed here but have no native target yet.
176    pub(crate) fn geometry_bindings(
177        &self,
178        scope: &ComponentInstancePath,
179    ) -> ElementRefGeometryBindings {
180        let mut bindings = ElementRefGeometryBindings::new();
181        for (id, readers) in &self.geometry_readers {
182            if id.component.is_within(scope)
183                && let Some(node) = self.active.get(id)
184            {
185                bindings
186                    .entry(*node)
187                    .or_default()
188                    .insert(id.clone(), readers.clone());
189            }
190        }
191        bindings
192    }
193
194    pub(crate) fn take_dirty(&mut self) -> BTreeSet<ComponentInstancePath> {
195        std::mem::take(&mut self.dirty)
196    }
197
198    pub(crate) fn remove_scope(&mut self, root: &ComponentInstancePath) {
199        self.active.retain(|id, _| !id.component.is_within(root));
200        self.geometry_readers.retain(|id, readers| {
201            if id.component.is_within(root) {
202                self.dirty.extend(
203                    readers
204                        .iter()
205                        .filter(|reader| !reader.owner.is_within(root))
206                        .map(|reader| reader.owner.clone()),
207                );
208                return false;
209            }
210            readers.retain(|reader| !reader.owner.is_within(root));
211            !readers.is_empty()
212        });
213        self.dirty.retain(|owner| !owner.is_within(root));
214    }
215
216    pub(crate) fn reset_contribution(&mut self, reader: &crate::read_dependency::ReadDependency) {
217        crate::read_dependency::retain_readers(&mut self.geometry_readers, |existing| {
218            existing != reader
219        });
220    }
221    pub(crate) fn retain_contributions(
222        &mut self,
223        scope: &ComponentInstancePath,
224        active: &BTreeSet<crate::read_dependency::ReadContribution>,
225    ) {
226        crate::read_dependency::retain_readers(&mut self.geometry_readers, |reader| {
227            reader.retained_in_contribution_scope(scope, active)
228        });
229    }
230
231    pub(crate) fn retain_reader_scope(
232        &mut self,
233        scope: &ComponentInstancePath,
234        active: &BTreeSet<ComponentInstancePath>,
235    ) {
236        crate::read_dependency::retain_readers(&mut self.geometry_readers, |reader| {
237            reader.retained_in_owner_scope(scope, active)
238        });
239        self.dirty
240            .retain(|owner| !owner.is_within(scope) || owner == scope || active.contains(owner));
241    }
242
243    /// End subscriptions whose provider or executable reader left the final
244    /// invocation manifest. A surviving external reader receives removal once.
245    pub(crate) fn retain_scope(
246        &mut self,
247        scope: &ComponentInstancePath,
248        active: &BTreeSet<ComponentInstancePath>,
249    ) {
250        self.retain_reader_scope(scope, active);
251        let retained = |path: &ComponentInstancePath| {
252            !path.is_within(scope) || path == scope || active.contains(path)
253        };
254        self.active.retain(|id, _| retained(id.component()));
255        self.geometry_readers.retain(|id, readers| {
256            if retained(id.component()) {
257                true
258            } else {
259                self.dirty
260                    .extend(readers.iter().map(|reader| reader.owner.clone()));
261                false
262            }
263        });
264    }
265}
266
267#[derive(Clone, Debug, Error, Eq, PartialEq)]
268pub enum ElementRefError {
269    #[error("element-ref key `{0}` must be 1-128 ASCII letters, digits, `_`, `-`, `.`, or `:`")]
270    InvalidKey(String),
271    #[error("element ref `{0:?}` is stale or unmounted")]
272    Stale(ElementRefId),
273    #[error("element ref `{0:?}` was not declared by the active formal component render")]
274    Undeclared(ElementRefId),
275    #[error("element ref `{0:?}` is bound to more than one node")]
276    DuplicateBinding(ElementRefId),
277    #[error("element ref `{0:?}` requires a stable node key")]
278    MissingNodeKey(ElementRefId),
279    #[error("component `{component}` has no mounted element ref `{key}`")]
280    UnknownKey {
281        component: ComponentInstancePath,
282        key: String,
283    },
284}
285
286#[derive(Clone, Debug, PartialEq)]
287pub(crate) enum ElementCommand {
288    Focus {
289        window: String,
290        node: NodeId,
291    },
292    ScrollTo {
293        window: String,
294        node: NodeId,
295        x: f64,
296        y: f64,
297    },
298    ScrollIntoView {
299        window: String,
300        node: NodeId,
301    },
302}
303
304impl ElementCommand {
305    pub(crate) fn window(&self) -> &str {
306        match self {
307            Self::Focus { window, .. }
308            | Self::ScrollTo { window, .. }
309            | Self::ScrollIntoView { window, .. } => window,
310        }
311    }
312}
313
314#[cfg(test)]
315mod tests {
316    use super::*;
317
318    fn nodes() -> (NodeId, NodeId) {
319        let mut tree = crate::RetainedUiTree::new();
320        tree.reconcile(crate::UiNode::text("first")).unwrap();
321        let first = tree.root_id().unwrap();
322        tree.reconcile(crate::UiNode::box_node(Vec::new())).unwrap();
323        (first, tree.root_id().unwrap())
324    }
325
326    fn measured(width: f64) -> crate::ElementGeometry {
327        let bounds = crate::GeometryBounds::new(0.0, 0.0, width, 24.0).unwrap();
328        crate::ElementGeometry {
329            layout: bounds,
330            visual: bounds,
331            clip: None,
332        }
333    }
334
335    #[test]
336    fn logical_ref_subscription_survives_all_binding_transitions_without_repeats() {
337        let provider = ComponentInstancePath::root("Provider", "main");
338        let first_reader = provider.child("Reader", "first");
339        let second_reader = provider.child("Reader", "second");
340        let reference = ElementRef::new(ElementRefId::new(provider.clone(), "field").unwrap());
341        let readers = BTreeSet::from([first_reader.clone(), second_reader.clone()]);
342        let (first, second) = nodes();
343        let mut refs = ElementRefRegistry::new();
344        let geometry = crate::GeometryRegistry::new();
345        for reader in &readers {
346            assert_eq!(refs.resolve_and_track_geometry(&reference, reader), None);
347        }
348        refs.reconcile(&provider, BTreeMap::new());
349        assert!(refs.take_dirty().is_empty());
350        for node in [first, first, second] {
351            refs.reconcile(&provider, BTreeMap::from([(reference.id().clone(), node)]));
352            geometry.sync_ref_readers(&provider, refs.geometry_bindings(&provider));
353            let dirty = refs.take_dirty();
354            if node == first && geometry.get(first).is_some() {
355                assert!(dirty.is_empty(), "unchanged binding must stay idle");
356            } else {
357                assert_eq!(dirty, readers);
358            }
359            geometry.update(node, measured(if node == first { 120.0 } else { 180.0 }));
360            geometry.take_dirty();
361        }
362        refs.reconcile(&provider, BTreeMap::new());
363        geometry.sync_ref_readers(&provider, refs.geometry_bindings(&provider));
364        assert_eq!(refs.take_dirty(), readers);
365        assert!(geometry.take_dirty().is_empty());
366        geometry.update(first, measured(200.0));
367        geometry.update(second, measured(220.0));
368        assert!(
369            geometry.take_dirty().is_empty(),
370            "neither old node stays subscribed"
371        );
372        refs.reconcile(&provider, BTreeMap::new());
373        assert!(refs.take_dirty().is_empty());
374    }
375
376    #[test]
377    fn rebinding_does_not_remove_an_independent_direct_geometry_read() {
378        let provider = ComponentInstancePath::root("Provider", "main");
379        let owner = provider.child("Reader", "shared");
380        let reference = ElementRef::new(ElementRefId::new(provider.clone(), "field").unwrap());
381        let (first, second) = nodes();
382        let geometry = crate::GeometryRegistry::new();
383        let mut refs = ElementRefRegistry::new();
384        refs.resolve_and_track_geometry(&reference, &owner);
385        refs.reconcile(&provider, BTreeMap::from([(reference.id().clone(), first)]));
386        geometry.sync_ref_readers(&provider, refs.geometry_bindings(&provider));
387        geometry.read_tracked(first, &owner);
388        geometry.update(first, measured(120.0));
389        geometry.take_dirty();
390        refs.reconcile(
391            &provider,
392            BTreeMap::from([(reference.id().clone(), second)]),
393        );
394        geometry.sync_ref_readers(&provider, refs.geometry_bindings(&provider));
395        geometry.update(first, measured(140.0));
396        assert_eq!(geometry.take_dirty(), BTreeSet::from([owner.clone()]));
397        let dependency = crate::read_dependency::ReadDependency::component(&owner);
398        geometry.reset_contribution(&dependency);
399        geometry.sync_ref_readers(&provider, refs.geometry_bindings(&provider));
400        geometry.take_dirty();
401        geometry.update(first, measured(160.0));
402        assert!(geometry.take_dirty().is_empty());
403        geometry.update(second, measured(180.0));
404        assert_eq!(geometry.take_dirty(), BTreeSet::from([owner]));
405    }
406
407    #[test]
408    fn contribution_retarget_and_last_item_prune_detach_native_and_logical_reads() {
409        let provider = ComponentInstancePath::root("Provider", "main");
410        let owner = provider.child("Reader", "shared");
411        let collection = crate::VirtualCollectionId {
412            component: owner.clone(),
413            key: "rows".into(),
414        };
415        let scope = owner.child("VirtualCollection", "rows");
416        let direct = crate::read_dependency::ReadDependency::component(&owner);
417        let item = crate::read_dependency::ReadDependency::virtual_item(&owner, &collection, "a");
418        let first_ref = ElementRef::new(ElementRefId::new(provider.clone(), "first").unwrap());
419        let second_ref = ElementRef::new(ElementRefId::new(provider.clone(), "second").unwrap());
420        let (first, second) = nodes();
421        let geometry = crate::GeometryRegistry::new();
422        let mut refs = ElementRefRegistry::new();
423        refs.reconcile(
424            &provider,
425            BTreeMap::from([
426                (first_ref.id().clone(), first),
427                (second_ref.id().clone(), second),
428            ]),
429        );
430        for reader in [&direct, &item] {
431            refs.resolve_and_track_geometry(&first_ref, reader);
432            geometry.read_ref_tracked(first_ref.id(), first, reader);
433        }
434        geometry.update(first, measured(100.0));
435        geometry.update(second, measured(120.0));
436        geometry.take_dirty();
437        refs.reset_contribution(&direct);
438        geometry.reset_contribution(&direct);
439        refs.resolve_and_track_geometry(&second_ref, &direct);
440        geometry.read_ref_tracked(second_ref.id(), second, &direct);
441        refs.retain_contributions(&scope, &BTreeSet::new());
442        geometry.retain_contributions(&scope, &BTreeSet::new());
443        geometry.update(first, measured(140.0));
444        assert!(geometry.take_dirty().is_empty());
445        geometry.update(second, measured(160.0));
446        assert_eq!(geometry.take_dirty(), BTreeSet::from([owner.clone()]));
447        refs.reset_contribution(&direct);
448        geometry.reset_contribution(&direct);
449        refs.reconcile(&provider, BTreeMap::new());
450        assert!(refs.take_dirty().is_empty());
451        assert!(refs.geometry_readers.is_empty());
452        geometry.update(second, measured(180.0));
453        assert!(geometry.take_dirty().is_empty());
454    }
455
456    #[test]
457    fn reader_teardown_and_provider_removal_notify_only_surviving_owners() {
458        let root = ComponentInstancePath::root("App", "main");
459        let provider = root.child("Provider", "source");
460        let internal = provider.child("Reader", "internal");
461        let external = root.child("Reader", "external");
462        let removed = root.child("Reader", "removed");
463        let reference = ElementRef::new(ElementRefId::new(provider.clone(), "field").unwrap());
464        let (node, _) = nodes();
465        let mut refs = ElementRefRegistry::new();
466        let geometry = crate::GeometryRegistry::new();
467        for reader in [&internal, &external, &removed] {
468            refs.resolve_and_track_geometry(&reference, reader);
469        }
470        refs.reconcile(&root, BTreeMap::from([(reference.id().clone(), node)]));
471        geometry.sync_ref_readers(&root, refs.geometry_bindings(&root));
472        refs.take_dirty();
473        geometry.update(node, measured(100.0));
474        geometry.take_dirty();
475        let active = BTreeSet::from([provider.clone(), internal.clone(), external.clone()]);
476        refs.retain_reader_scope(&root, &active);
477        geometry.retain_reader_scope(&root, &active);
478        refs.remove_scope(&provider);
479        geometry.remove_scope(&provider);
480        assert_eq!(refs.take_dirty(), BTreeSet::from([external]));
481        assert!(refs.geometry_readers.is_empty());
482        geometry.update(node, measured(120.0));
483        assert!(geometry.take_dirty().is_empty());
484    }
485
486    #[test]
487    fn final_manifest_prunes_provider_subscription_without_reviving_removed_readers() {
488        let root = ComponentInstancePath::root("App", "main");
489        let provider = root.child("Provider", "source");
490        let reader = root.child("Reader", "external");
491        let removed = root.child("Reader", "removed");
492        let reference = ElementRef::new(ElementRefId::new(provider.clone(), "field").unwrap());
493        let (node, _) = nodes();
494        let mut refs = ElementRefRegistry::new();
495        let geometry = crate::GeometryRegistry::new();
496        for owner in [&reader, &removed] {
497            refs.resolve_and_track_geometry(&reference, owner);
498        }
499        refs.reconcile(&root, BTreeMap::from([(reference.id().clone(), node)]));
500        geometry.sync_ref_readers(&root, refs.geometry_bindings(&root));
501        refs.take_dirty();
502        geometry.update(node, measured(100.0));
503        geometry.take_dirty();
504        let active = BTreeSet::from([reader.clone()]);
505        refs.retain_scope(&root, &active);
506        geometry.retain_reader_scope(&root, &active);
507        assert_eq!(refs.take_dirty(), BTreeSet::from([reader]));
508        assert!(refs.geometry_readers.is_empty());
509        assert!(refs.resolve(&reference).is_err());
510        geometry.update(node, measured(120.0));
511        assert!(geometry.take_dirty().is_empty());
512    }
513
514    #[test]
515    fn failed_candidate_restores_last_good_binding_and_subscriptions() {
516        let provider = ComponentInstancePath::root("Provider", "main");
517        let owner = provider.child("Reader", "reader");
518        let dependency = crate::read_dependency::ReadDependency::component(&owner);
519        let reference = ElementRef::new(ElementRefId::new(provider.clone(), "field").unwrap());
520        let (first, second) = nodes();
521        let mut refs = ElementRefRegistry::new();
522        let geometry = crate::GeometryRegistry::new();
523        refs.resolve_and_track_geometry(&reference, &owner);
524        refs.reconcile(&provider, BTreeMap::from([(reference.id().clone(), first)]));
525        geometry.sync_ref_readers(&provider, refs.geometry_bindings(&provider));
526        refs.take_dirty();
527        geometry.update(first, measured(100.0));
528        geometry.take_dirty();
529        let saved_refs = refs.clone();
530        let saved_geometry = geometry.snapshot();
531        refs.reset_contribution(&dependency);
532        geometry.reset_contribution(&dependency);
533        refs.resolve_and_track_geometry(&reference, &owner);
534        refs.reconcile(
535            &provider,
536            BTreeMap::from([(reference.id().clone(), second)]),
537        );
538        geometry.sync_ref_readers(&provider, refs.geometry_bindings(&provider));
539        refs = saved_refs;
540        geometry.restore(saved_geometry);
541        assert_eq!(refs.resolve(&reference), Ok(first));
542        assert!(refs.take_dirty().is_empty());
543        geometry.update(second, measured(180.0));
544        assert!(geometry.take_dirty().is_empty());
545        geometry.update(first, measured(120.0));
546        assert_eq!(geometry.take_dirty(), BTreeSet::from([owner]));
547    }
548
549    #[test]
550    fn stale_refs_do_not_rebind_after_scope_removal() {
551        let component = ComponentInstancePath::root("Panel", "main");
552        let reference = ElementRef::new(ElementRefId::new(component.clone(), "field").unwrap());
553        let mut tree = crate::RetainedUiTree::new();
554        tree.reconcile(crate::UiNode::text("field")).unwrap();
555        let mut registry = ElementRefRegistry::new();
556        registry.reconcile(
557            &component,
558            BTreeMap::from([(reference.id().clone(), tree.root_id().unwrap())]),
559        );
560        assert!(registry.resolve(&reference).is_ok());
561        registry.remove_scope(&component);
562        assert!(matches!(
563            registry.resolve(&reference),
564            Err(ElementRefError::Stale(_))
565        ));
566    }
567
568    #[test]
569    fn pending_geometry_reader_binds_after_the_ref_commits() {
570        let component = ComponentInstancePath::root("Panel", "main");
571        let reader = component.child("Inspector", "reader");
572        let reference = ElementRef::new(ElementRefId::new(component.clone(), "field").unwrap());
573        let mut tree = crate::RetainedUiTree::new();
574        tree.reconcile(crate::UiNode::text("field")).unwrap();
575        let node = tree.root_id().unwrap();
576        let mut refs = ElementRefRegistry::new();
577        assert_eq!(refs.resolve_and_track_geometry(&reference, &reader), None);
578
579        refs.reconcile(&component, BTreeMap::from([(reference.id().clone(), node)]));
580        let geometry = crate::GeometryRegistry::new();
581        geometry.sync_ref_readers(&component, refs.geometry_bindings(&component));
582        geometry.update(
583            node,
584            crate::ElementGeometry {
585                layout: crate::GeometryBounds::new(0.0, 0.0, 120.0, 24.0).unwrap(),
586                visual: crate::GeometryBounds::new(0.0, 0.0, 120.0, 24.0).unwrap(),
587                clip: None,
588            },
589        );
590        assert_eq!(geometry.take_dirty(), BTreeSet::from([reader.clone()]));
591
592        assert_eq!(
593            refs.resolve_and_track_geometry(&reference, &reader),
594            Some(node)
595        );
596        tree.reconcile(crate::UiNode::box_node(Vec::new())).unwrap();
597        let replacement = tree.root_id().unwrap();
598        assert_ne!(replacement, node);
599        refs.reconcile(
600            &component,
601            BTreeMap::from([(reference.id().clone(), replacement)]),
602        );
603        geometry.sync_ref_readers(&component, refs.geometry_bindings(&component));
604        geometry.retain_nodes(&BTreeSet::from([replacement]));
605        geometry.update(
606            replacement,
607            crate::ElementGeometry {
608                layout: crate::GeometryBounds::new(0.0, 0.0, 160.0, 24.0).unwrap(),
609                visual: crate::GeometryBounds::new(0.0, 0.0, 160.0, 24.0).unwrap(),
610                clip: None,
611            },
612        );
613        assert_eq!(geometry.take_dirty(), BTreeSet::from([reader]));
614    }
615}