update
This commit is contained in:
+47
-11
@@ -20,6 +20,7 @@ use gpui_component_assets::Assets;
|
||||
use shell::ElyShell;
|
||||
use shell::chrome::{TRAFFIC_LIGHT_ORIGIN_X, TRAFFIC_LIGHT_ORIGIN_Y};
|
||||
use shortcuts::bind_shortcuts;
|
||||
use url::Url;
|
||||
|
||||
use crate::brand::{DEEP_LINK_PREFIX, PRODUCT_NAME};
|
||||
|
||||
@@ -55,7 +56,7 @@ actions!(
|
||||
fn main() {
|
||||
init_tracing();
|
||||
let pending_deep_links = PendingDeepLinks::default();
|
||||
pending_deep_links.push(startup_deep_links(env::args().skip(1)));
|
||||
pending_deep_links.push(startup_open_urls(env::args().skip(1)));
|
||||
let open_url_queue = pending_deep_links.clone();
|
||||
let application = Application::new().with_assets(Assets);
|
||||
|
||||
@@ -248,7 +249,7 @@ fn open_deep_links(
|
||||
current_window: &Rc<RefCell<Option<BrowserWindowTarget>>>,
|
||||
cx: &mut App,
|
||||
) {
|
||||
let urls = urls.into_iter().filter_map(|url| parse_ely_deep_link(&url)).collect::<Vec<_>>();
|
||||
let urls = urls.into_iter().filter_map(|url| parse_open_url(&url)).collect::<Vec<_>>();
|
||||
|
||||
if urls.is_empty() {
|
||||
return;
|
||||
@@ -314,9 +315,19 @@ fn parse_ely_deep_link(value: &str) -> Option<UrlText> {
|
||||
.flatten()
|
||||
}
|
||||
|
||||
fn startup_deep_links(args: impl IntoIterator<Item = String>) -> Vec<String> {
|
||||
fn parse_open_url(value: &str) -> Option<UrlText> {
|
||||
parse_ely_deep_link(value).or_else(|| parse_external_open_url(value))
|
||||
}
|
||||
|
||||
fn parse_external_open_url(value: &str) -> Option<UrlText> {
|
||||
let parsed = UrlText::from_address_text(value).ok()?;
|
||||
let url = Url::parse(parsed.as_str()).ok()?;
|
||||
matches!(url.scheme(), "http" | "https").then(|| UrlText::parse(url.to_string()).ok()).flatten()
|
||||
}
|
||||
|
||||
fn startup_open_urls(args: impl IntoIterator<Item = String>) -> Vec<String> {
|
||||
args.into_iter()
|
||||
.filter_map(|arg| parse_ely_deep_link(&arg).map(|url| url.as_str().to_string()))
|
||||
.filter_map(|arg| parse_open_url(&arg).map(|url| url.as_str().to_string()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -346,7 +357,7 @@ fn open_private_window(_: &OpenPrivateWindow, cx: &mut App) {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{PendingDeepLinks, parse_ely_deep_link, startup_deep_links};
|
||||
use super::{PendingDeepLinks, parse_ely_deep_link, parse_open_url, startup_open_urls};
|
||||
|
||||
#[test]
|
||||
fn pending_deep_links_drains_urls_in_order() {
|
||||
@@ -381,16 +392,41 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn startup_deep_links_filter_and_normalize_args() {
|
||||
let links = startup_deep_links(
|
||||
["--ignored", " ELY://history ", "https://example.com", "ely://auth/callback?code=abc"]
|
||||
.into_iter()
|
||||
.map(str::to_string),
|
||||
fn parse_open_url_accepts_external_pages() {
|
||||
let url = parse_open_url(" HTTPS://example.com ");
|
||||
|
||||
assert_eq!(url.as_ref().map(|url| url.as_str()), Some("https://example.com/"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_open_url_accepts_address_text_domains() {
|
||||
let url = parse_open_url("servo.org");
|
||||
|
||||
assert_eq!(url.as_ref().map(|url| url.as_str()), Some("https://servo.org/"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn startup_open_urls_filter_and_normalize_args() {
|
||||
let links = startup_open_urls(
|
||||
[
|
||||
"--ignored",
|
||||
" ELY://history ",
|
||||
"https://example.com",
|
||||
"servo.org",
|
||||
"ely://auth/callback?code=abc",
|
||||
]
|
||||
.into_iter()
|
||||
.map(str::to_string),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
links,
|
||||
vec!["ely://history".to_string(), "ely://auth/callback?code=abc".to_string()]
|
||||
vec![
|
||||
"ely://history".to_string(),
|
||||
"https://example.com/".to_string(),
|
||||
"https://servo.org/".to_string(),
|
||||
"ely://auth/callback?code=abc".to_string(),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ use ely_domain::{
|
||||
use ely_servo_host::{
|
||||
HidpiScaleRequest, KeyboardTextRequest, MouseClickRequest, MouseHoverRequest,
|
||||
NavigationRequest, PageZoomRequest, PermissionDecision, PermissionRequest, ResizeRequest,
|
||||
ScrollRequest, ServoHost, ServoSurfaceSize, SoftwareServoHost,
|
||||
ScrollRequest, ServoHost, ServoHostError, ServoSurfaceSize, SoftwareServoHost,
|
||||
};
|
||||
|
||||
#[path = "servo_live_types.rs"]
|
||||
@@ -54,24 +54,18 @@ impl ServoLiveClient {
|
||||
self.apply_navigation(&request, &webview_id, tab_id, requested_url)?;
|
||||
self.apply_input(&request, &webview_id)?;
|
||||
// Match Servo's `examples/winit_minimal.rs`: spin the event loop on
|
||||
// the embedder-side hot path, never paint. Painting is reactive in
|
||||
// Servo — `notify_new_frame_ready` on the delegate flags the
|
||||
// session, and the next `poll` (gated on `has_pending_frame`)
|
||||
// performs the single paint + present for that frame. Forcing a
|
||||
// paint here would clear the surface to the WebRender background
|
||||
// and present it *before* Servo has composited the navigated
|
||||
// page, which is what produced the per-redirect white-flash on
|
||||
// sites that perform a chain of redirects (google.com → /
|
||||
// → /?zx=…). The `ServoLiveFrame` we return only carries the
|
||||
// snapshot metadata; the on-screen surface is owned by Servo via
|
||||
// the native NSView and updated through `poll`.
|
||||
// the embedder-side hot path, never paint. Servo's public
|
||||
// rendering contract says `notify_new_frame_ready` is the signal
|
||||
// for `WebView::paint`; URL/title/load-status callbacks are
|
||||
// metadata updates and travel through snapshots. Painting here
|
||||
// would present before Servo has composited the navigated page,
|
||||
// which produced per-redirect white flashes.
|
||||
self.host.tick();
|
||||
if !self.session_uses_native_surface(&request.tab_id) {
|
||||
return Err(ServoLiveError::NativeSurfaceUnavailable);
|
||||
if self.session_uses_native_surface(&request.tab_id) {
|
||||
return self.presented_frame_from_session(&request.tab_id, &webview_id).map(Some);
|
||||
}
|
||||
|
||||
let frame = self.frame_from_session(&request.tab_id, &webview_id)?;
|
||||
Ok(Some(frame))
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
pub fn poll(&mut self, tab_id: String) -> Result<Option<ServoLiveFrame>, ServoLiveError> {
|
||||
@@ -82,15 +76,22 @@ impl ServoLiveClient {
|
||||
let uses_native_surface = session.native_surface_id.is_some();
|
||||
|
||||
self.host.tick();
|
||||
if !self.host.snapshot(&webview_id)?.has_pending_frame() {
|
||||
let snapshot = self.host.snapshot(&webview_id)?;
|
||||
if !snapshot.has_pending_frame() && !snapshot.has_pending_metadata() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
if !uses_native_surface {
|
||||
return Err(ServoLiveError::NativeSurfaceUnavailable);
|
||||
if uses_native_surface {
|
||||
if snapshot.has_pending_frame() {
|
||||
self.host.paint_without_readback_with_completion(&webview_id, false)?;
|
||||
}
|
||||
return self.presented_frame_from_session(&tab_id, &webview_id).map(Some);
|
||||
}
|
||||
self.host.paint_without_readback_with_completion(&webview_id, false)?;
|
||||
self.frame_from_session(&tab_id, &webview_id).map(Some)
|
||||
|
||||
if snapshot.has_pending_frame() {
|
||||
self.host.paint(&webview_id)?;
|
||||
}
|
||||
self.rendered_frame_from_session(&tab_id, &webview_id)
|
||||
}
|
||||
|
||||
pub fn close(&mut self, tab_id: String) -> Result<(), ServoLiveError> {
|
||||
@@ -252,8 +253,7 @@ impl ServoLiveClient {
|
||||
// `set_history`) is the source of truth — if it already
|
||||
// matches the requested URL, this URL change came *from*
|
||||
// Servo and only needs an embedder-side bookkeeping sync.
|
||||
let servo_current_url =
|
||||
self.host.snapshot(webview_id)?.url().map(str::to_string);
|
||||
let servo_current_url = self.host.snapshot(webview_id)?.url().map(str::to_string);
|
||||
if servo_current_url.as_deref() == Some(requested_url.as_str()) {
|
||||
if let Some(session) = self.sessions.get_mut(&request.tab_id) {
|
||||
session.requested_url = Some(requested_url.as_str().to_string());
|
||||
@@ -314,7 +314,7 @@ impl ServoLiveClient {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn frame_from_session(
|
||||
fn presented_frame_from_session(
|
||||
&self,
|
||||
tab_id: &str,
|
||||
webview_id: &WebViewId,
|
||||
@@ -325,7 +325,7 @@ impl ServoLiveClient {
|
||||
}));
|
||||
};
|
||||
if session.native_surface_id.is_some() {
|
||||
let snapshot = self.host.snapshot(webview_id)?;
|
||||
let snapshot = self.host.snapshot_and_mark_metadata_observed(webview_id)?;
|
||||
return Ok(ServoLiveFrame::from_presented(
|
||||
snapshot,
|
||||
session.width,
|
||||
@@ -336,6 +336,29 @@ impl ServoLiveClient {
|
||||
Err(ServoLiveError::NativeSurfaceUnavailable)
|
||||
}
|
||||
|
||||
fn rendered_frame_from_session(
|
||||
&self,
|
||||
tab_id: &str,
|
||||
webview_id: &WebViewId,
|
||||
) -> Result<Option<ServoLiveFrame>, ServoLiveError> {
|
||||
let Some(session) = self.sessions.get(tab_id) else {
|
||||
return Err(ServoLiveError::Host(ServoHostError::WebViewNotFound {
|
||||
id: webview_id.clone(),
|
||||
}));
|
||||
};
|
||||
let rendered_frame = match self.host.last_rendered_frame() {
|
||||
Ok(frame) => frame,
|
||||
Err(ServoHostError::RenderedFrameUnavailable) => return Ok(None),
|
||||
Err(error) => return Err(ServoLiveError::Host(error)),
|
||||
};
|
||||
let snapshot = self.host.snapshot_and_mark_metadata_observed(webview_id)?;
|
||||
Ok(Some(ServoLiveFrame::from_rendered(
|
||||
snapshot,
|
||||
rendered_frame,
|
||||
session.device_pixel_ratio,
|
||||
)))
|
||||
}
|
||||
|
||||
fn session_uses_native_surface(&self, tab_id: &str) -> bool {
|
||||
self.sessions.get(tab_id).is_some_and(|session| session.native_surface_id.is_some())
|
||||
}
|
||||
@@ -425,12 +448,7 @@ mod tests {
|
||||
// physical dimensions, so only the hidpi push should fire.
|
||||
let change = ViewportChange::between(
|
||||
&request(2880, 1800, 2.0, SERVO_DEFAULT_PAGE_ZOOM_PERCENT),
|
||||
&session(
|
||||
2880,
|
||||
1800,
|
||||
SERVO_DEFAULT_DEVICE_PIXEL_RATIO,
|
||||
SERVO_DEFAULT_PAGE_ZOOM_PERCENT,
|
||||
),
|
||||
&session(2880, 1800, SERVO_DEFAULT_DEVICE_PIXEL_RATIO, SERVO_DEFAULT_PAGE_ZOOM_PERCENT),
|
||||
);
|
||||
assert_eq!(
|
||||
change,
|
||||
@@ -443,18 +461,8 @@ mod tests {
|
||||
// On a 1.0-DPR display the fresh session already matches the
|
||||
// request — Servo's defaults are exactly what we asked for.
|
||||
let change = ViewportChange::between(
|
||||
&request(
|
||||
1280,
|
||||
720,
|
||||
SERVO_DEFAULT_DEVICE_PIXEL_RATIO,
|
||||
SERVO_DEFAULT_PAGE_ZOOM_PERCENT,
|
||||
),
|
||||
&session(
|
||||
1280,
|
||||
720,
|
||||
SERVO_DEFAULT_DEVICE_PIXEL_RATIO,
|
||||
SERVO_DEFAULT_PAGE_ZOOM_PERCENT,
|
||||
),
|
||||
&request(1280, 720, SERVO_DEFAULT_DEVICE_PIXEL_RATIO, SERVO_DEFAULT_PAGE_ZOOM_PERCENT),
|
||||
&session(1280, 720, SERVO_DEFAULT_DEVICE_PIXEL_RATIO, SERVO_DEFAULT_PAGE_ZOOM_PERCENT),
|
||||
);
|
||||
assert_eq!(change, ViewportChange::default());
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use ely_domain::SitePermissionDecision;
|
||||
use ely_servo_host::{ServoHostError, WebViewSnapshot, WebViewState};
|
||||
use ely_servo_host::{RenderedFrame, ServoHostError, WebViewSnapshot, WebViewState};
|
||||
use gpui::NativeSurfaceHandle;
|
||||
use serde::Serialize;
|
||||
use thiserror::Error;
|
||||
@@ -90,6 +90,34 @@ impl ServoLiveFrame {
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn from_rendered(
|
||||
snapshot: WebViewSnapshot,
|
||||
rendered_frame: RenderedFrame,
|
||||
device_pixel_ratio: f32,
|
||||
) -> Self {
|
||||
let width = rendered_frame.width();
|
||||
let height = rendered_frame.height();
|
||||
let (css_viewport_width, css_viewport_height) =
|
||||
css_viewport_size(width, height, device_pixel_ratio);
|
||||
Self {
|
||||
loaded_url: snapshot.url().map(str::to_string),
|
||||
title: snapshot.title().map(str::to_string),
|
||||
render_state: render_state_label(snapshot.state()).to_string(),
|
||||
width,
|
||||
height,
|
||||
device_pixel_ratio,
|
||||
css_viewport_width,
|
||||
css_viewport_height,
|
||||
#[cfg(all(test, feature = "live-site-smoke"))]
|
||||
non_white_pixel_count: rendered_frame.non_white_pixel_count(),
|
||||
#[cfg(all(test, feature = "live-site-smoke"))]
|
||||
content_pixel_count: rendered_frame.content_pixel_count(),
|
||||
#[cfg(all(test, feature = "live-site-smoke"))]
|
||||
sample_hash: rendered_frame.sample_hash(),
|
||||
rgba_bytes: Some(rendered_frame.rgba_bytes().to_vec()),
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn loaded_url(&self) -> Option<&str> {
|
||||
self.loaded_url.as_deref()
|
||||
@@ -155,6 +183,9 @@ impl ServoLiveFrame {
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn for_test(width: u32, height: u32, rgba_bytes: Vec<u8>) -> Self {
|
||||
#[cfg(all(test, feature = "live-site-smoke"))]
|
||||
let summary =
|
||||
ely_servo_host::RenderedFrameSummary::from_rgba_bytes(width, height, &rgba_bytes);
|
||||
Self {
|
||||
loaded_url: Some("https://example.com/".to_string()),
|
||||
title: Some("Example".to_string()),
|
||||
@@ -165,11 +196,11 @@ impl ServoLiveFrame {
|
||||
css_viewport_width: width,
|
||||
css_viewport_height: height,
|
||||
#[cfg(all(test, feature = "live-site-smoke"))]
|
||||
non_white_pixel_count: 0,
|
||||
non_white_pixel_count: summary.non_white_pixel_count(),
|
||||
#[cfg(all(test, feature = "live-site-smoke"))]
|
||||
content_pixel_count: 0,
|
||||
content_pixel_count: summary.content_pixel_count(),
|
||||
#[cfg(all(test, feature = "live-site-smoke"))]
|
||||
sample_hash: 0,
|
||||
sample_hash: summary.sample_hash(),
|
||||
rgba_bytes: Some(rgba_bytes),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,10 +79,8 @@ impl WebSurfaceStore {
|
||||
// which destroys and reallocates the framebuffer — the source of
|
||||
// the per-frame blank flash. The first ensure (when no prior
|
||||
// `last_ensure_key` is set) always fires so the page can load.
|
||||
let already_ensured = self
|
||||
.surfaces
|
||||
.get(tab.id())
|
||||
.is_some_and(|surface| surface.last_ensure_key.is_some());
|
||||
let already_ensured =
|
||||
self.surfaces.get(tab.id()).is_some_and(|surface| surface.last_ensure_key.is_some());
|
||||
if already_ensured
|
||||
&& self
|
||||
.surfaces
|
||||
@@ -211,9 +209,9 @@ impl WebSurfaceStore {
|
||||
// one frame of the gesture settling. Without this boost the
|
||||
// idle 80 ms polling adds a noticeable lag between letting go
|
||||
// of a resize and the page re-laying-out at the final size.
|
||||
let any_settling = visible_tab_ids
|
||||
.iter()
|
||||
.any(|tab_id| self.surfaces.get(tab_id).is_some_and(|s| s.viewport_size_is_settling(now)));
|
||||
let any_settling = visible_tab_ids.iter().any(|tab_id| {
|
||||
self.surfaces.get(tab_id).is_some_and(|s| s.viewport_size_is_settling(now))
|
||||
});
|
||||
if any_settling {
|
||||
return ACTIVE_POLL_INTERVAL;
|
||||
}
|
||||
|
||||
@@ -201,7 +201,7 @@ fn assert_web_surface_resizes_prd_site() -> Result<(), Box<dyn Error>> {
|
||||
store.ensure_surface(&tab, ProfileDataMode::Transient, &[]);
|
||||
let _ = wait_for_ready_frame_at_size(
|
||||
&mut store,
|
||||
tab.id(),
|
||||
&tab,
|
||||
case,
|
||||
LIVE_SURFACE_WIDTH,
|
||||
LIVE_SURFACE_HEIGHT,
|
||||
@@ -216,7 +216,7 @@ fn assert_web_surface_resizes_prd_site() -> Result<(), Box<dyn Error>> {
|
||||
store.ensure_surface(&tab, ProfileDataMode::Transient, &[]);
|
||||
wait_for_ready_frame_at_size(
|
||||
&mut store,
|
||||
tab.id(),
|
||||
&tab,
|
||||
case,
|
||||
RESIZED_LIVE_SURFACE_WIDTH,
|
||||
RESIZED_LIVE_SURFACE_HEIGHT,
|
||||
@@ -377,7 +377,7 @@ fn wait_for_ready_frame_at_scroll(
|
||||
|
||||
fn wait_for_ready_frame_at_size(
|
||||
store: &mut WebSurfaceStore,
|
||||
tab_id: &TabId,
|
||||
tab: &BrowserTab,
|
||||
case: &LiveSiteCase,
|
||||
expected_width: u32,
|
||||
expected_height: u32,
|
||||
@@ -386,8 +386,9 @@ fn wait_for_ready_frame_at_size(
|
||||
let mut last_error = None;
|
||||
|
||||
loop {
|
||||
store.tick(std::slice::from_ref(tab_id));
|
||||
match store.state(tab_id) {
|
||||
store.ensure_surface(tab, ProfileDataMode::Transient, &[]);
|
||||
store.tick(std::slice::from_ref(tab.id()));
|
||||
match store.state(tab.id()) {
|
||||
Some(WebSurfaceState::Ready(frame))
|
||||
if frame.size().width == expected_width
|
||||
&& frame.size().height == expected_height =>
|
||||
|
||||
@@ -395,7 +395,9 @@ impl WebSurfaceRuntime {
|
||||
let Some(scoped) = self.worker.take() else {
|
||||
return;
|
||||
};
|
||||
if let Some(path) = scoped.transient_profile_data_dir {
|
||||
let ScopedWorker { worker, transient_profile_data_dir } = scoped;
|
||||
drop(worker);
|
||||
if let Some(path) = transient_profile_data_dir {
|
||||
let _ = fs::remove_dir_all(path);
|
||||
}
|
||||
}
|
||||
@@ -404,7 +406,9 @@ impl WebSurfaceRuntime {
|
||||
let Some(scoped) = self.direct_client.take() else {
|
||||
return;
|
||||
};
|
||||
if let Some(path) = scoped.transient_profile_data_dir {
|
||||
let ScopedDirectClient { client, transient_profile_data_dir } = scoped;
|
||||
drop(client);
|
||||
if let Some(path) = transient_profile_data_dir {
|
||||
let _ = fs::remove_dir_all(path);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user