use std::cell::RefCell;
use std::collections::{BTreeMap, BTreeSet};
use std::rc::Rc;
use thiserror::Error;
use crate::{ComponentInstancePath, NodeId, UiValue};
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct GeometryBounds {
pub x: f64,
pub y: f64,
pub width: f64,
pub height: f64,
}
impl GeometryBounds {
pub fn new(x: f64, y: f64, width: f64, height: f64) -> Result<Self, GeometryError> {
if [x, y, width, height].into_iter().all(f64::is_finite) && width >= 0.0 && height >= 0.0 {
Ok(Self {
x,
y,
width,
height,
})
} else {
Err(GeometryError::InvalidBounds {
x,
y,
width,
height,
})
}
}
#[must_use]
pub fn into_value(self) -> UiValue {
UiValue::Map(BTreeMap::from([
("x".to_owned(), UiValue::Float(self.x)),
("y".to_owned(), UiValue::Float(self.y)),
("width".to_owned(), UiValue::Float(self.width)),
("height".to_owned(), UiValue::Float(self.height)),
]))
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct ElementGeometry {
pub layout: GeometryBounds,
pub visual: GeometryBounds,
pub clip: Option<GeometryBounds>,
}
#[derive(Clone, Debug, Default)]
struct GeometryState {
committed: BTreeMap<NodeId, ElementGeometry>,
presented: BTreeSet<NodeId>,
readers: BTreeMap<NodeId, BTreeSet<ComponentInstancePath>>,
dirty: BTreeSet<ComponentInstancePath>,
}
#[derive(Clone, Debug, Default)]
pub struct GeometryRegistry {
inner: Rc<RefCell<Rc<GeometryState>>>,
}
impl GeometryRegistry {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn len(&self) -> usize {
self.inner.borrow().committed.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.inner.borrow().committed.is_empty()
}
pub fn update(&self, node: NodeId, geometry: ElementGeometry) -> bool {
let mut current = self.inner.borrow_mut();
let state = Rc::make_mut(&mut current);
state.presented.insert(node);
if state.committed.get(&node) == Some(&geometry) {
return false;
}
state.committed.insert(node, geometry);
let readers = state.readers.get(&node).cloned().unwrap_or_default();
state.dirty.extend(readers);
true
}
pub fn read(
&self,
node: NodeId,
reader: &ComponentInstancePath,
) -> Result<ElementGeometry, GeometryError> {
let mut current = self.inner.borrow_mut();
let state = Rc::make_mut(&mut current);
let geometry = state
.committed
.get(&node)
.copied()
.ok_or(GeometryError::Unavailable(node))?;
state
.readers
.entry(node)
.or_default()
.insert(reader.clone());
Ok(geometry)
}
pub(crate) fn read_tracked(
&self,
node: NodeId,
reader: &ComponentInstancePath,
) -> Option<ElementGeometry> {
let mut current = self.inner.borrow_mut();
let state = Rc::make_mut(&mut current);
state
.readers
.entry(node)
.or_default()
.insert(reader.clone());
state.committed.get(&node).copied()
}
pub(crate) fn register_readers(&self, node: NodeId, readers: BTreeSet<ComponentInstancePath>) {
if readers.is_empty() {
return;
}
let mut current = self.inner.borrow_mut();
let state = Rc::make_mut(&mut current);
let committed = state.committed.contains_key(&node);
for reader in readers {
if state
.readers
.entry(node)
.or_default()
.insert(reader.clone())
&& committed
{
state.dirty.insert(reader);
}
}
}
pub(crate) fn get(&self, node: NodeId) -> Option<ElementGeometry> {
self.inner.borrow().committed.get(&node).copied()
}
pub(crate) fn begin_frame(&self) {
Rc::make_mut(&mut self.inner.borrow_mut()).presented.clear();
}
pub(crate) fn is_presented(&self, node: NodeId) -> bool {
self.inner.borrow().presented.contains(&node)
}
pub(crate) fn retain_nodes(&self, active: &BTreeSet<NodeId>) {
let mut current = self.inner.borrow_mut();
let state = Rc::make_mut(&mut current);
state.committed.retain(|node, _| active.contains(node));
state.presented.retain(|node| active.contains(node));
state.readers.retain(|node, _| active.contains(node));
}
pub(crate) fn take_dirty(&self) -> BTreeSet<ComponentInstancePath> {
std::mem::take(&mut Rc::make_mut(&mut self.inner.borrow_mut()).dirty)
}
pub(crate) fn snapshot(&self) -> GeometrySnapshot {
GeometrySnapshot(Rc::clone(&self.inner.borrow()))
}
pub(crate) fn restore(&self, snapshot: GeometrySnapshot) {
*self.inner.borrow_mut() = snapshot.0;
}
}
#[derive(Clone, Debug)]
pub(crate) struct GeometrySnapshot(Rc<GeometryState>);
#[derive(Clone, Debug, Error, PartialEq)]
pub enum GeometryError {
#[error("invalid geometry x={x}, y={y}, width={width}, height={height}")]
InvalidBounds {
x: f64,
y: f64,
width: f64,
height: f64,
},
#[error("geometry for retained node {0} is not committed")]
Unavailable(NodeId),
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn changed_geometry_invalidates_exact_readers_and_snapshot_restores() {
let mut tree = crate::RetainedUiTree::new();
tree.reconcile(crate::UiNode::text("field")).unwrap();
let node = tree.root_id().unwrap();
let reader = ComponentInstancePath::root("Panel", "main");
let registry = GeometryRegistry::new();
let initial = ElementGeometry {
layout: GeometryBounds::new(0.0, 0.0, 100.0, 20.0).unwrap(),
visual: GeometryBounds::new(0.0, 0.0, 100.0, 20.0).unwrap(),
clip: None,
};
registry.update(node, initial);
assert!(registry.is_presented(node));
registry.begin_frame();
assert!(!registry.is_presented(node));
assert!(!registry.update(node, initial));
assert!(registry.is_presented(node));
assert_eq!(registry.read(node, &reader).unwrap(), initial);
let snapshot = registry.snapshot();
registry.update(
node,
ElementGeometry {
layout: GeometryBounds::new(0.0, 0.0, 120.0, 20.0).unwrap(),
visual: GeometryBounds::new(0.0, 0.0, 120.0, 20.0).unwrap(),
clip: None,
},
);
assert!(registry.take_dirty().contains(&reader));
registry.restore(snapshot);
assert_eq!(registry.read(node, &reader).unwrap(), initial);
}
}