Route web surface scrolls to Servo hit point
This commit is contained in:
@@ -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>,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user