1use 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
24pub(crate) struct ConstellationWebView {
27 webview_id: WebViewId,
29
30 pub active_top_level_pipeline_id: Option<PipelineId>,
32
33 pub active_top_level_pipeline_epoch: Epoch,
35
36 pub pending_changes: Vec<SessionHistoryChange>,
42
43 pub focused_browsing_context_id: BrowsingContextId,
47
48 pub hovered_browsing_context_id: Option<BrowsingContextId>,
51
52 pub last_mouse_move_point: Point2D<f32, CSSPixel>,
55
56 pub session_history: JointSessionHistory,
58
59 pub session_history_traversal_request_queue: VecDeque<SessionHistoryTraversalRequest>,
64
65 pub ongoing_history_traversal_request: Option<OngoingHistoryTraversalRequest>,
68
69 pending_viewport_details: HashMap<BrowsingContextId, ViewportDetails>,
72
73 pub user_content_manager_id: Option<UserContentManagerId>,
77
78 theme: Theme,
81
82 pub accessibility_active: bool,
89
90 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 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 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 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 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 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 self.has_pending_change() {
320 return;
321 }
322
323 for screenshot_request in self.screenshot_readiness_requests.iter_mut() {
324 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 return None;
339 };
340 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
433pub(crate) struct OngoingHistoryTraversalRequest {
436 pub traversal_request: SessionHistoryTraversalRequest,
438 pub pipelines_awaiting_activation: FxHashSet<PipelineId>,
443}