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
@@ -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,
@@ -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"
)
}
}
@@ -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<u32>,
@@ -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<u32>,
+4
View File
@@ -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<Subscription>,
}
@@ -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));
+56 -27
View File
@@ -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<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);
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();
}
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,
);
}
}
+20 -3
View File
@@ -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<WebSurfaceKeyboardFocusState>,
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)
@@ -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<String>,
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;
@@ -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<WebSurfaceSitePermission>,
}
@@ -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,
&[],
)
}
@@ -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.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) {
@@ -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),
@@ -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),