forked from
me.webbeef.org/browser.html
Rewild Your Web
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)]