diff --git a/agents.md b/agents.md index da735ad..30b0db0 100644 --- a/agents.md +++ b/agents.md @@ -13,7 +13,7 @@ in the same commit as any change that makes it stale. local persistence. No IO except through callers. - `crates/ely_servo_host` — Servo embedding, rendering contexts, the `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`. - `crates/ely_sync_client` — worker API client, device identity/trust, 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; clicks, text, and allow-once permission transfers retain strict ordering. 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`, Windows `cmd start`/`explorer /select`, Linux `xdg-open`); the command overlay closes on Escape. diff --git a/crates/ely_app/src/services/servo_live.rs b/crates/ely_app/src/services/servo_live.rs index a2901f4..2712a89 100644 --- a/crates/ely_app/src/services/servo_live.rs +++ b/crates/ely_app/src/services/servo_live.rs @@ -151,6 +151,7 @@ impl ServoLiveClient { height: request.height, page_zoom_percent: request.page_zoom_percent, device_pixel_ratio: request.device_pixel_ratio, + color_scheme: request.color_scheme, scroll_delta_x: request.scroll_delta_x, scroll_delta_y: request.scroll_delta_y, scroll_point_x: request.scroll_point_x, diff --git a/crates/ely_app/src/services/servo_live_ipc.rs b/crates/ely_app/src/services/servo_live_ipc.rs index 682d310..eeda762 100644 --- a/crates/ely_app/src/services/servo_live_ipc.rs +++ b/crates/ely_app/src/services/servo_live_ipc.rs @@ -256,7 +256,7 @@ mod tests { #[test] fn reply_rejects_oversized_frame_before_readback_allocation() { 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 ); let mut input = Cursor::new(header.into_bytes()); @@ -269,19 +269,19 @@ mod tests { #[test] 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.push(b'\n'); let reply = read_reply(&mut Cursor::new(header))?; - assert_eq!(reply.protocol_version, Some(3)); + assert_eq!(reply.protocol_version, Some(4)); Ok(()) } #[test] 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.push(b'\n'); @@ -327,7 +327,7 @@ mod tests { fn reply_parses_permission_consumption_without_a_frame() -> Result<(), ServoLiveError> { let profile_id = ely_domain::ProfileId::new(); 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(), ); let mut input = Cursor::new(header.into_bytes()); @@ -350,7 +350,7 @@ mod tests { #[test] fn hardware_reply_uses_surface_without_rgba_allocation() -> Result<(), ServoLiveError> { 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},", "\"current_surface_id\":7,", "\"frame\":{\"loaded_url\":null,\"title\":null,\"state\":\"complete\",", @@ -402,7 +402,7 @@ mod tests { #[cfg(target_os = "macos")] fn hardware_header(current_surface_id: u64, handle_width: u32, handle_height: u32) -> String { 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" ) } } diff --git a/crates/ely_app/src/services/servo_live_types.rs b/crates/ely_app/src/services/servo_live_types.rs index b5d338d..50bc902 100644 --- a/crates/ely_app/src/services/servo_live_types.rs +++ b/crates/ely_app/src/services/servo_live_types.rs @@ -2,6 +2,7 @@ use std::sync::Arc; use std::{collections::TryReserveError, io, path::PathBuf}; +use ely_domain::ColorScheme; use serde::Serialize; use thiserror::Error; @@ -24,6 +25,7 @@ pub(crate) struct ServoLiveEnsureRequest { pub(crate) page_zoom_percent: u16, /// Display scale factor used to derive Servo's CSS viewport. pub(crate) device_pixel_ratio: f32, + pub(crate) color_scheme: ColorScheme, pub(crate) scroll_delta_x: i32, pub(crate) scroll_delta_y: i32, pub(crate) scroll_point_x: Option, diff --git a/crates/ely_app/src/services/servo_live_wire.rs b/crates/ely_app/src/services/servo_live_wire.rs index 2bbf3f4..22820e1 100644 --- a/crates/ely_app/src/services/servo_live_wire.rs +++ b/crates/ely_app/src/services/servo_live_wire.rs @@ -1,8 +1,9 @@ +use ely_domain::ColorScheme; use serde::{Deserialize, Serialize}; 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_BYTE_COUNT: usize = 256 * 1024 * 1024; pub(super) const MAX_RESPONSE_HEADER_BYTES: usize = 256 * 1024; @@ -22,6 +23,7 @@ pub(super) enum LiveRequest { height: u32, page_zoom_percent: u16, device_pixel_ratio: f32, + color_scheme: ColorScheme, scroll_delta_x: i32, scroll_delta_y: i32, scroll_point_x: Option, diff --git a/crates/ely_app/src/shell/mod.rs b/crates/ely_app/src/shell/mod.rs index 8644a04..c4df1d4 100644 --- a/crates/ely_app/src/shell/mod.rs +++ b/crates/ely_app/src/shell/mod.rs @@ -136,6 +136,7 @@ pub struct ElyShell { pub(crate) local_state_save_scheduled: bool, _command_subscription: Subscription, _translucency_subscription: Subscription, + _appearance_subscription: Subscription, _quit_save_subscription: Option, } @@ -251,6 +252,8 @@ impl ElyShell { } ShellState::StartupError(_) => None, }; + let appearance_subscription = + cx.observe_window_appearance(window, |_shell, _window, cx| cx.notify()); let mut shell = Self { state, focus_handle: cx.focus_handle(), @@ -307,6 +310,7 @@ impl ElyShell { auth_flow_phase: auth::AuthFlowPhase::Idle, _command_subscription: command_subscription, _translucency_subscription: translucency_subscription, + _appearance_subscription: appearance_subscription, _quit_save_subscription: None, }; shell._quit_save_subscription = Some(local_persistence::register_quit_save(cx)); diff --git a/crates/ely_app/src/shell/render.rs b/crates/ely_app/src/shell/render.rs index 7b534f8..a2349d7 100644 --- a/crates/ely_app/src/shell/render.rs +++ b/crates/ely_app/src/shell/render.rs @@ -27,35 +27,39 @@ impl Render for ElyShell { match &self.state { ShellState::Ready(core) => match core.snapshot() { Ok(snapshot) => { - apply_color_mode( - resolve_color_mode(snapshot.appearance.theme_mode(), appearance), - cx, - ); + let color_scheme = + resolve_color_scheme(snapshot.appearance.theme_mode(), appearance); + self.web_surfaces.set_color_scheme(color_scheme); + apply_color_scheme(color_scheme, cx); match active_tab_from_snapshot(&snapshot) { Some(active_tab) => self.render_browser(&snapshot, active_tab, window, cx), None => render_error("active tab missing from snapshot".to_string()), } } Err(error) => { - apply_color_mode( - resolve_color_mode(ely_domain::ThemeMode::default(), appearance), - cx, - ); + let color_scheme = + resolve_color_scheme(ely_domain::ThemeMode::default(), appearance); + self.web_surfaces.set_color_scheme(color_scheme); + apply_color_scheme(color_scheme, cx); render_error(error.to_string()) } }, ShellState::StartupError(message) => { - apply_color_mode( - resolve_color_mode(ely_domain::ThemeMode::default(), appearance), - cx, - ); + let color_scheme = + resolve_color_scheme(ely_domain::ThemeMode::default(), appearance); + self.web_surfaces.set_color_scheme(color_scheme); + apply_color_scheme(color_scheme, cx); render_error(message.clone()) } } } } -fn apply_color_mode(mode: colors::Mode, cx: &mut Context) { +fn apply_color_scheme(color_scheme: ely_domain::ColorScheme, cx: &mut Context) { + let mode = match color_scheme { + ely_domain::ColorScheme::Light => colors::Mode::Light, + ely_domain::ColorScheme::Dark => colors::Mode::Dark, + }; colors::set_mode(mode); let component_mode = match mode { @@ -68,22 +72,19 @@ fn apply_color_mode(mode: colors::Mode, cx: &mut Context) { gpui_component::Theme::global_mut(cx).font_family = SANS_FAMILY.into(); } -fn resolve_color_mode( +fn resolve_color_scheme( theme_mode: ely_domain::ThemeMode, window_appearance: gpui::WindowAppearance, -) -> colors::Mode { - match theme_mode { - ely_domain::ThemeMode::Light => colors::Mode::Light, - ely_domain::ThemeMode::Dark => colors::Mode::Dark, - ely_domain::ThemeMode::System => match window_appearance { - gpui::WindowAppearance::Dark | gpui::WindowAppearance::VibrantDark => { - colors::Mode::Dark - } - gpui::WindowAppearance::Light | gpui::WindowAppearance::VibrantLight => { - colors::Mode::Light - } - }, - } +) -> ely_domain::ColorScheme { + let system = match window_appearance { + gpui::WindowAppearance::Dark | gpui::WindowAppearance::VibrantDark => { + ely_domain::ColorScheme::Dark + } + gpui::WindowAppearance::Light | gpui::WindowAppearance::VibrantLight => { + ely_domain::ColorScheme::Light + } + }; + theme_mode.resolve(system) } 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())) .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, + ); + } +} diff --git a/crates/ely_app/src/shell/web_surface.rs b/crates/ely_app/src/shell/web_surface.rs index 018baf6..9f0ec8e 100644 --- a/crates/ely_app/src/shell/web_surface.rs +++ b/crates/ely_app/src/shell/web_surface.rs @@ -1,7 +1,7 @@ use std::collections::BTreeMap; 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}; @@ -23,16 +23,27 @@ pub(super) struct WebSurfaceStore { /// Singleton because only one tab at a time holds keyboard focus /// across the whole window. Lives on the store, not per-tab. pub(super) keyboard_focus: Option, + color_scheme: ColorScheme, } impl WebSurfaceStore { 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)] 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)] @@ -72,6 +83,7 @@ impl WebSurfaceStore { tab.profile_id().clone(), profile_data_mode, tab.zoom_percent(), + self.color_scheme, permissions, ); 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)] pub(super) fn surface_for_test(&self, tab_id: &TabId) -> Option<&PerTabSurface> { self.surfaces.get(tab_id) diff --git a/crates/ely_app/src/shell/web_surface_runtime.rs b/crates/ely_app/src/shell/web_surface_runtime.rs index 5132417..93636d0 100644 --- a/crates/ely_app/src/shell/web_surface_runtime.rs +++ b/crates/ely_app/src/shell/web_surface_runtime.rs @@ -4,7 +4,7 @@ use std::{ time::{Duration, Instant}, }; -use ely_domain::{BrowserTab, TabId}; +use ely_domain::{BrowserTab, ColorScheme, TabId}; use crate::services::{ ProfileDataMode, @@ -43,6 +43,7 @@ pub(super) struct WebSurfaceRuntime { transient_cleanup_error: Option, client_factory: LiveRuntimeClientFactory, last_generation: u64, + color_scheme: ColorScheme, } const SIDECAR_RESTART_BASE_DELAY: Duration = Duration::from_millis(250); @@ -60,6 +61,7 @@ impl WebSurfaceRuntime { transient_cleanup_error, client_factory: new_servo_live_client, last_generation: 0, + color_scheme: ColorScheme::Light, } } @@ -74,6 +76,7 @@ impl WebSurfaceRuntime { transient_cleanup_error: None, client_factory, last_generation: 0, + color_scheme: ColorScheme::Light, } } @@ -125,6 +128,7 @@ impl WebSurfaceRuntime { height: size.height, page_zoom_percent: zoom_percent, device_pixel_ratio: size.device_pixel_ratio_f32(), + color_scheme: self.color_scheme, scroll_delta_x, scroll_delta_y, 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( &mut self, tab_id: &TabId, @@ -472,3 +480,6 @@ mod retry_tests; #[cfg(test)] #[path = "web_surface_runtime_tests.rs"] mod tests; +#[cfg(test)] +#[path = "web_surface_theme_tests.rs"] +mod theme_tests; diff --git a/crates/ely_app/src/shell/web_surface_state.rs b/crates/ely_app/src/shell/web_surface_state.rs index 8838f3a..ca0176d 100644 --- a/crates/ely_app/src/shell/web_surface_state.rs +++ b/crates/ely_app/src/shell/web_surface_state.rs @@ -1,6 +1,6 @@ use std::time::{Duration, Instant}; -use ely_domain::{ProfileId, TabId}; +use ely_domain::{ColorScheme, ProfileId, TabId}; use gpui::{Bounds, Pixels}; use crate::services::ProfileDataMode; @@ -309,6 +309,7 @@ pub(super) struct WebSurfaceEnsureKey { profile_id: ProfileId, profile_data_mode: ProfileDataMode, zoom_percent: u16, + color_scheme: ColorScheme, permissions: Vec, } @@ -319,6 +320,7 @@ impl WebSurfaceEnsureKey { profile_id: ProfileId, profile_data_mode: ProfileDataMode, zoom_percent: u16, + color_scheme: ColorScheme, permissions: &[WebSurfaceSitePermission], ) -> Self { Self { @@ -327,6 +329,7 @@ impl WebSurfaceEnsureKey { profile_id, profile_data_mode, zoom_percent, + color_scheme, permissions: permissions.to_vec(), } } @@ -446,6 +449,7 @@ mod tests { profile_id.clone(), ProfileDataMode::Persistent, 100, + ColorScheme::Light, &[], ) } diff --git a/crates/ely_app/src/shell/web_surface_theme_tests.rs b/crates/ely_app/src/shell/web_surface_theme_tests.rs new file mode 100644 index 0000000..ce840e7 --- /dev/null +++ b/crates/ely_app/src/shell/web_surface_theme_tests.rs @@ -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> = Mutex::new(Vec::new()); + +struct ThemeRecordingClient; + +impl LiveRuntimeClient for ThemeRecordingClient { + fn ensure( + &mut self, + request: ServoLiveEnsureRequest, + ) -> Result, 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, 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(()) +} diff --git a/crates/ely_app/src/shell/web_surface_worker_dispatch.rs b/crates/ely_app/src/shell/web_surface_worker_dispatch.rs index b9c4731..936da40 100644 --- a/crates/ely_app/src/shell/web_surface_worker_dispatch.rs +++ b/crates/ely_app/src/shell/web_surface_worker_dispatch.rs @@ -25,6 +25,7 @@ pub(super) fn can_merge_consecutive_scroll( && latest.height == previous.height && latest.page_zoom_percent == previous.page_zoom_percent && 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) { diff --git a/crates/ely_app/src/shell/web_surface_worker_scroll_tests.rs b/crates/ely_app/src/shell/web_surface_worker_scroll_tests.rs index 86b3aa4..2c59b93 100644 --- a/crates/ely_app/src/shell/web_surface_worker_scroll_tests.rs +++ b/crates/ely_app/src/shell/web_surface_worker_scroll_tests.rs @@ -78,6 +78,7 @@ fn ensure_request(scroll_delta: i32) -> ServoLiveEnsureRequest { height: 480, page_zoom_percent: 100, device_pixel_ratio: 1.0, + color_scheme: ely_domain::ColorScheme::Light, scroll_delta_x: scroll_delta, scroll_delta_y: scroll_delta, scroll_point_x: (scroll_delta != 0).then_some(1), diff --git a/crates/ely_app/src/shell/web_surface_worker_tests.rs b/crates/ely_app/src/shell/web_surface_worker_tests.rs index fbe4678..f049abb 100644 --- a/crates/ely_app/src/shell/web_surface_worker_tests.rs +++ b/crates/ely_app/src/shell/web_surface_worker_tests.rs @@ -450,6 +450,7 @@ fn ensure_request(tab_id: &str, input: RecordedInput) -> ServoLiveEnsureRequest height: 480, page_zoom_percent: 100, device_pixel_ratio: 1.0, + color_scheme: ely_domain::ColorScheme::Light, scroll_delta_x: i32::from(scroll), scroll_delta_y: i32::from(scroll), scroll_point_x: scroll.then_some(1), diff --git a/crates/ely_domain/src/appearance.rs b/crates/ely_domain/src/appearance.rs index d6cbef5..bab944a 100644 --- a/crates/ely_domain/src/appearance.rs +++ b/crates/ely_domain/src/appearance.rs @@ -19,6 +19,24 @@ pub enum ThemeMode { 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 MAX_TRANSLUCENCY_PCT: u8 = 100; @@ -77,7 +95,7 @@ impl AppearanceSettings { #[cfg(test)] mod tests { - use super::{AppearanceSettings, ThemeMode, WallpaperTheme}; + use super::{AppearanceSettings, ColorScheme, ThemeMode, WallpaperTheme}; #[test] fn default_settings_use_dawn_and_system_mode() { @@ -103,6 +121,13 @@ mod tests { 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] fn translucency_setter_clamps_above_max() { let mut settings = AppearanceSettings::default(); diff --git a/crates/ely_domain/src/lib.rs b/crates/ely_domain/src/lib.rs index b9b1cff..1c76b2f 100644 --- a/crates/ely_domain/src/lib.rs +++ b/crates/ely_domain/src/lib.rs @@ -24,7 +24,8 @@ mod tab_group; mod url_text; 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 bookmark::BookmarkEntry; diff --git a/crates/ely_servo_host/src/bin/ely_servo_sidecar/live.rs b/crates/ely_servo_host/src/bin/ely_servo_sidecar/live.rs index d7c342f..4b038bc 100644 --- a/crates/ely_servo_host/src/bin/ely_servo_sidecar/live.rs +++ b/crates/ely_servo_host/src/bin/ely_servo_sidecar/live.rs @@ -21,8 +21,8 @@ use super::{ }, live_request::{MAX_REQUEST_LINE_BYTES, RequestLineRead, read_request_line}, live_session::{ - LiveInput, LiveSession, apply_input, apply_layout, apply_permissions, bind_profile, - ensure_session, + LiveInput, LiveSession, apply_color_scheme, apply_input, apply_layout, apply_permissions, + bind_profile, ensure_session, }, }; @@ -159,6 +159,7 @@ fn handle_request( height, page_zoom_percent, device_pixel_ratio, + color_scheme, scroll_delta_x, scroll_delta_y, scroll_point_x, @@ -184,6 +185,7 @@ fn handle_request( let session = ensure_session(host, sessions, tab_id.clone(), &tab, &profile, width, height)?; apply_layout(host, session, width, height, page_zoom_percent, device_pixel_ratio)?; + apply_color_scheme(host, session, color_scheme)?; apply_permissions( host, session, diff --git a/crates/ely_servo_host/src/bin/ely_servo_sidecar/live_protocol.rs b/crates/ely_servo_host/src/bin/ely_servo_sidecar/live_protocol.rs index 4926217..e52a838 100644 --- a/crates/ely_servo_host/src/bin/ely_servo_sidecar/live_protocol.rs +++ b/crates/ely_servo_host/src/bin/ely_servo_sidecar/live_protocol.rs @@ -1,5 +1,6 @@ use std::{io, path::PathBuf}; +use ely_domain::ColorScheme; use ely_servo_host::{ ConsumedPermission, IOSurfaceHandle, RenderedFrame, ServoHostError, WebViewSnapshot, WebViewState, @@ -10,7 +11,7 @@ use thiserror::Error; #[cfg(all(feature = "hardware-render", target_os = "macos"))] 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_BYTE_COUNT: usize = 256 * 1024 * 1024; pub(super) const MAX_RESPONSE_HEADER_BYTES: usize = 256 * 1024; @@ -35,6 +36,7 @@ pub(super) enum LiveRequest { page_zoom_percent: u16, #[serde(default = "default_device_pixel_ratio")] device_pixel_ratio: f32, + color_scheme: ColorScheme, #[serde(default)] scroll_delta_x: i32, #[serde(default)] @@ -401,7 +403,7 @@ mod tests { #[test] fn ensure_defaults_optional_input_fields() -> Result<(), serde_json::Error> { let request = serde_json::from_str::( - 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!( @@ -409,6 +411,7 @@ mod tests { LiveRequest::Ensure { page_zoom_percent: 100, device_pixel_ratio: 1.0, + color_scheme: ColorScheme::Dark, scroll_delta_x: 0, scroll_delta_y: 0, ready_surface_ids, @@ -419,19 +422,28 @@ mod tests { Ok(()) } + #[test] + fn ensure_requires_color_scheme() { + let request = serde_json::from_str::( + 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] fn handshake_deserializes_protocol_version() -> Result<(), serde_json::Error> { let request = - serde_json::from_str::(r#"{"type":"handshake","protocol_version":3}"#)?; + serde_json::from_str::(r#"{"type":"handshake","protocol_version":4}"#)?; - assert!(matches!(request, LiveRequest::Handshake { protocol_version: 3 })); + assert!(matches!(request, LiveRequest::Handshake { protocol_version: 4 })); Ok(()) } #[test] fn site_permission_requires_revision() { let request = serde_json::from_str::( - 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()); diff --git a/crates/ely_servo_host/src/bin/ely_servo_sidecar/live_session.rs b/crates/ely_servo_host/src/bin/ely_servo_sidecar/live_session.rs index 9505a12..0dac268 100644 --- a/crates/ely_servo_host/src/bin/ely_servo_sidecar/live_session.rs +++ b/crates/ely_servo_host/src/bin/ely_servo_sidecar/live_session.rs @@ -1,11 +1,11 @@ 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::{ - HidpiScaleRequest, KeyboardTextRequest, MouseClickRequest, MouseHoverRequest, PageZoomRequest, - PermissionDecision, PermissionSnapshotEntry, PermissionSnapshotRequest, - PermissionSnapshotState, RenderedFrame, ResizeRequest, ScrollRequest, ServoHost, - ServoSurfaceSize, SoftwareServoHost, + ColorSchemeRequest, HidpiScaleRequest, KeyboardTextRequest, MouseClickRequest, + MouseHoverRequest, PageZoomRequest, PermissionDecision, PermissionSnapshotEntry, + PermissionSnapshotRequest, PermissionSnapshotState, RenderedFrame, ResizeRequest, + ScrollRequest, ServoHost, ServoSurfaceSize, SoftwareServoHost, }; use super::live_protocol::{LiveSidecarError, LiveSitePermission}; @@ -170,6 +170,18 @@ pub(super) fn apply_permissions( .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) scroll_delta_x: i32, pub(super) scroll_delta_y: i32, diff --git a/crates/ely_servo_host/src/bin/ely_servo_sidecar/live_tests.rs b/crates/ely_servo_host/src/bin/ely_servo_sidecar/live_tests.rs index 9bf0465..9d513df 100644 --- a/crates/ely_servo_host/src/bin/ely_servo_sidecar/live_tests.rs +++ b/crates/ely_servo_host/src/bin/ely_servo_sidecar/live_tests.rs @@ -67,7 +67,7 @@ fn request_line_accepts_a_bounded_eof_terminated_frame() -> Result<(), Box Vec { - 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()); line.resize(bytes - 1, b' '); line.push(b'\n'); diff --git a/crates/ely_servo_host/src/host.rs b/crates/ely_servo_host/src/host.rs index fe954f8..cc4c3af 100644 --- a/crates/ely_servo_host/src/host.rs +++ b/crates/ely_servo_host/src/host.rs @@ -1,5 +1,6 @@ use ely_domain::{ - ProfileId, SiteOrigin, SitePermissionDecision, SitePermissionFeature, TabId, UrlText, WebViewId, + ColorScheme, ProfileId, SiteOrigin, SitePermissionDecision, SitePermissionFeature, TabId, + UrlText, WebViewId, }; use crate::ServoHostError; @@ -280,6 +281,12 @@ pub struct HidpiScaleRequest { 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)] pub struct MouseClickRequest { pub webview_id: WebViewId, @@ -385,6 +392,8 @@ pub trait ServoHost { 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 hover(&mut self, request: MouseHoverRequest) -> Result<(), ServoHostError>; diff --git a/crates/ely_servo_host/src/lib.rs b/crates/ely_servo_host/src/lib.rs index b0bf971..88db7c6 100644 --- a/crates/ely_servo_host/src/lib.rs +++ b/crates/ely_servo_host/src/lib.rs @@ -22,11 +22,12 @@ pub use error::ServoHostError; #[cfg(feature = "hardware-render")] pub use hardware_rendering_context::HardwareOffscreenContext; pub use host::{ - ConsumedPermission, HidpiScaleRequest, KeyboardTextRequest, MAX_PAGE_TITLE_BYTES, - MouseClickRequest, MouseDragRequest, MouseHoverRequest, NavigationRequest, PageZoomRequest, - PermissionDecision, PermissionSnapshotEntry, PermissionSnapshotRequest, - PermissionSnapshotState, RenderedFrame, RenderedFrameSummary, ResizeRequest, ScrollRequest, - ServoHost, TouchTapRequest, WebViewSnapshot, WebViewSnapshotPending, WebViewState, + ColorSchemeRequest, ConsumedPermission, HidpiScaleRequest, KeyboardTextRequest, + MAX_PAGE_TITLE_BYTES, MouseClickRequest, MouseDragRequest, MouseHoverRequest, + NavigationRequest, PageZoomRequest, PermissionDecision, PermissionSnapshotEntry, + PermissionSnapshotRequest, PermissionSnapshotState, RenderedFrame, RenderedFrameSummary, + ResizeRequest, ScrollRequest, ServoHost, TouchTapRequest, WebViewSnapshot, + WebViewSnapshotPending, WebViewState, }; pub use iosurface_handle::{IOSurfaceHandle, IOSurfaceIdentity}; #[cfg(feature = "servo-engine")] diff --git a/crates/ely_servo_host/src/runtime.rs b/crates/ely_servo_host/src/runtime.rs index fcea317..a977588 100644 --- a/crates/ely_servo_host/src/runtime.rs +++ b/crates/ely_servo_host/src/runtime.rs @@ -9,11 +9,11 @@ use std::{ }; use dpi::PhysicalSize; -use ely_domain::{ProfileId, TabId, WebViewId}; +use ely_domain::{ColorScheme, ProfileId, TabId, WebViewId}; use raw_window_handle::{HasDisplayHandle, HasWindowHandle}; use servo::{ - DevicePoint, DeviceVector2D, Opts, Scroll, Servo, ServoBuilder, WebViewBuilder, WebViewPoint, - WebViewVector, + DevicePoint, DeviceVector2D, Opts, Scroll, Servo, ServoBuilder, Theme, WebViewBuilder, + WebViewPoint, WebViewVector, }; #[path = "runtime_context.rs"] @@ -32,8 +32,8 @@ use runtime_preferences::ely_servo_preferences; use url::Url; use crate::{ - ConsumedPermission, HidpiScaleRequest, KeyboardTextRequest, MouseClickRequest, - MouseDragRequest, MouseHoverRequest, NavigationRequest, PageZoomRequest, + ColorSchemeRequest, ConsumedPermission, HidpiScaleRequest, KeyboardTextRequest, + MouseClickRequest, MouseDragRequest, MouseHoverRequest, NavigationRequest, PageZoomRequest, PermissionSnapshotRequest, RenderedFrame, ResizeRequest, ScrollRequest, ServoHost, ServoHostError, TouchTapRequest, WebViewSnapshot, WebViewState, runtime_input::{ @@ -204,13 +204,15 @@ impl ServoHost for SoftwareServoHost { webview.delegate.set_state(WebViewState::Loading); if should_create_initial_document { 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()) .url(url) // The live path pushes DPR before first navigation. Preserve that scale when // replacing the about:blank WebView so CSS viewport = physical surface / DPR. .hidpi_scale_factor(hidpi_scale_factor) .build(); + replacement.notify_theme_change(servo_theme(webview.color_scheme)); + webview.webview = replacement; // The input-accepting invariant lives in `webview_for_input`. webview.webview.show(); webview.webview.focus(); @@ -303,6 +305,18 @@ impl ServoHost for SoftwareServoHost { 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> { let webview = self.webview_for_input(&request.webview_id)?; send_mouse_hover(&webview.webview, request.x, request.y); @@ -440,6 +454,7 @@ impl SoftwareServoHost { webview, delegate, requested_url: None, + color_scheme: ColorScheme::Light, }, ); @@ -472,3 +487,10 @@ impl SoftwareServoHost { Ok(webview) } } + +fn servo_theme(color_scheme: ColorScheme) -> Theme { + match color_scheme { + ColorScheme::Light => Theme::Light, + ColorScheme::Dark => Theme::Dark, + } +} diff --git a/crates/ely_servo_host/src/runtime_webview.rs b/crates/ely_servo_host/src/runtime_webview.rs index e16b7e8..8971b79 100644 --- a/crates/ely_servo_host/src/runtime_webview.rs +++ b/crates/ely_servo_host/src/runtime_webview.rs @@ -1,6 +1,6 @@ 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 url::Url; @@ -19,6 +19,7 @@ pub(super) struct HostWebView { pub(super) webview: WebView, pub(super) delegate: Rc, pub(super) requested_url: Option, + pub(super) color_scheme: ColorScheme, } impl HostWebView { diff --git a/crates/ely_servo_host/tests/sidecar.rs b/crates/ely_servo_host/tests/sidecar.rs index 58a1efd..6c13a28 100644 --- a/crates/ely_servo_host/tests/sidecar.rs +++ b/crates/ely_servo_host/tests/sidecar.rs @@ -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 stdout = child.stdout.take().ok_or_else(|| io::Error::other("missing 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.push(b'\n'); diff --git a/crates/ely_servo_host/tests/sidecar/support.rs b/crates/ely_servo_host/tests/sidecar/support.rs index c65e4df..09bcf10 100644 --- a/crates/ely_servo_host/tests/sidecar/support.rs +++ b/crates/ely_servo_host/tests/sidecar/support.rs @@ -24,7 +24,7 @@ use super::pages::{ pub(super) const WIDTH: u32 = 360; pub(super) const HEIGHT: u32 = 240; 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; 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, "page_zoom_percent": 100, "device_pixel_ratio": 1.0, + "color_scheme": "light", "site_permission_generation": 0, "site_permissions": [], }) diff --git a/crates/ely_servo_host/tests/software_host.rs b/crates/ely_servo_host/tests/software_host.rs index a41bc2c..4163c4a 100644 --- a/crates/ely_servo_host/tests/software_host.rs +++ b/crates/ely_servo_host/tests/software_host.rs @@ -6,12 +6,13 @@ use std::{ process::{Command, Stdio}, }; -use ely_domain::{ProfileId, SiteOrigin, SitePermissionFeature, TabId, UrlText}; +use ely_domain::{ColorScheme, ProfileId, SiteOrigin, SitePermissionFeature, TabId, UrlText}; use ely_servo_host::{ - HidpiScaleRequest, KeyboardTextRequest, MouseClickRequest, MouseDragRequest, NavigationRequest, - PageZoomRequest, PermissionDecision, PermissionSnapshotEntry, PermissionSnapshotRequest, - PermissionSnapshotState, ResizeRequest, ScrollRequest, ServoHost, ServoHostError, - ServoSurfaceSize, SoftwareServoHost, TouchTapRequest, WebViewState, + ColorSchemeRequest, HidpiScaleRequest, KeyboardTextRequest, MouseClickRequest, + MouseDragRequest, NavigationRequest, PageZoomRequest, PermissionDecision, + PermissionSnapshotEntry, PermissionSnapshotRequest, PermissionSnapshotState, ResizeRequest, + ScrollRequest, ServoHost, ServoHostError, ServoSurfaceSize, SoftwareServoHost, TouchTapRequest, + WebViewState, }; 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 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 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"; struct PrdSiteCompatibilityCase { @@ -235,6 +237,29 @@ fn exercise_real_servo_webview_lifecycle() -> Result<(), Box> { "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)?; host.navigate(NavigationRequest { webview_id: webview_id.clone(), tab_id, url })?; diff --git a/docs/servo-embedding-architecture.md b/docs/servo-embedding-architecture.md index 990d091..8043ce3 100644 --- a/docs/servo-embedding-architecture.md +++ b/docs/servo-embedding-architecture.md @@ -50,8 +50,9 @@ transfers the IOSurface send right; the JSON port number is diagnostic metadata. The protocol supports: -- `handshake`: verify protocol version `3` before accepting browser commands. -- `ensure`: create or update a WebView, navigation, viewport, zoom, permissions, and input. +- `handshake`: verify protocol version `4` before accepting browser commands. +- `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. - `close`: destroy one tab's WebView. - `shutdown`: acknowledge graceful process shutdown so Servo flushes profile storage.