Rewild Your Web
web
browser
dweb
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)]