From 6314e1f777d47483c37412dc796f25358fe81663 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Sun, 10 May 2026 13:59:04 -0400 Subject: [PATCH] Make WebView input invariant a property of the dispatch path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Servo's hit-test silently absorbs notify_input_event on a hidden or unfocused WebView. Today show()+focus() are called at creation and on the first-navigate rebuild, but every later sibling-WebView creation also calls focus() — silently stealing focus from the foreground tab. load() (later navigates), resize, set_page_zoom, and paint never re-focus, so a click on the visible tab can land on an unreachable WebView and disappear. Move the invariant from a state spread across creation/navigation/ tab-switching into a property of the dispatch path itself: a private webview_for_input(id) helper re-asserts show()+focus() and returns the WebView; click/hover/drag/touch_tap/scroll/type_text all go through it. The cosmetic show/focus calls in create_webview_in_context and the navigate-rebuild branch stay (first-frame paint), now annotated to point to webview_for_input as the input-path owner. Not a complete fix on its own. focus() goes through constellation_proxy asynchronously (servo crate webview.rs:352), so debug_assert!(focused()) right after focus() would race the constellation — doc comment is the only guard. Sidecar integration tests pass 9/9 but only exercise single-WebView sessions; multi-tab focus stealing on Google / YouTube / Twitter still needs human verification in the live shell. Linus's critique of the GPUI-side data structure (PerTabSurface Option-as-queue + Ensure bundling config+input+frame) is a separate layer untouched by this change. Pre-existing tests/software_host.rs::manages_real_servo_webview_lifecycle already times out on https://servo.org/ with state=Complete but has_pending_frame=false on b8795bf without this change, so unrelated. --- crates/ely_servo_host/src/runtime.rs | 76 +++++++++++++++------------- 1 file changed, 40 insertions(+), 36 deletions(-) diff --git a/crates/ely_servo_host/src/runtime.rs b/crates/ely_servo_host/src/runtime.rs index fa95614..e8e5050 100644 --- a/crates/ely_servo_host/src/runtime.rs +++ b/crates/ely_servo_host/src/runtime.rs @@ -148,12 +148,12 @@ impl ServoHost for SoftwareServoHost { .delegate(webview.delegate.clone()) .url(url) .build(); - // Fresh WebView is hidden+unfocused by default; without this - // pair Servo's hit-test silently drops every input event. - // Existing WebViews keep their visibility/focus across loads - // — calling focus() here every navigation would let a - // background tab finishing a load steal focus from the - // foreground tab. + // Cosmetic: makes the freshly built WebView paint its + // first frame. The input-accepting invariant lives in + // `webview_for_input`; we deliberately do not re-show or + // re-focus on the `load()` branch so a background tab + // finishing a load cannot steal focus from the foreground + // tab between the user's mouse-down and the next render. webview.webview.show(); webview.webview.focus(); } else { @@ -164,15 +164,11 @@ impl ServoHost for SoftwareServoHost { } fn scroll(&mut self, request: ScrollRequest) -> Result<(), ServoHostError> { - let webview = self - .webviews - .get(&request.webview_id) - .ok_or_else(|| ServoHostError::WebViewNotFound { id: request.webview_id.clone() })?; - if request.delta_x == 0 && request.delta_y == 0 { return Ok(()); } + let webview = self.webview_for_input(&request.webview_id)?; webview.webview.notify_scroll_event( Scroll::Delta(WebViewVector::Device(DeviceVector2D::new( request.delta_x as f32, @@ -206,31 +202,19 @@ impl ServoHost for SoftwareServoHost { } fn hover(&mut self, request: MouseHoverRequest) -> Result<(), ServoHostError> { - let webview = self - .webviews - .get(&request.webview_id) - .ok_or_else(|| ServoHostError::WebViewNotFound { id: request.webview_id.clone() })?; - + let webview = self.webview_for_input(&request.webview_id)?; send_mouse_hover(&webview.webview, request.x, request.y); Ok(()) } fn click(&mut self, request: MouseClickRequest) -> Result<(), ServoHostError> { - let webview = self - .webviews - .get(&request.webview_id) - .ok_or_else(|| ServoHostError::WebViewNotFound { id: request.webview_id.clone() })?; - + let webview = self.webview_for_input(&request.webview_id)?; send_mouse_click(&webview.webview, request.x, request.y); Ok(()) } fn drag(&mut self, request: MouseDragRequest) -> Result<(), ServoHostError> { - let webview = self - .webviews - .get(&request.webview_id) - .ok_or_else(|| ServoHostError::WebViewNotFound { id: request.webview_id.clone() })?; - + let webview = self.webview_for_input(&request.webview_id)?; send_mouse_drag( &webview.webview, request.from_x, @@ -242,21 +226,13 @@ impl ServoHost for SoftwareServoHost { } fn touch_tap(&mut self, request: TouchTapRequest) -> Result<(), ServoHostError> { - let webview = self - .webviews - .get(&request.webview_id) - .ok_or_else(|| ServoHostError::WebViewNotFound { id: request.webview_id.clone() })?; - + let webview = self.webview_for_input(&request.webview_id)?; send_touch_tap(&webview.webview, request.x, request.y); Ok(()) } fn type_text(&mut self, request: KeyboardTextRequest) -> Result<(), ServoHostError> { - let webview = self - .webviews - .get(&request.webview_id) - .ok_or_else(|| ServoHostError::WebViewNotFound { id: request.webview_id.clone() })?; - + let webview = self.webview_for_input(&request.webview_id)?; send_keyboard_text(&webview.webview, &request.text); Ok(()) } @@ -365,6 +341,11 @@ impl SoftwareServoHost { let webview = WebViewBuilder::new(&self.servo, rendering_context.clone()) .delegate(delegate.clone()) .build(); + // Cosmetic: makes the first frame paint into the rendering + // context. The input-accepting invariant is owned by + // `webview_for_input`, which re-asserts show/focus on every + // dispatch — so a sibling tab's later creation (which would + // steal focus here) cannot break input on this WebView. webview.show(); webview.focus(); @@ -400,6 +381,29 @@ impl SoftwareServoHost { .ok_or_else(|| ServoHostError::WebViewNotFound { id: webview_id.clone() }) } + /// Returns a WebView guaranteed to accept input. + /// + /// Servo's hit-test silently absorbs `notify_input_event` on a + /// hidden or unfocused WebView. The create + first-navigate paths + /// call `show()`/`focus()` for first-frame visibility, but every + /// later operation that creates a sibling WebView (multi-tab) + /// calls `focus()` on the new one, silently stealing focus from + /// the foreground tab. `load()` (later navigates), `resize`, + /// `set_page_zoom`, and `paint` do not re-focus, so by the time a + /// click arrives the visible tab's WebView is unreachable. + /// Re-asserting per dispatch keeps the invariant on the dispatch + /// path instead of spread across creation, navigation, and + /// tab-switching. + fn webview_for_input( + &self, + webview_id: &WebViewId, + ) -> Result<&HostWebView, ServoHostError> { + let webview = self.webview(webview_id)?; + webview.webview.show(); + webview.webview.focus(); + Ok(webview) + } + fn read_rendered_frame( rendering_context: &dyn RenderingContext, ) -> Result {