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 hidden: bool,
85
86 pub accessibility_active: bool,
93
94 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 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 pub(crate) fn theme(&self) -> Theme {
134 self.theme
135 }
136
137 pub(crate) fn hidden(&self) -> bool {
139 self.hidden
140 }
141
142 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 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 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 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 self.has_pending_change() {
336 return;
337 }
338
339 for screenshot_request in self.screenshot_readiness_requests.iter_mut() {
340 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 return None;
355 };
356 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
449pub(crate) struct OngoingHistoryTraversalRequest {
452 pub traversal_request: SessionHistoryTraversalRequest,
454 pub pipelines_awaiting_activation: FxHashSet<PipelineId>,
459}