constellation: Pass system theme to new Pipelines (#37132)

Previously, when the theme was set it was only set on currently active
`Window`s. This change makes setting the `Theme` stateful. Now the
`Constellation` tracks what theme is applied to a `WebView` and properly
passes that value to new `Pipeline`s when they are constructed. In
addition, the value is passed to layout when that is constructed as
well.

Testing: this change adds a unit test.

Signed-off-by: Martin Robinson <mrobinson@igalia.com>
This commit is contained in:
Martin Robinson 2025-05-26 14:05:38 +02:00 committed by GitHub
parent c96de69e80
commit d3e57a513c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 201 additions and 74 deletions

1
Cargo.lock generated
View file

@ -1925,6 +1925,7 @@ dependencies = [
"servo_malloc_size_of", "servo_malloc_size_of",
"servo_url", "servo_url",
"strum_macros", "strum_macros",
"stylo",
"stylo_traits", "stylo_traits",
"url", "url",
"webdriver", "webdriver",

View file

@ -172,6 +172,7 @@ use crate::browsingcontext::{
AllBrowsingContextsIterator, BrowsingContext, FullyActiveBrowsingContextsIterator, AllBrowsingContextsIterator, BrowsingContext, FullyActiveBrowsingContextsIterator,
NewBrowsingContextInfo, NewBrowsingContextInfo,
}; };
use crate::constellation_webview::ConstellationWebView;
use crate::event_loop::EventLoop; use crate::event_loop::EventLoop;
use crate::pipeline::{InitialPipelineState, Pipeline}; use crate::pipeline::{InitialPipelineState, Pipeline};
use crate::process_manager::ProcessManager; use crate::process_manager::ProcessManager;
@ -229,18 +230,6 @@ struct WebrenderWGPU {
wgpu_image_map: WGPUImageMap, wgpu_image_map: WGPUImageMap,
} }
/// Servo supports multiple top-level browsing contexts or “webviews”, so `Constellation` needs to
/// store webview-specific data for bookkeeping.
struct WebView {
/// The currently focused browsing context in this webview for key events.
/// The focused pipeline is the current entry of the focused browsing
/// context.
focused_browsing_context_id: BrowsingContextId,
/// The joint session history for this webview.
session_history: JointSessionHistory,
}
/// A browsing context group. /// A browsing context group.
/// ///
/// <https://html.spec.whatwg.org/multipage/#browsing-context-group> /// <https://html.spec.whatwg.org/multipage/#browsing-context-group>
@ -324,7 +313,7 @@ pub struct Constellation<STF, SWF> {
compositor_proxy: CompositorProxy, compositor_proxy: CompositorProxy,
/// Bookkeeping data for all webviews in the constellation. /// Bookkeeping data for all webviews in the constellation.
webviews: WebViewManager<WebView>, webviews: WebViewManager<ConstellationWebView>,
/// Channels for the constellation to send messages to the public /// Channels for the constellation to send messages to the public
/// resource-related threads. There are two groups of resource threads: one /// resource-related threads. There are two groups of resource threads: one
@ -895,6 +884,16 @@ where
if self.shutting_down { if self.shutting_down {
return; return;
} }
let Some(theme) = self
.webviews
.get(webview_id)
.map(ConstellationWebView::theme)
else {
warn!("Tried to create Pipeline for uknown WebViewId: {webview_id:?}");
return;
};
debug!( debug!(
"{}: Creating new pipeline in {}", "{}: Creating new pipeline in {}",
pipeline_id, browsing_context_id pipeline_id, browsing_context_id
@ -973,6 +972,7 @@ where
time_profiler_chan: self.time_profiler_chan.clone(), time_profiler_chan: self.time_profiler_chan.clone(),
mem_profiler_chan: self.mem_profiler_chan.clone(), mem_profiler_chan: self.mem_profiler_chan.clone(),
viewport_details: initial_viewport_details, viewport_details: initial_viewport_details,
theme,
event_loop, event_loop,
load_data, load_data,
prev_throttled: throttled, prev_throttled: throttled,
@ -1436,8 +1436,8 @@ where
size_type, size_type,
); );
}, },
EmbedderToConstellationMessage::ThemeChange(theme) => { EmbedderToConstellationMessage::ThemeChange(webview_id, theme) => {
self.handle_theme_change(theme); self.handle_theme_change(webview_id, theme);
}, },
EmbedderToConstellationMessage::TickAnimation(webview_ids) => { EmbedderToConstellationMessage::TickAnimation(webview_ids) => {
self.handle_tick_animation(webview_ids) self.handle_tick_animation(webview_ids)
@ -3142,13 +3142,8 @@ where
// Register this new top-level browsing context id as a webview and set // Register this new top-level browsing context id as a webview and set
// its focused browsing context to be itself. // its focused browsing context to be itself.
self.webviews.add( self.webviews
webview_id, .add(webview_id, ConstellationWebView::new(browsing_context_id));
WebView {
focused_browsing_context_id: browsing_context_id,
session_history: JointSessionHistory::new(),
},
);
// https://html.spec.whatwg.org/multipage/#creating-a-new-browsing-context-group // https://html.spec.whatwg.org/multipage/#creating-a-new-browsing-context-group
let mut new_bc_group: BrowsingContextGroup = Default::default(); let mut new_bc_group: BrowsingContextGroup = Default::default();
@ -3554,10 +3549,7 @@ where
self.pipelines.insert(new_pipeline_id, pipeline); self.pipelines.insert(new_pipeline_id, pipeline);
self.webviews.add( self.webviews.add(
new_webview_id, new_webview_id,
WebView { ConstellationWebView::new(new_browsing_context_id),
focused_browsing_context_id: new_browsing_context_id,
session_history: JointSessionHistory::new(),
},
); );
// https://html.spec.whatwg.org/multipage/#bcg-append // https://html.spec.whatwg.org/multipage/#bcg-append
@ -5623,18 +5615,31 @@ where
} }
} }
/// Handle theme change events from the embedder and forward them to the script thread /// Handle theme change events from the embedder and forward them to all appropriate `ScriptThread`s.
#[cfg_attr( #[cfg_attr(
feature = "tracing", feature = "tracing",
tracing::instrument(skip_all, fields(servo_profiling = true), level = "trace") tracing::instrument(skip_all, fields(servo_profiling = true), level = "trace")
)] )]
fn handle_theme_change(&mut self, theme: Theme) { fn handle_theme_change(&mut self, webview_id: WebViewId, theme: Theme) {
let Some(webview) = self.webviews.get_mut(webview_id) else {
warn!("Received theme change request for uknown WebViewId: {webview_id:?}");
return;
};
if !webview.set_theme(theme) {
return;
}
for pipeline in self.pipelines.values() { for pipeline in self.pipelines.values() {
let msg = ScriptThreadMessage::ThemeChange(pipeline.id, theme); if pipeline.webview_id != webview_id {
if let Err(err) = pipeline.event_loop.send(msg) { continue;
}
if let Err(error) = pipeline
.event_loop
.send(ScriptThreadMessage::ThemeChange(pipeline.id, theme))
{
warn!( warn!(
"{}: Failed to send theme change event to pipeline ({:?}).", "{}: Failed to send theme change event to pipeline ({error:?}).",
pipeline.id, err pipeline.id,
); );
} }
} }

View file

@ -0,0 +1,45 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use base::id::BrowsingContextId;
use embedder_traits::Theme;
use crate::session_history::JointSessionHistory;
/// The `Constellation`'s view of a `WebView` in the embedding layer. This tracks all of the
/// `Constellation` state for this `WebView`.
pub(crate) struct ConstellationWebView {
/// The currently focused browsing context in this webview for key events.
/// The focused pipeline is the current entry of the focused browsing
/// context.
pub focused_browsing_context_id: BrowsingContextId,
/// The joint session history for this webview.
pub session_history: JointSessionHistory,
/// The [`Theme`] that this [`ConstellationWebView`] uses. This is communicated to all
/// `ScriptThread`s so that they know how to render the contents of a particular `WebView.
theme: Theme,
}
impl ConstellationWebView {
pub(crate) fn new(focused_browsing_context_id: BrowsingContextId) -> Self {
Self {
focused_browsing_context_id,
session_history: JointSessionHistory::new(),
theme: Theme::Light,
}
}
/// Set the [`Theme`] on this [`ConstellationWebView`] returning true if the theme changed.
pub(crate) fn set_theme(&mut self, new_theme: Theme) -> bool {
let old_theme = std::mem::replace(&mut self.theme, new_theme);
old_theme != self.theme
}
/// Get the [`Theme`] of this [`ConstellationWebView`].
pub(crate) fn theme(&self) -> Theme {
self.theme
}
}

View file

@ -9,6 +9,7 @@ mod tracing;
mod browsingcontext; mod browsingcontext;
mod constellation; mod constellation;
mod constellation_webview;
mod event_loop; mod event_loop;
mod logging; mod logging;
mod pipeline; mod pipeline;

View file

@ -25,7 +25,7 @@ use constellation_traits::{LoadData, SWManagerMsg, ScriptToConstellationChan};
use crossbeam_channel::{Sender, unbounded}; use crossbeam_channel::{Sender, unbounded};
use devtools_traits::{DevtoolsControlMsg, ScriptToDevtoolsControlMsg}; use devtools_traits::{DevtoolsControlMsg, ScriptToDevtoolsControlMsg};
use embedder_traits::user_content_manager::UserContentManager; use embedder_traits::user_content_manager::UserContentManager;
use embedder_traits::{AnimationState, FocusSequenceNumber, ViewportDetails}; use embedder_traits::{AnimationState, FocusSequenceNumber, Theme, ViewportDetails};
use fonts::{SystemFontServiceProxy, SystemFontServiceProxySender}; use fonts::{SystemFontServiceProxy, SystemFontServiceProxySender};
use ipc_channel::Error; use ipc_channel::Error;
use ipc_channel::ipc::{self, IpcReceiver, IpcSender}; use ipc_channel::ipc::{self, IpcReceiver, IpcSender};
@ -61,7 +61,7 @@ pub struct Pipeline {
/// The ID of the browsing context that contains this Pipeline. /// The ID of the browsing context that contains this Pipeline.
pub browsing_context_id: BrowsingContextId, pub browsing_context_id: BrowsingContextId,
/// The ID of the top-level browsing context that contains this Pipeline. /// The [`WebViewId`] of the `WebView` that contains this Pipeline.
pub webview_id: WebViewId, pub webview_id: WebViewId,
pub opener: Option<BrowsingContextId>, pub opener: Option<BrowsingContextId>,
@ -170,6 +170,9 @@ pub struct InitialPipelineState {
/// The initial [`ViewportDetails`] to use when starting this new [`Pipeline`]. /// The initial [`ViewportDetails`] to use when starting this new [`Pipeline`].
pub viewport_details: ViewportDetails, pub viewport_details: ViewportDetails,
/// The initial [`Theme`] to use when starting this new [`Pipeline`].
pub theme: Theme,
/// The ID of the pipeline namespace for this script thread. /// The ID of the pipeline namespace for this script thread.
pub pipeline_namespace_id: PipelineNamespaceId, pub pipeline_namespace_id: PipelineNamespaceId,
@ -224,6 +227,7 @@ impl Pipeline {
opener: state.opener, opener: state.opener,
load_data: state.load_data.clone(), load_data: state.load_data.clone(),
viewport_details: state.viewport_details, viewport_details: state.viewport_details,
theme: state.theme,
}; };
if let Err(e) = script_chan.send(ScriptThreadMessage::AttachLayout(new_layout_info)) if let Err(e) = script_chan.send(ScriptThreadMessage::AttachLayout(new_layout_info))
@ -280,6 +284,7 @@ impl Pipeline {
time_profiler_chan: state.time_profiler_chan, time_profiler_chan: state.time_profiler_chan,
mem_profiler_chan: state.mem_profiler_chan, mem_profiler_chan: state.mem_profiler_chan,
viewport_details: state.viewport_details, viewport_details: state.viewport_details,
theme: state.theme,
script_chan: script_chan.clone(), script_chan: script_chan.clone(),
load_data: state.load_data.clone(), load_data: state.load_data.clone(),
script_port, script_port,
@ -494,6 +499,7 @@ pub struct UnprivilegedPipelineContent {
time_profiler_chan: time::ProfilerChan, time_profiler_chan: time::ProfilerChan,
mem_profiler_chan: profile_mem::ProfilerChan, mem_profiler_chan: profile_mem::ProfilerChan,
viewport_details: ViewportDetails, viewport_details: ViewportDetails,
theme: Theme,
script_chan: IpcSender<ScriptThreadMessage>, script_chan: IpcSender<ScriptThreadMessage>,
load_data: LoadData, load_data: LoadData,
script_port: IpcReceiver<ScriptThreadMessage>, script_port: IpcReceiver<ScriptThreadMessage>,
@ -544,6 +550,7 @@ impl UnprivilegedPipelineContent {
memory_profiler_sender: self.mem_profiler_chan.clone(), memory_profiler_sender: self.mem_profiler_chan.clone(),
devtools_server_sender: self.devtools_ipc_sender, devtools_server_sender: self.devtools_ipc_sender,
viewport_details: self.viewport_details, viewport_details: self.viewport_details,
theme: self.theme,
pipeline_namespace_id: self.pipeline_namespace_id, pipeline_namespace_id: self.pipeline_namespace_id,
content_process_shutdown_sender: content_process_shutdown_chan, content_process_shutdown_sender: content_process_shutdown_chan,
webgl_chan: self.webgl_chan, webgl_chan: self.webgl_chan,

View file

@ -15,7 +15,7 @@ use base::Epoch;
use base::id::{PipelineId, WebViewId}; use base::id::{PipelineId, WebViewId};
use compositing_traits::CrossProcessCompositorApi; use compositing_traits::CrossProcessCompositorApi;
use constellation_traits::ScrollState; use constellation_traits::ScrollState;
use embedder_traits::{UntrustedNodeAddress, ViewportDetails}; use embedder_traits::{Theme, UntrustedNodeAddress, ViewportDetails};
use euclid::default::{Point2D as UntypedPoint2D, Rect as UntypedRect}; use euclid::default::{Point2D as UntypedPoint2D, Rect as UntypedRect};
use euclid::{Point2D, Scale, Size2D, Vector2D}; use euclid::{Point2D, Scale, Size2D, Vector2D};
use fnv::FnvHashMap; use fnv::FnvHashMap;
@ -503,8 +503,7 @@ impl LayoutThread {
Scale::new(config.viewport_details.hidpi_scale_factor.get()), Scale::new(config.viewport_details.hidpi_scale_factor.get()),
Box::new(LayoutFontMetricsProvider(config.font_context.clone())), Box::new(LayoutFontMetricsProvider(config.font_context.clone())),
ComputedValues::initial_values_with_font_override(font), ComputedValues::initial_values_with_font_override(font),
// TODO: obtain preferred color scheme from embedder config.theme.into(),
PrefersColorScheme::Light,
); );
LayoutThread { LayoutThread {
@ -951,7 +950,8 @@ impl LayoutThread {
size_did_change || pixel_ratio_did_change size_did_change || pixel_ratio_did_change
} }
fn theme_did_change(&self, theme: PrefersColorScheme) -> bool { fn theme_did_change(&self, theme: Theme) -> bool {
let theme: PrefersColorScheme = theme.into();
theme != self.device().color_scheme() theme != self.device().color_scheme()
} }
@ -959,7 +959,7 @@ impl LayoutThread {
fn update_device( fn update_device(
&mut self, &mut self,
viewport_details: ViewportDetails, viewport_details: ViewportDetails,
theme: PrefersColorScheme, theme: Theme,
guards: &StylesheetGuards, guards: &StylesheetGuards,
) { ) {
let device = Device::new( let device = Device::new(
@ -969,7 +969,7 @@ impl LayoutThread {
Scale::new(viewport_details.hidpi_scale_factor.get()), Scale::new(viewport_details.hidpi_scale_factor.get()),
Box::new(LayoutFontMetricsProvider(self.font_context.clone())), Box::new(LayoutFontMetricsProvider(self.font_context.clone())),
self.stylist.device().default_computed_values().to_arc(), self.stylist.device().default_computed_values().to_arc(),
theme, theme.into(),
); );
// Preserve any previously computed root font size. // Preserve any previously computed root font size.

View file

@ -223,6 +223,7 @@ impl HTMLIFrameElement {
old_pipeline_id, old_pipeline_id,
sandbox: sandboxed, sandbox: sandboxed,
viewport_details, viewport_details,
theme: window.theme(),
}; };
window window
.as_global_scope() .as_global_scope()
@ -238,6 +239,7 @@ impl HTMLIFrameElement {
opener: None, opener: None,
load_data, load_data,
viewport_details, viewport_details,
theme: window.theme(),
}; };
self.pipeline_id.set(Some(new_pipeline_id)); self.pipeline_id.set(Some(new_pipeline_id));
@ -250,6 +252,7 @@ impl HTMLIFrameElement {
old_pipeline_id, old_pipeline_id,
sandbox: sandboxed, sandbox: sandboxed,
viewport_details, viewport_details,
theme: window.theme(),
}; };
window window
.as_global_scope() .as_global_scope()

View file

@ -80,7 +80,6 @@ use style::dom::OpaqueNode;
use style::error_reporting::{ContextualParseError, ParseErrorReporter}; use style::error_reporting::{ContextualParseError, ParseErrorReporter};
use style::properties::PropertyId; use style::properties::PropertyId;
use style::properties::style_structs::Font; use style::properties::style_structs::Font;
use style::queries::values::PrefersColorScheme;
use style::selector_parser::PseudoElement; use style::selector_parser::PseudoElement;
use style::str::HTML_SPACE_CHARACTERS; use style::str::HTML_SPACE_CHARACTERS;
use style::stylesheets::UrlExtraData; use style::stylesheets::UrlExtraData;
@ -269,7 +268,7 @@ pub(crate) struct Window {
/// Platform theme. /// Platform theme.
#[no_trace] #[no_trace]
theme: Cell<PrefersColorScheme>, theme: Cell<Theme>,
/// Parent id associated with this page, if any. /// Parent id associated with this page, if any.
#[no_trace] #[no_trace]
@ -2739,13 +2738,13 @@ impl Window {
self.viewport_details.get() self.viewport_details.get()
} }
/// Get the theme of this [`Window`].
pub(crate) fn theme(&self) -> Theme {
self.theme.get()
}
/// Handle a theme change request, triggering a reflow is any actual change occured. /// Handle a theme change request, triggering a reflow is any actual change occured.
pub(crate) fn handle_theme_change(&self, new_theme: Theme) { pub(crate) fn handle_theme_change(&self, new_theme: Theme) {
let new_theme = match new_theme {
Theme::Light => PrefersColorScheme::Light,
Theme::Dark => PrefersColorScheme::Dark,
};
if self.theme.get() == new_theme { if self.theme.get() == new_theme {
return; return;
} }
@ -3033,6 +3032,7 @@ impl Window {
player_context: WindowGLContext, player_context: WindowGLContext,
#[cfg(feature = "webgpu")] gpu_id_hub: Arc<IdentityHub>, #[cfg(feature = "webgpu")] gpu_id_hub: Arc<IdentityHub>,
inherited_secure_context: Option<bool>, inherited_secure_context: Option<bool>,
theme: Theme,
) -> DomRoot<Self> { ) -> DomRoot<Self> {
let error_reporter = CSSErrorReporter { let error_reporter = CSSErrorReporter {
pipelineid: pipeline_id, pipelineid: pipeline_id,
@ -3118,7 +3118,7 @@ impl Window {
throttled: Cell::new(false), throttled: Cell::new(false),
layout_marker: DomRefCell::new(Rc::new(Cell::new(true))), layout_marker: DomRefCell::new(Rc::new(Cell::new(true))),
current_event: DomRefCell::new(None), current_event: DomRefCell::new(None),
theme: Cell::new(PrefersColorScheme::Light), theme: Cell::new(theme),
trusted_types: Default::default(), trusted_types: Default::default(),
}); });

View file

@ -329,6 +329,9 @@ impl WindowProxy {
opener: Some(self.browsing_context_id), opener: Some(self.browsing_context_id),
load_data, load_data,
viewport_details: window.viewport_details(), viewport_details: window.viewport_details(),
// Use the current `WebView`'s theme initially, but the embedder may
// change this later.
theme: window.theme(),
}; };
ScriptThread::process_attach_layout(new_layout_info, document.origin().clone()); ScriptThread::process_attach_layout(new_layout_info, document.origin().clone());
// TODO: if noopener is false, copy the sessionStorage storage area of the creator origin. // TODO: if noopener is false, copy the sessionStorage storage area of the creator origin.

View file

@ -12,7 +12,7 @@ use base::cross_process_instant::CrossProcessInstant;
use base::id::{BrowsingContextId, PipelineId, WebViewId}; use base::id::{BrowsingContextId, PipelineId, WebViewId};
use constellation_traits::LoadData; use constellation_traits::LoadData;
use crossbeam_channel::Sender; use crossbeam_channel::Sender;
use embedder_traits::ViewportDetails; use embedder_traits::{Theme, ViewportDetails};
use http::header; use http::header;
use net_traits::request::{ use net_traits::request::{
CredentialsMode, InsecureRequestsPolicy, RedirectMode, RequestBuilder, RequestMode, CredentialsMode, InsecureRequestsPolicy, RedirectMode, RequestBuilder, RequestMode,
@ -159,6 +159,9 @@ pub(crate) struct InProgressLoad {
/// this load. /// this load.
#[no_trace] #[no_trace]
pub(crate) url_list: Vec<ServoUrl>, pub(crate) url_list: Vec<ServoUrl>,
/// The [`Theme`] to use for this page, once it loads.
#[no_trace]
pub(crate) theme: Theme,
} }
impl InProgressLoad { impl InProgressLoad {
@ -171,6 +174,7 @@ impl InProgressLoad {
parent_info: Option<PipelineId>, parent_info: Option<PipelineId>,
opener: Option<BrowsingContextId>, opener: Option<BrowsingContextId>,
viewport_details: ViewportDetails, viewport_details: ViewportDetails,
theme: Theme,
origin: MutableOrigin, origin: MutableOrigin,
load_data: LoadData, load_data: LoadData,
) -> InProgressLoad { ) -> InProgressLoad {
@ -189,6 +193,7 @@ impl InProgressLoad {
canceller: Default::default(), canceller: Default::default(),
load_data, load_data,
url_list: vec![url], url_list: vec![url],
theme,
} }
} }

View file

@ -403,14 +403,20 @@ impl ScriptThreadFactory for ScriptThread {
WebViewId::install(state.webview_id); WebViewId::install(state.webview_id);
let roots = RootCollection::new(); let roots = RootCollection::new();
let _stack_roots = ThreadLocalStackRoots::new(&roots); let _stack_roots = ThreadLocalStackRoots::new(&roots);
let id = state.id;
let browsing_context_id = state.browsing_context_id;
let webview_id = state.webview_id;
let parent_info = state.parent_info;
let opener = state.opener;
let memory_profiler_sender = state.memory_profiler_sender.clone(); let memory_profiler_sender = state.memory_profiler_sender.clone();
let viewport_details = state.viewport_details;
let in_progress_load = InProgressLoad::new(
state.id,
state.browsing_context_id,
state.webview_id,
state.parent_info,
state.opener,
state.viewport_details,
state.theme,
MutableOrigin::new(load_data.url.origin()),
load_data,
);
let reporter_name = format!("script-reporter-{:?}", state.id);
let script_thread = ScriptThread::new(state, layout_factory, system_font_service); let script_thread = ScriptThread::new(state, layout_factory, system_font_service);
SCRIPT_THREAD_ROOT.with(|root| { SCRIPT_THREAD_ROOT.with(|root| {
@ -419,19 +425,8 @@ impl ScriptThreadFactory for ScriptThread {
let mut failsafe = ScriptMemoryFailsafe::new(&script_thread); let mut failsafe = ScriptMemoryFailsafe::new(&script_thread);
let origin = MutableOrigin::new(load_data.url.origin()); script_thread.pre_page_load(in_progress_load);
script_thread.pre_page_load(InProgressLoad::new(
id,
browsing_context_id,
webview_id,
parent_info,
opener,
viewport_details,
origin,
load_data,
));
let reporter_name = format!("script-reporter-{:?}", id);
memory_profiler_sender.run_with_memory_reporting( memory_profiler_sender.run_with_memory_reporting(
|| { || {
script_thread.start(CanGc::note()); script_thread.start(CanGc::note());
@ -2435,6 +2430,7 @@ impl ScriptThread {
opener, opener,
load_data, load_data,
viewport_details, viewport_details,
theme,
} = new_layout_info; } = new_layout_info;
// Kick off the fetch for the new resource. // Kick off the fetch for the new resource.
@ -2446,6 +2442,7 @@ impl ScriptThread {
parent_info, parent_info,
opener, opener,
viewport_details, viewport_details,
theme,
origin, origin,
load_data, load_data,
); );
@ -3189,6 +3186,7 @@ impl ScriptThread {
time_profiler_chan: self.senders.time_profiler_sender.clone(), time_profiler_chan: self.senders.time_profiler_sender.clone(),
compositor_api: self.compositor_api.clone(), compositor_api: self.compositor_api.clone(),
viewport_details: incomplete.viewport_details, viewport_details: incomplete.viewport_details,
theme: incomplete.theme,
}; };
// Create the window and document objects. // Create the window and document objects.
@ -3228,6 +3226,7 @@ impl ScriptThread {
#[cfg(feature = "webgpu")] #[cfg(feature = "webgpu")]
self.gpu_id_hub.clone(), self.gpu_id_hub.clone(),
incomplete.load_data.inherited_secure_context, incomplete.load_data.inherited_secure_context,
incomplete.theme,
); );
let _realm = enter_realm(&*window); let _realm = enter_realm(&*window);

View file

@ -17,14 +17,21 @@ use std::rc::Rc;
use anyhow::ensure; use anyhow::ensure;
use common::{ServoTest, run_api_tests}; use common::{ServoTest, run_api_tests};
use servo::{ use servo::{
JSValue, JavaScriptEvaluationError, LoadStatus, WebView, WebViewBuilder, WebViewDelegate, JSValue, JavaScriptEvaluationError, LoadStatus, Theme, WebView, WebViewBuilder, WebViewDelegate,
}; };
use url::Url;
#[derive(Default)] #[derive(Default)]
struct WebViewDelegateImpl { struct WebViewDelegateImpl {
url_changed: Cell<bool>, url_changed: Cell<bool>,
} }
impl WebViewDelegateImpl {
pub(crate) fn reset(&self) {
self.url_changed.set(false);
}
}
impl WebViewDelegate for WebViewDelegateImpl { impl WebViewDelegate for WebViewDelegateImpl {
fn notify_url_changed(&self, _webview: servo::WebView, _url: url::Url) { fn notify_url_changed(&self, _webview: servo::WebView, _url: url::Url) {
self.url_changed.set(true); self.url_changed.set(true);
@ -128,10 +135,40 @@ fn test_create_webview_and_immediately_drop_webview_before_shutdown(
Ok(()) Ok(())
} }
fn test_theme_change(servo_test: &ServoTest) -> Result<(), anyhow::Error> {
let delegate = Rc::new(WebViewDelegateImpl::default());
let webview = WebViewBuilder::new(servo_test.servo())
.delegate(delegate.clone())
.url(Url::parse("data:text/html,page one").unwrap())
.build();
let is_dark_theme_script = "window.matchMedia('(prefers-color-scheme: dark)').matches";
// The default theme is "light".
let result = evaluate_javascript(servo_test, webview.clone(), is_dark_theme_script);
ensure!(result == Ok(JSValue::Boolean(false)));
// Changing the theme updates the current page.
webview.notify_theme_change(Theme::Dark);
let result = evaluate_javascript(servo_test, webview.clone(), is_dark_theme_script);
ensure!(result == Ok(JSValue::Boolean(true)));
delegate.reset();
webview.load(Url::parse("data:text/html,page two").unwrap());
servo_test.spin(move || Ok(!delegate.url_changed.get()))?;
// The theme persists after a navigation.
let result = evaluate_javascript(servo_test, webview.clone(), is_dark_theme_script);
ensure!(result == Ok(JSValue::Boolean(true)));
Ok(())
}
fn main() { fn main() {
run_api_tests!( run_api_tests!(
test_create_webview, test_create_webview,
test_evaluate_javascript_basic, test_evaluate_javascript_basic,
test_theme_change,
// This test needs to be last, as it tests creating and dropping // This test needs to be last, as it tests creating and dropping
// a WebView right before shutdown. // a WebView right before shutdown.
test_create_webview_and_immediately_drop_webview_before_shutdown test_create_webview_and_immediately_drop_webview_before_shutdown

View file

@ -395,7 +395,10 @@ impl WebView {
pub fn notify_theme_change(&self, theme: Theme) { pub fn notify_theme_change(&self, theme: Theme) {
self.inner() self.inner()
.constellation_proxy .constellation_proxy
.send(EmbedderToConstellationMessage::ThemeChange(theme)) .send(EmbedderToConstellationMessage::ThemeChange(
self.id(),
theme,
))
} }
pub fn load(&self, url: Url) { pub fn load(&self, url: Url) {

View file

@ -16,7 +16,7 @@ use canvas_traits::canvas::{CanvasId, CanvasMsg};
use devtools_traits::{DevtoolScriptControlMsg, ScriptToDevtoolsControlMsg, WorkerId}; use devtools_traits::{DevtoolScriptControlMsg, ScriptToDevtoolsControlMsg, WorkerId};
use embedder_traits::{ use embedder_traits::{
AnimationState, EmbedderMsg, FocusSequenceNumber, JSValue, JavaScriptEvaluationError, AnimationState, EmbedderMsg, FocusSequenceNumber, JSValue, JavaScriptEvaluationError,
JavaScriptEvaluationId, MediaSessionEvent, TouchEventResult, ViewportDetails, JavaScriptEvaluationId, MediaSessionEvent, Theme, TouchEventResult, ViewportDetails,
WebDriverMessageId, WebDriverMessageId,
}; };
use euclid::default::Size2D as UntypedSize2D; use euclid::default::Size2D as UntypedSize2D;
@ -417,6 +417,8 @@ pub struct IFrameLoadInfoWithData {
pub sandbox: IFrameSandboxState, pub sandbox: IFrameSandboxState,
/// The initial viewport size for this iframe. /// The initial viewport size for this iframe.
pub viewport_details: ViewportDetails, pub viewport_details: ViewportDetails,
/// The [`Theme`] to use within this iframe.
pub theme: Theme,
} }
/// Resources required by workerglobalscopes /// Resources required by workerglobalscopes

View file

@ -55,7 +55,7 @@ pub enum EmbedderToConstellationMessage {
/// Inform the Constellation that a `WebView`'s [`ViewportDetails`] have changed. /// Inform the Constellation that a `WebView`'s [`ViewportDetails`] have changed.
ChangeViewportDetails(WebViewId, ViewportDetails, WindowSizeType), ChangeViewportDetails(WebViewId, ViewportDetails, WindowSizeType),
/// Inform the constellation of a theme change. /// Inform the constellation of a theme change.
ThemeChange(Theme), ThemeChange(WebViewId, Theme),
/// Requests that the constellation instruct script/layout to try to layout again and tick /// Requests that the constellation instruct script/layout to try to layout again and tick
/// animations. /// animations.
TickAnimation(Vec<WebViewId>), TickAnimation(Vec<WebViewId>),

View file

@ -35,6 +35,7 @@ serde = { workspace = true }
servo_url = { path = "../../url" } servo_url = { path = "../../url" }
strum_macros = { workspace = true } strum_macros = { workspace = true }
stylo_traits = { workspace = true } stylo_traits = { workspace = true }
stylo = { workspace = true }
url = { workspace = true } url = { workspace = true }
webdriver = { workspace = true } webdriver = { workspace = true }
webrender_api = { workspace = true } webrender_api = { workspace = true }

View file

@ -34,6 +34,7 @@ use pixels::Image;
use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde::{Deserialize, Deserializer, Serialize, Serializer};
use servo_url::ServoUrl; use servo_url::ServoUrl;
use strum_macros::IntoStaticStr; use strum_macros::IntoStaticStr;
use style::queries::values::PrefersColorScheme;
use style_traits::CSSPixel; use style_traits::CSSPixel;
use url::Url; use url::Url;
use webrender_api::units::{DeviceIntPoint, DeviceIntRect, DeviceIntSize, DevicePixel}; use webrender_api::units::{DeviceIntPoint, DeviceIntRect, DeviceIntSize, DevicePixel};
@ -598,6 +599,16 @@ pub enum Theme {
/// Dark theme. /// Dark theme.
Dark, Dark,
} }
impl From<Theme> for PrefersColorScheme {
fn from(value: Theme) -> Self {
match value {
Theme::Light => PrefersColorScheme::Light,
Theme::Dark => PrefersColorScheme::Dark,
}
}
}
// The type of MediaSession action. // The type of MediaSession action.
/// <https://w3c.github.io/mediasession/#enumdef-mediasessionaction> /// <https://w3c.github.io/mediasession/#enumdef-mediasessionaction>
#[derive(Clone, Debug, Deserialize, Eq, Hash, MallocSizeOf, PartialEq, Serialize)] #[derive(Clone, Debug, Deserialize, Eq, Hash, MallocSizeOf, PartialEq, Serialize)]

View file

@ -68,6 +68,8 @@ pub struct NewLayoutInfo {
pub load_data: LoadData, pub load_data: LoadData,
/// Initial [`ViewportDetails`] for this layout. /// Initial [`ViewportDetails`] for this layout.
pub viewport_details: ViewportDetails, pub viewport_details: ViewportDetails,
/// The [`Theme`] of the new layout.
pub theme: Theme,
} }
/// When a pipeline is closed, should its browsing context be discarded too? /// When a pipeline is closed, should its browsing context be discarded too?
@ -321,6 +323,8 @@ pub struct InitialScriptState {
pub devtools_server_sender: Option<IpcSender<ScriptToDevtoolsControlMsg>>, pub devtools_server_sender: Option<IpcSender<ScriptToDevtoolsControlMsg>>,
/// Initial [`ViewportDetails`] for the frame that is initiating this `ScriptThread`. /// Initial [`ViewportDetails`] for the frame that is initiating this `ScriptThread`.
pub viewport_details: ViewportDetails, pub viewport_details: ViewportDetails,
/// Initial [`Theme`] for the frame that is initiating this `ScriptThread`.
pub theme: Theme,
/// The ID of the pipeline namespace for this script thread. /// The ID of the pipeline namespace for this script thread.
pub pipeline_namespace_id: PipelineNamespaceId, pub pipeline_namespace_id: PipelineNamespaceId,
/// A ping will be sent on this channel once the script thread shuts down. /// A ping will be sent on this channel once the script thread shuts down.

View file

@ -20,7 +20,7 @@ use base::Epoch;
use base::id::{BrowsingContextId, PipelineId, WebViewId}; use base::id::{BrowsingContextId, PipelineId, WebViewId};
use compositing_traits::CrossProcessCompositorApi; use compositing_traits::CrossProcessCompositorApi;
use constellation_traits::{LoadData, ScrollState}; use constellation_traits::{LoadData, ScrollState};
use embedder_traits::{UntrustedNodeAddress, ViewportDetails}; use embedder_traits::{Theme, UntrustedNodeAddress, ViewportDetails};
use euclid::default::{Point2D, Rect}; use euclid::default::{Point2D, Rect};
use fnv::FnvHashMap; use fnv::FnvHashMap;
use fonts::{FontContext, SystemFontServiceProxy}; use fonts::{FontContext, SystemFontServiceProxy};
@ -46,7 +46,6 @@ use style::invalidation::element::restyle_hints::RestyleHint;
use style::media_queries::Device; use style::media_queries::Device;
use style::properties::PropertyId; use style::properties::PropertyId;
use style::properties::style_structs::Font; use style::properties::style_structs::Font;
use style::queries::values::PrefersColorScheme;
use style::selector_parser::{PseudoElement, RestyleDamage, Snapshot}; use style::selector_parser::{PseudoElement, RestyleDamage, Snapshot};
use style::stylesheets::Stylesheet; use style::stylesheets::Stylesheet;
use webrender_api::ImageKey; use webrender_api::ImageKey;
@ -182,6 +181,7 @@ pub struct LayoutConfig {
pub time_profiler_chan: time::ProfilerChan, pub time_profiler_chan: time::ProfilerChan,
pub compositor_api: CrossProcessCompositorApi, pub compositor_api: CrossProcessCompositorApi,
pub viewport_details: ViewportDetails, pub viewport_details: ViewportDetails,
pub theme: Theme,
} }
pub trait LayoutFactory: Send + Sync { pub trait LayoutFactory: Send + Sync {
@ -428,7 +428,7 @@ pub struct ReflowRequest {
/// The set of image animations. /// The set of image animations.
pub node_to_image_animation_map: FxHashMap<OpaqueNode, ImageAnimationState>, pub node_to_image_animation_map: FxHashMap<OpaqueNode, ImageAnimationState>,
/// The theme for the window /// The theme for the window
pub theme: PrefersColorScheme, pub theme: Theme,
/// The node highlighted by the devtools, if any /// The node highlighted by the devtools, if any
pub highlighted_dom_node: Option<OpaqueNode>, pub highlighted_dom_node: Option<OpaqueNode>,
} }