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
@@ -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,
@@ -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::<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!(
@@ -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::<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]
fn handshake_deserializes_protocol_version() -> Result<(), serde_json::Error> {
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(())
}
#[test]
fn site_permission_requires_revision() {
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());
@@ -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,
@@ -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> {
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');
+10 -1
View File
@@ -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>;
+6 -5
View File
@@ -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")]
+28 -6
View File
@@ -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,
}
}
+2 -1
View File
@@ -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<HostWebViewDelegate>,
pub(super) requested_url: Option<String>,
pub(super) color_scheme: ColorScheme,
}
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 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');
@@ -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": [],
})
+30 -5
View File
@@ -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<dyn Error>> {
"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 })?;