korvin-core 0.2.1

The core for korvin frontend framework
Documentation
use self::value_cache::IntoJsValue;
use crate::{
    data::{AttributeName, AttributeValue, EventListenerWrapper, KorvinClosure, TagName},
    mutation::{
        element::builder_mutation::{
            marker::create::ElementCreateMutation,
            marker::finish::ElementFinishMutation,
            modify::set_text::ElementSetTextMutation,
            modify::{
                add_event_listener::{by_event_kind, ElementAddEventListenerMutation},
                ElementBuilderModifyMutation,
            },
            modify::{
                set_attribute::ElementSetAttributeMutation,
                set_input_value::ElementSetInputValueMutation,
            },
        },
        traits::Perform,
    },
};
use std::{collections::BTreeMap, hash::Hasher, iter::empty, sync::Arc};
use wasm_bindgen::{
    convert::{FromWasmAbi, RefFromWasmAbi},
    prelude::Closure,
};

pub mod value_cache {
    use super::calculate_hash;
    use crate::data::Value;
    use std::{cell::RefCell, collections::BTreeMap};

    pub trait IntoJsValue: std::hash::Hash + Copy {
        fn into_value(self) -> Value;
    }

    impl<T> IntoJsValue for T
    where
        T: Into<Value> + std::hash::Hash + Copy,
    {
        fn into_value(self) -> Value {
            self.into()
        }
    }

    #[derive(Default)]
    pub(crate) struct ValueCache {
        previous: BTreeMap<u64, Value>,
        current: BTreeMap<u64, Value>,
    }

    impl ValueCache {
        pub(super) fn cached(&mut self, value: impl IntoJsValue) -> Value {
            let hash = calculate_hash(&value);
            self.current
                .entry(hash)
                .or_insert_with(|| {
                    self.previous
                        .remove(&hash)
                        .unwrap_or_else(|| value.into_value())
                })
                .clone()
        }
        pub(crate) fn next_rebuild(&mut self) {
            std::mem::swap(&mut self.current, &mut self.previous);
            self.current.clear();
        }
    }

    thread_local! {
        pub(crate) static VALUE_CACHE: RefCell<ValueCache> = Default::default();
    }
}

pub trait AsElementBuilder {
    fn into_builder(self) -> ElementBuilder;
    fn builder(kind: impl IntoJsValue) -> ElementBuilder {
        ElementBuilder::builder(kind)
    }
    fn text(self, text: impl IntoJsValue) -> ElementBuilder;
    fn input_value(self, value: impl IntoJsValue) -> ElementBuilder;
    fn event<Key: std::hash::Hash, EventKind>(
        self,
        key: Key,
        name: impl IntoJsValue,
        callback: impl Fn(EventKind) + 'static,
    ) -> ElementBuilder
    where
        EventKind: std::fmt::Debug + Sized + RefFromWasmAbi + FromWasmAbi + 'static,
        by_event_kind::ElementAddEventListenerMutation<EventKind>:
            Into<ElementAddEventListenerMutation> + Perform;
    fn child(self, child: impl Into<ElementBuilder>) -> ElementBuilder;
    fn key(self, key: impl std::hash::Hash) -> ElementBuilder;
    fn children(
        self,
        children: impl IntoIterator<Item = impl Into<ElementBuilder>>,
    ) -> ElementBuilder;
    fn attribute(self, attribute: impl IntoJsValue, value: impl IntoJsValue) -> ElementBuilder;
    fn build(self) -> ElementWithChildrenRecipe;
}

pub struct ElementBuilder {
    key: Option<u64>,
    kind: TagName,
    attributes: BTreeMap<AttributeName, AttributeValue>,
    text: Option<AttributeValue>,
    input_value: Option<AttributeValue>,
    children: Vec<ElementBuilder>,
    event_listeners: Vec<ElementAddEventListenerMutation>,
}

pub fn calculate_hash<T: std::hash::Hash>(t: &T) -> u64 {
    let mut s = std::collections::hash_map::DefaultHasher::new();
    t.hash(&mut s);
    s.finish()
}

#[derive(Debug)]
pub struct ElementRecipe {
    pub key: Option<u64>,
    pub create: ElementCreateMutation,
    pub modify: Vec<ElementBuilderModifyMutation>,
    pub finish: ElementFinishMutation,
}

#[derive(Debug)]
pub struct ElementWithChildrenRecipe {
    pub element: ElementRecipe,
    pub children: Vec<Self>,
}

macro_rules! cached {
    ($value:expr) => {
        value_cache::VALUE_CACHE.with(|value_cache| {
            value_cache
                .try_borrow_mut()
                .ok()
                .map(|mut value_cache| value_cache.cached($value))
                .unwrap_or_else(|| $value.into_value())
        })
    };
}

impl<'a> From<&'a str> for ElementBuilder {
    fn from(val: &'a str) -> Self {
        ElementBuilder::builder(val)
    }
}

impl<'a> AsElementBuilder for &'a str {
    fn into_builder(self) -> ElementBuilder {
        ElementBuilder::from(self).into_builder()
    }

    fn text(self, text: impl IntoJsValue) -> ElementBuilder {
        ElementBuilder::from(self).text(text)
    }

    fn input_value(self, value: impl IntoJsValue) -> ElementBuilder {
        ElementBuilder::from(self).input_value(value)
    }

    fn event<Key: std::hash::Hash, EventKind>(
        self,
        key: Key,
        name: impl IntoJsValue,
        callback: impl Fn(EventKind) + 'static,
    ) -> ElementBuilder
    where
        EventKind: std::fmt::Debug + Sized + RefFromWasmAbi + FromWasmAbi + 'static,
        by_event_kind::ElementAddEventListenerMutation<EventKind>:
            Into<ElementAddEventListenerMutation> + Perform,
    {
        ElementBuilder::from(self).event(key, name, callback)
    }

    fn child(self, child: impl Into<ElementBuilder>) -> ElementBuilder {
        ElementBuilder::from(self).child(child.into())
    }

    fn key(self, key: impl std::hash::Hash) -> ElementBuilder {
        ElementBuilder::from(self).key(key)
    }

    fn children(
        self,
        children: impl IntoIterator<Item = impl Into<ElementBuilder>>,
    ) -> ElementBuilder {
        ElementBuilder::from(self).children(children)
    }

    fn attribute(self, attribute: impl IntoJsValue, value: impl IntoJsValue) -> ElementBuilder {
        ElementBuilder::from(self).attribute(attribute, value)
    }

    fn build(self) -> ElementWithChildrenRecipe {
        ElementBuilder::from(self).build()
    }
}

impl AsElementBuilder for ElementBuilder {
    fn into_builder(self) -> ElementBuilder {
        self
    }
    fn builder(kind: impl IntoJsValue) -> Self {
        let kind = cached!(kind).into();
        Self {
            key: None,
            kind,
            input_value: None,
            text: None,
            event_listeners: Default::default(),
            attributes: Default::default(),
            children: Default::default(),
        }
    }

    fn text(mut self, text: impl IntoJsValue) -> Self {
        self.text = Some(cached!(text).into());
        self
    }

    fn input_value(mut self, value: impl IntoJsValue) -> Self {
        self.input_value = Some(cached!(value).into());
        self
    }
    fn event<Key: std::hash::Hash, EventKind>(
        mut self,
        key: Key,
        name: impl IntoJsValue,
        callback: impl Fn(EventKind) + 'static,
    ) -> Self
    where
        EventKind: std::fmt::Debug + Sized + RefFromWasmAbi + FromWasmAbi + 'static,
        by_event_kind::ElementAddEventListenerMutation<EventKind>:
            Into<ElementAddEventListenerMutation> + Perform,
    {
        let hash = calculate_hash(&key);
        let listener = EventListenerWrapper::<EventKind> {
            name: cached!(name).into(),
            closure: KorvinClosure {
                hash,
                closure: Arc::new(Closure::new(callback)),
            },
        };
        self.event_listeners
            .push(by_event_kind::ElementAddEventListenerMutation { listener }.into());
        self
    }
    fn child(mut self, child: impl Into<ElementBuilder>) -> Self {
        self.children.push(child.into());
        self
    }
    fn key(mut self, key: impl std::hash::Hash) -> Self {
        self.key = Some(calculate_hash(&key));
        self
    }

    fn children(self, children: impl IntoIterator<Item = impl Into<ElementBuilder>>) -> Self {
        children
            .into_iter()
            .fold(self, |parent, child| parent.child(child))
    }

    fn attribute(mut self, attribute: impl IntoJsValue, value: impl IntoJsValue) -> Self {
        self.attributes
            .insert(cached!(attribute).into(), cached!(value).into());
        self
    }

    fn build(self) -> ElementWithChildrenRecipe {
        let Self {
            key,
            kind,
            attributes,
            children,
            text,
            event_listeners,
            input_value,
        } = self;

        let element = ElementRecipe {
            key,
            create: ElementCreateMutation { kind },
            modify: empty()
                .chain(
                    attributes
                        .into_iter()
                        .map(|(attribute, value)| ElementSetAttributeMutation {
                            attribute,
                            value: Some(value),
                        })
                        .map(ElementBuilderModifyMutation::from),
                )
                .chain(
                    text.into_iter()
                        .map(|value| ElementSetTextMutation { value: Some(value) })
                        .map(ElementBuilderModifyMutation::from),
                )
                .chain(
                    input_value
                        .into_iter()
                        .map(|value| ElementSetInputValueMutation { value })
                        .map(ElementBuilderModifyMutation::from),
                )
                .chain(
                    event_listeners
                        .into_iter()
                        .map(ElementBuilderModifyMutation::from),
                )
                .collect(),
            finish: ElementFinishMutation {},
        };
        ElementWithChildrenRecipe {
            element,
            children: children.into_iter().map(Self::build).collect(),
        }
    }
}