Make WebView input invariant a property of the dispatch path
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.
This commit is contained in:
@@ -148,12 +148,12 @@ impl ServoHost for SoftwareServoHost {
|
|||||||
.delegate(webview.delegate.clone())
|
.delegate(webview.delegate.clone())
|
||||||
.url(url)
|
.url(url)
|
||||||
.build();
|
.build();
|
||||||
// Fresh WebView is hidden+unfocused by default; without this
|
// Cosmetic: makes the freshly built WebView paint its
|
||||||
// pair Servo's hit-test silently drops every input event.
|
// first frame. The input-accepting invariant lives in
|
||||||
// Existing WebViews keep their visibility/focus across loads
|
// `webview_for_input`; we deliberately do not re-show or
|
||||||
// — calling focus() here every navigation would let a
|
// re-focus on the `load()` branch so a background tab
|
||||||
// background tab finishing a load steal focus from the
|
// finishing a load cannot steal focus from the foreground
|
||||||
// foreground tab.
|
// tab between the user's mouse-down and the next render.
|
||||||
webview.webview.show();
|
webview.webview.show();
|
||||||
webview.webview.focus();
|
webview.webview.focus();
|
||||||
} else {
|
} else {
|
||||||
@@ -164,15 +164,11 @@ impl ServoHost for SoftwareServoHost {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn scroll(&mut self, request: ScrollRequest) -> Result<(), ServoHostError> {
|
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 {
|
if request.delta_x == 0 && request.delta_y == 0 {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let webview = self.webview_for_input(&request.webview_id)?;
|
||||||
webview.webview.notify_scroll_event(
|
webview.webview.notify_scroll_event(
|
||||||
Scroll::Delta(WebViewVector::Device(DeviceVector2D::new(
|
Scroll::Delta(WebViewVector::Device(DeviceVector2D::new(
|
||||||
request.delta_x as f32,
|
request.delta_x as f32,
|
||||||
@@ -206,31 +202,19 @@ impl ServoHost for SoftwareServoHost {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn hover(&mut self, request: MouseHoverRequest) -> Result<(), ServoHostError> {
|
fn hover(&mut self, request: MouseHoverRequest) -> Result<(), ServoHostError> {
|
||||||
let webview = self
|
let webview = self.webview_for_input(&request.webview_id)?;
|
||||||
.webviews
|
|
||||||
.get(&request.webview_id)
|
|
||||||
.ok_or_else(|| ServoHostError::WebViewNotFound { id: request.webview_id.clone() })?;
|
|
||||||
|
|
||||||
send_mouse_hover(&webview.webview, request.x, request.y);
|
send_mouse_hover(&webview.webview, request.x, request.y);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn click(&mut self, request: MouseClickRequest) -> Result<(), ServoHostError> {
|
fn click(&mut self, request: MouseClickRequest) -> Result<(), ServoHostError> {
|
||||||
let webview = self
|
let webview = self.webview_for_input(&request.webview_id)?;
|
||||||
.webviews
|
|
||||||
.get(&request.webview_id)
|
|
||||||
.ok_or_else(|| ServoHostError::WebViewNotFound { id: request.webview_id.clone() })?;
|
|
||||||
|
|
||||||
send_mouse_click(&webview.webview, request.x, request.y);
|
send_mouse_click(&webview.webview, request.x, request.y);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn drag(&mut self, request: MouseDragRequest) -> Result<(), ServoHostError> {
|
fn drag(&mut self, request: MouseDragRequest) -> Result<(), ServoHostError> {
|
||||||
let webview = self
|
let webview = self.webview_for_input(&request.webview_id)?;
|
||||||
.webviews
|
|
||||||
.get(&request.webview_id)
|
|
||||||
.ok_or_else(|| ServoHostError::WebViewNotFound { id: request.webview_id.clone() })?;
|
|
||||||
|
|
||||||
send_mouse_drag(
|
send_mouse_drag(
|
||||||
&webview.webview,
|
&webview.webview,
|
||||||
request.from_x,
|
request.from_x,
|
||||||
@@ -242,21 +226,13 @@ impl ServoHost for SoftwareServoHost {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn touch_tap(&mut self, request: TouchTapRequest) -> Result<(), ServoHostError> {
|
fn touch_tap(&mut self, request: TouchTapRequest) -> Result<(), ServoHostError> {
|
||||||
let webview = self
|
let webview = self.webview_for_input(&request.webview_id)?;
|
||||||
.webviews
|
|
||||||
.get(&request.webview_id)
|
|
||||||
.ok_or_else(|| ServoHostError::WebViewNotFound { id: request.webview_id.clone() })?;
|
|
||||||
|
|
||||||
send_touch_tap(&webview.webview, request.x, request.y);
|
send_touch_tap(&webview.webview, request.x, request.y);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn type_text(&mut self, request: KeyboardTextRequest) -> Result<(), ServoHostError> {
|
fn type_text(&mut self, request: KeyboardTextRequest) -> Result<(), ServoHostError> {
|
||||||
let webview = self
|
let webview = self.webview_for_input(&request.webview_id)?;
|
||||||
.webviews
|
|
||||||
.get(&request.webview_id)
|
|
||||||
.ok_or_else(|| ServoHostError::WebViewNotFound { id: request.webview_id.clone() })?;
|
|
||||||
|
|
||||||
send_keyboard_text(&webview.webview, &request.text);
|
send_keyboard_text(&webview.webview, &request.text);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -365,6 +341,11 @@ impl SoftwareServoHost {
|
|||||||
let webview = WebViewBuilder::new(&self.servo, rendering_context.clone())
|
let webview = WebViewBuilder::new(&self.servo, rendering_context.clone())
|
||||||
.delegate(delegate.clone())
|
.delegate(delegate.clone())
|
||||||
.build();
|
.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.show();
|
||||||
webview.focus();
|
webview.focus();
|
||||||
|
|
||||||
@@ -400,6 +381,29 @@ impl SoftwareServoHost {
|
|||||||
.ok_or_else(|| ServoHostError::WebViewNotFound { id: webview_id.clone() })
|
.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(
|
fn read_rendered_frame(
|
||||||
rendering_context: &dyn RenderingContext,
|
rendering_context: &dyn RenderingContext,
|
||||||
) -> Result<RenderedFrame, ServoHostError> {
|
) -> Result<RenderedFrame, ServoHostError> {
|
||||||
|
|||||||
Reference in New Issue
Block a user