From 90161c1c91b053517af6f6af5cef7d19e7b8c280 Mon Sep 17 00:00:00 2001 From: webbeef Date: Sun, 23 Mar 2025 03:59:19 -0700 Subject: [PATCH] script: Allow opening links in a new `WebView` (#35017) This changes starts tracking the keyboard modifier state in the `Constellation` and forwards it with every input event. The state is used to modify the target of link click so when the platform-dependent alternate action key is enabled, the target is overriden to "_blank". In addition, specification step numbers and text is updated. Signed-off-by: webbeef Signed-off-by: Martin Robinson Co-authored-by: Martin Robinson --- components/constellation/constellation.rs | 50 ++++++++++++++++++- components/script/dom/document.rs | 26 +++++++++- components/script/links.rs | 58 ++++++++++++++--------- components/script/script_thread.rs | 2 + components/shared/script/lib.rs | 3 ++ 5 files changed, 114 insertions(+), 25 deletions(-) diff --git a/components/constellation/constellation.rs b/components/constellation/constellation.rs index 512b0e8176f..168e1e74924 100644 --- a/components/constellation/constellation.rs +++ b/components/constellation/constellation.rs @@ -131,6 +131,7 @@ use ipc_channel::Error as IpcError; use ipc_channel::ipc::{self, IpcReceiver, IpcSender}; use ipc_channel::router::ROUTER; use keyboard_types::webdriver::Event as WebDriverInputEvent; +use keyboard_types::{Key, KeyState, KeyboardEvent, Modifiers}; use log::{debug, error, info, trace, warn}; use media::WindowGLContext; use net_traits::pub_domains::reg_host; @@ -454,6 +455,9 @@ pub struct Constellation { /// currently being pressed. pressed_mouse_buttons: u16, + /// The currently activated keyboard modifiers. + active_keyboard_modifiers: Modifiers, + /// If True, exits on thread failure instead of displaying about:failure hard_fail: bool, @@ -730,6 +734,7 @@ where canvas_ipc_sender, pending_approval_navigations: HashMap::new(), pressed_mouse_buttons: 0, + active_keyboard_modifiers: Modifiers::empty(), hard_fail, active_media_session: None, user_agent: state.user_agent, @@ -2830,6 +2835,38 @@ where } } + fn update_active_keybord_modifiers(&mut self, event: &KeyboardEvent) { + self.active_keyboard_modifiers = event.modifiers; + + // `KeyboardEvent::modifiers` contains the pre-existing modifiers before this key was + // either pressed or released, but `active_keyboard_modifiers` should track the subsequent + // state. If this event will update that state, we need to ensure that we are tracking what + // the event changes. + let modified_modifier = match event.key { + Key::Alt => Modifiers::ALT, + Key::AltGraph => Modifiers::ALT_GRAPH, + Key::CapsLock => Modifiers::CAPS_LOCK, + Key::Control => Modifiers::CONTROL, + Key::Fn => Modifiers::FN, + Key::FnLock => Modifiers::FN_LOCK, + Key::Meta => Modifiers::META, + Key::NumLock => Modifiers::NUM_LOCK, + Key::ScrollLock => Modifiers::SCROLL_LOCK, + Key::Shift => Modifiers::SHIFT, + Key::Symbol => Modifiers::SYMBOL, + Key::SymbolLock => Modifiers::SYMBOL_LOCK, + Key::Hyper => Modifiers::HYPER, + // The web doesn't make a distinction between these keys (there is only + // "meta") so map "super" to "meta". + Key::Super => Modifiers::META, + _ => return, + }; + match event.state { + KeyState::Down => self.active_keyboard_modifiers.insert(modified_modifier), + KeyState::Up => self.active_keyboard_modifiers.remove(modified_modifier), + } + } + fn forward_input_event( &mut self, webview_id: WebViewId, @@ -2840,9 +2877,14 @@ where self.update_pressed_mouse_buttons(event); } - // The constellation tracks the state of pressed mouse buttons and updates the event - // here to reflect the current state. + if let InputEvent::Keyboard(event) = &event { + self.update_active_keybord_modifiers(event); + } + + // The constellation tracks the state of pressed mouse buttons and keyboard + // modifiers and updates the event here to reflect the current state. let pressed_mouse_buttons = self.pressed_mouse_buttons; + let active_keyboard_modifiers = self.active_keyboard_modifiers; // TODO: Click should be handled internally in the `Document`. if let InputEvent::MouseButton(event) = &event { @@ -2885,6 +2927,7 @@ where let event = ConstellationInputEvent { hit_test_result, pressed_mouse_buttons, + active_keyboard_modifiers, event, }; @@ -4392,11 +4435,13 @@ where let event = match event { WebDriverInputEvent::Keyboard(event) => ConstellationInputEvent { pressed_mouse_buttons: self.pressed_mouse_buttons, + active_keyboard_modifiers: event.modifiers, hit_test_result: None, event: InputEvent::Keyboard(event), }, WebDriverInputEvent::Composition(event) => ConstellationInputEvent { pressed_mouse_buttons: self.pressed_mouse_buttons, + active_keyboard_modifiers: self.active_keyboard_modifiers, hit_test_result: None, event: InputEvent::Ime(ImeEvent::Composition(event)), }, @@ -4422,6 +4467,7 @@ where pipeline_id, ConstellationInputEvent { pressed_mouse_buttons: self.pressed_mouse_buttons, + active_keyboard_modifiers: event.modifiers, hit_test_result: None, event: InputEvent::Keyboard(event), }, diff --git a/components/script/dom/document.rs b/components/script/dom/document.rs index fe0e51a0bb4..e3651dd4a57 100644 --- a/components/script/dom/document.rs +++ b/components/script/dom/document.rs @@ -36,7 +36,7 @@ use html5ever::{LocalName, Namespace, QualName, local_name, namespace_url, ns}; use hyper_serde::Serde; use ipc_channel::ipc; use js::rust::{HandleObject, HandleValue}; -use keyboard_types::{Code, Key, KeyState}; +use keyboard_types::{Code, Key, KeyState, Modifiers}; use metrics::{InteractiveFlag, InteractiveWindow, ProgressiveWebMetrics}; use mime::{self, Mime}; use net_traits::CookieSource::NonHTTP; @@ -531,6 +531,9 @@ pub(crate) struct Document { /// The lifetime of an intersection observer is specified at /// . intersection_observers: DomRefCell>>, + /// The active keyboard modifiers for the WebView. This is updated when receiving any input event. + #[no_trace] + active_keyboard_modifiers: Cell, } #[allow(non_snake_case)] @@ -3868,6 +3871,7 @@ impl Document { inherited_insecure_requests_policy: Cell::new(inherited_insecure_requests_policy), intersection_observer_task_queued: Cell::new(false), intersection_observers: Default::default(), + active_keyboard_modifiers: Cell::new(Modifiers::empty()), } } @@ -3888,6 +3892,26 @@ impl Document { .unwrap_or(InsecureRequestsPolicy::DoNotUpgrade) } + /// Update the active keyboard modifiers for this [`Document`] while handling events. + pub(crate) fn update_active_keyboard_modifiers(&self, modifiers: Modifiers) { + self.active_keyboard_modifiers.set(modifiers); + } + + pub(crate) fn alternate_action_keyboard_modifier_active(&self) -> bool { + #[cfg(target_os = "macos")] + { + self.active_keyboard_modifiers + .get() + .contains(Modifiers::META) + } + #[cfg(not(target_os = "macos"))] + { + self.active_keyboard_modifiers + .get() + .contains(Modifiers::CONTROL) + } + } + /// Note a pending compositor event, to be processed at the next `update_the_rendering` task. pub(crate) fn note_pending_input_event(&self, event: ConstellationInputEvent) { let mut pending_compositor_events = self.pending_input_events.borrow_mut(); diff --git a/components/script/links.rs b/components/script/links.rs index 1f95354f1a7..f997f50ebc2 100644 --- a/components/script/links.rs +++ b/components/script/links.rs @@ -342,30 +342,45 @@ pub(crate) fn follow_hyperlink( relations: LinkRelations, hyperlink_suffix: Option, ) { - // Step 1. If subject cannot navigate, then return. + // Step 1: If subject cannot navigate, then return. if subject.cannot_navigate() { return; } - // Step 2, done in Step 7. + // Step 2: Let targetAttributeValue be the empty string. + // This is done below. + + // Step 3: If subject is an a or area element, then set targetAttributeValue to the + // result of getting an element's target given subject. + // + // Also allow the user to open links in a new WebView by pressing either the meta or + // control key (depending on the platform). let document = subject.owner_document(); - let window = document.window(); - - // Step 3: source browsing context. - let source = document.browsing_context().unwrap(); - - // Step 4-5: target attribute. let target_attribute_value = if subject.is::() || subject.is::() { - get_element_target(subject) + if document.alternate_action_keyboard_modifier_active() { + Some("_blank".into()) + } else { + get_element_target(subject) + } } else { None }; - // Step 6. + // Step 4: Let urlRecord be the result of encoding-parsing a URL given subject's href + // attribute value, relative to subject's node document. + // Step 5: If urlRecord is failure, then return. + // TODO: Implement this. + + // Step 6: Let noopener be the result of getting an element's noopener with subject, + // urlRecord, and targetAttributeValue. let noopener = relations.get_element_noopener(target_attribute_value.as_ref()); - // Step 7. + // Step 7: Let targetNavigable be the first return value of applying the rules for + // choosing a navigable given targetAttributeValue, subject's node navigable, and + // noopener. + let window = document.window(); + let source = document.browsing_context().unwrap(); let (maybe_chosen, history_handling) = match target_attribute_value { Some(name) => { let (maybe_chosen, new) = source.choose_browsing_context(name, noopener); @@ -379,7 +394,7 @@ pub(crate) fn follow_hyperlink( None => (Some(window.window_proxy()), NavigationHistoryBehavior::Push), }; - // Step 8. + // Step 8: If targetNavigable is null, then return. let chosen = match maybe_chosen { Some(proxy) => proxy, None => return, @@ -387,17 +402,13 @@ pub(crate) fn follow_hyperlink( if let Some(target_document) = chosen.document() { let target_window = target_document.window(); - // Step 9, dis-owning target's opener, if necessary - // will have been done as part of Step 7 above - // in choose_browsing_context/create_auxiliary_browsing_context. - - // Step 10, 11. TODO: if parsing the URL failed, navigate to error page. + // Step 9: Let urlString be the result of applying the URL serializer to urlRecord. + // TODO: Implement this. let attribute = subject.get_attribute(&ns!(), &local_name!("href")).unwrap(); let mut href = attribute.Value(); - // Step 11: append a hyperlink suffix. - // https://www.w3.org/Bugs/Public/show_bug.cgi?id=28925 + // Step 10: If hyperlinkSuffix is non-null, then append it to urlString. if let Some(suffix) = hyperlink_suffix { href.push_str(&suffix); } @@ -405,17 +416,20 @@ pub(crate) fn follow_hyperlink( return; }; - // Step 12. + // Step 11: Let referrerPolicy be the current state of subject's referrerpolicy content attribute. let referrer_policy = referrer_policy_for_element(subject); - // Step 13 + // Step 12: If subject's link types includes the noreferrer keyword, then set + // referrerPolicy to "no-referrer". let referrer = if relations.contains(LinkRelations::NO_REFERRER) { Referrer::NoReferrer } else { target_window.as_global_scope().get_referrer() }; - // Step 14 + // Step 13: Navigate targetNavigable to urlString using subject's node document, + // with referrerPolicy set to referrerPolicy, userInvolvement set to + // userInvolvement, and sourceElement set to subject. let pipeline_id = target_window.as_global_scope().pipeline_id(); let secure = target_window.as_global_scope().is_secure_context(); let load_data = LoadData::new( diff --git a/components/script/script_thread.rs b/components/script/script_thread.rs index df838581550..9f20a59cdf0 100644 --- a/components/script/script_thread.rs +++ b/components/script/script_thread.rs @@ -1069,6 +1069,8 @@ impl ScriptThread { let window = document.window(); let _realm = enter_realm(document.window()); for event in document.take_pending_input_events().into_iter() { + document.update_active_keyboard_modifiers(event.active_keyboard_modifiers); + match event.event { InputEvent::MouseButton(mouse_button_event) => { document.handle_mouse_button_event( diff --git a/components/shared/script/lib.rs b/components/shared/script/lib.rs index 701e6085dac..dfb7484d3bb 100644 --- a/components/shared/script/lib.rs +++ b/components/shared/script/lib.rs @@ -38,6 +38,7 @@ use euclid::{Rect, Scale, Size2D, UnknownUnit}; use http::{HeaderMap, Method}; use ipc_channel::Error as IpcError; use ipc_channel::ipc::{IpcReceiver, IpcSender}; +use keyboard_types::Modifiers; use log::warn; use malloc_size_of_derive::MallocSizeOf; use media::WindowGLContext; @@ -401,6 +402,8 @@ pub struct ConstellationInputEvent { /// The pressed mouse button state of the constellation when this input /// event was triggered. pub pressed_mouse_buttons: u16, + /// The currently active keyboard modifiers. + pub active_keyboard_modifiers: Modifiers, /// The [`InputEvent`] itself. pub event: InputEvent, }