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 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#[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 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 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}