Scope Servo live runtimes by profile
This commit is contained in:
@@ -31,7 +31,7 @@ pub(crate) fn transient_profile_data_dir(
|
||||
)))
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
|
||||
pub(crate) enum ProfileDataMode {
|
||||
Persistent,
|
||||
Transient,
|
||||
|
||||
@@ -16,13 +16,23 @@ use super::{
|
||||
};
|
||||
|
||||
pub(super) struct WebSurfaceRuntime {
|
||||
state: RuntimeState,
|
||||
clients: BTreeMap<WebSurfaceRuntimeScope, ScopedRuntimeClient>,
|
||||
sessions: BTreeMap<TabId, WebSurfaceSession>,
|
||||
client_factory: LiveRuntimeClientFactory,
|
||||
}
|
||||
|
||||
impl WebSurfaceRuntime {
|
||||
pub(super) fn new() -> Self {
|
||||
Self { state: RuntimeState::Empty, sessions: BTreeMap::new() }
|
||||
Self {
|
||||
clients: BTreeMap::new(),
|
||||
sessions: BTreeMap::new(),
|
||||
client_factory: new_servo_live_client,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn new_with_client_factory(client_factory: LiveRuntimeClientFactory) -> Self {
|
||||
Self { clients: BTreeMap::new(), sessions: BTreeMap::new(), client_factory }
|
||||
}
|
||||
|
||||
pub(super) fn ensure_tab(
|
||||
@@ -38,19 +48,13 @@ impl WebSurfaceRuntime {
|
||||
|
||||
let requested_url = tab.url().as_str().to_string();
|
||||
let zoom_percent = tab.zoom_percent();
|
||||
let (state, sessions) = (&mut self.state, &mut self.sessions);
|
||||
let RuntimeState::Ready { scope: active_scope, client, .. } = state else {
|
||||
return Err("Servo live runtime is unavailable".to_string());
|
||||
};
|
||||
if active_scope != &scope {
|
||||
return Err(active_scope.error_for(&scope));
|
||||
}
|
||||
let (scroll_delta_x, scroll_delta_y, scroll_point_x, scroll_point_y) =
|
||||
scroll_wire_fields(input.scroll_delta, input.scroll_point)?;
|
||||
let user_navigation_input = input_requests_history_navigation(&input);
|
||||
|
||||
let session = sessions.entry(tab.id().clone()).or_insert_with(WebSurfaceSession::default);
|
||||
let next_scroll_offset = input.scroll_offset;
|
||||
let started_loading = {
|
||||
let session = session_for_scope(&mut self.sessions, tab.id(), scope.clone());
|
||||
let started_loading = session.started_loading(&requested_url, size, zoom_percent);
|
||||
if started_loading {
|
||||
session.pending_user_navigation = false;
|
||||
@@ -58,7 +62,11 @@ impl WebSurfaceRuntime {
|
||||
if user_navigation_input {
|
||||
session.pending_user_navigation = true;
|
||||
}
|
||||
let frame = client
|
||||
started_loading
|
||||
};
|
||||
|
||||
let frame = self
|
||||
.client_for_scope(&scope)?
|
||||
.ensure(ServoLiveEnsureRequest {
|
||||
tab_id: tab.id().as_str().to_string(),
|
||||
profile_id: tab.profile_id().as_str().to_string(),
|
||||
@@ -77,8 +85,7 @@ impl WebSurfaceRuntime {
|
||||
hover_y: input.hover_point.map(|point| point.y()),
|
||||
typed_text: input.typed_text,
|
||||
site_permissions: permissions.iter().map(ServoLiveSitePermission::from).collect(),
|
||||
})
|
||||
.map_err(|error| error.to_string())?
|
||||
})?
|
||||
.map(|frame| {
|
||||
WebSurfaceFrame::from_live_frame(
|
||||
requested_url.clone(),
|
||||
@@ -89,10 +96,13 @@ impl WebSurfaceRuntime {
|
||||
.map_err(|error| error.to_string())
|
||||
})
|
||||
.transpose()?;
|
||||
let url_change = frame
|
||||
.as_ref()
|
||||
.and_then(|frame| session.url_change_for(tab.id(), requested_url.as_str(), frame));
|
||||
let url_change = frame.as_ref().and_then(|frame| {
|
||||
self.sessions
|
||||
.get_mut(tab.id())
|
||||
.and_then(|session| session.url_change_for(tab.id(), requested_url.as_str(), frame))
|
||||
});
|
||||
|
||||
let session = session_for_scope(&mut self.sessions, tab.id(), scope);
|
||||
session.requested_url = requested_url.clone();
|
||||
session.size = size;
|
||||
session.zoom_percent = zoom_percent;
|
||||
@@ -102,43 +112,37 @@ impl WebSurfaceRuntime {
|
||||
}
|
||||
|
||||
pub(super) fn tick(&mut self, visible_tab_ids: &[TabId]) -> Vec<WebSurfaceRuntimeFrame> {
|
||||
let (state, sessions) = (&mut self.state, &mut self.sessions);
|
||||
let RuntimeState::Ready { client, .. } = state else {
|
||||
return Vec::new();
|
||||
};
|
||||
|
||||
let mut frames = Vec::new();
|
||||
for (tab_id, session) in sessions {
|
||||
if !visible_tab_ids.iter().any(|visible_tab_id| visible_tab_id == tab_id) {
|
||||
continue;
|
||||
}
|
||||
match client.poll(tab_id.as_str().to_string()) {
|
||||
for job in visible_poll_jobs(&self.sessions, visible_tab_ids) {
|
||||
let result = match self.clients.get_mut(&job.scope) {
|
||||
Some(client) => client.client.poll(job.tab_id.as_str().to_string()),
|
||||
None => Err(missing_runtime_message(&job.scope)),
|
||||
};
|
||||
match result {
|
||||
Ok(Some(frame)) => match WebSurfaceFrame::from_live_frame(
|
||||
session.requested_url.clone(),
|
||||
session.scroll_offset,
|
||||
session.zoom_percent,
|
||||
job.requested_url.clone(),
|
||||
job.scroll_offset,
|
||||
job.zoom_percent,
|
||||
frame,
|
||||
) {
|
||||
Ok(frame) => {
|
||||
let requested_url = session.requested_url.clone();
|
||||
let url_change =
|
||||
session.url_change_for(tab_id, requested_url.as_str(), &frame);
|
||||
let url_change = self.sessions.get_mut(&job.tab_id).and_then(|session| {
|
||||
session.url_change_for(&job.tab_id, job.requested_url.as_str(), &frame)
|
||||
});
|
||||
frames.push(WebSurfaceRuntimeFrame::Ready {
|
||||
tab_id: tab_id.clone(),
|
||||
tab_id: job.tab_id,
|
||||
frame: Box::new(frame),
|
||||
url_change,
|
||||
})
|
||||
}
|
||||
Err(error) => frames.push(WebSurfaceRuntimeFrame::Failed {
|
||||
tab_id: tab_id.clone(),
|
||||
tab_id: job.tab_id,
|
||||
message: error.to_string(),
|
||||
}),
|
||||
},
|
||||
Ok(None) => {}
|
||||
Err(error) => frames.push(WebSurfaceRuntimeFrame::Failed {
|
||||
tab_id: tab_id.clone(),
|
||||
message: error.to_string(),
|
||||
}),
|
||||
Err(error) => frames
|
||||
.push(WebSurfaceRuntimeFrame::Failed { tab_id: job.tab_id, message: error }),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,38 +150,82 @@ impl WebSurfaceRuntime {
|
||||
}
|
||||
|
||||
fn ensure_runtime(&mut self, scope: WebSurfaceRuntimeScope) -> Result<(), String> {
|
||||
match &self.state {
|
||||
RuntimeState::Empty => {
|
||||
if self.clients.contains_key(&scope) {
|
||||
return Ok(());
|
||||
}
|
||||
let (config_dir, transient_profile_data_dir) = config_dir_for_scope(&scope)?;
|
||||
let client = ServoLiveClient::new(config_dir).map_err(|error| error.to_string())?;
|
||||
self.state = RuntimeState::Ready { scope, client, transient_profile_data_dir };
|
||||
let client = (self.client_factory)(config_dir)?;
|
||||
self.clients.insert(scope, ScopedRuntimeClient { client, transient_profile_data_dir });
|
||||
Ok(())
|
||||
}
|
||||
RuntimeState::Ready { scope: active_scope, .. } if active_scope == &scope => Ok(()),
|
||||
RuntimeState::Ready { scope: active_scope, .. } => Err(active_scope.error_for(&scope)),
|
||||
|
||||
fn client_for_scope(
|
||||
&mut self,
|
||||
scope: &WebSurfaceRuntimeScope,
|
||||
) -> Result<&mut dyn LiveRuntimeClient, String> {
|
||||
match self.clients.get_mut(scope) {
|
||||
Some(client) => Ok(client.client.as_mut()),
|
||||
None => Err(missing_runtime_message(scope)),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn client_count_for_test(&self) -> usize {
|
||||
self.clients.len()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn session_scope_for_test(&self, tab_id: &TabId) -> Option<&WebSurfaceRuntimeScope> {
|
||||
self.sessions.get(tab_id).map(|session| &session.scope)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for WebSurfaceRuntime {
|
||||
fn drop(&mut self) {
|
||||
let RuntimeState::Ready { transient_profile_data_dir: Some(path), .. } = &self.state else {
|
||||
return;
|
||||
};
|
||||
let transient_profile_data_dirs = self
|
||||
.clients
|
||||
.values()
|
||||
.filter_map(|client| client.transient_profile_data_dir.clone())
|
||||
.collect::<Vec<_>>();
|
||||
self.clients.clear();
|
||||
for path in transient_profile_data_dirs {
|
||||
let _ = fs::remove_dir_all(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum RuntimeState {
|
||||
Empty,
|
||||
Ready {
|
||||
scope: WebSurfaceRuntimeScope,
|
||||
client: ServoLiveClient,
|
||||
transient_profile_data_dir: Option<PathBuf>,
|
||||
},
|
||||
type LiveRuntimeClientFactory = fn(PathBuf) -> Result<Box<dyn LiveRuntimeClient>, String>;
|
||||
|
||||
trait LiveRuntimeClient {
|
||||
fn ensure(&mut self, request: ServoLiveEnsureRequest) -> Result<Option<WebLiveFrame>, String>;
|
||||
|
||||
fn poll(&mut self, tab_id: String) -> Result<Option<WebLiveFrame>, String>;
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
type WebLiveFrame = crate::services::servo_live::ServoLiveFrame;
|
||||
|
||||
impl LiveRuntimeClient for ServoLiveClient {
|
||||
fn ensure(&mut self, request: ServoLiveEnsureRequest) -> Result<Option<WebLiveFrame>, String> {
|
||||
ServoLiveClient::ensure(self, request).map_err(|error| error.to_string())
|
||||
}
|
||||
|
||||
fn poll(&mut self, tab_id: String) -> Result<Option<WebLiveFrame>, String> {
|
||||
ServoLiveClient::poll(self, tab_id).map_err(|error| error.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
fn new_servo_live_client(config_dir: PathBuf) -> Result<Box<dyn LiveRuntimeClient>, String> {
|
||||
ServoLiveClient::new(config_dir)
|
||||
.map(|client| Box::new(client) as Box<dyn LiveRuntimeClient>)
|
||||
.map_err(|error| error.to_string())
|
||||
}
|
||||
|
||||
struct ScopedRuntimeClient {
|
||||
client: Box<dyn LiveRuntimeClient>,
|
||||
transient_profile_data_dir: Option<PathBuf>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
|
||||
struct WebSurfaceRuntimeScope {
|
||||
profile_id: ProfileId,
|
||||
profile_data_mode: ProfileDataMode,
|
||||
@@ -187,20 +235,11 @@ impl WebSurfaceRuntimeScope {
|
||||
fn new(profile_id: ProfileId, profile_data_mode: ProfileDataMode) -> Self {
|
||||
Self { profile_id, profile_data_mode }
|
||||
}
|
||||
|
||||
fn error_for(&self, requested: &Self) -> String {
|
||||
format!(
|
||||
"Servo live runtime is already attached to profile {} ({:?}); requested profile {} ({:?})",
|
||||
self.profile_id.as_str(),
|
||||
self.profile_data_mode,
|
||||
requested.profile_id.as_str(),
|
||||
requested.profile_data_mode
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
#[derive(Clone)]
|
||||
struct WebSurfaceSession {
|
||||
scope: WebSurfaceRuntimeScope,
|
||||
requested_url: String,
|
||||
size: WebSurfaceSize,
|
||||
zoom_percent: u16,
|
||||
@@ -209,6 +248,17 @@ struct WebSurfaceSession {
|
||||
}
|
||||
|
||||
impl WebSurfaceSession {
|
||||
fn new(scope: WebSurfaceRuntimeScope) -> Self {
|
||||
Self {
|
||||
scope,
|
||||
requested_url: String::new(),
|
||||
size: WebSurfaceSize::default(),
|
||||
zoom_percent: 0,
|
||||
scroll_offset: WebSurfaceScrollOffset::default(),
|
||||
pending_user_navigation: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn started_loading(
|
||||
&self,
|
||||
requested_url: &str,
|
||||
@@ -245,6 +295,15 @@ impl WebSurfaceSession {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct WebSurfacePollJob {
|
||||
tab_id: TabId,
|
||||
scope: WebSurfaceRuntimeScope,
|
||||
requested_url: String,
|
||||
scroll_offset: WebSurfaceScrollOffset,
|
||||
zoom_percent: u16,
|
||||
}
|
||||
|
||||
pub(super) struct WebSurfaceEnsureResult {
|
||||
pub(super) requested_url: String,
|
||||
pub(super) started_loading: bool,
|
||||
@@ -290,6 +349,46 @@ fn config_dir_for_scope(
|
||||
}
|
||||
}
|
||||
|
||||
fn session_for_scope<'a>(
|
||||
sessions: &'a mut BTreeMap<TabId, WebSurfaceSession>,
|
||||
tab_id: &TabId,
|
||||
scope: WebSurfaceRuntimeScope,
|
||||
) -> &'a mut WebSurfaceSession {
|
||||
let session =
|
||||
sessions.entry(tab_id.clone()).or_insert_with(|| WebSurfaceSession::new(scope.clone()));
|
||||
if session.scope != scope {
|
||||
*session = WebSurfaceSession::new(scope);
|
||||
}
|
||||
session
|
||||
}
|
||||
|
||||
fn visible_poll_jobs(
|
||||
sessions: &BTreeMap<TabId, WebSurfaceSession>,
|
||||
visible_tab_ids: &[TabId],
|
||||
) -> Vec<WebSurfacePollJob> {
|
||||
sessions
|
||||
.iter()
|
||||
.filter(|(tab_id, _)| {
|
||||
visible_tab_ids.iter().any(|visible_tab_id| visible_tab_id == *tab_id)
|
||||
})
|
||||
.map(|(tab_id, session)| WebSurfacePollJob {
|
||||
tab_id: tab_id.clone(),
|
||||
scope: session.scope.clone(),
|
||||
requested_url: session.requested_url.clone(),
|
||||
scroll_offset: session.scroll_offset,
|
||||
zoom_percent: session.zoom_percent,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn missing_runtime_message(scope: &WebSurfaceRuntimeScope) -> String {
|
||||
format!(
|
||||
"Servo live runtime is unavailable for profile {} ({:?})",
|
||||
scope.profile_id.as_str(),
|
||||
scope.profile_data_mode
|
||||
)
|
||||
}
|
||||
|
||||
fn scroll_wire_fields(
|
||||
delta: Option<super::web_surface_geometry::WebSurfaceScrollDelta>,
|
||||
point: Option<super::web_surface_geometry::WebSurfaceClickPoint>,
|
||||
@@ -318,3 +417,7 @@ impl From<&WebSurfaceSitePermission> for ServoLiveSitePermission {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "web_surface_runtime_tests.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use ely_domain::{BrowserTab, ProfileId, SpaceId, TabId, UrlText};
|
||||
|
||||
use crate::{
|
||||
services::ProfileDataMode,
|
||||
shell::{
|
||||
web_surface_geometry::{WebSurfaceScrollOffset, WebSurfaceSize},
|
||||
web_surface_state::WebSurfacePendingInput,
|
||||
},
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn runtime_keeps_independent_clients_for_profile_scopes() -> Result<(), String> {
|
||||
let mut runtime = WebSurfaceRuntime::new_with_client_factory(fake_client_factory);
|
||||
let first_profile = ProfileId::new();
|
||||
let second_profile = ProfileId::new();
|
||||
let first_tab = web_tab(TabId::new(), first_profile.clone(), "https://example.com/first")?;
|
||||
let second_tab = web_tab(TabId::new(), second_profile.clone(), "https://example.com/second")?;
|
||||
|
||||
runtime.ensure_tab(
|
||||
&first_tab,
|
||||
surface_size(),
|
||||
ProfileDataMode::Transient,
|
||||
&[],
|
||||
pending_input(),
|
||||
)?;
|
||||
runtime.ensure_tab(
|
||||
&second_tab,
|
||||
surface_size(),
|
||||
ProfileDataMode::Transient,
|
||||
&[],
|
||||
pending_input(),
|
||||
)?;
|
||||
runtime.ensure_tab(
|
||||
&first_tab,
|
||||
surface_size(),
|
||||
ProfileDataMode::Transient,
|
||||
&[],
|
||||
pending_input(),
|
||||
)?;
|
||||
|
||||
assert_eq!(runtime.client_count_for_test(), 2);
|
||||
assert_eq!(
|
||||
runtime.session_scope_for_test(first_tab.id()),
|
||||
Some(&WebSurfaceRuntimeScope::new(first_profile, ProfileDataMode::Transient)),
|
||||
);
|
||||
assert_eq!(
|
||||
runtime.session_scope_for_test(second_tab.id()),
|
||||
Some(&WebSurfaceRuntimeScope::new(second_profile, ProfileDataMode::Transient)),
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn session_scope_change_resets_tab_state() {
|
||||
let tab_id = TabId::new();
|
||||
let first_scope = WebSurfaceRuntimeScope::new(ProfileId::new(), ProfileDataMode::Persistent);
|
||||
let second_scope = WebSurfaceRuntimeScope::new(ProfileId::new(), ProfileDataMode::Transient);
|
||||
let mut sessions = BTreeMap::new();
|
||||
|
||||
let session = session_for_scope(&mut sessions, &tab_id, first_scope);
|
||||
session.requested_url = "https://example.com/old".to_string();
|
||||
session.size = surface_size();
|
||||
session.zoom_percent = 150;
|
||||
session.scroll_offset = WebSurfaceScrollOffset::default();
|
||||
session.pending_user_navigation = true;
|
||||
|
||||
let session = session_for_scope(&mut sessions, &tab_id, second_scope.clone());
|
||||
|
||||
assert_eq!(session.scope, second_scope);
|
||||
assert_eq!(session.requested_url, "");
|
||||
assert_eq!(session.size, WebSurfaceSize::default());
|
||||
assert_eq!(session.zoom_percent, 0);
|
||||
assert_eq!(session.scroll_offset, WebSurfaceScrollOffset::default());
|
||||
assert!(!session.pending_user_navigation);
|
||||
}
|
||||
|
||||
struct FakeLiveRuntimeClient;
|
||||
|
||||
impl LiveRuntimeClient for FakeLiveRuntimeClient {
|
||||
fn ensure(&mut self, _request: ServoLiveEnsureRequest) -> Result<Option<WebLiveFrame>, String> {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn poll(&mut self, _tab_id: String) -> Result<Option<WebLiveFrame>, String> {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
fn fake_client_factory(
|
||||
_config_dir: std::path::PathBuf,
|
||||
) -> Result<Box<dyn LiveRuntimeClient>, String> {
|
||||
Ok(Box::new(FakeLiveRuntimeClient))
|
||||
}
|
||||
|
||||
fn web_tab(tab_id: TabId, profile_id: ProfileId, url: &str) -> Result<BrowserTab, String> {
|
||||
let url = UrlText::parse(url).map_err(|error| error.to_string())?;
|
||||
Ok(BrowserTab::new(tab_id, SpaceId::new(), profile_id, "Web", url))
|
||||
}
|
||||
|
||||
fn surface_size() -> WebSurfaceSize {
|
||||
WebSurfaceSize { width: 640, height: 480, device_pixel_ratio_percent: 100 }
|
||||
}
|
||||
|
||||
fn pending_input() -> WebSurfacePendingInput {
|
||||
WebSurfacePendingInput {
|
||||
scroll_offset: WebSurfaceScrollOffset::default(),
|
||||
scroll_delta: None,
|
||||
scroll_point: None,
|
||||
click_point: None,
|
||||
hover_point: None,
|
||||
typed_text: None,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user