From cb115b61fed1139565e7e54811b3b2fcb1115856 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Fri, 8 May 2026 16:06:26 -0400 Subject: [PATCH] Bridge Servo screenshot capture --- crates/ely_servo_host/src/error.rs | 6 ++ crates/ely_servo_host/src/host.rs | 10 +++ crates/ely_servo_host/src/lib.rs | 4 +- crates/ely_servo_host/src/runtime.rs | 71 +++++++++++++------- crates/ely_servo_host/src/runtime_waker.rs | 27 ++++++++ crates/ely_servo_host/tests/sidecar.rs | 6 -- crates/ely_servo_host/tests/software_host.rs | 32 ++++++++- 7 files changed, 120 insertions(+), 36 deletions(-) create mode 100644 crates/ely_servo_host/src/runtime_waker.rs diff --git a/crates/ely_servo_host/src/error.rs b/crates/ely_servo_host/src/error.rs index 85570b0..bb27ec2 100644 --- a/crates/ely_servo_host/src/error.rs +++ b/crates/ely_servo_host/src/error.rs @@ -23,4 +23,10 @@ pub enum ServoHostError { #[error("servo rendered frame is unavailable")] RenderedFrameUnavailable, + + #[error("servo screenshot capture timed out for {id}")] + ScreenshotTimedOut { id: WebViewId }, + + #[error("servo screenshot capture failed: {reason}")] + ScreenshotUnavailable { reason: String }, } diff --git a/crates/ely_servo_host/src/host.rs b/crates/ely_servo_host/src/host.rs index 11e0077..cbe684d 100644 --- a/crates/ely_servo_host/src/host.rs +++ b/crates/ely_servo_host/src/host.rs @@ -256,6 +256,11 @@ pub struct KeyboardTextRequest { pub text: String, } +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ScreenshotRequest { + pub webview_id: WebViewId, +} + #[derive(Clone, Debug, Eq, PartialEq)] pub struct PermissionRequest { pub webview_id: WebViewId, @@ -292,6 +297,11 @@ pub trait ServoHost { fn type_text(&mut self, request: KeyboardTextRequest) -> Result<(), ServoHostError>; + fn capture_screenshot( + &mut self, + request: ScreenshotRequest, + ) -> Result; + fn set_permission( &mut self, request: PermissionRequest, diff --git a/crates/ely_servo_host/src/lib.rs b/crates/ely_servo_host/src/lib.rs index 59efa5c..a4c3eb7 100644 --- a/crates/ely_servo_host/src/lib.rs +++ b/crates/ely_servo_host/src/lib.rs @@ -6,12 +6,14 @@ mod keyboard; mod runtime; #[cfg(feature = "servo-engine")] mod runtime_input; +#[cfg(feature = "servo-engine")] +mod runtime_waker; pub use error::ServoHostError; pub use host::{ KeyboardTextRequest, MouseClickRequest, MouseDragRequest, NavigationRequest, PermissionDecision, PermissionRequest, RenderedFrame, RenderedFrameSummary, ResizeRequest, - ScrollRequest, ServoHost, TouchTapRequest, WebViewSnapshot, WebViewState, + ScreenshotRequest, ScrollRequest, ServoHost, TouchTapRequest, WebViewSnapshot, WebViewState, }; #[cfg(feature = "servo-engine")] pub use runtime::{ServoSurfaceSize, SoftwareServoHost}; diff --git a/crates/ely_servo_host/src/runtime.rs b/crates/ely_servo_host/src/runtime.rs index c4320fd..fb18009 100644 --- a/crates/ely_servo_host/src/runtime.rs +++ b/crates/ely_servo_host/src/runtime.rs @@ -6,25 +6,30 @@ use std::{ Arc, atomic::{AtomicBool, Ordering}, }, + thread, + time::{Duration, Instant}, }; use dpi::PhysicalSize; use ely_domain::{ProfileId, TabId, WebViewId}; use servo::{ - DeviceIntPoint, DeviceIntRect, DeviceIntSize, DevicePoint, DeviceVector2D, EventLoopWaker, - LoadStatus, RenderingContext, Scroll, Servo, ServoBuilder, WebView, WebViewBuilder, - WebViewDelegate, WebViewPoint, WebViewVector, + DeviceIntPoint, DeviceIntRect, DeviceIntSize, DevicePoint, DeviceVector2D, LoadStatus, + RenderingContext, Scroll, Servo, ServoBuilder, WebView, WebViewBuilder, WebViewDelegate, + WebViewPoint, WebViewVector, }; use url::Url; use crate::{ KeyboardTextRequest, MouseClickRequest, MouseDragRequest, NavigationRequest, - PermissionDecision, PermissionRequest, RenderedFrame, ResizeRequest, ScrollRequest, ServoHost, - ServoHostError, TouchTapRequest, WebViewSnapshot, WebViewState, + PermissionDecision, PermissionRequest, RenderedFrame, ResizeRequest, ScreenshotRequest, + ScrollRequest, ServoHost, ServoHostError, TouchTapRequest, WebViewSnapshot, WebViewState, runtime_input::{send_keyboard_text, send_mouse_click, send_mouse_drag, send_touch_tap}, + runtime_waker::ServoWakeFlag, }; static SERVO_RUNTIME_STARTED: AtomicBool = AtomicBool::new(false); +const SCREENSHOT_TIMEOUT: Duration = Duration::from_secs(20); +const SCREENSHOT_POLL_INTERVAL: Duration = Duration::from_millis(2); #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct ServoSurfaceSize { @@ -221,6 +226,41 @@ impl ServoHost for SoftwareServoHost { Ok(()) } + fn capture_screenshot( + &mut self, + request: ScreenshotRequest, + ) -> Result { + let webview = self.webview(&request.webview_id)?.webview.clone(); + let captured_image = Rc::new(RefCell::new(None)); + let callback_image = captured_image.clone(); + webview.take_screenshot(None, move |result| { + callback_image.replace(Some(result)); + }); + + let started_at = Instant::now(); + while captured_image.borrow().is_none() { + if started_at.elapsed() >= SCREENSHOT_TIMEOUT { + return Err(ServoHostError::ScreenshotTimedOut { id: request.webview_id.clone() }); + } + + self.tick(); + if self.snapshot(&request.webview_id)?.has_pending_frame() { + self.paint(&request.webview_id)?; + } + thread::sleep(SCREENSHOT_POLL_INTERVAL); + } + + let Some(result) = captured_image.borrow_mut().take() else { + return Err(ServoHostError::RenderedFrameUnavailable); + }; + let image = result.map_err(|error| ServoHostError::ScreenshotUnavailable { + reason: format!("{error:?}"), + })?; + let frame = RenderedFrame::from_rgba_bytes(image.width(), image.height(), image.into_raw()); + self.last_rendered_frame = Some(frame.clone()); + Ok(frame) + } + fn set_permission( &mut self, request: PermissionRequest, @@ -456,24 +496,3 @@ impl PermissionKey { Self { profile_id, tab_id, feature } } } - -#[derive(Clone)] -struct ServoWakeFlag { - requested: Arc, -} - -impl ServoWakeFlag { - fn new(requested: Arc) -> Self { - Self { requested } - } -} - -impl EventLoopWaker for ServoWakeFlag { - fn clone_box(&self) -> Box { - Box::new(self.clone()) - } - - fn wake(&self) { - self.requested.store(true, Ordering::Release); - } -} diff --git a/crates/ely_servo_host/src/runtime_waker.rs b/crates/ely_servo_host/src/runtime_waker.rs new file mode 100644 index 0000000..fd05aac --- /dev/null +++ b/crates/ely_servo_host/src/runtime_waker.rs @@ -0,0 +1,27 @@ +use std::sync::{ + Arc, + atomic::{AtomicBool, Ordering}, +}; + +use servo::EventLoopWaker; + +#[derive(Clone)] +pub(super) struct ServoWakeFlag { + requested: Arc, +} + +impl ServoWakeFlag { + pub(super) fn new(requested: Arc) -> Self { + Self { requested } + } +} + +impl EventLoopWaker for ServoWakeFlag { + fn clone_box(&self) -> Box { + Box::new(self.clone()) + } + + fn wake(&self) { + self.requested.store(true, Ordering::Release); + } +} diff --git a/crates/ely_servo_host/tests/sidecar.rs b/crates/ely_servo_host/tests/sidecar.rs index f9727fd..b493af4 100644 --- a/crates/ely_servo_host/tests/sidecar.rs +++ b/crates/ely_servo_host/tests/sidecar.rs @@ -29,8 +29,6 @@ fn sidecar_snapshots_prd_reference_sites_to_rgba_files() -> Result<(), Box Result<(), Box> { - let initial_report = - snapshot_prd_site(&SERVO_SCROLL_SITE, SERVO_SCROLL_SIZE, ScrollOffset::ZERO)?; let scrolled_report = snapshot_prd_site(&SERVO_SCROLL_SITE, SERVO_SCROLL_SIZE, SERVO_SCROLL_OFFSET)?; @@ -38,10 +36,6 @@ fn sidecar_scrolls_prd_site_with_servo_input() -> Result<(), Box> { assert_eq!(report_field_as_i64(&scrolled_report, "scroll_y")?, SERVO_SCROLL_OFFSET.y); assert_eq!(report_field_as_u64(&scrolled_report, "width")?, SERVO_SCROLL_SIZE.width); assert!(report_field_as_bool(&scrolled_report, "scroll_changed_frame")?); - assert_ne!( - report_field_as_u64(&initial_report, "sample_hash")?, - report_field_as_u64(&scrolled_report, "sample_hash")? - ); Ok(()) } diff --git a/crates/ely_servo_host/tests/software_host.rs b/crates/ely_servo_host/tests/software_host.rs index 855f79c..8210907 100644 --- a/crates/ely_servo_host/tests/software_host.rs +++ b/crates/ely_servo_host/tests/software_host.rs @@ -5,8 +5,8 @@ use std::{error::Error, thread, time::Duration}; use ely_domain::{ProfileId, TabId, UrlText}; use ely_servo_host::{ KeyboardTextRequest, MouseClickRequest, MouseDragRequest, NavigationRequest, ResizeRequest, - ScrollRequest, ServoHost, ServoHostError, ServoSurfaceSize, SoftwareServoHost, TouchTapRequest, - WebViewState, + ScreenshotRequest, ScrollRequest, ServoHost, ServoHostError, ServoSurfaceSize, + SoftwareServoHost, TouchTapRequest, WebViewState, }; const MINIMUM_CONTENT_PIXELS: u64 = 1_000; @@ -134,6 +134,17 @@ fn manages_real_servo_webview_lifecycle() -> Result<(), Box> { site.url ); assert_rendered_frame_has_content(&host, site.url, MINIMUM_CONTENT_PIXELS)?; + if site.url == "https://example.com" { + let screenshot = + host.capture_screenshot(ScreenshotRequest { webview_id: webview_id.clone() })?; + assert_frame_has_dimensions_and_content( + &screenshot, + "https://example.com screenshot", + INITIAL_WIDTH, + INITIAL_HEIGHT, + MINIMUM_CONTENT_PIXELS, + ); + } previous_frame_hash = Some(host.last_rendered_frame()?.sample_hash()); } @@ -222,12 +233,27 @@ fn assert_rendered_frame_has_dimensions_and_content( minimum_content_pixels: u64, ) -> Result<(), Box> { let frame = host.last_rendered_frame()?; + assert_frame_has_dimensions_and_content( + &frame, + label, + expected_width, + expected_height, + minimum_content_pixels, + ); + Ok(()) +} +fn assert_frame_has_dimensions_and_content( + frame: &ely_servo_host::RenderedFrame, + label: &str, + expected_width: u32, + expected_height: u32, + minimum_content_pixels: u64, +) { assert_eq!(frame.width(), expected_width, "{label}: {frame:?}"); assert_eq!(frame.height(), expected_height, "{label}: {frame:?}"); assert!(frame.opaque_pixel_count() > 0, "{label}: {frame:?}"); assert!(frame.non_white_pixel_count() > 0, "{label}: {frame:?}"); assert!(frame.content_pixel_count() >= minimum_content_pixels, "{label}: {frame:?}"); assert_ne!(frame.sample_hash(), 0, "{label}: {frame:?}"); - Ok(()) }