diff --git a/components/config/opts.rs b/components/config/opts.rs index 6fd11bea1bf..47d92f986d5 100644 --- a/components/config/opts.rs +++ b/components/config/opts.rs @@ -5,7 +5,6 @@ //! Configuration options for a single run of the servo application. Created //! from command line arguments. -use crate::prefs::{self, PrefValue}; use euclid::Size2D; use getopts::{Matches, Options}; use servo_geometry::DeviceIndependentPixel; @@ -640,18 +639,6 @@ pub fn from_cmdline_args(mut opts: Options, args: &[String]) -> ArgumentParsingR "Run as a content process and connect to the given pipe", "servo-ipc-channel.abcdefg", ); - opts.optmulti( - "", - "pref", - "A preference to set to enable", - "dom.bluetooth.enabled", - ); - opts.optmulti( - "", - "pref", - "A preference to set to enable", - "dom.webgpu.enabled", - ); opts.optflag("b", "no-native-titlebar", "Do not use native titlebar"); opts.optflag("w", "webrender", "Use webrender backend"); opts.optopt("G", "graphics", "Select graphics backend (gl or es2)", "gl"); @@ -926,16 +913,6 @@ pub fn from_cmdline_args(mut opts: Options, args: &[String]) -> ArgumentParsingR set_options(opts); - // These must happen after setting the default options, since the prefs rely on - // on the resource path. - // Note that command line preferences have the highest precedence - - prefs::add_user_prefs(); - - for pref in opt_match.opt_strs("pref").iter() { - parse_pref_from_command_line(pref); - } - if let Some(layout_threads) = layout_threads { set_pref!(layout.threads, layout_threads as i64); } @@ -965,31 +942,6 @@ pub fn get() -> RwLockReadGuard<'static, Opts> { OPTIONS.read().unwrap() } -pub fn parse_pref_from_command_line(pref: &str) { - let split: Vec<&str> = pref.splitn(2, '=').collect(); - let pref_name = split[0]; - let pref_value = parse_cli_pref_value(split.get(1).cloned()); - prefs::pref_map() - .set(pref_name, pref_value) - .expect(format!("Error setting preference: {}", pref).as_str()); -} - -fn parse_cli_pref_value(input: Option<&str>) -> PrefValue { - match input { - Some("true") | None => PrefValue::Bool(true), - Some("false") => PrefValue::Bool(false), - Some(string) => { - if let Some(int) = string.parse::().ok() { - PrefValue::Int(int) - } else if let Some(float) = string.parse::().ok() { - PrefValue::Float(float) - } else { - PrefValue::from(string) - } - }, - } -} - pub fn parse_url_or_filename(cwd: &Path, input: &str) -> Result { match ServoUrl::parse(input) { Ok(url) => Ok(url), diff --git a/components/config/pref_util.rs b/components/config/pref_util.rs index 8a3fde0c99b..ba4dd78f6e8 100644 --- a/components/config/pref_util.rs +++ b/components/config/pref_util.rs @@ -220,6 +220,17 @@ impl<'m, P: Clone> Preferences<'m, P> { } } + /// Has the preference been modified from its original value? + pub fn is_default(&self, key: &str) -> Result { + if let Some(accessor) = self.accessors.get(key) { + let user = (accessor.getter)(&self.default_prefs); + let default = (accessor.getter)(&self.user_prefs.read().unwrap()); + Ok(default == user) + } else { + Err(PrefError::NoSuchPref(String::from(key))) + } + } + /// Creates an iterator over all keys and values pub fn iter<'a>(&'a self) -> impl Iterator + 'a { let prefs = self.user_prefs.read().unwrap(); diff --git a/components/config/prefs.rs b/components/config/prefs.rs index 459f3328324..6de159b91df 100644 --- a/components/config/prefs.rs +++ b/components/config/prefs.rs @@ -2,15 +2,10 @@ * 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 crate::basedir::default_config_dir; -use crate::opts; use embedder_traits::resources::{self, Resource}; use serde_json::{self, Value}; use std::borrow::ToOwned; use std::collections::HashMap; -use std::fs::File; -use std::io::{stderr, Read, Write}; -use std::path::PathBuf; use crate::pref_util::Preferences; pub use crate::pref_util::{PrefError, PrefValue}; @@ -58,42 +53,12 @@ pub fn pref_map() -> &'static Preferences<'static, Prefs> { &PREFS } -pub(crate) fn add_user_prefs() { - if let Some(path) = user_prefs_path() { - init_user_prefs(path); +pub fn add_user_prefs(prefs: HashMap) { + if let Err(error) = PREFS.set_all(prefs.into_iter()) { + panic!("Error setting preference: {:?}", error); } } -fn user_prefs_path() -> Option { - opts::get() - .config_dir - .clone() - .or_else(|| default_config_dir()) - .map(|path| path.join("prefs.json")) - .filter(|path| path.exists()) -} - -fn init_user_prefs(path: PathBuf) { - if let Ok(mut file) = File::open(&path) { - let mut txt = String::new(); - file.read_to_string(&mut txt) - .expect("Can't read user prefs"); - match read_prefs_map(&txt) { - Ok(prefs) => { - if let Err(error) = PREFS.set_all(prefs.into_iter()) { - writeln!(&mut stderr(), "Error setting preference: {:?}", error) - } else { - Ok(()) - } - }, - Err(error) => writeln!(&mut stderr(), "Error parsing prefs.json: {:?}", error), - } - } else { - writeln!(&mut stderr(), "Error opening user prefs from {:?}", path) - } - .expect("failed printing to stderr"); -} - pub fn read_prefs_map(txt: &str) -> Result, PrefError> { let prefs: HashMap = serde_json::from_str(txt).map_err(|e| PrefError::JsonParseErr(e))?; diff --git a/components/config/tests/opts.rs b/components/config/tests/opts.rs index abb1d0a9dfc..e0b36456c90 100644 --- a/components/config/tests/opts.rs +++ b/components/config/tests/opts.rs @@ -2,11 +2,7 @@ * 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/. */ -#[macro_use] -extern crate servo_config; - -use servo_config::opts::{parse_pref_from_command_line, parse_url_or_filename}; -use servo_config::{prefs, prefs::PrefValue}; +use servo_config::opts::parse_url_or_filename; use std::path::Path; #[cfg(not(target_os = "windows"))] @@ -88,54 +84,3 @@ fn test_argument_parsing_special() { assert_eq!(url.query(), None); assert_eq!(url.fragment(), None); } - -#[test] -fn test_invalid_prefs_from_command_line_panics() { - let err_msg = std::panic::catch_unwind(|| { - parse_pref_from_command_line("doesntexist=true"); - }) - .err() - .and_then(|a| a.downcast_ref::().cloned()) - .expect("Should panic"); - assert!( - err_msg.starts_with("Error setting preference"), - "Message should describe the problem" - ); - assert!( - err_msg.contains("doesntexist"), - "Message should mention the name of the preference" - ); -} - -#[test] -fn test_parse_pref_from_command_line() { - // Test with boolean values. - parse_pref_from_command_line("dom.bluetooth.enabled=true"); - assert_eq!( - prefs::pref_map().get("dom.bluetooth.enabled"), - PrefValue::Bool(true) - ); - assert_eq!(pref!(dom.bluetooth.enabled), true); - - parse_pref_from_command_line("dom.bluetooth.enabled=false"); - assert_eq!( - prefs::pref_map().get("dom.bluetooth.enabled"), - PrefValue::Bool(false) - ); - assert_eq!(pref!(dom.bluetooth.enabled), false); - - // Test with numbers - parse_pref_from_command_line("layout.threads=42"); - assert_eq!(pref!(layout.threads), 42); - - // Test string. - parse_pref_from_command_line("shell.homepage=str"); - assert_eq!(pref!(shell.homepage), "str"); - - // Test with no value (defaults to true). - prefs::pref_map() - .set("dom.bluetooth.enabled", false) - .unwrap(); - parse_pref_from_command_line("dom.bluetooth.enabled"); - assert_eq!(pref!(dom.bluetooth.enabled), true); -} diff --git a/ports/libsimpleservo/api/src/lib.rs b/ports/libsimpleservo/api/src/lib.rs index cf133ff0a5b..7dd1881243f 100644 --- a/ports/libsimpleservo/api/src/lib.rs +++ b/ports/libsimpleservo/api/src/lib.rs @@ -7,6 +7,7 @@ extern crate log; pub mod gl_glue; +pub use servo::config::prefs::{add_user_prefs, PrefValue}; pub use servo::embedder_traits::{ ContextMenuResult, MediaSessionPlaybackState, PermissionPrompt, PermissionRequest, PromptResult, }; @@ -19,6 +20,7 @@ use servo::compositing::windowing::{ AnimationState, EmbedderCoordinates, EmbedderMethods, MouseWindowEvent, WindowEvent, WindowMethods, }; +use servo::config::prefs::pref_map; use servo::embedder_traits::resources::{self, Resource, ResourceReaderMethods}; use servo::embedder_traits::{ EmbedderMsg, EmbedderProxy, MediaSessionEvent, PromptDefinition, PromptOrigin, @@ -36,6 +38,7 @@ use servo::webrender_surfman::WebrenderSurfman; use servo::{self, gl, BrowserId, Servo}; use servo_media::player::context as MediaPlayerContext; use std::cell::RefCell; +use std::collections::HashMap; use std::mem; use std::os::raw::c_void; use std::path::PathBuf; @@ -55,7 +58,6 @@ pub use servo::embedder_traits::EventLoopWaker; pub struct InitOptions { pub args: Vec, - pub url: Option, pub coordinates: Coordinates, pub density: f32, pub xr_discovery: Option, @@ -63,6 +65,7 @@ pub struct InitOptions { pub gl_context_pointer: Option<*const c_void>, pub native_display_pointer: Option<*const c_void>, pub native_widget: *mut c_void, + pub prefs: Option>, } #[derive(Clone, Debug)] @@ -173,6 +176,44 @@ pub fn is_uri_valid(url: &str) -> bool { ServoUrl::parse(url).is_ok() } +/// Retrieve a snapshot of the current preferences +pub fn get_prefs() -> HashMap { + pref_map() + .iter() + .map(|(key, value)| { + let is_default = pref_map().is_default(&key).unwrap(); + (key, (value, is_default)) + }) + .collect() +} + +/// Retrieve a preference. +pub fn get_pref(key: &str) -> (PrefValue, bool) { + if let Ok(is_default) = pref_map().is_default(&key) { + (pref_map().get(key), is_default) + } else { + (PrefValue::Missing, false) + } +} + +/// Restore a preference to its default value. +pub fn reset_pref(key: &str) -> bool { + pref_map().reset(key).is_ok() +} + +/// Restore all the preferences to their default values. +pub fn reset_all_prefs() { + pref_map().reset_all(); +} + +/// Change the value of a preference. +pub fn set_pref(key: &str, val: PrefValue) -> Result<(), &'static str> { + pref_map() + .set(key, val) + .map(|_| ()) + .map_err(|_| "Pref set failed") +} + /// Initialize Servo. At that point, we need a valid GL context. /// In the future, this will be done in multiple steps. pub fn init( @@ -195,16 +236,14 @@ pub fn init( opts::from_cmdline_args(Options::new(), &args); } - let embedder_url = init_opts.url.as_ref().and_then(|s| ServoUrl::parse(s).ok()); - let cmdline_url = opts::get().url.clone(); + if let Some(prefs) = init_opts.prefs { + add_user_prefs(prefs); + } + let pref_url = ServoUrl::parse(&pref!(shell.homepage)).ok(); let blank_url = ServoUrl::parse("about:blank").ok(); - let url = embedder_url - .or(cmdline_url) - .or(pref_url) - .or(blank_url) - .unwrap(); + let url = pref_url.or(blank_url).unwrap(); gl.clear_color(1.0, 1.0, 1.0, 1.0); gl.clear(gl::COLOR_BUFFER_BIT); diff --git a/ports/libsimpleservo/capi/src/lib.rs b/ports/libsimpleservo/capi/src/lib.rs index 5bd8bf3dd60..e37ee89178e 100644 --- a/ports/libsimpleservo/capi/src/lib.rs +++ b/ports/libsimpleservo/capi/src/lib.rs @@ -8,6 +8,8 @@ extern crate lazy_static; #[macro_use] extern crate log; +mod prefs; + #[cfg(target_os = "windows")] mod vslogger; @@ -187,7 +189,7 @@ fn do_redirect_stdout_stderr(handler: LogHandlerFn) -> Result<(), ()> { fn call(f: F) -> T where - F: Fn(&mut ServoGlue) -> Result, + F: FnOnce(&mut ServoGlue) -> Result, { match SERVO.with(|s| match s.borrow_mut().as_mut() { Some(ref mut s) => (f)(s), @@ -235,7 +237,6 @@ pub struct CHostCallbacks { #[repr(C)] pub struct CInitOptions { pub args: *const c_char, - pub url: *const c_char, pub width: i32, pub height: i32, pub density: f32, @@ -243,6 +244,7 @@ pub struct CInitOptions { pub vslogger_mod_list: *const *const c_char, pub vslogger_mod_size: u32, pub native_widget: *mut c_void, + pub prefs: *const prefs::CPrefList, } #[repr(C)] @@ -427,15 +429,18 @@ unsafe fn init( warn!("Error redirecting stdout/stderr: {}", reason); } - let url = CStr::from_ptr(opts.url); - let url = url.to_str().map(|s| s.to_string()).ok(); - let coordinates = Coordinates::new(0, 0, opts.width, opts.height, opts.width, opts.height); + let prefs = if opts.prefs.is_null() { + None + } else { + Some((*opts.prefs).convert()) + }; + let opts = InitOptions { args, - url, coordinates, + prefs, density: opts.density, xr_discovery: None, enable_subpixel_text_antialiasing: opts.enable_subpixel_text_antialiasing, @@ -532,6 +537,9 @@ pub extern "C" fn resize(width: i32, height: i32) { pub extern "C" fn perform_updates() { catch_any_panic(|| { debug!("perform_updates"); + // We might have allocated some memory to respond to a potential + // request, from the embedder, for a copy of Servo's preferences. + prefs::free_prefs(); call(|s| s.perform_updates()); }); } diff --git a/ports/libsimpleservo/capi/src/prefs.rs b/ports/libsimpleservo/capi/src/prefs.rs new file mode 100644 index 00000000000..b339636f50d --- /dev/null +++ b/ports/libsimpleservo/capi/src/prefs.rs @@ -0,0 +1,241 @@ +/* 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/. */ + +//! Servo's internal preferences are not C-compatible. To expose the preference to the embedder, +//! we keep a C-compatible copy of the preferences alive (LOCALCPREFS). The embedder can +//! retrieve an array (CPREFS) of struct of pointers (CPrefs) to the C-compatible preferences +//! (LocalCPref). + +use crate::catch_any_panic; +use crate::simpleservo::{self, PrefValue}; +use std::cell::RefCell; +use std::collections::{BTreeMap, HashMap}; +use std::ffi::{CStr, CString}; +use std::os::raw::{c_char, c_void}; + +thread_local! { + // CPREFS keeps alive a set of CPref that are sent over to the embedder. + // The CPREFS are structs holding pointers to values held alive by LOCALCPREFS. + // This is emptied in free_prefs the next time perform_updates is called. + static CPREFS: RefCell> = RefCell::new(Vec::new()); + static LOCALCPREFS: RefCell> = RefCell::new(BTreeMap::new()); +} + +struct LocalCPref { + key: CString, + value: LocalCPrefValue, + is_default: bool, +} + +#[derive(Debug)] +enum LocalCPrefValue { + Float(f64), + Int(i64), + Str(CString), + Bool(bool), + Missing, +} + +impl LocalCPrefValue { + pub fn new(v: &PrefValue) -> LocalCPrefValue { + match v { + PrefValue::Float(v) => LocalCPrefValue::Float(*v), + PrefValue::Int(v) => LocalCPrefValue::Int(*v), + PrefValue::Str(v) => LocalCPrefValue::Str(CString::new(v.clone()).unwrap()), + PrefValue::Bool(v) => LocalCPrefValue::Bool(*v), + PrefValue::Missing => LocalCPrefValue::Missing, + } + } +} + +#[repr(C)] +pub struct CPrefList { + pub len: usize, + pub list: *const CPref, +} + +impl CPrefList { + pub fn convert(&self) -> HashMap { + let slice = unsafe { std::slice::from_raw_parts(self.list, self.len) }; + slice.iter().map(|cpref| cpref.convert()).collect() + } +} + +#[repr(C)] +pub struct CPref { + pub pref_type: CPrefType, + pub key: *const c_char, + pub value: *const c_void, + pub is_default: bool, +} + +impl CPref { + fn new(local: &LocalCPref) -> CPref { + let (pref_type, value) = match &local.value { + LocalCPrefValue::Float(v) => (CPrefType::Float, v as *const f64 as *const c_void), + LocalCPrefValue::Int(v) => (CPrefType::Int, v as *const i64 as *const c_void), + LocalCPrefValue::Bool(v) => (CPrefType::Bool, v as *const bool as *const c_void), + LocalCPrefValue::Str(v) => (CPrefType::Str, v.as_ptr() as *const c_void), + LocalCPrefValue::Missing => (CPrefType::Missing, std::ptr::null()), + }; + CPref { + key: local.key.as_ptr(), + is_default: local.is_default, + pref_type, + value, + } + } + fn convert(&self) -> (String, PrefValue) { + let key = unsafe { CStr::from_ptr(self.key) }; + let key = key.to_str().expect("Can't read string").to_string(); + let value = unsafe { + match self.pref_type { + CPrefType::Float => PrefValue::Float(*(self.value as *const f64)), + CPrefType::Int => PrefValue::Int(*(self.value as *const i64)), + CPrefType::Bool => PrefValue::Bool(*(self.value as *const bool)), + CPrefType::Str => PrefValue::Str({ + let value = CStr::from_ptr(self.value as *const c_char); + value.to_str().expect("Can't read string").to_string() + }), + CPrefType::Missing => PrefValue::Missing, + } + }; + (key, value) + } +} + +#[repr(C)] +pub enum CPrefType { + Float, + Int, + Str, + Bool, + Missing, +} + +#[no_mangle] +pub extern "C" fn get_pref_as_float(ptr: *const c_void) -> *const f64 { + ptr as *const f64 +} + +#[no_mangle] +pub extern "C" fn get_pref_as_int(ptr: *const c_void) -> *const i64 { + ptr as *const i64 +} + +#[no_mangle] +pub extern "C" fn get_pref_as_str(ptr: *const c_void) -> *const c_char { + ptr as *const c_char +} + +#[no_mangle] +pub extern "C" fn get_pref_as_bool(ptr: *const c_void) -> *const bool { + ptr as *const bool +} + +#[no_mangle] +pub extern "C" fn reset_all_prefs() { + catch_any_panic(|| { + debug!("reset_all_prefs"); + simpleservo::reset_all_prefs() + }) +} + +#[no_mangle] +pub extern "C" fn reset_pref(key: *const c_char) -> bool { + catch_any_panic(|| { + debug!("reset_pref"); + let key = unsafe { CStr::from_ptr(key) }; + let key = key.to_str().expect("Can't read string"); + simpleservo::reset_pref(key) + }) +} + +#[no_mangle] +pub extern "C" fn get_pref(key: *const c_char) -> CPref { + catch_any_panic(|| { + debug!("get_pref"); + LOCALCPREFS.with(|localmap| { + let key = unsafe { CStr::from_ptr(key) }; + let key = key.to_str().expect("Can't read string"); + let (value, is_default) = simpleservo::get_pref(key); + let local = LocalCPref { + key: CString::new(key).unwrap(), + value: LocalCPrefValue::new(&value), + is_default: is_default, + }; + let cpref = CPref::new(&local); + localmap.borrow_mut().insert(key.to_string(), local); + cpref + }) + }) +} + +fn set_pref(key: *const c_char, value: PrefValue) -> bool { + catch_any_panic(|| { + debug!("set_pref"); + let key = unsafe { CStr::from_ptr(key) }; + let key = key.to_str().expect("Can't read string"); + simpleservo::set_pref(key, value).is_ok() + }) +} + +#[no_mangle] +pub extern "C" fn set_float_pref(key: *const c_char, value: f64) -> bool { + set_pref(key, PrefValue::Float(value)) +} + +#[no_mangle] +pub extern "C" fn set_int_pref(key: *const c_char, value: i64) -> bool { + set_pref(key, PrefValue::Int(value)) +} + +#[no_mangle] +pub extern "C" fn set_bool_pref(key: *const c_char, value: bool) -> bool { + set_pref(key, PrefValue::Bool(value)) +} + +#[no_mangle] +pub extern "C" fn set_str_pref(key: *const c_char, value: *const c_char) -> bool { + let value = unsafe { CStr::from_ptr(value) }; + let value = value.to_str().expect("Can't read string").to_string(); + set_pref(key, PrefValue::Str(value)) +} + +#[no_mangle] +pub extern "C" fn get_prefs() -> CPrefList { + // Called from any thread + catch_any_panic(|| { + debug!("get_prefs"); + let map = simpleservo::get_prefs(); + let local: BTreeMap = map + .into_iter() + .map(|(key, (value, is_default))| { + let l = LocalCPref { + key: CString::new(key.clone()).unwrap(), + value: LocalCPrefValue::new(&value), + is_default: is_default, + }; + (key, l) + }) + .collect(); + + let ptrs: Vec = local.iter().map(|(_, local)| CPref::new(&local)).collect(); + + let list = CPrefList { + len: ptrs.len(), + list: ptrs.as_ptr(), + }; + + LOCALCPREFS.with(|p| *p.borrow_mut() = local); + CPREFS.with(|p| *p.borrow_mut() = ptrs); + + list + }) +} + +pub(crate) fn free_prefs() { + LOCALCPREFS.with(|p| p.borrow_mut().clear()); + CPREFS.with(|p| p.borrow_mut().clear()); +} diff --git a/ports/winit/Cargo.toml b/ports/winit/Cargo.toml index 1850f8acbdd..5b6cc3b3c37 100644 --- a/ports/winit/Cargo.toml +++ b/ports/winit/Cargo.toml @@ -11,7 +11,6 @@ publish = false [[bin]] name = "servo" path = "main.rs" -test = false bench = false [target.'cfg(windows)'.build-dependencies] diff --git a/ports/winit/main2.rs b/ports/winit/main2.rs index 707cf1d422c..1319e2cb60d 100644 --- a/ports/winit/main2.rs +++ b/ports/winit/main2.rs @@ -18,6 +18,7 @@ mod events_loop; mod headed_window; mod headless_window; mod keyutils; +mod prefs; mod resources; mod window_trait; @@ -102,9 +103,22 @@ pub fn main() { "Set custom user agent string (or ios / android / desktop for platform default)", "NCSA Mosaic/1.0 (X11;SunOS 4.1.4 sun4m)", ); + opts.optmulti( + "", + "pref", + "A preference to set to enable", + "dom.bluetooth.enabled", + ); + opts.optmulti( + "", + "pref", + "A preference to set to disable", + "dom.webgpu.enabled=false", + ); let opts_matches; let content_process_token; + match opts::from_cmdline_args(opts, &args) { ArgumentParsingResult::ContentProcess(matches, token) => { opts_matches = matches; @@ -119,6 +133,8 @@ pub fn main() { }, }; + prefs::register_user_prefs(&opts_matches); + // TODO: once log-panics is released, can this be replaced by // log_panics::init()? panic::set_hook(Box::new(|info| { diff --git a/ports/winit/prefs.rs b/ports/winit/prefs.rs new file mode 100644 index 00000000000..16f4de85b81 --- /dev/null +++ b/ports/winit/prefs.rs @@ -0,0 +1,127 @@ +/* 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 servo::embedder_traits; +use getopts::{Matches, Options}; +use servo::config::opts::{self, ArgumentParsingResult}; +use servo::config::prefs::{self, PrefValue}; +use std::collections::HashMap; +use std::fs::File; +use std::io::Read; +use servo::servo_config::basedir; + +pub fn register_user_prefs(opts_matches: &Matches) { + // Read user's prefs.json and then parse --pref command line args. + + let user_prefs_path = opts::get() + .config_dir + .clone() + .or_else(|| basedir::default_config_dir()) + .map(|path| path.join("prefs.json")) + .filter(|path| path.exists()); + + let mut userprefs = if let Some(path) = user_prefs_path { + let mut file = File::open(&path).expect("Error opening user prefs"); + let mut txt = String::new(); + file.read_to_string(&mut txt).expect("Can't read user prefs file"); + prefs::read_prefs_map(&txt).expect("Can't parse user prefs file") + } else { + HashMap::new() + }; + + let argprefs: HashMap = opts_matches.opt_strs("pref").iter().map(|pref| { + let split: Vec<&str> = pref.splitn(2, '=').collect(); + let pref_name = split[0]; + let pref_value = match split.get(1).cloned() { + Some("true") | None => PrefValue::Bool(true), + Some("false") => PrefValue::Bool(false), + Some(string) => { + if let Some(int) = string.parse::().ok() { + PrefValue::Int(int) + } else if let Some(float) = string.parse::().ok() { + PrefValue::Float(float) + } else { + PrefValue::from(string) + } + }, + }; + (pref_name.to_string(), pref_value) + }).collect(); + + // --pref overrides user prefs.json + userprefs.extend(argprefs); + + prefs::add_user_prefs(userprefs); +} + +// Use for test +#[allow(dead_code)] +fn test_parse_pref(arg: &str) { + embedder_traits::resources::set_for_tests(); + let mut opts = Options::new(); + opts.optmulti("", "pref", "", ""); + let args = vec![ + "servo".to_string(), + "--pref".to_string(), + arg.to_string() + ]; + let matches = match opts::from_cmdline_args(opts, &args) { + ArgumentParsingResult::ContentProcess(m, _) => m, + ArgumentParsingResult::ChromeProcess(m) => m, + }; + register_user_prefs(&matches); +} + +#[test] +fn test_parse_pref_from_command_line() { + use servo::servo_config::pref; + // Test with boolean values. + test_parse_pref("dom.bluetooth.enabled=true"); + assert_eq!( + prefs::pref_map().get("dom.bluetooth.enabled"), + PrefValue::Bool(true) + ); + assert_eq!(pref!(dom.bluetooth.enabled), true); + + test_parse_pref("dom.bluetooth.enabled=false"); + assert_eq!( + prefs::pref_map().get("dom.bluetooth.enabled"), + PrefValue::Bool(false) + ); + assert_eq!(pref!(dom.bluetooth.enabled), false); + + // Test with numbers + test_parse_pref("layout.threads=42"); + assert_eq!(pref!(layout.threads), 42); + + // Test string. + test_parse_pref("shell.homepage=str"); + assert_eq!(pref!(shell.homepage), "str"); + + // Test with no value (defaults to true). + prefs::pref_map() + .set("dom.bluetooth.enabled", false) + .unwrap(); + test_parse_pref("dom.bluetooth.enabled"); + assert_eq!(pref!(dom.bluetooth.enabled), true); +} + + +#[test] +fn test_invalid_prefs_from_command_line_panics() { + let err_msg = std::panic::catch_unwind(|| { + test_parse_pref("doesntexist=true"); + }) + .err() + .and_then(|a| a.downcast_ref::().cloned()) + .expect("Should panic"); + assert!( + err_msg.starts_with("Error setting preference"), + "Message should describe the problem" + ); + assert!( + err_msg.contains("doesntexist"), + "Message should mention the name of the preference" + ); +} diff --git a/support/hololens/ServoApp/App.h b/support/hololens/ServoApp/App.h index 4772306e91f..3820da8b952 100644 --- a/support/hololens/ServoApp/App.h +++ b/support/hololens/ServoApp/App.h @@ -6,19 +6,19 @@ #include "App.xaml.g.h" namespace winrt::ServoApp::implementation { +using namespace winrt::Windows::Foundation; +using namespace winrt::Windows::UI::Xaml; +using namespace winrt::Windows::ApplicationModel; +using namespace winrt::Windows::ApplicationModel::Activation; + struct App : AppT { App(); - void createRootFrame(winrt::Windows::UI::Xaml::Controls::Frame &, bool, - winrt::Windows::Foundation::IInspectable const &); - void OnLaunched( - Windows::ApplicationModel::Activation::LaunchActivatedEventArgs const &); - void App::OnActivated( - Windows::ApplicationModel::Activation::IActivatedEventArgs const &); - void OnSuspending(IInspectable const &, - Windows::ApplicationModel::SuspendingEventArgs const &); - void OnNavigationFailed( - IInspectable const &, - Windows::UI::Xaml::Navigation::NavigationFailedEventArgs const &); + void createRootFrame(Controls::Frame &, bool, IInspectable const &); + void OnLaunched(LaunchActivatedEventArgs const &); + void App::OnActivated(IActivatedEventArgs const &); + void OnSuspending(IInspectable const &, SuspendingEventArgs const &); + void OnNavigationFailed(IInspectable const &, + Navigation::NavigationFailedEventArgs const &); }; } // namespace winrt::ServoApp::implementation diff --git a/support/hololens/ServoApp/App.xaml b/support/hololens/ServoApp/App.xaml index 015f637b3df..333b37f88b7 100644 --- a/support/hololens/ServoApp/App.xaml +++ b/support/hololens/ServoApp/App.xaml @@ -4,4 +4,8 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:ServoApp"> + + + + diff --git a/support/hololens/ServoApp/Assets/UI/stop.png b/support/hololens/ServoApp/Assets/UI/cross.png similarity index 100% rename from support/hololens/ServoApp/Assets/UI/stop.png rename to support/hololens/ServoApp/Assets/UI/cross.png diff --git a/support/hololens/ServoApp/BrowserPage.cpp b/support/hololens/ServoApp/BrowserPage.cpp index e8ba12ba2b3..a2b13bf60b9 100644 --- a/support/hololens/ServoApp/BrowserPage.cpp +++ b/support/hololens/ServoApp/BrowserPage.cpp @@ -8,6 +8,11 @@ #include "BrowserPage.g.cpp" #include "DefaultUrl.h" +#include "winrt/Microsoft.UI.Xaml.Controls.h" +#include "winrt/Microsoft.UI.Xaml.XamlTypeInfo.h" +#include "winrt/Windows.UI.Text.h" +#include "winrt/Windows.UI.Xaml.Documents.h" // For Run.Text() + using namespace std::placeholders; using namespace winrt::Windows::Foundation; using namespace winrt::Windows::UI::Xaml; @@ -18,6 +23,7 @@ using namespace winrt::Windows::UI::Notifications; using namespace winrt::Windows::Data::Xml::Dom; namespace winrt::ServoApp::implementation { + BrowserPage::BrowserPage() { InitializeComponent(); BindServoEvents(); @@ -84,13 +90,12 @@ void BrowserPage::BindServoEvents() { }); } -void BrowserPage::OnURLFocused(Windows::Foundation::IInspectable const &) { +void BrowserPage::OnURLFocused(IInspectable const &) { urlTextbox().SelectAll(); } void BrowserPage::OnURLKeyboardAccelerator( - Windows::Foundation::IInspectable const &, - Windows::UI::Xaml::Input::KeyboardAcceleratorInvokedEventArgs const &) { + IInspectable const &, Input::KeyboardAcceleratorInvokedEventArgs const &) { urlTextbox().Focus(FocusState::Programmatic); } @@ -116,10 +121,7 @@ void BrowserPage::SetTransientMode(bool transient) { void BrowserPage::SetArgs(hstring args) { servoControl().SetArgs(args); } -void BrowserPage::Shutdown() { - ToastNotificationManager::History().Clear(); - servoControl().Shutdown(); -} +void BrowserPage::Shutdown() { servoControl().Shutdown(); } /**** USER INTERACTIONS WITH UI ****/ @@ -148,23 +150,153 @@ void BrowserPage::OnHomeButtonClicked(IInspectable const &, servoControl().LoadURIOrSearch(DEFAULT_URL); } +// Given a pref, update its associated UI control. +void BrowserPage::UpdatePref(ServoApp::Pref pref, Controls::Control ctrl) { + auto value = pref.Value(); + auto type = value.as().Type(); + if (type == PropertyType::Boolean) { + ctrl.as().IsChecked(unbox_value(value)); + } else if (type == PropertyType::Double) { + ctrl.as().Value( + unbox_value(value)); + } else if (type == PropertyType::Int64) { + ctrl.as().Value( + (double)unbox_value(value)); + } else if (type == PropertyType::String) { + ctrl.as().Text(unbox_value(value)); + } + auto stack = ctrl.Parent().as(); + auto font = winrt::Windows::UI::Text::FontWeights::Normal(); + if (!pref.IsDefault()) { + font = winrt::Windows::UI::Text::FontWeights::Bold(); + } + stack.Children().GetAt(0).as().FontWeight(font); + stack.Children().GetAt(2).as().IsEnabled(!pref.IsDefault()); +} + +// Retrieve the preference list from Servo and build the preference table. +void BrowserPage::BuildPrefList() { + // It would be better to use a template and bindings, but the + // takes too long to generate all the items, and + // it's pretty difficiult to have different controls depending + // on the pref type. + prefList().Children().Clear(); + for (auto pref : ServoControl().Preferences()) { + auto value = pref.Value(); + auto type = value.as().Type(); + std::optional ctrl; + if (type == PropertyType::Boolean) { + auto checkbox = Controls::CheckBox(); + checkbox.IsChecked(unbox_value(value)); + checkbox.Click([=](const auto &, auto const &) { + auto upref = ServoControl().SetBoolPref( + pref.Key(), checkbox.IsChecked().GetBoolean()); + UpdatePref(upref, checkbox); + }); + ctrl = checkbox; + } else if (type == PropertyType::String) { + auto textbox = Controls::TextBox(); + textbox.Text(unbox_value(value)); + textbox.KeyUp([=](const auto &, Input::KeyRoutedEventArgs const &e) { + if (e.Key() == Windows::System::VirtualKey::Enter) { + auto upref = ServoControl().SetStringPref(pref.Key(), textbox.Text()); + UpdatePref(upref, textbox); + } + }); + ctrl = textbox; + } else if (type == PropertyType::Int64) { + // Note: These are *not* under Windows::UI:Xaml namespace. + auto nbox = Microsoft::UI::Xaml::Controls::NumberBox(); + nbox.Value((double)unbox_value(value)); + nbox.SpinButtonPlacementMode( + Microsoft::UI::Xaml::Controls::NumberBoxSpinButtonPlacementMode:: + Inline); + nbox.ValueChanged([=](const auto &, const auto &) { + int val = (int)nbox.Value(); + auto upref = ServoControl().SetIntPref(pref.Key(), val); + UpdatePref(upref, nbox); + }); + ctrl = nbox; + } else if (type == PropertyType::Double) { + auto nbox = Microsoft::UI::Xaml::Controls::NumberBox(); + nbox.Value(unbox_value(value)); + nbox.ValueChanged([=](const auto &, const auto &) { + auto upref = + ServoControl().SetIntPref(pref.Key(), (int64_t)nbox.Value()); + UpdatePref(upref, (Controls::Control &)nbox); + }); + ctrl = nbox; + } + if (ctrl.has_value()) { + auto stack = Controls::StackPanel(); + stack.Tag(winrt::box_value(pref.Key())); + stack.Padding({4, 4, 4, 4}); + stack.Orientation(Controls::Orientation::Horizontal); + auto key = Controls::TextBlock(); + key.Text(pref.Key()); + key.Width(350); + if (!pref.IsDefault()) { + auto font = winrt::Windows::UI::Text::FontWeights::Bold(); + key.FontWeight(font); + } + stack.Children().Append(key); + ctrl->Width(300); + ctrl->Margin({4, 0, 40, 0}); + stack.Children().Append(*ctrl); + auto reset = Controls::Button(); + reset.Content(winrt::box_value(L"reset")); + reset.IsEnabled(!pref.IsDefault()); + reset.Click([=](const auto &, auto const &) { + auto upref = ServoControl().ResetPref(pref.Key()); + UpdatePref(upref, *ctrl); + }); + stack.Children().Append(reset); + prefList().Children().Append(stack); + } + } +} + +void BrowserPage::OnPrefererenceSearchboxEdited( + IInspectable const &, Input::KeyRoutedEventArgs const &) { + auto input = preferenceSearchbox().Text(); + for (auto element : prefList().Children()) { + auto ctrl = (Controls::Control &)element; + if (input.size() == 0) { + ctrl.Visibility(Visibility::Visible); + } else { + auto tag = ctrl.Tag(); + std::wstring key = static_cast(unbox_value(tag)); + bool not_found = key.find(input) == std::wstring::npos; + ctrl.Visibility(not_found ? Visibility::Collapsed : Visibility::Visible); + } + } +} + void BrowserPage::OnDevtoolsButtonClicked(IInspectable const &, RoutedEventArgs const &) { - auto toastTemplate = ToastTemplateType::ToastText01; - auto toastXml = ToastNotificationManager::GetTemplateContent(toastTemplate); - auto toastTextElements = toastXml.GetElementsByTagName(L"text"); - std::wstring message; - if (mDevtoolsStatus == DevtoolsStatus::Stopped) { - message = L"Devtools server hasn't started"; - } else if (mDevtoolsStatus == DevtoolsStatus::Running) { - message = L"DevTools server has started on port " + - std::to_wstring(mDevtoolsPort); - } else if (mDevtoolsStatus == DevtoolsStatus::Failed) { - message = L"Error: could not start DevTools"; + if (toolbox().Visibility() == Visibility::Visible) { + prefList().Children().Clear(); + toolbox().Visibility(Visibility::Collapsed); + return; + } + + toolbox().Visibility(Visibility::Visible); + + BuildPrefList(); + + // FIXME: we could use template + binding for this. + auto ok = mDevtoolsStatus == DevtoolsStatus::Running ? Visibility::Visible + : Visibility::Collapsed; + auto ko = mDevtoolsStatus == DevtoolsStatus::Failed ? Visibility::Visible + : Visibility::Collapsed; + auto wip = mDevtoolsStatus == DevtoolsStatus::Stopped ? Visibility::Visible + : Visibility::Collapsed; + DevtoolsStatusOK().Visibility(ok); + DevtoolsStatusKO().Visibility(ko); + DevtoolsStatusWIP().Visibility(wip); + if (mDevtoolsStatus == DevtoolsStatus::Running) { + DevtoolsPort().Text(std::to_wstring(mDevtoolsPort)); } - toastTextElements.Item(0).InnerText(message); - auto toast = ToastNotification(toastXml); - ToastNotificationManager::CreateToastNotifier().Show(toast); } void BrowserPage::OnURLEdited(IInspectable const &, @@ -177,15 +309,13 @@ void BrowserPage::OnURLEdited(IInspectable const &, } } -void BrowserPage::OnMediaControlsPlayClicked( - Windows::Foundation::IInspectable const &, - Windows::UI::Xaml::RoutedEventArgs const &) { +void BrowserPage::OnMediaControlsPlayClicked(IInspectable const &, + RoutedEventArgs const &) { servoControl().SendMediaSessionAction( static_cast(servo::Servo::MediaSessionActionType::Play)); } -void BrowserPage::OnMediaControlsPauseClicked( - Windows::Foundation::IInspectable const &, - Windows::UI::Xaml::RoutedEventArgs const &) { +void BrowserPage::OnMediaControlsPauseClicked(IInspectable const &, + RoutedEventArgs const &) { servoControl().SendMediaSessionAction( static_cast(servo::Servo::MediaSessionActionType::Pause)); } diff --git a/support/hololens/ServoApp/BrowserPage.h b/support/hololens/ServoApp/BrowserPage.h index bb57afad68a..bae31d22ff3 100644 --- a/support/hololens/ServoApp/BrowserPage.h +++ b/support/hololens/ServoApp/BrowserPage.h @@ -9,6 +9,10 @@ namespace winrt::ServoApp::implementation { +using namespace winrt::Windows; +using namespace winrt::Windows::Foundation; +using namespace winrt::Windows::UI::Xaml; + static const hstring SERVO_SCHEME = L"fxr"; static const hstring SERVO_SCHEME_SLASH_SLASH = L"fxr://"; @@ -16,35 +20,32 @@ struct BrowserPage : BrowserPageT { public: BrowserPage(); - void OnForwardButtonClicked(Windows::Foundation::IInspectable const &, - Windows::UI::Xaml::RoutedEventArgs const &); - void OnBackButtonClicked(Windows::Foundation::IInspectable const &, - Windows::UI::Xaml::RoutedEventArgs const &); - void OnReloadButtonClicked(Windows::Foundation::IInspectable const &, - Windows::UI::Xaml::RoutedEventArgs const &); - void OnStopButtonClicked(Windows::Foundation::IInspectable const &, - Windows::UI::Xaml::RoutedEventArgs const &); - void OnHomeButtonClicked(Windows::Foundation::IInspectable const &, - Windows::UI::Xaml::RoutedEventArgs const &); - void OnDevtoolsButtonClicked(Windows::Foundation::IInspectable const &, - Windows::UI::Xaml::RoutedEventArgs const &); - void OnURLEdited(Windows::Foundation::IInspectable const &, - Windows::UI::Xaml::Input::KeyRoutedEventArgs const &); - void OnURLFocused(Windows::Foundation::IInspectable const &); - void OnURLKeyboardAccelerator( - Windows::Foundation::IInspectable const &, - Windows::UI::Xaml::Input::KeyboardAcceleratorInvokedEventArgs const &); + void OnForwardButtonClicked(IInspectable const &, RoutedEventArgs const &); + void OnBackButtonClicked(IInspectable const &, RoutedEventArgs const &); + void OnReloadButtonClicked(IInspectable const &, RoutedEventArgs const &); + void OnStopButtonClicked(IInspectable const &, RoutedEventArgs const &); + void OnHomeButtonClicked(IInspectable const &, RoutedEventArgs const &); + void OnDevtoolsButtonClicked(IInspectable const &, RoutedEventArgs const &); + void OnURLEdited(IInspectable const &, Input::KeyRoutedEventArgs const &); + void OnURLFocused(IInspectable const &); + void + OnURLKeyboardAccelerator(IInspectable const &, + Input::KeyboardAcceleratorInvokedEventArgs const &); void Shutdown(); - void LoadServoURI(Windows::Foundation::Uri uri); + void LoadServoURI(Uri uri); void SetTransientMode(bool); void SetArgs(hstring); - void OnMediaControlsPlayClicked(Windows::Foundation::IInspectable const &, - Windows::UI::Xaml::RoutedEventArgs const &); - void OnMediaControlsPauseClicked(Windows::Foundation::IInspectable const &, - Windows::UI::Xaml::RoutedEventArgs const &); + void OnMediaControlsPlayClicked(IInspectable const &, + RoutedEventArgs const &); + void OnMediaControlsPauseClicked(IInspectable const &, + RoutedEventArgs const &); + void OnPrefererenceSearchboxEdited(IInspectable const &, + Input::KeyRoutedEventArgs const &); private: + void UpdatePref(ServoApp::Pref, Controls::Control); void BindServoEvents(); + void BuildPrefList(); DevtoolsStatus mDevtoolsStatus = DevtoolsStatus::Stopped; unsigned int mDevtoolsPort = 0; }; diff --git a/support/hololens/ServoApp/BrowserPage.xaml b/support/hololens/ServoApp/BrowserPage.xaml index 3fa6cf84982..da4ea5c9792 100644 --- a/support/hololens/ServoApp/BrowserPage.xaml +++ b/support/hololens/ServoApp/BrowserPage.xaml @@ -6,7 +6,11 @@ xmlns:local="using:ServoApp" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:muxc="using:Microsoft.UI.Xaml.Controls" mc:Ignorable="d"> + + +