Route web surface scrolls to Servo hit point

This commit is contained in:
2026-05-12 23:29:40 -04:00
parent 6f279bf3e6
commit 0db1caad69
15 changed files with 220 additions and 126 deletions
+9 -7
View File
@@ -69,7 +69,6 @@ impl ServoLiveClient {
})
}
pub fn ensure(
&mut self,
request: ServoLiveEnsureRequest,
@@ -84,6 +83,8 @@ impl ServoLiveClient {
device_pixel_ratio: request.device_pixel_ratio,
scroll_delta_x: request.scroll_delta_x,
scroll_delta_y: request.scroll_delta_y,
scroll_point_x: request.scroll_point_x,
scroll_point_y: request.scroll_point_y,
click_x: request.click_x,
click_y: request.click_y,
hover_x: request.hover_x,
@@ -136,9 +137,8 @@ impl ServoLiveClient {
// upper limit is `width * height * 4` (RGBA8); `0` is the
// explicit "hardware path active, sample the IOSurface
// instead" signal — anything else is a protocol violation.
let pixel_byte_count = (report.width as u64)
.saturating_mul(report.height as u64)
.saturating_mul(4);
let pixel_byte_count =
(report.width as u64).saturating_mul(report.height as u64).saturating_mul(4);
let advertised = report.rgba_byte_count as u64;
if advertised != 0 && advertised != pixel_byte_count {
return Err(ServoLiveError::FrameBudgetExceeded {
@@ -157,9 +157,7 @@ impl ServoLiveClient {
// fs::read, no temp file.
let mut rgba_bytes = vec![0u8; report.rgba_byte_count];
if report.rgba_byte_count > 0 {
self.stdout
.read_exact(&mut rgba_bytes)
.map_err(ServoLiveError::FrameRead)?;
self.stdout.read_exact(&mut rgba_bytes).map_err(ServoLiveError::FrameRead)?;
}
let mut frame = ServoLiveFrame::from_parts(report, rgba_bytes);
@@ -220,6 +218,8 @@ pub(crate) struct ServoLiveEnsureRequest {
pub(crate) device_pixel_ratio: f32,
pub(crate) scroll_delta_x: i32,
pub(crate) scroll_delta_y: i32,
pub(crate) scroll_point_x: Option<u32>,
pub(crate) scroll_point_y: Option<u32>,
pub(crate) click_x: Option<u32>,
pub(crate) click_y: Option<u32>,
pub(crate) hover_x: Option<u32>,
@@ -410,6 +410,8 @@ enum LiveRequest {
device_pixel_ratio: f32,
scroll_delta_x: i32,
scroll_delta_y: i32,
scroll_point_x: Option<u32>,
scroll_point_y: Option<u32>,
click_x: Option<u32>,
click_y: Option<u32>,
hover_x: Option<u32>,
+30 -16
View File
@@ -27,11 +27,7 @@ pub(super) struct WebSurfaceStore {
impl WebSurfaceStore {
pub(super) fn new() -> Self {
Self {
runtime: WebSurfaceRuntime::new(),
surfaces: BTreeMap::new(),
keyboard_focus: None,
}
Self { runtime: WebSurfaceRuntime::new(), surfaces: BTreeMap::new(), keyboard_focus: None }
}
pub(super) fn state(&self, tab_id: &TabId) -> Option<&WebSurfaceState> {
@@ -102,11 +98,21 @@ impl WebSurfaceStore {
tab_id: &TabId,
requested_url: &str,
delta: Point<Pixels>,
position: Point<Pixels>,
scale_factor: f32,
) -> WebSurfaceInputOutcome {
let Some(delta) = WebSurfaceScrollDelta::from_point(delta, scale_factor) else {
return WebSurfaceInputOutcome::DroppedZeroDelta;
};
let Some(bounds) = self.surfaces.get(tab_id).and_then(|surface| surface.viewport_bounds)
else {
return WebSurfaceInputOutcome::DroppedNoViewportBounds;
};
let Some(point) =
WebSurfaceClickPoint::from_window_position(bounds, position, scale_factor)
else {
return WebSurfaceInputOutcome::DroppedOutOfBounds;
};
let surface = self.surface_mut(tab_id);
let scroll = surface
@@ -121,6 +127,7 @@ impl WebSurfaceStore {
Some(current) => current.combined_with(delta),
None => delta,
});
surface.pending_scroll_point = Some(point);
// Drop any buffered click — its viewport coordinates were
// captured against the pre-scroll page, so applying it after
// the scroll would land on the wrong DOM element. Keep
@@ -170,12 +177,14 @@ impl WebSurfaceStore {
position: Point<Pixels>,
scale_factor: f32,
) -> WebSurfaceInputOutcome {
let surface = self.surfaces.get_mut(tab_id).filter(|surface| surface.viewport_bounds.is_some());
let surface =
self.surfaces.get_mut(tab_id).filter(|surface| surface.viewport_bounds.is_some());
let Some(surface) = surface else {
return WebSurfaceInputOutcome::DroppedNoViewportBounds;
};
let bounds = surface.viewport_bounds.expect("viewport_bounds checked above");
let Some(point) = WebSurfaceClickPoint::from_window_position(bounds, position, scale_factor)
let Some(point) =
WebSurfaceClickPoint::from_window_position(bounds, position, scale_factor)
else {
return WebSurfaceInputOutcome::DroppedOutOfBounds;
};
@@ -190,12 +199,12 @@ impl WebSurfaceStore {
position: Point<Pixels>,
scale_factor: f32,
) -> WebSurfaceInputOutcome {
let Some(bounds) =
self.surfaces.get(tab_id).and_then(|surface| surface.viewport_bounds)
let Some(bounds) = self.surfaces.get(tab_id).and_then(|surface| surface.viewport_bounds)
else {
return WebSurfaceInputOutcome::DroppedNoViewportBounds;
};
let Some(point) = WebSurfaceClickPoint::from_window_position(bounds, position, scale_factor)
let Some(point) =
WebSurfaceClickPoint::from_window_position(bounds, position, scale_factor)
else {
return WebSurfaceInputOutcome::DroppedOutOfBounds;
};
@@ -206,11 +215,8 @@ impl WebSurfaceStore {
.map(|surface| surface.scroll_offset_for(requested_url))
.unwrap_or_default();
let state = WebSurfaceClickState {
requested_url: requested_url.to_string(),
scroll_offset,
point,
};
let state =
WebSurfaceClickState { requested_url: requested_url.to_string(), scroll_offset, point };
self.keyboard_focus = Some(WebSurfaceKeyboardFocusState {
tab_id: tab_id.clone(),
requested_url: requested_url.to_string(),
@@ -273,6 +279,7 @@ impl WebSurfaceStore {
let surface = self.surface_mut(tab_id);
let scroll_offset = surface.scroll_offset_for(requested_url);
let scroll_delta = surface.pending_scroll_delta.take();
let scroll_point = surface.pending_scroll_point.take();
let click_point = surface
.click_point
.take()
@@ -287,7 +294,14 @@ impl WebSurfaceStore {
.map(|state| state.text);
let hover_point = surface.hover_point.take();
WebSurfacePendingInput { scroll_offset, scroll_delta, click_point, hover_point, typed_text }
WebSurfacePendingInput {
scroll_offset,
scroll_delta,
scroll_point,
click_point,
hover_point,
typed_text,
}
}
fn previous_ready_frame(
@@ -67,6 +67,7 @@ impl ElyShell {
tab_id: TabId,
requested_url: String,
delta: Point<Pixels>,
position: Point<Pixels>,
scale_factor: f32,
cx: &mut Context<Self>,
) {
@@ -74,6 +75,7 @@ impl ElyShell {
&tab_id,
requested_url.as_str(),
delta,
position,
scale_factor,
) == WebSurfaceInputOutcome::Applied
{
@@ -45,6 +45,8 @@ impl WebSurfaceRuntime {
if active_scope != &scope {
return Err(active_scope.error_for(&scope));
}
let (scroll_delta_x, scroll_delta_y, scroll_point_x, scroll_point_y) =
scroll_wire_fields(input.scroll_delta, input.scroll_point)?;
let session = sessions.entry(tab.id().clone()).or_insert_with(WebSurfaceSession::default);
let next_scroll_offset = input.scroll_offset;
@@ -58,8 +60,10 @@ impl WebSurfaceRuntime {
height: size.height,
page_zoom_percent: zoom_percent,
device_pixel_ratio: size.device_pixel_ratio_f32(),
scroll_delta_x: input.scroll_delta.map_or(0, |delta| delta.x()),
scroll_delta_y: input.scroll_delta.map_or(0, |delta| delta.y()),
scroll_delta_x,
scroll_delta_y,
scroll_point_x,
scroll_point_y,
click_x: input.click_point.map(|point| point.x()),
click_y: input.click_point.map(|point| point.y()),
hover_x: input.hover_point.map(|point| point.x()),
@@ -227,6 +231,20 @@ fn config_dir_for_scope(
}
}
fn scroll_wire_fields(
delta: Option<super::web_surface_geometry::WebSurfaceScrollDelta>,
point: Option<super::web_surface_geometry::WebSurfaceClickPoint>,
) -> Result<(i32, i32, Option<u32>, Option<u32>), String> {
match delta {
Some(delta) => {
let point = point
.ok_or_else(|| "Servo scroll input is missing a viewport point".to_string())?;
Ok((delta.x(), delta.y(), Some(point.x()), Some(point.y())))
}
None => Ok((0, 0, None, None)),
}
}
impl From<&WebSurfaceSitePermission> for ServoLiveSitePermission {
fn from(permission: &WebSurfaceSitePermission) -> Self {
Self::new(
@@ -45,6 +45,7 @@ pub(super) struct WebSurfaceTextInputState {
pub(super) struct WebSurfacePendingInput {
pub(super) scroll_offset: WebSurfaceScrollOffset,
pub(super) scroll_delta: Option<WebSurfaceScrollDelta>,
pub(super) scroll_point: Option<WebSurfaceClickPoint>,
pub(super) click_point: Option<WebSurfaceClickPoint>,
pub(super) hover_point: Option<WebSurfaceClickPoint>,
pub(super) typed_text: Option<String>,
@@ -116,6 +117,7 @@ pub(super) struct PerTabSurface {
pub(super) hover_point: Option<WebSurfaceClickPoint>,
pub(super) click_point: Option<WebSurfaceClickState>,
pub(super) pending_scroll_delta: Option<WebSurfaceScrollDelta>,
pub(super) pending_scroll_point: Option<WebSurfaceClickPoint>,
pub(super) scroll_offset: Option<WebSurfaceScrollState>,
pub(super) typed_text: Option<WebSurfaceTextInputState>,
pub(super) state: Option<WebSurfaceState>,
@@ -130,6 +132,7 @@ impl PerTabSurface {
hover_point: None,
click_point: None,
pending_scroll_delta: None,
pending_scroll_point: None,
scroll_offset: None,
typed_text: None,
state: None,
+27 -11
View File
@@ -42,12 +42,14 @@ fn scroll_delta_enters_pending_input_after_wheel() -> Result<(), Box<dyn Error>>
tab.id(),
tab.url().as_str(),
point(px(0.0), px(140.0)),
point(px(320.0), px(240.0)),
1.0,
));
assert_applied(store.record_scroll_delta(
tab.id(),
tab.url().as_str(),
point(px(0.0), px(60.0)),
point(px(300.0), px(220.0)),
1.0,
));
@@ -55,6 +57,7 @@ fn scroll_delta_enters_pending_input_after_wheel() -> Result<(), Box<dyn Error>>
assert_eq!(input.scroll_offset.y(), 200);
assert_eq!(input.scroll_delta.map(|delta| (delta.x(), delta.y())), Some((0, 200)));
assert_eq!(input.scroll_point.map(|point| (point.x(), point.y())), Some((300, 220)));
Ok(())
}
@@ -91,7 +94,13 @@ fn scroll_after_click_keeps_keyboard_focus_and_typed_text() -> Result<(), Box<dy
assert_applied(store.record_click_point(tab.id(), url, point(px(160.0), px(120.0)), 1.0));
assert_applied(store.record_typed_text(tab.id(), url, "h"));
assert_applied(store.record_scroll_delta(tab.id(), url, point(px(0.0), px(140.0)), 1.0));
assert_applied(store.record_scroll_delta(
tab.id(),
url,
point(px(0.0), px(140.0)),
point(px(160.0), px(120.0)),
1.0,
));
assert_eq!(
store.record_typed_text(tab.id(), url, "i"),
@@ -134,7 +143,13 @@ fn retina_scale_factor_doubles_every_input_coordinate() -> Result<(), Box<dyn Er
assert_applied(store.record_viewport_size(tab.id(), web_bounds(), 2.0));
assert_applied(store.record_click_point(tab.id(), url, point(px(160.0), px(120.0)), 2.0));
assert_applied(store.record_typed_text(tab.id(), url, "h"));
assert_applied(store.record_scroll_delta(tab.id(), url, point(px(0.0), px(140.0)), 2.0));
assert_applied(store.record_scroll_delta(
tab.id(),
url,
point(px(0.0), px(140.0)),
point(px(160.0), px(120.0)),
2.0,
));
let input = store.take_pending_input(tab.id(), url);
@@ -145,8 +160,7 @@ fn retina_scale_factor_doubles_every_input_coordinate() -> Result<(), Box<dyn Er
);
assert_eq!(input.scroll_offset.y(), 280, "scroll offset accumulates in device px");
assert_eq!(
input.click_point,
None,
input.click_point, None,
"scroll drops the buffered click — its viewport coords are stale",
);
Ok(())
@@ -181,12 +195,7 @@ fn click_before_viewport_measured_reports_no_viewport_bounds() -> Result<(), Box
let tab = web_tab("https://example.com/form")?;
assert_eq!(
store.record_click_point(
tab.id(),
tab.url().as_str(),
point(px(160.0), px(120.0)),
1.0,
),
store.record_click_point(tab.id(), tab.url().as_str(), point(px(160.0), px(120.0)), 1.0,),
WebSurfaceInputOutcome::DroppedNoViewportBounds,
);
Ok(())
@@ -206,6 +215,7 @@ fn zero_wheel_delta_reports_zero_delta() -> Result<(), Box<dyn Error>> {
tab.id(),
tab.url().as_str(),
point(px(0.0), px(0.0)),
point(px(160.0), px(120.0)),
1.0,
),
WebSurfaceInputOutcome::DroppedZeroDelta,
@@ -276,7 +286,13 @@ fn zero_wheel_delta_must_not_erase_buffered_click() -> Result<(), Box<dyn Error>
assert_applied(store.record_click_point(tab.id(), url, point(px(160.0), px(120.0)), 1.0));
assert_eq!(
store.record_scroll_delta(tab.id(), url, point(px(0.0), px(0.0)), 1.0),
store.record_scroll_delta(
tab.id(),
url,
point(px(0.0), px(0.0)),
point(px(160.0), px(120.0)),
1.0,
),
WebSurfaceInputOutcome::DroppedZeroDelta,
);
+3 -12
View File
@@ -72,12 +72,7 @@ fn error_page(message: &str) -> impl IntoElement {
.text_color(rgb(colors::INK))
.child("Page unavailable"),
)
.child(
div()
.text_size(px(14.0))
.text_color(rgb(colors::INK_3))
.child(message.to_string()),
)
.child(div().text_size(px(14.0)).text_color(rgb(colors::INK_3)).child(message.to_string()))
}
fn render_web_surface(
@@ -95,12 +90,7 @@ fn render_web_surface(
.size_full()
.min_w_0()
.overflow_hidden()
.child(
div()
.absolute()
.inset_0()
.child(content),
)
.child(div().absolute().inset_0().child(content))
.child(render_viewport_tracker(tab.id().clone(), tracker_entity))
.child(render_input_overlay(input_tab_id, input_url, input_entity))
.into_any_element()
@@ -169,6 +159,7 @@ fn render_input_overlay(
scroll_tab_id.clone(),
scroll_url.clone(),
delta,
event.position,
scale_factor,
cx,
);