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    }
73}
74
75#[derive(Clone, Debug, Default)]
76pub struct ElementRefRegistry {
77    active: BTreeMap<ElementRefId, NodeId>,
78    pending_geometry_readers: BTreeMap<ElementRefId, BTreeSet<ComponentInstancePath>>,
79}
80
81impl ElementRefRegistry {
82    #[must_use]
83    pub fn new() -> Self {
84        Self::default()
85    }
86
87    #[must_use]
88    pub fn len(&self) -> usize {
89        self.active.len()
90    }
91
92    #[must_use]
93    pub fn is_empty(&self) -> bool {
94        self.active.is_empty()
95    }
96
97    pub fn iter(&self) -> impl ExactSizeIterator<Item = (&ElementRefId, &NodeId)> {
98        self.active.iter()
99    }
100
101    /// Resolve the last committed retained identity.
102    ///
103    /// # Errors
104    ///
105    /// Returns [`ElementRefError::Stale`] after unmount or before commit.
106    pub fn resolve(&self, reference: &ElementRef) -> Result<NodeId, ElementRefError> {
107        self.active
108            .get(reference.id())
109            .copied()
110            .ok_or_else(|| ElementRefError::Stale(reference.id().clone()))
111    }
112
113    /// Resolve a mounted component-local ref key.
114    ///
115    /// # Errors
116    ///
117    /// Returns [`ElementRefError::UnknownKey`] when no binding is committed.
118    pub fn resolve_key(
119        &self,
120        component: &ComponentInstancePath,
121        key: &str,
122    ) -> Result<ElementRef, ElementRefError> {
123        self.active
124            .keys()
125            .find(|id| id.component() == component && id.key() == key)
126            .cloned()
127            .map(ElementRef::new)
128            .ok_or_else(|| ElementRefError::UnknownKey {
129                component: component.clone(),
130                key: key.to_owned(),
131            })
132    }
133
134    pub(crate) fn resolve_and_track_geometry(
135        &mut self,
136        reference: &ElementRef,
137        reader: &ComponentInstancePath,
138    ) -> Option<NodeId> {
139        self.pending_geometry_readers
140            .entry(reference.id().clone())
141            .or_default()
142            .insert(reader.clone());
143        self.active.get(reference.id()).copied()
144    }
145
146    pub(crate) fn reconcile(
147        &mut self,
148        root: &ComponentInstancePath,
149        candidate: BTreeMap<ElementRefId, NodeId>,
150    ) -> BTreeMap<NodeId, BTreeSet<ComponentInstancePath>> {
151        self.active.retain(|id, _| !id.component.is_within(root));
152        self.active.extend(candidate);
153        let pending = self
154            .pending_geometry_readers
155            .keys()
156            .filter(|id| id.component.is_within(root))
157            .cloned()
158            .collect::<Vec<_>>();
159        let mut resolved = BTreeMap::<NodeId, BTreeSet<ComponentInstancePath>>::new();
160        for id in pending {
161            let readers = self
162                .pending_geometry_readers
163                .remove(&id)
164                .unwrap_or_default();
165            if let Some(node) = self.active.get(&id) {
166                resolved.entry(*node).or_default().extend(readers);
167            }
168        }
169        resolved
170    }
171
172    pub(crate) fn remove_scope(&mut self, root: &ComponentInstancePath) {
173        self.active.retain(|id, _| !id.component.is_within(root));
174        self.pending_geometry_readers.retain(|id, readers| {
175            if id.component.is_within(root) {
176                return false;
177            }
178            readers.retain(|reader| !reader.is_within(root));
179            !readers.is_empty()
180        });
181    }
182}
183
184#[derive(Clone, Debug, Error, Eq, PartialEq)]
185pub enum ElementRefError {
186    #[error("element-ref key `{0}` must be 1-128 ASCII letters, digits, `_`, `-`, `.`, or `:`")]
187    InvalidKey(String),
188    #[error("element ref `{0:?}` is stale or unmounted")]
189    Stale(ElementRefId),
190    #[error("element ref `{0:?}` was not declared by the active formal component render")]
191    Undeclared(ElementRefId),
192    #[error("element ref `{0:?}` is bound to more than one node")]
193    DuplicateBinding(ElementRefId),
194    #[error("element ref `{0:?}` requires a stable node key")]
195    MissingNodeKey(ElementRefId),
196    #[error("component `{component}` has no mounted element ref `{key}`")]
197    UnknownKey {
198        component: ComponentInstancePath,
199        key: String,
200    },
201}
202
203#[derive(Clone, Debug, PartialEq)]
204pub(crate) enum ElementCommand {
205    Focus {
206        window: String,
207        node: NodeId,
208    },
209    ScrollTo {
210        window: String,
211        node: NodeId,
212        x: f64,
213        y: f64,
214    },
215    ScrollIntoView {
216        window: String,
217        node: NodeId,
218    },
219}
220
221impl ElementCommand {
222    pub(crate) fn window(&self) -> &str {
223        match self {
224            Self::Focus { window, .. }
225            | Self::ScrollTo { window, .. }
226            | Self::ScrollIntoView { window, .. } => window,
227        }
228    }
229}
230
231#[cfg(test)]
232mod tests {
233    use super::*;
234
235    #[test]
236    fn stale_refs_do_not_rebind_after_scope_removal() {
237        let component = ComponentInstancePath::root("Panel", "main");
238        let reference = ElementRef::new(ElementRefId::new(component.clone(), "field").unwrap());
239        let mut tree = crate::RetainedUiTree::new();
240        tree.reconcile(crate::UiNode::text("field")).unwrap();
241        let mut registry = ElementRefRegistry::new();
242        registry.reconcile(
243            &component,
244            BTreeMap::from([(reference.id().clone(), tree.root_id().unwrap())]),
245        );
246        assert!(registry.resolve(&reference).is_ok());
247        registry.remove_scope(&component);
248        assert!(matches!(
249            registry.resolve(&reference),
250            Err(ElementRefError::Stale(_))
251        ));
252    }
253
254    #[test]
255    fn pending_geometry_reader_binds_after_the_ref_commits() {
256        let component = ComponentInstancePath::root("Panel", "main");
257        let reader = component.child("Inspector", "reader");
258        let reference = ElementRef::new(ElementRefId::new(component.clone(), "field").unwrap());
259        let mut tree = crate::RetainedUiTree::new();
260        tree.reconcile(crate::UiNode::text("field")).unwrap();
261        let node = tree.root_id().unwrap();
262        let mut refs = ElementRefRegistry::new();
263        assert_eq!(refs.resolve_and_track_geometry(&reference, &reader), None);
264
265        let resolved = refs.reconcile(&component, BTreeMap::from([(reference.id().clone(), node)]));
266        let geometry = crate::GeometryRegistry::new();
267        for (node, readers) in resolved {
268            geometry.register_readers(node, readers);
269        }
270        geometry.update(
271            node,
272            crate::ElementGeometry {
273                layout: crate::GeometryBounds::new(0.0, 0.0, 120.0, 24.0).unwrap(),
274                visual: crate::GeometryBounds::new(0.0, 0.0, 120.0, 24.0).unwrap(),
275                clip: None,
276            },
277        );
278        assert_eq!(geometry.take_dirty(), BTreeSet::from([reader.clone()]));
279
280        assert_eq!(
281            refs.resolve_and_track_geometry(&reference, &reader),
282            Some(node)
283        );
284        tree.reconcile(crate::UiNode::box_node(Vec::new())).unwrap();
285        let replacement = tree.root_id().unwrap();
286        assert_ne!(replacement, node);
287        let resolved = refs.reconcile(
288            &component,
289            BTreeMap::from([(reference.id().clone(), replacement)]),
290        );
291        for (node, readers) in resolved {
292            geometry.register_readers(node, readers);
293        }
294        geometry.retain_nodes(&BTreeSet::from([replacement]));
295        geometry.update(
296            replacement,
297            crate::ElementGeometry {
298                layout: crate::GeometryBounds::new(0.0, 0.0, 160.0, 24.0).unwrap(),
299                visual: crate::GeometryBounds::new(0.0, 0.0, 160.0, 24.0).unwrap(),
300                clip: None,
301            },
302        );
303        assert_eq!(geometry.take_dirty(), BTreeSet::from([reader]));
304    }
305}