korvin_core/
element_builder.rs

1use self::value_cache::IntoJsValue;
2use crate::{
3    data::{AttributeName, AttributeValue, EventListenerWrapper, KorvinClosure, TagName},
4    mutation::{
5        element::builder_mutation::{
6            marker::create::ElementCreateMutation,
7            marker::finish::ElementFinishMutation,
8            modify::set_text::ElementSetTextMutation,
9            modify::{
10                add_event_listener::{by_event_kind, ElementAddEventListenerMutation},
11                ElementBuilderModifyMutation,
12            },
13            modify::{
14                set_attribute::ElementSetAttributeMutation,
15                set_input_value::ElementSetInputValueMutation,
16            },
17        },
18        traits::Perform,
19    },
20};
21use std::{collections::BTreeMap, hash::Hasher, iter::empty, sync::Arc};
22use wasm_bindgen::{
23    convert::{FromWasmAbi, RefFromWasmAbi},
24    prelude::Closure,
25};
26
27pub mod value_cache {
28    use super::calculate_hash;
29    use crate::data::Value;
30    use std::{cell::RefCell, collections::BTreeMap};
31
32    pub trait IntoJsValue: std::hash::Hash + Copy {
33        fn into_value(self) -> Value;
34    }
35
36    impl<T> IntoJsValue for T
37    where
38        T: Into<Value> + std::hash::Hash + Copy,
39    {
40        fn into_value(self) -> Value {
41            self.into()
42        }
43    }
44
45    #[derive(Default)]
46    pub(crate) struct ValueCache {
47        previous: BTreeMap<u64, Value>,
48        current: BTreeMap<u64, Value>,
49    }
50
51    impl ValueCache {
52        pub(super) fn cached(&mut self, value: impl IntoJsValue) -> Value {
53            let hash = calculate_hash(&value);
54            self.current
55                .entry(hash)
56                .or_insert_with(|| {
57                    self.previous
58                        .remove(&hash)
59                        .unwrap_or_else(|| value.into_value())
60                })
61                .clone()
62        }
63        pub(crate) fn next_rebuild(&mut self) {
64            std::mem::swap(&mut self.current, &mut self.previous);
65            self.current.clear();
66        }
67    }
68
69    thread_local! {
70        pub(crate) static VALUE_CACHE: RefCell<ValueCache> = Default::default();
71    }
72}
73
74pub trait AsElementBuilder {
75    fn into_builder(self) -> ElementBuilder;
76    fn builder(kind: impl IntoJsValue) -> ElementBuilder {
77        ElementBuilder::builder(kind)
78    }
79    fn text(self, text: impl IntoJsValue) -> ElementBuilder;
80    fn input_value(self, value: impl IntoJsValue) -> ElementBuilder;
81    fn event<Key: std::hash::Hash, EventKind>(
82        self,
83        key: Key,
84        name: impl IntoJsValue,
85        callback: impl Fn(EventKind) + 'static,
86    ) -> ElementBuilder
87    where
88        EventKind: std::fmt::Debug + Sized + RefFromWasmAbi + FromWasmAbi + 'static,
89        by_event_kind::ElementAddEventListenerMutation<EventKind>:
90            Into<ElementAddEventListenerMutation> + Perform;
91    fn child(self, child: impl Into<ElementBuilder>) -> ElementBuilder;
92    fn key(self, key: impl std::hash::Hash) -> ElementBuilder;
93    fn children(
94        self,
95        children: impl IntoIterator<Item = impl Into<ElementBuilder>>,
96    ) -> ElementBuilder;
97    fn attribute(self, attribute: impl IntoJsValue, value: impl IntoJsValue) -> ElementBuilder;
98    fn build(self) -> ElementWithChildrenRecipe;
99}
100
101pub struct ElementBuilder {
102    key: Option<u64>,
103    kind: TagName,
104    attributes: BTreeMap<AttributeName, AttributeValue>,
105    text: Option<AttributeValue>,
106    input_value: Option<AttributeValue>,
107    children: Vec<ElementBuilder>,
108    event_listeners: Vec<ElementAddEventListenerMutation>,
109}
110
111pub fn calculate_hash<T: std::hash::Hash>(t: &T) -> u64 {
112    let mut s = std::collections::hash_map::DefaultHasher::new();
113    t.hash(&mut s);
114    s.finish()
115}
116
117#[derive(Debug)]
118pub struct ElementRecipe {
119    pub key: Option<u64>,
120    pub create: ElementCreateMutation,
121    pub modify: Vec<ElementBuilderModifyMutation>,
122    pub finish: ElementFinishMutation,
123}
124
125#[derive(Debug)]
126pub struct ElementWithChildrenRecipe {
127    pub element: ElementRecipe,
128    pub children: Vec<Self>,
129}
130
131macro_rules! cached {
132    ($value:expr) => {
133        value_cache::VALUE_CACHE.with(|value_cache| {
134            value_cache
135                .try_borrow_mut()
136                .ok()
137                .map(|mut value_cache| value_cache.cached($value))
138                .unwrap_or_else(|| $value.into_value())
139        })
140    };
141}
142
143impl<'a> From<&'a str> for ElementBuilder {
144    fn from(val: &'a str) -> Self {
145        ElementBuilder::builder(val)
146    }
147}
148
149impl<'a> AsElementBuilder for &'a str {
150    fn into_builder(self) -> ElementBuilder {
151        ElementBuilder::from(self).into_builder()
152    }
153
154    fn text(self, text: impl IntoJsValue) -> ElementBuilder {
155        ElementBuilder::from(self).text(text)
156    }
157
158    fn input_value(self, value: impl IntoJsValue) -> ElementBuilder {
159        ElementBuilder::from(self).input_value(value)
160    }
161
162    fn event<Key: std::hash::Hash, EventKind>(
163        self,
164        key: Key,
165        name: impl IntoJsValue,
166        callback: impl Fn(EventKind) + 'static,
167    ) -> ElementBuilder
168    where
169        EventKind: std::fmt::Debug + Sized + RefFromWasmAbi + FromWasmAbi + 'static,
170        by_event_kind::ElementAddEventListenerMutation<EventKind>:
171            Into<ElementAddEventListenerMutation> + Perform,
172    {
173        ElementBuilder::from(self).event(key, name, callback)
174    }
175
176    fn child(self, child: impl Into<ElementBuilder>) -> ElementBuilder {
177        ElementBuilder::from(self).child(child.into())
178    }
179
180    fn key(self, key: impl std::hash::Hash) -> ElementBuilder {
181        ElementBuilder::from(self).key(key)
182    }
183
184    fn children(
185        self,
186        children: impl IntoIterator<Item = impl Into<ElementBuilder>>,
187    ) -> ElementBuilder {
188        ElementBuilder::from(self).children(children)
189    }
190
191    fn attribute(self, attribute: impl IntoJsValue, value: impl IntoJsValue) -> ElementBuilder {
192        ElementBuilder::from(self).attribute(attribute, value)
193    }
194
195    fn build(self) -> ElementWithChildrenRecipe {
196        ElementBuilder::from(self).build()
197    }
198}
199
200impl AsElementBuilder for ElementBuilder {
201    fn into_builder(self) -> ElementBuilder {
202        self
203    }
204    fn builder(kind: impl IntoJsValue) -> Self {
205        let kind = cached!(kind).into();
206        Self {
207            key: None,
208            kind,
209            input_value: None,
210            text: None,
211            event_listeners: Default::default(),
212            attributes: Default::default(),
213            children: Default::default(),
214        }
215    }
216
217    fn text(mut self, text: impl IntoJsValue) -> Self {
218        self.text = Some(cached!(text).into());
219        self
220    }
221
222    fn input_value(mut self, value: impl IntoJsValue) -> Self {
223        self.input_value = Some(cached!(value).into());
224        self
225    }
226    fn event<Key: std::hash::Hash, EventKind>(
227        mut self,
228        key: Key,
229        name: impl IntoJsValue,
230        callback: impl Fn(EventKind) + 'static,
231    ) -> Self
232    where
233        EventKind: std::fmt::Debug + Sized + RefFromWasmAbi + FromWasmAbi + 'static,
234        by_event_kind::ElementAddEventListenerMutation<EventKind>:
235            Into<ElementAddEventListenerMutation> + Perform,
236    {
237        let hash = calculate_hash(&key);
238        let listener = EventListenerWrapper::<EventKind> {
239            name: cached!(name).into(),
240            closure: KorvinClosure {
241                hash,
242                closure: Arc::new(Closure::new(callback)),
243            },
244        };
245        self.event_listeners
246            .push(by_event_kind::ElementAddEventListenerMutation { listener }.into());
247        self
248    }
249    fn child(mut self, child: impl Into<ElementBuilder>) -> Self {
250        self.children.push(child.into());
251        self
252    }
253    fn key(mut self, key: impl std::hash::Hash) -> Self {
254        self.key = Some(calculate_hash(&key));
255        self
256    }
257
258    fn children(self, children: impl IntoIterator<Item = impl Into<ElementBuilder>>) -> Self {
259        children
260            .into_iter()
261            .fold(self, |parent, child| parent.child(child))
262    }
263
264    fn attribute(mut self, attribute: impl IntoJsValue, value: impl IntoJsValue) -> Self {
265        self.attributes
266            .insert(cached!(attribute).into(), cached!(value).into());
267        self
268    }
269
270    fn build(self) -> ElementWithChildrenRecipe {
271        let Self {
272            key,
273            kind,
274            attributes,
275            children,
276            text,
277            event_listeners,
278            input_value,
279        } = self;
280
281        let element = ElementRecipe {
282            key,
283            create: ElementCreateMutation { kind },
284            modify: empty()
285                .chain(
286                    attributes
287                        .into_iter()
288                        .map(|(attribute, value)| ElementSetAttributeMutation {
289                            attribute,
290                            value: Some(value),
291                        })
292                        .map(ElementBuilderModifyMutation::from),
293                )
294                .chain(
295                    text.into_iter()
296                        .map(|value| ElementSetTextMutation { value: Some(value) })
297                        .map(ElementBuilderModifyMutation::from),
298                )
299                .chain(
300                    input_value
301                        .into_iter()
302                        .map(|value| ElementSetInputValueMutation { value })
303                        .map(ElementBuilderModifyMutation::from),
304                )
305                .chain(
306                    event_listeners
307                        .into_iter()
308                        .map(ElementBuilderModifyMutation::from),
309                )
310                .collect(),
311            finish: ElementFinishMutation {},
312        };
313        ElementWithChildrenRecipe {
314            element,
315            children: children.into_iter().map(Self::build).collect(),
316        }
317    }
318}