forked from
me.webbeef.org/browser.html
Rewild Your Web
1--- original
2+++ modified
3@@ -0,0 +1,387 @@
4+/* This Source Code Form is subject to the terms of the Mozilla Public
5+ * License, v. 2.0. If a copy of the MPL was not distributed with this
6+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
7+
8+//! Extensible preference system for embedders.
9+//!
10+//! This module provides a trait and helper functions that allow embedders to define
11+//! their own type-safe preferences using the `#[derive(EmbedderPreferences)]` macro.
12+//!
13+//! # Example
14+//!
15+//! ```rust,ignore
16+//! use servo_config_macro::EmbedderPreferences;
17+//! use serde::{Deserialize, Serialize};
18+//!
19+//! #[derive(Clone, Debug, Deserialize, Serialize, EmbedderPreferences)]
20+//! #[namespace = "myembedder"]
21+//! pub struct MyPreferences {
22+//! pub theme: String,
23+//! pub enable_feature: bool,
24+//! }
25+//!
26+//! impl Default for MyPreferences {
27+//! fn default() -> Self {
28+//! Self {
29+//! theme: "system".to_string(),
30+//! enable_feature: false,
31+//! }
32+//! }
33+//! }
34+//!
35+//! // Generate global storage and accessors
36+//! servo_config::define_embedder_prefs!(MyPreferences);
37+//!
38+//! // Load from file (merges with defaults)
39+//! prefs::load(Path::new("~/.config/myembedder/prefs.json"));
40+//!
41+//! // Save to file
42+//! prefs::save(Path::new("~/.config/myembedder/prefs.json"));
43+//! ```
44+
45+use std::collections::HashMap;
46+use std::path::Path;
47+use std::sync::{LazyLock, RwLock};
48+use std::{fs, io};
49+
50+use serde::Serialize;
51+use serde::de::DeserializeOwned;
52+
53+use crate::pref_util::PrefValue;
54+use crate::prefs::OBSERVERS;
55+
56+/// Type for callbacks that handle preference changes from JavaScript.
57+/// The callback receives the local preference name (without namespace) and the new value.
58+pub type EmbedderPrefSetterCallback = fn(&str, PrefValue);
59+
60+/// Registry of callbacks for embedder preference setters, keyed by namespace.
61+static SETTER_CALLBACKS: LazyLock<RwLock<HashMap<&'static str, EmbedderPrefSetterCallback>>> =
62+ LazyLock::new(|| RwLock::new(HashMap::new()));
63+
64+/// Register a callback to be invoked when preferences in the given namespace
65+/// are set via ServoInternals (JavaScript API).
66+///
67+/// This allows embedders to update their local preference storage when
68+/// preferences are changed from JavaScript.
69+pub fn register_setter_callback(namespace: &'static str, callback: EmbedderPrefSetterCallback) {
70+ SETTER_CALLBACKS
71+ .write()
72+ .unwrap()
73+ .insert(namespace, callback);
74+}
75+
76+/// Set an embedder preference from JavaScript (via ServoInternals).
77+///
78+/// This function:
79+/// 1. Parses the namespaced name to extract namespace and local name
80+/// 2. Updates the embedder prefs registry
81+/// 3. Invokes the registered callback for the namespace (if any)
82+/// 4. Notifies all observers
83+///
84+/// Use this function when preferences are set from JavaScript.
85+/// For Rust-side changes, use `prefs::set()` instead which handles everything.
86+pub fn set_embedder_pref_from_script(full_name: &str, value: PrefValue) {
87+ // Parse namespace from "browserhtml.theme" -> ("browserhtml", "theme")
88+ let Some((namespace, local_name)) = full_name.split_once('.') else {
89+ // Not a namespaced preference, just update the registry
90+ crate::prefs::set_embedder_pref(full_name, value);
91+ return;
92+ };
93+
94+ // Update the registry
95+ crate::prefs::set_embedder_pref(full_name, value.clone());
96+
97+ // Invoke the callback for this namespace if registered
98+ if let Some(callback) = SETTER_CALLBACKS.read().unwrap().get(namespace) {
99+ callback(local_name, value.clone());
100+ }
101+
102+ // Notify observers with the full namespaced name
103+ let static_name = Box::leak(full_name.to_string().into_boxed_str()) as &'static str;
104+ let changes = [(static_name, value)];
105+ for observer in &*OBSERVERS.read().unwrap() {
106+ observer.prefs_changed(&changes);
107+ }
108+}
109+
110+/// Trait that embedder preference structs must implement.
111+///
112+/// This trait is automatically implemented by the `#[derive(EmbedderPreferences)]` macro.
113+/// It provides a common interface for accessing and modifying embedder preferences,
114+/// as well as computing diffs for observer notifications.
115+pub trait EmbedderPreferences: Send + Sync + Clone + 'static {
116+ /// The namespace prefix for this preference set (e.g., "browserhtml").
117+ ///
118+ /// All preference names will be prefixed with `"{namespace}."` when reported
119+ /// to observers. This prevents collisions with Servo's core preferences and
120+ /// other embedders' preferences.
121+ fn namespace() -> &'static str;
122+
123+ /// Get a preference value by its local name (without namespace prefix).
124+ ///
125+ /// Returns `None` if the preference name is not recognized.
126+ fn get_value(&self, name: &str) -> Option<PrefValue>;
127+
128+ /// Set a preference value by its local name (without namespace prefix).
129+ ///
130+ /// Returns `true` if the preference was found and set successfully,
131+ /// `false` otherwise.
132+ fn set_value(&mut self, name: &str, value: PrefValue) -> bool;
133+
134+ /// Compute the diff between this preferences instance and another.
135+ ///
136+ /// Returns a list of `(local_name, new_value)` pairs for preferences
137+ /// that differ between `self` and `other`.
138+ fn diff(&self, other: &Self) -> Vec<(&'static str, PrefValue)>;
139+
140+ /// List all preference names in this struct (local names, without namespace).
141+ fn pref_names() -> &'static [&'static str];
142+}
143+
144+/// Error type for preference persistence operations.
145+#[derive(Debug)]
146+pub enum PrefsPersistError {
147+ /// I/O error reading or writing the file.
148+ Io(io::Error),
149+ /// JSON serialization/deserialization error.
150+ Json(serde_json::Error),
151+}
152+
153+impl std::fmt::Display for PrefsPersistError {
154+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
155+ match self {
156+ PrefsPersistError::Io(e) => write!(f, "I/O error: {}", e),
157+ PrefsPersistError::Json(e) => write!(f, "JSON error: {}", e),
158+ }
159+ }
160+}
161+
162+impl std::error::Error for PrefsPersistError {
163+ fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
164+ match self {
165+ PrefsPersistError::Io(e) => Some(e),
166+ PrefsPersistError::Json(e) => Some(e),
167+ }
168+ }
169+}
170+
171+impl From<io::Error> for PrefsPersistError {
172+ fn from(e: io::Error) -> Self {
173+ PrefsPersistError::Io(e)
174+ }
175+}
176+
177+impl From<serde_json::Error> for PrefsPersistError {
178+ fn from(e: serde_json::Error) -> Self {
179+ PrefsPersistError::Json(e)
180+ }
181+}
182+
183+/// Load embedder preferences from a JSON file.
184+///
185+/// Returns the default value if a i/o or json error occurs.
186+pub fn load_from_path<T: DeserializeOwned + Default>(path: &Path) -> T {
187+ if !path.exists() {
188+ return T::default();
189+ }
190+ let Ok(contents) = fs::read_to_string(path) else {
191+ return T::default();
192+ };
193+ serde_json::from_str(&contents).unwrap_or_default()
194+}
195+
196+/// Save embedder preferences to a JSON file.
197+///
198+/// Creates parent directories if they don't exist.
199+pub fn save_to_path<T: Serialize>(prefs: &T, path: &Path) -> Result<(), PrefsPersistError> {
200+ if let Some(parent) = path.parent() {
201+ if !parent.exists() {
202+ fs::create_dir_all(parent)?;
203+ }
204+ }
205+ let contents = serde_json::to_string_pretty(prefs)?;
206+ fs::write(path, contents)?;
207+ Ok(())
208+}
209+
210+/// Notify observers of embedder preference changes with namespaced keys.
211+///
212+/// This function computes the diff between the old and new preference values,
213+/// prepends the namespace to each changed preference name, updates the embedder
214+/// preference registry, and notifies all registered observers.
215+///
216+/// # Example
217+///
218+/// ```rust,ignore
219+/// let old_prefs = prefs::get().clone();
220+/// let mut new_prefs = old_prefs.clone();
221+/// new_prefs.theme = "dark".to_string();
222+/// notify_embedder_pref_changes(&old_prefs, &new_prefs);
223+/// // Observers receive: [("browserhtml.theme", PrefValue::Str("dark"))]
224+/// ```
225+pub fn notify_embedder_pref_changes<T: EmbedderPreferences>(old: &T, new: &T) {
226+ let changes = new.diff(old);
227+ if changes.is_empty() {
228+ return;
229+ }
230+
231+ let namespace = T::namespace();
232+ // Create namespaced changes: "browserhtml.theme" instead of "theme"
233+ let namespaced: Vec<_> = changes
234+ .into_iter()
235+ .map(|(name, value)| {
236+ let full_name = Box::leak(format!("{}.{}", namespace, name).into_boxed_str());
237+ // Update the embedder prefs registry so ServoInternals can access these
238+ crate::prefs::set_embedder_pref(full_name, value.clone());
239+ (full_name as &'static str, value)
240+ })
241+ .collect();
242+
243+ for observer in &*OBSERVERS.read().unwrap() {
244+ observer.prefs_changed(&namespaced);
245+ }
246+}
247+
248+/// Sync all embedder preferences to the registry.
249+///
250+/// This should be called after loading preferences to ensure the registry
251+/// is up-to-date with all current embedder preference values.
252+pub fn sync_to_registry<T: EmbedderPreferences>(prefs: &T) {
253+ let namespace = T::namespace();
254+ for name in T::pref_names() {
255+ if let Some(value) = prefs.get_value(name) {
256+ let full_name = format!("{}.{}", namespace, name);
257+ crate::prefs::set_embedder_pref(&full_name, value);
258+ }
259+ }
260+}
261+
262+/// Macro to define embedder preference storage with accessor functions.
263+///
264+/// This macro generates:
265+/// - `get()` - Read access to current preferences
266+/// - `set(prefs)` - Update preferences, notify observers, and auto-save if a path was set
267+/// - `load(path)` - Load preferences from a JSON file and remember path for auto-save
268+/// - `save(path)` - Save current preferences to a JSON file
269+/// - A callback for JavaScript-initiated preference changes
270+///
271+/// # Example
272+///
273+/// ```rust,ignore
274+/// // In your embedder's prefs module:
275+/// servo_config::define_embedder_prefs!(MyPreferences);
276+///
277+/// // Load from file at startup (also enables auto-save on changes)
278+/// if let Err(e) = prefs::load(Path::new("prefs.json")) {
279+/// eprintln!("Failed to load preferences: {}", e);
280+/// }
281+///
282+/// // Read preferences
283+/// let theme = prefs::get().theme.clone();
284+///
285+/// // Update preferences (notifies observers and auto-saves to the loaded path)
286+/// let mut new_prefs = (*prefs::get()).clone();
287+/// new_prefs.theme = "dark".to_string();
288+/// prefs::set(new_prefs);
289+/// ```
290+#[macro_export]
291+macro_rules! define_embedder_prefs {
292+ ($prefs_type:ty) => {
293+ use std::path::{Path, PathBuf};
294+ use std::sync::{RwLock, RwLockReadGuard};
295+
296+ static PREFS: RwLock<$prefs_type> = RwLock::new(<$prefs_type>::DEFAULT);
297+ static PREFS_PATH: RwLock<Option<PathBuf>> = RwLock::new(None);
298+
299+ /// Get the current set of embedder preferences.
300+ pub fn get() -> RwLockReadGuard<'static, $prefs_type> {
301+ PREFS.read().unwrap()
302+ }
303+
304+ /// Set the embedder preferences, notify observers of changes, and auto-save.
305+ ///
306+ /// If preferences were previously loaded with `load()`, this will automatically
307+ /// save the updated preferences to the same file.
308+ pub fn set(prefs: $prefs_type) {
309+ let old = PREFS.read().unwrap().clone();
310+ *PREFS.write().unwrap() = prefs.clone();
311+ $crate::embedder_prefs::notify_embedder_pref_changes(&old, &prefs);
312+
313+ // Auto-save if we have a path
314+ if let Some(ref path) = *PREFS_PATH.read().unwrap() {
315+ if let Err(e) = $crate::embedder_prefs::save_to_path(&prefs, path) {
316+ log::warn!("Failed to auto-save preferences: {}", e);
317+ }
318+ }
319+ }
320+
321+ /// Internal function to set a single preference value from JavaScript.
322+ /// Called by the registered setter callback.
323+ fn set_single_pref_from_script(local_name: &str, value: $crate::pref_util::PrefValue) {
324+ use $crate::embedder_prefs::EmbedderPreferences;
325+
326+ let mut prefs = PREFS.write().unwrap();
327+ if prefs.set_value(local_name, value) {
328+ // Auto-save if we have a path
329+ if let Some(ref path) = *PREFS_PATH.read().unwrap() {
330+ if let Err(e) = $crate::embedder_prefs::save_to_path(&*prefs, path) {
331+ log::warn!("Failed to auto-save preferences: {}", e);
332+ }
333+ }
334+ }
335+ }
336+
337+ /// Load embedder preferences from a JSON file.
338+ ///
339+ /// If the file exists and is valid, updates the global preferences
340+ /// and notifies observers of any changes. If the file doesn't exist,
341+ /// the current preferences are left unchanged (but the directory is created).
342+ ///
343+ /// The path is remembered for auto-save: subsequent calls to `set()`
344+ /// will automatically save to this path.
345+ ///
346+ /// After loading (or using defaults), all preferences are synced to the
347+ /// embedder prefs registry so they can be accessed via ServoInternals.
348+ ///
349+ /// Also registers a callback for JavaScript-initiated preference changes.
350+ pub fn load(path: &Path) -> Result<(), $crate::embedder_prefs::PrefsPersistError> {
351+ use $crate::embedder_prefs::EmbedderPreferences;
352+
353+ // Create the config directory if it doesn't exist
354+ if let Some(parent) = path.parent() {
355+ if !parent.exists() {
356+ std::fs::create_dir_all(parent)?;
357+ }
358+ }
359+
360+ // Remember the path for auto-save
361+ *PREFS_PATH.write().unwrap() = Some(path.to_path_buf());
362+
363+ let prefs = $crate::embedder_prefs::load_from_path::<$prefs_type>(path);
364+ // Use internal set to avoid double-save
365+ let old = PREFS.read().unwrap().clone();
366+ *PREFS.write().unwrap() = prefs.clone();
367+ $crate::embedder_prefs::notify_embedder_pref_changes(&old, &prefs);
368+
369+ // Sync all current preferences to the registry (including defaults)
370+ // so they're accessible via ServoInternals
371+ $crate::embedder_prefs::sync_to_registry(&*PREFS.read().unwrap());
372+
373+ // Register callback for JavaScript-initiated preference changes
374+ $crate::embedder_prefs::register_setter_callback(
375+ <$prefs_type as EmbedderPreferences>::namespace(),
376+ set_single_pref_from_script,
377+ );
378+
379+ Ok(())
380+ }
381+
382+ /// Save the current embedder preferences to a JSON file.
383+ ///
384+ /// Creates parent directories if they don't exist.
385+ pub fn save(path: &Path) -> Result<(), $crate::embedder_prefs::PrefsPersistError> {
386+ let prefs = PREFS.read().unwrap();
387+ $crate::embedder_prefs::save_to_path(&*prefs, path)
388+ }
389+ };
390+}