hobo_core/
element.rs

1use crate::prelude::*;
2use futures_signals::signal::SignalExt;
3pub use hobo_derive::Element;
4use std::{
5	any::TypeId,
6	borrow::Cow,
7	collections::{HashMap, HashSet},
8};
9
10/// An `Element` with specific type erased
11#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Element)]
12pub struct SomeElement(pub Entity);
13
14#[derive(Default)]
15pub(crate) struct Classes {
16	pub(crate) marks: HashSet<TypeId>,
17	pub(crate) styles: HashMap<u64, css::Style>,
18}
19
20#[derive(Default)]
21struct SignalHandlesCollection(Vec<discard::DiscardOnDrop<futures_signals::CancelableFutureHandle>>);
22
23/// Marker trait for an entity that has `web_sys::Node`, `web_sys::Element`, `web_sys::EventTarget` and one of `web_sys::HtmlElement` or `web_sys::SvgElement` as attached components
24pub trait Element: AsEntity + Sized {
25	fn add_child(&self, child: impl Element) {
26		if self.is_dead() { log::warn!("add_child parent dead {:?}", self.as_entity()); return; }
27		if child.is_dead() { log::warn!("add_child child dead {:?}", child.as_entity()); return; }
28		self.get_cmp_mut_or_default::<Children>().push(child.as_entity());
29		child.get_cmp_mut_or_default::<Parent>().0 = self.as_entity();
30
31		// why not unwrapping? how can this fail?
32		if let (Some(parent_node), Some(child_node)) = (self.try_get_cmp::<web_sys::Node>(), child.try_get_cmp::<web_sys::Node>()) {
33			parent_node.append_child(&child_node).expect("can't append child");
34		}
35	}
36	fn child(self, child: impl Element) -> Self { self.add_child(child); self }
37	fn add_children<Item: Element>(&self, children: impl IntoIterator<Item = Item>) { for child in children.into_iter() { self.add_child(child); } }
38	fn children<Item: Element>(self, children: impl IntoIterator<Item = Item>) -> Self { self.add_children(children); self }
39	fn leave_parent(self) {
40		if self.is_dead() { log::warn!("leave_parent child dead {:?}", self.as_entity()); return; }
41		let parent = self.get_cmp::<Parent>().0;
42		if parent.is_dead() { log::warn!("leave_parent parent dead {:?}", self.as_entity()); return; }
43
44		if let (Some(parent_node), Some(child_node)) = (parent.try_get_cmp::<web_sys::Node>(), self.try_get_cmp::<web_sys::Node>()) {
45			parent_node.remove_child(&child_node).expect("can't remove child");
46		}
47
48		self.remove_cmp::<Parent>();
49		let mut siblings = parent.get_cmp_mut::<Children>();
50		if let Some(child_pos) = siblings.0.iter().position(|&x| x == self.as_entity()) {
51			siblings.0.remove(child_pos);
52		}
53	}
54
55	// add a child at an index, useful to update tables without regenerating the whole container element
56	fn add_child_at(&self, at_id: usize, child: impl Element) {
57		if self.is_dead() { log::warn!("add_child_at parent dead {:?}", self.as_entity()); return; }
58		if child.is_dead() { log::warn!("add_child_at child dead {:?}", child.as_entity()); return; }
59		let mut children = self.get_cmp_mut_or_default::<Children>();
60		let shifted_sibling = children.get(at_id).copied();
61		children.insert(at_id, child.as_entity());
62		child.get_cmp_mut_or_default::<Parent>().0 = self.as_entity();
63
64		if let (Some(parent_node), Some(child_node), shifted_sibling_node) = (
65			self.try_get_cmp::<web_sys::Node>(),
66			child.try_get_cmp::<web_sys::Node>(),
67			shifted_sibling.map(|x| x.try_get_cmp::<web_sys::Node>()).flatten(),
68		) {
69			parent_node
70				.insert_before(&child_node, shifted_sibling_node.as_ref().map(|x| &**x as &web_sys::Node))
71				.expect("can't append child");
72		}
73	}
74
75	// be mindful about holding child references with this one
76	fn add_child_signal<S, E>(&self, signal: S) where
77		E: Element,
78		S: futures_signals::signal::Signal<Item = E> + 'static,
79	{
80		// placeholder at first
81		let mut child = crate::components::div().class(crate::css::Display::None).erase();
82		self.add_child(child);
83		let (handle, fut) = futures_signals::cancelable_future(signal.for_each(move |new_child| {
84			let new_child = new_child.erase();
85			child.replace_with(new_child);
86			child = new_child;
87			async move {}
88		}), || {});
89
90		wasm_bindgen_futures::spawn_local(fut);
91		self.get_cmp_mut_or_default::<SignalHandlesCollection>().0.push(handle);
92	}
93	fn child_signal<S, E>(self, signal: S) -> Self where
94		E: Element,
95		S: futures_signals::signal::Signal<Item = E> + 'static,
96	{ self.add_child_signal(signal); self }
97
98	fn set_class_tagged<Tag: std::hash::Hash + 'static>(&self, tag: Tag, style: impl Into<css::Style>) {
99		if self.is_dead() { log::warn!("set_class_tagged dead {:?}", self.as_entity()); return; }
100
101		// tested and different types with same byte-level representation hash to the same thing (not surprising)
102		// i.e. the type is not taken into account when hashing so I have to do it manually
103		let tag_hash = {
104			use std::hash::{Hash, Hasher};
105			let mut hasher = std::collections::hash_map::DefaultHasher::new();
106			TypeId::of::<Tag>().hash(&mut hasher);
107			tag.hash(&mut hasher);
108			hasher.finish()
109		};
110
111		self.get_cmp_mut_or_default::<Classes>().styles.insert(tag_hash, style.into());
112	}
113	// Cannot mix impl Into<css::Style> with generic type arguments
114	fn set_class_typed<Type: 'static>(&self, style: css::Style) {
115		self.set_class_tagged(TypeId::of::<Type>(), style)
116	}
117	fn set_class(&self, style: impl Into<css::Style>) { self.set_class_tagged(0u64, style); }
118	fn add_class(&self, style: impl Into<css::Style>) {
119		let id = self.try_get_cmp::<Classes>().map(|x| x.styles.len() as u64).unwrap_or(0);
120		self.set_class_tagged(id, style);
121	}
122	fn class(self, style: impl Into<css::Style>) -> Self { self.add_class(style); self }
123	fn class_tagged<Tag: std::hash::Hash + 'static>(self, tag: Tag, style: impl Into<css::Style>) -> Self { self.set_class_tagged(tag, style); self }
124	fn class_typed<Type: 'static>(self, style: css::Style) -> Self { self.set_class_typed::<Type>(style); self }
125
126	fn set_class_signal<S, I>(&self, signal: S) where
127		I: Into<css::Style>,
128		S: futures_signals::signal::Signal<Item = I> + 'static,
129	{
130		let entity = self.as_entity();
131		if entity.is_dead() { log::warn!("set_class_signal dead entity {:?}", entity); return; }
132		let (handle, fut) = futures_signals::cancelable_future(signal.for_each(move |class| {
133			SomeElement(entity).set_class(class);
134			async move { }
135		}), || {});
136
137		wasm_bindgen_futures::spawn_local(fut);
138		self.get_cmp_mut_or_default::<SignalHandlesCollection>().0.push(handle);
139	}
140	fn class_signal<S, I>(self, signal: S) -> Self where
141		I: Into<css::Style>,
142		S: futures_signals::signal::Signal<Item = I> + 'static,
143	{ self.set_class_signal(signal); self }
144
145	fn set_class_typed_signal<Type, S, I>(&self, signal: S) where
146		Type: 'static,
147		I: Into<css::Style>,
148		S: futures_signals::signal::Signal<Item = I> + 'static,
149	{
150		let entity = self.as_entity();
151		if entity.is_dead() { log::warn!("set_class_signal dead entity {:?}", entity); return; }
152		let (handle, fut) = futures_signals::cancelable_future(signal.for_each(move |class| {
153			SomeElement(entity).set_class_typed::<Type>(class.into());
154			async move { }
155		}), || {});
156
157		wasm_bindgen_futures::spawn_local(fut);
158		self.get_cmp_mut_or_default::<SignalHandlesCollection>().0.push(handle);
159	}
160	fn class_typed_signal<Type, S, I>(self, signal: S) -> Self where
161		Type: 'static,
162		I: Into<css::Style>,
163		S: futures_signals::signal::Signal<Item = I> + 'static,
164	{ self.set_class_typed_signal::<Type, S, I>(signal); self }
165
166	fn set_class_tagged_signal<Tag, S, I>(&self, tag: Tag, signal: S) where
167		Tag: std::hash::Hash + Copy + 'static,
168		I: Into<css::Style>,
169		S: futures_signals::signal::Signal<Item = I> + 'static,
170	{
171		let entity = self.as_entity();
172		if entity.is_dead() { log::warn!("set_class_signal dead entity {:?}", entity); return; }
173		let (handle, fut) = futures_signals::cancelable_future(signal.for_each(move |class| {
174			SomeElement(entity).set_class_tagged(tag, class);
175			async move { }
176		}), || {});
177
178		wasm_bindgen_futures::spawn_local(fut);
179		self.get_cmp_mut_or_default::<SignalHandlesCollection>().0.push(handle);
180	}
181	fn class_tagged_signal<Tag, S, I>(self, tag: Tag, signal: S) -> Self where
182		Tag: std::hash::Hash + Copy + 'static,
183		I: Into<css::Style>,
184		S: futures_signals::signal::Signal<Item = I> + 'static,
185	{ self.set_class_tagged_signal::<Tag, S, I>(tag, signal); self }
186
187	fn set_attr<'k, 'v>(&self, key: impl Into<Cow<'k, str>>, value: impl Into<Cow<'v, str>>) {
188		if self.is_dead() { log::warn!("set_attr dead {:?}", self.as_entity()); return; }
189		let key = key.into();
190		let value = value.into();
191		self.get_cmp::<web_sys::Element>().set_attribute(&key, &value).unwrap_or_else(|_| panic!("can't set attribute {} to {}", key, value));
192	}
193	fn attr<'k, 'v>(self, key: impl Into<Cow<'k, str>>, value: impl Into<Cow<'v, str>>) -> Self { self.set_attr(key, value); self }
194	fn set_bool_attr<'k>(&self, key: impl Into<Cow<'k, str>>, value: bool) { if value { self.set_attr(key, "") } else { self.remove_attr(key) } }
195	fn bool_attr<'k>(self, key: impl Into<Cow<'k, str>>, value: bool) -> Self { self.set_bool_attr(key, value); self }
196	fn remove_attr<'k>(&self, key: impl Into<Cow<'k, str>>) {
197		if self.is_dead() { log::warn!("remove_attr dead {:?}", self.as_entity()); return; }
198		self.get_cmp::<web_sys::Element>().remove_attribute(&key.into()).expect("can't remove attribute");
199	}
200
201	fn set_attr_signal<'k, 'v, S, K, V>(&self, signal: S) where
202		K: Into<Cow<'k, str>>,
203		V: Into<Cow<'v, str>>,
204		S: futures_signals::signal::Signal<Item = (K, V)> + 'static,
205	{
206		let entity = self.as_entity();
207		if entity.is_dead() { log::warn!("set_attr_signal dead entity {:?}", entity); return; }
208		let (handle, fut) = futures_signals::cancelable_future(signal.for_each(move |(k, v)| {
209			SomeElement(entity).set_attr(k, v);
210			async move { }
211		}), || {});
212
213		wasm_bindgen_futures::spawn_local(fut);
214		self.get_cmp_mut_or_default::<SignalHandlesCollection>().0.push(handle);
215	}
216	fn attr_signal<'k, 'v, S, K, V>(self, x: S) -> Self where
217		K: Into<Cow<'k, str>>,
218		V: Into<Cow<'v, str>>,
219		S: futures_signals::signal::Signal<Item = (K, V)> + 'static,
220	{ self.set_attr_signal(x); self }
221
222	fn set_bool_attr_signal<'k, S, K>(&self, attr: K, signal: S) where
223		K: Into<Cow<'k, str>>,
224		S: futures_signals::signal::Signal<Item = bool> + 'static,
225	{
226		let entity = self.as_entity();
227		if entity.is_dead() { log::warn!("set_attr_signal dead entity {:?}", entity); return; }
228		let attr = attr.into().into_owned();
229		let (handle, fut) = futures_signals::cancelable_future(signal.for_each(move |v| {
230			SomeElement(entity).set_bool_attr(&attr, v);
231			async move { }
232		}), || {});
233
234		wasm_bindgen_futures::spawn_local(fut);
235		self.get_cmp_mut_or_default::<SignalHandlesCollection>().0.push(handle);
236	}
237	fn bool_attr_signal<'k, S, K>(self, attr: K, x: S) -> Self where
238		K: Into<Cow<'k, str>>,
239		S: futures_signals::signal::Signal<Item = bool> + 'static,
240	{ self.set_bool_attr_signal(attr, x); self }
241
242	fn set_text<'a>(&self, text: impl Into<std::borrow::Cow<'a, str>>) {
243		if self.is_dead() { log::warn!("set_text dead entity {:?}", self.as_entity()); return; }
244		self.get_cmp::<web_sys::Node>().set_text_content(Some(&text.into()));
245	}
246	fn text<'a>(self, x: impl Into<std::borrow::Cow<'a, str>>) -> Self { self.set_text(x); self }
247
248	fn set_text_signal<'a, S, I>(&self, signal: S) where
249		I: Into<Cow<'a, str>>,
250		S: futures_signals::signal::Signal<Item = I> + 'static,
251	{
252		let entity = self.as_entity();
253		if entity.is_dead() { log::warn!("set_text_signal dead entity {:?}", entity); return; }
254		let (handle, fut) = futures_signals::cancelable_future(signal.for_each(move |text| {
255			SomeElement(entity).set_text(text);
256			async move { }
257		}), || {});
258
259		wasm_bindgen_futures::spawn_local(fut);
260		self.get_cmp_mut_or_default::<SignalHandlesCollection>().0.push(handle);
261	}
262	fn text_signal<'a, S, I>(self, x: S) -> Self where
263		I: Into<Cow<'a, str>>,
264		S: futures_signals::signal::Signal<Item = I> + 'static,
265	{ self.set_text_signal(x); self }
266
267	fn set_style(&self, style: impl AppendProperty) {
268		let mut props = Vec::new();
269		style.append_property(&mut props);
270		self.set_attr(web_str::style(), props.iter().map(std::string::ToString::to_string).collect::<String>());
271	}
272	fn style(self, style: impl AppendProperty) -> Self { self.set_style(style); self }
273	fn remove_style(&self) { self.remove_attr(web_str::style()); }
274
275	fn set_style_signal<S, I>(&self, signal: S) where
276		I: AppendProperty,
277		S: futures_signals::signal::Signal<Item = I> + 'static,
278	{
279		let entity = self.as_entity();
280		if entity.is_dead() { log::warn!("set_style_signal dead entity {:?}", entity); return; }
281		let (handle, fut) = futures_signals::cancelable_future(signal.for_each(move |style| {
282			SomeElement(entity).set_style(style);
283			async move { }
284		}), || {});
285
286		wasm_bindgen_futures::spawn_local(fut);
287		self.get_cmp_mut_or_default::<SignalHandlesCollection>().0.push(handle);
288	}
289	fn style_signal<S, I>(self, signal: S) -> Self where
290		I: AppendProperty,
291		S: futures_signals::signal::Signal<Item = I> + 'static,
292	{ self.set_style_signal(signal); self }
293
294	fn mark<T: 'static>(self) -> Self {
295		if self.is_dead() { log::warn!("mark dead {:?}", self.as_entity()); return self; }
296		self.get_cmp_mut_or_default::<Classes>().marks.insert(TypeId::of::<T>());
297		self
298	}
299	fn unmark<T: 'static>(self) -> Self {
300		if self.is_dead() { log::warn!("unmark dead {:?}", self.as_entity()); return self; }
301		self.get_cmp_mut_or_default::<Classes>().marks.remove(&TypeId::of::<T>());
302		self
303	}
304	fn mark_signal<T: 'static, S>(self, signal: S) -> Self where
305		S: futures_signals::signal::Signal<Item = bool> + 'static,
306	{
307		let entity = self.as_entity();
308		if entity.is_dead() { log::warn!("mark_signal dead entity {:?}", entity); return self; }
309		let (handle, fut) = futures_signals::cancelable_future(signal.for_each(move |enabled| {
310			if enabled { SomeElement(entity).mark::<T>(); } else { SomeElement(entity).unmark::<T>(); }
311			async move { }
312		}), || {});
313
314		wasm_bindgen_futures::spawn_local(fut);
315		self.get_cmp_mut_or_default::<SignalHandlesCollection>().0.push(handle);
316		self
317	}
318
319	fn with_component<T: 'static>(self, f: impl FnOnce(&Self) -> T) -> Self { self.add_component(f(&self)); self }
320
321	// TODO: this should steal components from other and delete it
322	// instead of deleting self
323	// this would cause a lot less issue with invalidating stuff
324	// !!!!!! NOT TRUE - any handler that was created with the new entity will be busted, so this is fine
325	fn replace_with<T: Element>(&self, other: T) -> T {
326		let other_entity = other.as_entity();
327		if self.is_dead() { log::warn!("replace_with dead {:?}", self.as_entity()); return other; }
328
329		// why not unwrapping? how can this fail?
330		if let (Some(this), Some(other)) = (self.try_get_cmp::<web_sys::Element>(), other_entity.try_get_cmp::<web_sys::Node>()) {
331			this.replace_with_with_node_1(&other).unwrap();
332		}
333
334		// Fix up reference in parent
335		if let Some(parent) = self.try_get_cmp::<Parent>().map(|x| x.0) {
336			if parent.is_dead() { log::warn!("replace_with parent dead {:?}", parent); return other; }
337			let mut children = parent.get_cmp_mut::<Children>();
338			let position = children.0.iter().position(|&x| x == self.as_entity()).expect("entity claims to be a child while missing in parent");
339			children.0[position] = other.as_entity();
340			other_entity.get_cmp_mut_or_default::<Parent>().0 = parent;
341		}
342
343		self.remove();
344		// WORLD.remove_entity(self);
345		other
346	}
347
348	fn with(self, f: impl FnOnce(&Self)) -> Self { f(&self); self }
349	fn erase(&self) -> SomeElement { SomeElement(self.as_entity()) }
350}
351
352impl<T: Element> Element for &T {}
353impl<T: Element> Element for &mut T {}