diff options
author | Gae24 <96017547+Gae24@users.noreply.github.com> | 2025-01-15 20:45:29 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2025-01-15 19:45:29 +0000 |
commit | d470f219b1cb80f50bb9050a7649627397f7a5d9 (patch) | |
tree | ab726a47db048f1a14edab25d686f2c2b10e52d5 /components | |
parent | cd9e831e91e7587a71c5fa401b3721b5cfad8f43 (diff) | |
download | servo-d470f219b1cb80f50bb9050a7649627397f7a5d9.tar.gz servo-d470f219b1cb80f50bb9050a7649627397f7a5d9.zip |
Implement Clipboard Event Api (#33576)
* implement ClipboardEvent interface
Signed-off-by: Gae24 <96017547+Gae24@users.noreply.github.com>
* draft implementation of clipboard events
Signed-off-by: Gae24 <96017547+Gae24@users.noreply.github.com>
* handle received clipboard events inside html elemtents
Signed-off-by: Gae24 <96017547+Gae24@users.noreply.github.com>
* use rustdoc style
Signed-off-by: Gae24 <96017547+Gae24@users.noreply.github.com>
* fix compilation errors due to rebase
Signed-off-by: Gae24 <96017547+Gae24@users.noreply.github.com>
* update arboard crate
Signed-off-by: Gae24 <96017547+Gae24@users.noreply.github.com>
* improve paste events
Signed-off-by: Gae24 <96017547+Gae24@users.noreply.github.com>
* code cleanup
revert arboard crate's update, handle text only
Signed-off-by: Gae24 <96017547+Gae24@users.noreply.github.com>
* restrict visibility of some methods to script crate
Signed-off-by: Gae24 <96017547+Gae24@users.noreply.github.com>
* propagate CanGc argument
Signed-off-by: Gae24 <96017547+Gae24@users.noreply.github.com>
* simplify handle_clipboard_msg
Signed-off-by: Gae24 <96017547+Gae24@users.noreply.github.com>
* remove code duplication
Signed-off-by: Gae24 <96017547+Gae24@users.noreply.github.com>
* fix potential borrow hazard
Signed-off-by: Gae24 <96017547+Gae24@users.noreply.github.com>
* add clipboard_event pref, restore unit test code
Signed-off-by: Gae24 <96017547+Gae24@users.noreply.github.com>
* retrict visibility of some document's methods
Signed-off-by: Gae24 <96017547+Gae24@users.noreply.github.com>
* check if clipboardevent is trusted
Signed-off-by: Gae24 <96017547+Gae24@users.noreply.github.com>
* enable clipboardevent
Signed-off-by: Gae24 <96017547+Gae24@users.noreply.github.com>
* fix compilation for egl ports
Signed-off-by: Gae24 <96017547+Gae24@users.noreply.github.com>
---------
Signed-off-by: Gae24 <96017547+Gae24@users.noreply.github.com>
Diffstat (limited to 'components')
-rw-r--r-- | components/compositing/windowing.rs | 7 | ||||
-rw-r--r-- | components/config/prefs.rs | 2 | ||||
-rw-r--r-- | components/constellation/constellation.rs | 51 | ||||
-rw-r--r-- | components/constellation/tracing.rs | 3 | ||||
-rw-r--r-- | components/script/dom/clipboardevent.rs | 97 | ||||
-rw-r--r-- | components/script/dom/datatransfer.rs | 26 | ||||
-rw-r--r-- | components/script/dom/document.rs | 200 | ||||
-rw-r--r-- | components/script/dom/htmlinputelement.rs | 10 | ||||
-rw-r--r-- | components/script/dom/htmltextareaelement.rs | 8 | ||||
-rw-r--r-- | components/script/dom/mod.rs | 1 | ||||
-rw-r--r-- | components/script/dom/webidls/ClipboardEvent.webidl | 15 | ||||
-rw-r--r-- | components/script/drag_data_store.rs | 12 | ||||
-rw-r--r-- | components/script/script_thread.rs | 4 | ||||
-rw-r--r-- | components/script/textinput.rs | 93 | ||||
-rw-r--r-- | components/servo/lib.rs | 3 | ||||
-rw-r--r-- | components/shared/compositing/constellation_msg.rs | 8 | ||||
-rw-r--r-- | components/shared/embedder/lib.rs | 4 | ||||
-rw-r--r-- | components/shared/script/lib.rs | 28 |
18 files changed, 550 insertions, 22 deletions
diff --git a/components/compositing/windowing.rs b/components/compositing/windowing.rs index dce66be05ca..f2648246c2b 100644 --- a/components/compositing/windowing.rs +++ b/components/compositing/windowing.rs @@ -14,8 +14,8 @@ use keyboard_types::{CompositionEvent, KeyboardEvent}; use libc::c_void; use net::protocols::ProtocolRegistry; use script_traits::{ - GamepadEvent, MediaSessionActionType, MouseButton, Theme, TouchEventType, TouchId, - TraversalDirection, WheelDelta, + ClipboardEventType, GamepadEvent, MediaSessionActionType, MouseButton, Theme, TouchEventType, + TouchId, TraversalDirection, WheelDelta, }; use servo_geometry::{DeviceIndependentIntRect, DeviceIndependentIntSize, DeviceIndependentPixel}; use servo_url::ServoUrl; @@ -134,6 +134,8 @@ pub enum EmbedderEvent { Gamepad(GamepadEvent), /// Vertical Synchronization tick Vsync, + /// Sent when access to clipboard is required + ClipboardAction(ClipboardEventType), } impl Debug for EmbedderEvent { @@ -196,6 +198,7 @@ impl Debug for EmbedderEvent { EmbedderEvent::ReplaceNativeSurface(..) => write!(f, "ReplaceNativeSurface"), EmbedderEvent::Gamepad(..) => write!(f, "Gamepad"), EmbedderEvent::Vsync => write!(f, "Vsync"), + EmbedderEvent::ClipboardAction(_) => write!(f, "ClipboardAction"), } } } diff --git a/components/config/prefs.rs b/components/config/prefs.rs index 9c4edf30e3a..1561198f281 100644 --- a/components/config/prefs.rs +++ b/components/config/prefs.rs @@ -75,6 +75,7 @@ pub struct Preferences { pub dom_allow_scripts_to_close_windows: bool, pub dom_canvas_capture_enabled: bool, pub dom_canvas_text_enabled: bool, + pub dom_clipboardevent_enabled: bool, pub dom_composition_event_enabled: bool, pub dom_crypto_subtle_enabled: bool, pub dom_customelements_enabled: bool, @@ -236,6 +237,7 @@ impl Preferences { dom_bluetooth_testing_enabled: false, dom_canvas_capture_enabled: false, dom_canvas_text_enabled: true, + dom_clipboardevent_enabled: true, dom_composition_event_enabled: false, dom_crypto_subtle_enabled: true, dom_customelements_enabled: true, diff --git a/components/constellation/constellation.rs b/components/constellation/constellation.rs index 0558c234f51..50932dbad9d 100644 --- a/components/constellation/constellation.rs +++ b/components/constellation/constellation.rs @@ -138,13 +138,13 @@ use script_layout_interface::{LayoutFactory, ScriptThreadFactory}; use script_traits::CompositorEvent::{MouseButtonEvent, MouseMoveEvent}; use script_traits::{ webdriver_msg, AnimationState, AnimationTickType, AuxiliaryBrowsingContextLoadInfo, - BroadcastMsg, CompositorEvent, ConstellationControlMsg, DiscardBrowsingContext, - DocumentActivity, DocumentState, GamepadEvent, IFrameLoadInfo, IFrameLoadInfoWithData, - IFrameSandboxState, IFrameSizeMsg, Job, LayoutMsg as FromLayoutMsg, LoadData, LoadOrigin, - LogEntry, MediaSessionActionType, MessagePortMsg, MouseEventType, NavigationHistoryBehavior, - PortMessageTask, SWManagerMsg, SWManagerSenders, ScriptMsg as FromScriptMsg, - ScriptToConstellationChan, ServiceWorkerManagerFactory, ServiceWorkerMsg, - StructuredSerializedData, Theme, TraversalDirection, UpdatePipelineIdReason, + BroadcastMsg, ClipboardEventType, CompositorEvent, ConstellationControlMsg, + DiscardBrowsingContext, DocumentActivity, DocumentState, GamepadEvent, IFrameLoadInfo, + IFrameLoadInfoWithData, IFrameSandboxState, IFrameSizeMsg, Job, LayoutMsg as FromLayoutMsg, + LoadData, LoadOrigin, LogEntry, MediaSessionActionType, MessagePortMsg, MouseEventType, + NavigationHistoryBehavior, PortMessageTask, SWManagerMsg, SWManagerSenders, + ScriptMsg as FromScriptMsg, ScriptToConstellationChan, ServiceWorkerManagerFactory, + ServiceWorkerMsg, StructuredSerializedData, Theme, TraversalDirection, UpdatePipelineIdReason, WebDriverCommandMsg, WindowSizeData, WindowSizeType, }; use serde::{Deserialize, Serialize}; @@ -1491,6 +1491,9 @@ where FromCompositorMsg::Gamepad(gamepad_event) => { self.handle_gamepad_msg(gamepad_event); }, + FromCompositorMsg::Clipboard(clipboard_event) => { + self.handle_clipboard_msg(clipboard_event); + }, } } @@ -4269,6 +4272,40 @@ where #[cfg_attr( feature = "tracing", + tracing::instrument(skip_all, fields(servo_profiling = true)) + )] + fn handle_clipboard_msg(&mut self, event: ClipboardEventType) { + let focused_browsing_context_id = self + .webviews + .focused_webview() + .map(|(_, webview)| webview.focused_browsing_context_id); + + if let Some(browsing_context_id) = focused_browsing_context_id { + let event = CompositorEvent::ClipboardEvent(event); + let pipeline_id = match self.browsing_contexts.get(&browsing_context_id) { + Some(ctx) => ctx.pipeline_id, + None => { + return warn!( + "{}: Got clipboard event for nonexistent browsing context", + browsing_context_id, + ); + }, + }; + let msg = ConstellationControlMsg::SendEvent(pipeline_id, event); + let result = match self.pipelines.get(&pipeline_id) { + Some(pipeline) => pipeline.event_loop.send(msg), + None => { + return debug!("{}: Got clipboard event after closure", pipeline_id); + }, + }; + if let Err(e) = result { + self.handle_send_error(pipeline_id, e); + } + } + } + + #[cfg_attr( + feature = "tracing", tracing::instrument(skip_all, fields(servo_profiling = true), level = "trace") )] fn handle_reload_msg(&mut self, top_level_browsing_context_id: TopLevelBrowsingContextId) { diff --git a/components/constellation/tracing.rs b/components/constellation/tracing.rs index 88df4b49d6b..6c9924341da 100644 --- a/components/constellation/tracing.rs +++ b/components/constellation/tracing.rs @@ -93,6 +93,7 @@ mod from_compositor { Self::IMEDismissed => target!("IMEDismissed"), Self::ReadyToPresent(..) => target!("ReadyToPresent"), Self::Gamepad(..) => target!("Gamepad"), + Self::Clipboard(..) => target!("Clipboard"), } } } @@ -114,6 +115,7 @@ mod from_compositor { Self::CompositionEvent(..) => target_variant!("CompositionEvent"), Self::IMEDismissedEvent => target_variant!("IMEDismissedEvent"), Self::GamepadEvent(..) => target_variant!("GamepadEvent"), + Self::ClipboardEvent(..) => target_variant!("ClipboardEvent"), } } } @@ -215,6 +217,7 @@ mod from_script { Self::WebViewBlurred => target_variant!("WebViewBlurred"), Self::AllowUnload(..) => target_variant!("AllowUnload"), Self::Keyboard(..) => target_variant!("Keyboard"), + Self::ClearClipboardContents => target_variant!("ClearClipboardContents"), Self::GetClipboardContents(..) => target_variant!("GetClipboardContents"), Self::SetClipboardContents(..) => target_variant!("SetClipboardContents"), Self::SetCursor(..) => target_variant!("SetCursor"), diff --git a/components/script/dom/clipboardevent.rs b/components/script/dom/clipboardevent.rs new file mode 100644 index 00000000000..4791b73e7a9 --- /dev/null +++ b/components/script/dom/clipboardevent.rs @@ -0,0 +1,97 @@ +/* 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 dom_struct::dom_struct; +use js::rust::HandleObject; + +use crate::dom::bindings::codegen::Bindings::ClipboardEventBinding::{ + ClipboardEventInit, ClipboardEventMethods, +}; +use crate::dom::bindings::codegen::Bindings::EventBinding::EventMethods; +use crate::dom::bindings::inheritance::Castable; +use crate::dom::bindings::reflector::reflect_dom_object_with_proto; +use crate::dom::bindings::root::{DomRoot, MutNullableDom}; +use crate::dom::bindings::str::DOMString; +use crate::dom::datatransfer::DataTransfer; +use crate::dom::event::{Event, EventBubbles, EventCancelable}; +use crate::dom::window::Window; +use crate::script_runtime::CanGc; + +#[dom_struct] +pub struct ClipboardEvent { + event: Event, + clipboard_data: MutNullableDom<DataTransfer>, +} + +impl ClipboardEvent { + fn new_inherited() -> ClipboardEvent { + ClipboardEvent { + event: Event::new_inherited(), + clipboard_data: MutNullableDom::new(None), + } + } + + pub(crate) fn new( + window: &Window, + proto: Option<HandleObject>, + type_: DOMString, + can_bubble: EventBubbles, + cancelable: EventCancelable, + clipboard_data: Option<&DataTransfer>, + can_gc: CanGc, + ) -> DomRoot<ClipboardEvent> { + let ev = reflect_dom_object_with_proto( + Box::new(ClipboardEvent::new_inherited()), + window, + proto, + can_gc, + ); + ev.upcast::<Event>() + .InitEvent(type_, bool::from(can_bubble), bool::from(cancelable)); + ev.clipboard_data.set(clipboard_data); + ev + } + + pub(crate) fn set_clipboard_data(&self, clipboard_data: Option<&DataTransfer>) { + self.clipboard_data.set(clipboard_data); + } + + pub(crate) fn get_clipboard_data(&self) -> Option<DomRoot<DataTransfer>> { + self.clipboard_data.get() + } +} + +impl ClipboardEventMethods<crate::DomTypeHolder> for ClipboardEvent { + /// <https://www.w3.org/TR/clipboard-apis/#dom-clipboardevent-clipboardevent> + fn Constructor( + window: &Window, + proto: Option<HandleObject>, + can_gc: CanGc, + type_: DOMString, + init: &ClipboardEventInit, + ) -> DomRoot<ClipboardEvent> { + // Missing composed field + let bubbles = EventBubbles::from(init.parent.bubbles); + let cancelable = EventCancelable::from(init.parent.cancelable); + ClipboardEvent::new( + window, + proto, + type_, + bubbles, + cancelable, + init.clipboardData.as_deref(), + can_gc, + ) + } + + /// <https://www.w3.org/TR/clipboard-apis/#dom-clipboardevent-clipboarddata> + fn GetClipboardData(&self) -> Option<DomRoot<DataTransfer>> { + self.clipboard_data.get() + } + + /// <https://dom.spec.whatwg.org/#dom-event-istrusted> + fn IsTrusted(&self) -> bool { + self.event.IsTrusted() + } +} diff --git a/components/script/dom/datatransfer.rs b/components/script/dom/datatransfer.rs index 001d036919b..6f2e857c214 100644 --- a/components/script/dom/datatransfer.rs +++ b/components/script/dom/datatransfer.rs @@ -2,7 +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/. */ -use std::cell::RefCell; +use std::cell::{Ref, RefCell}; use std::rc::Rc; use dom_struct::dom_struct; @@ -64,11 +64,8 @@ impl DataTransfer { window: &Window, proto: Option<HandleObject>, can_gc: CanGc, + data_store: Rc<RefCell<Option<DragDataStore>>>, ) -> DomRoot<DataTransfer> { - let mut drag_data_store = DragDataStore::new(); - drag_data_store.set_mode(Mode::ReadWrite); - - let data_store = Rc::new(RefCell::new(Some(drag_data_store))); let item_list = DataTransferItemList::new(window, Rc::clone(&data_store)); reflect_dom_object_with_proto( @@ -78,6 +75,18 @@ impl DataTransfer { can_gc, ) } + + pub(crate) fn new( + window: &Window, + data_store: Rc<RefCell<Option<DragDataStore>>>, + can_gc: CanGc, + ) -> DomRoot<DataTransfer> { + Self::new_with_proto(window, None, can_gc, data_store) + } + + pub(crate) fn data_store(&self) -> Option<Ref<DragDataStore>> { + Ref::filter_map(self.data_store.borrow(), |data_store| data_store.as_ref()).ok() + } } impl DataTransferMethods<crate::DomTypeHolder> for DataTransfer { @@ -87,7 +96,12 @@ impl DataTransferMethods<crate::DomTypeHolder> for DataTransfer { proto: Option<HandleObject>, can_gc: CanGc, ) -> DomRoot<DataTransfer> { - DataTransfer::new_with_proto(window, proto, can_gc) + let mut drag_data_store = DragDataStore::new(); + drag_data_store.set_mode(Mode::ReadWrite); + + let data_store = Rc::new(RefCell::new(Some(drag_data_store))); + + DataTransfer::new_with_proto(window, proto, can_gc, data_store) } /// <https://html.spec.whatwg.org/multipage/#dom-datatransfer-dropeffect> diff --git a/components/script/dom/document.rs b/components/script/dom/document.rs index 11a0c0935cd..b73bec01967 100644 --- a/components/script/dom/document.rs +++ b/components/script/dom/document.rs @@ -48,8 +48,9 @@ use profile_traits::ipc as profile_ipc; use profile_traits::time::{TimerMetadata, TimerMetadataFrameType, TimerMetadataReflowType}; use script_layout_interface::{PendingRestyle, TrustedNodeAddress}; use script_traits::{ - AnimationState, AnimationTickType, CompositorEvent, DocumentActivity, MouseButton, - MouseEventType, ScriptMsg, TouchEventType, TouchId, UntrustedNodeAddress, WheelDelta, + AnimationState, AnimationTickType, ClipboardEventType, CompositorEvent, DocumentActivity, + MouseButton, MouseEventType, ScriptMsg, TouchEventType, TouchId, UntrustedNodeAddress, + WheelDelta, }; use servo_arc::Arc; use servo_atoms::Atom; @@ -111,11 +112,13 @@ use crate::dom::bindings::xmlname::{ namespace_from_domstring, validate_and_extract, xml_name_type, }; use crate::dom::cdatasection::CDATASection; +use crate::dom::clipboardevent::ClipboardEvent; use crate::dom::comment::Comment; use crate::dom::compositionevent::CompositionEvent; use crate::dom::cssstylesheet::CSSStyleSheet; use crate::dom::customelementregistry::CustomElementDefinition; use crate::dom::customevent::CustomEvent; +use crate::dom::datatransfer::DataTransfer; use crate::dom::documentfragment::DocumentFragment; use crate::dom::documentorshadowroot::{DocumentOrShadowRoot, StyleSheetInDocument}; use crate::dom::documenttype::DocumentType; @@ -182,6 +185,7 @@ use crate::dom::wheelevent::WheelEvent; use crate::dom::window::Window; use crate::dom::windowproxy::WindowProxy; use crate::dom::xpathevaluator::XPathEvaluator; +use crate::drag_data_store::{DragDataStore, Kind, Mode, PlainString}; use crate::fetch::FetchCanceller; use crate::iframe_collection::IFrameCollection; use crate::messaging::{CommonScriptMsg, MainThreadScriptMsg}; @@ -1443,6 +1447,198 @@ impl Document { event.fire(target, can_gc); } + /// <https://www.w3.org/TR/clipboard-apis/#clipboard-actions> + pub(crate) fn handle_clipboard_action( + &self, + action: ClipboardEventType, + can_gc: CanGc, + ) -> bool { + // The script_triggered flag is set if the action runs because of a script, e.g. document.execCommand() + let script_triggered = false; + + // The script_may_access_clipboard flag is set + // if action is paste and the script thread is allowed to read from clipboard or + // if action is copy or cut and the script thread is allowed to modify the clipboard + let script_may_access_clipboard = false; + + // Step 1 If the script-triggered flag is set and the script-may-access-clipboard flag is unset + if script_triggered && !script_may_access_clipboard { + return false; + } + + // Step 2 Fire a clipboard event + let event = ClipboardEvent::new( + &self.window, + None, + DOMString::from(action.as_str()), + EventBubbles::Bubbles, + EventCancelable::Cancelable, + None, + can_gc, + ); + self.fire_clipboard_event(&event, action, can_gc); + + // Step 3 If a script doesn't call preventDefault() + // the event will be handled inside target's VirtualMethods::handle_event + + let e = event.upcast::<Event>(); + + if !e.IsTrusted() { + return false; + } + + // Step 4 If the event was canceled, then + if e.DefaultPrevented() { + match e.Type().str() { + "copy" => { + // Step 4.1 Call the write content to the clipboard algorithm, + // passing on the DataTransferItemList items, a clear-was-called flag and a types-to-clear list. + if let Some(clipboard_data) = event.get_clipboard_data() { + let drag_data_store = + clipboard_data.data_store().expect("This shouldn't fail"); + self.write_content_to_the_clipboard(&drag_data_store); + } + }, + "cut" => { + // Step 4.1 Call the write content to the clipboard algorithm, + // passing on the DataTransferItemList items, a clear-was-called flag and a types-to-clear list. + if let Some(clipboard_data) = event.get_clipboard_data() { + let drag_data_store = + clipboard_data.data_store().expect("This shouldn't fail"); + self.write_content_to_the_clipboard(&drag_data_store); + } + + // Step 4.2 Fire a clipboard event named clipboardchange + self.fire_clipboardchange_event(can_gc); + }, + "paste" => return false, + _ => (), + } + } + //Step 5 + true + } + + /// <https://www.w3.org/TR/clipboard-apis/#fire-a-clipboard-event> + fn fire_clipboard_event( + &self, + event: &ClipboardEvent, + action: ClipboardEventType, + can_gc: CanGc, + ) { + // Step 1 Let clear_was_called be false + // Step 2 Let types_to_clear an empty list + let mut drag_data_store = DragDataStore::new(); + + // Step 4 let clipboard-entry be the sequence number of clipboard content, null if the OS doesn't support it. + + // Step 5 let trusted be true if the event is generated by the user agent, false otherwise + let trusted = true; + + // Step 6 if the context is editable: + let focused = self.get_focused_element(); + let body = self.GetBody(); + + let target = match (&focused, &body) { + (Some(focused), _) => focused.upcast(), + (&None, Some(body)) => body.upcast(), + (&None, &None) => self.window.upcast(), + }; + // Step 6.2 else TODO require Selection see https://github.com/w3c/clipboard-apis/issues/70 + + // Step 7 + match action { + ClipboardEventType::Copy | ClipboardEventType::Cut => { + // Step 7.2.1 + drag_data_store.set_mode(Mode::ReadWrite); + }, + ClipboardEventType::Paste(ref contents) => { + // Step 7.1.1 + drag_data_store.set_mode(Mode::ReadOnly); + // Step 7.1.2 If trusted or the implementation gives script-generated events access to the clipboard + if trusted { + // Step 7.1.2.1 For each clipboard-part on the OS clipboard: + + // Step 7.1.2.1.1 If clipboard-part contains plain text, then + let plain_string = PlainString::new( + DOMString::from_string(contents.to_string()), + DOMString::from("text/plain"), + ); + let _ = drag_data_store.add(Kind::Text(plain_string)); + + // Step 7.1.2.1.2 TODO If clipboard-part represents file references, then for each file reference + // Step 7.1.2.1.3 TODO If clipboard-part contains HTML- or XHTML-formatted text then + + // Step 7.1.3 Update clipboard-event-data’s files to match clipboard-event-data’s items + // Step 7.1.4 Update clipboard-event-data’s types to match clipboard-event-data’s items + } + }, + ClipboardEventType::Change => (), + } + + // Step 3 + let clipboard_event_data = DataTransfer::new( + &self.window, + Rc::new(RefCell::new(Some(drag_data_store))), + can_gc, + ); + + // Step 8 + event.set_clipboard_data(Some(&clipboard_event_data)); + let event = event.upcast::<Event>(); + // Step 9 + event.set_trusted(trusted); + // Step 10 Set event’s composed to true. + // Step 11 + event.dispatch(target, false, can_gc); + } + + pub(crate) fn fire_clipboardchange_event(&self, can_gc: CanGc) { + let clipboardchange_event = ClipboardEvent::new( + &self.window, + None, + DOMString::from("clipboardchange"), + EventBubbles::Bubbles, + EventCancelable::Cancelable, + None, + can_gc, + ); + self.fire_clipboard_event(&clipboardchange_event, ClipboardEventType::Change, can_gc); + } + + /// <https://www.w3.org/TR/clipboard-apis/#write-content-to-the-clipboard> + fn write_content_to_the_clipboard(&self, drag_data_store: &DragDataStore) { + // Step 1 + if drag_data_store.list_len() > 0 { + // Step 1.1 Clear the clipboard. + self.send_to_embedder(EmbedderMsg::ClearClipboardContents); + // Step 1.2 + for item in drag_data_store.iter_item_list() { + match item { + Kind::Text(string) => { + // Step 1.2.1.1 Ensure encoding is correct per OS and locale conventions + // Step 1.2.1.2 Normalize line endings according to platform conventions + // Step 1.2.1.3 + self.send_to_embedder(EmbedderMsg::SetClipboardContents(string.data())); + }, + Kind::File(_) => { + // Step 1.2.2 If data is of a type listed in the mandatory data types list, then + // Step 1.2.2.1 Place part on clipboard with the appropriate OS clipboard format description + // Step 1.2.3 Else this is left to the implementation + }, + } + } + } else { + // Step 2.1 + if drag_data_store.clear_was_called { + // Step 2.1.1 If types-to-clear list is empty, clear the clipboard + self.send_to_embedder(EmbedderMsg::ClearClipboardContents); + // Step 2.1.2 Else remove the types in the list from the clipboard + // As of now this can't be done with Arboard, and it's possible that will be removed from the spec + } + } + } + #[allow(unsafe_code)] pub(crate) unsafe fn handle_mouse_move_event( &self, diff --git a/components/script/dom/htmlinputelement.rs b/components/script/dom/htmlinputelement.rs index 7c4a1a626cd..9de33dcaa72 100644 --- a/components/script/dom/htmlinputelement.rs +++ b/components/script/dom/htmlinputelement.rs @@ -48,6 +48,7 @@ use crate::dom::bindings::inheritance::Castable; use crate::dom::bindings::reflector::DomObject; use crate::dom::bindings::root::{DomRoot, LayoutDom, MutNullableDom}; use crate::dom::bindings::str::{DOMString, USVString}; +use crate::dom::clipboardevent::ClipboardEvent; use crate::dom::compositionevent::CompositionEvent; use crate::dom::document::Document; use crate::dom::element::{AttributeMutation, Element, LayoutElementHelpers}; @@ -79,7 +80,10 @@ use crate::textinput::KeyReaction::{ DispatchInput, Nothing, RedrawSelection, TriggerDefaultAction, }; use crate::textinput::Lines::Single; -use crate::textinput::{Direction, SelectionDirection, TextInput, UTF16CodeUnits, UTF8Bytes}; +use crate::textinput::{ + handle_text_clipboard_action, Direction, SelectionDirection, TextInput, UTF16CodeUnits, + UTF8Bytes, +}; const DEFAULT_SUBMIT_VALUE: &str = "Submit"; const DEFAULT_RESET_VALUE: &str = "Reset"; @@ -2648,6 +2652,10 @@ impl VirtualMethods for HTMLInputElement { } event.mark_as_handled(); } + } else if let Some(clipboard_event) = event.downcast::<ClipboardEvent>() { + if !event.DefaultPrevented() { + handle_text_clipboard_action(self, &self.textinput, clipboard_event, CanGc::note()); + } } self.validity_state() diff --git a/components/script/dom/htmltextareaelement.rs b/components/script/dom/htmltextareaelement.rs index 9ea70e03bd6..fe0c822aa48 100644 --- a/components/script/dom/htmltextareaelement.rs +++ b/components/script/dom/htmltextareaelement.rs @@ -23,6 +23,7 @@ use crate::dom::bindings::error::ErrorResult; use crate::dom::bindings::inheritance::Castable; use crate::dom::bindings::root::{DomRoot, LayoutDom, MutNullableDom}; use crate::dom::bindings::str::DOMString; +use crate::dom::clipboardevent::ClipboardEvent; use crate::dom::compositionevent::CompositionEvent; use crate::dom::document::Document; use crate::dom::element::{AttributeMutation, Element, LayoutElementHelpers}; @@ -42,7 +43,8 @@ use crate::dom::validitystate::{ValidationFlags, ValidityState}; use crate::dom::virtualmethods::VirtualMethods; use crate::script_runtime::CanGc; use crate::textinput::{ - Direction, KeyReaction, Lines, SelectionDirection, TextInput, UTF16CodeUnits, UTF8Bytes, + handle_text_clipboard_action, Direction, KeyReaction, Lines, SelectionDirection, TextInput, + UTF16CodeUnits, UTF8Bytes, }; #[dom_struct] @@ -675,6 +677,10 @@ impl VirtualMethods for HTMLTextAreaElement { } event.mark_as_handled(); } + } else if let Some(clipboard_event) = event.downcast::<ClipboardEvent>() { + if !event.DefaultPrevented() { + handle_text_clipboard_action(self, &self.textinput, clipboard_event, CanGc::note()); + } } self.validity_state() diff --git a/components/script/dom/mod.rs b/components/script/dom/mod.rs index 8f51e796320..cfada01bf18 100644 --- a/components/script/dom/mod.rs +++ b/components/script/dom/mod.rs @@ -254,6 +254,7 @@ pub(crate) mod channelmergernode; pub(crate) mod channelsplitternode; pub(crate) mod characterdata; pub(crate) mod client; +pub(crate) mod clipboardevent; pub(crate) mod closeevent; pub(crate) mod comment; pub(crate) mod compositionevent; diff --git a/components/script/dom/webidls/ClipboardEvent.webidl b/components/script/dom/webidls/ClipboardEvent.webidl new file mode 100644 index 00000000000..d23700d8415 --- /dev/null +++ b/components/script/dom/webidls/ClipboardEvent.webidl @@ -0,0 +1,15 @@ +/* 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 http://mozilla.org/MPL/2.0/. */ + +// https://w3c.github.io/clipboard-apis/ + +[Exposed=Window, Pref="dom_clipboardevent_enabled"] +interface ClipboardEvent : Event { + constructor (DOMString type, optional ClipboardEventInit eventInitDict = {}); + readonly attribute DataTransfer? clipboardData; +}; + +dictionary ClipboardEventInit : EventInit { + DataTransfer? clipboardData = null; +}; diff --git a/components/script/drag_data_store.rs b/components/script/drag_data_store.rs index a4db339dff4..6073695de90 100644 --- a/components/script/drag_data_store.rs +++ b/components/script/drag_data_store.rs @@ -31,6 +31,10 @@ impl PlainString { pub(crate) fn new(data: DOMString, type_: DOMString) -> Self { Self { data, type_ } } + + pub fn data(&self) -> String { + self.data.to_string() + } } #[derive(Clone)] @@ -99,7 +103,6 @@ pub(crate) enum Mode { /// <https://html.spec.whatwg.org/multipage/#concept-dnd-rw> ReadWrite, /// <https://html.spec.whatwg.org/multipage/#concept-dnd-ro> - #[allow(dead_code)] // TODO this used by ClipboardEvent. ReadOnly, /// <https://html.spec.whatwg.org/multipage/#concept-dnd-p> Protected, @@ -115,6 +118,7 @@ pub(crate) struct DragDataStore { mode: Mode, /// <https://html.spec.whatwg.org/multipage/#drag-data-store-allowed-effects-state> allowed_effects_state: String, + pub clear_was_called: bool, } impl DragDataStore { @@ -128,6 +132,7 @@ impl DragDataStore { bitmap: None, mode: Mode::Protected, allowed_effects_state: String::from("uninitialized"), + clear_was_called: false, } } @@ -255,6 +260,10 @@ impl DragDataStore { self.item_list.len() } + pub(crate) fn iter_item_list(&self) -> std::slice::Iter<'_, Kind> { + self.item_list.iter() + } + pub(crate) fn get_item(&self, index: usize) -> Option<Kind> { self.item_list.get(index).cloned() } @@ -265,6 +274,7 @@ impl DragDataStore { pub(crate) fn clear_list(&mut self) { self.item_list.clear(); + self.clear_was_called = true; } } diff --git a/components/script/script_thread.rs b/components/script/script_thread.rs index 0cf14937c4c..08ce5fa7540 100644 --- a/components/script/script_thread.rs +++ b/components/script/script_thread.rs @@ -1141,6 +1141,10 @@ impl ScriptThread { CompositorEvent::GamepadEvent(gamepad_event) => { window.as_global_scope().handle_gamepad_event(gamepad_event); }, + + CompositorEvent::ClipboardEvent(clipboard_action) => { + document.handle_clipboard_action(clipboard_action, can_gc); + }, } } ScriptThread::set_user_interacting(false); diff --git a/components/script/textinput.rs b/components/script/textinput.rs index 338303fc7b8..d59cd5b6a5d 100644 --- a/components/script/textinput.rs +++ b/components/script/textinput.rs @@ -10,12 +10,21 @@ use std::default::Default; use std::ops::{Add, AddAssign, Range}; use keyboard_types::{Key, KeyState, Modifiers, ShortcutMatcher}; +use script_traits::ScriptToConstellationChan; use unicode_segmentation::UnicodeSegmentation; use crate::clipboard_provider::ClipboardProvider; +use crate::dom::bindings::cell::DomRefCell; +use crate::dom::bindings::codegen::Bindings::EventBinding::Event_Binding::EventMethods; +use crate::dom::bindings::inheritance::Castable; use crate::dom::bindings::str::DOMString; use crate::dom::compositionevent::CompositionEvent; +use crate::dom::event::Event; use crate::dom::keyboardevent::KeyboardEvent; +use crate::dom::node::NodeTraits; +use crate::dom::types::ClipboardEvent; +use crate::drag_data_store::{DragDataStore, Kind}; +use crate::script_runtime::CanGc; #[derive(Clone, Copy, PartialEq)] pub enum Selection { @@ -1128,4 +1137,88 @@ impl<T: ClipboardProvider> TextInput<T> { .fold(UTF8Bytes::zero(), |acc, x| acc + x.len_utf8()); self.edit_point.index = byte_offset; } + + fn paste_contents(&mut self, drag_data_store: &DragDataStore) { + for item in drag_data_store.iter_item_list() { + if let Kind::Text(string) = item { + self.insert_string(string.data()); + } + } + } +} + +/// <https://www.w3.org/TR/clipboard-apis/#clipboard-actions> step 3 +pub(crate) fn handle_text_clipboard_action( + owning_node: &impl NodeTraits, + textinput: &DomRefCell<TextInput<ScriptToConstellationChan>>, + event: &ClipboardEvent, + can_gc: CanGc, +) -> bool { + let e = event.upcast::<Event>(); + + if !e.IsTrusted() { + return false; + } + + // Step 3 + match e.Type().str() { + "copy" => { + let selection = textinput.borrow().get_selection_text(); + + // Step 3.1 Copy the selected contents, if any, to the clipboard + if let Some(text) = selection { + textinput + .borrow_mut() + .clipboard_provider + .set_clipboard_contents(text); + } + + // Step 3.2 Fire a clipboard event named clipboardchange + owning_node + .owner_document() + .fire_clipboardchange_event(can_gc); + }, + "cut" => { + let selection = textinput.borrow().get_selection_text(); + + // Step 3.1 If there is a selection in an editable context where cutting is enabled, then + if let Some(text) = selection { + // Step 3.1.1 Copy the selected contents, if any, to the clipboard + textinput + .borrow_mut() + .clipboard_provider + .set_clipboard_contents(text); + + // Step 3.1.2 Remove the contents of the selection from the document and collapse the selection. + textinput.borrow_mut().delete_char(Direction::Backward); + + // Step 3.1.3 Fire a clipboard event named clipboardchange + owning_node + .owner_document() + .fire_clipboardchange_event(can_gc); + + // Step 3.1.4 Queue tasks to fire any events that should fire due to the modification. + } else { + // Step 3.2 Else, if there is no selection or the context is not editable, then + return false; + } + }, + "paste" => { + // Step 3.1 If there is a selection or cursor in an editable context where pasting is enabled, then + if let Some(data) = event.get_clipboard_data() { + // Step 3.1.1 Insert the most suitable content found on the clipboard, if any, into the context. + let drag_data_store = data.data_store().expect("This shouldn't fail"); + textinput.borrow_mut().paste_contents(&drag_data_store); + + // Step 3.1.2 Queue tasks to fire any events that should fire due to the modification. + } else { + // Step 3.2 Else return false. + return false; + } + }, + _ => (), + } + + //Step 5 + true } diff --git a/components/servo/lib.rs b/components/servo/lib.rs index d9b2e560fba..a7215f372db 100644 --- a/components/servo/lib.rs +++ b/components/servo/lib.rs @@ -909,6 +909,9 @@ where EmbedderEvent::Vsync => { self.compositor.on_vsync(); }, + EmbedderEvent::ClipboardAction(clipboard_event) => { + self.send_to_constellation(ConstellationMsg::Clipboard(clipboard_event)); + }, } false } diff --git a/components/shared/compositing/constellation_msg.rs b/components/shared/compositing/constellation_msg.rs index 46d7cc5978f..8b641ecf46e 100644 --- a/components/shared/compositing/constellation_msg.rs +++ b/components/shared/compositing/constellation_msg.rs @@ -12,8 +12,9 @@ use embedder_traits::Cursor; use ipc_channel::ipc::IpcSender; use keyboard_types::{CompositionEvent, KeyboardEvent}; use script_traits::{ - AnimationTickType, CompositorEvent, GamepadEvent, LogEntry, MediaSessionActionType, Theme, - TraversalDirection, WebDriverCommandMsg, WindowSizeData, WindowSizeType, + AnimationTickType, ClipboardEventType, CompositorEvent, GamepadEvent, LogEntry, + MediaSessionActionType, Theme, TraversalDirection, WebDriverCommandMsg, WindowSizeData, + WindowSizeType, }; use servo_url::ServoUrl; @@ -88,6 +89,8 @@ pub enum ConstellationMsg { ReadyToPresent(Vec<WebViewId>), /// Gamepad state has changed Gamepad(GamepadEvent), + /// Inform the constellation of a clipboard event. + Clipboard(ClipboardEventType), } impl fmt::Debug for ConstellationMsg { @@ -134,6 +137,7 @@ impl ConstellationMsg { ClearCache => "ClearCache", ReadyToPresent(..) => "ReadyToPresent", Gamepad(..) => "Gamepad", + Clipboard(..) => "Clipboard", } } } diff --git a/components/shared/embedder/lib.rs b/components/shared/embedder/lib.rs index 2548cad70f3..b03fa1035df 100644 --- a/components/shared/embedder/lib.rs +++ b/components/shared/embedder/lib.rs @@ -192,6 +192,8 @@ pub enum EmbedderMsg { AllowUnload(IpcSender<bool>), /// Sends an unconsumed key event back to the embedder. Keyboard(KeyboardEvent), + /// Inform embedder to clear the clipboard + ClearClipboardContents, /// Gets system clipboard contents GetClipboardContents(IpcSender<String>), /// Sets system clipboard contents @@ -256,6 +258,7 @@ pub enum CompositorEventVariant { CompositionEvent, IMEDismissedEvent, GamepadEvent, + ClipboardEvent, } impl Debug for EmbedderMsg { @@ -269,6 +272,7 @@ impl Debug for EmbedderMsg { EmbedderMsg::AllowUnload(..) => write!(f, "AllowUnload"), EmbedderMsg::AllowNavigationRequest(..) => write!(f, "AllowNavigationRequest"), EmbedderMsg::Keyboard(..) => write!(f, "Keyboard"), + EmbedderMsg::ClearClipboardContents => write!(f, "ClearClipboardContents"), EmbedderMsg::GetClipboardContents(..) => write!(f, "GetClipboardContents"), EmbedderMsg::SetClipboardContents(..) => write!(f, "SetClipboardContents"), EmbedderMsg::SetCursor(..) => write!(f, "SetCursor"), diff --git a/components/shared/script/lib.rs b/components/shared/script/lib.rs index e6901be54b6..60f7abba6d6 100644 --- a/components/shared/script/lib.rs +++ b/components/shared/script/lib.rs @@ -535,6 +535,31 @@ pub struct WheelDelta { pub mode: WheelMode, } +/// The types of clipboard events +#[derive(Clone, Debug, Deserialize, Serialize)] +pub enum ClipboardEventType { + /// Contents of the system clipboard are changed + Change, + /// Copy + Copy, + /// Cut + Cut, + /// Paste + Paste(String), +} + +impl ClipboardEventType { + /// Convert to event name + pub fn as_str(&self) -> &str { + match *self { + ClipboardEventType::Change => "clipboardchange", + ClipboardEventType::Copy => "copy", + ClipboardEventType::Cut => "cut", + ClipboardEventType::Paste(..) => "paste", + } + } +} + /// Events from the compositor that the script thread needs to know about #[derive(Debug, Deserialize, Serialize)] pub enum CompositorEvent { @@ -574,6 +599,8 @@ pub enum CompositorEvent { IMEDismissedEvent, /// Connected gamepad state updated GamepadEvent(GamepadEvent), + /// A clipboard action was requested + ClipboardEvent(ClipboardEventType), } impl From<&CompositorEvent> for CompositorEventVariant { @@ -588,6 +615,7 @@ impl From<&CompositorEvent> for CompositorEventVariant { CompositorEvent::CompositionEvent(..) => CompositorEventVariant::CompositionEvent, CompositorEvent::IMEDismissedEvent => CompositorEventVariant::IMEDismissedEvent, CompositorEvent::GamepadEvent(..) => CompositorEventVariant::GamepadEvent, + CompositorEvent::ClipboardEvent(..) => CompositorEventVariant::ClipboardEvent, } } } |