1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
//! Erasable web type stand-ins used as callback parameters.
//!
//! `struct`s in this module are only inhabited with the `"callbacks"` feature enabled.  
//! Without it, they become [uninhabited](https://doc.rust-lang.org/nomicon/exotic-sizes.html#empty-types) and are erased entirely at compile-time, so any code paths that depend on them can in turn be removed too.
#![allow(clippy::inline_always)]

use crate::sealed::Sealed;

/// Used as DOM reference callback parameter. (Expand for implementation contract!)
///
/// When you receive a [`DomRef`] containing a stand-in type, use [`Materialize::materialize`] to convert it to the actual value.
///
/// # Implementation Contract
///
/// > **This is not a soundness contract**. Code using this type must not rely on it for soundness. However, it is free to panic when encountering an incorrect implementation.
///
/// ## For VDOM-to-DOM renderers:
///
/// If a renderer invoked a callback with the [`Added`](`DomRef::Added`) variant, it **must** invoke it with the [`Removing`](`DomRef::Removing`) variant before destroying or reusing the relevant part of the DOM.
///
/// This includes cases where the identity of the `CallbackRef` or DOM node changes, in which case the new reference is [`Added`](`DomRef::Added`) after, in this order, [`Removing`](`DomRef::Removing`) the old reference and updating the relevant part(s) of the DOM.
///
/// ## For apps/VDOM renderers:
///
/// Tearing down and reconstructing the child DOM according to the current child VDOM must be possible at any time.
///
/// <!-- The above is a fairly strict constraint. It's here so that renderers aren't forced to (partially) double-buffer the VDOM, even if the current "default" renderer `lignin-dom` does so. -->
///
/// Please refer to the variant documentation for more information.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum DomRef<T> {
	/// When constructing the DOM, this variant is passed **after** all child elements have been processed and, if applicable, the element has been added to the document tree.
	///
	/// In particular, this means:
	///
	/// - Manipulating child elements is possible (but this can cause panics to occur later on if an incompatible child diff occurs).
	/// - Traversing ancestors and their attributes should work.
	/// - Scrolling the node into view and grabbing focus should work.
	/// - **Any siblings and ancestor siblings may be in an indeterminate state at this point!**
	Added(T),
	/// When tearing down the DOM, this variant is passed **before** any child elements are processed and, if applicable, the element is removed from to the document tree.
	///
	/// In particular, this means:
	///
	/// - **Child elements must be restored to a clean state compatible with their VDOM here!**
	/// - Traversing ancestors and their attributes should still work.
	/// - **Any siblings and ancestor siblings may be in an indeterminate state at this point!**
	Removing(T),
}
impl<T> Sealed for DomRef<T> {}

macro_rules! web_types {
	{$(
		$(#[$($attrs:tt)*])*
		($container:ident, $container_str:literal) => $contents:ty
	),*$(,)?} => {$(
		// It's unfortunately not possible to puzzle the first line together like below, since it ends up cut off in the overview.
		$(#[$($attrs)*])*
		///
		/// Use [`Materialize::materialize`] to convert it to the actual value.
		#[cfg_attr(feature = "callbacks", repr(transparent))]
		#[derive(Debug, Clone)]
		pub struct $container(
			#[cfg(feature = "callbacks")] $contents,
			#[cfg(not(feature = "callbacks"))] FeatureNeeded,
		);
		impl Sealed for $container {}
		impl<'a> Sealed for &'a $container {}
		impl $container {
			/// Creates a new [`
			#[doc = $container_str]
			/// `] instance. The `"callbacks"` feature is required to use this function.
			#[cfg_attr(
				not(feature = "callbacks"),
				deprecated = "The `\"callbacks\"` feature is required to use this function."
			)]
			#[inline(always)]
			#[must_use]
			pub fn new(
				#[cfg(feature = "callbacks")] value: $contents,
				#[cfg(not(feature = "callbacks"))] value: FeatureNeeded,
			) -> Self {
				Self(value)
			}
		}
	)?};
}

web_types! {
	/// Erasable stand-in for [`web_sys::Comment`](https://docs.rs/web-sys/0.3/web_sys/struct.Comment.html) used as callback parameter.
	(Comment, "Comment") => web_sys::Comment,

	/// Erasable stand-in for [`web_sys::Element`](https://docs.rs/web-sys/0.3/web_sys/struct.Element.html) used as callback parameter.
	(Element, "Element") => web_sys::Element,

	/// Erasable stand-in for [`web_sys::Event`](https://docs.rs/web-sys/0.3/web_sys/struct.Event.html) used as callback parameter.
	(Event, "Event") => web_sys::Event,

	/// Erasable stand-in for [`web_sys::HtmlElement`](https://docs.rs/web-sys/0.3/web_sys/struct.HtmlElement.html) used as callback parameter.
	(HtmlElement, "HtmlElement") => web_sys::HtmlElement,

	/// Erasable stand-in for [`web_sys::SvgElement`](https://docs.rs/web-sys/0.3/web_sys/struct.SvgElement.html) used as callback parameter.
	(SvgElement, "HtmlElement") => web_sys::SvgElement,

	/// Erasable stand-in for [`web_sys::Text`](https://docs.rs/web-sys/0.3/web_sys/struct.Text.html) used as callback parameter.
	(Text, "Text") => web_sys::Text,
}

macro_rules! conversions {
	{$(
		$container:ty => $contents:ty
	),*$(,)?} => {$(
		#[cfg(feature = "callbacks")]
		impl Materialize<$contents> for $container {
			#[inline(always)] // No-op.
			fn materialize(self) -> $contents {
				self.0
			}
		}

		#[cfg(feature = "callbacks")]
		impl<'a> Materialize<&'a $contents> for &'a $container {
			#[inline(always)] // No-op.
			fn materialize(self) -> &'a $contents {
				unsafe {&*(self as *const $container).cast() }
			}
		}

		#[cfg(not(feature = "callbacks"))]
		impl<AnyType> Materialize<AnyType> for $container {
			#[inline(always)]
			fn materialize(self) -> AnyType {
				unreachable!()
			}
		}

		#[cfg(not(feature = "callbacks"))]
		impl<'a, AnyType> Materialize<&'a AnyType> for &'a $container {
			#[inline(always)]
			fn materialize(self) -> &'a AnyType {
				unreachable!()
			}
		}

		#[cfg(feature = "callbacks")]
		impl From<$contents> for $container {
			#[inline(always)] // No-op.
			fn from(contents: $contents) -> Self {
				Self(contents)
			}
		}

		#[cfg(feature = "callbacks")]
		impl<'a> From<&'a $contents> for &'a $container {
			#[inline(always)] // No-op.
			fn from(contents: &'a $contents) -> Self {
				unsafe {
					&*(contents as *const $contents).cast()
				}
			}
		}
	)*};
}

impl<T: Materialize<U>, U> Materialize<DomRef<U>> for DomRef<T> {
	#[inline(always)]
	fn materialize(self) -> DomRef<U> {
		match self {
			Self::Added(added) => DomRef::Added(added.materialize()),
			Self::Removing(removing) => DomRef::Removing(removing.materialize()),
		}
	}
}

conversions! {
	Comment => web_sys::Comment,
	Element => web_sys::Element,
	Event => web_sys::Event,
	HtmlElement => web_sys::HtmlElement,
	SvgElement => web_sys::SvgElement,
	Text => web_sys::Text,
}

/// Empty. Replaces erasable values in this module if the `"callbacks"` feature is not active.
#[doc(hidden)]
#[allow(clippy::empty_enum)]
#[derive(Debug, Clone)]
pub enum FeatureNeeded {}
impl FeatureNeeded {
	#[allow(dead_code)]
	fn map<T, U>(self, _: T) -> Option<U> {
		let _ = self;
		unreachable!()
	}
}

/// Convert a DOM stand-in to its web type value. This is a no-op with the `"callbacks"` feature and unreachable otherwise.
///
/// The extra trait is necessary because `Into` conflicts on `T: From<T>` and `Option<T>: From<T>`.
///
/// **Warning**:
///
/// Without the `"callbacks"` feature, the stand-ins in this module implement [`Materialize`] for any target type!  
/// Make sure to check if your package compiles with this feature enables, most easily by requiring it in the `[dev-dependencies]` section of your *Cargo.toml*.
pub trait Materialize<T: Sized>: Sized + Sealed {
	/// Convert a DOM stand-in to its web type value. This is a no-op with the `"callbacks"` feature and unreachable otherwise.
	fn materialize(self) -> T;
}