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 .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 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 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 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 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 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}