Skip to main content

script/dom/document/
iframe_collection.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use std::cell::Ref;
6use std::default::Default;
7use std::iter;
8
9use embedder_traits::ViewportDetails;
10use js::context::{JSContext, NoGC};
11use layout_api::IFrameSizes;
12use paint_api::PinchZoomInfos;
13use script_bindings::cell::DomRefCell;
14use servo_base::id::BrowsingContextId;
15use servo_constellation_traits::{IFrameSizeMsg, ScriptToConstellationMessage, WindowSizeType};
16
17use crate::dom::NodeTraits;
18use crate::dom::bindings::inheritance::Castable;
19use crate::dom::bindings::root::{Dom, DomRoot};
20use crate::dom::html::htmliframeelement::HTMLIFrameElement;
21use crate::dom::iterators::ShadowIncluding;
22use crate::dom::node::Node;
23use crate::dom::types::Window;
24use crate::event_loop::script_thread::with_script_thread;
25
26#[derive(JSTraceable, MallocSizeOf)]
27#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
28pub(crate) struct IFrame {
29    pub(crate) element: Dom<HTMLIFrameElement>,
30    #[no_trace]
31    pub(crate) size: Option<ViewportDetails>,
32}
33
34#[derive(Default, JSTraceable, MallocSizeOf)]
35#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
36pub(crate) struct IFrameCollection {
37    /// The `<iframe>`s in the collection. These are kept in DOM tree order to ensure that
38    /// requestAnimationFrame callbacks respect that order.
39    iframes: DomRefCell<Vec<IFrame>>,
40    /// The same `<iframe>`s in [`Self::iframes`], but stored in insertion order for use
41    /// in the `WindowProxy` subframe getter.
42    iframes_in_insertion_order: DomRefCell<Vec<Dom<HTMLIFrameElement>>>,
43}
44
45impl IFrameCollection {
46    pub(crate) fn new() -> Self {
47        Self {
48            iframes: Default::default(),
49            iframes_in_insertion_order: Default::default(),
50        }
51    }
52
53    pub(crate) fn add(&self, no_gc: &NoGC, iframe_element: &HTMLIFrameElement) {
54        let iframe_node = iframe_element.upcast::<Node>();
55
56        // During `moveBefore`, nodes are attached to the tree again without detaching
57        // them in order to preserve state. Here we remove any pre-existing entry for
58        // this iframe element from the collection and preserve its old size.
59        let size = self.remove(no_gc, iframe_element);
60
61        // Look forward for the next `<iframe>` in the document in order to find the new
62        // insertion point in the DOM-ordered list of frames. This optimizes for the parser
63        // case where the `<iframe>` is likely being inserted at the end of the DOM and there
64        // are very few subsequent nodes.
65        let insertion_index = iframe_node
66            .following_nodes(
67                iframe_element.owner_document().upcast::<Node>(),
68                ShadowIncluding::Yes,
69            )
70            .find_map(DomRoot::downcast::<HTMLIFrameElement>)
71            .and_then(|following_iframe| {
72                self.iframes
73                    .borrow()
74                    .iter()
75                    .position(|iframe| *iframe.element == *following_iframe)
76            })
77            .unwrap_or(self.iframes.borrow().len());
78
79        self.iframes.safe_borrow_mut(no_gc).insert(
80            insertion_index,
81            IFrame {
82                element: Dom::from_ref(iframe_element),
83                size,
84            },
85        );
86
87        self.iframes_in_insertion_order
88            .safe_borrow_mut(no_gc)
89            .push(Dom::from_ref(iframe_element));
90    }
91
92    pub(crate) fn remove(
93        &self,
94        no_gc: &NoGC,
95        iframe_element: &HTMLIFrameElement,
96    ) -> Option<ViewportDetails> {
97        self.iframes_in_insertion_order
98            .safe_borrow_mut(no_gc)
99            .retain(|iframe| *iframe != iframe_element);
100        let mut iframes = self.iframes.safe_borrow_mut(no_gc);
101        iframes
102            .iter()
103            .position(|iframe| &*iframe.element == iframe_element)
104            .and_then(|index| iframes.remove(index).size)
105    }
106
107    /// Get the [`BrowsingContextId`] of the `<iframe>` element at the given
108    /// position in insertion order, filtering out `<iframe>`s that do not have
109    /// a browsing context.
110    pub(crate) fn at_insertion_index(&self, index: usize) -> Option<BrowsingContextId> {
111        self.iframes_in_insertion_order
112            .borrow()
113            .iter()
114            .filter_map(|iframe| iframe.browsing_context_id())
115            .nth(index)
116    }
117
118    /// Get a count of the iframes in this [`IframeCollection`] that have active browsing
119    /// contexts.
120    pub(crate) fn active_iframe_count(&self) -> usize {
121        self.iframes_in_insertion_order
122            .borrow()
123            .iter()
124            .filter(|iframe| iframe.browsing_context_id().is_some())
125            .count()
126    }
127
128    /// Get an iframe elment matching the provided browsing context id, if it exists.
129    pub(crate) fn element(
130        &self,
131        browsing_context_id: BrowsingContextId,
132    ) -> Option<DomRoot<HTMLIFrameElement>> {
133        self.iframes
134            .borrow()
135            .iter()
136            .find(|iframe| iframe.element.browsing_context_id() == Some(browsing_context_id))
137            .map(|iframe| iframe.element.as_rooted())
138    }
139
140    /// Get the viewport details for the iframe matching the provided browsing context id, if it exists.
141    pub(crate) fn viewport_details(
142        &self,
143        browsing_context_id: BrowsingContextId,
144    ) -> Option<ViewportDetails> {
145        self.iframes
146            .borrow()
147            .iter()
148            .find(|iframe| iframe.element.browsing_context_id() == Some(browsing_context_id))
149            .and_then(|iframe| iframe.size)
150    }
151
152    /// Set the size of an `<iframe>` in the collection given its `BrowsingContextId` and
153    /// the new size. Returns the old size.
154    fn set_viewport_details(
155        &self,
156        no_gc: &NoGC,
157        browsing_context_id: BrowsingContextId,
158        new_size: ViewportDetails,
159    ) -> Option<ViewportDetails> {
160        // Top-level document destruction can destroy an entire tree of frames, which
161        // means that the the `<iframe>` we are targeting at this moment might not exist.
162        self.iframes
163            .safe_borrow_mut(no_gc)
164            .iter_mut()
165            .find(|iframe| iframe.element.browsing_context_id() == Some(browsing_context_id))
166            .and_then(|iframe| iframe.size.replace(new_size))
167    }
168
169    /// Update the recorded iframe sizes of the contents of layout. Return a
170    /// [`Vec<IFrameSizeMsg>`] containing the messages to send to the `Constellation`. A
171    /// message is only sent when the size actually changes.
172    pub(crate) fn handle_new_iframe_sizes_after_layout(
173        &self,
174        cx: &mut JSContext,
175        window: &Window,
176        new_iframe_sizes: IFrameSizes,
177    ) {
178        if new_iframe_sizes.is_empty() {
179            return;
180        }
181
182        let size_messages: Vec<_> = new_iframe_sizes
183            .into_iter()
184            .filter_map(|(browsing_context_id, iframe_size)| {
185                // Batch resize message to any local `Pipeline`s now, rather than waiting for them
186                // to filter asynchronously through the `Constellation`. This allows the new value
187                // to be reflected immediately in layout.
188                let viewport_details = iframe_size.viewport_details;
189                with_script_thread(|script_thread| {
190                    script_thread.handle_resize_message(
191                        iframe_size.pipeline_id,
192                        viewport_details,
193                        WindowSizeType::Resize,
194                    );
195
196                    // Additionally, update the `VisualViewport` of the `Iframe`. This allows us
197                    // to process the resize for `VisualViewport` in the corrent timing. Note that
198                    // `VisualViewport` for iframes would practically follow layout viewport.
199                    script_thread.handle_update_pinch_zoom_infos(
200                        cx,
201                        iframe_size.pipeline_id,
202                        PinchZoomInfos::new_from_viewport_size(viewport_details.size),
203                    )
204                });
205
206                let old_viewport_details =
207                    self.set_viewport_details(cx.no_gc(), browsing_context_id, viewport_details);
208                // The `Constellation` should be up-to-date even when the in-ScriptThread pipelines
209                // might not be.
210                if old_viewport_details == Some(viewport_details) {
211                    return None;
212                }
213
214                let size_type = match old_viewport_details {
215                    Some(_) => WindowSizeType::Resize,
216                    None => WindowSizeType::Initial,
217                };
218
219                Some(IFrameSizeMsg {
220                    browsing_context_id,
221                    size: viewport_details,
222                    type_: size_type,
223                })
224            })
225            .collect();
226
227        if !size_messages.is_empty() {
228            window.send_to_constellation(ScriptToConstellationMessage::IFrameSizes(size_messages));
229        }
230    }
231
232    pub(crate) fn iter(&self) -> impl Iterator<Item = DomRoot<HTMLIFrameElement>> + use<'_> {
233        let mut items = Some(Ref::map(self.iframes.borrow(), |vec| &vec[..]));
234        iter::from_fn(move || {
235            let mut item = None;
236            let rest = Ref::map(items.take()?, |items| {
237                let mut iter = items.iter();
238                item = iter.next().map(|item| item.element.as_rooted());
239                iter.as_slice()
240            });
241            if item.is_some() {
242                items = Some(rest);
243            }
244            item
245        })
246    }
247}