Rewild Your Web
at main 754 lines 34 kB view raw
1--- original 2+++ modified 3@@ -116,6 +116,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@@ -131,12 +132,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@@ -510,6 +511,19 @@ 31 32 /// Whether accessibility trees are being built and sent to the underlying platform. 33 pub(crate) accessibility_active: bool, 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@@ -729,6 +743,9 @@ 51 screenshot_readiness_requests: Vec::new(), 52 user_contents_for_manager_id: Default::default(), 53 accessibility_active: false, 54+ embedded_webview_to_iframe: FxHashMap::default(), 55+ active_ime_webview: None, 56+ embedder_error_listeners: Default::default(), 57 }; 58 59 constellation.run(); 60@@ -754,6 +771,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@@ -1045,6 +1074,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@@ -1055,6 +1089,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@@ -1534,11 +1575,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@@ -1565,6 +1602,18 @@ 119 EmbedderToConstellationMessage::SetAccessibilityActive(active) => { 120 self.set_accessibility_active(active); 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@@ -1788,6 +1837,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@@ -1818,6 +1873,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@@ -1984,6 +2045,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@@ -2057,6 +2135,129 @@ 188 let _ = event_loop.send(ScriptThreadMessage::TriggerGarbageCollection); 189 } 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@@ -3174,6 +3375,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@@ -3450,8 +3658,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@@ -3550,6 +3777,361 @@ 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_pipeline_id, 460+ new_browsing_context_id, 461+ user_content_manager_id, 462+ hide_focus, 463+ ), 464+ ); 465+ 466+ // Create a new browsing context group for embedded webviews (they are fully isolated) 467+ let bc_group_id = BrowsingContextGroupId(self.browsing_context_group_next_id); 468+ self.browsing_context_group_next_id += 1; 469+ let mut bc_group = BrowsingContextGroup::default(); 470+ bc_group 471+ .top_level_browsing_context_set 472+ .insert(new_webview_id); 473+ self.browsing_context_group_set 474+ .insert(bc_group_id, bc_group); 475+ 476+ // Use the viewport details from the request if the embedder didn't provide specific ones 477+ let final_viewport_details = if embedded_viewport_details.size.width > 0.0 { 478+ embedded_viewport_details 479+ } else { 480+ viewport_details 481+ }; 482+ 483+ self.add_pending_change(SessionHistoryChange { 484+ webview_id: new_webview_id, 485+ browsing_context_id: new_browsing_context_id, 486+ new_pipeline_id, 487+ replace: None, 488+ new_browsing_context_info: Some(NewBrowsingContextInfo { 489+ // Embedded webviews need parent_pipeline_id for rendering hierarchy, 490+ // but they are treated as top-level for window.parent purposes. 491+ parent_pipeline_id: Some(parent_pipeline_id), 492+ is_private: is_parent_private, 493+ inherited_secure_context: is_parent_secure, 494+ throttled: is_parent_throttled, 495+ }), 496+ viewport_details: final_viewport_details, 497+ }); 498+ 499+ debug!( 500+ "Created embedded webview {} with pipeline {} for iframe in {}", 501+ new_webview_id, new_pipeline_id, parent_pipeline_id 502+ ); 503+ } 504+ 505+ /// Handle a `window.open()` request from an embedded webview. 506+ /// This creates a new embedded webview and notifies the parent browserhtml shell 507+ /// to create an `<iframe embed>` that adopts the pre-created webview. 508+ #[allow(clippy::too_many_arguments)] 509+ fn handle_embedded_auxiliary( 510+ &mut self, 511+ load_data: LoadData, 512+ opener_webview_id: WebViewId, 513+ opener_pipeline_id: PipelineId, 514+ response_sender: GenericSender<Option<AuxiliaryWebViewCreationResponse>>, 515+ target_url: Option<ServoUrl>, 516+ ) { 517+ // Find the parent webview (browserhtml shell) that contains this embedded webview 518+ let Some(&(_, parent_pipeline_id)) = 519+ self.embedded_webview_to_iframe.get(&opener_webview_id) 520+ else { 521+ warn!( 522+ "Cannot find parent pipeline for embedded webview {}", 523+ opener_webview_id 524+ ); 525+ let _ = response_sender.send(None); 526+ return; 527+ }; 528+ 529+ let Some(parent_pipeline) = self.pipelines.get(&parent_pipeline_id) else { 530+ warn!( 531+ "Parent pipeline {} not found for embedded webview {}", 532+ parent_pipeline_id, opener_webview_id 533+ ); 534+ let _ = response_sender.send(None); 535+ return; 536+ }; 537+ let parent_webview_id = parent_pipeline.webview_id; 538+ 539+ // Request a new WebView ID from the embedder 540+ let Some((webview_id_sender, webview_id_receiver)) = generic_channel::channel() else { 541+ warn!("Failed to create channel for embedded auxiliary webview"); 542+ let _ = response_sender.send(None); 543+ return; 544+ }; 545+ self.embedder_proxy 546+ .send(EmbedderMsg::AllowOpeningEmbeddedWebView( 547+ parent_webview_id, 548+ webview_id_sender, 549+ )); 550+ let NewWebViewDetails { 551+ webview_id: new_webview_id, 552+ viewport_details, 553+ user_content_manager_id, 554+ } = match webview_id_receiver.recv() { 555+ Ok(Some(new_webview_details)) => new_webview_details, 556+ Ok(None) | Err(_) => { 557+ let _ = response_sender.send(None); 558+ return; 559+ }, 560+ }; 561+ let new_browsing_context_id = BrowsingContextId::from(new_webview_id); 562+ 563+ // Get opener pipeline info 564+ let (script_sender, opener_browsing_context_id) = 565+ match self.pipelines.get(&opener_pipeline_id) { 566+ Some(pipeline) => (pipeline.event_loop.clone(), pipeline.browsing_context_id), 567+ None => { 568+ warn!( 569+ "{}: Embedded auxiliary loaded url in closed pipeline", 570+ opener_pipeline_id 571+ ); 572+ let _ = response_sender.send(None); 573+ return; 574+ }, 575+ }; 576+ let (is_opener_private, is_opener_throttled, is_opener_secure) = 577+ match self.browsing_contexts.get(&opener_browsing_context_id) { 578+ Some(ctx) => (ctx.is_private, ctx.throttled, ctx.inherited_secure_context), 579+ None => { 580+ warn!( 581+ "{}: Embedded auxiliary {} loaded in closed opener browsing context", 582+ opener_browsing_context_id, new_browsing_context_id, 583+ ); 584+ let _ = response_sender.send(None); 585+ return; 586+ }, 587+ }; 588+ 589+ let new_pipeline_id = PipelineId::new(); 590+ // Capture the URL before load_data is moved into the pipeline 591+ let load_url = target_url.unwrap_or_else(|| load_data.url.clone()); 592+ let pipeline = Pipeline::new_already_spawned( 593+ new_pipeline_id, 594+ new_browsing_context_id, 595+ new_webview_id, 596+ Some(opener_browsing_context_id), // Set opener for window.opener support 597+ script_sender, 598+ self.paint_proxy.clone(), 599+ is_opener_throttled, 600+ load_data, 601+ ); 602+ 603+ // Send the response to script so window.open() returns a proper WindowProxy 604+ let _ = response_sender.send(Some(AuxiliaryWebViewCreationResponse { 605+ new_webview_id, 606+ new_pipeline_id, 607+ user_content_manager_id, 608+ })); 609+ 610+ // Track this as an embedded webview (inheriting from the opener's embedded status) 611+ self.embedded_webview_to_iframe.insert( 612+ new_webview_id, 613+ (new_browsing_context_id, parent_pipeline_id), 614+ ); 615+ 616+ assert!(!self.pipelines.contains_key(&new_pipeline_id)); 617+ self.pipelines.insert(new_pipeline_id, pipeline); 618+ self.webviews.insert( 619+ new_webview_id, 620+ ConstellationWebView::new( 621+ new_webview_id, 622+ new_pipeline_id, 623+ new_browsing_context_id, 624+ user_content_manager_id, 625+ ), 626+ ); 627+ 628+ // Create a new browsing context group for the new embedded webview 629+ let bc_group_id = BrowsingContextGroupId(self.browsing_context_group_next_id); 630+ self.browsing_context_group_next_id += 1; 631+ let mut bc_group = BrowsingContextGroup::default(); 632+ bc_group 633+ .top_level_browsing_context_set 634+ .insert(new_webview_id); 635+ self.browsing_context_group_set 636+ .insert(bc_group_id, bc_group); 637+ 638+ self.add_pending_change(SessionHistoryChange { 639+ webview_id: new_webview_id, 640+ browsing_context_id: new_browsing_context_id, 641+ new_pipeline_id, 642+ replace: None, 643+ new_browsing_context_info: Some(NewBrowsingContextInfo { 644+ parent_pipeline_id: Some(parent_pipeline_id), 645+ is_private: is_opener_private, 646+ inherited_secure_context: is_opener_secure, 647+ throttled: is_opener_throttled, 648+ }), 649+ viewport_details, 650+ }); 651+ 652+ // Notify the parent browserhtml shell to create an <iframe embed> that adopts this webview 653+ self.embedder_proxy 654+ .send(EmbedderMsg::EmbeddedWebViewCreated( 655+ parent_webview_id, 656+ new_webview_id, 657+ new_browsing_context_id, 658+ new_pipeline_id, 659+ load_url, 660+ )); 661+ 662+ debug!( 663+ "Created embedded auxiliary webview {} with pipeline {} from opener {}", 664+ new_webview_id, new_pipeline_id, opener_webview_id 665+ ); 666+ } 667+ 668+ /// Handle a notification from an embedded webview that should be forwarded 669+ /// to its parent iframe element as a DOM event. 670+ fn handle_embedded_webview_notification( 671+ &mut self, 672+ webview_id: WebViewId, 673+ event: EmbeddedWebViewEventType, 674+ ) { 675+ // Track the active IME webview for virtual keyboard input routing 676+ match &event { 677+ EmbeddedWebViewEventType::EmbedderControlShow { request, .. } => { 678+ if matches!(request, EmbedderControlRequest::InputMethod { .. }) { 679+ self.active_ime_webview = Some(webview_id); 680+ } 681+ }, 682+ EmbeddedWebViewEventType::EmbedderControlHide { .. } => { 683+ // Clear the active IME webview when any control is hidden 684+ // (we could track control IDs to be more precise, but this is simpler) 685+ if self.active_ime_webview == Some(webview_id) { 686+ self.active_ime_webview = None; 687+ } 688+ }, 689+ _ => {}, 690+ } 691+ 692+ // Check if this webview is embedded 693+ let Some(&(browsing_context_id, parent_pipeline_id)) = 694+ self.embedded_webview_to_iframe.get(&webview_id) 695+ else { 696+ // Not an embedded webview, ignore the notification 697+ return; 698+ }; 699+ 700+ // Forward to parent pipeline's script thread 701+ let Some(pipeline) = self.pipelines.get(&parent_pipeline_id) else { 702+ warn!( 703+ "Parent pipeline {:?} not found for embedded webview {:?}", 704+ parent_pipeline_id, webview_id 705+ ); 706+ return; 707+ }; 708+ 709+ let _ = pipeline 710+ .event_loop 711+ .send(ScriptThreadMessage::DispatchEmbeddedWebViewEvent { 712+ target: browsing_context_id, 713+ parent: parent_pipeline_id, 714+ event, 715+ }); 716+ } 717+ 718 #[servo_tracing::instrument(skip_all)] 719 fn handle_refresh_cursor(&self, pipeline_id: PipelineId) { 720 let Some(pipeline) = self.pipelines.get(&pipeline_id) else { 721@@ -4676,7 +5258,7 @@ 722 } 723 724 #[servo_tracing::instrument(skip_all)] 725- fn notify_history_changed(&self, webview_id: WebViewId) { 726+ fn notify_history_changed(&mut self, webview_id: WebViewId) { 727 // Send a flat projection of the history to embedder. 728 // The final vector is a concatenation of the URLs of the past 729 // entries, the current entry and the future entries. 730@@ -4779,9 +5361,23 @@ 731 ); 732 self.embedder_proxy.send(EmbedderMsg::HistoryChanged( 733 webview_id, 734- entries, 735+ entries.clone(), 736 current_index, 737 )); 738+ 739+ // Notify embedded webview parent if this is an embedded webview 740+ // First send the full history for tracking can_go_back/can_go_forward 741+ self.handle_embedded_webview_notification( 742+ webview_id, 743+ EmbeddedWebViewEventType::HistoryChanged(entries.clone(), current_index), 744+ ); 745+ // Then send the URL change event 746+ if let Some(current_url) = entries.get(current_index) { 747+ self.handle_embedded_webview_notification( 748+ webview_id, 749+ EmbeddedWebViewEventType::UrlChanged(current_url.clone()), 750+ ); 751+ } 752 } 753 754 #[servo_tracing::instrument(skip_all)]