git.lucas.co / cce-browser
web browser (Servo)
git clone https://git.lucas.co/cce-browser.git

commit4e86d2b8e793683b3ac6f54a932eabde456f0e80
parenteb131f90e2
authorLucas Galante <[email protected]>
date2026-08-24 11:54
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,