2021-02-03 07:26:04 +00:00
|
|
|
//! Virtual Node Support
|
|
|
|
//! VNodes represent lazily-constructed VDom trees that support diffing and event handlers.
|
|
|
|
//!
|
|
|
|
//! These VNodes should be *very* cheap and *very* fast to construct - building a full tree should be insanely quick.
|
|
|
|
|
2021-03-11 00:42:10 +00:00
|
|
|
use crate::{
|
|
|
|
events::VirtualEvent,
|
2021-05-28 16:56:21 +00:00
|
|
|
innerlude::{Context, Properties, ScopeIdx, FC},
|
2021-03-11 00:42:10 +00:00
|
|
|
};
|
2021-02-03 07:26:04 +00:00
|
|
|
use bumpalo::Bump;
|
2021-05-28 16:56:21 +00:00
|
|
|
use std::{cell::RefCell, fmt::Debug, marker::PhantomData, rc::Rc};
|
2021-02-03 07:26:04 +00:00
|
|
|
|
2021-02-21 02:59:16 +00:00
|
|
|
/// A domtree represents the result of "Viewing" the context
|
|
|
|
/// It's a placeholder over vnodes, to make working with lifetimes easier
|
2021-03-18 22:54:26 +00:00
|
|
|
pub struct DomTree {
|
|
|
|
// this should *never* be publicly accessible to external
|
|
|
|
pub(crate) root: VNode<'static>,
|
|
|
|
}
|
2021-02-21 02:59:16 +00:00
|
|
|
|
2021-02-03 07:26:04 +00:00
|
|
|
/// Tools for the base unit of the virtual dom - the VNode
|
|
|
|
/// VNodes are intended to be quickly-allocated, lightweight enum values.
|
|
|
|
///
|
|
|
|
/// Components will be generating a lot of these very quickly, so we want to
|
|
|
|
/// limit the amount of heap allocations / overly large enum sizes.
|
2021-03-11 00:42:10 +00:00
|
|
|
pub enum VNode<'src> {
|
|
|
|
/// An element node (node type `ELEMENT_NODE`).
|
|
|
|
Element(&'src VElement<'src>),
|
2021-02-03 07:26:04 +00:00
|
|
|
|
2021-03-11 00:42:10 +00:00
|
|
|
/// A text node (node type `TEXT_NODE`).
|
|
|
|
Text(VText<'src>),
|
|
|
|
|
|
|
|
/// A "suspended component"
|
|
|
|
/// This is a masqeurade over an underlying future that needs to complete
|
|
|
|
/// When the future is completed, the VNode will then trigger a render
|
|
|
|
Suspended,
|
|
|
|
|
|
|
|
/// A User-defined componen node (node type COMPONENT_NODE)
|
|
|
|
Component(VComponent<'src>),
|
|
|
|
}
|
2021-02-03 07:26:04 +00:00
|
|
|
|
2021-03-11 00:42:10 +00:00
|
|
|
impl<'a> VNode<'a> {
|
|
|
|
/// Low-level constructor for making a new `Node` of type element with given
|
|
|
|
/// parts.
|
|
|
|
///
|
|
|
|
/// This is primarily intended for JSX and templating proc-macros to compile
|
|
|
|
/// down into. If you are building nodes by-hand, prefer using the
|
|
|
|
/// `dodrio::builder::*` APIs.
|
|
|
|
#[inline]
|
|
|
|
pub fn element(
|
|
|
|
bump: &'a Bump,
|
2021-04-01 04:01:42 +00:00
|
|
|
key: NodeKey<'a>,
|
2021-03-11 00:42:10 +00:00
|
|
|
tag_name: &'a str,
|
|
|
|
listeners: &'a [Listener<'a>],
|
|
|
|
attributes: &'a [Attribute<'a>],
|
|
|
|
children: &'a [VNode<'a>],
|
|
|
|
namespace: Option<&'a str>,
|
|
|
|
) -> VNode<'a> {
|
|
|
|
let element = bump.alloc_with(|| VElement {
|
|
|
|
key,
|
|
|
|
tag_name,
|
|
|
|
listeners,
|
|
|
|
attributes,
|
|
|
|
children,
|
|
|
|
namespace,
|
|
|
|
});
|
|
|
|
VNode::Element(element)
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Construct a new text node with the given text.
|
|
|
|
#[inline]
|
|
|
|
pub fn text(text: &'a str) -> VNode<'a> {
|
|
|
|
VNode::Text(VText { text })
|
|
|
|
}
|
2021-02-03 07:26:04 +00:00
|
|
|
|
2021-03-11 00:42:10 +00:00
|
|
|
#[inline]
|
|
|
|
pub(crate) fn key(&self) -> NodeKey {
|
|
|
|
match &self {
|
|
|
|
VNode::Text(_) => NodeKey::NONE,
|
|
|
|
VNode::Element(e) => e.key,
|
|
|
|
VNode::Suspended => {
|
|
|
|
todo!()
|
|
|
|
}
|
2021-03-18 22:54:26 +00:00
|
|
|
VNode::Component(c) => c.key,
|
2021-02-08 00:14:04 +00:00
|
|
|
}
|
2021-02-03 07:26:04 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-03-11 00:42:10 +00:00
|
|
|
// ========================================================
|
|
|
|
// VElement (div, h1, etc), attrs, keys, listener handle
|
|
|
|
// ========================================================
|
|
|
|
pub struct VElement<'a> {
|
|
|
|
/// Elements have a tag name, zero or more attributes, and zero or more
|
2021-04-01 04:01:42 +00:00
|
|
|
pub key: NodeKey<'a>,
|
2021-03-11 00:42:10 +00:00
|
|
|
pub tag_name: &'a str,
|
|
|
|
pub listeners: &'a [Listener<'a>],
|
|
|
|
pub attributes: &'a [Attribute<'a>],
|
|
|
|
pub children: &'a [VNode<'a>],
|
|
|
|
pub namespace: Option<&'a str>,
|
|
|
|
}
|
2021-02-03 07:26:04 +00:00
|
|
|
|
2021-03-11 00:42:10 +00:00
|
|
|
/// An attribute on a DOM node, such as `id="my-thing"` or
|
|
|
|
/// `href="https://example.com"`.
|
2021-03-12 21:58:30 +00:00
|
|
|
#[derive(Clone, Debug)]
|
2021-03-11 00:42:10 +00:00
|
|
|
pub struct Attribute<'a> {
|
|
|
|
pub name: &'static str,
|
|
|
|
pub value: &'a str,
|
|
|
|
}
|
2021-02-03 07:26:04 +00:00
|
|
|
|
2021-03-11 00:42:10 +00:00
|
|
|
impl<'a> Attribute<'a> {
|
|
|
|
/// Get this attribute's name, such as `"id"` in `<div id="my-thing" />`.
|
|
|
|
#[inline]
|
|
|
|
pub fn name(&self) -> &'a str {
|
|
|
|
self.name
|
|
|
|
}
|
2021-02-03 07:26:04 +00:00
|
|
|
|
2021-03-11 00:42:10 +00:00
|
|
|
/// The attribute value, such as `"my-thing"` in `<div id="my-thing" />`.
|
|
|
|
#[inline]
|
|
|
|
pub fn value(&self) -> &'a str {
|
|
|
|
self.value
|
|
|
|
}
|
2021-02-03 07:26:04 +00:00
|
|
|
|
2021-03-11 00:42:10 +00:00
|
|
|
/// Certain attributes are considered "volatile" and can change via user
|
|
|
|
/// input that we can't see when diffing against the old virtual DOM. For
|
|
|
|
/// these attributes, we want to always re-set the attribute on the physical
|
|
|
|
/// DOM node, even if the old and new virtual DOM nodes have the same value.
|
|
|
|
#[inline]
|
|
|
|
pub(crate) fn is_volatile(&self) -> bool {
|
|
|
|
match self.name {
|
|
|
|
"value" | "checked" | "selected" => true,
|
|
|
|
_ => false,
|
2021-02-03 07:26:04 +00:00
|
|
|
}
|
|
|
|
}
|
2021-03-11 00:42:10 +00:00
|
|
|
}
|
2021-02-03 07:26:04 +00:00
|
|
|
|
2021-03-11 00:42:10 +00:00
|
|
|
pub struct ListenerHandle {
|
|
|
|
pub event: &'static str,
|
|
|
|
pub scope: ScopeIdx,
|
|
|
|
pub id: usize,
|
|
|
|
}
|
2021-03-03 07:27:26 +00:00
|
|
|
|
2021-03-11 00:42:10 +00:00
|
|
|
/// An event listener.
|
|
|
|
pub struct Listener<'bump> {
|
|
|
|
/// The type of event to listen for.
|
|
|
|
pub(crate) event: &'static str,
|
2021-02-15 19:14:28 +00:00
|
|
|
|
2021-03-11 00:42:10 +00:00
|
|
|
pub scope: ScopeIdx,
|
|
|
|
pub id: usize,
|
2021-02-15 19:14:28 +00:00
|
|
|
|
2021-05-28 16:56:21 +00:00
|
|
|
/// The callback to invoke when the event happens.
|
2021-03-11 00:42:10 +00:00
|
|
|
pub(crate) callback: &'bump (dyn Fn(VirtualEvent)),
|
|
|
|
}
|
2021-02-03 07:26:04 +00:00
|
|
|
|
2021-03-11 00:42:10 +00:00
|
|
|
/// The key for keyed children.
|
|
|
|
///
|
|
|
|
/// Keys must be unique among siblings.
|
|
|
|
///
|
|
|
|
/// If any sibling is keyed, then they all must be keyed.
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
2021-04-01 04:01:42 +00:00
|
|
|
pub struct NodeKey<'a>(pub(crate) Option<&'a str>);
|
2021-02-03 07:26:04 +00:00
|
|
|
|
2021-04-01 04:01:42 +00:00
|
|
|
impl<'a> Default for NodeKey<'a> {
|
|
|
|
fn default() -> NodeKey<'a> {
|
2021-03-11 00:42:10 +00:00
|
|
|
NodeKey::NONE
|
|
|
|
}
|
|
|
|
}
|
2021-04-01 04:01:42 +00:00
|
|
|
impl<'a> NodeKey<'a> {
|
2021-03-11 00:42:10 +00:00
|
|
|
/// The default, lack of a key.
|
2021-04-01 04:01:42 +00:00
|
|
|
pub const NONE: NodeKey<'a> = NodeKey(None);
|
2021-03-11 00:42:10 +00:00
|
|
|
|
|
|
|
/// Is this key `NodeKey::NONE`?
|
|
|
|
#[inline]
|
|
|
|
pub fn is_none(&self) -> bool {
|
|
|
|
*self == Self::NONE
|
2021-02-03 07:26:04 +00:00
|
|
|
}
|
|
|
|
|
2021-03-11 00:42:10 +00:00
|
|
|
/// Is this key not `NodeKey::NONE`?
|
|
|
|
#[inline]
|
|
|
|
pub fn is_some(&self) -> bool {
|
|
|
|
!self.is_none()
|
2021-02-03 07:26:04 +00:00
|
|
|
}
|
|
|
|
|
2021-03-11 00:42:10 +00:00
|
|
|
/// Create a new `NodeKey`.
|
|
|
|
///
|
|
|
|
/// `key` must not be `u32::MAX`.
|
|
|
|
#[inline]
|
2021-04-01 04:01:42 +00:00
|
|
|
pub fn new(key: &'a str) -> Self {
|
|
|
|
NodeKey(Some(key))
|
2021-03-11 00:42:10 +00:00
|
|
|
}
|
2021-02-03 07:26:04 +00:00
|
|
|
}
|
|
|
|
|
2021-03-12 21:58:30 +00:00
|
|
|
#[derive(Debug, PartialEq)]
|
2021-03-11 00:42:10 +00:00
|
|
|
pub struct VText<'bump> {
|
|
|
|
pub text: &'bump str,
|
|
|
|
}
|
2021-02-03 07:26:04 +00:00
|
|
|
|
2021-03-11 00:42:10 +00:00
|
|
|
impl<'a> VText<'a> {
|
|
|
|
// / Create an new `VText` instance with the specified text.
|
2021-04-01 04:01:42 +00:00
|
|
|
pub fn new(text: &'a str) -> Self {
|
2021-03-11 00:42:10 +00:00
|
|
|
VText { text: text.into() }
|
2021-02-03 07:26:04 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-03-11 00:42:10 +00:00
|
|
|
// ==============================
|
|
|
|
// Custom components
|
|
|
|
// ==============================
|
|
|
|
|
2021-02-03 07:26:04 +00:00
|
|
|
/// Virtual Components for custom user-defined components
|
|
|
|
/// Only supports the functional syntax
|
2021-03-14 00:11:06 +00:00
|
|
|
pub type StableScopeAddres = RefCell<Option<u32>>;
|
|
|
|
pub type VCompAssociatedScope = RefCell<Option<ScopeIdx>>;
|
2021-03-11 00:42:10 +00:00
|
|
|
|
|
|
|
pub struct VComponent<'src> {
|
2021-04-01 04:01:42 +00:00
|
|
|
pub key: NodeKey<'src>,
|
2021-03-14 00:11:06 +00:00
|
|
|
|
|
|
|
pub stable_addr: Rc<StableScopeAddres>,
|
|
|
|
pub ass_scope: Rc<VCompAssociatedScope>,
|
2021-03-12 19:27:32 +00:00
|
|
|
|
|
|
|
pub comparator: Rc<dyn Fn(&VComponent) -> bool + 'src>,
|
2021-03-18 22:54:26 +00:00
|
|
|
pub caller: Rc<dyn Fn(Context) -> DomTree + 'src>,
|
2021-03-12 19:27:32 +00:00
|
|
|
|
|
|
|
// a pointer into the bump arena (given by the 'src lifetime)
|
|
|
|
raw_props: *const (),
|
|
|
|
|
|
|
|
// a pointer to the raw fn typ
|
2021-03-14 00:11:06 +00:00
|
|
|
pub user_fc: *const (),
|
2021-03-11 00:42:10 +00:00
|
|
|
_p: PhantomData<&'src ()>,
|
|
|
|
}
|
2021-02-12 05:29:46 +00:00
|
|
|
|
2021-03-11 00:42:10 +00:00
|
|
|
impl<'a> VComponent<'a> {
|
|
|
|
// use the type parameter on props creation and move it into a portable context
|
|
|
|
// this lets us keep scope generic *and* downcast its props when we need to:
|
|
|
|
// - perform comparisons when diffing (memoization)
|
|
|
|
// TODO: lift the requirement that props need to be static
|
|
|
|
// we want them to borrow references... maybe force implementing a "to_static_unsafe" trait
|
2021-04-01 04:01:42 +00:00
|
|
|
pub fn new<P: Properties + 'a>(component: FC<P>, props: &'a P, key: Option<&'a str>) -> Self {
|
2021-03-12 19:27:32 +00:00
|
|
|
let caller_ref = component as *const ();
|
|
|
|
|
|
|
|
let raw_props = props as *const P as *const ();
|
|
|
|
|
|
|
|
let props_comparator = move |other: &VComponent| {
|
|
|
|
// Safety:
|
|
|
|
// We are guaranteed that the props will be of the same type because
|
|
|
|
// there is no way to create a VComponent other than this `new` method.
|
|
|
|
//
|
|
|
|
// Therefore, if the render functions are identical (by address), then so will be
|
|
|
|
// props type paramter (because it is the same render function). Therefore, we can be
|
|
|
|
// sure
|
2021-03-14 00:11:06 +00:00
|
|
|
if caller_ref == other.user_fc {
|
2021-03-12 19:27:32 +00:00
|
|
|
let real_other = unsafe { &*(other.raw_props as *const _ as *const P) };
|
|
|
|
real_other == props
|
|
|
|
} else {
|
|
|
|
false
|
|
|
|
}
|
|
|
|
};
|
2021-03-11 00:42:10 +00:00
|
|
|
|
2021-03-18 22:54:26 +00:00
|
|
|
let caller = Rc::new(create_closure(component, raw_props));
|
2021-03-11 00:42:10 +00:00
|
|
|
|
2021-04-01 04:01:42 +00:00
|
|
|
let key = match key {
|
|
|
|
Some(key) => NodeKey::new(key),
|
|
|
|
None => NodeKey(None),
|
|
|
|
};
|
|
|
|
|
2021-03-12 19:27:32 +00:00
|
|
|
Self {
|
2021-04-01 04:01:42 +00:00
|
|
|
key,
|
2021-03-14 00:11:06 +00:00
|
|
|
ass_scope: Rc::new(RefCell::new(None)),
|
|
|
|
user_fc: caller_ref,
|
2021-03-12 19:27:32 +00:00
|
|
|
raw_props: props as *const P as *const _,
|
|
|
|
_p: PhantomData,
|
2021-03-18 22:54:26 +00:00
|
|
|
caller,
|
2021-03-12 19:27:32 +00:00
|
|
|
comparator: Rc::new(props_comparator),
|
|
|
|
stable_addr: Rc::new(RefCell::new(None)),
|
|
|
|
}
|
2021-02-03 07:26:04 +00:00
|
|
|
}
|
|
|
|
}
|
2021-03-18 22:54:26 +00:00
|
|
|
|
|
|
|
fn create_closure<'a, P: Properties + 'a>(
|
|
|
|
component: FC<P>,
|
|
|
|
raw_props: *const (),
|
|
|
|
) -> impl for<'r> Fn(Context<'r>) -> DomTree + 'a {
|
|
|
|
move |ctx: Context| -> DomTree {
|
|
|
|
// cast back into the right lifetime
|
|
|
|
let safe_props: &'a P = unsafe { &*(raw_props as *const P) };
|
|
|
|
component(ctx, safe_props)
|
|
|
|
}
|
|
|
|
}
|