Add Servo page zoom support
This commit is contained in:
@@ -36,6 +36,7 @@ actions!(
|
|||||||
OpenTaskManager,
|
OpenTaskManager,
|
||||||
Quit,
|
Quit,
|
||||||
RestoreClosedTab,
|
RestoreClosedTab,
|
||||||
|
ResetZoom,
|
||||||
SelectNextSpace,
|
SelectNextSpace,
|
||||||
SelectNextTab,
|
SelectNextTab,
|
||||||
SelectPreviousSpace,
|
SelectPreviousSpace,
|
||||||
@@ -44,6 +45,8 @@ actions!(
|
|||||||
ToggleFavoriteTab,
|
ToggleFavoriteTab,
|
||||||
TogglePinnedTab,
|
TogglePinnedTab,
|
||||||
ToggleSidebar,
|
ToggleSidebar,
|
||||||
|
ZoomIn,
|
||||||
|
ZoomOut,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -78,6 +81,10 @@ fn main() {
|
|||||||
MenuItem::action("Command Mode", FocusCommandMode),
|
MenuItem::action("Command Mode", FocusCommandMode),
|
||||||
MenuItem::action("Toggle Sidebar", ToggleSidebar),
|
MenuItem::action("Toggle Sidebar", ToggleSidebar),
|
||||||
MenuItem::separator(),
|
MenuItem::separator(),
|
||||||
|
MenuItem::action("Zoom In", ZoomIn),
|
||||||
|
MenuItem::action("Zoom Out", ZoomOut),
|
||||||
|
MenuItem::action("Reset Zoom", ResetZoom),
|
||||||
|
MenuItem::separator(),
|
||||||
MenuItem::action("Close Tab", CloseCurrentTab),
|
MenuItem::action("Close Tab", CloseCurrentTab),
|
||||||
MenuItem::separator(),
|
MenuItem::separator(),
|
||||||
MenuItem::action("Restore Closed Tab", RestoreClosedTab),
|
MenuItem::action("Restore Closed Tab", RestoreClosedTab),
|
||||||
|
|||||||
@@ -165,7 +165,9 @@ fn append_snapshot_args(
|
|||||||
.arg("--scroll-x")
|
.arg("--scroll-x")
|
||||||
.arg(request.scroll_x.to_string())
|
.arg(request.scroll_x.to_string())
|
||||||
.arg("--scroll-y")
|
.arg("--scroll-y")
|
||||||
.arg(request.scroll_y.to_string());
|
.arg(request.scroll_y.to_string())
|
||||||
|
.arg("--page-zoom-percent")
|
||||||
|
.arg(request.page_zoom_percent.to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SidecarSnapshotRequest {
|
impl SidecarSnapshotRequest {
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
use ely_domain::{ProfileId, SiteOrigin, SitePermissionDecision, SitePermissionFeature, UrlText};
|
use ely_domain::{
|
||||||
|
DEFAULT_ZOOM_PERCENT, ProfileId, SiteOrigin, SitePermissionDecision, SitePermissionFeature,
|
||||||
|
UrlText,
|
||||||
|
};
|
||||||
|
|
||||||
use super::ProfileDataMode;
|
use super::ProfileDataMode;
|
||||||
|
|
||||||
@@ -11,6 +14,7 @@ pub struct SidecarSnapshotRequest {
|
|||||||
pub(in crate::services) height: u32,
|
pub(in crate::services) height: u32,
|
||||||
pub(in crate::services) scroll_x: i32,
|
pub(in crate::services) scroll_x: i32,
|
||||||
pub(in crate::services) scroll_y: i32,
|
pub(in crate::services) scroll_y: i32,
|
||||||
|
pub(in crate::services) page_zoom_percent: u16,
|
||||||
pub(in crate::services) click_point: Option<SidecarClickPoint>,
|
pub(in crate::services) click_point: Option<SidecarClickPoint>,
|
||||||
pub(in crate::services) typed_text: Option<String>,
|
pub(in crate::services) typed_text: Option<String>,
|
||||||
pub(in crate::services) site_permissions: Vec<SidecarSitePermission>,
|
pub(in crate::services) site_permissions: Vec<SidecarSitePermission>,
|
||||||
@@ -27,6 +31,7 @@ impl SidecarSnapshotRequest {
|
|||||||
height,
|
height,
|
||||||
scroll_x: 0,
|
scroll_x: 0,
|
||||||
scroll_y: 0,
|
scroll_y: 0,
|
||||||
|
page_zoom_percent: DEFAULT_ZOOM_PERCENT,
|
||||||
click_point: None,
|
click_point: None,
|
||||||
typed_text: None,
|
typed_text: None,
|
||||||
site_permissions: Vec::new(),
|
site_permissions: Vec::new(),
|
||||||
@@ -46,6 +51,12 @@ impl SidecarSnapshotRequest {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn with_page_zoom_percent(mut self, page_zoom_percent: u16) -> Self {
|
||||||
|
self.page_zoom_percent = page_zoom_percent;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn with_click_point(mut self, x: u32, y: u32) -> Self {
|
pub fn with_click_point(mut self, x: u32, y: u32) -> Self {
|
||||||
self.click_point = Some(SidecarClickPoint { x, y });
|
self.click_point = Some(SidecarClickPoint { x, y });
|
||||||
@@ -78,6 +89,11 @@ impl SidecarSnapshotRequest {
|
|||||||
pub(crate) fn profile_data_mode_for_test(&self) -> ProfileDataMode {
|
pub(crate) fn profile_data_mode_for_test(&self) -> ProfileDataMode {
|
||||||
self.profile_data_mode
|
self.profile_data_mode
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub(crate) fn page_zoom_percent_for_test(&self) -> u16 {
|
||||||
|
self.page_zoom_percent
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
|||||||
@@ -45,8 +45,8 @@ use web_surface::WebSurfaceStore;
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
CloseCurrentTab, FocusAddressBar, FocusCommandMode, OpenDownloads, OpenHistory, OpenNewTab,
|
CloseCurrentTab, FocusAddressBar, FocusCommandMode, OpenDownloads, OpenHistory, OpenNewTab,
|
||||||
OpenSettings, OpenTaskManager, RestoreClosedTab, SelectNextTab, SelectPreviousTab,
|
OpenSettings, OpenTaskManager, ResetZoom, RestoreClosedTab, SelectNextTab, SelectPreviousTab,
|
||||||
ToggleFavoriteTab, TogglePinnedTab,
|
ToggleFavoriteTab, TogglePinnedTab, ZoomIn, ZoomOut,
|
||||||
};
|
};
|
||||||
|
|
||||||
enum ShellState {
|
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(
|
fn on_close_current_tab(
|
||||||
&mut self,
|
&mut self,
|
||||||
_: &CloseCurrentTab,
|
_: &CloseCurrentTab,
|
||||||
@@ -350,6 +374,10 @@ impl ElyShell {
|
|||||||
self.restore_closed_tab(window, cx);
|
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(
|
fn on_select_next_tab(
|
||||||
&mut self,
|
&mut self,
|
||||||
_: &SelectNextTab,
|
_: &SelectNextTab,
|
||||||
@@ -385,4 +413,12 @@ impl ElyShell {
|
|||||||
) {
|
) {
|
||||||
self.toggle_active_tab_pinned(cx);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ impl ElyShell {
|
|||||||
.on_action(cx.listener(Self::on_open_new_tab))
|
.on_action(cx.listener(Self::on_open_new_tab))
|
||||||
.on_action(cx.listener(Self::on_open_settings))
|
.on_action(cx.listener(Self::on_open_settings))
|
||||||
.on_action(cx.listener(Self::on_open_task_manager))
|
.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_restore_closed_tab))
|
||||||
.on_action(cx.listener(Self::on_select_next_space))
|
.on_action(cx.listener(Self::on_select_next_space))
|
||||||
.on_action(cx.listener(Self::on_select_next_tab))
|
.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_favorite_tab))
|
||||||
.on_action(cx.listener(Self::on_toggle_pinned_tab))
|
.on_action(cx.listener(Self::on_toggle_pinned_tab))
|
||||||
.on_action(cx.listener(Self::on_toggle_sidebar))
|
.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))
|
.bg(rgb(ELY_THEME.canvas))
|
||||||
.text_color(rgb(ELY_THEME.ink))
|
.text_color(rgb(ELY_THEME.ink))
|
||||||
.flex()
|
.flex()
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ use super::{
|
|||||||
},
|
},
|
||||||
web_surface_state::{
|
web_surface_state::{
|
||||||
WebSurfaceClickState, WebSurfaceClient, WebSurfaceKeyboardFocusState, WebSurfaceRequest,
|
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 size = self.viewport_sizes.get(tab.id()).copied()?;
|
||||||
let requested_url = tab.url().as_str().to_string();
|
let requested_url = tab.url().as_str().to_string();
|
||||||
let scroll_offset = self.scroll_offset_for(tab.id(), requested_url.as_str());
|
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 click_point = self.click_point_for(tab.id(), requested_url.as_str(), scroll_offset);
|
||||||
let typed_text =
|
let typed_text =
|
||||||
self.typed_text_for(tab.id(), requested_url.as_str(), scroll_offset, click_point);
|
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()) {
|
if self.is_loading_requested_url(tab.id(), requested_url.as_str()) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
if self.has_current_state(
|
let state_key = WebSurfaceStateKey {
|
||||||
tab.id(),
|
requested_url: &requested_url,
|
||||||
&requested_url,
|
|
||||||
size,
|
size,
|
||||||
scroll_offset,
|
scroll_offset,
|
||||||
|
zoom_percent,
|
||||||
click_point,
|
click_point,
|
||||||
typed_text.as_deref(),
|
typed_text: typed_text.as_deref(),
|
||||||
) {
|
};
|
||||||
|
if self.has_current_state(tab.id(), state_key) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,6 +87,7 @@ impl WebSurfaceStore {
|
|||||||
requested_url,
|
requested_url,
|
||||||
size,
|
size,
|
||||||
scroll_offset,
|
scroll_offset,
|
||||||
|
zoom_percent,
|
||||||
click_point,
|
click_point,
|
||||||
typed_text: typed_text.clone(),
|
typed_text: typed_text.clone(),
|
||||||
message: message.clone(),
|
message: message.clone(),
|
||||||
@@ -100,9 +103,14 @@ impl WebSurfaceStore {
|
|||||||
requested_url: requested_url.clone(),
|
requested_url: requested_url.clone(),
|
||||||
size,
|
size,
|
||||||
scroll_offset,
|
scroll_offset,
|
||||||
|
zoom_percent,
|
||||||
click_point,
|
click_point,
|
||||||
typed_text: typed_text.clone(),
|
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,
|
size.height,
|
||||||
)
|
)
|
||||||
.with_profile_data_mode(profile_data_mode)
|
.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 {
|
if let Some(click_point) = click_point {
|
||||||
snapshot_request = snapshot_request.with_click_point(click_point.x(), click_point.y());
|
snapshot_request = snapshot_request.with_click_point(click_point.x(), click_point.y());
|
||||||
}
|
}
|
||||||
@@ -126,6 +135,7 @@ impl WebSurfaceStore {
|
|||||||
requested_url,
|
requested_url,
|
||||||
size,
|
size,
|
||||||
scroll_offset,
|
scroll_offset,
|
||||||
|
zoom_percent,
|
||||||
click_point,
|
click_point,
|
||||||
typed_text,
|
typed_text,
|
||||||
client,
|
client,
|
||||||
@@ -133,79 +143,70 @@ impl WebSurfaceStore {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn has_current_state(
|
fn has_current_state(&self, tab_id: &TabId, key: WebSurfaceStateKey<'_>) -> bool {
|
||||||
&self,
|
|
||||||
tab_id: &TabId,
|
|
||||||
requested_url: &str,
|
|
||||||
size: WebSurfaceSize,
|
|
||||||
scroll_offset: WebSurfaceScrollOffset,
|
|
||||||
click_point: Option<WebSurfaceClickPoint>,
|
|
||||||
typed_text: Option<&str>,
|
|
||||||
) -> bool {
|
|
||||||
match self.states.get(tab_id) {
|
match self.states.get(tab_id) {
|
||||||
Some(WebSurfaceState::Loading {
|
Some(WebSurfaceState::Loading {
|
||||||
requested_url: current_url,
|
requested_url: current_url,
|
||||||
size: current_size,
|
size: current_size,
|
||||||
scroll_offset: current_scroll_offset,
|
scroll_offset: current_scroll_offset,
|
||||||
|
zoom_percent: current_zoom_percent,
|
||||||
click_point: current_click_point,
|
click_point: current_click_point,
|
||||||
typed_text: current_typed_text,
|
typed_text: current_typed_text,
|
||||||
..
|
..
|
||||||
}) => {
|
}) => {
|
||||||
current_url == requested_url
|
current_url == key.requested_url
|
||||||
&& *current_size == size
|
&& *current_size == key.size
|
||||||
&& *current_scroll_offset == scroll_offset
|
&& *current_scroll_offset == key.scroll_offset
|
||||||
&& *current_click_point == click_point
|
&& *current_zoom_percent == key.zoom_percent
|
||||||
&& current_typed_text.as_deref() == typed_text
|
&& *current_click_point == key.click_point
|
||||||
|
&& current_typed_text.as_deref() == key.typed_text
|
||||||
}
|
}
|
||||||
Some(WebSurfaceState::Ready(frame)) => {
|
Some(WebSurfaceState::Ready(frame)) => {
|
||||||
frame.requested_url == requested_url
|
frame.requested_url == key.requested_url
|
||||||
&& frame.size() == size
|
&& frame.size() == key.size
|
||||||
&& frame.scroll_offset() == scroll_offset
|
&& frame.scroll_offset() == key.scroll_offset
|
||||||
&& frame.click_point() == click_point
|
&& frame.zoom_percent() == key.zoom_percent
|
||||||
&& frame.typed_text() == typed_text
|
&& frame.click_point() == key.click_point
|
||||||
|
&& frame.typed_text() == key.typed_text
|
||||||
}
|
}
|
||||||
Some(WebSurfaceState::Failed {
|
Some(WebSurfaceState::Failed {
|
||||||
requested_url: current_url,
|
requested_url: current_url,
|
||||||
size: current_size,
|
size: current_size,
|
||||||
scroll_offset: current_scroll_offset,
|
scroll_offset: current_scroll_offset,
|
||||||
|
zoom_percent: current_zoom_percent,
|
||||||
click_point: current_click_point,
|
click_point: current_click_point,
|
||||||
typed_text: current_typed_text,
|
typed_text: current_typed_text,
|
||||||
..
|
..
|
||||||
}) => {
|
}) => {
|
||||||
current_url == requested_url
|
current_url == key.requested_url
|
||||||
&& *current_size == size
|
&& *current_size == key.size
|
||||||
&& *current_scroll_offset == scroll_offset
|
&& *current_scroll_offset == key.scroll_offset
|
||||||
&& *current_click_point == click_point
|
&& *current_zoom_percent == key.zoom_percent
|
||||||
&& current_typed_text.as_deref() == typed_text
|
&& *current_click_point == key.click_point
|
||||||
|
&& current_typed_text.as_deref() == key.typed_text
|
||||||
}
|
}
|
||||||
None => false,
|
None => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn is_loading(
|
pub(super) fn is_loading(&self, tab_id: &TabId, key: WebSurfaceStateKey<'_>) -> bool {
|
||||||
&self,
|
|
||||||
tab_id: &TabId,
|
|
||||||
requested_url: &str,
|
|
||||||
size: WebSurfaceSize,
|
|
||||||
scroll_offset: WebSurfaceScrollOffset,
|
|
||||||
click_point: Option<WebSurfaceClickPoint>,
|
|
||||||
typed_text: Option<&str>,
|
|
||||||
) -> bool {
|
|
||||||
matches!(
|
matches!(
|
||||||
self.states.get(tab_id),
|
self.states.get(tab_id),
|
||||||
Some(WebSurfaceState::Loading {
|
Some(WebSurfaceState::Loading {
|
||||||
requested_url: current_url,
|
requested_url: current_url,
|
||||||
size: current_size,
|
size: current_size,
|
||||||
scroll_offset: current_scroll_offset,
|
scroll_offset: current_scroll_offset,
|
||||||
|
zoom_percent: current_zoom_percent,
|
||||||
click_point: current_click_point,
|
click_point: current_click_point,
|
||||||
typed_text: current_typed_text,
|
typed_text: current_typed_text,
|
||||||
..
|
..
|
||||||
})
|
})
|
||||||
if current_url == requested_url
|
if current_url == key.requested_url
|
||||||
&& *current_size == size
|
&& *current_size == key.size
|
||||||
&& *current_scroll_offset == scroll_offset
|
&& *current_scroll_offset == key.scroll_offset
|
||||||
&& *current_click_point == click_point
|
&& *current_zoom_percent == key.zoom_percent
|
||||||
&& current_typed_text.as_deref() == typed_text
|
&& *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) {
|
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(frame.clone())
|
||||||
}
|
}
|
||||||
Some(WebSurfaceState::Loading {
|
Some(WebSurfaceState::Loading {
|
||||||
requested_url: current_url, previous_frame, ..
|
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,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -409,68 +420,8 @@ pub(super) fn is_external_web_url(url: &str) -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
#[path = "web_surface_tests.rs"]
|
||||||
use std::error::Error;
|
mod tests;
|
||||||
|
|
||||||
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)?,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(all(test, feature = "live-site-smoke"))]
|
#[cfg(all(test, feature = "live-site-smoke"))]
|
||||||
#[path = "web_surface_live_site_tests.rs"]
|
#[path = "web_surface_live_site_tests.rs"]
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ use super::{
|
|||||||
web_surface_frame::WebSurfaceFrame,
|
web_surface_frame::WebSurfaceFrame,
|
||||||
web_surface_geometry::{WebSurfaceClickPoint, WebSurfaceScrollOffset, WebSurfaceSize},
|
web_surface_geometry::{WebSurfaceClickPoint, WebSurfaceScrollOffset, WebSurfaceSize},
|
||||||
web_surface_permissions::sidecar_site_permissions_for_tab,
|
web_surface_permissions::sidecar_site_permissions_for_tab,
|
||||||
web_surface_state::{WebSurfaceRequest, WebSurfaceState},
|
web_surface_state::{WebSurfaceRequest, WebSurfaceState, WebSurfaceStateKey},
|
||||||
web_surface_view::{
|
web_surface_view::{
|
||||||
render_failed_web_surface, render_loading_web_surface, render_ready_web_surface,
|
render_failed_web_surface, render_loading_web_surface, render_ready_web_surface,
|
||||||
},
|
},
|
||||||
@@ -23,6 +23,7 @@ struct PendingWebSurfaceFrame {
|
|||||||
requested_url: String,
|
requested_url: String,
|
||||||
size: WebSurfaceSize,
|
size: WebSurfaceSize,
|
||||||
scroll_offset: WebSurfaceScrollOffset,
|
scroll_offset: WebSurfaceScrollOffset,
|
||||||
|
zoom_percent: u16,
|
||||||
click_point: Option<WebSurfaceClickPoint>,
|
click_point: Option<WebSurfaceClickPoint>,
|
||||||
typed_text: Option<String>,
|
typed_text: Option<String>,
|
||||||
}
|
}
|
||||||
@@ -74,6 +75,7 @@ impl ElyShell {
|
|||||||
requested_url,
|
requested_url,
|
||||||
size,
|
size,
|
||||||
scroll_offset,
|
scroll_offset,
|
||||||
|
zoom_percent,
|
||||||
click_point,
|
click_point,
|
||||||
typed_text,
|
typed_text,
|
||||||
client,
|
client,
|
||||||
@@ -85,6 +87,7 @@ impl ElyShell {
|
|||||||
requested_url,
|
requested_url,
|
||||||
size,
|
size,
|
||||||
scroll_offset,
|
scroll_offset,
|
||||||
|
zoom_percent,
|
||||||
click_point,
|
click_point,
|
||||||
typed_text,
|
typed_text,
|
||||||
};
|
};
|
||||||
@@ -112,17 +115,19 @@ impl ElyShell {
|
|||||||
requested_url,
|
requested_url,
|
||||||
size,
|
size,
|
||||||
scroll_offset,
|
scroll_offset,
|
||||||
|
zoom_percent,
|
||||||
click_point,
|
click_point,
|
||||||
typed_text,
|
typed_text,
|
||||||
} = pending_frame;
|
} = pending_frame;
|
||||||
if !self.web_surfaces.is_loading(
|
let state_key = WebSurfaceStateKey {
|
||||||
&tab_id,
|
requested_url: requested_url.as_str(),
|
||||||
requested_url.as_str(),
|
|
||||||
size,
|
size,
|
||||||
scroll_offset,
|
scroll_offset,
|
||||||
|
zoom_percent,
|
||||||
click_point,
|
click_point,
|
||||||
typed_text.as_deref(),
|
typed_text: typed_text.as_deref(),
|
||||||
) {
|
};
|
||||||
|
if !self.web_surfaces.is_loading(&tab_id, state_key) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,6 +135,7 @@ impl ElyShell {
|
|||||||
Ok(snapshot) => match WebSurfaceFrame::from_snapshot(
|
Ok(snapshot) => match WebSurfaceFrame::from_snapshot(
|
||||||
requested_url.clone(),
|
requested_url.clone(),
|
||||||
scroll_offset,
|
scroll_offset,
|
||||||
|
zoom_percent,
|
||||||
click_point,
|
click_point,
|
||||||
typed_text.clone(),
|
typed_text.clone(),
|
||||||
snapshot,
|
snapshot,
|
||||||
@@ -139,6 +145,7 @@ impl ElyShell {
|
|||||||
requested_url,
|
requested_url,
|
||||||
size,
|
size,
|
||||||
scroll_offset,
|
scroll_offset,
|
||||||
|
zoom_percent,
|
||||||
click_point,
|
click_point,
|
||||||
typed_text,
|
typed_text,
|
||||||
message: error.to_string(),
|
message: error.to_string(),
|
||||||
@@ -148,6 +155,7 @@ impl ElyShell {
|
|||||||
requested_url,
|
requested_url,
|
||||||
size,
|
size,
|
||||||
scroll_offset,
|
scroll_offset,
|
||||||
|
zoom_percent,
|
||||||
click_point,
|
click_point,
|
||||||
typed_text,
|
typed_text,
|
||||||
message: error.to_string(),
|
message: error.to_string(),
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ pub(super) struct WebSurfaceFrame {
|
|||||||
width: u32,
|
width: u32,
|
||||||
height: u32,
|
height: u32,
|
||||||
scroll_offset: WebSurfaceScrollOffset,
|
scroll_offset: WebSurfaceScrollOffset,
|
||||||
|
zoom_percent: u16,
|
||||||
click_point: Option<WebSurfaceClickPoint>,
|
click_point: Option<WebSurfaceClickPoint>,
|
||||||
typed_text: Option<String>,
|
typed_text: Option<String>,
|
||||||
#[cfg(all(test, feature = "live-site-smoke"))]
|
#[cfg(all(test, feature = "live-site-smoke"))]
|
||||||
@@ -35,6 +36,7 @@ impl WebSurfaceFrame {
|
|||||||
pub(super) fn from_snapshot(
|
pub(super) fn from_snapshot(
|
||||||
requested_url: String,
|
requested_url: String,
|
||||||
scroll_offset: WebSurfaceScrollOffset,
|
scroll_offset: WebSurfaceScrollOffset,
|
||||||
|
zoom_percent: u16,
|
||||||
click_point: Option<WebSurfaceClickPoint>,
|
click_point: Option<WebSurfaceClickPoint>,
|
||||||
typed_text: Option<String>,
|
typed_text: Option<String>,
|
||||||
snapshot: SidecarSnapshot,
|
snapshot: SidecarSnapshot,
|
||||||
@@ -66,6 +68,7 @@ impl WebSurfaceFrame {
|
|||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
scroll_offset,
|
scroll_offset,
|
||||||
|
zoom_percent,
|
||||||
click_point,
|
click_point,
|
||||||
typed_text,
|
typed_text,
|
||||||
#[cfg(all(test, feature = "live-site-smoke"))]
|
#[cfg(all(test, feature = "live-site-smoke"))]
|
||||||
@@ -89,6 +92,9 @@ impl WebSurfaceFrame {
|
|||||||
pub(super) fn detail_label(&self) -> String {
|
pub(super) fn detail_label(&self) -> String {
|
||||||
let mut detail =
|
let mut detail =
|
||||||
format!("{} {}", self.render_state(), self.scroll_offset.detail_label(self.size()));
|
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 {
|
if let Some(click_point) = self.click_point {
|
||||||
detail = format!("{detail} {}", click_point.detail_label());
|
detail = format!("{detail} {}", click_point.detail_label());
|
||||||
}
|
}
|
||||||
@@ -110,6 +116,10 @@ impl WebSurfaceFrame {
|
|||||||
self.scroll_offset
|
self.scroll_offset
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(super) fn zoom_percent(&self) -> u16 {
|
||||||
|
self.zoom_percent
|
||||||
|
}
|
||||||
|
|
||||||
pub(super) fn click_point(&self) -> Option<WebSurfaceClickPoint> {
|
pub(super) fn click_point(&self) -> Option<WebSurfaceClickPoint> {
|
||||||
self.click_point
|
self.click_point
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ fn render_web_surface_frame(case: &LiveSiteCase) -> Result<WebSurfaceFrame, Box<
|
|||||||
let frame = WebSurfaceFrame::from_snapshot(
|
let frame = WebSurfaceFrame::from_snapshot(
|
||||||
request.requested_url,
|
request.requested_url,
|
||||||
request.scroll_offset,
|
request.scroll_offset,
|
||||||
|
request.zoom_percent,
|
||||||
request.click_point,
|
request.click_point,
|
||||||
request.typed_text,
|
request.typed_text,
|
||||||
snapshot,
|
snapshot,
|
||||||
|
|||||||
@@ -40,6 +40,16 @@ pub(super) struct WebSurfaceTextInputState {
|
|||||||
pub(super) text: String,
|
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 {
|
pub(super) enum WebSurfaceClient {
|
||||||
Ready(ServoSidecarClient),
|
Ready(ServoSidecarClient),
|
||||||
Unavailable(String),
|
Unavailable(String),
|
||||||
@@ -59,6 +69,7 @@ pub(super) struct WebSurfaceRequest {
|
|||||||
pub(super) requested_url: String,
|
pub(super) requested_url: String,
|
||||||
pub(super) size: WebSurfaceSize,
|
pub(super) size: WebSurfaceSize,
|
||||||
pub(super) scroll_offset: WebSurfaceScrollOffset,
|
pub(super) scroll_offset: WebSurfaceScrollOffset,
|
||||||
|
pub(super) zoom_percent: u16,
|
||||||
pub(super) click_point: Option<WebSurfaceClickPoint>,
|
pub(super) click_point: Option<WebSurfaceClickPoint>,
|
||||||
pub(super) typed_text: Option<String>,
|
pub(super) typed_text: Option<String>,
|
||||||
pub(super) client: ServoSidecarClient,
|
pub(super) client: ServoSidecarClient,
|
||||||
@@ -70,6 +81,7 @@ pub(super) enum WebSurfaceState {
|
|||||||
requested_url: String,
|
requested_url: String,
|
||||||
size: WebSurfaceSize,
|
size: WebSurfaceSize,
|
||||||
scroll_offset: WebSurfaceScrollOffset,
|
scroll_offset: WebSurfaceScrollOffset,
|
||||||
|
zoom_percent: u16,
|
||||||
click_point: Option<WebSurfaceClickPoint>,
|
click_point: Option<WebSurfaceClickPoint>,
|
||||||
typed_text: Option<String>,
|
typed_text: Option<String>,
|
||||||
previous_frame: Option<WebSurfaceFrame>,
|
previous_frame: Option<WebSurfaceFrame>,
|
||||||
@@ -79,6 +91,7 @@ pub(super) enum WebSurfaceState {
|
|||||||
requested_url: String,
|
requested_url: String,
|
||||||
size: WebSurfaceSize,
|
size: WebSurfaceSize,
|
||||||
scroll_offset: WebSurfaceScrollOffset,
|
scroll_offset: WebSurfaceScrollOffset,
|
||||||
|
zoom_percent: u16,
|
||||||
click_point: Option<WebSurfaceClickPoint>,
|
click_point: Option<WebSurfaceClickPoint>,
|
||||||
typed_text: Option<String>,
|
typed_text: Option<String>,
|
||||||
message: 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)?))
|
||||||
|
}
|
||||||
@@ -10,9 +10,9 @@ pub(crate) use profile::{
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
CloseCurrentTab, FocusAddressBar, FocusCommandMode, OpenDownloads, OpenHistory, OpenNewTab,
|
CloseCurrentTab, FocusAddressBar, FocusCommandMode, OpenDownloads, OpenHistory, OpenNewTab,
|
||||||
OpenPrivateWindow, OpenSettings, OpenTaskManager, Quit, RestoreClosedTab, SelectNextSpace,
|
OpenPrivateWindow, OpenSettings, OpenTaskManager, Quit, ResetZoom, RestoreClosedTab,
|
||||||
SelectNextTab, SelectPreviousSpace, SelectPreviousTab, SplitRight, ToggleFavoriteTab,
|
SelectNextSpace, SelectNextTab, SelectPreviousSpace, SelectPreviousTab, SplitRight,
|
||||||
ToggleSidebar,
|
ToggleFavoriteTab, ToggleSidebar, ZoomIn, ZoomOut,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
|
#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
|
||||||
@@ -47,6 +47,9 @@ pub(crate) enum ShortcutAction {
|
|||||||
SplitRight,
|
SplitRight,
|
||||||
ToggleSidebar,
|
ToggleSidebar,
|
||||||
ToggleFavoriteTab,
|
ToggleFavoriteTab,
|
||||||
|
ZoomIn,
|
||||||
|
ZoomOut,
|
||||||
|
ResetZoom,
|
||||||
OpenDownloads,
|
OpenDownloads,
|
||||||
OpenHistory,
|
OpenHistory,
|
||||||
OpenSettings,
|
OpenSettings,
|
||||||
@@ -70,6 +73,9 @@ impl ShortcutAction {
|
|||||||
Self::SplitRight => "Split Right",
|
Self::SplitRight => "Split Right",
|
||||||
Self::ToggleSidebar => "Toggle Sidebar",
|
Self::ToggleSidebar => "Toggle Sidebar",
|
||||||
Self::ToggleFavoriteTab => "Toggle Favorite",
|
Self::ToggleFavoriteTab => "Toggle Favorite",
|
||||||
|
Self::ZoomIn => "Zoom In",
|
||||||
|
Self::ZoomOut => "Zoom Out",
|
||||||
|
Self::ResetZoom => "Reset Zoom",
|
||||||
Self::OpenDownloads => "Open Downloads",
|
Self::OpenDownloads => "Open Downloads",
|
||||||
Self::OpenHistory => "Open History",
|
Self::OpenHistory => "Open History",
|
||||||
Self::OpenSettings => "Open Settings",
|
Self::OpenSettings => "Open Settings",
|
||||||
@@ -91,7 +97,10 @@ impl ShortcutAction {
|
|||||||
| Self::SelectPreviousTab
|
| Self::SelectPreviousTab
|
||||||
| Self::SplitRight
|
| Self::SplitRight
|
||||||
| Self::ToggleSidebar
|
| Self::ToggleSidebar
|
||||||
| Self::ToggleFavoriteTab => "Tabs",
|
| Self::ToggleFavoriteTab
|
||||||
|
| Self::ZoomIn
|
||||||
|
| Self::ZoomOut
|
||||||
|
| Self::ResetZoom => "Tabs",
|
||||||
Self::OpenDownloads | Self::OpenHistory => "Library",
|
Self::OpenDownloads | Self::OpenHistory => "Library",
|
||||||
Self::OpenSettings | Self::OpenTaskManager => "System",
|
Self::OpenSettings | Self::OpenTaskManager => "System",
|
||||||
Self::Quit => "Application",
|
Self::Quit => "Application",
|
||||||
@@ -113,6 +122,9 @@ impl ShortcutAction {
|
|||||||
Self::SplitRight => Some(">split-right"),
|
Self::SplitRight => Some(">split-right"),
|
||||||
Self::ToggleSidebar => None,
|
Self::ToggleSidebar => None,
|
||||||
Self::ToggleFavoriteTab => Some(">favorite"),
|
Self::ToggleFavoriteTab => Some(">favorite"),
|
||||||
|
Self::ZoomIn => Some(">zoom-in"),
|
||||||
|
Self::ZoomOut => Some(">zoom-out"),
|
||||||
|
Self::ResetZoom => Some(">reset-zoom"),
|
||||||
Self::OpenDownloads => Some(">open-downloads"),
|
Self::OpenDownloads => Some(">open-downloads"),
|
||||||
Self::OpenHistory => Some(">open-history"),
|
Self::OpenHistory => Some(">open-history"),
|
||||||
Self::OpenSettings => Some(">open-settings"),
|
Self::OpenSettings => Some(">open-settings"),
|
||||||
@@ -164,6 +176,9 @@ pub(crate) const SHORTCUT_ACTIONS: &[ShortcutAction] = &[
|
|||||||
ShortcutAction::SplitRight,
|
ShortcutAction::SplitRight,
|
||||||
ShortcutAction::ToggleSidebar,
|
ShortcutAction::ToggleSidebar,
|
||||||
ShortcutAction::ToggleFavoriteTab,
|
ShortcutAction::ToggleFavoriteTab,
|
||||||
|
ShortcutAction::ZoomIn,
|
||||||
|
ShortcutAction::ZoomOut,
|
||||||
|
ShortcutAction::ResetZoom,
|
||||||
ShortcutAction::OpenDownloads,
|
ShortcutAction::OpenDownloads,
|
||||||
ShortcutAction::OpenHistory,
|
ShortcutAction::OpenHistory,
|
||||||
ShortcutAction::OpenSettings,
|
ShortcutAction::OpenSettings,
|
||||||
@@ -196,6 +211,12 @@ pub(crate) const SHORTCUT_BINDINGS: &[ShortcutBinding] = &[
|
|||||||
shortcut(ShortcutAction::RestoreClosedTab, ShortcutPlatform::WindowsLinux, "ctrl-shift-t"),
|
shortcut(ShortcutAction::RestoreClosedTab, ShortcutPlatform::WindowsLinux, "ctrl-shift-t"),
|
||||||
shortcut(ShortcutAction::ToggleFavoriteTab, ShortcutPlatform::Macos, "cmd-shift-f"),
|
shortcut(ShortcutAction::ToggleFavoriteTab, ShortcutPlatform::Macos, "cmd-shift-f"),
|
||||||
shortcut(ShortcutAction::ToggleFavoriteTab, ShortcutPlatform::WindowsLinux, "ctrl-shift-f"),
|
shortcut(ShortcutAction::ToggleFavoriteTab, ShortcutPlatform::WindowsLinux, "ctrl-shift-f"),
|
||||||
|
shortcut(ShortcutAction::ZoomIn, ShortcutPlatform::Macos, "cmd-="),
|
||||||
|
shortcut(ShortcutAction::ZoomIn, ShortcutPlatform::WindowsLinux, "ctrl-="),
|
||||||
|
shortcut(ShortcutAction::ZoomOut, ShortcutPlatform::Macos, "cmd--"),
|
||||||
|
shortcut(ShortcutAction::ZoomOut, ShortcutPlatform::WindowsLinux, "ctrl--"),
|
||||||
|
shortcut(ShortcutAction::ResetZoom, ShortcutPlatform::Macos, "cmd-0"),
|
||||||
|
shortcut(ShortcutAction::ResetZoom, ShortcutPlatform::WindowsLinux, "ctrl-0"),
|
||||||
shortcut(ShortcutAction::FocusCommandMode, ShortcutPlatform::Macos, "cmd-shift-p"),
|
shortcut(ShortcutAction::FocusCommandMode, ShortcutPlatform::Macos, "cmd-shift-p"),
|
||||||
shortcut(ShortcutAction::FocusCommandMode, ShortcutPlatform::WindowsLinux, "ctrl-shift-p"),
|
shortcut(ShortcutAction::FocusCommandMode, ShortcutPlatform::WindowsLinux, "ctrl-shift-p"),
|
||||||
shortcut(ShortcutAction::SelectNextSpace, ShortcutPlatform::Macos, "cmd-alt-right"),
|
shortcut(ShortcutAction::SelectNextSpace, ShortcutPlatform::Macos, "cmd-alt-right"),
|
||||||
@@ -243,6 +264,7 @@ pub(crate) fn key_binding_for_action(action: ShortcutAction, keystroke: &str) ->
|
|||||||
ShortcutAction::OpenSettings => KeyBinding::new(keystroke, OpenSettings, None),
|
ShortcutAction::OpenSettings => KeyBinding::new(keystroke, OpenSettings, None),
|
||||||
ShortcutAction::OpenTaskManager => KeyBinding::new(keystroke, OpenTaskManager, None),
|
ShortcutAction::OpenTaskManager => KeyBinding::new(keystroke, OpenTaskManager, None),
|
||||||
ShortcutAction::Quit => KeyBinding::new(keystroke, Quit, None),
|
ShortcutAction::Quit => KeyBinding::new(keystroke, Quit, None),
|
||||||
|
ShortcutAction::ResetZoom => KeyBinding::new(keystroke, ResetZoom, None),
|
||||||
ShortcutAction::RestoreClosedTab => KeyBinding::new(keystroke, RestoreClosedTab, None),
|
ShortcutAction::RestoreClosedTab => KeyBinding::new(keystroke, RestoreClosedTab, None),
|
||||||
ShortcutAction::SelectNextSpace => KeyBinding::new(keystroke, SelectNextSpace, None),
|
ShortcutAction::SelectNextSpace => KeyBinding::new(keystroke, SelectNextSpace, None),
|
||||||
ShortcutAction::SelectNextTab => KeyBinding::new(keystroke, SelectNextTab, None),
|
ShortcutAction::SelectNextTab => KeyBinding::new(keystroke, SelectNextTab, None),
|
||||||
@@ -253,6 +275,8 @@ pub(crate) fn key_binding_for_action(action: ShortcutAction, keystroke: &str) ->
|
|||||||
ShortcutAction::SplitRight => KeyBinding::new(keystroke, SplitRight, None),
|
ShortcutAction::SplitRight => KeyBinding::new(keystroke, SplitRight, None),
|
||||||
ShortcutAction::ToggleFavoriteTab => KeyBinding::new(keystroke, ToggleFavoriteTab, None),
|
ShortcutAction::ToggleFavoriteTab => KeyBinding::new(keystroke, ToggleFavoriteTab, None),
|
||||||
ShortcutAction::ToggleSidebar => KeyBinding::new(keystroke, ToggleSidebar, None),
|
ShortcutAction::ToggleSidebar => KeyBinding::new(keystroke, ToggleSidebar, None),
|
||||||
|
ShortcutAction::ZoomIn => KeyBinding::new(keystroke, ZoomIn, None),
|
||||||
|
ShortcutAction::ZoomOut => KeyBinding::new(keystroke, ZoomOut, None),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -133,6 +133,10 @@ impl BrowserCore {
|
|||||||
self.archive_idle_tabs(SystemTime::now())?;
|
self.archive_idle_tabs(SystemTime::now())?;
|
||||||
return Ok(true);
|
return Ok(true);
|
||||||
}
|
}
|
||||||
|
if let Some(percent) = zoom_percent(command)? {
|
||||||
|
self.set_active_tab_zoom_percent(percent)?;
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
if let Some(percent) = reading_progress_percent(command)? {
|
if let Some(percent) = reading_progress_percent(command)? {
|
||||||
self.set_active_tab_reading_progress(percent)?;
|
self.set_active_tab_reading_progress(percent)?;
|
||||||
return Ok(true);
|
return Ok(true);
|
||||||
@@ -167,6 +171,18 @@ impl BrowserCore {
|
|||||||
self.open_new_tab()?;
|
self.open_new_tab()?;
|
||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
|
"zoom-in" | "zoom in" => {
|
||||||
|
self.zoom_active_tab_in()?;
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
"zoom-out" | "zoom out" => {
|
||||||
|
self.zoom_active_tab_out()?;
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
"reset-zoom" | "reset zoom" | "actual-size" | "actual size" => {
|
||||||
|
self.reset_active_tab_zoom()?;
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
"move-tab-up" | "move tab up" | "tab-up" | "tab up" => self.move_active_tab_up(),
|
"move-tab-up" | "move tab up" | "tab-up" | "tab up" => self.move_active_tab_up(),
|
||||||
"move-tab-down" | "move tab down" | "tab-down" | "tab down" => {
|
"move-tab-down" | "move tab down" | "tab-down" | "tab down" => {
|
||||||
self.move_active_tab_down()
|
self.move_active_tab_down()
|
||||||
@@ -446,3 +462,19 @@ impl BrowserCore {
|
|||||||
.transpose()
|
.transpose()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn zoom_percent(command: &str) -> Result<Option<u16>, CoreError> {
|
||||||
|
let command = command.trim();
|
||||||
|
let lowercased = command.to_ascii_lowercase();
|
||||||
|
let value = ["zoom ", "set-zoom ", "set zoom "]
|
||||||
|
.into_iter()
|
||||||
|
.find_map(|prefix| lowercased.starts_with(prefix).then(|| &command[prefix.len()..]));
|
||||||
|
let Some(value) = value else { return Ok(None) };
|
||||||
|
|
||||||
|
let percent = value
|
||||||
|
.trim()
|
||||||
|
.trim_end_matches('%')
|
||||||
|
.parse::<u16>()
|
||||||
|
.map_err(|_| CoreError::Domain(ely_domain::DomainError::InvalidCommand))?;
|
||||||
|
Ok(Some(ely_domain::validate_zoom_percent(percent)?))
|
||||||
|
}
|
||||||
|
|||||||
@@ -220,6 +220,30 @@ impl BrowserCore {
|
|||||||
Ok(next_pinned)
|
Ok(next_pinned)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_active_tab_zoom_percent(&mut self, zoom_percent: u16) -> Result<u16, CoreError> {
|
||||||
|
let active_tab = self.active_tab_mut()?;
|
||||||
|
active_tab.set_zoom_percent(zoom_percent)?;
|
||||||
|
Ok(active_tab.zoom_percent())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn zoom_active_tab_in(&mut self) -> Result<u16, CoreError> {
|
||||||
|
let active_tab = self.active_tab_mut()?;
|
||||||
|
active_tab.zoom_in();
|
||||||
|
Ok(active_tab.zoom_percent())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn zoom_active_tab_out(&mut self) -> Result<u16, CoreError> {
|
||||||
|
let active_tab = self.active_tab_mut()?;
|
||||||
|
active_tab.zoom_out();
|
||||||
|
Ok(active_tab.zoom_percent())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reset_active_tab_zoom(&mut self) -> Result<u16, CoreError> {
|
||||||
|
let active_tab = self.active_tab_mut()?;
|
||||||
|
active_tab.reset_zoom();
|
||||||
|
Ok(active_tab.zoom_percent())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn set_tab_sync_enabled(
|
pub fn set_tab_sync_enabled(
|
||||||
&mut self,
|
&mut self,
|
||||||
tab_id: &TabId,
|
tab_id: &TabId,
|
||||||
@@ -337,6 +361,11 @@ impl BrowserCore {
|
|||||||
.ok_or(CoreError::MissingActiveTab)
|
.ok_or(CoreError::MissingActiveTab)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn active_tab_mut(&mut self) -> Result<&mut BrowserTab, CoreError> {
|
||||||
|
let active_index = self.active_tab_index()?;
|
||||||
|
self.tabs.get_mut(active_index).ok_or(CoreError::MissingActiveTab)
|
||||||
|
}
|
||||||
|
|
||||||
pub(super) fn build_tab(&self, url: UrlText) -> BrowserTab {
|
pub(super) fn build_tab(&self, url: UrlText) -> BrowserTab {
|
||||||
self.build_tab_for(self.active_space_id.clone(), self.active_profile_id.clone(), url)
|
self.build_tab_for(self.active_space_id.clone(), self.active_profile_id.clone(), url)
|
||||||
.with_parent_tab_id(self.active_tab_id.clone())
|
.with_parent_tab_id(self.active_tab_id.clone())
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
|
|
||||||
use ely_browser_core::{BrowserCore, CoreError, InitialBrowserConfig};
|
use ely_browser_core::{BrowserCore, CoreError, InitialBrowserConfig};
|
||||||
use ely_domain::{CommandIntent, CommandScope, NewTabDestination, SearchEngine, TabState, UrlText};
|
use ely_domain::{
|
||||||
|
CommandIntent, CommandScope, DEFAULT_ZOOM_PERCENT, DomainError, MAX_ZOOM_PERCENT,
|
||||||
|
MIN_ZOOM_PERCENT, NewTabDestination, SearchEngine, TabState, UrlText,
|
||||||
|
};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn opens_new_tab_below_active_tab() -> Result<(), Box<dyn Error>> {
|
fn opens_new_tab_below_active_tab() -> Result<(), Box<dyn Error>> {
|
||||||
@@ -357,6 +360,71 @@ fn new_tab_command_uses_selected_destination() -> Result<(), Box<dyn Error>> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn active_tab_zoom_updates_tab_state() -> Result<(), Box<dyn Error>> {
|
||||||
|
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
|
||||||
|
|
||||||
|
assert_eq!(core.active_tab()?.zoom_percent(), DEFAULT_ZOOM_PERCENT);
|
||||||
|
assert_eq!(core.zoom_active_tab_in()?, DEFAULT_ZOOM_PERCENT + 10);
|
||||||
|
assert_eq!(core.zoom_active_tab_out()?, DEFAULT_ZOOM_PERCENT);
|
||||||
|
assert_eq!(core.set_active_tab_zoom_percent(125)?, 125);
|
||||||
|
assert_eq!(core.active_tab()?.zoom_factor(), 1.25);
|
||||||
|
assert_eq!(core.reset_active_tab_zoom()?, DEFAULT_ZOOM_PERCENT);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn active_tab_zoom_clamps_incremental_commands() -> Result<(), Box<dyn Error>> {
|
||||||
|
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
|
||||||
|
|
||||||
|
core.set_active_tab_zoom_percent(MAX_ZOOM_PERCENT)?;
|
||||||
|
assert_eq!(core.zoom_active_tab_in()?, MAX_ZOOM_PERCENT);
|
||||||
|
|
||||||
|
core.set_active_tab_zoom_percent(MIN_ZOOM_PERCENT)?;
|
||||||
|
assert_eq!(core.zoom_active_tab_out()?, MIN_ZOOM_PERCENT);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn active_tab_zoom_rejects_out_of_range_percent() -> Result<(), Box<dyn Error>> {
|
||||||
|
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
|
||||||
|
|
||||||
|
let error = match core.set_active_tab_zoom_percent(5) {
|
||||||
|
Err(error) => error,
|
||||||
|
Ok(_) => return Err("zoom should require an in-range percent".into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
error,
|
||||||
|
CoreError::Domain(DomainError::InvalidZoomPercent {
|
||||||
|
value: 5,
|
||||||
|
min: MIN_ZOOM_PERCENT,
|
||||||
|
max: MAX_ZOOM_PERCENT,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn zoom_commands_update_active_tab() -> Result<(), Box<dyn Error>> {
|
||||||
|
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
|
||||||
|
|
||||||
|
core.set_command_query(">zoom-in");
|
||||||
|
let intent = core.submit_command()?;
|
||||||
|
assert_eq!(intent, Some(CommandIntent::Command("zoom-in".to_string())));
|
||||||
|
assert_eq!(core.active_tab()?.zoom_percent(), DEFAULT_ZOOM_PERCENT + 10);
|
||||||
|
|
||||||
|
core.set_command_query(">zoom 125%");
|
||||||
|
core.submit_command()?;
|
||||||
|
assert_eq!(core.active_tab()?.zoom_percent(), 125);
|
||||||
|
|
||||||
|
core.set_command_query(">actual-size");
|
||||||
|
core.submit_command()?;
|
||||||
|
assert_eq!(core.active_tab()?.zoom_percent(), DEFAULT_ZOOM_PERCENT);
|
||||||
|
assert_eq!(core.command_query(), "");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn switching_spaces_restores_each_space_active_tab() -> Result<(), Box<dyn Error>> {
|
fn switching_spaces_restores_each_space_active_tab() -> Result<(), Box<dyn Error>> {
|
||||||
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
|
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
|
||||||
|
|||||||
@@ -41,6 +41,9 @@ pub enum DomainError {
|
|||||||
#[error("invalid reading progress percent: {value}")]
|
#[error("invalid reading progress percent: {value}")]
|
||||||
InvalidReadingProgressPercent { value: String },
|
InvalidReadingProgressPercent { value: String },
|
||||||
|
|
||||||
|
#[error("invalid tab zoom percent: {value} (expected {min}-{max})")]
|
||||||
|
InvalidZoomPercent { value: u16, min: u16, max: u16 },
|
||||||
|
|
||||||
#[error("invalid plugin manifest: {reason}")]
|
#[error("invalid plugin manifest: {reason}")]
|
||||||
InvalidPluginManifest { reason: String },
|
InvalidPluginManifest { reason: String },
|
||||||
|
|
||||||
|
|||||||
@@ -60,7 +60,10 @@ pub use sync::{
|
|||||||
SyncConnectionState, SyncObjectKind, SyncObjectPolicy, SyncObjectState, SyncObjectStatus,
|
SyncConnectionState, SyncObjectKind, SyncObjectPolicy, SyncObjectState, SyncObjectStatus,
|
||||||
SyncStatus,
|
SyncStatus,
|
||||||
};
|
};
|
||||||
pub use tab::{BrowserTab, TabFlags, TabState};
|
pub use tab::{
|
||||||
|
BrowserTab, DEFAULT_ZOOM_PERCENT, MAX_ZOOM_PERCENT, MIN_ZOOM_PERCENT, TabFlags, TabState,
|
||||||
|
ZOOM_PERCENT_STEP, validate_zoom_percent,
|
||||||
|
};
|
||||||
pub use tab_group::TabGroup;
|
pub use tab_group::TabGroup;
|
||||||
pub use update::UpdatePolicy;
|
pub use update::UpdatePolicy;
|
||||||
pub use url_text::UrlText;
|
pub use url_text::UrlText;
|
||||||
|
|||||||
@@ -2,6 +2,11 @@ use std::time::SystemTime;
|
|||||||
|
|
||||||
use crate::{DomainError, ProfileId, SpaceId, SplitId, TabGroupId, TabId, UrlText};
|
use crate::{DomainError, ProfileId, SpaceId, SplitId, TabGroupId, TabId, UrlText};
|
||||||
|
|
||||||
|
pub const DEFAULT_ZOOM_PERCENT: u16 = 100;
|
||||||
|
pub const MIN_ZOOM_PERCENT: u16 = 25;
|
||||||
|
pub const MAX_ZOOM_PERCENT: u16 = 500;
|
||||||
|
pub const ZOOM_PERCENT_STEP: u16 = 10;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
pub enum TabState {
|
pub enum TabState {
|
||||||
Loading,
|
Loading,
|
||||||
@@ -34,6 +39,7 @@ pub struct BrowserTab {
|
|||||||
split_id: Option<SplitId>,
|
split_id: Option<SplitId>,
|
||||||
sort_key: u64,
|
sort_key: u64,
|
||||||
sync_enabled: bool,
|
sync_enabled: bool,
|
||||||
|
zoom_percent: u16,
|
||||||
created_at: SystemTime,
|
created_at: SystemTime,
|
||||||
last_active_at: SystemTime,
|
last_active_at: SystemTime,
|
||||||
}
|
}
|
||||||
@@ -62,6 +68,7 @@ impl BrowserTab {
|
|||||||
split_id: None,
|
split_id: None,
|
||||||
sort_key: 0,
|
sort_key: 0,
|
||||||
sync_enabled: true,
|
sync_enabled: true,
|
||||||
|
zoom_percent: DEFAULT_ZOOM_PERCENT,
|
||||||
created_at,
|
created_at,
|
||||||
last_active_at: created_at,
|
last_active_at: created_at,
|
||||||
}
|
}
|
||||||
@@ -209,6 +216,35 @@ impl BrowserTab {
|
|||||||
self.sync_enabled
|
self.sync_enabled
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn zoom_percent(&self) -> u16 {
|
||||||
|
self.zoom_percent
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn zoom_factor(&self) -> f32 {
|
||||||
|
f32::from(self.zoom_percent) / f32::from(DEFAULT_ZOOM_PERCENT)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_zoom_percent(&mut self, zoom_percent: u16) -> Result<(), DomainError> {
|
||||||
|
self.zoom_percent = validate_zoom_percent(zoom_percent)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn zoom_in(&mut self) {
|
||||||
|
self.zoom_percent =
|
||||||
|
self.zoom_percent.saturating_add(ZOOM_PERCENT_STEP).min(MAX_ZOOM_PERCENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn zoom_out(&mut self) {
|
||||||
|
self.zoom_percent =
|
||||||
|
self.zoom_percent.saturating_sub(ZOOM_PERCENT_STEP).max(MIN_ZOOM_PERCENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reset_zoom(&mut self) {
|
||||||
|
self.zoom_percent = DEFAULT_ZOOM_PERCENT;
|
||||||
|
}
|
||||||
|
|
||||||
pub fn set_split_id(&mut self, split_id: SplitId) {
|
pub fn set_split_id(&mut self, split_id: SplitId) {
|
||||||
self.split_id = Some(split_id);
|
self.split_id = Some(split_id);
|
||||||
}
|
}
|
||||||
@@ -230,3 +266,11 @@ impl BrowserTab {
|
|||||||
self.sync_enabled = sync_enabled;
|
self.sync_enabled = sync_enabled;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn validate_zoom_percent(value: u16) -> Result<u16, DomainError> {
|
||||||
|
if (MIN_ZOOM_PERCENT..=MAX_ZOOM_PERCENT).contains(&value) {
|
||||||
|
return Ok(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(DomainError::InvalidZoomPercent { value, min: MIN_ZOOM_PERCENT, max: MAX_ZOOM_PERCENT })
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ use std::{
|
|||||||
|
|
||||||
use ely_domain::TabId;
|
use ely_domain::TabId;
|
||||||
use ely_servo_host::{
|
use ely_servo_host::{
|
||||||
KeyboardTextRequest, MouseClickRequest, MouseDragRequest, NavigationRequest, PermissionRequest,
|
KeyboardTextRequest, MouseClickRequest, MouseDragRequest, NavigationRequest, PageZoomRequest,
|
||||||
ScrollRequest, ServoHost, ServoHostError, ServoSurfaceSize, SoftwareServoHost, TouchTapRequest,
|
PermissionRequest, ScrollRequest, ServoHost, ServoHostError, ServoSurfaceSize,
|
||||||
WebViewSnapshot, WebViewState,
|
SoftwareServoHost, TouchTapRequest, WebViewSnapshot, WebViewState,
|
||||||
};
|
};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
@@ -59,6 +59,10 @@ fn run_snapshot(args: SnapshotArgs) -> Result<(), SidecarError> {
|
|||||||
let tab_id = TabId::new();
|
let tab_id = TabId::new();
|
||||||
let webview_id = host.create_webview(tab_id.clone(), args.profile_id.clone())?;
|
let webview_id = host.create_webview(tab_id.clone(), args.profile_id.clone())?;
|
||||||
apply_site_permissions(&mut host, &webview_id, &args)?;
|
apply_site_permissions(&mut host, &webview_id, &args)?;
|
||||||
|
host.set_page_zoom(PageZoomRequest {
|
||||||
|
webview_id: webview_id.clone(),
|
||||||
|
zoom_factor: f32::from(args.page_zoom_percent) / 100.0,
|
||||||
|
})?;
|
||||||
|
|
||||||
host.navigate(NavigationRequest {
|
host.navigate(NavigationRequest {
|
||||||
webview_id: webview_id.clone(),
|
webview_id: webview_id.clone(),
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
use std::{env, num::ParseIntError, path::PathBuf};
|
use std::{env, num::ParseIntError, path::PathBuf};
|
||||||
|
|
||||||
use ely_domain::{ProfileId, SiteOrigin, SitePermissionDecision, SitePermissionFeature, UrlText};
|
use ely_domain::{
|
||||||
|
DEFAULT_ZOOM_PERCENT, ProfileId, SiteOrigin, SitePermissionDecision, SitePermissionFeature,
|
||||||
|
UrlText, validate_zoom_percent,
|
||||||
|
};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
@@ -17,6 +20,7 @@ pub(super) struct SnapshotArgs {
|
|||||||
pub(super) height: u32,
|
pub(super) height: u32,
|
||||||
pub(super) scroll_x: i32,
|
pub(super) scroll_x: i32,
|
||||||
pub(super) scroll_y: i32,
|
pub(super) scroll_y: i32,
|
||||||
|
pub(super) page_zoom_percent: u16,
|
||||||
pub(super) click_point: Option<ClickPoint>,
|
pub(super) click_point: Option<ClickPoint>,
|
||||||
pub(super) drag_points: Option<DragPoints>,
|
pub(super) drag_points: Option<DragPoints>,
|
||||||
pub(super) touch_point: Option<ClickPoint>,
|
pub(super) touch_point: Option<ClickPoint>,
|
||||||
@@ -122,6 +126,7 @@ fn parse_snapshot_args(
|
|||||||
let mut height = None;
|
let mut height = None;
|
||||||
let mut scroll_x = 0;
|
let mut scroll_x = 0;
|
||||||
let mut scroll_y = 0;
|
let mut scroll_y = 0;
|
||||||
|
let mut page_zoom_percent = DEFAULT_ZOOM_PERCENT;
|
||||||
let mut click_x = None;
|
let mut click_x = None;
|
||||||
let mut click_y = None;
|
let mut click_y = None;
|
||||||
let mut drag_from_x = None;
|
let mut drag_from_x = None;
|
||||||
@@ -162,6 +167,12 @@ fn parse_snapshot_args(
|
|||||||
scroll_y =
|
scroll_y =
|
||||||
parse_scroll_delta("--scroll-y", next_argument(&mut args, "--scroll-y")?)?
|
parse_scroll_delta("--scroll-y", next_argument(&mut args, "--scroll-y")?)?
|
||||||
}
|
}
|
||||||
|
"--page-zoom-percent" => {
|
||||||
|
page_zoom_percent = parse_zoom_percent(
|
||||||
|
"--page-zoom-percent",
|
||||||
|
next_argument(&mut args, "--page-zoom-percent")?,
|
||||||
|
)?
|
||||||
|
}
|
||||||
"--click-x" => {
|
"--click-x" => {
|
||||||
click_x = Some(parse_click_coordinate(
|
click_x = Some(parse_click_coordinate(
|
||||||
"--click-x",
|
"--click-x",
|
||||||
@@ -248,6 +259,7 @@ fn parse_snapshot_args(
|
|||||||
height: height.ok_or(SidecarArgsError::MissingRequiredArgument { name: "--height" })?,
|
height: height.ok_or(SidecarArgsError::MissingRequiredArgument { name: "--height" })?,
|
||||||
scroll_x,
|
scroll_x,
|
||||||
scroll_y,
|
scroll_y,
|
||||||
|
page_zoom_percent,
|
||||||
click_point,
|
click_point,
|
||||||
drag_points,
|
drag_points,
|
||||||
touch_point,
|
touch_point,
|
||||||
@@ -302,6 +314,15 @@ fn parse_click_coordinate(name: &'static str, value: String) -> Result<u32, Side
|
|||||||
value.parse::<u32>().map_err(|source| SidecarArgsError::InvalidInteger { name, value, source })
|
value.parse::<u32>().map_err(|source| SidecarArgsError::InvalidInteger { name, value, source })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse_zoom_percent(name: &'static str, value: String) -> Result<u16, SidecarArgsError> {
|
||||||
|
let percent = value.parse::<u16>().map_err(|source| SidecarArgsError::InvalidInteger {
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
source,
|
||||||
|
})?;
|
||||||
|
Ok(validate_zoom_percent(percent)?)
|
||||||
|
}
|
||||||
|
|
||||||
fn parse_path(name: &'static str, value: String) -> Result<PathBuf, SidecarArgsError> {
|
fn parse_path(name: &'static str, value: String) -> Result<PathBuf, SidecarArgsError> {
|
||||||
if value.trim().is_empty() {
|
if value.trim().is_empty() {
|
||||||
return Err(SidecarArgsError::EmptyPath { name });
|
return Err(SidecarArgsError::EmptyPath { name });
|
||||||
@@ -315,7 +336,9 @@ mod tests {
|
|||||||
use std::{env, path::PathBuf};
|
use std::{env, path::PathBuf};
|
||||||
|
|
||||||
use super::{SidecarArgsError, SidecarCommand, parse_command};
|
use super::{SidecarArgsError, SidecarCommand, parse_command};
|
||||||
use ely_domain::{DomainError, ProfileId, SitePermissionDecision, SitePermissionFeature};
|
use ely_domain::{
|
||||||
|
DEFAULT_ZOOM_PERCENT, DomainError, ProfileId, SitePermissionDecision, SitePermissionFeature,
|
||||||
|
};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parses_snapshot_profile_identity() -> Result<(), SidecarArgsError> {
|
fn parses_snapshot_profile_identity() -> Result<(), SidecarArgsError> {
|
||||||
@@ -326,6 +349,7 @@ mod tests {
|
|||||||
let SidecarCommand::Snapshot(args) = command;
|
let SidecarCommand::Snapshot(args) = command;
|
||||||
assert_eq!(args.profile_id, profile_id);
|
assert_eq!(args.profile_id, profile_id);
|
||||||
assert_eq!(args.profile_data_dir, profile_data_dir);
|
assert_eq!(args.profile_data_dir, profile_data_dir);
|
||||||
|
assert_eq!(args.page_zoom_percent, DEFAULT_ZOOM_PERCENT);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -378,6 +402,33 @@ mod tests {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_snapshot_page_zoom_percent() -> Result<(), SidecarArgsError> {
|
||||||
|
let profile_id = ProfileId::new();
|
||||||
|
let profile_data_dir = env::temp_dir().join(profile_id.as_str());
|
||||||
|
let mut command = snapshot_command_args(&profile_id, profile_data_dir);
|
||||||
|
command.push("--page-zoom-percent".to_string());
|
||||||
|
command.push("125".to_string());
|
||||||
|
|
||||||
|
let SidecarCommand::Snapshot(args) = parse_command(command)?;
|
||||||
|
assert_eq!(args.page_zoom_percent, 125);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_out_of_range_snapshot_page_zoom_percent() {
|
||||||
|
let profile_id = ProfileId::new();
|
||||||
|
let profile_data_dir = env::temp_dir().join(profile_id.as_str());
|
||||||
|
let mut command = snapshot_command_args(&profile_id, profile_data_dir);
|
||||||
|
command.push("--page-zoom-percent".to_string());
|
||||||
|
command.push("5".to_string());
|
||||||
|
|
||||||
|
assert!(matches!(
|
||||||
|
parse_command(command),
|
||||||
|
Err(SidecarArgsError::Domain(DomainError::InvalidZoomPercent { value: 5, .. }))
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
fn parse_snapshot_command(
|
fn parse_snapshot_command(
|
||||||
profile_id: &ProfileId,
|
profile_id: &ProfileId,
|
||||||
profile_data_dir: PathBuf,
|
profile_data_dir: PathBuf,
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ pub(super) struct SnapshotReport {
|
|||||||
sample_hash: u64,
|
sample_hash: u64,
|
||||||
scroll_x: i32,
|
scroll_x: i32,
|
||||||
scroll_y: i32,
|
scroll_y: i32,
|
||||||
|
page_zoom_percent: u16,
|
||||||
scroll_changed_frame: bool,
|
scroll_changed_frame: bool,
|
||||||
click_x: Option<u32>,
|
click_x: Option<u32>,
|
||||||
click_y: Option<u32>,
|
click_y: Option<u32>,
|
||||||
@@ -67,6 +68,7 @@ impl SnapshotReport {
|
|||||||
sample_hash: frame.sample_hash(),
|
sample_hash: frame.sample_hash(),
|
||||||
scroll_x: args.scroll_x,
|
scroll_x: args.scroll_x,
|
||||||
scroll_y: args.scroll_y,
|
scroll_y: args.scroll_y,
|
||||||
|
page_zoom_percent: args.page_zoom_percent,
|
||||||
scroll_changed_frame: changes.scroll,
|
scroll_changed_frame: changes.scroll,
|
||||||
click_x: args.click_point.map(|point| point.x),
|
click_x: args.click_point.map(|point| point.x),
|
||||||
click_y: args.click_point.map(|point| point.y),
|
click_y: args.click_point.map(|point| point.y),
|
||||||
|
|||||||
@@ -229,6 +229,12 @@ pub struct ResizeRequest {
|
|||||||
pub height: u32,
|
pub height: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub struct PageZoomRequest {
|
||||||
|
pub webview_id: WebViewId,
|
||||||
|
pub zoom_factor: f32,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
pub struct MouseClickRequest {
|
pub struct MouseClickRequest {
|
||||||
pub webview_id: WebViewId,
|
pub webview_id: WebViewId,
|
||||||
@@ -301,6 +307,8 @@ pub trait ServoHost {
|
|||||||
|
|
||||||
fn resize(&mut self, request: ResizeRequest) -> Result<(), ServoHostError>;
|
fn resize(&mut self, request: ResizeRequest) -> Result<(), ServoHostError>;
|
||||||
|
|
||||||
|
fn set_page_zoom(&mut self, request: PageZoomRequest) -> Result<(), ServoHostError>;
|
||||||
|
|
||||||
fn click(&mut self, request: MouseClickRequest) -> Result<(), ServoHostError>;
|
fn click(&mut self, request: MouseClickRequest) -> Result<(), ServoHostError>;
|
||||||
|
|
||||||
fn drag(&mut self, request: MouseDragRequest) -> Result<(), ServoHostError>;
|
fn drag(&mut self, request: MouseDragRequest) -> Result<(), ServoHostError>;
|
||||||
|
|||||||
@@ -10,10 +10,12 @@ mod runtime_input;
|
|||||||
mod runtime_permissions;
|
mod runtime_permissions;
|
||||||
#[cfg(feature = "servo-engine")]
|
#[cfg(feature = "servo-engine")]
|
||||||
mod runtime_waker;
|
mod runtime_waker;
|
||||||
|
#[cfg(feature = "servo-engine")]
|
||||||
|
mod runtime_webview;
|
||||||
|
|
||||||
pub use error::ServoHostError;
|
pub use error::ServoHostError;
|
||||||
pub use host::{
|
pub use host::{
|
||||||
KeyboardTextRequest, MouseClickRequest, MouseDragRequest, NavigationRequest,
|
KeyboardTextRequest, MouseClickRequest, MouseDragRequest, NavigationRequest, PageZoomRequest,
|
||||||
PermissionDecision, PermissionRequest, RenderedFrame, RenderedFrameSummary, ResizeRequest,
|
PermissionDecision, PermissionRequest, RenderedFrame, RenderedFrameSummary, ResizeRequest,
|
||||||
ScreenshotRequest, ScrollRequest, ServoHost, TouchTapRequest, WebViewSnapshot, WebViewState,
|
ScreenshotRequest, ScrollRequest, ServoHost, TouchTapRequest, WebViewSnapshot, WebViewState,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use std::{
|
use std::{
|
||||||
cell::{Cell, RefCell},
|
cell::RefCell,
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
rc::Rc,
|
rc::Rc,
|
||||||
@@ -14,21 +14,19 @@ use std::{
|
|||||||
use dpi::PhysicalSize;
|
use dpi::PhysicalSize;
|
||||||
use ely_domain::{ProfileId, TabId, WebViewId};
|
use ely_domain::{ProfileId, TabId, WebViewId};
|
||||||
use servo::{
|
use servo::{
|
||||||
DeviceIntPoint, DeviceIntRect, DeviceIntSize, DevicePoint, DeviceVector2D, LoadStatus, Opts,
|
DeviceIntPoint, DeviceIntRect, DeviceIntSize, DevicePoint, DeviceVector2D, Opts,
|
||||||
RenderingContext, Scroll, Servo, ServoBuilder, WebView, WebViewBuilder, WebViewDelegate,
|
RenderingContext, Scroll, Servo, ServoBuilder, WebViewBuilder, WebViewPoint, WebViewVector,
|
||||||
WebViewPoint, WebViewVector,
|
|
||||||
};
|
};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
KeyboardTextRequest, MouseClickRequest, MouseDragRequest, NavigationRequest,
|
KeyboardTextRequest, MouseClickRequest, MouseDragRequest, NavigationRequest, PageZoomRequest,
|
||||||
PermissionDecision, PermissionRequest, RenderedFrame, ResizeRequest, ScreenshotRequest,
|
PermissionDecision, PermissionRequest, RenderedFrame, ResizeRequest, ScreenshotRequest,
|
||||||
ScrollRequest, ServoHost, ServoHostError, TouchTapRequest, WebViewSnapshot, WebViewState,
|
ScrollRequest, ServoHost, ServoHostError, TouchTapRequest, WebViewSnapshot, WebViewState,
|
||||||
runtime_input::{send_keyboard_text, send_mouse_click, send_mouse_drag, send_touch_tap},
|
runtime_input::{send_keyboard_text, send_mouse_click, send_mouse_drag, send_touch_tap},
|
||||||
runtime_permissions::{
|
runtime_permissions::{PermissionStore, set_permission_decision},
|
||||||
PermissionStore, permission_decision_for_webview, set_permission_decision,
|
|
||||||
},
|
|
||||||
runtime_waker::ServoWakeFlag,
|
runtime_waker::ServoWakeFlag,
|
||||||
|
runtime_webview::{HostWebView, HostWebViewDelegate},
|
||||||
};
|
};
|
||||||
|
|
||||||
static SERVO_RUNTIME_STARTED: AtomicBool = AtomicBool::new(false);
|
static SERVO_RUNTIME_STARTED: AtomicBool = AtomicBool::new(false);
|
||||||
@@ -194,6 +192,16 @@ impl ServoHost for SoftwareServoHost {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn set_page_zoom(&mut self, request: PageZoomRequest) -> Result<(), ServoHostError> {
|
||||||
|
let webview = self
|
||||||
|
.webviews
|
||||||
|
.get(&request.webview_id)
|
||||||
|
.ok_or_else(|| ServoHostError::WebViewNotFound { id: request.webview_id.clone() })?;
|
||||||
|
|
||||||
|
webview.webview.set_page_zoom(request.zoom_factor);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn click(&mut self, request: MouseClickRequest) -> Result<(), ServoHostError> {
|
fn click(&mut self, request: MouseClickRequest) -> Result<(), ServoHostError> {
|
||||||
let webview = self
|
let webview = self
|
||||||
.webviews
|
.webviews
|
||||||
@@ -356,141 +364,3 @@ impl SoftwareServoHost {
|
|||||||
Ok(RenderedFrame::from_rgba_bytes(size.width, size.height, image.into_raw()))
|
Ok(RenderedFrame::from_rgba_bytes(size.width, size.height, image.into_raw()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct HostWebView {
|
|
||||||
tab_id: TabId,
|
|
||||||
profile_id: ProfileId,
|
|
||||||
webview: WebView,
|
|
||||||
delegate: Rc<HostWebViewDelegate>,
|
|
||||||
requested_url: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl HostWebView {
|
|
||||||
fn snapshot(&self, webview_id: &WebViewId) -> WebViewSnapshot {
|
|
||||||
WebViewSnapshot::new(
|
|
||||||
webview_id.clone(),
|
|
||||||
self.tab_id.clone(),
|
|
||||||
self.profile_id.clone(),
|
|
||||||
self.state(),
|
|
||||||
self.current_url(),
|
|
||||||
self.current_title(),
|
|
||||||
self.delegate.has_pending_frame(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn state(&self) -> WebViewState {
|
|
||||||
let state = self.delegate.state();
|
|
||||||
if matches!(state, WebViewState::Crashed | WebViewState::Sleeping) {
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(requested_url) = &self.requested_url
|
|
||||||
&& self.current_url().as_deref() != Some(requested_url.as_str())
|
|
||||||
{
|
|
||||||
return WebViewState::Loading;
|
|
||||||
}
|
|
||||||
|
|
||||||
state
|
|
||||||
}
|
|
||||||
|
|
||||||
fn current_url(&self) -> Option<String> {
|
|
||||||
self.webview.url().map(|url| url.to_string()).or_else(|| self.delegate.url())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn current_title(&self) -> Option<String> {
|
|
||||||
self.webview.page_title().or_else(|| self.delegate.title())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct HostWebViewDelegate {
|
|
||||||
profile_id: ProfileId,
|
|
||||||
permissions: PermissionStore,
|
|
||||||
state: RefCell<WebViewState>,
|
|
||||||
url: RefCell<Option<String>>,
|
|
||||||
title: RefCell<Option<String>>,
|
|
||||||
has_pending_frame: Cell<bool>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl HostWebViewDelegate {
|
|
||||||
fn new(profile_id: ProfileId, permissions: PermissionStore) -> Self {
|
|
||||||
Self {
|
|
||||||
profile_id,
|
|
||||||
permissions,
|
|
||||||
state: RefCell::new(WebViewState::Created),
|
|
||||||
url: RefCell::new(None),
|
|
||||||
title: RefCell::new(None),
|
|
||||||
has_pending_frame: Cell::new(false),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_state(&self, state: WebViewState) {
|
|
||||||
self.state.replace(state);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn state(&self) -> WebViewState {
|
|
||||||
self.state.borrow().clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn url(&self) -> Option<String> {
|
|
||||||
self.url.borrow().clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn title(&self) -> Option<String> {
|
|
||||||
self.title.borrow().clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn has_pending_frame(&self) -> bool {
|
|
||||||
self.has_pending_frame.get()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn mark_frame_presented(&self) {
|
|
||||||
self.has_pending_frame.set(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WebViewDelegate for HostWebViewDelegate {
|
|
||||||
fn notify_url_changed(&self, _webview: WebView, url: Url) {
|
|
||||||
self.url.replace(Some(url.to_string()));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn notify_page_title_changed(&self, _webview: WebView, title: Option<String>) {
|
|
||||||
self.title.replace(title);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn notify_load_status_changed(&self, _webview: WebView, status: LoadStatus) {
|
|
||||||
let state = match status {
|
|
||||||
LoadStatus::Started | LoadStatus::HeadParsed => WebViewState::Loading,
|
|
||||||
LoadStatus::Complete => WebViewState::Complete,
|
|
||||||
};
|
|
||||||
self.set_state(state);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn notify_new_frame_ready(&self, _webview: WebView) {
|
|
||||||
self.has_pending_frame.set(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn notify_crashed(&self, _webview: WebView, _reason: String, _backtrace: Option<String>) {
|
|
||||||
self.set_state(WebViewState::Crashed);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn request_navigation(&self, _webview: WebView, navigation_request: servo::NavigationRequest) {
|
|
||||||
navigation_request.allow();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn request_permission(&self, webview: WebView, permission_request: servo::PermissionRequest) {
|
|
||||||
match permission_decision_for_webview(
|
|
||||||
&self.permissions,
|
|
||||||
&self.profile_id,
|
|
||||||
&webview,
|
|
||||||
self.url(),
|
|
||||||
permission_request.feature(),
|
|
||||||
) {
|
|
||||||
Some(PermissionDecision::AllowOnce | PermissionDecision::AllowAlways) => {
|
|
||||||
permission_request.allow();
|
|
||||||
}
|
|
||||||
Some(PermissionDecision::DenyAlways) | None => {
|
|
||||||
permission_request.deny();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,148 @@
|
|||||||
|
use std::{cell::Cell, cell::RefCell, rc::Rc};
|
||||||
|
|
||||||
|
use ely_domain::{ProfileId, TabId, WebViewId};
|
||||||
|
use servo::{LoadStatus, WebView, WebViewDelegate};
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
PermissionDecision, WebViewSnapshot, WebViewState,
|
||||||
|
runtime_permissions::{PermissionStore, permission_decision_for_webview},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub(super) struct HostWebView {
|
||||||
|
pub(super) tab_id: TabId,
|
||||||
|
pub(super) profile_id: ProfileId,
|
||||||
|
pub(super) webview: WebView,
|
||||||
|
pub(super) delegate: Rc<HostWebViewDelegate>,
|
||||||
|
pub(super) requested_url: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HostWebView {
|
||||||
|
pub(super) fn snapshot(&self, webview_id: &WebViewId) -> WebViewSnapshot {
|
||||||
|
WebViewSnapshot::new(
|
||||||
|
webview_id.clone(),
|
||||||
|
self.tab_id.clone(),
|
||||||
|
self.profile_id.clone(),
|
||||||
|
self.state(),
|
||||||
|
self.current_url(),
|
||||||
|
self.current_title(),
|
||||||
|
self.delegate.has_pending_frame(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn state(&self) -> WebViewState {
|
||||||
|
let state = self.delegate.state();
|
||||||
|
if matches!(state, WebViewState::Crashed | WebViewState::Sleeping) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(requested_url) = &self.requested_url
|
||||||
|
&& self.current_url().as_deref() != Some(requested_url.as_str())
|
||||||
|
{
|
||||||
|
return WebViewState::Loading;
|
||||||
|
}
|
||||||
|
|
||||||
|
state
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn current_url(&self) -> Option<String> {
|
||||||
|
self.webview.url().map(|url| url.to_string()).or_else(|| self.delegate.url())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn current_title(&self) -> Option<String> {
|
||||||
|
self.webview.page_title().or_else(|| self.delegate.title())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) struct HostWebViewDelegate {
|
||||||
|
profile_id: ProfileId,
|
||||||
|
permissions: PermissionStore,
|
||||||
|
state: RefCell<WebViewState>,
|
||||||
|
url: RefCell<Option<String>>,
|
||||||
|
title: RefCell<Option<String>>,
|
||||||
|
has_pending_frame: Cell<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HostWebViewDelegate {
|
||||||
|
pub(super) fn new(profile_id: ProfileId, permissions: PermissionStore) -> Self {
|
||||||
|
Self {
|
||||||
|
profile_id,
|
||||||
|
permissions,
|
||||||
|
state: RefCell::new(WebViewState::Created),
|
||||||
|
url: RefCell::new(None),
|
||||||
|
title: RefCell::new(None),
|
||||||
|
has_pending_frame: Cell::new(false),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn set_state(&self, state: WebViewState) {
|
||||||
|
self.state.replace(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn state(&self) -> WebViewState {
|
||||||
|
self.state.borrow().clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn url(&self) -> Option<String> {
|
||||||
|
self.url.borrow().clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn title(&self) -> Option<String> {
|
||||||
|
self.title.borrow().clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn has_pending_frame(&self) -> bool {
|
||||||
|
self.has_pending_frame.get()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn mark_frame_presented(&self) {
|
||||||
|
self.has_pending_frame.set(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WebViewDelegate for HostWebViewDelegate {
|
||||||
|
fn notify_url_changed(&self, _webview: WebView, url: Url) {
|
||||||
|
self.url.replace(Some(url.to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn notify_page_title_changed(&self, _webview: WebView, title: Option<String>) {
|
||||||
|
self.title.replace(title);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn notify_load_status_changed(&self, _webview: WebView, status: LoadStatus) {
|
||||||
|
let state = match status {
|
||||||
|
LoadStatus::Started | LoadStatus::HeadParsed => WebViewState::Loading,
|
||||||
|
LoadStatus::Complete => WebViewState::Complete,
|
||||||
|
};
|
||||||
|
self.set_state(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn notify_new_frame_ready(&self, _webview: WebView) {
|
||||||
|
self.has_pending_frame.set(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn notify_crashed(&self, _webview: WebView, _reason: String, _backtrace: Option<String>) {
|
||||||
|
self.set_state(WebViewState::Crashed);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn request_navigation(&self, _webview: WebView, navigation_request: servo::NavigationRequest) {
|
||||||
|
navigation_request.allow();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn request_permission(&self, webview: WebView, permission_request: servo::PermissionRequest) {
|
||||||
|
match permission_decision_for_webview(
|
||||||
|
&self.permissions,
|
||||||
|
&self.profile_id,
|
||||||
|
&webview,
|
||||||
|
self.url(),
|
||||||
|
permission_request.feature(),
|
||||||
|
) {
|
||||||
|
Some(PermissionDecision::AllowOnce | PermissionDecision::AllowAlways) => {
|
||||||
|
permission_request.allow();
|
||||||
|
}
|
||||||
|
Some(PermissionDecision::DenyAlways) | None => {
|
||||||
|
permission_request.deny();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,7 +10,7 @@ use std::{
|
|||||||
|
|
||||||
use ely_domain::{ProfileId, SiteOrigin, SitePermissionFeature, TabId, UrlText};
|
use ely_domain::{ProfileId, SiteOrigin, SitePermissionFeature, TabId, UrlText};
|
||||||
use ely_servo_host::{
|
use ely_servo_host::{
|
||||||
KeyboardTextRequest, MouseClickRequest, MouseDragRequest, NavigationRequest,
|
KeyboardTextRequest, MouseClickRequest, MouseDragRequest, NavigationRequest, PageZoomRequest,
|
||||||
PermissionDecision, PermissionRequest, ResizeRequest, ScreenshotRequest, ScrollRequest,
|
PermissionDecision, PermissionRequest, ResizeRequest, ScreenshotRequest, ScrollRequest,
|
||||||
ServoHost, ServoHostError, ServoSurfaceSize, SoftwareServoHost, TouchTapRequest, WebViewState,
|
ServoHost, ServoHostError, ServoSurfaceSize, SoftwareServoHost, TouchTapRequest, WebViewState,
|
||||||
};
|
};
|
||||||
@@ -120,6 +120,18 @@ fn exercise_real_servo_webview_lifecycle() -> Result<(), Box<dyn Error>> {
|
|||||||
);
|
);
|
||||||
assert_rendered_frame_has_content(&host, "data:text/html", 1)?;
|
assert_rendered_frame_has_content(&host, "data:text/html", 1)?;
|
||||||
|
|
||||||
|
let previous_frame_hash = host.last_rendered_frame()?.sample_hash();
|
||||||
|
host.set_page_zoom(PageZoomRequest { webview_id: webview_id.clone(), zoom_factor: 1.25 })?;
|
||||||
|
let snapshot = wait_for_rendered_webview(&mut host, &webview_id, Some(previous_frame_hash))?;
|
||||||
|
assert_eq!(snapshot.state(), &WebViewState::Complete, "snapshot: {snapshot:?}");
|
||||||
|
assert_rendered_frame_has_content(&host, "data:text/html zoomed", 1)?;
|
||||||
|
assert_ne!(host.last_rendered_frame()?.sample_hash(), previous_frame_hash);
|
||||||
|
|
||||||
|
let previous_frame_hash = host.last_rendered_frame()?.sample_hash();
|
||||||
|
host.set_page_zoom(PageZoomRequest { webview_id: webview_id.clone(), zoom_factor: 1.0 })?;
|
||||||
|
let snapshot = wait_for_rendered_webview(&mut host, &webview_id, Some(previous_frame_hash))?;
|
||||||
|
assert_eq!(snapshot.state(), &WebViewState::Complete, "snapshot: {snapshot:?}");
|
||||||
|
|
||||||
let previous_frame_hash = host.last_rendered_frame()?.sample_hash();
|
let previous_frame_hash = host.last_rendered_frame()?.sample_hash();
|
||||||
host.click(MouseClickRequest { webview_id: webview_id.clone(), x: 160, y: 120 })?;
|
host.click(MouseClickRequest { webview_id: webview_id.clone(), x: 160, y: 120 })?;
|
||||||
let snapshot = wait_for_rendered_webview(&mut host, &webview_id, Some(previous_frame_hash))?;
|
let snapshot = wait_for_rendered_webview(&mut host, &webview_id, Some(previous_frame_hash))?;
|
||||||
|
|||||||
Reference in New Issue
Block a user