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 accessibility is active for this webview.
83    ///
84    /// Set by [`crate::Constellation::set_accessibility_active()`], and forwarded to the
85    /// webview’s *active* pipelines (of those that represent documents) at any given moment
86    /// via [`ScriptThreadMessage::SetAccessibilityActive`] in `set_accessibility_active()`
87    /// and [`crate::Constellation::set_frame_tree_for_webview()`].
88    pub accessibility_active: bool,
89
90    /// Pending screenshot readiness requests. These are collected until the screenshot is
91    /// ready to take place, at which point the Constellation informs the renderer that it
92    /// can start the process of taking the screenshot.
93    screenshot_readiness_requests: Vec<ScreenshotReadinessRequest>,
94}
95
96impl ConstellationWebView {
97    pub(crate) fn new(
98        webview_id: WebViewId,
99        focused_browsing_context_id: BrowsingContextId,
100        user_content_manager_id: Option<UserContentManagerId>,
101    ) -> Self {
102        Self {
103            webview_id,
104            user_content_manager_id,
105            active_top_level_pipeline_id: None,
106            active_top_level_pipeline_epoch: Epoch::default(),
107            pending_changes: Default::default(),
108            focused_browsing_context_id,
109            hovered_browsing_context_id: None,
110            last_mouse_move_point: Default::default(),
111            session_history: JointSessionHistory::new(),
112            session_history_traversal_request_queue: Default::default(),
113            ongoing_history_traversal_request: None,
114            pending_viewport_details: Default::default(),
115            theme: Theme::Light,
116            accessibility_active: false,
117            screenshot_readiness_requests: Default::default(),
118        }
119    }
120
121    /// Set the [`Theme`] on this [`ConstellationWebView`] returning true if the theme changed.
122    pub(crate) fn set_theme(&mut self, new_theme: Theme) -> bool {
123        let old_theme = std::mem::replace(&mut self.theme, new_theme);
124        old_theme != self.theme
125    }
126
127    /// Get the [`Theme`] of this [`ConstellationWebView`].
128    pub(crate) fn theme(&self) -> Theme {
129        self.theme
130    }
131
132    fn target_pipeline_id_for_input_event(
133        &self,
134        event: &ConstellationInputEvent,
135        browsing_contexts: &FxHashMap<BrowsingContextId, BrowsingContext>,
136    ) -> Option<PipelineId> {
137        if let Some(hit_test_result) = &event.hit_test_result {
138            return Some(hit_test_result.pipeline_id);
139        }
140
141        // If there's no hit test, send the event to either the hovered or focused browsing context,
142        // depending on the event type.
143        let browsing_context_id = if matches!(event.event.event, InputEvent::MouseLeftViewport(_)) {
144            self.hovered_browsing_context_id
145                .unwrap_or(self.focused_browsing_context_id)
146        } else {
147            self.focused_browsing_context_id
148        };
149
150        Some(browsing_contexts.get(&browsing_context_id)?.pipeline_id)
151    }
152
153    /// Forward the [`InputEvent`] to this [`ConstellationWebView`]. Returns false if
154    /// the event could not be forwarded or true otherwise.
155    pub(crate) fn forward_input_event(
156        &mut self,
157        event: ConstellationInputEvent,
158        pipelines: &FxHashMap<PipelineId, Pipeline>,
159        browsing_contexts: &FxHashMap<BrowsingContextId, BrowsingContext>,
160    ) -> bool {
161        let Some(pipeline_id) = self.target_pipeline_id_for_input_event(&event, browsing_contexts)
162        else {
163            warn!("Unknown pipeline for input event. Ignoring.");
164            return false;
165        };
166        let Some(pipeline) = pipelines.get(&pipeline_id) else {
167            warn!("Unknown pipeline id {pipeline_id:?} for input event. Ignoring.");
168            return false;
169        };
170
171        let mut update_hovered_browsing_context =
172            |newly_hovered_browsing_context_id, focus_moving_to_another_iframe: bool| {
173                let old_hovered_context_id = std::mem::replace(
174                    &mut self.hovered_browsing_context_id,
175                    newly_hovered_browsing_context_id,
176                );
177                if old_hovered_context_id == newly_hovered_browsing_context_id {
178                    return;
179                }
180                let Some(old_hovered_context_id) = old_hovered_context_id else {
181                    return;
182                };
183                let Some(pipeline) = browsing_contexts
184                    .get(&old_hovered_context_id)
185                    .and_then(|browsing_context| pipelines.get(&browsing_context.pipeline_id))
186                else {
187                    return;
188                };
189
190                let mut synthetic_mouse_leave_event = event.clone();
191                synthetic_mouse_leave_event.event.event =
192                    InputEvent::MouseLeftViewport(MouseLeftViewportEvent {
193                        focus_moving_to_another_iframe,
194                    });
195
196                let _ = pipeline
197                    .event_loop
198                    .send(ScriptThreadMessage::SendInputEvent(
199                        self.webview_id,
200                        pipeline.id,
201                        synthetic_mouse_leave_event,
202                    ));
203            };
204
205        if let InputEvent::MouseLeftViewport(_) = &event.event.event {
206            update_hovered_browsing_context(None, false);
207            return true;
208        }
209
210        if let InputEvent::MouseMove(_) = &event.event.event {
211            update_hovered_browsing_context(Some(pipeline.browsing_context_id), true);
212            self.last_mouse_move_point = event
213                .hit_test_result
214                .as_ref()
215                .expect("MouseMove events should always have hit tests.")
216                .point_in_viewport;
217        }
218
219        let _ = pipeline
220            .event_loop
221            .send(ScriptThreadMessage::SendInputEvent(
222                self.webview_id,
223                pipeline.id,
224                event,
225            ));
226        true
227    }
228
229    pub(crate) fn close_browsing_context(&mut self, browsing_context_id: BrowsingContextId) {
230        self.take_pending_viewport_details(&browsing_context_id);
231        self.session_history
232            .remove_entries_for_browsing_context(browsing_context_id);
233    }
234
235    /// If there is an ongoing history traversal request that is waiting on documents to
236    /// reload, check to see if none of its pipelines are awaiting activation. If that's the
237    /// case unset the ongoing request and return it.
238    pub(crate) fn maybe_finish_ongoing_session_history_traversal_request(
239        &mut self,
240    ) -> Option<SessionHistoryTraversalRequest> {
241        let ongoing_history_traversal_request = self.ongoing_history_traversal_request.as_mut()?;
242
243        let pipelines_with_pending_changes = self
244            .pending_changes
245            .iter()
246            .map(|change| change.new_pipeline_id)
247            .collect::<FxHashSet<_>>();
248        ongoing_history_traversal_request
249            .pipelines_awaiting_activation
250            .retain(|pipeline_id| pipelines_with_pending_changes.contains(pipeline_id));
251
252        if !ongoing_history_traversal_request
253            .pipelines_awaiting_activation
254            .is_empty()
255        {
256            return None;
257        }
258        Some(
259            self.ongoing_history_traversal_request
260                .take()
261                .expect("Guaranteed above")
262                .traversal_request,
263        )
264    }
265
266    pub(crate) fn has_pending_change(&self) -> bool {
267        !self.pending_changes.is_empty()
268    }
269
270    pub(crate) fn pipeline_is_pending(&self, pipeline_id: PipelineId) -> bool {
271        self.pending_changes
272            .iter()
273            .any(|pending_change| pending_change.new_pipeline_id == pipeline_id)
274    }
275
276    pub(crate) fn add_pending_change(&mut self, change: SessionHistoryChange) {
277        debug!(
278            "adding pending session history change with {}",
279            if change.replace.is_some() {
280                "replacement"
281            } else {
282                "no replacement"
283            },
284        );
285        self.pending_changes.push(change);
286    }
287
288    pub(crate) fn remove_pending_change_for_pipeline(
289        &mut self,
290        pipeline_id: PipelineId,
291    ) -> Option<SessionHistoryChange> {
292        let pending_index = self
293            .pending_changes
294            .iter()
295            .rposition(|change| change.new_pipeline_id == pipeline_id)?;
296        Some(self.pending_changes.swap_remove(pending_index))
297    }
298
299    #[servo_tracing::instrument(skip_all)]
300    pub(crate) fn handle_screenshot_readiness_request(
301        &mut self,
302        browsing_contexts: &FxHashMap<BrowsingContextId, BrowsingContext>,
303        pipelines: &FxHashMap<PipelineId, Pipeline>,
304    ) {
305        self.screenshot_readiness_requests
306            .push(ScreenshotReadinessRequest {
307                pipeline_states: Default::default(),
308                state: Default::default(),
309            });
310        self.send_screenshot_readiness_requests_to_pipelines(browsing_contexts, pipelines);
311    }
312
313    pub(crate) fn send_screenshot_readiness_requests_to_pipelines(
314        &mut self,
315        browsing_contexts: &FxHashMap<BrowsingContextId, BrowsingContext>,
316        pipelines: &FxHashMap<PipelineId, Pipeline>,
317    ) {
318        // If there are pending loads, wait for those to complete.
319        if self.has_pending_change() {
320            return;
321        }
322
323        for screenshot_request in self.screenshot_readiness_requests.iter_mut() {
324            // Ignore this request if it is not pending.
325            if screenshot_request.state != ScreenshotRequestState::Pending {
326                continue;
327            }
328
329            screenshot_request.pipeline_states = FullyActiveBrowsingContextsIterator::new(
330                self.webview_id.into(),
331                browsing_contexts,
332                pipelines,
333            )
334            .filter_map(|browsing_context| {
335                let pipeline_id = browsing_context.pipeline_id;
336                let Some(pipeline) = pipelines.get(&pipeline_id) else {
337                    // This can happen while Servo is shutting down, so just ignore it for now.
338                    return None;
339                };
340                // If the rectangle for this BrowsingContext is zero, it will never be
341                // painted. In this case, don't query screenshot readiness as it won't
342                // contribute to the final output image.
343                if browsing_context.viewport_details.size == Size2D::zero() {
344                    return None;
345                }
346                let _ = pipeline
347                    .event_loop
348                    .send(ScriptThreadMessage::RequestScreenshotReadiness(
349                        pipeline.webview_id,
350                        pipeline_id,
351                    ));
352                Some((pipeline_id, None))
353            })
354            .collect();
355            screenshot_request.state = ScreenshotRequestState::WaitingOnScript;
356        }
357    }
358
359    #[servo_tracing::instrument(skip_all)]
360    pub(crate) fn handle_screenshot_readiness_response(
361        &mut self,
362        updated_pipeline_id: PipelineId,
363        response: ScreenshotReadinessResponse,
364        paint_proxy: &PaintProxy,
365    ) {
366        if self.screenshot_readiness_requests.is_empty() {
367            return;
368        }
369
370        self.screenshot_readiness_requests
371            .retain_mut(|screenshot_request| {
372                if screenshot_request.state != ScreenshotRequestState::WaitingOnScript {
373                    return true;
374                }
375
376                let mut has_pending_pipeline = false;
377                screenshot_request
378                    .pipeline_states
379                    .retain(|pipeline_id, state| {
380                        if *pipeline_id != updated_pipeline_id {
381                            has_pending_pipeline |= state.is_none();
382                            return true;
383                        }
384                        match response {
385                            ScreenshotReadinessResponse::Ready(epoch) => {
386                                *state = Some(epoch);
387                                true
388                            },
389                            ScreenshotReadinessResponse::NoLongerActive => false,
390                        }
391                    });
392
393                if has_pending_pipeline {
394                    return true;
395                }
396
397                let pipelines_and_epochs = screenshot_request
398                    .pipeline_states
399                    .iter()
400                    .map(|(pipeline_id, epoch)| {
401                        (
402                            *pipeline_id,
403                            epoch.expect("Should have an epoch when pipeline is ready."),
404                        )
405                    })
406                    .collect();
407                paint_proxy.send(PaintMessage::ScreenshotReadinessReponse(
408                    self.webview_id,
409                    pipelines_and_epochs,
410                ));
411
412                false
413            });
414    }
415
416    pub(crate) fn add_viewport_details(
417        &mut self,
418        browsing_context_id: BrowsingContextId,
419        viewport_details: ViewportDetails,
420    ) {
421        self.pending_viewport_details
422            .insert(browsing_context_id, viewport_details);
423    }
424
425    pub(crate) fn take_pending_viewport_details(
426        &mut self,
427        browsing_context_id: &BrowsingContextId,
428    ) -> Option<ViewportDetails> {
429        self.pending_viewport_details.remove(browsing_context_id)
430    }
431}
432
433/// A [`HistoryTraversalRequest`] that is in progress because it is waiting
434/// for documents that need reloading.
435pub(crate) struct OngoingHistoryTraversalRequest {
436    /// The [`HistoryTraversalRequest`] that spawned this series of navigations.
437    pub traversal_request: SessionHistoryTraversalRequest,
438    /// The ids of all the `Pipeline`s that needed reloading for this traversal.
439    /// Multiple pipelines can be traversed if the top-level document contained
440    /// `<iframe>`s / browsing contexts. The traversal is only done when all of
441    /// the pipelines are ready or have failed to load.
442    pub pipelines_awaiting_activation: FxHashSet<PipelineId>,
443}