Rewild Your Web
web browser dweb
at main 752 lines 34 kB view raw
1--- original 2+++ modified 3@@ -114,6 +114,7 @@ 4 use canvas_traits::webgl::WebGLThreads; 5 use constellation_traits::{ 6 AuxiliaryWebViewCreationRequest, AuxiliaryWebViewCreationResponse, DocumentState, 7+ EmbeddedWebViewCreationRequest, EmbeddedWebViewCreationResponse, EmbeddedWebViewEventType, 8 EmbedderToConstellationMessage, IFrameLoadInfo, IFrameLoadInfoWithData, IFrameSizeMsg, Job, 9 LoadData, LogEntry, MessagePortMsg, NavigationHistoryBehavior, PaintMetricEvent, 10 PortMessageTask, PortTransferInfo, SWManagerMsg, SWManagerSenders, ScreenshotReadinessResponse, 11@@ -129,12 +130,12 @@ 12 use embedder_traits::resources::{self, Resource}; 13 use embedder_traits::user_contents::{UserContentManagerId, UserContents}; 14 use embedder_traits::{ 15- AnimationState, EmbedderControlId, EmbedderControlResponse, EmbedderMsg, EmbedderProxy, 16- FocusSequenceNumber, InputEvent, InputEventAndId, JSValue, JavaScriptEvaluationError, 17- JavaScriptEvaluationId, KeyboardEvent, MediaSessionActionType, MediaSessionEvent, 18- MediaSessionPlaybackState, MouseButton, MouseButtonAction, MouseButtonEvent, NewWebViewDetails, 19- PaintHitTestResult, Theme, ViewportDetails, WebDriverCommandMsg, WebDriverLoadStatus, 20- WebDriverScriptCommand, 21+ AnimationState, EmbedderControlId, EmbedderControlRequest, EmbedderControlResponse, 22+ EmbedderMsg, EmbedderProxy, FocusSequenceNumber, InputEvent, InputEventAndId, JSValue, 23+ JavaScriptEvaluationError, JavaScriptEvaluationId, KeyboardEvent, MediaSessionActionType, 24+ MediaSessionEvent, MediaSessionPlaybackState, MouseButton, MouseButtonAction, MouseButtonEvent, 25+ NewWebViewDetails, PaintHitTestResult, Theme, ViewportDetails, WebDriverCommandMsg, 26+ WebDriverLoadStatus, WebDriverScriptCommand, 27 }; 28 use euclid::Size2D; 29 use euclid::default::Size2D as UntypedSize2D; 30@@ -507,6 +508,19 @@ 31 /// to the `UserContents` need to be forwared to all the `ScriptThread`s that host 32 /// the relevant `WebView`. 33 pub(crate) user_contents_for_manager_id: FxHashMap<UserContentManagerId, UserContents>, 34+ 35+ /// Map from embedded WebView ID to the parent iframe's (browsing_context_id, pipeline_id). 36+ /// Used to route events from the embedded webview back to the iframe element. 37+ embedded_webview_to_iframe: FxHashMap<WebViewId, (BrowsingContextId, PipelineId)>, 38+ 39+ /// The embedded webview that currently has an active IME input focused. 40+ /// Set when EmbedderControlShow with InputMethod is received. 41+ /// Used by the virtual keyboard to route keystrokes to the correct webview. 42+ active_ime_webview: Option<WebViewId>, 43+ 44+ /// Set of script event loop IDs that have registered embedder error listeners. 45+ /// Only these event loops will receive DispatchServoError messages. 46+ embedder_error_listeners: FxHashSet<ScriptEventLoopId>, 47 } 48 49 /// State needed to construct a constellation. 50@@ -725,6 +739,9 @@ 51 pending_viewport_changes: Default::default(), 52 screenshot_readiness_requests: Vec::new(), 53 user_contents_for_manager_id: Default::default(), 54+ embedded_webview_to_iframe: FxHashMap::default(), 55+ active_ime_webview: None, 56+ embedder_error_listeners: Default::default(), 57 }; 58 59 constellation.run(); 60@@ -750,6 +767,18 @@ 61 fn clean_up_finished_script_event_loops(&mut self) { 62 self.event_loop_join_handles 63 .retain(|join_handle| !join_handle.is_finished()); 64+ 65+ // Clean up embedder error listeners for event loops that are being dropped. 66+ // We collect the IDs of event loops that are still alive, then remove any 67+ // listeners not in that set. 68+ let live_event_loop_ids: FxHashSet<_> = self 69+ .event_loops 70+ .iter() 71+ .filter_map(|weak| weak.upgrade().map(|el| el.id())) 72+ .collect(); 73+ self.embedder_error_listeners 74+ .retain(|id| live_event_loop_ids.contains(id)); 75+ 76 self.event_loops 77 .retain(|event_loop| event_loop.upgrade().is_some()); 78 } 79@@ -1041,6 +1070,11 @@ 80 .get(&webview_id) 81 .and_then(|webview| webview.user_content_manager_id); 82 83+ let hide_focus = self 84+ .webviews 85+ .get(&webview_id) 86+ .is_some_and(|webview| webview.hide_focus); 87+ 88 let new_pipeline_info = NewPipelineInfo { 89 parent_info: parent_pipeline_id, 90 new_pipeline_id, 91@@ -1051,6 +1085,13 @@ 92 viewport_details: initial_viewport_details, 93 user_content_manager_id, 94 theme, 95+ // Only set is_embedded_webview=true if this browsing context IS the embedded webview itself, 96+ // not for regular iframes inside the embedded webview that happen to share the same webview_id 97+ is_embedded_webview: self 98+ .embedded_webview_to_iframe 99+ .get(&webview_id) 100+ .is_some_and(|(embedded_bc_id, _)| *embedded_bc_id == browsing_context_id), 101+ hide_focus, 102 }; 103 let pipeline = match Pipeline::spawn(new_pipeline_info, event_loop, self, throttled) { 104 Ok(pipeline) => pipeline, 105@@ -1522,11 +1563,7 @@ 106 } 107 }, 108 EmbedderToConstellationMessage::PreferencesUpdated(updates) => { 109- let event_loops = self 110- .pipelines 111- .values() 112- .map(|pipeline| pipeline.event_loop.clone()); 113- for event_loop in event_loops { 114+ for event_loop in self.event_loops() { 115 let _ = event_loop.send(ScriptThreadMessage::PreferencesUpdated( 116 updates 117 .iter() 118@@ -1550,6 +1587,18 @@ 119 EmbedderToConstellationMessage::UpdatePinchZoomInfos(pipeline_id, pinch_zoom) => { 120 self.handle_update_pinch_zoom_infos(pipeline_id, pinch_zoom); 121 }, 122+ EmbedderToConstellationMessage::NotifyServoError(error_type, message) => { 123+ // Broadcast error only to script threads that have registered 124+ // embedder error listeners 125+ for event_loop in self.event_loops() { 126+ if self.embedder_error_listeners.contains(&event_loop.id()) { 127+ let _ = event_loop.send(ScriptThreadMessage::DispatchServoError( 128+ error_type.clone(), 129+ message.clone(), 130+ )); 131+ } 132+ } 133+ }, 134 } 135 } 136 137@@ -1773,6 +1822,12 @@ 138 self.broadcast_channels 139 .remove_broadcast_channel_router(router_id); 140 }, 141+ ScriptToConstellationMessage::RegisterEmbedderErrorListener(event_loop_id) => { 142+ self.embedder_error_listeners.insert(event_loop_id); 143+ }, 144+ ScriptToConstellationMessage::UnregisterEmbedderErrorListener(event_loop_id) => { 145+ self.embedder_error_listeners.remove(&event_loop_id); 146+ }, 147 ScriptToConstellationMessage::ScheduleBroadcast(router_id, message) => { 148 if self 149 .check_origin_against_pipeline(&source_pipeline_id, &message.origin) 150@@ -1803,6 +1858,12 @@ 151 ScriptToConstellationMessage::CreateAuxiliaryWebView(load_info) => { 152 self.handle_script_new_auxiliary(load_info); 153 }, 154+ ScriptToConstellationMessage::CreateEmbeddedWebView(request) => { 155+ self.handle_create_embedded_webview(request); 156+ }, 157+ ScriptToConstellationMessage::RemoveEmbeddedWebView(webview_id) => { 158+ self.handle_close_top_level_browsing_context(webview_id); 159+ }, 160 ScriptToConstellationMessage::ChangeRunningAnimationsState(animation_state) => { 161 self.handle_change_running_animations_state(source_pipeline_id, animation_state) 162 }, 163@@ -1969,6 +2030,23 @@ 164 new_value, 165 ); 166 }, 167+ ScriptToConstellationMessage::BroadcastPreferenceChange(name, value) => { 168+ // Broadcast preference change to all script threads 169+ let updates = vec![(name.clone(), value.clone())]; 170+ for event_loop in self.event_loops() { 171+ let _ = 172+ event_loop.send(ScriptThreadMessage::PreferencesUpdated(updates.clone())); 173+ } 174+ 175+ // For namespaced (embedder) preferences, also notify the embedder process 176+ // so it can invoke the registered setter callback to persist the preference. 177+ // This is essential in multiprocess mode where the setter callback is only 178+ // registered in the main (embedder) process. 179+ if name.contains('.') { 180+ self.embedder_proxy 181+ .send(EmbedderMsg::EmbedderPreferenceChanged(name, value)); 182+ } 183+ }, 184 ScriptToConstellationMessage::MediaSessionEvent(pipeline_id, event) => { 185 // Unlikely at this point, but we may receive events coming from 186 // different media sessions, so we set the active media session based 187@@ -2037,6 +2115,129 @@ 188 ScriptToConstellationMessage::RespondToScreenshotReadinessRequest(response) => { 189 self.handle_screenshot_readiness_response(source_pipeline_id, response); 190 }, 191+ ScriptToConstellationMessage::EmbeddedWebViewNotification(event) => { 192+ self.handle_embedded_webview_notification(webview_id, event); 193+ }, 194+ ScriptToConstellationMessage::EmbeddedWebViewLoad(embedded_webview_id, url) => { 195+ // Only allow if this is a valid embedded webview 196+ let ctx_id = BrowsingContextId::from(embedded_webview_id); 197+ let pipeline_id = match self.browsing_contexts.get(&ctx_id) { 198+ Some(ctx) => ctx.pipeline_id, 199+ None => { 200+ return warn!( 201+ "EmbeddedWebViewLoad for unknown browsing context {:?}", 202+ embedded_webview_id 203+ ); 204+ }, 205+ }; 206+ let load_data = LoadData::new_for_new_unrelated_webview(url); 207+ self.load_url( 208+ embedded_webview_id, 209+ pipeline_id, 210+ load_data, 211+ NavigationHistoryBehavior::Push, 212+ ); 213+ }, 214+ ScriptToConstellationMessage::EmbeddedWebViewReload(embedded_webview_id) => { 215+ // Only allow if this is a valid embedded webview 216+ if self.webviews.contains_key(&embedded_webview_id) { 217+ self.handle_reload_msg(embedded_webview_id); 218+ } else { 219+ warn!( 220+ "EmbeddedWebViewReload for unknown or non-embedded webview {:?}", 221+ embedded_webview_id 222+ ); 223+ } 224+ }, 225+ ScriptToConstellationMessage::EmbeddedWebViewTraverseHistory( 226+ embedded_webview_id, 227+ direction, 228+ traversal_id, 229+ ) => { 230+ // Only allow if this is a valid embedded webview 231+ if self.webviews.contains_key(&embedded_webview_id) { 232+ self.handle_traverse_history_msg(embedded_webview_id, direction); 233+ // Notify the embedded webview parent about traversal completion 234+ self.handle_embedded_webview_notification( 235+ embedded_webview_id, 236+ EmbeddedWebViewEventType::HistoryTraversalComplete(traversal_id), 237+ ); 238+ } else { 239+ warn!( 240+ "EmbeddedWebViewTraverseHistory for unknown or non-embedded webview {:?}", 241+ embedded_webview_id 242+ ); 243+ } 244+ }, 245+ ScriptToConstellationMessage::EmbeddedWebViewTakeScreenshot( 246+ embedded_webview_id, 247+ request, 248+ response_sender, 249+ ) => { 250+ // Only allow if this is a valid embedded webview 251+ if self.webviews.contains_key(&embedded_webview_id) { 252+ self.paint_proxy 253+ .cross_process_paint_api 254+ .request_encoded_screenshot(embedded_webview_id, request, response_sender); 255+ } else { 256+ let _ = response_sender.send(Err( 257+ embedder_traits::EmbeddedWebViewScreenshotError::WebViewDoesNotExist, 258+ )); 259+ } 260+ }, 261+ ScriptToConstellationMessage::ForwardEventToEmbeddedWebView( 262+ embedded_webview_id, 263+ event, 264+ ) => { 265+ // Forward the input event to the embedded webview via Paint. 266+ // This is called after the parent webview's script thread determined 267+ // via DOM hit testing that the event target is an embedded iframe. 268+ if self.webviews.contains_key(&embedded_webview_id) { 269+ self.paint_proxy 270+ .send(PaintMessage::ForwardInputEventToEmbeddedWebView( 271+ embedded_webview_id, 272+ event, 273+ )); 274+ } 275+ }, 276+ ScriptToConstellationMessage::EmbeddedWebViewControlResponse(id, response) => { 277+ // Route the control response to the embedded webview's script thread. 278+ // This is sent from the parent shell after user interaction with a custom control UI. 279+ self.handle_embedder_control_response(id, response); 280+ }, 281+ ScriptToConstellationMessage::EmbeddedWebViewSetPageZoom(embedded_webview_id, zoom) => { 282+ // Validate this is a known embedded webview 283+ let ctx_id = BrowsingContextId::from(embedded_webview_id); 284+ if self.browsing_contexts.get(&ctx_id).is_none() { 285+ return warn!( 286+ "EmbeddedWebViewSetPageZoom for unknown browsing context {:?}", 287+ embedded_webview_id 288+ ); 289+ } 290+ // Send to Paint component 291+ self.paint_proxy 292+ .send(PaintMessage::SetPageZoom(embedded_webview_id, zoom)); 293+ }, 294+ ScriptToConstellationMessage::InjectInputToActiveIme(event) => { 295+ // Route the input event to the webview that currently has an active IME input. 296+ // This is used by the virtual keyboard to send keystrokes to the focused input field. 297+ if let Some(target_webview_id) = self.active_ime_webview { 298+ self.forward_input_event(target_webview_id, event, None); 299+ } else { 300+ debug!("InjectInputToActiveIme called but no active IME webview is tracked"); 301+ } 302+ }, 303+ ScriptToConstellationMessage::SetActiveImeWebView(webview_id) => { 304+ // Set the active IME webview for virtual keyboard input routing. 305+ // Used when the system webview (non-embedded) shows an input method control. 306+ self.active_ime_webview = Some(webview_id); 307+ }, 308+ ScriptToConstellationMessage::ClearActiveImeWebView(webview_id) => { 309+ // Clear the active IME webview when the system webview hides an input method control. 310+ if self.active_ime_webview == Some(webview_id) { 311+ self.active_ime_webview = None; 312+ } 313+ }, 314 } 315 } 316 317@@ -3135,6 +3336,13 @@ 318 /// <https://html.spec.whatwg.org/multipage/#destroy-a-top-level-traversable> 319 fn handle_close_top_level_browsing_context(&mut self, webview_id: WebViewId) { 320 debug!("{webview_id}: Closing"); 321+ 322+ // Notify embedded webview parent before closing (if this is an embedded webview) 323+ self.handle_embedded_webview_notification(webview_id, EmbeddedWebViewEventType::Closed); 324+ 325+ // Clean up the embedded webview mapping 326+ self.embedded_webview_to_iframe.remove(&webview_id); 327+ 328 let browsing_context_id = BrowsingContextId::from(webview_id); 329 // Step 5. Remove traversable from the user agent's top-level traversable set. 330 let browsing_context = 331@@ -3411,8 +3619,27 @@ 332 opener_webview_id, 333 opener_pipeline_id, 334 response_sender, 335+ target_url, 336 } = load_info; 337 338+ // Check if the opener is an embedded webview 339+ if self 340+ .embedded_webview_to_iframe 341+ .contains_key(&opener_webview_id) 342+ { 343+ // For embedded webviews, create a new embedded webview and notify the parent 344+ // browserhtml shell to create an <iframe embed> that adopts it. 345+ self.handle_embedded_auxiliary( 346+ load_data, 347+ opener_webview_id, 348+ opener_pipeline_id, 349+ response_sender, 350+ target_url, 351+ ); 352+ return; 353+ } 354+ 355+ // For normal webviews, use the blocking flow 356 let Some((webview_id_sender, webview_id_receiver)) = generic_channel::channel() else { 357 warn!("Failed to create channel"); 358 let _ = response_sender.send(None); 359@@ -3510,6 +3737,359 @@ 360 }); 361 } 362 363+ /// Handle a request to create an embedded webview (from an iframe with "embed" attribute). 364+ /// This creates a new top-level WebView with its own WebViewId, similar to auxiliary webviews. 365+ fn handle_create_embedded_webview(&mut self, request: EmbeddedWebViewCreationRequest) { 366+ let EmbeddedWebViewCreationRequest { 367+ load_data, 368+ parent_pipeline_id, 369+ parent_webview_id, 370+ viewport_details, 371+ theme: _theme, 372+ hide_focus, 373+ response_sender, 374+ } = request; 375+ 376+ // Request a new WebView ID from the embedder using the embedded webview message. 377+ // Unlike regular popups, embedded webviews should not create new tabs in the UI. 378+ let Some((webview_id_sender, webview_id_receiver)) = generic_channel::channel() else { 379+ warn!("Failed to create channel for embedded webview"); 380+ let _ = response_sender.send(None); 381+ return; 382+ }; 383+ self.embedder_proxy 384+ .send(EmbedderMsg::AllowOpeningEmbeddedWebView( 385+ parent_webview_id, 386+ webview_id_sender, 387+ )); 388+ let NewWebViewDetails { 389+ webview_id: new_webview_id, 390+ viewport_details: embedded_viewport_details, 391+ user_content_manager_id, 392+ } = match webview_id_receiver.recv() { 393+ Ok(Some(new_webview_details)) => new_webview_details, 394+ Ok(None) | Err(_) => { 395+ debug!("Embedder rejected embedded webview creation"); 396+ let _ = response_sender.send(None); 397+ return; 398+ }, 399+ }; 400+ let new_browsing_context_id = BrowsingContextId::from(new_webview_id); 401+ 402+ let (script_sender, parent_browsing_context_id) = 403+ match self.pipelines.get(&parent_pipeline_id) { 404+ Some(pipeline) => (pipeline.event_loop.clone(), pipeline.browsing_context_id), 405+ None => { 406+ warn!( 407+ "{}: Embedded webview created in closed parent pipeline", 408+ parent_pipeline_id 409+ ); 410+ let _ = response_sender.send(None); 411+ return; 412+ }, 413+ }; 414+ let (is_parent_private, is_parent_throttled, is_parent_secure) = 415+ match self.browsing_contexts.get(&parent_browsing_context_id) { 416+ Some(ctx) => (ctx.is_private, ctx.throttled, ctx.inherited_secure_context), 417+ None => { 418+ warn!( 419+ "{}: Embedded webview {} created in closed parent browsing context", 420+ parent_browsing_context_id, new_browsing_context_id, 421+ ); 422+ let _ = response_sender.send(None); 423+ return; 424+ }, 425+ }; 426+ 427+ let new_pipeline_id = PipelineId::new(); 428+ let pipeline = Pipeline::new_already_spawned( 429+ new_pipeline_id, 430+ new_browsing_context_id, 431+ new_webview_id, 432+ None, // No opener for embedded webviews - they are isolated 433+ script_sender, 434+ self.paint_proxy.clone(), 435+ is_parent_throttled, 436+ load_data, 437+ ); 438+ 439+ // Send the response before adding to constellation state 440+ let _ = response_sender.send(Some(EmbeddedWebViewCreationResponse { 441+ new_webview_id, 442+ new_browsing_context_id, 443+ new_pipeline_id, 444+ user_content_manager_id, 445+ })); 446+ 447+ // Track this as an embedded webview 448+ self.embedded_webview_to_iframe.insert( 449+ new_webview_id, 450+ (new_browsing_context_id, parent_pipeline_id), 451+ ); 452+ 453+ assert!(!self.pipelines.contains_key(&new_pipeline_id)); 454+ self.pipelines.insert(new_pipeline_id, pipeline); 455+ self.webviews.insert( 456+ new_webview_id, 457+ ConstellationWebView::new_with_hide_focus( 458+ new_webview_id, 459+ new_browsing_context_id, 460+ user_content_manager_id, 461+ hide_focus, 462+ ), 463+ ); 464+ 465+ // Create a new browsing context group for embedded webviews (they are fully isolated) 466+ let bc_group_id = BrowsingContextGroupId(self.browsing_context_group_next_id); 467+ self.browsing_context_group_next_id += 1; 468+ let mut bc_group = BrowsingContextGroup::default(); 469+ bc_group 470+ .top_level_browsing_context_set 471+ .insert(new_webview_id); 472+ self.browsing_context_group_set 473+ .insert(bc_group_id, bc_group); 474+ 475+ // Use the viewport details from the request if the embedder didn't provide specific ones 476+ let final_viewport_details = if embedded_viewport_details.size.width > 0.0 { 477+ embedded_viewport_details 478+ } else { 479+ viewport_details 480+ }; 481+ 482+ self.add_pending_change(SessionHistoryChange { 483+ webview_id: new_webview_id, 484+ browsing_context_id: new_browsing_context_id, 485+ new_pipeline_id, 486+ replace: None, 487+ new_browsing_context_info: Some(NewBrowsingContextInfo { 488+ // Embedded webviews need parent_pipeline_id for rendering hierarchy, 489+ // but they are treated as top-level for window.parent purposes. 490+ parent_pipeline_id: Some(parent_pipeline_id), 491+ is_private: is_parent_private, 492+ inherited_secure_context: is_parent_secure, 493+ throttled: is_parent_throttled, 494+ }), 495+ viewport_details: final_viewport_details, 496+ }); 497+ 498+ debug!( 499+ "Created embedded webview {} with pipeline {} for iframe in {}", 500+ new_webview_id, new_pipeline_id, parent_pipeline_id 501+ ); 502+ } 503+ 504+ /// Handle a `window.open()` request from an embedded webview. 505+ /// This creates a new embedded webview and notifies the parent browserhtml shell 506+ /// to create an `<iframe embed>` that adopts the pre-created webview. 507+ #[allow(clippy::too_many_arguments)] 508+ fn handle_embedded_auxiliary( 509+ &mut self, 510+ load_data: LoadData, 511+ opener_webview_id: WebViewId, 512+ opener_pipeline_id: PipelineId, 513+ response_sender: GenericSender<Option<AuxiliaryWebViewCreationResponse>>, 514+ target_url: Option<ServoUrl>, 515+ ) { 516+ // Find the parent webview (browserhtml shell) that contains this embedded webview 517+ let Some(&(_, parent_pipeline_id)) = 518+ self.embedded_webview_to_iframe.get(&opener_webview_id) 519+ else { 520+ warn!( 521+ "Cannot find parent pipeline for embedded webview {}", 522+ opener_webview_id 523+ ); 524+ let _ = response_sender.send(None); 525+ return; 526+ }; 527+ 528+ let Some(parent_pipeline) = self.pipelines.get(&parent_pipeline_id) else { 529+ warn!( 530+ "Parent pipeline {} not found for embedded webview {}", 531+ parent_pipeline_id, opener_webview_id 532+ ); 533+ let _ = response_sender.send(None); 534+ return; 535+ }; 536+ let parent_webview_id = parent_pipeline.webview_id; 537+ 538+ // Request a new WebView ID from the embedder 539+ let Some((webview_id_sender, webview_id_receiver)) = generic_channel::channel() else { 540+ warn!("Failed to create channel for embedded auxiliary webview"); 541+ let _ = response_sender.send(None); 542+ return; 543+ }; 544+ self.embedder_proxy 545+ .send(EmbedderMsg::AllowOpeningEmbeddedWebView( 546+ parent_webview_id, 547+ webview_id_sender, 548+ )); 549+ let NewWebViewDetails { 550+ webview_id: new_webview_id, 551+ viewport_details, 552+ user_content_manager_id, 553+ } = match webview_id_receiver.recv() { 554+ Ok(Some(new_webview_details)) => new_webview_details, 555+ Ok(None) | Err(_) => { 556+ let _ = response_sender.send(None); 557+ return; 558+ }, 559+ }; 560+ let new_browsing_context_id = BrowsingContextId::from(new_webview_id); 561+ 562+ // Get opener pipeline info 563+ let (script_sender, opener_browsing_context_id) = 564+ match self.pipelines.get(&opener_pipeline_id) { 565+ Some(pipeline) => (pipeline.event_loop.clone(), pipeline.browsing_context_id), 566+ None => { 567+ warn!( 568+ "{}: Embedded auxiliary loaded url in closed pipeline", 569+ opener_pipeline_id 570+ ); 571+ let _ = response_sender.send(None); 572+ return; 573+ }, 574+ }; 575+ let (is_opener_private, is_opener_throttled, is_opener_secure) = 576+ match self.browsing_contexts.get(&opener_browsing_context_id) { 577+ Some(ctx) => (ctx.is_private, ctx.throttled, ctx.inherited_secure_context), 578+ None => { 579+ warn!( 580+ "{}: Embedded auxiliary {} loaded in closed opener browsing context", 581+ opener_browsing_context_id, new_browsing_context_id, 582+ ); 583+ let _ = response_sender.send(None); 584+ return; 585+ }, 586+ }; 587+ 588+ let new_pipeline_id = PipelineId::new(); 589+ // Capture the URL before load_data is moved into the pipeline 590+ let load_url = target_url.unwrap_or_else(|| load_data.url.clone()); 591+ let pipeline = Pipeline::new_already_spawned( 592+ new_pipeline_id, 593+ new_browsing_context_id, 594+ new_webview_id, 595+ Some(opener_browsing_context_id), // Set opener for window.opener support 596+ script_sender, 597+ self.paint_proxy.clone(), 598+ is_opener_throttled, 599+ load_data, 600+ ); 601+ 602+ // Send the response to script so window.open() returns a proper WindowProxy 603+ let _ = response_sender.send(Some(AuxiliaryWebViewCreationResponse { 604+ new_webview_id, 605+ new_pipeline_id, 606+ user_content_manager_id, 607+ })); 608+ 609+ // Track this as an embedded webview (inheriting from the opener's embedded status) 610+ self.embedded_webview_to_iframe.insert( 611+ new_webview_id, 612+ (new_browsing_context_id, parent_pipeline_id), 613+ ); 614+ 615+ assert!(!self.pipelines.contains_key(&new_pipeline_id)); 616+ self.pipelines.insert(new_pipeline_id, pipeline); 617+ self.webviews.insert( 618+ new_webview_id, 619+ ConstellationWebView::new( 620+ new_webview_id, 621+ new_browsing_context_id, 622+ user_content_manager_id, 623+ ), 624+ ); 625+ 626+ // Create a new browsing context group for the new embedded webview 627+ let bc_group_id = BrowsingContextGroupId(self.browsing_context_group_next_id); 628+ self.browsing_context_group_next_id += 1; 629+ let mut bc_group = BrowsingContextGroup::default(); 630+ bc_group 631+ .top_level_browsing_context_set 632+ .insert(new_webview_id); 633+ self.browsing_context_group_set 634+ .insert(bc_group_id, bc_group); 635+ 636+ self.add_pending_change(SessionHistoryChange { 637+ webview_id: new_webview_id, 638+ browsing_context_id: new_browsing_context_id, 639+ new_pipeline_id, 640+ replace: None, 641+ new_browsing_context_info: Some(NewBrowsingContextInfo { 642+ parent_pipeline_id: Some(parent_pipeline_id), 643+ is_private: is_opener_private, 644+ inherited_secure_context: is_opener_secure, 645+ throttled: is_opener_throttled, 646+ }), 647+ viewport_details, 648+ }); 649+ 650+ // Notify the parent browserhtml shell to create an <iframe embed> that adopts this webview 651+ self.embedder_proxy 652+ .send(EmbedderMsg::EmbeddedWebViewCreated( 653+ parent_webview_id, 654+ new_webview_id, 655+ new_browsing_context_id, 656+ new_pipeline_id, 657+ load_url, 658+ )); 659+ 660+ debug!( 661+ "Created embedded auxiliary webview {} with pipeline {} from opener {}", 662+ new_webview_id, new_pipeline_id, opener_webview_id 663+ ); 664+ } 665+ 666+ /// Handle a notification from an embedded webview that should be forwarded 667+ /// to its parent iframe element as a DOM event. 668+ fn handle_embedded_webview_notification( 669+ &mut self, 670+ webview_id: WebViewId, 671+ event: EmbeddedWebViewEventType, 672+ ) { 673+ // Track the active IME webview for virtual keyboard input routing 674+ match &event { 675+ EmbeddedWebViewEventType::EmbedderControlShow { request, .. } => { 676+ if matches!(request, EmbedderControlRequest::InputMethod { .. }) { 677+ self.active_ime_webview = Some(webview_id); 678+ } 679+ }, 680+ EmbeddedWebViewEventType::EmbedderControlHide { .. } => { 681+ // Clear the active IME webview when any control is hidden 682+ // (we could track control IDs to be more precise, but this is simpler) 683+ if self.active_ime_webview == Some(webview_id) { 684+ self.active_ime_webview = None; 685+ } 686+ }, 687+ _ => {}, 688+ } 689+ 690+ // Check if this webview is embedded 691+ let Some(&(browsing_context_id, parent_pipeline_id)) = 692+ self.embedded_webview_to_iframe.get(&webview_id) 693+ else { 694+ // Not an embedded webview, ignore the notification 695+ return; 696+ }; 697+ 698+ // Forward to parent pipeline's script thread 699+ let Some(pipeline) = self.pipelines.get(&parent_pipeline_id) else { 700+ warn!( 701+ "Parent pipeline {:?} not found for embedded webview {:?}", 702+ parent_pipeline_id, webview_id 703+ ); 704+ return; 705+ }; 706+ 707+ let _ = pipeline 708+ .event_loop 709+ .send(ScriptThreadMessage::DispatchEmbeddedWebViewEvent { 710+ target: browsing_context_id, 711+ parent: parent_pipeline_id, 712+ event, 713+ }); 714+ } 715+ 716 #[servo_tracing::instrument(skip_all)] 717 fn handle_refresh_cursor(&self, pipeline_id: PipelineId) { 718 let Some(pipeline) = self.pipelines.get(&pipeline_id) else { 719@@ -4632,7 +5212,7 @@ 720 } 721 722 #[servo_tracing::instrument(skip_all)] 723- fn notify_history_changed(&self, webview_id: WebViewId) { 724+ fn notify_history_changed(&mut self, webview_id: WebViewId) { 725 // Send a flat projection of the history to embedder. 726 // The final vector is a concatenation of the URLs of the past 727 // entries, the current entry and the future entries. 728@@ -4735,9 +5315,23 @@ 729 ); 730 self.embedder_proxy.send(EmbedderMsg::HistoryChanged( 731 webview_id, 732- entries, 733+ entries.clone(), 734 current_index, 735 )); 736+ 737+ // Notify embedded webview parent if this is an embedded webview 738+ // First send the full history for tracking can_go_back/can_go_forward 739+ self.handle_embedded_webview_notification( 740+ webview_id, 741+ EmbeddedWebViewEventType::HistoryChanged(entries.clone(), current_index), 742+ ); 743+ // Then send the URL change event 744+ if let Some(current_url) = entries.get(current_index) { 745+ self.handle_embedded_webview_notification( 746+ webview_id, 747+ EmbeddedWebViewEventType::UrlChanged(current_url.clone()), 748+ ); 749+ } 750 } 751 752 #[servo_tracing::instrument(skip_all)]