Rewild Your Web
at main 390 lines 15 kB view raw
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+}