1use crate::prelude::*;
2use futures_signals::signal::{Signal, SignalExt};
3pub use hobo_derive::AsElement;
4use std::{
5 any::TypeId,
6 borrow::Cow,
7 collections::{HashMap, HashSet},
8};
9
10#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, AsElement)]
12pub struct Element(pub Entity);
13
14#[derive(Default, Debug)]
15pub(crate) struct Classes {
16 pub(crate) marks: HashSet<TypeId>,
17
18 pub(crate) styles: HashMap<u64, (css::Style, usize)>,
27}
28
29#[cfg(feature = "experimental")]
30pub struct InDom;
31
32#[cfg(feature = "experimental")]
33#[derive(Default)]
34struct OnDomAttachCbs(Vec<Box<dyn FnOnce() + Send + Sync + 'static>>);
35
36#[derive(Default)]
37struct SignalHandlesCollection(Vec<discard::DiscardOnDrop<futures_signals::CancelableFutureHandle>>);
38
39#[cfg(debug_assertions)]
40pub struct Complainer(i32, Closure<dyn Fn()>);
41
42#[cfg(debug_assertions)]
43impl Complainer {
44 pub fn new(entity: Entity) -> Self {
45 let f = Closure::wrap(Box::new(move || {
46 #[wasm_bindgen]
49 extern {
50 #[wasm_bindgen(js_namespace = console)]
51 fn error(msg: String);
52
53 type Error;
54
55 #[wasm_bindgen(constructor)]
56 fn new() -> Error;
57
58 #[wasm_bindgen(structural, method, getter)]
59 fn stack(error: &Error) -> String;
60 }
61
62 log::warn!("[Complainer] Element {} wasn't parented in 1 sec, it's probably a bug\n\nStack:\n\n{}", entity.0, Error::new().stack());
64 }) as Box<dyn Fn()>);
65 let id = web_sys::window().unwrap().set_interval_with_callback_and_timeout_and_arguments_0(f.as_ref().unchecked_ref(), 1000).unwrap();
66
67 Complainer(id, f)
68 }
69}
70
71#[cfg(debug_assertions)]
72impl Drop for Complainer {
73 fn drop(&mut self) {
74 web_sys::window().unwrap().clear_interval_with_handle(self.0);
75 }
76}
77
78impl Element {
79 #[track_caller]
80 fn add_child(self, child: Element) {
81 if self.is_dead() { log::warn!("add_child parent dead {:?}", self.as_entity()); return; }
82 if child.is_dead() { log::warn!("add_child child dead {:?}", child.as_entity()); return; }
83
84 self.get_cmp_mut_or_default::<Children>().push(child.as_entity());
85 child.get_cmp_mut_or_default::<Parent>().0 = self.as_entity();
86
87 if let (Some(parent_node), Some(child_node)) = (self.try_get_cmp::<web_sys::Node>(), child.try_get_cmp::<web_sys::Node>()) {
88 parent_node.append_child(&child_node).expect("can't append child");
89 } else {
90 let parent_has = if self.has_cmp::<web_sys::Node>() { "has" } else { "doesn't have" };
91 let child_has = if child.has_cmp::<web_sys::Node>() { "has" } else { "doesn't have" };
92 log::warn!("trying to add_child, but child {child_has} web_sys::Node and parent {parent_has} web_sys::Node");
93 }
94
95 #[cfg(debug_assertions)] {
96 let caller = std::panic::Location::caller();
97 child.set_attr("data-location", &format!("{}:{}", caller.file(), caller.line()));
98 }
99
100 #[cfg(feature = "experimental")]
104 if !child.has_cmp::<InDom>() {
105 if self.has_cmp::<InDom>() {
106 child.add_component(InDom);
107 if let Some(mut callbacks) = child.try_get_cmp_mut::<OnDomAttachCbs>() {
108 for cb in std::mem::take(&mut callbacks.0) { cb(); }
109 child.remove_cmp::<OnDomAttachCbs>();
110 }
111 } else if let Some(mut callbacks) = child.try_get_cmp_mut::<OnDomAttachCbs>() {
112 self.get_cmp_mut_or_default::<OnDomAttachCbs>().0.append(&mut callbacks.0);
113 }
114 }
115
116 #[cfg(debug_assertions)]
117 child.remove_cmp::<Complainer>();
118 }
119
120 fn leave_parent(self) {
121 if self.is_dead() { log::warn!("leave_parent child dead {:?}", self.as_entity()); return; }
122 let parent = self.get_cmp::<Parent>().0;
123 if parent.is_dead() { log::warn!("leave_parent parent dead {:?}", self.as_entity()); return; }
124
125 if let (Some(parent_node), Some(child_node)) = (parent.try_get_cmp::<web_sys::Node>(), self.try_get_cmp::<web_sys::Node>()) {
126 parent_node.remove_child(&child_node).expect("can't remove child");
127 }
128
129 self.remove_cmp::<Parent>();
130 let mut siblings = parent.get_cmp_mut::<Children>();
131 if let Some(child_pos) = siblings.0.iter().position(|&x| x == self.as_entity()) {
132 siblings.0.remove(child_pos);
133 }
134 }
135
136 #[track_caller]
137 fn add_child_at(self, at_index: usize, child: Element) {
138 if self.is_dead() { log::warn!("add_child_at parent dead {:?}", self.as_entity()); return; }
139 if child.is_dead() { log::warn!("add_child_at child dead {:?}", child.as_entity()); return; }
140
141 let mut children = self.get_cmp_mut_or_default::<Children>();
142 let shifted_sibling = children.get(at_index).copied();
143 children.insert(at_index, child.as_entity());
144 child.get_cmp_mut_or_default::<Parent>().0 = self.as_entity();
145
146 if let (Some(parent_node), Some(child_node), shifted_sibling_node) = (
147 self.try_get_cmp::<web_sys::Node>(),
148 child.try_get_cmp::<web_sys::Node>(),
149 shifted_sibling.and_then(|x| x.try_get_cmp::<web_sys::Node>()),
150 ) {
151 parent_node
152 .insert_before(&child_node, shifted_sibling_node.as_ref().map(|x| &**x as &web_sys::Node))
153 .expect("can't append child");
154
155
156 #[cfg(debug_assertions)] {
157 let caller = std::panic::Location::caller();
158 child.set_attr("data-location", &format!("{}:{}", caller.file(), caller.line()));
159 }
160 }
161 }
162
163 #[track_caller]
165 fn add_child_signal<S>(self, signal: S) where
166 S: Signal<Item = Element> + 'static,
167 {
168 let mut child = crate::create::div().class(crate::css::Display::None).as_element();
170 self.add_child(child);
171 let (handle, fut) = futures_signals::cancelable_future(signal.for_each(move |new_child| {
172 let new_child = new_child.as_element();
173 child.replace_with(new_child);
174 child = new_child;
175 std::future::ready(())
176 }), Default::default);
177
178 wasm_bindgen_futures::spawn_local(fut);
179 self.get_cmp_mut_or_default::<SignalHandlesCollection>().0.push(handle);
180 }
181
182 #[track_caller]
183 fn replace_with(self, other: Element) {
184 let other_entity = other.as_entity();
185 if self.is_dead() { log::warn!("replace_with dead {:?}", self.as_entity()); return; }
186
187 if let (Some(this), Some(other)) = (self.try_get_cmp::<web_sys::Element>(), other_entity.try_get_cmp::<web_sys::Node>()) {
188 this.replace_with_with_node_1(&other).unwrap();
189 } else {
190 let self_has = if self.has_cmp::<web_sys::Node>() { "has" } else { "doesn't have" };
191 let other_has = if other.has_cmp::<web_sys::Node>() { "has" } else { "doesn't have" };
192 log::warn!("trying to replace_with, but self {self_has} web_sys::Node and other {other_has} web_sys::Node");
193 }
194
195 #[cfg(debug_assertions)] {
196 let caller = std::panic::Location::caller();
197 other.set_attr("data-location", &format!("{}:{}", caller.file(), caller.line()));
198 }
199
200 if let Some(parent) = self.try_get_cmp::<Parent>().map(|x| x.0) {
202 if parent.is_dead() { log::warn!("replace_with parent dead {:?}", parent); return; }
203 let mut children = parent.get_cmp_mut::<Children>();
204 let position = children.0.iter().position(|&x| x == self.as_entity()).expect("entity claims to be a child while missing in parent");
205 children.0[position] = other.as_entity();
206 other_entity.get_cmp_mut_or_default::<Parent>().0 = parent;
207
208 #[cfg(debug_assertions)]
209 other.remove_cmp::<Complainer>();
210 }
211
212 self.remove();
213 }
214}
215
216pub trait AsElement: AsEntity + Sized {
218 #[cfg(feature = "experimental")]
219 const MARK: Option<fn() -> std::any::TypeId> = None;
220
221 #[cfg(all(debug_assertions, feature = "experimental"))]
222 const TYPE: Option<fn() -> &'static str> = None;
223
224 #[track_caller]
225 fn add_child<T: AsElement>(&self, child: T) {
226 #[cfg(feature = "experimental")]
227 if let Some(mark) = T::MARK { child.get_cmp_mut_or_default::<Classes>().marks.insert(mark()); }
228
229 #[cfg(all(debug_assertions, feature = "experimental"))]
230 if let Some(type_id) = T::TYPE { child.set_attr("data-type", type_id()); }
231
232 Element::add_child(self.as_element(), child.as_element());
233 }
234 #[track_caller] #[must_use] fn child(self, child: impl AsElement) -> Self { self.add_child(child); self }
235 #[track_caller] #[must_use] fn with_child<T: AsElement>(self, f: impl FnOnce(&Self) -> T) -> Self { let c = f(&self); self.child(c) }
236 #[track_caller] fn add_children<Item: AsElement>(&self, children: impl IntoIterator<Item = Item>) { for child in children.into_iter() { self.add_child(child); } }
237 #[track_caller] #[must_use] fn children<Item: AsElement>(self, children: impl IntoIterator<Item = Item>) -> Self { self.add_children(children); self }
238 fn leave_parent(self) { Element::leave_parent(self.as_element()) }
239
240 #[track_caller]
242 fn add_child_at<T: AsElement>(&self, at_index: usize, child: T) {
243 #[cfg(feature = "experimental")]
244 if let Some(mark) = T::MARK { child.get_cmp_mut_or_default::<Classes>().marks.insert(mark()); }
245
246 #[cfg(all(debug_assertions, feature = "experimental"))]
247 if let Some(type_id) = T::TYPE { child.set_attr("data-type", type_id()); }
248
249 Element::add_child_at(self.as_element(), at_index, child.as_element());
250 }
251
252 #[track_caller]
254 fn add_child_signal<S, E>(&self, signal: S) where
255 E: AsElement,
256 S: Signal<Item = E> + 'static,
257 {
258 Element::add_child_signal(self.as_element(), signal.map(|x| x.as_element()));
259 }
260 #[track_caller]
261 #[must_use]
262 fn child_signal<S, E>(self, signal: S) -> Self where
263 E: AsElement,
264 S: Signal<Item = E> + 'static,
265 { self.add_child_signal(signal); self }
266
267 fn set_class_tagged<Tag: std::hash::Hash + 'static>(&self, tag: Tag, style: impl Into<css::Style>) {
268 if self.is_dead() { log::warn!("set_class_tagged dead {:?}", self.as_entity()); return; }
269
270 let tag_hash = {
273 use std::hash::{Hash, Hasher};
274 let mut hasher = std::collections::hash_map::DefaultHasher::new();
275 TypeId::of::<Tag>().hash(&mut hasher);
276 tag.hash(&mut hasher);
277 hasher.finish()
278 };
279
280 let mut classes = self.get_cmp_mut_or_default::<Classes>();
281 let len = classes.styles.len();
282 classes.styles.insert(tag_hash, (style.into(), len));
283 }
284 fn set_class_typed<Type: 'static>(&self, style: impl Into<css::Style>) { self.set_class_tagged(TypeId::of::<Type>(), style) }
285 fn set_class(&self, style: impl Into<css::Style>) { self.set_class_tagged(0u64, style); }
286 fn add_class(&self, style: impl Into<css::Style>) {
287 let id = self.try_get_cmp::<Classes>().map(|x| x.styles.len() as u64).unwrap_or(0);
288 self.set_class_tagged(id, style);
289 }
290 #[must_use] fn class(self, style: impl Into<css::Style>) -> Self { self.add_class(style); self }
291 #[must_use] fn class_tagged<Tag: std::hash::Hash + 'static>(self, tag: Tag, style: impl Into<css::Style>) -> Self { self.set_class_tagged(tag, style); self }
292 #[must_use] fn class_typed<Type: 'static>(self, style: impl Into<css::Style>) -> Self { self.set_class_typed::<Type>(style); self }
293
294 fn set_class_signal<S, I>(&self, signal: S) where
295 I: Into<css::Style>,
296 S: Signal<Item = I> + 'static,
297 {
298 let entity = self.as_entity();
299 if entity.is_dead() { log::warn!("set_class_signal dead entity {:?}", entity); return; }
300 let (handle, fut) = futures_signals::cancelable_future(signal.for_each(move |class| {
301 Element(entity).set_class(class);
302 std::future::ready(())
303 }), Default::default);
304
305 wasm_bindgen_futures::spawn_local(fut);
306 self.get_cmp_mut_or_default::<SignalHandlesCollection>().0.push(handle);
307 }
308 #[must_use]
309 fn class_signal<S, I>(self, signal: S) -> Self where
310 I: Into<css::Style>,
311 S: Signal<Item = I> + 'static,
312 { self.set_class_signal(signal); self }
313
314 fn set_class_typed_signal<Type, S, I>(&self, signal: S) where
315 Type: 'static,
316 I: Into<css::Style>,
317 S: Signal<Item = I> + 'static,
318 {
319 let entity = self.as_entity();
320 if entity.is_dead() { log::warn!("set_class_signal dead entity {:?}", entity); return; }
321 let (handle, fut) = futures_signals::cancelable_future(signal.for_each(move |class| {
322 Element(entity).set_class_typed::<Type>(class.into());
323 std::future::ready(())
324 }), Default::default);
325
326 wasm_bindgen_futures::spawn_local(fut);
327 self.get_cmp_mut_or_default::<SignalHandlesCollection>().0.push(handle);
328 }
329 #[must_use]
330 fn class_typed_signal<Type, S, I>(self, signal: S) -> Self where
331 Type: 'static,
332 I: Into<css::Style>,
333 S: Signal<Item = I> + 'static,
334 { self.set_class_typed_signal::<Type, S, I>(signal); self }
335
336 fn set_class_tagged_signal<Tag, S, I>(&self, tag: Tag, signal: S) where
337 Tag: std::hash::Hash + Copy + 'static,
338 I: Into<css::Style>,
339 S: Signal<Item = I> + 'static,
340 {
341 let entity = self.as_entity();
342 if entity.is_dead() { log::warn!("set_class_signal dead entity {:?}", entity); return; }
343 let (handle, fut) = futures_signals::cancelable_future(signal.for_each(move |class| {
344 Element(entity).set_class_tagged(tag, class);
345 std::future::ready(())
346 }), Default::default);
347
348 wasm_bindgen_futures::spawn_local(fut);
349 self.get_cmp_mut_or_default::<SignalHandlesCollection>().0.push(handle);
350 }
351 #[must_use]
352 fn class_tagged_signal<Tag, S, I>(self, tag: Tag, signal: S) -> Self where
353 Tag: std::hash::Hash + Copy + 'static,
354 I: Into<css::Style>,
355 S: Signal<Item = I> + 'static,
356 { self.set_class_tagged_signal::<Tag, S, I>(tag, signal); self }
357
358 fn get_attr<'k>(&self, key: impl Into<Cow<'k, str>>) -> Option<String> {
359 if self.is_dead() { log::warn!("get_attr dead {:?}", self.as_entity()); return None; }
360 let key = key.into();
361 self.get_cmp::<web_sys::Element>().get_attribute(&key)
362 }
363 fn set_attr<'k, 'v>(&self, key: impl Into<Cow<'k, str>>, value: impl Into<Cow<'v, str>>) {
364 if self.is_dead() { log::warn!("set_attr dead {:?}", self.as_entity()); return; }
365 let key = key.into();
366 let value = value.into();
367 self.get_cmp::<web_sys::Element>().set_attribute(&key, &value).unwrap_or_else(|_| panic!("can't set attribute {} to {}", key, value));
368 }
369 #[must_use] fn attr<'k, 'v>(self, key: impl Into<Cow<'k, str>>, value: impl Into<Cow<'v, str>>) -> Self { self.set_attr(key, value); self }
370 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) } }
371 #[must_use] fn bool_attr<'k>(self, key: impl Into<Cow<'k, str>>, value: bool) -> Self { self.set_bool_attr(key, value); self }
372 fn remove_attr<'k>(&self, key: impl Into<Cow<'k, str>>) {
373 if self.is_dead() { log::warn!("remove_attr dead {:?}", self.as_entity()); return; }
374 self.get_cmp::<web_sys::Element>().remove_attribute(&key.into()).expect("can't remove attribute");
375 }
376
377 fn set_attr_signal<'k, 'v, S, K, V>(&self, attr: K, signal: S) where
378 K: Into<Cow<'k, str>>,
379 V: Into<Cow<'v, str>>,
380 S: Signal<Item = V> + 'static,
381 {
382 let entity = self.as_entity();
383 if entity.is_dead() { log::warn!("set_attr_signal dead entity {:?}", entity); return; }
384 let attr = attr.into().into_owned();
385 let (handle, fut) = futures_signals::cancelable_future(signal.for_each(move |v| {
386 Element(entity).set_attr(&attr, v);
387 std::future::ready(())
388 }), Default::default);
389
390 wasm_bindgen_futures::spawn_local(fut);
391 self.get_cmp_mut_or_default::<SignalHandlesCollection>().0.push(handle);
392 }
393 #[must_use]
394 fn attr_signal<'k, 'v, S, K, V>(self, attr: K, signal: S) -> Self where
395 K: Into<Cow<'k, str>>,
396 V: Into<Cow<'v, str>>,
397 S: Signal<Item = V> + 'static,
398 { self.set_attr_signal(attr, signal); self }
399
400 fn set_bool_attr_signal<'k, S, K>(&self, attr: K, signal: S) where
401 K: Into<Cow<'k, str>>,
402 S: Signal<Item = bool> + 'static,
403 {
404 let entity = self.as_entity();
405 if entity.is_dead() { log::warn!("set_attr_signal dead entity {:?}", entity); return; }
406 let attr = attr.into().into_owned();
407 let (handle, fut) = futures_signals::cancelable_future(signal.for_each(move |v| {
408 Element(entity).set_bool_attr(&attr, v);
409 std::future::ready(())
410 }), Default::default);
411
412 wasm_bindgen_futures::spawn_local(fut);
413 self.get_cmp_mut_or_default::<SignalHandlesCollection>().0.push(handle);
414 }
415 #[must_use]
416 fn bool_attr_signal<'k, S, K>(self, attr: K, signal: S) -> Self where
417 K: Into<Cow<'k, str>>,
418 S: Signal<Item = bool> + 'static,
419 { self.set_bool_attr_signal(attr, signal); self }
420
421 fn set_text<'a>(&self, text: impl Into<std::borrow::Cow<'a, str>>) {
422 if self.is_dead() { log::warn!("set_text dead entity {:?}", self.as_entity()); return; }
423 self.get_cmp::<web_sys::Node>().set_text_content(Some(&text.into()));
424 }
425 #[must_use] fn text<'a>(self, x: impl Into<std::borrow::Cow<'a, str>>) -> Self { self.set_text(x); self }
426
427 fn set_text_signal<'a, S, I>(&self, signal: S) where
428 I: Into<Cow<'a, str>>,
429 S: Signal<Item = I> + 'static,
430 {
431 let entity = self.as_entity();
432 if entity.is_dead() { log::warn!("set_text_signal dead entity {:?}", entity); return; }
433 let (handle, fut) = futures_signals::cancelable_future(signal.for_each(move |text| {
434 Element(entity).set_text(text);
435 std::future::ready(())
436 }), Default::default);
437
438 wasm_bindgen_futures::spawn_local(fut);
439 self.get_cmp_mut_or_default::<SignalHandlesCollection>().0.push(handle);
440 }
441 #[must_use]
442 fn text_signal<'a, S, I>(self, x: S) -> Self where
443 I: Into<Cow<'a, str>>,
444 S: Signal<Item = I> + 'static,
445 { self.set_text_signal(x); self }
446
447 fn set_style(&self, style: impl AppendProperty) {
448 let mut props = Vec::new();
449 style.append_property(&mut props);
450 self.set_attr(web_str::style(), props.iter().map(std::string::ToString::to_string).collect::<String>());
451 }
452 #[must_use] fn style(self, style: impl AppendProperty) -> Self { self.set_style(style); self }
453 fn remove_style(&self) { self.remove_attr(web_str::style()); }
454
455 fn set_style_signal<S, I>(&self, signal: S) where
456 I: AppendProperty,
457 S: Signal<Item = I> + 'static,
458 {
459 let entity = self.as_entity();
460 if entity.is_dead() { log::warn!("set_style_signal dead entity {:?}", entity); return; }
461 let (handle, fut) = futures_signals::cancelable_future(signal.for_each(move |style| {
462 Element(entity).set_style(style);
463 std::future::ready(())
464 }), Default::default);
465
466 wasm_bindgen_futures::spawn_local(fut);
467 self.get_cmp_mut_or_default::<SignalHandlesCollection>().0.push(handle);
468 }
469 #[must_use]
470 fn style_signal<S, I>(self, signal: S) -> Self where
471 I: AppendProperty,
472 S: Signal<Item = I> + 'static,
473 { self.set_style_signal(signal); self }
474
475 fn mark<T: 'static>(self) -> Self {
476 if self.is_dead() { log::warn!("mark dead {:?}", self.as_entity()); return self; }
477 self.get_cmp_mut_or_default::<Classes>().marks.insert(TypeId::of::<T>());
478 self
479 }
480 fn unmark<T: 'static>(self) -> Self {
481 if self.is_dead() { log::warn!("unmark dead {:?}", self.as_entity()); return self; }
482 self.get_cmp_mut_or_default::<Classes>().marks.remove(&TypeId::of::<T>());
483 self
484 }
485 #[must_use]
486 fn mark_signal<T: 'static, S>(self, signal: S) -> Self where
487 S: Signal<Item = bool> + 'static,
488 {
489 let entity = self.as_entity();
490 if entity.is_dead() { log::warn!("mark_signal dead entity {:?}", entity); return self; }
491 let (handle, fut) = futures_signals::cancelable_future(signal.for_each(move |enabled| {
492 if enabled { Element(entity).mark::<T>(); } else { Element(entity).unmark::<T>(); }
493 std::future::ready(())
494 }), Default::default);
495
496 wasm_bindgen_futures::spawn_local(fut);
497 self.get_cmp_mut_or_default::<SignalHandlesCollection>().0.push(handle);
498 self
499 }
500
501 #[must_use] fn with_component<T: 'static>(self, f: impl FnOnce(&Self) -> T) -> Self { self.add_component(f(&self)); self }
502
503 #[track_caller]
505 fn replace_with<T: AsElement>(&self, other: T) -> T {
506 #[cfg(feature = "experimental")]
507 if let Some(mark) = T::MARK { other.get_cmp_mut_or_default::<Classes>().marks.insert(mark()); }
508
509 #[cfg(all(debug_assertions, feature = "experimental"))]
510 if let Some(type_id) = T::TYPE { other.set_attr("data-type", type_id()); }
511
512 Element::replace_with(self.as_element(), other.as_element());
513 other
514 }
515
516 fn parent(&self) -> Element {
517 let parent = self.get_cmp::<Parent>().0;
518 debug_assert!(parent.try_get_cmp::<web_sys::HtmlElement>().is_some());
519 Element(parent)
520 }
521
522 #[cfg(feature = "experimental")]
523 fn add_on_dom_attach(&self, cb: impl FnOnce() + Send + Sync + 'static) {
524 if self.has_cmp::<InDom>() { cb(); return; }
525 self.get_cmp_mut_or_default::<OnDomAttachCbs>().0.push(Box::new(cb));
526 }
527 #[cfg(feature = "experimental")]
528 fn on_dom_attach(self, cb: impl FnOnce() + Send + Sync + 'static) -> Self { self.add_on_dom_attach(cb); self }
529
530 #[deprecated = "use .tap() instead"]
531 fn with(self, f: impl FnOnce(&Self)) -> Self { f(&self); self }
532 fn as_element(&self) -> Element { Element(self.as_entity()) }
533}
534
535impl<T: AsElement> AsElement for &T {}
536impl<T: AsElement> AsElement for &mut T {}