use std::{any::Any, marker::PhantomData, rc::Rc};
use crate::{
RenderTargetId,
diff::{CreatedVNode, context::DiffContext},
innerlude::*,
mutations::append_children_to_with_result,
render_driver::RenderDriver,
};
#[derive(Clone, PartialEq)]
#[allow(non_camel_case_types)]
pub struct PortalProps {
target: RenderTargetId,
children: LastRenderedNode,
}
impl Properties for PortalProps {
type ComponentBuilder<RenderFn, Marker> = PortalComponentBuilder<RenderFn, Marker, ((), ())>;
fn component_builder<RenderFn, Marker>(
render_fn: RenderFn,
) -> Self::ComponentBuilder<RenderFn, Marker> {
PortalComponentBuilder {
render_fn,
builder: PortalPropsBuilder {
fields: ((), ()),
_phantom: (),
},
_marker: PhantomData,
}
}
fn into_vcomponent<M: 'static>(self, render_fn: impl ComponentFunction<Self, M>) -> VComponent {
let type_name = std::any::type_name_of_val(&render_fn);
let render_fn_ptr = render_fn.fn_ptr();
let props = Box::new(VProps::new(
render_fn,
<Self as Properties>::memoize,
self,
type_name,
));
VComponent::new_with_driver(type_name, render_fn_ptr, Rc::new(PortalDriver), props)
}
fn memoize(&mut self, new: &Self) -> bool {
let equal = self == new;
self.target = new.target;
self.children = new.children.clone();
equal
}
}
impl<RenderFn, Marker> ComponentBuilderRender<RenderFn, Marker> for PortalProps
where
RenderFn: ComponentFunction<PortalProps, Marker>,
Marker: 'static,
{
fn into_vcomponent(self, render_fn: RenderFn) -> VComponent {
<Self as Properties>::into_vcomponent(self, render_fn)
}
}
#[must_use]
#[allow(dead_code, non_camel_case_types, non_snake_case)]
pub struct PortalPropsBuilder<TypedBuilderFields> {
fields: TypedBuilderFields,
_phantom: (),
}
#[allow(dead_code, non_camel_case_types, missing_docs)]
impl<__children> PortalPropsBuilder<((), __children)> {
#[allow(clippy::type_complexity)]
pub fn target(
self,
target: RenderTargetId,
) -> PortalPropsBuilder<((RenderTargetId,), __children)> {
let (_, children) = self.fields;
PortalPropsBuilder {
fields: ((target,), children),
_phantom: self._phantom,
}
}
}
#[allow(dead_code, non_camel_case_types, missing_docs)]
impl<__target> PortalPropsBuilder<(__target, ())> {
#[allow(clippy::type_complexity)]
pub fn children(self, children: Element) -> PortalPropsBuilder<(__target, (Element,))> {
let (target, _) = self.fields;
PortalPropsBuilder {
fields: (target, (children,)),
_phantom: self._phantom,
}
}
}
#[allow(dead_code, non_camel_case_types, missing_docs)]
impl PortalPropsBuilder<((RenderTargetId,), (Element,))> {
pub fn build(self) -> PortalProps {
let (target, children) = self.fields;
PortalProps {
target: target.0,
children: LastRenderedNode::new(children.0),
}
}
}
#[must_use]
#[allow(dead_code, non_camel_case_types, non_snake_case)]
pub struct PortalComponentBuilder<RenderFn, ComponentMarker, TypedBuilderFields> {
render_fn: RenderFn,
builder: PortalPropsBuilder<TypedBuilderFields>,
_marker: PhantomData<ComponentMarker>,
}
#[allow(dead_code, non_camel_case_types, missing_docs)]
impl<RenderFn, ComponentMarker, __children>
PortalComponentBuilder<RenderFn, ComponentMarker, ((), __children)>
{
#[allow(clippy::type_complexity)]
pub fn target(
self,
target: RenderTargetId,
) -> PortalComponentBuilder<RenderFn, ComponentMarker, ((RenderTargetId,), __children)> {
PortalComponentBuilder {
render_fn: self.render_fn,
builder: self.builder.target(target),
_marker: self._marker,
}
}
}
#[allow(dead_code, non_camel_case_types, missing_docs)]
impl<RenderFn, ComponentMarker, __target>
PortalComponentBuilder<RenderFn, ComponentMarker, (__target, ())>
{
#[allow(clippy::type_complexity)]
pub fn children(
self,
children: Element,
) -> PortalComponentBuilder<RenderFn, ComponentMarker, (__target, (Element,))> {
PortalComponentBuilder {
render_fn: self.render_fn,
builder: self.builder.children(children),
_marker: self._marker,
}
}
}
#[allow(dead_code, non_camel_case_types, missing_docs)]
impl<RenderFn, ComponentMarker>
PortalComponentBuilder<RenderFn, ComponentMarker, ((RenderTargetId,), (Element,))>
{
pub fn build(self) -> ComponentBuilderOutput<RenderFn, PortalProps, ComponentMarker> {
ComponentBuilderOutput::new(self.render_fn, self.builder.build())
}
}
#[allow(non_snake_case)]
#[cfg_attr(coverage_nightly, coverage(off))]
pub fn Portal(_: PortalProps) -> Element {
unreachable!("Portal should not be called directly")
}
struct PortalDriver;
fn portal_props(dom: &VirtualDom, scope_id: ScopeId) -> (RenderTargetId, LastRenderedNode) {
let props = dom.scopes[scope_id.index()]
.props
.props()
.downcast_ref::<PortalProps>()
.expect("portal scope carries PortalProps");
(props.target, props.children.clone())
}
fn place_children(
scope_id: ScopeId,
target_id: RenderTargetId,
children: LastRenderedNode,
old_root_mount: Option<MountId>,
parent: Option<MountId>,
dom: &mut VirtualDom,
render_to: Option<&mut (dyn WriteMutations + '_)>,
) -> MountId {
debug_assert_eq!(
dom.runtime.current_render_target_id(),
target_id,
"portal (re)mount runs inside the portal scope, whose target_id routes its writes"
);
let should_mount = render_to.is_some();
let root_mount = if let Some(to) = render_to {
append_children_to_with_result(to, ElementId::ROOT, dom.runtime.clone(), |to| {
let created = place_children_inner(dom, &children, old_root_mount, parent, Some(to));
(created.nodes, created.mount)
})
} else {
place_children_inner(dom, &children, old_root_mount, parent, None).mount
};
dom.scopes[scope_id.index()].last_rendered_node =
Some(MountedOutput::new(children, root_mount));
dom.runtime
.get_state(scope_id)
.set_root_mount(Some(root_mount));
if should_mount {
dom.runtime.get_state(scope_id).mount(&dom.runtime);
}
root_mount
}
fn place_children_inner(
dom: &mut VirtualDom,
children: &LastRenderedNode,
old_root_mount: Option<MountId>,
parent: Option<MountId>,
to: Option<&mut (dyn WriteMutations + '_)>,
) -> CreatedVNode {
children
.as_vnode()
.create_or_reuse_mount(dom, old_root_mount, parent, parent, to)
}
impl RenderDriver for PortalDriver {
fn as_any(&self) -> &dyn Any {
self
}
fn create(
&self,
dom: &mut VirtualDom,
scope_id: ScopeId,
parent: Option<MountId>,
to: Option<&mut (dyn WriteMutations + '_)>,
) -> usize {
if let Some(old_output) = dom.scopes[scope_id.index()].last_rendered_node.clone() {
let target_id = dom.runtime.get_state(scope_id).target_id();
let root_mount = old_output.root_mount();
let children = old_output.node().clone();
return dom.runtime.clone().with_scope_on_stack(scope_id, || {
place_children(
scope_id,
target_id,
children,
Some(root_mount),
parent,
dom,
to,
);
0
});
}
let (target_id, children) = portal_props(dom, scope_id);
dom.runtime.set_scope_target_id(scope_id, target_id);
dom.runtime.clone().with_scope_on_stack(scope_id, || {
place_children(scope_id, target_id, children, None, parent, dom, to);
0
})
}
fn diff(
&self,
dom: &mut VirtualDom,
scope_id: ScopeId,
_: Option<DiffContext<'_>>,
mut to: Option<&mut (dyn WriteMutations + '_)>,
) {
let (target_id, new_children) = portal_props(dom, scope_id);
dom.runtime.clone().with_scope_on_stack(scope_id, || {
let old_children = dom.scopes[scope_id.index()]
.last_rendered_node
.take()
.unwrap();
let old_target_id = dom.runtime.get_state(scope_id).target_id();
if old_target_id != target_id {
let old_mount = old_children.root_mount();
let logical_parent = dom.mounted_logical_parent(old_mount);
let render_parent = dom
.mounted_render_parent(old_mount)
.expect("portal root should have a render parent before retarget");
old_children
.as_vnode()
.remove_node_inner(old_mount, dom, to.as_deref_mut(), true);
dom.runtime.set_scope_target_id(scope_id, target_id);
let root_mount = place_children(
scope_id,
target_id,
new_children,
None,
logical_parent,
dom,
to.as_deref_mut(),
);
dom.set_mounted_render_parent(root_mount, render_parent);
return;
}
let old_mount = old_children.root_mount();
let old_render_parent = dom
.mounted_render_parent(old_mount)
.expect("portal root should have a render parent before diff");
let mut render_to = to.filter(|_| dom.runtime.scope_should_render(scope_id));
let new_mount = old_children.mounted_vnode().diff_node(
new_children.as_vnode(),
dom,
render_to.as_deref_mut(),
);
dom.replace_mounted_component_root_mount(old_mount, new_mount, old_render_parent);
dom.scopes[scope_id.index()].last_rendered_node =
Some(MountedOutput::new(new_children, new_mount));
dom.runtime
.get_state(scope_id)
.set_root_mount(Some(new_mount));
if render_to.is_some() {
dom.runtime.get_state(scope_id).mount(&dom.runtime);
}
})
}
fn remove(
&self,
dom: &mut VirtualDom,
scope_id: ScopeId,
render_to: Option<&mut (dyn WriteMutations + '_)>,
destroy_component_state: bool,
) {
dom.runtime.clone().with_scope_on_stack(scope_id, || {
let node = dom.scopes[scope_id.index()]
.last_rendered_node
.as_ref()
.cloned()
.expect("portal scope must have rendered before remove");
node.as_vnode().remove_node_inner(
node.root_mount(),
dom,
render_to,
destroy_component_state,
);
});
if destroy_component_state {
dom.drop_scope(scope_id);
}
}
}