fix: sync website color scheme

This commit is contained in:
2026-07-10 17:31:10 -04:00
parent 37eb0a7825
commit 303a3aa926
28 changed files with 340 additions and 78 deletions
+5 -1
View File
@@ -13,7 +13,7 @@ in the same commit as any change that makes it stale.
local persistence. No IO except through callers. local persistence. No IO except through callers.
- `crates/ely_servo_host` — Servo embedding, rendering contexts, the - `crates/ely_servo_host` — Servo embedding, rendering contexts, the
`ely_servo_sidecar` binary (one Servo process per profile; stdio JSON `ely_servo_sidecar` binary (one Servo process per profile; stdio JSON
protocol v3 + macOS IOSurface Mach transport). See protocol v4 + macOS IOSurface Mach transport). See
`docs/servo-embedding-architecture.md`. `docs/servo-embedding-architecture.md`.
- `crates/ely_sync_client` — worker API client, device identity/trust, - `crates/ely_sync_client` — worker API client, device identity/trust,
E2E snapshot crypto, native-keychain bearer storage, sync-owner store. E2E snapshot crypto, native-keychain bearer storage, sync-owner store.
@@ -92,6 +92,10 @@ Real and verified:
profile worker boundary while preserving total device-pixel distance; profile worker boundary while preserving total device-pixel distance;
clicks, text, and allow-once permission transfers retain strict ordering. clicks, text, and allow-once permission transfers retain strict ordering.
Verified by a blocked-worker burst regression and the live-site scroll e2e. Verified by a blocked-worker burst regression and the live-site scroll e2e.
- Website color scheme: resolved System/Light/Dark state crosses sidecar
protocol v4 and reaches `WebView::notify_theme_change`; GPUI appearance
events trigger a shell repaint, and first-navigation WebView replacement
preserves the selected scheme. Verified with real Servo media-query pixels.
- Cross-platform: download Open/Reveal use per-OS launchers (macOS `open`, - Cross-platform: download Open/Reveal use per-OS launchers (macOS `open`,
Windows `cmd start`/`explorer /select`, Linux `xdg-open`); the command Windows `cmd start`/`explorer /select`, Linux `xdg-open`); the command
overlay closes on Escape. overlay closes on Escape.
@@ -151,6 +151,7 @@ impl ServoLiveClient {
height: request.height, height: request.height,
page_zoom_percent: request.page_zoom_percent, page_zoom_percent: request.page_zoom_percent,
device_pixel_ratio: request.device_pixel_ratio, device_pixel_ratio: request.device_pixel_ratio,
color_scheme: request.color_scheme,
scroll_delta_x: request.scroll_delta_x, scroll_delta_x: request.scroll_delta_x,
scroll_delta_y: request.scroll_delta_y, scroll_delta_y: request.scroll_delta_y,
scroll_point_x: request.scroll_point_x, scroll_point_x: request.scroll_point_x,
@@ -256,7 +256,7 @@ mod tests {
#[test] #[test]
fn reply_rejects_oversized_frame_before_readback_allocation() { fn reply_rejects_oversized_frame_before_readback_allocation() {
let header = format!( let header = format!(
"{{\"protocol_version\":3,\"error\":null,\"frame\":{{\"loaded_url\":null,\"title\":null,\"state\":\"complete\",\"width\":{0},\"height\":{0},\"device_pixel_ratio\":1.0,\"css_viewport_width\":{0},\"css_viewport_height\":{0},\"rgba_byte_count\":1073741824,\"pixels_changed\":true}}}}\n", "{{\"protocol_version\":4,\"error\":null,\"frame\":{{\"loaded_url\":null,\"title\":null,\"state\":\"complete\",\"width\":{0},\"height\":{0},\"device_pixel_ratio\":1.0,\"css_viewport_width\":{0},\"css_viewport_height\":{0},\"rgba_byte_count\":1073741824,\"pixels_changed\":true}}}}\n",
MAX_FRAME_DIMENSION MAX_FRAME_DIMENSION
); );
let mut input = Cursor::new(header.into_bytes()); let mut input = Cursor::new(header.into_bytes());
@@ -269,19 +269,19 @@ mod tests {
#[test] #[test]
fn reply_accepts_the_exact_header_limit() -> Result<(), ServoLiveError> { fn reply_accepts_the_exact_header_limit() -> Result<(), ServoLiveError> {
let mut header = br#"{"protocol_version":3,"error":null,"frame":null}"#.to_vec(); let mut header = br#"{"protocol_version":4,"error":null,"frame":null}"#.to_vec();
header.resize(MAX_RESPONSE_HEADER_BYTES - 1, b' '); header.resize(MAX_RESPONSE_HEADER_BYTES - 1, b' ');
header.push(b'\n'); header.push(b'\n');
let reply = read_reply(&mut Cursor::new(header))?; let reply = read_reply(&mut Cursor::new(header))?;
assert_eq!(reply.protocol_version, Some(3)); assert_eq!(reply.protocol_version, Some(4));
Ok(()) Ok(())
} }
#[test] #[test]
fn reply_rejects_one_byte_over_the_header_limit() { fn reply_rejects_one_byte_over_the_header_limit() {
let mut header = br#"{"protocol_version":3,"error":null,"frame":null}"#.to_vec(); let mut header = br#"{"protocol_version":4,"error":null,"frame":null}"#.to_vec();
header.resize(MAX_RESPONSE_HEADER_BYTES, b' '); header.resize(MAX_RESPONSE_HEADER_BYTES, b' ');
header.push(b'\n'); header.push(b'\n');
@@ -327,7 +327,7 @@ mod tests {
fn reply_parses_permission_consumption_without_a_frame() -> Result<(), ServoLiveError> { fn reply_parses_permission_consumption_without_a_frame() -> Result<(), ServoLiveError> {
let profile_id = ely_domain::ProfileId::new(); let profile_id = ely_domain::ProfileId::new();
let header = format!( let header = format!(
"{{\"protocol_version\":3,\"error\":null,\"frame\":null,\"permission_consumptions\":[{{\"profile_id\":\"{}\",\"origin\":\"https://example.com\",\"feature\":\"camera\",\"grant_revision\":7}}]}}\n", "{{\"protocol_version\":4,\"error\":null,\"frame\":null,\"permission_consumptions\":[{{\"profile_id\":\"{}\",\"origin\":\"https://example.com\",\"feature\":\"camera\",\"grant_revision\":7}}]}}\n",
profile_id.as_str(), profile_id.as_str(),
); );
let mut input = Cursor::new(header.into_bytes()); let mut input = Cursor::new(header.into_bytes());
@@ -350,7 +350,7 @@ mod tests {
#[test] #[test]
fn hardware_reply_uses_surface_without_rgba_allocation() -> Result<(), ServoLiveError> { fn hardware_reply_uses_surface_without_rgba_allocation() -> Result<(), ServoLiveError> {
let header = concat!( let header = concat!(
"{\"protocol_version\":3,\"error\":null,", "{\"protocol_version\":4,\"error\":null,",
"\"surface_handle\":{\"mach_port_name\":91,\"surface_id\":7,\"width\":64,\"height\":48},", "\"surface_handle\":{\"mach_port_name\":91,\"surface_id\":7,\"width\":64,\"height\":48},",
"\"current_surface_id\":7,", "\"current_surface_id\":7,",
"\"frame\":{\"loaded_url\":null,\"title\":null,\"state\":\"complete\",", "\"frame\":{\"loaded_url\":null,\"title\":null,\"state\":\"complete\",",
@@ -402,7 +402,7 @@ mod tests {
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
fn hardware_header(current_surface_id: u64, handle_width: u32, handle_height: u32) -> String { fn hardware_header(current_surface_id: u64, handle_width: u32, handle_height: u32) -> String {
format!( format!(
"{{\"protocol_version\":3,\"error\":null,\"surface_handle\":{{\"mach_port_name\":91,\"surface_id\":7,\"width\":{handle_width},\"height\":{handle_height}}},\"current_surface_id\":{current_surface_id},\"frame\":{{\"loaded_url\":null,\"title\":null,\"state\":\"complete\",\"width\":64,\"height\":48,\"device_pixel_ratio\":1.0,\"css_viewport_width\":64,\"css_viewport_height\":48,\"rgba_byte_count\":0,\"pixels_changed\":true}}}}\n" "{{\"protocol_version\":4,\"error\":null,\"surface_handle\":{{\"mach_port_name\":91,\"surface_id\":7,\"width\":{handle_width},\"height\":{handle_height}}},\"current_surface_id\":{current_surface_id},\"frame\":{{\"loaded_url\":null,\"title\":null,\"state\":\"complete\",\"width\":64,\"height\":48,\"device_pixel_ratio\":1.0,\"css_viewport_width\":64,\"css_viewport_height\":48,\"rgba_byte_count\":0,\"pixels_changed\":true}}}}\n"
) )
} }
} }
@@ -2,6 +2,7 @@
use std::sync::Arc; use std::sync::Arc;
use std::{collections::TryReserveError, io, path::PathBuf}; use std::{collections::TryReserveError, io, path::PathBuf};
use ely_domain::ColorScheme;
use serde::Serialize; use serde::Serialize;
use thiserror::Error; use thiserror::Error;
@@ -24,6 +25,7 @@ pub(crate) struct ServoLiveEnsureRequest {
pub(crate) page_zoom_percent: u16, pub(crate) page_zoom_percent: u16,
/// Display scale factor used to derive Servo's CSS viewport. /// Display scale factor used to derive Servo's CSS viewport.
pub(crate) device_pixel_ratio: f32, pub(crate) device_pixel_ratio: f32,
pub(crate) color_scheme: ColorScheme,
pub(crate) scroll_delta_x: i32, pub(crate) scroll_delta_x: i32,
pub(crate) scroll_delta_y: i32, pub(crate) scroll_delta_y: i32,
pub(crate) scroll_point_x: Option<u32>, pub(crate) scroll_point_x: Option<u32>,
@@ -1,8 +1,9 @@
use ely_domain::ColorScheme;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use super::ServoLiveSitePermission; use super::ServoLiveSitePermission;
pub(super) const LIVE_PROTOCOL_VERSION: u32 = 3; pub(super) const LIVE_PROTOCOL_VERSION: u32 = 4;
pub(super) const MAX_FRAME_DIMENSION: u32 = 16_384; pub(super) const MAX_FRAME_DIMENSION: u32 = 16_384;
pub(super) const MAX_FRAME_BYTE_COUNT: usize = 256 * 1024 * 1024; pub(super) const MAX_FRAME_BYTE_COUNT: usize = 256 * 1024 * 1024;
pub(super) const MAX_RESPONSE_HEADER_BYTES: usize = 256 * 1024; pub(super) const MAX_RESPONSE_HEADER_BYTES: usize = 256 * 1024;
@@ -22,6 +23,7 @@ pub(super) enum LiveRequest {
height: u32, height: u32,
page_zoom_percent: u16, page_zoom_percent: u16,
device_pixel_ratio: f32, device_pixel_ratio: f32,
color_scheme: ColorScheme,
scroll_delta_x: i32, scroll_delta_x: i32,
scroll_delta_y: i32, scroll_delta_y: i32,
scroll_point_x: Option<u32>, scroll_point_x: Option<u32>,
+4
View File
@@ -136,6 +136,7 @@ pub struct ElyShell {
pub(crate) local_state_save_scheduled: bool, pub(crate) local_state_save_scheduled: bool,
_command_subscription: Subscription, _command_subscription: Subscription,
_translucency_subscription: Subscription, _translucency_subscription: Subscription,
_appearance_subscription: Subscription,
_quit_save_subscription: Option<Subscription>, _quit_save_subscription: Option<Subscription>,
} }
@@ -251,6 +252,8 @@ impl ElyShell {
} }
ShellState::StartupError(_) => None, ShellState::StartupError(_) => None,
}; };
let appearance_subscription =
cx.observe_window_appearance(window, |_shell, _window, cx| cx.notify());
let mut shell = Self { let mut shell = Self {
state, state,
focus_handle: cx.focus_handle(), focus_handle: cx.focus_handle(),
@@ -307,6 +310,7 @@ impl ElyShell {
auth_flow_phase: auth::AuthFlowPhase::Idle, auth_flow_phase: auth::AuthFlowPhase::Idle,
_command_subscription: command_subscription, _command_subscription: command_subscription,
_translucency_subscription: translucency_subscription, _translucency_subscription: translucency_subscription,
_appearance_subscription: appearance_subscription,
_quit_save_subscription: None, _quit_save_subscription: None,
}; };
shell._quit_save_subscription = Some(local_persistence::register_quit_save(cx)); shell._quit_save_subscription = Some(local_persistence::register_quit_save(cx));
+56 -27
View File
@@ -27,35 +27,39 @@ impl Render for ElyShell {
match &self.state { match &self.state {
ShellState::Ready(core) => match core.snapshot() { ShellState::Ready(core) => match core.snapshot() {
Ok(snapshot) => { Ok(snapshot) => {
apply_color_mode( let color_scheme =
resolve_color_mode(snapshot.appearance.theme_mode(), appearance), resolve_color_scheme(snapshot.appearance.theme_mode(), appearance);
cx, self.web_surfaces.set_color_scheme(color_scheme);
); apply_color_scheme(color_scheme, cx);
match active_tab_from_snapshot(&snapshot) { match active_tab_from_snapshot(&snapshot) {
Some(active_tab) => self.render_browser(&snapshot, active_tab, window, cx), Some(active_tab) => self.render_browser(&snapshot, active_tab, window, cx),
None => render_error("active tab missing from snapshot".to_string()), None => render_error("active tab missing from snapshot".to_string()),
} }
} }
Err(error) => { Err(error) => {
apply_color_mode( let color_scheme =
resolve_color_mode(ely_domain::ThemeMode::default(), appearance), resolve_color_scheme(ely_domain::ThemeMode::default(), appearance);
cx, self.web_surfaces.set_color_scheme(color_scheme);
); apply_color_scheme(color_scheme, cx);
render_error(error.to_string()) render_error(error.to_string())
} }
}, },
ShellState::StartupError(message) => { ShellState::StartupError(message) => {
apply_color_mode( let color_scheme =
resolve_color_mode(ely_domain::ThemeMode::default(), appearance), resolve_color_scheme(ely_domain::ThemeMode::default(), appearance);
cx, self.web_surfaces.set_color_scheme(color_scheme);
); apply_color_scheme(color_scheme, cx);
render_error(message.clone()) render_error(message.clone())
} }
} }
} }
} }
fn apply_color_mode(mode: colors::Mode, cx: &mut Context<ElyShell>) { fn apply_color_scheme(color_scheme: ely_domain::ColorScheme, cx: &mut Context<ElyShell>) {
let mode = match color_scheme {
ely_domain::ColorScheme::Light => colors::Mode::Light,
ely_domain::ColorScheme::Dark => colors::Mode::Dark,
};
colors::set_mode(mode); colors::set_mode(mode);
let component_mode = match mode { let component_mode = match mode {
@@ -68,22 +72,19 @@ fn apply_color_mode(mode: colors::Mode, cx: &mut Context<ElyShell>) {
gpui_component::Theme::global_mut(cx).font_family = SANS_FAMILY.into(); gpui_component::Theme::global_mut(cx).font_family = SANS_FAMILY.into();
} }
fn resolve_color_mode( fn resolve_color_scheme(
theme_mode: ely_domain::ThemeMode, theme_mode: ely_domain::ThemeMode,
window_appearance: gpui::WindowAppearance, window_appearance: gpui::WindowAppearance,
) -> colors::Mode { ) -> ely_domain::ColorScheme {
match theme_mode { let system = match window_appearance {
ely_domain::ThemeMode::Light => colors::Mode::Light, gpui::WindowAppearance::Dark | gpui::WindowAppearance::VibrantDark => {
ely_domain::ThemeMode::Dark => colors::Mode::Dark, ely_domain::ColorScheme::Dark
ely_domain::ThemeMode::System => match window_appearance { }
gpui::WindowAppearance::Dark | gpui::WindowAppearance::VibrantDark => { gpui::WindowAppearance::Light | gpui::WindowAppearance::VibrantLight => {
colors::Mode::Dark ely_domain::ColorScheme::Light
} }
gpui::WindowAppearance::Light | gpui::WindowAppearance::VibrantLight => { };
colors::Mode::Light theme_mode.resolve(system)
}
},
}
} }
fn active_tab_from_snapshot(snapshot: &BrowserSnapshot) -> Option<&BrowserTab> { fn active_tab_from_snapshot(snapshot: &BrowserSnapshot) -> Option<&BrowserTab> {
@@ -428,3 +429,31 @@ pub(super) fn tab_profile_label(tab: &BrowserTab, profiles: &[ely_domain::Profil
.map(|profile| format!("Profile: {}", profile.name())) .map(|profile| format!("Profile: {}", profile.name()))
.unwrap_or_else(|| format!("Profile: {}", tab.profile_id().as_str())) .unwrap_or_else(|| format!("Profile: {}", tab.profile_id().as_str()))
} }
#[cfg(test)]
mod tests {
use ely_domain::{ColorScheme, ThemeMode};
use gpui::WindowAppearance;
use super::resolve_color_scheme;
#[test]
fn resolved_color_scheme_tracks_browser_and_system_modes() {
assert_eq!(
resolve_color_scheme(ThemeMode::System, WindowAppearance::Dark),
ColorScheme::Dark,
);
assert_eq!(
resolve_color_scheme(ThemeMode::System, WindowAppearance::VibrantLight),
ColorScheme::Light,
);
assert_eq!(
resolve_color_scheme(ThemeMode::Light, WindowAppearance::Dark),
ColorScheme::Light,
);
assert_eq!(
resolve_color_scheme(ThemeMode::Dark, WindowAppearance::Light),
ColorScheme::Dark,
);
}
}
+20 -3
View File
@@ -1,7 +1,7 @@
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use ely_domain::{BrowserTab, ProfileId, TabId, UrlText}; use ely_domain::{BrowserTab, ColorScheme, ProfileId, TabId, UrlText};
use crate::services::{ProfileDataMode, servo_live::ServoLivePermissionGrant}; use crate::services::{ProfileDataMode, servo_live::ServoLivePermissionGrant};
@@ -23,16 +23,27 @@ pub(super) struct WebSurfaceStore {
/// Singleton because only one tab at a time holds keyboard focus /// Singleton because only one tab at a time holds keyboard focus
/// across the whole window. Lives on the store, not per-tab. /// across the whole window. Lives on the store, not per-tab.
pub(super) keyboard_focus: Option<WebSurfaceKeyboardFocusState>, pub(super) keyboard_focus: Option<WebSurfaceKeyboardFocusState>,
color_scheme: ColorScheme,
} }
impl WebSurfaceStore { impl WebSurfaceStore {
pub(super) fn new() -> Self { pub(super) fn new() -> Self {
Self { runtime: WebSurfaceRuntime::new(), surfaces: BTreeMap::new(), keyboard_focus: None } Self {
runtime: WebSurfaceRuntime::new(),
surfaces: BTreeMap::new(),
keyboard_focus: None,
color_scheme: ColorScheme::Light,
}
} }
#[cfg(test)] #[cfg(test)]
pub(super) fn new_with_runtime(runtime: WebSurfaceRuntime) -> Self { pub(super) fn new_with_runtime(runtime: WebSurfaceRuntime) -> Self {
Self { runtime, surfaces: BTreeMap::new(), keyboard_focus: None } Self {
runtime,
surfaces: BTreeMap::new(),
keyboard_focus: None,
color_scheme: ColorScheme::Light,
}
} }
#[cfg(test)] #[cfg(test)]
@@ -72,6 +83,7 @@ impl WebSurfaceStore {
tab.profile_id().clone(), tab.profile_id().clone(),
profile_data_mode, profile_data_mode,
tab.zoom_percent(), tab.zoom_percent(),
self.color_scheme,
permissions, permissions,
); );
let scope_changed = self.surface_mut(tab.id()).reset_for_scope_change(&ensure_key); let scope_changed = self.surface_mut(tab.id()).reset_for_scope_change(&ensure_key);
@@ -365,6 +377,11 @@ impl WebSurfaceStore {
} }
} }
pub(super) fn set_color_scheme(&mut self, color_scheme: ColorScheme) {
self.color_scheme = color_scheme;
self.runtime.set_color_scheme(color_scheme);
}
#[cfg(test)] #[cfg(test)]
pub(super) fn surface_for_test(&self, tab_id: &TabId) -> Option<&PerTabSurface> { pub(super) fn surface_for_test(&self, tab_id: &TabId) -> Option<&PerTabSurface> {
self.surfaces.get(tab_id) self.surfaces.get(tab_id)
@@ -4,7 +4,7 @@ use std::{
time::{Duration, Instant}, time::{Duration, Instant},
}; };
use ely_domain::{BrowserTab, TabId}; use ely_domain::{BrowserTab, ColorScheme, TabId};
use crate::services::{ use crate::services::{
ProfileDataMode, ProfileDataMode,
@@ -43,6 +43,7 @@ pub(super) struct WebSurfaceRuntime {
transient_cleanup_error: Option<String>, transient_cleanup_error: Option<String>,
client_factory: LiveRuntimeClientFactory, client_factory: LiveRuntimeClientFactory,
last_generation: u64, last_generation: u64,
color_scheme: ColorScheme,
} }
const SIDECAR_RESTART_BASE_DELAY: Duration = Duration::from_millis(250); const SIDECAR_RESTART_BASE_DELAY: Duration = Duration::from_millis(250);
@@ -60,6 +61,7 @@ impl WebSurfaceRuntime {
transient_cleanup_error, transient_cleanup_error,
client_factory: new_servo_live_client, client_factory: new_servo_live_client,
last_generation: 0, last_generation: 0,
color_scheme: ColorScheme::Light,
} }
} }
@@ -74,6 +76,7 @@ impl WebSurfaceRuntime {
transient_cleanup_error: None, transient_cleanup_error: None,
client_factory, client_factory,
last_generation: 0, last_generation: 0,
color_scheme: ColorScheme::Light,
} }
} }
@@ -125,6 +128,7 @@ impl WebSurfaceRuntime {
height: size.height, height: size.height,
page_zoom_percent: zoom_percent, page_zoom_percent: zoom_percent,
device_pixel_ratio: size.device_pixel_ratio_f32(), device_pixel_ratio: size.device_pixel_ratio_f32(),
color_scheme: self.color_scheme,
scroll_delta_x, scroll_delta_x,
scroll_delta_y, scroll_delta_y,
scroll_point_x, scroll_point_x,
@@ -226,6 +230,10 @@ impl WebSurfaceRuntime {
}) })
} }
pub(super) fn set_color_scheme(&mut self, color_scheme: ColorScheme) {
self.color_scheme = color_scheme;
}
pub(super) fn prepare_tab_scope( pub(super) fn prepare_tab_scope(
&mut self, &mut self,
tab_id: &TabId, tab_id: &TabId,
@@ -472,3 +480,6 @@ mod retry_tests;
#[cfg(test)] #[cfg(test)]
#[path = "web_surface_runtime_tests.rs"] #[path = "web_surface_runtime_tests.rs"]
mod tests; mod tests;
#[cfg(test)]
#[path = "web_surface_theme_tests.rs"]
mod theme_tests;
@@ -1,6 +1,6 @@
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use ely_domain::{ProfileId, TabId}; use ely_domain::{ColorScheme, ProfileId, TabId};
use gpui::{Bounds, Pixels}; use gpui::{Bounds, Pixels};
use crate::services::ProfileDataMode; use crate::services::ProfileDataMode;
@@ -309,6 +309,7 @@ pub(super) struct WebSurfaceEnsureKey {
profile_id: ProfileId, profile_id: ProfileId,
profile_data_mode: ProfileDataMode, profile_data_mode: ProfileDataMode,
zoom_percent: u16, zoom_percent: u16,
color_scheme: ColorScheme,
permissions: Vec<WebSurfaceSitePermission>, permissions: Vec<WebSurfaceSitePermission>,
} }
@@ -319,6 +320,7 @@ impl WebSurfaceEnsureKey {
profile_id: ProfileId, profile_id: ProfileId,
profile_data_mode: ProfileDataMode, profile_data_mode: ProfileDataMode,
zoom_percent: u16, zoom_percent: u16,
color_scheme: ColorScheme,
permissions: &[WebSurfaceSitePermission], permissions: &[WebSurfaceSitePermission],
) -> Self { ) -> Self {
Self { Self {
@@ -327,6 +329,7 @@ impl WebSurfaceEnsureKey {
profile_id, profile_id,
profile_data_mode, profile_data_mode,
zoom_percent, zoom_percent,
color_scheme,
permissions: permissions.to_vec(), permissions: permissions.to_vec(),
} }
} }
@@ -446,6 +449,7 @@ mod tests {
profile_id.clone(), profile_id.clone(),
ProfileDataMode::Persistent, ProfileDataMode::Persistent,
100, 100,
ColorScheme::Light,
&[], &[],
) )
} }
@@ -0,0 +1,73 @@
use std::sync::Mutex;
use ely_domain::{BrowserTab, ColorScheme, ProfileId, SpaceId, TabId, UrlText};
use gpui::{Bounds, point, px, size};
use crate::services::{
ProfileDataMode,
servo_live::{ServoLiveEnsureRequest, ServoLiveFrame},
};
use super::{
super::{
web_surface::WebSurfaceStore,
web_surface_state::WebSurfaceInputOutcome,
web_surface_worker::{LiveRuntimeClient, LiveRuntimeClientError},
},
WebSurfaceRuntime,
};
static COLOR_SCHEMES: Mutex<Vec<ColorScheme>> = Mutex::new(Vec::new());
struct ThemeRecordingClient;
impl LiveRuntimeClient for ThemeRecordingClient {
fn ensure(
&mut self,
request: ServoLiveEnsureRequest,
) -> Result<Option<ServoLiveFrame>, LiveRuntimeClientError> {
COLOR_SCHEMES
.lock()
.map_err(|_| "color scheme recorder lock was poisoned".to_string())?
.push(request.color_scheme);
Ok(None)
}
fn poll(&mut self, _tab_id: String) -> Result<Option<ServoLiveFrame>, LiveRuntimeClientError> {
Ok(None)
}
fn close(&mut self, _tab_id: String) -> Result<(), LiveRuntimeClientError> {
Ok(())
}
}
#[test]
fn browser_color_scheme_reaches_each_web_surface_ensure() -> Result<(), String> {
COLOR_SCHEMES.lock().map_err(|_| "color scheme recorder lock was poisoned")?.clear();
let runtime =
WebSurfaceRuntime::new_with_client_factory(|_| Ok(Box::new(ThemeRecordingClient)));
let mut store = WebSurfaceStore::new_with_runtime(runtime);
let tab = BrowserTab::new(
TabId::new(),
SpaceId::new(),
ProfileId::new(),
"Theme",
UrlText::parse("https://example.com/theme").map_err(|error| error.to_string())?,
);
let bounds = Bounds::new(point(px(0.0), px(0.0)), size(px(640.0), px(480.0)));
assert_eq!(store.record_viewport_size(tab.id(), bounds, 1.0), WebSurfaceInputOutcome::Applied,);
let _ = store.ensure_surface(&tab, ProfileDataMode::Transient, &[]);
store.flush_runtime_for_test();
store.set_color_scheme(ColorScheme::Dark);
let _ = store.ensure_surface(&tab, ProfileDataMode::Transient, &[]);
store.flush_runtime_for_test();
assert_eq!(
*COLOR_SCHEMES.lock().map_err(|_| "color scheme recorder lock was poisoned")?,
vec![ColorScheme::Light, ColorScheme::Dark],
);
Ok(())
}
@@ -25,6 +25,7 @@ pub(super) fn can_merge_consecutive_scroll(
&& latest.height == previous.height && latest.height == previous.height
&& latest.page_zoom_percent == previous.page_zoom_percent && latest.page_zoom_percent == previous.page_zoom_percent
&& latest.device_pixel_ratio == previous.device_pixel_ratio && latest.device_pixel_ratio == previous.device_pixel_ratio
&& latest.color_scheme == previous.color_scheme
} }
pub(super) fn merge_consecutive_scroll(latest: &mut WorkerRequest, previous: &WorkerRequest) { pub(super) fn merge_consecutive_scroll(latest: &mut WorkerRequest, previous: &WorkerRequest) {
@@ -78,6 +78,7 @@ fn ensure_request(scroll_delta: i32) -> ServoLiveEnsureRequest {
height: 480, height: 480,
page_zoom_percent: 100, page_zoom_percent: 100,
device_pixel_ratio: 1.0, device_pixel_ratio: 1.0,
color_scheme: ely_domain::ColorScheme::Light,
scroll_delta_x: scroll_delta, scroll_delta_x: scroll_delta,
scroll_delta_y: scroll_delta, scroll_delta_y: scroll_delta,
scroll_point_x: (scroll_delta != 0).then_some(1), scroll_point_x: (scroll_delta != 0).then_some(1),
@@ -450,6 +450,7 @@ fn ensure_request(tab_id: &str, input: RecordedInput) -> ServoLiveEnsureRequest
height: 480, height: 480,
page_zoom_percent: 100, page_zoom_percent: 100,
device_pixel_ratio: 1.0, device_pixel_ratio: 1.0,
color_scheme: ely_domain::ColorScheme::Light,
scroll_delta_x: i32::from(scroll), scroll_delta_x: i32::from(scroll),
scroll_delta_y: i32::from(scroll), scroll_delta_y: i32::from(scroll),
scroll_point_x: scroll.then_some(1), scroll_point_x: scroll.then_some(1),
+26 -1
View File
@@ -19,6 +19,24 @@ pub enum ThemeMode {
Dark, Dark,
} }
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum ColorScheme {
#[default]
Light,
Dark,
}
impl ThemeMode {
pub fn resolve(self, system: ColorScheme) -> ColorScheme {
match self {
Self::System => system,
Self::Light => ColorScheme::Light,
Self::Dark => ColorScheme::Dark,
}
}
}
pub const DEFAULT_TRANSLUCENCY_PCT: u8 = 40; pub const DEFAULT_TRANSLUCENCY_PCT: u8 = 40;
pub const MAX_TRANSLUCENCY_PCT: u8 = 100; pub const MAX_TRANSLUCENCY_PCT: u8 = 100;
@@ -77,7 +95,7 @@ impl AppearanceSettings {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{AppearanceSettings, ThemeMode, WallpaperTheme}; use super::{AppearanceSettings, ColorScheme, ThemeMode, WallpaperTheme};
#[test] #[test]
fn default_settings_use_dawn_and_system_mode() { fn default_settings_use_dawn_and_system_mode() {
@@ -103,6 +121,13 @@ mod tests {
assert_eq!(settings.translucency_pct(), 75); assert_eq!(settings.translucency_pct(), 75);
} }
#[test]
fn theme_mode_resolves_explicit_and_system_color_schemes() {
assert_eq!(ThemeMode::System.resolve(ColorScheme::Dark), ColorScheme::Dark);
assert_eq!(ThemeMode::Light.resolve(ColorScheme::Dark), ColorScheme::Light);
assert_eq!(ThemeMode::Dark.resolve(ColorScheme::Light), ColorScheme::Dark);
}
#[test] #[test]
fn translucency_setter_clamps_above_max() { fn translucency_setter_clamps_above_max() {
let mut settings = AppearanceSettings::default(); let mut settings = AppearanceSettings::default();
+2 -1
View File
@@ -24,7 +24,8 @@ mod tab_group;
mod url_text; mod url_text;
pub use appearance::{ pub use appearance::{
AppearanceSettings, DEFAULT_TRANSLUCENCY_PCT, MAX_TRANSLUCENCY_PCT, ThemeMode, WallpaperTheme, AppearanceSettings, ColorScheme, DEFAULT_TRANSLUCENCY_PCT, MAX_TRANSLUCENCY_PCT, ThemeMode,
WallpaperTheme,
}; };
pub use archive::{ArchiveSource, ArchivedTab}; pub use archive::{ArchiveSource, ArchivedTab};
pub use bookmark::BookmarkEntry; pub use bookmark::BookmarkEntry;
@@ -21,8 +21,8 @@ use super::{
}, },
live_request::{MAX_REQUEST_LINE_BYTES, RequestLineRead, read_request_line}, live_request::{MAX_REQUEST_LINE_BYTES, RequestLineRead, read_request_line},
live_session::{ live_session::{
LiveInput, LiveSession, apply_input, apply_layout, apply_permissions, bind_profile, LiveInput, LiveSession, apply_color_scheme, apply_input, apply_layout, apply_permissions,
ensure_session, bind_profile, ensure_session,
}, },
}; };
@@ -159,6 +159,7 @@ fn handle_request(
height, height,
page_zoom_percent, page_zoom_percent,
device_pixel_ratio, device_pixel_ratio,
color_scheme,
scroll_delta_x, scroll_delta_x,
scroll_delta_y, scroll_delta_y,
scroll_point_x, scroll_point_x,
@@ -184,6 +185,7 @@ fn handle_request(
let session = let session =
ensure_session(host, sessions, tab_id.clone(), &tab, &profile, width, height)?; ensure_session(host, sessions, tab_id.clone(), &tab, &profile, width, height)?;
apply_layout(host, session, width, height, page_zoom_percent, device_pixel_ratio)?; apply_layout(host, session, width, height, page_zoom_percent, device_pixel_ratio)?;
apply_color_scheme(host, session, color_scheme)?;
apply_permissions( apply_permissions(
host, host,
session, session,
@@ -1,5 +1,6 @@
use std::{io, path::PathBuf}; use std::{io, path::PathBuf};
use ely_domain::ColorScheme;
use ely_servo_host::{ use ely_servo_host::{
ConsumedPermission, IOSurfaceHandle, RenderedFrame, ServoHostError, WebViewSnapshot, ConsumedPermission, IOSurfaceHandle, RenderedFrame, ServoHostError, WebViewSnapshot,
WebViewState, WebViewState,
@@ -10,7 +11,7 @@ use thiserror::Error;
#[cfg(all(feature = "hardware-render", target_os = "macos"))] #[cfg(all(feature = "hardware-render", target_os = "macos"))]
use super::iosurface_mach::IOSurfaceMachError; use super::iosurface_mach::IOSurfaceMachError;
pub(super) const LIVE_PROTOCOL_VERSION: u32 = 3; pub(super) const LIVE_PROTOCOL_VERSION: u32 = 4;
pub(super) const MAX_FRAME_DIMENSION: u32 = 16_384; pub(super) const MAX_FRAME_DIMENSION: u32 = 16_384;
pub(super) const MAX_FRAME_BYTE_COUNT: usize = 256 * 1024 * 1024; pub(super) const MAX_FRAME_BYTE_COUNT: usize = 256 * 1024 * 1024;
pub(super) const MAX_RESPONSE_HEADER_BYTES: usize = 256 * 1024; pub(super) const MAX_RESPONSE_HEADER_BYTES: usize = 256 * 1024;
@@ -35,6 +36,7 @@ pub(super) enum LiveRequest {
page_zoom_percent: u16, page_zoom_percent: u16,
#[serde(default = "default_device_pixel_ratio")] #[serde(default = "default_device_pixel_ratio")]
device_pixel_ratio: f32, device_pixel_ratio: f32,
color_scheme: ColorScheme,
#[serde(default)] #[serde(default)]
scroll_delta_x: i32, scroll_delta_x: i32,
#[serde(default)] #[serde(default)]
@@ -401,7 +403,7 @@ mod tests {
#[test] #[test]
fn ensure_defaults_optional_input_fields() -> Result<(), serde_json::Error> { fn ensure_defaults_optional_input_fields() -> Result<(), serde_json::Error> {
let request = serde_json::from_str::<LiveRequest>( let request = serde_json::from_str::<LiveRequest>(
r#"{"type":"ensure","tab_id":"tab","profile_id":"profile","url":"https://example.com","width":800,"height":600,"site_permission_generation":0}"#, r#"{"type":"ensure","tab_id":"tab","profile_id":"profile","url":"https://example.com","width":800,"height":600,"color_scheme":"dark","site_permission_generation":0}"#,
)?; )?;
assert!(matches!( assert!(matches!(
@@ -409,6 +411,7 @@ mod tests {
LiveRequest::Ensure { LiveRequest::Ensure {
page_zoom_percent: 100, page_zoom_percent: 100,
device_pixel_ratio: 1.0, device_pixel_ratio: 1.0,
color_scheme: ColorScheme::Dark,
scroll_delta_x: 0, scroll_delta_x: 0,
scroll_delta_y: 0, scroll_delta_y: 0,
ready_surface_ids, ready_surface_ids,
@@ -419,19 +422,28 @@ mod tests {
Ok(()) Ok(())
} }
#[test]
fn ensure_requires_color_scheme() {
let request = serde_json::from_str::<LiveRequest>(
r#"{"type":"ensure","tab_id":"tab","profile_id":"profile","url":"https://example.com","width":800,"height":600,"site_permission_generation":0}"#,
);
assert!(request.is_err());
}
#[test] #[test]
fn handshake_deserializes_protocol_version() -> Result<(), serde_json::Error> { fn handshake_deserializes_protocol_version() -> Result<(), serde_json::Error> {
let request = let request =
serde_json::from_str::<LiveRequest>(r#"{"type":"handshake","protocol_version":3}"#)?; serde_json::from_str::<LiveRequest>(r#"{"type":"handshake","protocol_version":4}"#)?;
assert!(matches!(request, LiveRequest::Handshake { protocol_version: 3 })); assert!(matches!(request, LiveRequest::Handshake { protocol_version: 4 }));
Ok(()) Ok(())
} }
#[test] #[test]
fn site_permission_requires_revision() { fn site_permission_requires_revision() {
let request = serde_json::from_str::<LiveRequest>( let request = serde_json::from_str::<LiveRequest>(
r#"{"type":"ensure","tab_id":"tab","profile_id":"profile","url":"https://example.com","width":800,"height":600,"site_permission_generation":0,"site_permissions":[{"origin":"https://example.com","feature":"camera","state":"allow-once"}]}"#, r#"{"type":"ensure","tab_id":"tab","profile_id":"profile","url":"https://example.com","width":800,"height":600,"color_scheme":"light","site_permission_generation":0,"site_permissions":[{"origin":"https://example.com","feature":"camera","state":"allow-once"}]}"#,
); );
assert!(request.is_err()); assert!(request.is_err());
@@ -1,11 +1,11 @@
use std::collections::{HashMap, hash_map::Entry}; use std::collections::{HashMap, hash_map::Entry};
use ely_domain::{ProfileId, TabId, validate_zoom_percent}; use ely_domain::{ColorScheme, ProfileId, TabId, validate_zoom_percent};
use ely_servo_host::{ use ely_servo_host::{
HidpiScaleRequest, KeyboardTextRequest, MouseClickRequest, MouseHoverRequest, PageZoomRequest, ColorSchemeRequest, HidpiScaleRequest, KeyboardTextRequest, MouseClickRequest,
PermissionDecision, PermissionSnapshotEntry, PermissionSnapshotRequest, MouseHoverRequest, PageZoomRequest, PermissionDecision, PermissionSnapshotEntry,
PermissionSnapshotState, RenderedFrame, ResizeRequest, ScrollRequest, ServoHost, PermissionSnapshotRequest, PermissionSnapshotState, RenderedFrame, ResizeRequest,
ServoSurfaceSize, SoftwareServoHost, ScrollRequest, ServoHost, ServoSurfaceSize, SoftwareServoHost,
}; };
use super::live_protocol::{LiveSidecarError, LiveSitePermission}; use super::live_protocol::{LiveSidecarError, LiveSitePermission};
@@ -170,6 +170,18 @@ pub(super) fn apply_permissions(
.map_err(LiveSidecarError::from) .map_err(LiveSidecarError::from)
} }
pub(super) fn apply_color_scheme(
host: &mut SoftwareServoHost,
session: &LiveSession,
color_scheme: ColorScheme,
) -> Result<(), LiveSidecarError> {
host.set_color_scheme(ColorSchemeRequest {
webview_id: session.webview_id.clone(),
color_scheme,
})
.map_err(LiveSidecarError::from)
}
pub(super) struct LiveInput { pub(super) struct LiveInput {
pub(super) scroll_delta_x: i32, pub(super) scroll_delta_x: i32,
pub(super) scroll_delta_y: i32, pub(super) scroll_delta_y: i32,
@@ -67,7 +67,7 @@ fn request_line_accepts_a_bounded_eof_terminated_frame() -> Result<(), Box<dyn s
} }
fn padded_handshake_line(bytes: usize) -> Vec<u8> { fn padded_handshake_line(bytes: usize) -> Vec<u8> {
let mut line = br#"{"type":"handshake","protocol_version":3}"#.to_vec(); let mut line = br#"{"type":"handshake","protocol_version":4}"#.to_vec();
assert!(bytes > line.len()); assert!(bytes > line.len());
line.resize(bytes - 1, b' '); line.resize(bytes - 1, b' ');
line.push(b'\n'); line.push(b'\n');
+10 -1
View File
@@ -1,5 +1,6 @@
use ely_domain::{ use ely_domain::{
ProfileId, SiteOrigin, SitePermissionDecision, SitePermissionFeature, TabId, UrlText, WebViewId, ColorScheme, ProfileId, SiteOrigin, SitePermissionDecision, SitePermissionFeature, TabId,
UrlText, WebViewId,
}; };
use crate::ServoHostError; use crate::ServoHostError;
@@ -280,6 +281,12 @@ pub struct HidpiScaleRequest {
pub scale_factor: f32, pub scale_factor: f32,
} }
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ColorSchemeRequest {
pub webview_id: WebViewId,
pub color_scheme: ColorScheme,
}
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]
pub struct MouseClickRequest { pub struct MouseClickRequest {
pub webview_id: WebViewId, pub webview_id: WebViewId,
@@ -385,6 +392,8 @@ pub trait ServoHost {
fn set_hidpi_scale(&mut self, request: HidpiScaleRequest) -> Result<(), ServoHostError>; fn set_hidpi_scale(&mut self, request: HidpiScaleRequest) -> Result<(), ServoHostError>;
fn set_color_scheme(&mut self, request: ColorSchemeRequest) -> Result<(), ServoHostError>;
fn click(&mut self, request: MouseClickRequest) -> Result<(), ServoHostError>; fn click(&mut self, request: MouseClickRequest) -> Result<(), ServoHostError>;
fn hover(&mut self, request: MouseHoverRequest) -> Result<(), ServoHostError>; fn hover(&mut self, request: MouseHoverRequest) -> Result<(), ServoHostError>;
+6 -5
View File
@@ -22,11 +22,12 @@ pub use error::ServoHostError;
#[cfg(feature = "hardware-render")] #[cfg(feature = "hardware-render")]
pub use hardware_rendering_context::HardwareOffscreenContext; pub use hardware_rendering_context::HardwareOffscreenContext;
pub use host::{ pub use host::{
ConsumedPermission, HidpiScaleRequest, KeyboardTextRequest, MAX_PAGE_TITLE_BYTES, ColorSchemeRequest, ConsumedPermission, HidpiScaleRequest, KeyboardTextRequest,
MouseClickRequest, MouseDragRequest, MouseHoverRequest, NavigationRequest, PageZoomRequest, MAX_PAGE_TITLE_BYTES, MouseClickRequest, MouseDragRequest, MouseHoverRequest,
PermissionDecision, PermissionSnapshotEntry, PermissionSnapshotRequest, NavigationRequest, PageZoomRequest, PermissionDecision, PermissionSnapshotEntry,
PermissionSnapshotState, RenderedFrame, RenderedFrameSummary, ResizeRequest, ScrollRequest, PermissionSnapshotRequest, PermissionSnapshotState, RenderedFrame, RenderedFrameSummary,
ServoHost, TouchTapRequest, WebViewSnapshot, WebViewSnapshotPending, WebViewState, ResizeRequest, ScrollRequest, ServoHost, TouchTapRequest, WebViewSnapshot,
WebViewSnapshotPending, WebViewState,
}; };
pub use iosurface_handle::{IOSurfaceHandle, IOSurfaceIdentity}; pub use iosurface_handle::{IOSurfaceHandle, IOSurfaceIdentity};
#[cfg(feature = "servo-engine")] #[cfg(feature = "servo-engine")]
+28 -6
View File
@@ -9,11 +9,11 @@ use std::{
}; };
use dpi::PhysicalSize; use dpi::PhysicalSize;
use ely_domain::{ProfileId, TabId, WebViewId}; use ely_domain::{ColorScheme, ProfileId, TabId, WebViewId};
use raw_window_handle::{HasDisplayHandle, HasWindowHandle}; use raw_window_handle::{HasDisplayHandle, HasWindowHandle};
use servo::{ use servo::{
DevicePoint, DeviceVector2D, Opts, Scroll, Servo, ServoBuilder, WebViewBuilder, WebViewPoint, DevicePoint, DeviceVector2D, Opts, Scroll, Servo, ServoBuilder, Theme, WebViewBuilder,
WebViewVector, WebViewPoint, WebViewVector,
}; };
#[path = "runtime_context.rs"] #[path = "runtime_context.rs"]
@@ -32,8 +32,8 @@ use runtime_preferences::ely_servo_preferences;
use url::Url; use url::Url;
use crate::{ use crate::{
ConsumedPermission, HidpiScaleRequest, KeyboardTextRequest, MouseClickRequest, ColorSchemeRequest, ConsumedPermission, HidpiScaleRequest, KeyboardTextRequest,
MouseDragRequest, MouseHoverRequest, NavigationRequest, PageZoomRequest, MouseClickRequest, MouseDragRequest, MouseHoverRequest, NavigationRequest, PageZoomRequest,
PermissionSnapshotRequest, RenderedFrame, ResizeRequest, ScrollRequest, ServoHost, PermissionSnapshotRequest, RenderedFrame, ResizeRequest, ScrollRequest, ServoHost,
ServoHostError, TouchTapRequest, WebViewSnapshot, WebViewState, ServoHostError, TouchTapRequest, WebViewSnapshot, WebViewState,
runtime_input::{ runtime_input::{
@@ -204,13 +204,15 @@ impl ServoHost for SoftwareServoHost {
webview.delegate.set_state(WebViewState::Loading); webview.delegate.set_state(WebViewState::Loading);
if should_create_initial_document { if should_create_initial_document {
let hidpi_scale_factor = webview.webview.hidpi_scale_factor(); let hidpi_scale_factor = webview.webview.hidpi_scale_factor();
webview.webview = WebViewBuilder::new(&servo, webview.rendering_context.clone()) let replacement = WebViewBuilder::new(&servo, webview.rendering_context.clone())
.delegate(webview.delegate.clone()) .delegate(webview.delegate.clone())
.url(url) .url(url)
// The live path pushes DPR before first navigation. Preserve that scale when // The live path pushes DPR before first navigation. Preserve that scale when
// replacing the about:blank WebView so CSS viewport = physical surface / DPR. // replacing the about:blank WebView so CSS viewport = physical surface / DPR.
.hidpi_scale_factor(hidpi_scale_factor) .hidpi_scale_factor(hidpi_scale_factor)
.build(); .build();
replacement.notify_theme_change(servo_theme(webview.color_scheme));
webview.webview = replacement;
// The input-accepting invariant lives in `webview_for_input`. // The input-accepting invariant lives in `webview_for_input`.
webview.webview.show(); webview.webview.show();
webview.webview.focus(); webview.webview.focus();
@@ -303,6 +305,18 @@ impl ServoHost for SoftwareServoHost {
Ok(()) Ok(())
} }
fn set_color_scheme(&mut self, request: ColorSchemeRequest) -> Result<(), ServoHostError> {
let webview = self
.webviews
.get_mut(&request.webview_id)
.ok_or_else(|| ServoHostError::WebViewNotFound { id: request.webview_id.clone() })?;
if webview.color_scheme != request.color_scheme {
webview.color_scheme = request.color_scheme;
webview.webview.notify_theme_change(servo_theme(request.color_scheme));
}
Ok(())
}
fn hover(&mut self, request: MouseHoverRequest) -> Result<(), ServoHostError> { fn hover(&mut self, request: MouseHoverRequest) -> Result<(), ServoHostError> {
let webview = self.webview_for_input(&request.webview_id)?; let webview = self.webview_for_input(&request.webview_id)?;
send_mouse_hover(&webview.webview, request.x, request.y); send_mouse_hover(&webview.webview, request.x, request.y);
@@ -440,6 +454,7 @@ impl SoftwareServoHost {
webview, webview,
delegate, delegate,
requested_url: None, requested_url: None,
color_scheme: ColorScheme::Light,
}, },
); );
@@ -472,3 +487,10 @@ impl SoftwareServoHost {
Ok(webview) Ok(webview)
} }
} }
fn servo_theme(color_scheme: ColorScheme) -> Theme {
match color_scheme {
ColorScheme::Light => Theme::Light,
ColorScheme::Dark => Theme::Dark,
}
}
+2 -1
View File
@@ -1,6 +1,6 @@
use std::{cell::Cell, cell::RefCell, rc::Rc}; use std::{cell::Cell, cell::RefCell, rc::Rc};
use ely_domain::{ProfileId, TabId, WebViewId}; use ely_domain::{ColorScheme, ProfileId, TabId, WebViewId};
use servo::{LoadStatus, RenderingContext, WebView, WebViewDelegate}; use servo::{LoadStatus, RenderingContext, WebView, WebViewDelegate};
use url::Url; use url::Url;
@@ -19,6 +19,7 @@ pub(super) struct HostWebView {
pub(super) webview: WebView, pub(super) webview: WebView,
pub(super) delegate: Rc<HostWebViewDelegate>, pub(super) delegate: Rc<HostWebViewDelegate>,
pub(super) requested_url: Option<String>, pub(super) requested_url: Option<String>,
pub(super) color_scheme: ColorScheme,
} }
impl HostWebView { impl HostWebView {
+1 -1
View File
@@ -208,7 +208,7 @@ fn live_sidecar_drains_an_oversized_request_line_and_recovers_the_handshake()
let mut stdin = child.stdin.take().ok_or_else(|| io::Error::other("missing stdin"))?; let mut stdin = child.stdin.take().ok_or_else(|| io::Error::other("missing stdin"))?;
let stdout = child.stdout.take().ok_or_else(|| io::Error::other("missing stdout"))?; let stdout = child.stdout.take().ok_or_else(|| io::Error::other("missing stdout"))?;
let mut stdout = BufReader::new(stdout); let mut stdout = BufReader::new(stdout);
let mut oversized = br#"{"type":"handshake","protocol_version":3}"#.to_vec(); let mut oversized = br#"{"type":"handshake","protocol_version":4}"#.to_vec();
oversized.resize(MAX_REQUEST_LINE_BYTES + 4_096, b' '); oversized.resize(MAX_REQUEST_LINE_BYTES + 4_096, b' ');
oversized.push(b'\n'); oversized.push(b'\n');
@@ -24,7 +24,7 @@ use super::pages::{
pub(super) const WIDTH: u32 = 360; pub(super) const WIDTH: u32 = 360;
pub(super) const HEIGHT: u32 = 240; pub(super) const HEIGHT: u32 = 240;
pub(super) const RESPONSE_TIMEOUT: Duration = Duration::from_secs(20); pub(super) const RESPONSE_TIMEOUT: Duration = Duration::from_secs(20);
pub(super) const LIVE_PROTOCOL_VERSION: u32 = 3; pub(super) const LIVE_PROTOCOL_VERSION: u32 = 4;
pub(super) const MAX_FRAME_DIMENSION: u32 = 16_384; pub(super) const MAX_FRAME_DIMENSION: u32 = 16_384;
const MAX_FRAME_BYTE_COUNT: usize = 256 * 1024 * 1024; const MAX_FRAME_BYTE_COUNT: usize = 256 * 1024 * 1024;
@@ -38,6 +38,7 @@ pub(super) fn ensure_request(tab_id: &TabId, profile_id: &ProfileId, url: &str)
"height": HEIGHT, "height": HEIGHT,
"page_zoom_percent": 100, "page_zoom_percent": 100,
"device_pixel_ratio": 1.0, "device_pixel_ratio": 1.0,
"color_scheme": "light",
"site_permission_generation": 0, "site_permission_generation": 0,
"site_permissions": [], "site_permissions": [],
}) })
+30 -5
View File
@@ -6,12 +6,13 @@ use std::{
process::{Command, Stdio}, process::{Command, Stdio},
}; };
use ely_domain::{ProfileId, SiteOrigin, SitePermissionFeature, TabId, UrlText}; use ely_domain::{ColorScheme, ProfileId, SiteOrigin, SitePermissionFeature, TabId, UrlText};
use ely_servo_host::{ use ely_servo_host::{
HidpiScaleRequest, KeyboardTextRequest, MouseClickRequest, MouseDragRequest, NavigationRequest, ColorSchemeRequest, HidpiScaleRequest, KeyboardTextRequest, MouseClickRequest,
PageZoomRequest, PermissionDecision, PermissionSnapshotEntry, PermissionSnapshotRequest, MouseDragRequest, NavigationRequest, PageZoomRequest, PermissionDecision,
PermissionSnapshotState, ResizeRequest, ScrollRequest, ServoHost, ServoHostError, PermissionSnapshotEntry, PermissionSnapshotRequest, PermissionSnapshotState, ResizeRequest,
ServoSurfaceSize, SoftwareServoHost, TouchTapRequest, WebViewState, ScrollRequest, ServoHost, ServoHostError, ServoSurfaceSize, SoftwareServoHost, TouchTapRequest,
WebViewState,
}; };
const MINIMUM_CONTENT_PIXELS: u64 = 1_000; const MINIMUM_CONTENT_PIXELS: u64 = 1_000;
@@ -29,6 +30,7 @@ const CLICK_PROBE_URL: &str = "data:text/html,%3C!doctype%20html%3E%3Ctitle%3ECl
const DRAG_PROBE_URL: &str = "data:text/html,%3C%21doctype%20html%3E%3Ctitle%3EDrag%20Probe%3C%2Ftitle%3E%3Cstyle%3Ebody%7Bmargin%3A0%3Bbackground%3A%23f6d365%3B%7Dbutton%7Bposition%3Aabsolute%3Bleft%3A80px%3Btop%3A80px%3Bwidth%3A220px%3Bheight%3A90px%3Bfont%3A28px%20sans-serif%3Bbackground%3A%23ffffff%3Bcolor%3A%23111111%3B%7D%3C%2Fstyle%3E%3Cbutton%20id%3Dbox%3EDrag%3C%2Fbutton%3E%3Cscript%3Elet%20dragging%3Dfalse%3Bconst%20box%3Ddocument.getElementById%28%27box%27%29%3BaddEventListener%28%27mousedown%27%2Cevent%3D%3E%7Bif%28event.target%3D%3D%3Dbox%29%7Bdragging%3Dtrue%3B%7D%7D%29%3BaddEventListener%28%27mousemove%27%2Cevent%3D%3E%7Bif%28dragging%26%26event.clientX%3E280%29%7Bdocument.body.style.background%3D%27%230039ff%27%3Bdocument.title%3D%27Dragged%27%3Bbox.textContent%3D%27Dragged%27%3B%7D%7D%29%3BaddEventListener%28%27mouseup%27%2C%28%29%3D%3E%7Bdragging%3Dfalse%3B%7D%29%3B%3C%2Fscript%3E"; const DRAG_PROBE_URL: &str = "data:text/html,%3C%21doctype%20html%3E%3Ctitle%3EDrag%20Probe%3C%2Ftitle%3E%3Cstyle%3Ebody%7Bmargin%3A0%3Bbackground%3A%23f6d365%3B%7Dbutton%7Bposition%3Aabsolute%3Bleft%3A80px%3Btop%3A80px%3Bwidth%3A220px%3Bheight%3A90px%3Bfont%3A28px%20sans-serif%3Bbackground%3A%23ffffff%3Bcolor%3A%23111111%3B%7D%3C%2Fstyle%3E%3Cbutton%20id%3Dbox%3EDrag%3C%2Fbutton%3E%3Cscript%3Elet%20dragging%3Dfalse%3Bconst%20box%3Ddocument.getElementById%28%27box%27%29%3BaddEventListener%28%27mousedown%27%2Cevent%3D%3E%7Bif%28event.target%3D%3D%3Dbox%29%7Bdragging%3Dtrue%3B%7D%7D%29%3BaddEventListener%28%27mousemove%27%2Cevent%3D%3E%7Bif%28dragging%26%26event.clientX%3E280%29%7Bdocument.body.style.background%3D%27%230039ff%27%3Bdocument.title%3D%27Dragged%27%3Bbox.textContent%3D%27Dragged%27%3B%7D%7D%29%3BaddEventListener%28%27mouseup%27%2C%28%29%3D%3E%7Bdragging%3Dfalse%3B%7D%29%3B%3C%2Fscript%3E";
const TOUCH_PROBE_URL: &str = "data:text/html,%3C%21doctype%20html%3E%3Ctitle%3ETouch%20Probe%3C%2Ftitle%3E%3Cstyle%3Ebody%7Bmargin%3A0%3Bbackground%3A%23c7f5d9%3B%7Dbutton%7Bposition%3Aabsolute%3Bleft%3A80px%3Btop%3A80px%3Bwidth%3A220px%3Bheight%3A90px%3Bfont%3A28px%20sans-serif%3Bbackground%3A%23ffffff%3Bcolor%3A%23111111%3Btouch-action%3Amanipulation%3B%7D%3C%2Fstyle%3E%3Cbutton%20ontouchstart%3D%22document.body.dataset.touch%3D%27start%27%3B%22%20onpointerdown%3D%22if%28%21document.body.dataset.pointerType%29%7Bdocument.body.dataset.pointerType%3Devent.pointerType%3B%7D%22%20onclick%3D%22if%28document.body.dataset.pointerType%21%3D%3D%27touch%27%29%7Bdocument.title%3Ddocument.body.dataset.pointerType%3Breturn%3B%7Ddocument.body.style.background%3D%27%230039ff%27%3Bdocument.title%3D%27Touched%27%3Bthis.textContent%3D%27Touched%27%3B%22%3ETap%3C%2Fbutton%3E"; const TOUCH_PROBE_URL: &str = "data:text/html,%3C%21doctype%20html%3E%3Ctitle%3ETouch%20Probe%3C%2Ftitle%3E%3Cstyle%3Ebody%7Bmargin%3A0%3Bbackground%3A%23c7f5d9%3B%7Dbutton%7Bposition%3Aabsolute%3Bleft%3A80px%3Btop%3A80px%3Bwidth%3A220px%3Bheight%3A90px%3Bfont%3A28px%20sans-serif%3Bbackground%3A%23ffffff%3Bcolor%3A%23111111%3Btouch-action%3Amanipulation%3B%7D%3C%2Fstyle%3E%3Cbutton%20ontouchstart%3D%22document.body.dataset.touch%3D%27start%27%3B%22%20onpointerdown%3D%22if%28%21document.body.dataset.pointerType%29%7Bdocument.body.dataset.pointerType%3Devent.pointerType%3B%7D%22%20onclick%3D%22if%28document.body.dataset.pointerType%21%3D%3D%27touch%27%29%7Bdocument.title%3Ddocument.body.dataset.pointerType%3Breturn%3B%7Ddocument.body.style.background%3D%27%230039ff%27%3Bdocument.title%3D%27Touched%27%3Bthis.textContent%3D%27Touched%27%3B%22%3ETap%3C%2Fbutton%3E";
const TEXT_PROBE_URL: &str = "data:text/html,%3C!doctype%20html%3E%3Ctitle%3EText%20Probe%3C%2Ftitle%3E%3Cstyle%3Ebody%7Bmargin%3A0%3Bbackground%3A%23d9e8ff%3Bfont%3A28px%20sans-serif%3B%7Dinput%7Bposition%3Aabsolute%3Bleft%3A80px%3Btop%3A80px%3Bwidth%3A260px%3Bheight%3A70px%3Bfont%3A28px%20sans-serif%3B%7Doutput%7Bposition%3Aabsolute%3Bleft%3A80px%3Btop%3A180px%3Bfont%3A32px%20sans-serif%3B%7D%3C%2Fstyle%3E%3Cinput%20id%3Dq%20autofocus%20oninput%3D%22document.body.style.background%3D%27%230039ff%27%3Bdocument.getElementById%28%27out%27%29.textContent%3Dthis.value%3B%22%3E%3Coutput%20id%3Dout%3Eempty%3C%2Foutput%3E"; const TEXT_PROBE_URL: &str = "data:text/html,%3C!doctype%20html%3E%3Ctitle%3EText%20Probe%3C%2Ftitle%3E%3Cstyle%3Ebody%7Bmargin%3A0%3Bbackground%3A%23d9e8ff%3Bfont%3A28px%20sans-serif%3B%7Dinput%7Bposition%3Aabsolute%3Bleft%3A80px%3Btop%3A80px%3Bwidth%3A260px%3Bheight%3A70px%3Bfont%3A28px%20sans-serif%3B%7Doutput%7Bposition%3Aabsolute%3Bleft%3A80px%3Btop%3A180px%3Bfont%3A32px%20sans-serif%3B%7D%3C%2Fstyle%3E%3Cinput%20id%3Dq%20autofocus%20oninput%3D%22document.body.style.background%3D%27%230039ff%27%3Bdocument.getElementById%28%27out%27%29.textContent%3Dthis.value%3B%22%3E%3Coutput%20id%3Dout%3Eempty%3C%2Foutput%3E";
const THEME_PROBE_URL: &str = "data:text/html,%3C!doctype%20html%3E%3Ctitle%3ETheme%20Probe%3C%2Ftitle%3E%3Cstyle%3Ebody%7Bmargin%3A0%3Bbackground%3A%23f1e2d3%3B%7D%40media%28prefers-color-scheme%3Adark%29%7Bbody%7Bbackground%3A%23112233%3B%7D%7D%3C%2Fstyle%3E";
const TEXT_PROBE_VALUE: &str = "ely42"; const TEXT_PROBE_VALUE: &str = "ely42";
struct PrdSiteCompatibilityCase { struct PrdSiteCompatibilityCase {
@@ -235,6 +237,29 @@ fn exercise_real_servo_webview_lifecycle() -> Result<(), Box<dyn Error>> {
"mismatch: {mismatch:?}" "mismatch: {mismatch:?}"
); );
host.set_color_scheme(ColorSchemeRequest {
webview_id: webview_id.clone(),
color_scheme: ColorScheme::Dark,
})?;
host.navigate(NavigationRequest {
webview_id: webview_id.clone(),
tab_id: tab_id.clone(),
url: UrlText::parse(THEME_PROBE_URL)?,
})?;
wait_for_rendered_webview_with_center_pixel(&mut host, &webview_id, None, [17, 34, 51])?;
let previous_frame_hash = host.last_rendered_frame()?.sample_hash();
host.set_color_scheme(ColorSchemeRequest {
webview_id: webview_id.clone(),
color_scheme: ColorScheme::Light,
})?;
wait_for_rendered_webview_with_center_pixel(
&mut host,
&webview_id,
Some(previous_frame_hash),
[241, 226, 211],
)?;
let url = UrlText::parse(CLICK_PROBE_URL)?; let url = UrlText::parse(CLICK_PROBE_URL)?;
host.navigate(NavigationRequest { webview_id: webview_id.clone(), tab_id, url })?; host.navigate(NavigationRequest { webview_id: webview_id.clone(), tab_id, url })?;
+3 -2
View File
@@ -50,8 +50,9 @@ transfers the IOSurface send right; the JSON port number is diagnostic metadata.
The protocol supports: The protocol supports:
- `handshake`: verify protocol version `3` before accepting browser commands. - `handshake`: verify protocol version `4` before accepting browser commands.
- `ensure`: create or update a WebView, navigation, viewport, zoom, permissions, and input. - `ensure`: create or update a WebView, navigation, viewport, zoom, resolved color scheme,
permissions, and input.
- `poll`: advance Servo and return pending frame or metadata state. - `poll`: advance Servo and return pending frame or metadata state.
- `close`: destroy one tab's WebView. - `close`: destroy one tab's WebView.
- `shutdown`: acknowledge graceful process shutdown so Servo flushes profile storage. - `shutdown`: acknowledge graceful process shutdown so Servo flushes profile storage.