1use std::{any::Any, marker::PhantomData, rc::Rc};
2
3use crate::{
4 RenderTargetId,
5 diff::{CreatedVNode, context::DiffContext},
6 innerlude::*,
7 mutations::append_children_to_with_result,
8 render_driver::RenderDriver,
9};
10
11#[derive(Clone, PartialEq)]
13#[allow(non_camel_case_types)]
14pub struct PortalProps {
15 target: RenderTargetId,
16 children: LastRenderedNode,
18}
19
20impl Properties for PortalProps {
21 type ComponentBuilder<RenderFn, Marker> = PortalComponentBuilder<RenderFn, Marker, ((), ())>;
22
23 fn component_builder<RenderFn, Marker>(
24 render_fn: RenderFn,
25 ) -> Self::ComponentBuilder<RenderFn, Marker> {
26 PortalComponentBuilder {
27 render_fn,
28 builder: PortalPropsBuilder {
29 fields: ((), ()),
30 _phantom: (),
31 },
32 _marker: PhantomData,
33 }
34 }
35
36 fn into_vcomponent<M: 'static>(self, render_fn: impl ComponentFunction<Self, M>) -> VComponent {
37 let type_name = std::any::type_name_of_val(&render_fn);
38 let render_fn_ptr = render_fn.fn_ptr();
39 let props = Box::new(VProps::new(
40 render_fn,
41 <Self as Properties>::memoize,
42 self,
43 type_name,
44 ));
45 VComponent::new_with_driver(type_name, render_fn_ptr, Rc::new(PortalDriver), props)
46 }
47
48 fn memoize(&mut self, new: &Self) -> bool {
49 let equal = self == new;
56 self.target = new.target;
57 self.children = new.children.clone();
58 equal
59 }
60}
61
62impl<RenderFn, Marker> ComponentBuilderRender<RenderFn, Marker> for PortalProps
63where
64 RenderFn: ComponentFunction<PortalProps, Marker>,
65 Marker: 'static,
66{
67 fn into_vcomponent(self, render_fn: RenderFn) -> VComponent {
68 <Self as Properties>::into_vcomponent(self, render_fn)
69 }
70}
71
72#[must_use]
73#[allow(dead_code, non_camel_case_types, non_snake_case)]
74pub struct PortalPropsBuilder<TypedBuilderFields> {
75 fields: TypedBuilderFields,
76 _phantom: (),
77}
78
79#[allow(dead_code, non_camel_case_types, missing_docs)]
80impl<__children> PortalPropsBuilder<((), __children)> {
81 #[allow(clippy::type_complexity)]
82 pub fn target(
83 self,
84 target: RenderTargetId,
85 ) -> PortalPropsBuilder<((RenderTargetId,), __children)> {
86 let (_, children) = self.fields;
87 PortalPropsBuilder {
88 fields: ((target,), children),
89 _phantom: self._phantom,
90 }
91 }
92}
93
94#[allow(dead_code, non_camel_case_types, missing_docs)]
95impl<__target> PortalPropsBuilder<(__target, ())> {
96 #[allow(clippy::type_complexity)]
97 pub fn children(self, children: Element) -> PortalPropsBuilder<(__target, (Element,))> {
98 let (target, _) = self.fields;
99 PortalPropsBuilder {
100 fields: (target, (children,)),
101 _phantom: self._phantom,
102 }
103 }
104}
105
106#[allow(dead_code, non_camel_case_types, missing_docs)]
107impl PortalPropsBuilder<((RenderTargetId,), (Element,))> {
108 pub fn build(self) -> PortalProps {
109 let (target, children) = self.fields;
110 PortalProps {
111 target: target.0,
112 children: LastRenderedNode::new(children.0),
113 }
114 }
115}
116
117#[must_use]
121#[allow(dead_code, non_camel_case_types, non_snake_case)]
122pub struct PortalComponentBuilder<RenderFn, ComponentMarker, TypedBuilderFields> {
123 render_fn: RenderFn,
124 builder: PortalPropsBuilder<TypedBuilderFields>,
125 _marker: PhantomData<ComponentMarker>,
126}
127
128#[allow(dead_code, non_camel_case_types, missing_docs)]
129impl<RenderFn, ComponentMarker, __children>
130 PortalComponentBuilder<RenderFn, ComponentMarker, ((), __children)>
131{
132 #[allow(clippy::type_complexity)]
133 pub fn target(
134 self,
135 target: RenderTargetId,
136 ) -> PortalComponentBuilder<RenderFn, ComponentMarker, ((RenderTargetId,), __children)> {
137 PortalComponentBuilder {
138 render_fn: self.render_fn,
139 builder: self.builder.target(target),
140 _marker: self._marker,
141 }
142 }
143}
144
145#[allow(dead_code, non_camel_case_types, missing_docs)]
146impl<RenderFn, ComponentMarker, __target>
147 PortalComponentBuilder<RenderFn, ComponentMarker, (__target, ())>
148{
149 #[allow(clippy::type_complexity)]
150 pub fn children(
151 self,
152 children: Element,
153 ) -> PortalComponentBuilder<RenderFn, ComponentMarker, (__target, (Element,))> {
154 PortalComponentBuilder {
155 render_fn: self.render_fn,
156 builder: self.builder.children(children),
157 _marker: self._marker,
158 }
159 }
160}
161
162#[allow(dead_code, non_camel_case_types, missing_docs)]
163impl<RenderFn, ComponentMarker>
164 PortalComponentBuilder<RenderFn, ComponentMarker, ((RenderTargetId,), (Element,))>
165{
166 pub fn build(self) -> ComponentBuilderOutput<RenderFn, PortalProps, ComponentMarker> {
167 ComponentBuilderOutput::new(self.render_fn, self.builder.build())
168 }
169}
170
171#[allow(non_snake_case)]
208#[cfg_attr(coverage_nightly, coverage(off))]
209pub fn Portal(_: PortalProps) -> Element {
210 unreachable!("Portal should not be called directly")
211}
212
213struct PortalDriver;
216
217fn portal_props(dom: &VirtualDom, scope_id: ScopeId) -> (RenderTargetId, LastRenderedNode) {
218 let props = dom.scopes[scope_id.index()]
219 .props
220 .props()
221 .downcast_ref::<PortalProps>()
222 .expect("portal scope carries PortalProps");
223 (props.target, props.children.clone())
224}
225
226fn place_children(
232 scope_id: ScopeId,
233 target_id: RenderTargetId,
234 children: LastRenderedNode,
235 old_root_mount: Option<MountId>,
236 parent: Option<MountId>,
237 dom: &mut VirtualDom,
238 render_to: Option<&mut (dyn WriteMutations + '_)>,
239) -> MountId {
240 debug_assert_eq!(
241 dom.runtime.current_render_target_id(),
242 target_id,
243 "portal (re)mount runs inside the portal scope, whose target_id routes its writes"
244 );
245 let should_mount = render_to.is_some();
246 let root_mount = if let Some(to) = render_to {
247 append_children_to_with_result(to, ElementId::ROOT, dom.runtime.clone(), |to| {
248 let created = place_children_inner(dom, &children, old_root_mount, parent, Some(to));
249 (created.nodes, created.mount)
250 })
251 } else {
252 place_children_inner(dom, &children, old_root_mount, parent, None).mount
253 };
254 dom.scopes[scope_id.index()].last_rendered_node =
255 Some(MountedOutput::new(children, root_mount));
256 dom.runtime
257 .get_state(scope_id)
258 .set_root_mount(Some(root_mount));
259 if should_mount {
260 dom.runtime.get_state(scope_id).mount(&dom.runtime);
261 }
262 root_mount
263}
264
265fn place_children_inner(
269 dom: &mut VirtualDom,
270 children: &LastRenderedNode,
271 old_root_mount: Option<MountId>,
272 parent: Option<MountId>,
273 to: Option<&mut (dyn WriteMutations + '_)>,
274) -> CreatedVNode {
275 children
276 .as_vnode()
277 .create_or_reuse_mount(dom, old_root_mount, parent, parent, to)
278}
279
280impl RenderDriver for PortalDriver {
281 fn as_any(&self) -> &dyn Any {
282 self
283 }
284
285 fn create(
286 &self,
287 dom: &mut VirtualDom,
288 scope_id: ScopeId,
289 parent: Option<MountId>,
290 to: Option<&mut (dyn WriteMutations + '_)>,
291 ) -> usize {
292 if let Some(old_output) = dom.scopes[scope_id.index()].last_rendered_node.clone() {
293 let target_id = dom.runtime.get_state(scope_id).target_id();
296 let root_mount = old_output.root_mount();
297 let children = old_output.node().clone();
298
299 return dom.runtime.clone().with_scope_on_stack(scope_id, || {
300 place_children(
301 scope_id,
302 target_id,
303 children,
304 Some(root_mount),
305 parent,
306 dom,
307 to,
308 );
309 0
310 });
311 }
312
313 let (target_id, children) = portal_props(dom, scope_id);
314 dom.runtime.set_scope_target_id(scope_id, target_id);
319
320 dom.runtime.clone().with_scope_on_stack(scope_id, || {
321 place_children(scope_id, target_id, children, None, parent, dom, to);
322 0
323 })
324 }
325
326 fn diff(
327 &self,
328 dom: &mut VirtualDom,
329 scope_id: ScopeId,
330 _: Option<DiffContext<'_>>,
331 mut to: Option<&mut (dyn WriteMutations + '_)>,
332 ) {
333 let (target_id, new_children) = portal_props(dom, scope_id);
334
335 dom.runtime.clone().with_scope_on_stack(scope_id, || {
336 let old_children = dom.scopes[scope_id.index()]
337 .last_rendered_node
338 .take()
339 .unwrap();
340 let old_target_id = dom.runtime.get_state(scope_id).target_id();
341
342 if old_target_id != target_id {
343 let old_mount = old_children.root_mount();
344 let logical_parent = dom.mounted_logical_parent(old_mount);
345 let render_parent = dom
346 .mounted_render_parent(old_mount)
347 .expect("portal root should have a render parent before retarget");
348
349 old_children
350 .as_vnode()
351 .remove_node_inner(old_mount, dom, to.as_deref_mut(), true);
352
353 dom.runtime.set_scope_target_id(scope_id, target_id);
358
359 let root_mount = place_children(
360 scope_id,
361 target_id,
362 new_children,
363 None,
364 logical_parent,
365 dom,
366 to.as_deref_mut(),
367 );
368 dom.set_mounted_render_parent(root_mount, render_parent);
369 return;
370 }
371
372 let old_mount = old_children.root_mount();
373 let old_render_parent = dom
374 .mounted_render_parent(old_mount)
375 .expect("portal root should have a render parent before diff");
376 let mut render_to = to.filter(|_| dom.runtime.scope_should_render(scope_id));
377 let new_mount = old_children.mounted_vnode().diff_node(
378 new_children.as_vnode(),
379 dom,
380 render_to.as_deref_mut(),
381 );
382 dom.replace_mounted_component_root_mount(old_mount, new_mount, old_render_parent);
383 dom.scopes[scope_id.index()].last_rendered_node =
384 Some(MountedOutput::new(new_children, new_mount));
385 dom.runtime
386 .get_state(scope_id)
387 .set_root_mount(Some(new_mount));
388 if render_to.is_some() {
389 dom.runtime.get_state(scope_id).mount(&dom.runtime);
390 }
391 })
392 }
393
394 fn remove(
395 &self,
396 dom: &mut VirtualDom,
397 scope_id: ScopeId,
398 render_to: Option<&mut (dyn WriteMutations + '_)>,
399 destroy_component_state: bool,
400 ) {
401 dom.runtime.clone().with_scope_on_stack(scope_id, || {
402 let node = dom.scopes[scope_id.index()]
406 .last_rendered_node
407 .as_ref()
408 .cloned()
409 .expect("portal scope must have rendered before remove");
410 node.as_vnode().remove_node_inner(
411 node.root_mount(),
412 dom,
413 render_to,
414 destroy_component_state,
415 );
416 });
417
418 if destroy_component_state {
419 dom.drop_scope(scope_id);
420 }
421 }
422}