Skip to main content

dioxus_core/
portal.rs

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/// Properties for the [`Portal`] component.
12#[derive(Clone, PartialEq)]
13#[allow(non_camel_case_types)]
14pub struct PortalProps {
15    target: RenderTargetId,
16    /// The children rendered into the portal target.
17    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        // Unconditionally adopt the new props' fields. Each `rsx!` macro
50        // expansion produces fresh `Rc<VNodeInner>` instances even for
51        // identical markup, so the `self == new` short-circuit on
52        // `Rc::ptr_eq` is effectively unreachable in practice - we still
53        // return the equality flag so callers can skip a redundant diff
54        // when the pointers happen to alias (e.g. cached test fixtures).
55        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/// A [`PortalProps`] builder that remembers the [`Portal`] component function it came from, mirroring
118/// the builder generated by `#[derive(Props)]`. The `rsx!` macro starts from the component function, so
119/// the field setters and `build` live on this wrapper rather than on [`PortalPropsBuilder`].
120#[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/// Render children into another renderer target while keeping their logical parent.
172///
173/// ## Details
174///
175/// A portal changes where renderer mutations for its children are written. It does not change
176/// component ownership, context, or event propagation. Children rendered through a portal can still
177/// consume context from their logical ancestors, and events from the portal target bubble through
178/// the same Dioxus component tree.
179///
180/// Each render target has its own [`ElementId`] arena. Hosts create targets with
181/// [`Runtime::create_render_target`] and serve them with a [`MultiWriter`]. A single
182/// [`WriteMutations`] writer automatically serves the root target; hosts with several targets can
183/// pass a `BTreeMap<RenderTargetId, W>` or their own [`MultiWriter`] implementation. If the host is
184/// not currently serving the target, renderer mutations for that target are skipped.
185///
186/// ## Example
187///
188/// ```rust
189/// # use dioxus::prelude::*;
190/// #[component]
191/// fn App(target: RenderTargetId) -> Element {
192///     rsx! {
193///         main { "Main view" }
194///         Portal {
195///             target,
196///             aside { "Rendered in another target" }
197///         }
198///     }
199/// }
200/// ```
201///
202/// ## Usage
203///
204/// Use `Portal` when a renderer exposes more than one target, such as a desktop child window,
205/// overlay root, or another renderer-owned mount point. For ordinary layout within the current
206/// target, render children directly instead.
207#[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
213/// The rendering lifecycle of a portal scope: its output lives at the root of another render
214/// target instead of mounting at the scope's slot.
215struct 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
226/// Place `children` inside `target_id`, record them as the scope's rendered output, and fire mount
227/// lifecycle when writes are enabled.
228///
229/// `old_root_mount` is `Some` when the portal has a previously committed root mount, and `None`
230/// when it should only allocate a fresh root mount.
231fn 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
265/// Place the portal children, reusing the previously committed root in place when
266/// its template is unchanged (e.g. promoting a background-rendered portal out of
267/// suspense) so the portal's scope subtree keeps its identity.
268fn 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            // The props' children handle is not mount-accurate after first create
294            // (mounts land on the rendered clone), so re-place from mounted output.
295            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        // The scope was allocated with its parent's target; declare it as
315        // a retargeting point before anything mounts under it. Later
316        // target changes are applied by the retarget arm of `diff`, which
317        // must observe the old target first.
318        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                // Ordering is correctness-critical: writes route through the
354                // portal scope's `target_id`, so the removal above resolves
355                // against the old target and `mount_children` below resolves
356                // against the new one.
357                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            // `PortalDriver::create` always sets `last_rendered_node` before
403            // returning, and removal only fires after a scope has gone
404            // through `create`, so the clone is always `Some`.
405            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}