Add Servo page zoom support

This commit is contained in:
2026-05-09 12:32:44 -04:00
parent 088d1166c4
commit 1faa7423c1
26 changed files with 697 additions and 280 deletions
+38 -2
View File
@@ -45,8 +45,8 @@ use web_surface::WebSurfaceStore;
use crate::{
CloseCurrentTab, FocusAddressBar, FocusCommandMode, OpenDownloads, OpenHistory, OpenNewTab,
OpenSettings, OpenTaskManager, RestoreClosedTab, SelectNextTab, SelectPreviousTab,
ToggleFavoriteTab, TogglePinnedTab,
OpenSettings, OpenTaskManager, ResetZoom, RestoreClosedTab, SelectNextTab, SelectPreviousTab,
ToggleFavoriteTab, TogglePinnedTab, ZoomIn, ZoomOut,
};
enum ShellState {
@@ -284,6 +284,30 @@ impl ElyShell {
}
}
fn zoom_active_tab_in(&mut self, cx: &mut Context<Self>) {
if let ShellState::Ready(core) = &mut self.state
&& core.zoom_active_tab_in().is_ok()
{
cx.notify();
}
}
fn zoom_active_tab_out(&mut self, cx: &mut Context<Self>) {
if let ShellState::Ready(core) = &mut self.state
&& core.zoom_active_tab_out().is_ok()
{
cx.notify();
}
}
fn reset_active_tab_zoom(&mut self, cx: &mut Context<Self>) {
if let ShellState::Ready(core) = &mut self.state
&& core.reset_active_tab_zoom().is_ok()
{
cx.notify();
}
}
fn on_close_current_tab(
&mut self,
_: &CloseCurrentTab,
@@ -350,6 +374,10 @@ impl ElyShell {
self.restore_closed_tab(window, cx);
}
fn on_reset_zoom(&mut self, _: &ResetZoom, _: &mut Window, cx: &mut Context<Self>) {
self.reset_active_tab_zoom(cx);
}
fn on_select_next_tab(
&mut self,
_: &SelectNextTab,
@@ -385,4 +413,12 @@ impl ElyShell {
) {
self.toggle_active_tab_pinned(cx);
}
fn on_zoom_in(&mut self, _: &ZoomIn, _: &mut Window, cx: &mut Context<Self>) {
self.zoom_active_tab_in(cx);
}
fn on_zoom_out(&mut self, _: &ZoomOut, _: &mut Window, cx: &mut Context<Self>) {
self.zoom_active_tab_out(cx);
}
}
+3
View File
@@ -51,6 +51,7 @@ impl ElyShell {
.on_action(cx.listener(Self::on_open_new_tab))
.on_action(cx.listener(Self::on_open_settings))
.on_action(cx.listener(Self::on_open_task_manager))
.on_action(cx.listener(Self::on_reset_zoom))
.on_action(cx.listener(Self::on_restore_closed_tab))
.on_action(cx.listener(Self::on_select_next_space))
.on_action(cx.listener(Self::on_select_next_tab))
@@ -60,6 +61,8 @@ impl ElyShell {
.on_action(cx.listener(Self::on_toggle_favorite_tab))
.on_action(cx.listener(Self::on_toggle_pinned_tab))
.on_action(cx.listener(Self::on_toggle_sidebar))
.on_action(cx.listener(Self::on_zoom_in))
.on_action(cx.listener(Self::on_zoom_out))
.bg(rgb(ELY_THEME.canvas))
.text_color(rgb(ELY_THEME.ink))
.flex()
+62 -111
View File
@@ -12,7 +12,7 @@ use super::{
},
web_surface_state::{
WebSurfaceClickState, WebSurfaceClient, WebSurfaceKeyboardFocusState, WebSurfaceRequest,
WebSurfaceScrollState, WebSurfaceState, WebSurfaceTextInputState,
WebSurfaceScrollState, WebSurfaceState, WebSurfaceStateKey, WebSurfaceTextInputState,
},
};
@@ -59,20 +59,22 @@ impl WebSurfaceStore {
let size = self.viewport_sizes.get(tab.id()).copied()?;
let requested_url = tab.url().as_str().to_string();
let scroll_offset = self.scroll_offset_for(tab.id(), requested_url.as_str());
let zoom_percent = tab.zoom_percent();
let click_point = self.click_point_for(tab.id(), requested_url.as_str(), scroll_offset);
let typed_text =
self.typed_text_for(tab.id(), requested_url.as_str(), scroll_offset, click_point);
if self.is_loading_requested_url(tab.id(), requested_url.as_str()) {
return None;
}
if self.has_current_state(
tab.id(),
&requested_url,
let state_key = WebSurfaceStateKey {
requested_url: &requested_url,
size,
scroll_offset,
zoom_percent,
click_point,
typed_text.as_deref(),
) {
typed_text: typed_text.as_deref(),
};
if self.has_current_state(tab.id(), state_key) {
return None;
}
@@ -85,6 +87,7 @@ impl WebSurfaceStore {
requested_url,
size,
scroll_offset,
zoom_percent,
click_point,
typed_text: typed_text.clone(),
message: message.clone(),
@@ -100,9 +103,14 @@ impl WebSurfaceStore {
requested_url: requested_url.clone(),
size,
scroll_offset,
zoom_percent,
click_point,
typed_text: typed_text.clone(),
previous_frame: self.previous_ready_frame(tab.id(), requested_url.as_str()),
previous_frame: self.previous_ready_frame(
tab.id(),
requested_url.as_str(),
zoom_percent,
),
},
);
@@ -113,7 +121,8 @@ impl WebSurfaceStore {
size.height,
)
.with_profile_data_mode(profile_data_mode)
.with_scroll_offset(scroll_offset.x(), scroll_offset.y());
.with_scroll_offset(scroll_offset.x(), scroll_offset.y())
.with_page_zoom_percent(zoom_percent);
if let Some(click_point) = click_point {
snapshot_request = snapshot_request.with_click_point(click_point.x(), click_point.y());
}
@@ -126,6 +135,7 @@ impl WebSurfaceStore {
requested_url,
size,
scroll_offset,
zoom_percent,
click_point,
typed_text,
client,
@@ -133,79 +143,70 @@ impl WebSurfaceStore {
})
}
fn has_current_state(
&self,
tab_id: &TabId,
requested_url: &str,
size: WebSurfaceSize,
scroll_offset: WebSurfaceScrollOffset,
click_point: Option<WebSurfaceClickPoint>,
typed_text: Option<&str>,
) -> bool {
fn has_current_state(&self, tab_id: &TabId, key: WebSurfaceStateKey<'_>) -> bool {
match self.states.get(tab_id) {
Some(WebSurfaceState::Loading {
requested_url: current_url,
size: current_size,
scroll_offset: current_scroll_offset,
zoom_percent: current_zoom_percent,
click_point: current_click_point,
typed_text: current_typed_text,
..
}) => {
current_url == requested_url
&& *current_size == size
&& *current_scroll_offset == scroll_offset
&& *current_click_point == click_point
&& current_typed_text.as_deref() == typed_text
current_url == key.requested_url
&& *current_size == key.size
&& *current_scroll_offset == key.scroll_offset
&& *current_zoom_percent == key.zoom_percent
&& *current_click_point == key.click_point
&& current_typed_text.as_deref() == key.typed_text
}
Some(WebSurfaceState::Ready(frame)) => {
frame.requested_url == requested_url
&& frame.size() == size
&& frame.scroll_offset() == scroll_offset
&& frame.click_point() == click_point
&& frame.typed_text() == typed_text
frame.requested_url == key.requested_url
&& frame.size() == key.size
&& frame.scroll_offset() == key.scroll_offset
&& frame.zoom_percent() == key.zoom_percent
&& frame.click_point() == key.click_point
&& frame.typed_text() == key.typed_text
}
Some(WebSurfaceState::Failed {
requested_url: current_url,
size: current_size,
scroll_offset: current_scroll_offset,
zoom_percent: current_zoom_percent,
click_point: current_click_point,
typed_text: current_typed_text,
..
}) => {
current_url == requested_url
&& *current_size == size
&& *current_scroll_offset == scroll_offset
&& *current_click_point == click_point
&& current_typed_text.as_deref() == typed_text
current_url == key.requested_url
&& *current_size == key.size
&& *current_scroll_offset == key.scroll_offset
&& *current_zoom_percent == key.zoom_percent
&& *current_click_point == key.click_point
&& current_typed_text.as_deref() == key.typed_text
}
None => false,
}
}
pub(super) fn is_loading(
&self,
tab_id: &TabId,
requested_url: &str,
size: WebSurfaceSize,
scroll_offset: WebSurfaceScrollOffset,
click_point: Option<WebSurfaceClickPoint>,
typed_text: Option<&str>,
) -> bool {
pub(super) fn is_loading(&self, tab_id: &TabId, key: WebSurfaceStateKey<'_>) -> bool {
matches!(
self.states.get(tab_id),
Some(WebSurfaceState::Loading {
requested_url: current_url,
size: current_size,
scroll_offset: current_scroll_offset,
zoom_percent: current_zoom_percent,
click_point: current_click_point,
typed_text: current_typed_text,
..
})
if current_url == requested_url
&& *current_size == size
&& *current_scroll_offset == scroll_offset
&& *current_click_point == click_point
&& current_typed_text.as_deref() == typed_text
if current_url == key.requested_url
&& *current_size == key.size
&& *current_scroll_offset == key.scroll_offset
&& *current_zoom_percent == key.zoom_percent
&& *current_click_point == key.click_point
&& current_typed_text.as_deref() == key.typed_text
)
}
@@ -217,14 +218,24 @@ impl WebSurfaceStore {
)
}
fn previous_ready_frame(&self, tab_id: &TabId, requested_url: &str) -> Option<WebSurfaceFrame> {
fn previous_ready_frame(
&self,
tab_id: &TabId,
requested_url: &str,
zoom_percent: u16,
) -> Option<WebSurfaceFrame> {
match self.states.get(tab_id) {
Some(WebSurfaceState::Ready(frame)) if frame.requested_url == requested_url => {
Some(WebSurfaceState::Ready(frame))
if frame.requested_url == requested_url && frame.zoom_percent() == zoom_percent =>
{
Some(frame.clone())
}
Some(WebSurfaceState::Loading {
requested_url: current_url, previous_frame, ..
}) if current_url == requested_url => previous_frame.clone(),
}) if current_url == requested_url => previous_frame
.as_ref()
.filter(|frame| frame.zoom_percent() == zoom_percent)
.cloned(),
_ => None,
}
}
@@ -409,68 +420,8 @@ pub(super) fn is_external_web_url(url: &str) -> bool {
}
#[cfg(test)]
mod tests {
use std::error::Error;
use ely_domain::{BrowserTab, ProfileId, SpaceId, TabId, UrlText};
use gpui::{Bounds, point, px, size};
use super::{ProfileDataMode, WebSurfaceStore};
#[test]
fn typed_text_enters_snapshot_request_after_clicked_viewport() -> Result<(), Box<dyn Error>> {
let mut store = WebSurfaceStore::new();
let tab = web_tab("https://example.com/form")?;
let bounds = Bounds::new(point(px(0.0), px(0.0)), size(px(640.0), px(480.0)));
assert!(store.record_viewport_size(tab.id(), bounds));
assert!(store.record_click_point(
tab.id(),
tab.url().as_str(),
point(px(160.0), px(120.0))
));
assert!(store.record_typed_text(tab.id(), tab.url().as_str(), "e"));
assert!(store.record_typed_text(tab.id(), tab.url().as_str(), "l"));
let request = store
.prepare_request(&tab, ProfileDataMode::Persistent)
.ok_or("missing web surface request")?;
assert_eq!(request.typed_text.as_deref(), Some("el"));
assert_eq!(request.snapshot_request.typed_text_for_test(), Some("el"));
assert_eq!(request.snapshot_request.profile_id_for_test(), tab.profile_id());
Ok(())
}
#[test]
fn private_profile_enters_transient_snapshot_request() -> Result<(), Box<dyn Error>> {
let mut store = WebSurfaceStore::new();
let tab = web_tab("https://example.com/private")?;
let bounds = Bounds::new(point(px(0.0), px(0.0)), size(px(640.0), px(480.0)));
assert!(store.record_viewport_size(tab.id(), bounds));
let request = store
.prepare_request(&tab, ProfileDataMode::Transient)
.ok_or("missing web surface request")?;
assert_eq!(
request.snapshot_request.profile_data_mode_for_test(),
ProfileDataMode::Transient
);
Ok(())
}
fn web_tab(url: &str) -> Result<BrowserTab, Box<dyn Error>> {
Ok(BrowserTab::new(
TabId::new(),
SpaceId::new(),
ProfileId::new(),
"Web",
UrlText::parse(url)?,
))
}
}
#[path = "web_surface_tests.rs"]
mod tests;
#[cfg(all(test, feature = "live-site-smoke"))]
#[path = "web_surface_live_site_tests.rs"]
@@ -12,7 +12,7 @@ use super::{
web_surface_frame::WebSurfaceFrame,
web_surface_geometry::{WebSurfaceClickPoint, WebSurfaceScrollOffset, WebSurfaceSize},
web_surface_permissions::sidecar_site_permissions_for_tab,
web_surface_state::{WebSurfaceRequest, WebSurfaceState},
web_surface_state::{WebSurfaceRequest, WebSurfaceState, WebSurfaceStateKey},
web_surface_view::{
render_failed_web_surface, render_loading_web_surface, render_ready_web_surface,
},
@@ -23,6 +23,7 @@ struct PendingWebSurfaceFrame {
requested_url: String,
size: WebSurfaceSize,
scroll_offset: WebSurfaceScrollOffset,
zoom_percent: u16,
click_point: Option<WebSurfaceClickPoint>,
typed_text: Option<String>,
}
@@ -74,6 +75,7 @@ impl ElyShell {
requested_url,
size,
scroll_offset,
zoom_percent,
click_point,
typed_text,
client,
@@ -85,6 +87,7 @@ impl ElyShell {
requested_url,
size,
scroll_offset,
zoom_percent,
click_point,
typed_text,
};
@@ -112,17 +115,19 @@ impl ElyShell {
requested_url,
size,
scroll_offset,
zoom_percent,
click_point,
typed_text,
} = pending_frame;
if !self.web_surfaces.is_loading(
&tab_id,
requested_url.as_str(),
let state_key = WebSurfaceStateKey {
requested_url: requested_url.as_str(),
size,
scroll_offset,
zoom_percent,
click_point,
typed_text.as_deref(),
) {
typed_text: typed_text.as_deref(),
};
if !self.web_surfaces.is_loading(&tab_id, state_key) {
return;
}
@@ -130,6 +135,7 @@ impl ElyShell {
Ok(snapshot) => match WebSurfaceFrame::from_snapshot(
requested_url.clone(),
scroll_offset,
zoom_percent,
click_point,
typed_text.clone(),
snapshot,
@@ -139,6 +145,7 @@ impl ElyShell {
requested_url,
size,
scroll_offset,
zoom_percent,
click_point,
typed_text,
message: error.to_string(),
@@ -148,6 +155,7 @@ impl ElyShell {
requested_url,
size,
scroll_offset,
zoom_percent,
click_point,
typed_text,
message: error.to_string(),
@@ -20,6 +20,7 @@ pub(super) struct WebSurfaceFrame {
width: u32,
height: u32,
scroll_offset: WebSurfaceScrollOffset,
zoom_percent: u16,
click_point: Option<WebSurfaceClickPoint>,
typed_text: Option<String>,
#[cfg(all(test, feature = "live-site-smoke"))]
@@ -35,6 +36,7 @@ impl WebSurfaceFrame {
pub(super) fn from_snapshot(
requested_url: String,
scroll_offset: WebSurfaceScrollOffset,
zoom_percent: u16,
click_point: Option<WebSurfaceClickPoint>,
typed_text: Option<String>,
snapshot: SidecarSnapshot,
@@ -66,6 +68,7 @@ impl WebSurfaceFrame {
width,
height,
scroll_offset,
zoom_percent,
click_point,
typed_text,
#[cfg(all(test, feature = "live-site-smoke"))]
@@ -89,6 +92,9 @@ impl WebSurfaceFrame {
pub(super) fn detail_label(&self) -> String {
let mut detail =
format!("{} {}", self.render_state(), self.scroll_offset.detail_label(self.size()));
if self.zoom_percent != ely_domain::DEFAULT_ZOOM_PERCENT {
detail = format!("{detail} zoom={}%", self.zoom_percent);
}
if let Some(click_point) = self.click_point {
detail = format!("{detail} {}", click_point.detail_label());
}
@@ -110,6 +116,10 @@ impl WebSurfaceFrame {
self.scroll_offset
}
pub(super) fn zoom_percent(&self) -> u16 {
self.zoom_percent
}
pub(super) fn click_point(&self) -> Option<WebSurfaceClickPoint> {
self.click_point
}
@@ -61,6 +61,7 @@ fn render_web_surface_frame(case: &LiveSiteCase) -> Result<WebSurfaceFrame, Box<
let frame = WebSurfaceFrame::from_snapshot(
request.requested_url,
request.scroll_offset,
request.zoom_percent,
request.click_point,
request.typed_text,
snapshot,
@@ -40,6 +40,16 @@ pub(super) struct WebSurfaceTextInputState {
pub(super) text: String,
}
#[derive(Clone, Copy)]
pub(super) struct WebSurfaceStateKey<'a> {
pub(super) requested_url: &'a str,
pub(super) size: WebSurfaceSize,
pub(super) scroll_offset: WebSurfaceScrollOffset,
pub(super) zoom_percent: u16,
pub(super) click_point: Option<WebSurfaceClickPoint>,
pub(super) typed_text: Option<&'a str>,
}
pub(super) enum WebSurfaceClient {
Ready(ServoSidecarClient),
Unavailable(String),
@@ -59,6 +69,7 @@ pub(super) struct WebSurfaceRequest {
pub(super) requested_url: String,
pub(super) size: WebSurfaceSize,
pub(super) scroll_offset: WebSurfaceScrollOffset,
pub(super) zoom_percent: u16,
pub(super) click_point: Option<WebSurfaceClickPoint>,
pub(super) typed_text: Option<String>,
pub(super) client: ServoSidecarClient,
@@ -70,6 +81,7 @@ pub(super) enum WebSurfaceState {
requested_url: String,
size: WebSurfaceSize,
scroll_offset: WebSurfaceScrollOffset,
zoom_percent: u16,
click_point: Option<WebSurfaceClickPoint>,
typed_text: Option<String>,
previous_frame: Option<WebSurfaceFrame>,
@@ -79,6 +91,7 @@ pub(super) enum WebSurfaceState {
requested_url: String,
size: WebSurfaceSize,
scroll_offset: WebSurfaceScrollOffset,
zoom_percent: u16,
click_point: Option<WebSurfaceClickPoint>,
typed_text: Option<String>,
message: String,
@@ -0,0 +1,70 @@
use std::error::Error;
use ely_domain::{BrowserTab, ProfileId, SpaceId, TabId, UrlText};
use gpui::{Bounds, point, px, size};
use super::{ProfileDataMode, WebSurfaceStore};
#[test]
fn typed_text_enters_snapshot_request_after_clicked_viewport() -> Result<(), Box<dyn Error>> {
let mut store = WebSurfaceStore::new();
let tab = web_tab("https://example.com/form")?;
let bounds = Bounds::new(point(px(0.0), px(0.0)), size(px(640.0), px(480.0)));
assert!(store.record_viewport_size(tab.id(), bounds));
assert!(store.record_click_point(tab.id(), tab.url().as_str(), point(px(160.0), px(120.0))));
assert!(store.record_typed_text(tab.id(), tab.url().as_str(), "e"));
assert!(store.record_typed_text(tab.id(), tab.url().as_str(), "l"));
let request = store
.prepare_request(&tab, ProfileDataMode::Persistent)
.ok_or("missing web surface request")?;
assert_eq!(request.typed_text.as_deref(), Some("el"));
assert_eq!(request.snapshot_request.typed_text_for_test(), Some("el"));
assert_eq!(request.snapshot_request.profile_id_for_test(), tab.profile_id());
assert_eq!(request.zoom_percent, ely_domain::DEFAULT_ZOOM_PERCENT);
assert_eq!(
request.snapshot_request.page_zoom_percent_for_test(),
ely_domain::DEFAULT_ZOOM_PERCENT
);
Ok(())
}
#[test]
fn private_profile_enters_transient_snapshot_request() -> Result<(), Box<dyn Error>> {
let mut store = WebSurfaceStore::new();
let tab = web_tab("https://example.com/private")?;
let bounds = Bounds::new(point(px(0.0), px(0.0)), size(px(640.0), px(480.0)));
assert!(store.record_viewport_size(tab.id(), bounds));
let request = store
.prepare_request(&tab, ProfileDataMode::Transient)
.ok_or("missing web surface request")?;
assert_eq!(request.snapshot_request.profile_data_mode_for_test(), ProfileDataMode::Transient);
Ok(())
}
#[test]
fn tab_zoom_enters_snapshot_request() -> Result<(), Box<dyn Error>> {
let mut store = WebSurfaceStore::new();
let mut tab = web_tab("https://example.com/zoom")?;
tab.set_zoom_percent(125)?;
let bounds = Bounds::new(point(px(0.0), px(0.0)), size(px(640.0), px(480.0)));
assert!(store.record_viewport_size(tab.id(), bounds));
let request = store
.prepare_request(&tab, ProfileDataMode::Persistent)
.ok_or("missing web surface request")?;
assert_eq!(request.zoom_percent, 125);
assert_eq!(request.snapshot_request.page_zoom_percent_for_test(), 125);
Ok(())
}
fn web_tab(url: &str) -> Result<BrowserTab, Box<dyn Error>> {
Ok(BrowserTab::new(TabId::new(), SpaceId::new(), ProfileId::new(), "Web", UrlText::parse(url)?))
}