gpui-rhai 0.1.8

A Rhai-authored declarative UI runtime for GPUI
Documentation
use crate::{ComponentInstancePath, VirtualCollectionId};
use std::collections::{BTreeMap, BTreeSet};

#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
pub(crate) enum ReadContribution {
    Component,
    VirtualItem {
        collection: VirtualCollectionId,
        key: String,
    },
}

#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
pub(crate) struct ReadDependency {
    pub owner: ComponentInstancePath,
    pub contribution: ReadContribution,
}

impl From<&ComponentInstancePath> for ReadDependency {
    fn from(owner: &ComponentInstancePath) -> Self {
        Self::component(owner)
    }
}
impl From<ComponentInstancePath> for ReadDependency {
    fn from(owner: ComponentInstancePath) -> Self {
        Self::component(&owner)
    }
}
impl From<&ReadDependency> for ReadDependency {
    fn from(reader: &ReadDependency) -> Self {
        reader.clone()
    }
}

impl ReadDependency {
    pub fn component(owner: &ComponentInstancePath) -> Self {
        Self {
            owner: owner.clone(),
            contribution: ReadContribution::Component,
        }
    }
    pub fn virtual_item(
        owner: &ComponentInstancePath,
        collection: &VirtualCollectionId,
        key: &str,
    ) -> Self {
        Self {
            owner: owner.clone(),
            contribution: ReadContribution::VirtualItem {
                collection: collection.clone(),
                key: key.to_owned(),
            },
        }
    }
    pub fn retained_in_owner_scope(
        &self,
        scope: &ComponentInstancePath,
        active: &BTreeSet<ComponentInstancePath>,
    ) -> bool {
        !self.owner.is_within(scope) || self.owner == *scope || active.contains(&self.owner)
    }
    pub fn retained_in_contribution_scope(
        &self,
        scope: &ComponentInstancePath,
        active: &BTreeSet<ReadContribution>,
    ) -> bool {
        match &self.contribution {
            ReadContribution::Component => true,
            ReadContribution::VirtualItem { collection, .. } => {
                !collection
                    .component
                    .child("VirtualCollection", collection.key.clone())
                    .is_within(scope)
                    || active.contains(&self.contribution)
            }
        }
    }
}

pub(crate) fn owners(
    readers: impl IntoIterator<Item = ReadDependency>,
) -> BTreeSet<ComponentInstancePath> {
    readers.into_iter().map(|reader| reader.owner).collect()
}

pub(crate) fn retain_readers<K: Ord>(
    index: &mut BTreeMap<K, BTreeSet<ReadDependency>>,
    predicate: impl Fn(&ReadDependency) -> bool,
) {
    for readers in index.values_mut() {
        readers.retain(&predicate);
    }
    index.retain(|_, readers| !readers.is_empty());
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::{
        ComponentStateSchema, ElementGeometry, GeometryBounds, GeometryRegistry, RetainedUiTree,
        StateField, StoreId, StoreRegistry, UiNode, UiValue, ValueSchema,
    };

    #[test]
    fn contribution_retention_is_shared_by_store_environment_and_geometry() {
        let owner = ComponentInstancePath::root("App", "reads");
        let collection = VirtualCollectionId {
            component: owner.clone(),
            key: "rows".into(),
        };
        let scope = owner.child("VirtualCollection", "rows");
        let direct = ReadDependency::component(&owner);
        let first = ReadDependency::virtual_item(&owner, &collection, "a");
        let second = ReadDependency::virtual_item(&owner, &collection, "b");
        let active = BTreeSet::from([first.contribution.clone()]);
        let mut stores = StoreRegistry::new();
        let id = StoreId::app("store");
        stores
            .declare(
                id.clone(),
                ComponentStateSchema::new(BTreeMap::from([(
                    "value".into(),
                    StateField::new(ValueSchema::integer(), UiValue::Integer(0)),
                )]))
                .unwrap(),
            )
            .unwrap();
        for reader in [&direct, &first, &second] {
            stores.read_dependency(reader, &id, "value").unwrap();
        }
        stores.retain_contributions(&scope, &active);
        stores.reset_contribution(&direct);
        assert_eq!(
            stores.write(&id, "value", UiValue::Integer(1)).unwrap(),
            BTreeSet::from([owner.clone()])
        );
        stores.reset_contribution(&first);
        assert!(
            stores
                .write(&id, "value", UiValue::Integer(2))
                .unwrap()
                .is_empty()
        );

        let mut environment =
            crate::environment_dependency::EnvironmentDependencyRegistry::default();
        for reader in [&direct, &first, &second] {
            environment.track_locale(Some("main"), reader);
            environment.track_theme(Some("main"), reader);
            environment.track_viewport("main", reader);
        }
        environment.retain_contributions(&scope, &active);
        environment.reset_contribution(&direct);
        assert_eq!(
            environment.invalidate_theme_app(),
            BTreeSet::from([owner.clone()])
        );
        environment.reset_contribution(&first);
        assert!(environment.invalidate_locale_app().is_empty());
        assert!(environment.invalidate_viewport("main").is_empty());

        let mut tree = RetainedUiTree::new();
        tree.reconcile(UiNode::text("target")).unwrap();
        let node = tree.root_id().unwrap();
        let geometry = GeometryRegistry::new();
        for reader in [&direct, &first, &second] {
            geometry.read_tracked(node, reader);
        }
        geometry.retain_contributions(&scope, &active);
        geometry.reset_contribution(&direct);
        let bounds = GeometryBounds::new(0.0, 0.0, 100.0, 20.0).unwrap();
        geometry.update(
            node,
            ElementGeometry {
                layout: bounds,
                visual: bounds,
                clip: None,
            },
        );
        assert_eq!(geometry.take_dirty(), BTreeSet::from([owner]));
        geometry.reset_contribution(&first);
        let moved = GeometryBounds::new(1.0, 0.0, 100.0, 20.0).unwrap();
        geometry.update(
            node,
            ElementGeometry {
                layout: moved,
                visual: moved,
                clip: None,
            },
        );
        assert!(geometry.take_dirty().is_empty());
    }
}