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