web browser (Servo)
git clone https://git.lucas.co/cce-browser.git
feat: report a color scheme to pages, defaulting to dark
Sites ship dark stylesheets behind `prefers-color-scheme: dark`, and
nothing was ever telling Servo which scheme to prefer, so every page
rendered its light theme. A new browser.color-scheme key ("dark" |
"light", dark by default, edited from the system-interface Browser page)
drives WebView::notify_theme_change, which is exactly the
PrefersColorScheme signal.
The theme is per-WebView, not per-Servo, so ServoHost holds it and three
paths have to apply it: build_webview for tabs it creates, the pump adopt
loop for page-opened (target=_blank / window.open) webviews the delegate
builds, and set_color_scheme re-notifying every open tab when the config
changes on focus regain. Miss one and the setting silently applies to
some tabs only.
Shadow-verified against a page that keys both CSS and matchMedia off the
query: default reports dark (matchMedia true, dark rules applied),
flipping the config to light repaints the open page light on focus
regain, and a tab opened by target=_blank comes up dark too.
Note this is only the signal — sites without a dark theme are unchanged.
Co-Authored-By: Claude Opus 5 <[email protected]>
src/main.rs | 2 ++
src/settings.rs | 33 +++++++++++++++++++++++++++++++++
src/webview.rs | 22 +++++++++++++++++++---
3 files changed, 54 insertions(+), 3 deletions(-)
diff --git a/src/main.rs b/src/main.rs
index fdda1a6..c6a8b7e 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -344,6 +344,7 @@ impl BrowserApp {
}
downloads::set_download_dir(new.download_dir.clone());
self.host.set_history_enabled(new.history);
+ self.host.set_color_scheme(new.color_scheme.into());
self.settings = new;
true
}
@@ -548,6 +549,7 @@ impl Application for BrowserApp {
let cursor = url_input.len();
let mut host = ServoHost::new(sender, url, (1200, 800));
host.set_history_enabled(settings.history);
+ host.set_color_scheme(settings.color_scheme.into());
Self {
host,
settings,
diff --git a/src/settings.rs b/src/settings.rs
index e214a19..9c9baa2 100644
--- a/src/settings.rs
+++ b/src/settings.rs
@@ -27,6 +27,35 @@ impl BarPosition {
}
}
+/// The color scheme reported to pages as `prefers-color-scheme`. Sites that
+/// ship a dark stylesheet honor it; sites that don't are unaffected — this is
+/// a signal, not a filter over their colors.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
+pub enum ColorScheme {
+ #[default]
+ Dark,
+ Light,
+}
+
+impl ColorScheme {
+ /// Config keys as written by the system-interface Browser page.
+ fn from_key(key: &str) -> Self {
+ match key {
+ "light" => Self::Light,
+ _ => Self::Dark,
+ }
+ }
+}
+
+impl From<ColorScheme> for servo::Theme {
+ fn from(scheme: ColorScheme) -> Self {
+ match scheme {
+ ColorScheme::Dark => servo::Theme::Dark,
+ ColorScheme::Light => servo::Theme::Light,
+ }
+ }
+}
+
#[derive(Debug, Clone, PartialEq)]
pub struct Settings {
pub homepage: String,
@@ -38,6 +67,8 @@ pub struct Settings {
pub history: bool,
/// Window edge the utility bar floats against.
pub bar_position: BarPosition,
+ /// What pages are told to prefer.
+ pub color_scheme: ColorScheme,
}
impl Default for Settings {
@@ -48,6 +79,7 @@ impl Default for Settings {
download_dir: None,
history: true,
bar_position: BarPosition::Top,
+ color_scheme: ColorScheme::Dark,
}
}
}
@@ -88,5 +120,6 @@ pub fn load() -> Settings {
download_dir,
history: b["history"].as_bool().unwrap_or(true),
bar_position: BarPosition::from_key(b["bar-position"].as_str().unwrap_or("top")),
+ color_scheme: ColorScheme::from_key(b["color-scheme"].as_str().unwrap_or("dark")),
}
}
diff --git a/src/webview.rs b/src/webview.rs
index 51fe1bc..127a30f 100644
--- a/src/webview.rs
+++ b/src/webview.rs
@@ -20,7 +20,7 @@ use servo::{
CreateNewWebViewRequest, DeviceIntRect, DevicePoint, EventLoopWaker, InputEvent,
Key as DomKey, KeyState, KeyboardEvent, LoadStatus, MouseButton as DomMouseButton,
MouseButtonAction, MouseButtonEvent, MouseMoveEvent, NavigationRequest, RenderingContext,
- Servo, ServoBuilder, SoftwareRenderingContext, WebView, WebViewBuilder, WebViewDelegate,
+ Servo, ServoBuilder, SoftwareRenderingContext, Theme, WebView, WebViewBuilder, WebViewDelegate,
WebViewId, WheelDelta, WheelEvent, WheelMode,
};
use servo::protocol_handler::ProtocolRegistry;
@@ -156,12 +156,23 @@ pub struct ServoHost {
scale: f32,
/// Settings gate for cce://history recording.
history_enabled: bool,
+ /// What every webview reports as `prefers-color-scheme`. Held here
+ /// because the theme is per-webview: tabs opened later have to be told.
+ theme: Theme,
}
impl ServoHost {
pub fn set_history_enabled(&mut self, on: bool) {
self.history_enabled = on;
}
+
+ /// Set the color scheme pages see, now and for tabs opened later.
+ pub fn set_color_scheme(&mut self, theme: Theme) {
+ self.theme = theme;
+ for tab in &self.tabs {
+ tab.webview.notify_theme_change(theme);
+ }
+ }
}
impl ServoHost {
@@ -219,16 +230,19 @@ impl ServoHost {
size_px,
scale: 1.0,
history_enabled: true,
+ theme: Theme::Light,
};
host.open_tab(url);
host
}
fn build_webview(&self, url: Url) -> WebView {
- WebViewBuilder::new(&self.servo, self.context.clone())
+ let webview = WebViewBuilder::new(&self.servo, self.context.clone())
.url(url)
.delegate(self.delegate.clone())
- .build()
+ .build();
+ webview.notify_theme_change(self.theme);
+ webview
}
/// Open a new tab and make it active.
@@ -335,6 +349,8 @@ impl ServoHost {
// newest one takes focus.
let opened: Vec<WebView> = self.shared.pending_new.borrow_mut().drain(..).collect();
for webview in opened {
+ // Built by the delegate, so it has not been told the theme yet.
+ webview.notify_theme_change(self.theme);
self.tabs.push(Tab {
webview,
title: None,