Skip to main content

servo_constellation/
constellation_webview.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::collections::{HashMap, VecDeque};
6
7use embedder_traits::user_contents::UserContentManagerId;
8use embedder_traits::{InputEvent, MouseLeftViewportEvent, Theme, ViewportDetails};
9use euclid::{Point2D, Size2D};
10use log::{debug, warn};
11use paint_api::{PaintMessage, PaintProxy};
12use rustc_hash::{FxHashMap, FxHashSet};
13use script_traits::{ConstellationInputEvent, ScriptThreadMessage};
14use servo_base::Epoch;
15use servo_base::id::{BrowsingContextId, PipelineId, WebViewId};
16use servo_constellation_traits::{ScreenshotReadinessResponse, SessionHistoryTraversalRequest};
17use style_traits::CSSPixel;
18
19use crate::browsingcontext::{BrowsingContext, FullyActiveBrowsingContextsIterator};
20use crate::pipeline::Pipeline;
21use crate::screenshot_readiness_request::{ScreenshotReadinessRequest, ScreenshotRequestState};
22use crate::session_history::{JointSessionHistory, SessionHistoryChange};
23
24/// The `Constellation`'s view of a `WebView` in the embedding layer. This tracks all of the
25/// `Constellation` state for this `WebView`.
26pub(crate) struct ConstellationWebView {
27    /// The [`WebViewId`] of this [`ConstellationWebView`].
28    webview_id: WebViewId,
29
30    /// The [`PipelineId`] of the currently active pipeline at the top level of this WebView.
31    pub active_top_level_pipeline_id: Option<PipelineId>,
32
33    /// A counter for changes to [`Self::active_top_level_pipeline_id`].
34    pub active_top_level_pipeline_epoch: Epoch,
35
36    /// When a navigation is performed, we do not immediately update
37    /// the session history, instead we ask the event loop to begin loading
38    /// the new document, and do not update the browsing context until the
39    /// document is active. Between starting the load and it activating,
40    /// we store a `SessionHistoryChange` object for the navigation in progress.
41    pub pending_changes: Vec<SessionHistoryChange>,
42
43    /// The currently focused browsing context in this webview for key events.
44    /// The focused pipeline is the current entry of the focused browsing
45    /// context.
46    pub focused_browsing_context_id: BrowsingContextId,
47
48    /// The [`BrowsingContextId`] of the currently hovered browsing context, to use for
49    /// knowing which frame is currently receiving cursor events.
50    pub hovered_browsing_context_id: Option<BrowsingContextId>,
51
52    /// The last mouse move point in the coordinate space of the Pipeline that it
53    /// happened int.
54    pub last_mouse_move_point: Point2D<f32, CSSPixel>,
55
56    /// The joint session history for this webview.
57    pub session_history: JointSessionHistory,
58
59    /// <https://html.spec.whatwg.org/multipage/#tn-session-history-traversal-queue>
60    ///
61    /// A queue of traversals that should be applied sequentially. The next item from
62    /// the queue is applied once [`Self::ongoing_history_traversal_request`] has finished.
63    pub session_history_traversal_request_queue: VecDeque<SessionHistoryTraversalRequest>,
64
65    /// The currently running session history traversal. This will be completed once all
66    /// `Pipeline`s in a traversal become active or their load fails for some other reason.
67    pub ongoing_history_traversal_request: Option<OngoingHistoryTraversalRequest>,
68
69    /// Pending viewport changes for browsing contexts that are not
70    /// yet known to the constellation.
71    pending_viewport_details: HashMap<BrowsingContextId, ViewportDetails>,
72
73    /// The [`UserContentManagerId`] for all pipelines in this `WebView`. This is `Some`
74    /// if the embedder has set a `UserContentManager` using the WebViewBuilder API and
75    /// it is `None` otherwise.
76    pub user_content_manager_id: Option<UserContentManagerId>,
77
78    /// The [`Theme`] that this [`ConstellationWebView`] uses. This is communicated to all
79    /// `ScriptThread`s so that they know how to render the contents of a particular `WebView.
80    theme: Theme,
81
82    /// Whether or not this entire [`ConstellationWebView`] is hidden. `WebView`s that
83    /// are hidden will be throttled.
84    hidden: bool,
85
86    /// Whether accessibility is active for this webview.
87    ///
88    /// Set by [`crate::Constellation::set_accessibility_active()`], and forwarded to the
89    /// webview’s *active* pipelines (of those that represent documents) at any given moment
90    /// via [`ScriptThreadMessage::SetAccessibilityActive`] in `set_accessibility_active()`
91    /// and [`crate::Constellation::set_frame_tree_for_webview()`].
92    pub accessibility_active: bool,
93
94    /// Pending screenshot readiness requests. These are collected until the screenshot is
95    /// ready to take place, at which point the Constellation informs the renderer that it
96    /// can start the process of taking the screenshot.
97    screenshot_readiness_requests: Vec<ScreenshotReadinessRequest>,
98}
99
100impl ConstellationWebView {
101    pub(crate) fn new(
102        webview_id: WebViewId,
103        focused_browsing_context_id: BrowsingContextId,
104        user_content_manager_id: Option<UserContentManagerId>,
105    ) -> Self {
106        Self {
107            webview_id,
108            user_content_manager_id,
109            active_top_level_pipeline_id: None,
110            active_top_level_pipeline_epoch: Epoch::default(),
111            pending_changes: Default::default(),
112            focused_browsing_context_id,
113            hovered_browsing_context_id: None,
114            last_mouse_move_point: Default::default(),
115            session_history: JointSessionHistory::new(),
116            session_history_traversal_request_queue: Default::default(),
117            ongoing_history_traversal_request: None,
118            pending_viewport_details: Default::default(),
119            theme: Theme::Light,
120            hidden: false,
121            accessibility_active: false,
122            screenshot_readiness_requests: Default::default(),
123        }
124    }
125
126    /// Set the [`Theme`] on this [`ConstellationWebView`] returning true if the theme changed.
127    pub(crate) fn set_theme(&mut self, new_theme: Theme) -> bool {
128        let old_theme = std::mem::replace(&mut self.theme, new_theme);
129        old_theme != self.theme
130    }
131
132    /// Get the [`Theme`] of this [`ConstellationWebView`].
133    pub(crate) fn theme(&self) -> Theme {
134        self.theme
135    }
136
137    /// Whether or not the [`ConstellationWebView`] is hidden.
138    pub(crate) fn hidden(&self) -> bool {
139        self.hidden
140    }
141
142    /// Set whether or not this [`ConstellationWebView`] is hidden, returning true if the value changed.
143    pub(crate) fn set_hidden(&mut self, hidden: bool) -> bool {
144        let old_hidden = std::mem::replace(&mut self.hidden, hidden);
145        old_hidden != self.hidden
146    }
147
148    fn target_pipeline_id_for_input_event(
149        &self,
150        event: &ConstellationInputEvent,
151        browsing_contexts: &FxHashMap<BrowsingContextId, BrowsingContext>,
152    ) -> Option<PipelineId> {
153        if let Some(hit_test_result) = &event.hit_test_result {
154            return Some(hit_test_result.pipeline_id);
155        }
156
157        // If there's no hit test, send the event to either the hovered or focused browsing context,
158        // depending on the event type.
159        let browsing_context_id = if matches!(event.event.event, InputEvent::MouseLeftViewport(_)) {
160            self.hovered_browsing_context_id
161                .unwrap_or(self.focused_browsing_context_id)
162        } else {
163            self.focused_browsing_context_id
164        };
165
166        Some(browsing_contexts.get(&browsing_context_id)?.pipeline_id)
167    }
168
169    /// Forward the [`InputEvent`] to this [`ConstellationWebView`]. Returns false if
170    /// the event could not be forwarded or true otherwise.
171    pub(crate) fn forward_input_event(
172        &mut self,
173        event: ConstellationInputEvent,
174        pipelines: &FxHashMap<PipelineId, Pipeline>,
175        browsing_contexts: &FxHashMap<BrowsingContextId, BrowsingContext>,
176    ) -> bool {
177        let Some(pipeline_id) = self.target_pipeline_id_for_input_event(&event, browsing_contexts)
178        else {
179            warn!("Unknown pipeline for input event. Ignoring.");
180            return false;
181        };
182        let Some(pipeline) = pipelines.get(&pipeline_id) else {
183            warn!("Unknown pipeline id {pipeline_id:?} for input event. Ignoring.");
184            return false;
185        };
186
187        let mut update_hovered_browsing_context =
188            |newly_hovered_browsing_context_id, focus_moving_to_another_iframe: bool| {
189                let old_hovered_context_id = std::mem::replace(
190                    &mut self.hovered_browsing_context_id,
191                    newly_hovered_browsing_context_id,
192                );
193                if old_hovered_context_id == newly_hovered_browsing_context_id {
194                    return;
195                }
196                let Some(old_hovered_context_id) = old_hovered_context_id else {
197                    return;
198                };
199                let Some(pipeline) = browsing_contexts
200                    .get(&old_hovered_context_id)
201                    .and_then(|browsing_context| pipelines.get(&browsing_context.pipeline_id))
202                else {
203                    return;
204                };
205
206                let mut synthetic_mouse_leave_event = event.clone();
207                synthetic_mouse_leave_event.event.event =
208                    InputEvent::MouseLeftViewport(MouseLeftViewportEvent {
209                        focus_moving_to_another_iframe,
210                    });
211
212                let _ = pipeline
213                    .event_loop
214                    .send(ScriptThreadMessage::SendInputEvent(
215                        self.webview_id,
216                        pipeline.id,
217                        synthetic_mouse_leave_event,
218                    ));
219            };
220
221        if let InputEvent::MouseLeftViewport(_) = &event.event.event {
222            update_hovered_browsing_context(None, false);
223            return true;
224        }
225
226        if let InputEvent::MouseMove(_) = &event.event.event {
227            update_hovered_browsing_context(Some(pipeline.browsing_context_id), true);
228            self.last_mouse_move_point = event
229                .hit_test_result
230                .as_ref()
231                .expect("MouseMove events should always have hit tests.")
232                .point_in_viewport;
233        }
234
235        let _ = pipeline
236            .event_loop
237            .send(ScriptThreadMessage::SendInputEvent(
238                self.webview_id,
239                pipeline.id,
240                event,
241            ));
242        true
243    }
244
245    pub(crate) fn close_browsing_context(&mut self, browsing_context_id: BrowsingContextId) {
246        self.take_pending_viewport_details(&browsing_context_id);
247        self.session_history
248            .remove_entries_for_browsing_context(browsing_context_id);
249    }
250
251    /// If there is an ongoing history traversal request that is waiting on documents to
252    /// reload, check to see if none of its pipelines are awaiting activation. If that's the
253    /// case unset the ongoing request and return it.
254    pub(crate) fn maybe_finish_ongoing_session_history_traversal_request(
255        &mut self,
256    ) -> Option<SessionHistoryTraversalRequest> {
257        let ongoing_history_traversal_request = self.ongoing_history_traversal_request.as_mut()?;
258
259        let pipelines_with_pending_changes = self
260            .pending_changes
261            .iter()
262            .map(|change| change.new_pipeline_id)
263            .collect::<FxHashSet<_>>();
264        ongoing_history_traversal_request
265            .pipelines_awaiting_activation
266            .retain(|pipeline_id| pipelines_with_pending_changes.contains(pipeline_id));
267
268        if !ongoing_history_traversal_request
269            .pipelines_awaiting_activation
270            .is_empty()
271        {
272            return None;
273        }
274        Some(
275            self.ongoing_history_traversal_request
276                .take()
277                .expect("Guaranteed above")
278                .traversal_request,
279        )
280    }
281
282    pub(crate) fn has_pending_change(&self) -> bool {
283        !self.pending_changes.is_empty()
284    }
285
286    pub(crate) fn pipeline_is_pending(&self, pipeline_id: PipelineId) -> bool {
287        self.pending_changes
288            .iter()
289            .any(|pending_change| pending_change.new_pipeline_id == pipeline_id)
290    }
291
292    pub(crate) fn add_pending_change(&mut self, change: SessionHistoryChange) {
293        debug!(
294            "adding pending session history change with {}",
295            if change.replace.is_some() {
296                "replacement"
297            } else {
298                "no replacement"
299            },
300        );
301        self.pending_changes.push(change);
302    }
303
304    pub(crate) fn remove_pending_change_for_pipeline(
305        &mut self,
306        pipeline_id: PipelineId,
307    ) -> Option<SessionHistoryChange> {
308        let pending_index = self
309            .pending_changes
310            .iter()
311            .rposition(|change| change.new_pipeline_id == pipeline_id)?;
312        Some(self.pending_changes.swap_remove(pending_index))
313    }
314
315    #[servo_tracing::instrument(skip_all)]
316    pub(crate) fn handle_screenshot_readiness_request(
317        &mut self,
318        browsing_contexts: &FxHashMap<BrowsingContextId, BrowsingContext>,
319        pipelines: &FxHashMap<PipelineId, Pipeline>,
320    ) {
321        self.screenshot_readiness_requests
322            .push(ScreenshotReadinessRequest {
323                pipeline_states: Default::default(),
324                state: Default::default(),
325            });
326        self.send_screenshot_readiness_requests_to_pipelines(browsing_contexts, pipelines);
327    }
328
329    pub(crate) fn send_screenshot_readiness_requests_to_pipelines(
330        &mut self,
331        browsing_contexts: &FxHashMap<BrowsingContextId, BrowsingContext>,
332        pipelines: &FxHashMap<PipelineId, Pipeline>,
333    ) {
334        // If there are pending loads, wait for those to complete.
335        if self.has_pending_change() {
336            return;
337        }
338
339        for screenshot_request in self.screenshot_readiness_requests.iter_mut() {
340            // Ignore this request if it is not pending.
341            if screenshot_request.state != ScreenshotRequestState::Pending {
342                continue;
343            }
344
345            screenshot_request.pipeline_states = FullyActiveBrowsingContextsIterator::new(
346                self.webview_id.into(),
347                browsing_contexts,
348                pipelines,
349            )
350            .filter_map(|browsing_context| {
351                let pipeline_id = browsing_context.pipeline_id;
352                let Some(pipeline) = pipelines.get(&pipeline_id) else {
353                    // This can happen while Servo is shutting down, so just ignore it for now.
354                    return None;
355                };
356                // If the rectangle for this BrowsingContext is zero, it will never be
357                // painted. In this case, don't query screenshot readiness as it won't
358                // contribute to the final output image.
359                if browsing_context.viewport_details.size == Size2D::zero() {
360                    return None;
361                }
362                let _ = pipeline
363                    .event_loop
364                    .send(ScriptThreadMessage::RequestScreenshotReadiness(
365                        pipeline.webview_id,
366                        pipeline_id,
367                    ));
368                Some((pipeline_id, None))
369            })
370            .collect();
371            screenshot_request.state = ScreenshotRequestState::WaitingOnScript;
372        }
373    }
374
375    #[servo_tracing::instrument(skip_all)]
376    pub(crate) fn handle_screenshot_readiness_response(
377        &mut self,
378        updated_pipeline_id: PipelineId,
379        response: ScreenshotReadinessResponse,
380        paint_proxy: &PaintProxy,
381    ) {
382        if self.screenshot_readiness_requests.is_empty() {
383            return;
384        }
385
386        self.screenshot_readiness_requests
387            .retain_mut(|screenshot_request| {
388                if screenshot_request.state != ScreenshotRequestState::WaitingOnScript {
389                    return true;
390                }
391
392                let mut has_pending_pipeline = false;
393                screenshot_request
394                    .pipeline_states
395                    .retain(|pipeline_id, state| {
396                        if *pipeline_id != updated_pipeline_id {
397                            has_pending_pipeline |= state.is_none();
398                            return true;
399                        }
400                        match response {
401                            ScreenshotReadinessResponse::Ready(epoch) => {
402                                *state = Some(epoch);
403                                true
404                            },
405                            ScreenshotReadinessResponse::NoLongerActive => false,
406                        }
407                    });
408
409                if has_pending_pipeline {
410                    return true;
411                }
412
413                let pipelines_and_epochs = screenshot_request
414                    .pipeline_states
415                    .iter()
416                    .map(|(pipeline_id, epoch)| {
417                        (
418                            *pipeline_id,
419                            epoch.expect("Should have an epoch when pipeline is ready."),
420                        )
421                    })
422                    .collect();
423                paint_proxy.send(PaintMessage::ScreenshotReadinessReponse(
424                    self.webview_id,
425                    pipelines_and_epochs,
426                ));
427
428                false
429            });
430    }
431
432    pub(crate) fn add_viewport_details(
433        &mut self,
434        browsing_context_id: BrowsingContextId,
435        viewport_details: ViewportDetails,
436    ) {
437        self.pending_viewport_details
438            .insert(browsing_context_id, viewport_details);
439    }
440
441    pub(crate) fn take_pending_viewport_details(
442        &mut self,
443        browsing_context_id: &BrowsingContextId,
444    ) -> Option<ViewportDetails> {
445        self.pending_viewport_details.remove(browsing_context_id)
446    }
447}
448
449/// A [`HistoryTraversalRequest`] that is in progress because it is waiting
450/// for documents that need reloading.
451pub(crate) struct OngoingHistoryTraversalRequest {
452    /// The [`HistoryTraversalRequest`] that spawned this series of navigations.
453    pub traversal_request: SessionHistoryTraversalRequest,
454    /// The ids of all the `Pipeline`s that needed reloading for this traversal.
455    /// Multiple pipelines can be traversed if the top-level document contained
456    /// `<iframe>`s / browsing contexts. The traversal is only done when all of
457    /// the pipelines are ready or have failed to load.
458    pub pipelines_awaiting_activation: FxHashSet<PipelineId>,
459}