use std::collections::{BTreeMap, BTreeSet};
use rhai::{CustomType, ImmutableString, TypeBuilder};
use thiserror::Error;
use crate::{ComponentInstancePath, NodeId};
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct ElementRefId {
component: ComponentInstancePath,
key: String,
}
impl ElementRefId {
pub fn new(
component: ComponentInstancePath,
key: impl Into<String>,
) -> Result<Self, ElementRefError> {
let key = key.into();
if key.is_empty()
|| key.len() > 128
|| !key.chars().all(|character| {
character.is_ascii_alphanumeric() || matches!(character, '_' | '-' | '.' | ':')
})
{
return Err(ElementRefError::InvalidKey(key));
}
Ok(Self { component, key })
}
#[must_use]
pub const fn component(&self) -> &ComponentInstancePath {
&self.component
}
#[must_use]
pub fn key(&self) -> &str {
&self.key
}
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct ElementRef {
id: ElementRefId,
}
impl ElementRef {
#[must_use]
pub const fn new(id: ElementRefId) -> Self {
Self { id }
}
#[must_use]
pub const fn id(&self) -> &ElementRefId {
&self.id
}
}
impl CustomType for ElementRef {
fn build(mut builder: TypeBuilder<Self>) {
builder
.with_name("ElementRef")
.with_get("key", |reference: &mut Self| {
ImmutableString::from(reference.id.key.as_str())
});
}
}
#[derive(Clone, Debug, Default)]
pub struct ElementRefRegistry {
active: BTreeMap<ElementRefId, NodeId>,
pending_geometry_readers: BTreeMap<ElementRefId, BTreeSet<ComponentInstancePath>>,
}
impl ElementRefRegistry {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn len(&self) -> usize {
self.active.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.active.is_empty()
}
pub fn iter(&self) -> impl ExactSizeIterator<Item = (&ElementRefId, &NodeId)> {
self.active.iter()
}
pub fn resolve(&self, reference: &ElementRef) -> Result<NodeId, ElementRefError> {
self.active
.get(reference.id())
.copied()
.ok_or_else(|| ElementRefError::Stale(reference.id().clone()))
}
pub fn resolve_key(
&self,
component: &ComponentInstancePath,
key: &str,
) -> Result<ElementRef, ElementRefError> {
self.active
.keys()
.find(|id| id.component() == component && id.key() == key)
.cloned()
.map(ElementRef::new)
.ok_or_else(|| ElementRefError::UnknownKey {
component: component.clone(),
key: key.to_owned(),
})
}
pub(crate) fn resolve_and_track_geometry(
&mut self,
reference: &ElementRef,
reader: &ComponentInstancePath,
) -> Option<NodeId> {
self.pending_geometry_readers
.entry(reference.id().clone())
.or_default()
.insert(reader.clone());
self.active.get(reference.id()).copied()
}
pub(crate) fn reconcile(
&mut self,
root: &ComponentInstancePath,
candidate: BTreeMap<ElementRefId, NodeId>,
) -> BTreeMap<NodeId, BTreeSet<ComponentInstancePath>> {
self.active.retain(|id, _| !id.component.is_within(root));
self.active.extend(candidate);
let pending = self
.pending_geometry_readers
.keys()
.filter(|id| id.component.is_within(root))
.cloned()
.collect::<Vec<_>>();
let mut resolved = BTreeMap::<NodeId, BTreeSet<ComponentInstancePath>>::new();
for id in pending {
let readers = self
.pending_geometry_readers
.remove(&id)
.unwrap_or_default();
if let Some(node) = self.active.get(&id) {
resolved.entry(*node).or_default().extend(readers);
}
}
resolved
}
pub(crate) fn remove_scope(&mut self, root: &ComponentInstancePath) {
self.active.retain(|id, _| !id.component.is_within(root));
self.pending_geometry_readers.retain(|id, readers| {
if id.component.is_within(root) {
return false;
}
readers.retain(|reader| !reader.is_within(root));
!readers.is_empty()
});
}
}
#[derive(Clone, Debug, Error, Eq, PartialEq)]
pub enum ElementRefError {
#[error("element-ref key `{0}` must be 1-128 ASCII letters, digits, `_`, `-`, `.`, or `:`")]
InvalidKey(String),
#[error("element ref `{0:?}` is stale or unmounted")]
Stale(ElementRefId),
#[error("element ref `{0:?}` was not declared by the active formal component render")]
Undeclared(ElementRefId),
#[error("element ref `{0:?}` is bound to more than one node")]
DuplicateBinding(ElementRefId),
#[error("element ref `{0:?}` requires a stable node key")]
MissingNodeKey(ElementRefId),
#[error("component `{component}` has no mounted element ref `{key}`")]
UnknownKey {
component: ComponentInstancePath,
key: String,
},
}
#[derive(Clone, Debug, PartialEq)]
pub(crate) enum ElementCommand {
Focus {
window: String,
node: NodeId,
},
ScrollTo {
window: String,
node: NodeId,
x: f64,
y: f64,
},
ScrollIntoView {
window: String,
node: NodeId,
},
}
impl ElementCommand {
pub(crate) fn window(&self) -> &str {
match self {
Self::Focus { window, .. }
| Self::ScrollTo { window, .. }
| Self::ScrollIntoView { window, .. } => window,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn stale_refs_do_not_rebind_after_scope_removal() {
let component = ComponentInstancePath::root("Panel", "main");
let reference = ElementRef::new(ElementRefId::new(component.clone(), "field").unwrap());
let mut tree = crate::RetainedUiTree::new();
tree.reconcile(crate::UiNode::text("field")).unwrap();
let mut registry = ElementRefRegistry::new();
registry.reconcile(
&component,
BTreeMap::from([(reference.id().clone(), tree.root_id().unwrap())]),
);
assert!(registry.resolve(&reference).is_ok());
registry.remove_scope(&component);
assert!(matches!(
registry.resolve(&reference),
Err(ElementRefError::Stale(_))
));
}
#[test]
fn pending_geometry_reader_binds_after_the_ref_commits() {
let component = ComponentInstancePath::root("Panel", "main");
let reader = component.child("Inspector", "reader");
let reference = ElementRef::new(ElementRefId::new(component.clone(), "field").unwrap());
let mut tree = crate::RetainedUiTree::new();
tree.reconcile(crate::UiNode::text("field")).unwrap();
let node = tree.root_id().unwrap();
let mut refs = ElementRefRegistry::new();
assert_eq!(refs.resolve_and_track_geometry(&reference, &reader), None);
let resolved = refs.reconcile(&component, BTreeMap::from([(reference.id().clone(), node)]));
let geometry = crate::GeometryRegistry::new();
for (node, readers) in resolved {
geometry.register_readers(node, readers);
}
geometry.update(
node,
crate::ElementGeometry {
layout: crate::GeometryBounds::new(0.0, 0.0, 120.0, 24.0).unwrap(),
visual: crate::GeometryBounds::new(0.0, 0.0, 120.0, 24.0).unwrap(),
clip: None,
},
);
assert_eq!(geometry.take_dirty(), BTreeSet::from([reader.clone()]));
assert_eq!(
refs.resolve_and_track_geometry(&reference, &reader),
Some(node)
);
tree.reconcile(crate::UiNode::box_node(Vec::new())).unwrap();
let replacement = tree.root_id().unwrap();
assert_ne!(replacement, node);
let resolved = refs.reconcile(
&component,
BTreeMap::from([(reference.id().clone(), replacement)]),
);
for (node, readers) in resolved {
geometry.register_readers(node, readers);
}
geometry.retain_nodes(&BTreeSet::from([replacement]));
geometry.update(
replacement,
crate::ElementGeometry {
layout: crate::GeometryBounds::new(0.0, 0.0, 160.0, 24.0).unwrap(),
visual: crate::GeometryBounds::new(0.0, 0.0, 160.0, 24.0).unwrap(),
clip: None,
},
);
assert_eq!(geometry.take_dirty(), BTreeSet::from([reader]));
}
}