--- original +++ modified @@ -114,6 +114,7 @@ use canvas_traits::webgl::WebGLThreads; use constellation_traits::{ AuxiliaryWebViewCreationRequest, AuxiliaryWebViewCreationResponse, DocumentState, + EmbeddedWebViewCreationRequest, EmbeddedWebViewCreationResponse, EmbeddedWebViewEventType, EmbedderToConstellationMessage, IFrameLoadInfo, IFrameLoadInfoWithData, IFrameSizeMsg, Job, LoadData, LogEntry, MessagePortMsg, NavigationHistoryBehavior, PaintMetricEvent, PortMessageTask, PortTransferInfo, SWManagerMsg, SWManagerSenders, ScreenshotReadinessResponse, @@ -129,12 +130,12 @@ use embedder_traits::resources::{self, Resource}; use embedder_traits::user_contents::{UserContentManagerId, UserContents}; use embedder_traits::{ - AnimationState, EmbedderControlId, EmbedderControlResponse, EmbedderMsg, EmbedderProxy, - FocusSequenceNumber, InputEvent, InputEventAndId, JSValue, JavaScriptEvaluationError, - JavaScriptEvaluationId, KeyboardEvent, MediaSessionActionType, MediaSessionEvent, - MediaSessionPlaybackState, MouseButton, MouseButtonAction, MouseButtonEvent, NewWebViewDetails, - PaintHitTestResult, Theme, ViewportDetails, WebDriverCommandMsg, WebDriverLoadStatus, - WebDriverScriptCommand, + AnimationState, EmbedderControlId, EmbedderControlRequest, EmbedderControlResponse, + EmbedderMsg, EmbedderProxy, FocusSequenceNumber, InputEvent, InputEventAndId, JSValue, + JavaScriptEvaluationError, JavaScriptEvaluationId, KeyboardEvent, MediaSessionActionType, + MediaSessionEvent, MediaSessionPlaybackState, MouseButton, MouseButtonAction, MouseButtonEvent, + NewWebViewDetails, PaintHitTestResult, Theme, ViewportDetails, WebDriverCommandMsg, + WebDriverLoadStatus, WebDriverScriptCommand, }; use euclid::Size2D; use euclid::default::Size2D as UntypedSize2D; @@ -507,6 +508,19 @@ /// to the `UserContents` need to be forwared to all the `ScriptThread`s that host /// the relevant `WebView`. pub(crate) user_contents_for_manager_id: FxHashMap, + + /// Map from embedded WebView ID to the parent iframe's (browsing_context_id, pipeline_id). + /// Used to route events from the embedded webview back to the iframe element. + embedded_webview_to_iframe: FxHashMap, + + /// The embedded webview that currently has an active IME input focused. + /// Set when EmbedderControlShow with InputMethod is received. + /// Used by the virtual keyboard to route keystrokes to the correct webview. + active_ime_webview: Option, + + /// Set of script event loop IDs that have registered embedder error listeners. + /// Only these event loops will receive DispatchServoError messages. + embedder_error_listeners: FxHashSet, } /// State needed to construct a constellation. @@ -725,6 +739,9 @@ pending_viewport_changes: Default::default(), screenshot_readiness_requests: Vec::new(), user_contents_for_manager_id: Default::default(), + embedded_webview_to_iframe: FxHashMap::default(), + active_ime_webview: None, + embedder_error_listeners: Default::default(), }; constellation.run(); @@ -750,6 +767,18 @@ fn clean_up_finished_script_event_loops(&mut self) { self.event_loop_join_handles .retain(|join_handle| !join_handle.is_finished()); + + // Clean up embedder error listeners for event loops that are being dropped. + // We collect the IDs of event loops that are still alive, then remove any + // listeners not in that set. + let live_event_loop_ids: FxHashSet<_> = self + .event_loops + .iter() + .filter_map(|weak| weak.upgrade().map(|el| el.id())) + .collect(); + self.embedder_error_listeners + .retain(|id| live_event_loop_ids.contains(id)); + self.event_loops .retain(|event_loop| event_loop.upgrade().is_some()); } @@ -1041,6 +1070,11 @@ .get(&webview_id) .and_then(|webview| webview.user_content_manager_id); + let hide_focus = self + .webviews + .get(&webview_id) + .is_some_and(|webview| webview.hide_focus); + let new_pipeline_info = NewPipelineInfo { parent_info: parent_pipeline_id, new_pipeline_id, @@ -1051,6 +1085,13 @@ viewport_details: initial_viewport_details, user_content_manager_id, theme, + // Only set is_embedded_webview=true if this browsing context IS the embedded webview itself, + // not for regular iframes inside the embedded webview that happen to share the same webview_id + is_embedded_webview: self + .embedded_webview_to_iframe + .get(&webview_id) + .is_some_and(|(embedded_bc_id, _)| *embedded_bc_id == browsing_context_id), + hide_focus, }; let pipeline = match Pipeline::spawn(new_pipeline_info, event_loop, self, throttled) { Ok(pipeline) => pipeline, @@ -1522,11 +1563,7 @@ } }, EmbedderToConstellationMessage::PreferencesUpdated(updates) => { - let event_loops = self - .pipelines - .values() - .map(|pipeline| pipeline.event_loop.clone()); - for event_loop in event_loops { + for event_loop in self.event_loops() { let _ = event_loop.send(ScriptThreadMessage::PreferencesUpdated( updates .iter() @@ -1550,6 +1587,18 @@ EmbedderToConstellationMessage::UpdatePinchZoomInfos(pipeline_id, pinch_zoom) => { self.handle_update_pinch_zoom_infos(pipeline_id, pinch_zoom); }, + EmbedderToConstellationMessage::NotifyServoError(error_type, message) => { + // Broadcast error only to script threads that have registered + // embedder error listeners + for event_loop in self.event_loops() { + if self.embedder_error_listeners.contains(&event_loop.id()) { + let _ = event_loop.send(ScriptThreadMessage::DispatchServoError( + error_type.clone(), + message.clone(), + )); + } + } + }, } } @@ -1773,6 +1822,12 @@ self.broadcast_channels .remove_broadcast_channel_router(router_id); }, + ScriptToConstellationMessage::RegisterEmbedderErrorListener(event_loop_id) => { + self.embedder_error_listeners.insert(event_loop_id); + }, + ScriptToConstellationMessage::UnregisterEmbedderErrorListener(event_loop_id) => { + self.embedder_error_listeners.remove(&event_loop_id); + }, ScriptToConstellationMessage::ScheduleBroadcast(router_id, message) => { if self .check_origin_against_pipeline(&source_pipeline_id, &message.origin) @@ -1803,6 +1858,12 @@ ScriptToConstellationMessage::CreateAuxiliaryWebView(load_info) => { self.handle_script_new_auxiliary(load_info); }, + ScriptToConstellationMessage::CreateEmbeddedWebView(request) => { + self.handle_create_embedded_webview(request); + }, + ScriptToConstellationMessage::RemoveEmbeddedWebView(webview_id) => { + self.handle_close_top_level_browsing_context(webview_id); + }, ScriptToConstellationMessage::ChangeRunningAnimationsState(animation_state) => { self.handle_change_running_animations_state(source_pipeline_id, animation_state) }, @@ -1969,6 +2030,23 @@ new_value, ); }, + ScriptToConstellationMessage::BroadcastPreferenceChange(name, value) => { + // Broadcast preference change to all script threads + let updates = vec![(name.clone(), value.clone())]; + for event_loop in self.event_loops() { + let _ = + event_loop.send(ScriptThreadMessage::PreferencesUpdated(updates.clone())); + } + + // For namespaced (embedder) preferences, also notify the embedder process + // so it can invoke the registered setter callback to persist the preference. + // This is essential in multiprocess mode where the setter callback is only + // registered in the main (embedder) process. + if name.contains('.') { + self.embedder_proxy + .send(EmbedderMsg::EmbedderPreferenceChanged(name, value)); + } + }, ScriptToConstellationMessage::MediaSessionEvent(pipeline_id, event) => { // Unlikely at this point, but we may receive events coming from // different media sessions, so we set the active media session based @@ -2037,6 +2115,129 @@ ScriptToConstellationMessage::RespondToScreenshotReadinessRequest(response) => { self.handle_screenshot_readiness_response(source_pipeline_id, response); }, + ScriptToConstellationMessage::EmbeddedWebViewNotification(event) => { + self.handle_embedded_webview_notification(webview_id, event); + }, + ScriptToConstellationMessage::EmbeddedWebViewLoad(embedded_webview_id, url) => { + // Only allow if this is a valid embedded webview + let ctx_id = BrowsingContextId::from(embedded_webview_id); + let pipeline_id = match self.browsing_contexts.get(&ctx_id) { + Some(ctx) => ctx.pipeline_id, + None => { + return warn!( + "EmbeddedWebViewLoad for unknown browsing context {:?}", + embedded_webview_id + ); + }, + }; + let load_data = LoadData::new_for_new_unrelated_webview(url); + self.load_url( + embedded_webview_id, + pipeline_id, + load_data, + NavigationHistoryBehavior::Push, + ); + }, + ScriptToConstellationMessage::EmbeddedWebViewReload(embedded_webview_id) => { + // Only allow if this is a valid embedded webview + if self.webviews.contains_key(&embedded_webview_id) { + self.handle_reload_msg(embedded_webview_id); + } else { + warn!( + "EmbeddedWebViewReload for unknown or non-embedded webview {:?}", + embedded_webview_id + ); + } + }, + ScriptToConstellationMessage::EmbeddedWebViewTraverseHistory( + embedded_webview_id, + direction, + traversal_id, + ) => { + // Only allow if this is a valid embedded webview + if self.webviews.contains_key(&embedded_webview_id) { + self.handle_traverse_history_msg(embedded_webview_id, direction); + // Notify the embedded webview parent about traversal completion + self.handle_embedded_webview_notification( + embedded_webview_id, + EmbeddedWebViewEventType::HistoryTraversalComplete(traversal_id), + ); + } else { + warn!( + "EmbeddedWebViewTraverseHistory for unknown or non-embedded webview {:?}", + embedded_webview_id + ); + } + }, + ScriptToConstellationMessage::EmbeddedWebViewTakeScreenshot( + embedded_webview_id, + request, + response_sender, + ) => { + // Only allow if this is a valid embedded webview + if self.webviews.contains_key(&embedded_webview_id) { + self.paint_proxy + .cross_process_paint_api + .request_encoded_screenshot(embedded_webview_id, request, response_sender); + } else { + let _ = response_sender.send(Err( + embedder_traits::EmbeddedWebViewScreenshotError::WebViewDoesNotExist, + )); + } + }, + ScriptToConstellationMessage::ForwardEventToEmbeddedWebView( + embedded_webview_id, + event, + ) => { + // Forward the input event to the embedded webview via Paint. + // This is called after the parent webview's script thread determined + // via DOM hit testing that the event target is an embedded iframe. + if self.webviews.contains_key(&embedded_webview_id) { + self.paint_proxy + .send(PaintMessage::ForwardInputEventToEmbeddedWebView( + embedded_webview_id, + event, + )); + } + }, + ScriptToConstellationMessage::EmbeddedWebViewControlResponse(id, response) => { + // Route the control response to the embedded webview's script thread. + // This is sent from the parent shell after user interaction with a custom control UI. + self.handle_embedder_control_response(id, response); + }, + ScriptToConstellationMessage::EmbeddedWebViewSetPageZoom(embedded_webview_id, zoom) => { + // Validate this is a known embedded webview + let ctx_id = BrowsingContextId::from(embedded_webview_id); + if self.browsing_contexts.get(&ctx_id).is_none() { + return warn!( + "EmbeddedWebViewSetPageZoom for unknown browsing context {:?}", + embedded_webview_id + ); + } + // Send to Paint component + self.paint_proxy + .send(PaintMessage::SetPageZoom(embedded_webview_id, zoom)); + }, + ScriptToConstellationMessage::InjectInputToActiveIme(event) => { + // Route the input event to the webview that currently has an active IME input. + // This is used by the virtual keyboard to send keystrokes to the focused input field. + if let Some(target_webview_id) = self.active_ime_webview { + self.forward_input_event(target_webview_id, event, None); + } else { + debug!("InjectInputToActiveIme called but no active IME webview is tracked"); + } + }, + ScriptToConstellationMessage::SetActiveImeWebView(webview_id) => { + // Set the active IME webview for virtual keyboard input routing. + // Used when the system webview (non-embedded) shows an input method control. + self.active_ime_webview = Some(webview_id); + }, + ScriptToConstellationMessage::ClearActiveImeWebView(webview_id) => { + // Clear the active IME webview when the system webview hides an input method control. + if self.active_ime_webview == Some(webview_id) { + self.active_ime_webview = None; + } + }, } } @@ -3135,6 +3336,13 @@ /// fn handle_close_top_level_browsing_context(&mut self, webview_id: WebViewId) { debug!("{webview_id}: Closing"); + + // Notify embedded webview parent before closing (if this is an embedded webview) + self.handle_embedded_webview_notification(webview_id, EmbeddedWebViewEventType::Closed); + + // Clean up the embedded webview mapping + self.embedded_webview_to_iframe.remove(&webview_id); + let browsing_context_id = BrowsingContextId::from(webview_id); // Step 5. Remove traversable from the user agent's top-level traversable set. let browsing_context = @@ -3411,8 +3619,27 @@ opener_webview_id, opener_pipeline_id, response_sender, + target_url, } = load_info; + // Check if the opener is an embedded webview + if self + .embedded_webview_to_iframe + .contains_key(&opener_webview_id) + { + // For embedded webviews, create a new embedded webview and notify the parent + // browserhtml shell to create an