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>
This commit is contained in:
Gae24 2025-01-15 20:45:29 +01:00 committed by GitHub
parent cd9e831e91
commit d470f219b1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 581 additions and 26 deletions

View file

@ -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"),
}
}
}

View file

@ -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,

View file

@ -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);
},
}
}
@ -4267,6 +4270,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")

View file

@ -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"),

View file

@ -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()
}
}

View file

@ -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>

View file

@ -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-datas files to match clipboard-event-datas items
// Step 7.1.4 Update clipboard-event-datas types to match clipboard-event-datas 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 events 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,

View file

@ -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()

View file

@ -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()

View file

@ -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;

View file

@ -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;
};

View file

@ -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;
}
}

View file

@ -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);

View file

@ -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
}

View file

@ -909,6 +909,9 @@ where
EmbedderEvent::Vsync => {
self.compositor.on_vsync();
},
EmbedderEvent::ClipboardAction(clipboard_event) => {
self.send_to_constellation(ConstellationMsg::Clipboard(clipboard_event));
},
}
false
}

View file

@ -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",
}
}
}

View file

@ -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"),

View file

@ -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,
}
}
}

View file

@ -156,6 +156,7 @@ mod from_servo {
Self::WebViewBlurred => target!("WebViewBlurred"),
Self::AllowUnload(..) => target!("AllowUnload"),
Self::Keyboard(..) => target!("Keyboard"),
Self::ClearClipboardContents => target!("ClearClipboardContents"),
Self::GetClipboardContents(..) => target!("GetClipboardContents"),
Self::SetClipboardContents(..) => target!("SetClipboardContents"),
Self::SetCursor(..) => target!("SetCursor"),
@ -236,6 +237,7 @@ mod to_servo {
Self::ReplaceNativeSurface(..) => target!("ReplaceNativeSurface"),
Self::Gamepad(..) => target!("Gamepad"),
Self::Vsync => target!("Vsync"),
Self::ClipboardAction(..) => target!("ClipboardAction"),
}
}
}

View file

@ -27,8 +27,8 @@ use servo::embedder_traits::{
};
use servo::ipc_channel::ipc::IpcSender;
use servo::script_traits::{
GamepadEvent, GamepadIndex, GamepadInputBounds, GamepadSupportedHapticEffects,
GamepadUpdateType, TouchEventType, TraversalDirection,
ClipboardEventType, GamepadEvent, GamepadIndex, GamepadInputBounds,
GamepadSupportedHapticEffects, GamepadUpdateType, TouchEventType, TraversalDirection,
};
use servo::servo_url::ServoUrl;
use servo::webrender_api::units::DeviceRect;
@ -483,6 +483,23 @@ where
Duration::from_secs(duration),
))
})
.shortcut(CMD_OR_CONTROL, 'X', || {
Some(EmbedderEvent::ClipboardAction(ClipboardEventType::Cut))
})
.shortcut(CMD_OR_CONTROL, 'C', || {
Some(EmbedderEvent::ClipboardAction(ClipboardEventType::Copy))
})
.shortcut(CMD_OR_CONTROL, 'V', || {
Some(EmbedderEvent::ClipboardAction(ClipboardEventType::Paste(
self.clipboard
.as_mut()
.and_then(|clipboard| clipboard.get_text().ok())
.unwrap_or_else(|| {
warn!("Error getting clipboard text. Returning empty string.");
String::new()
}),
)))
})
.shortcut(Modifiers::CONTROL, Key::F9, || {
Some(EmbedderEvent::CaptureWebRender)
})
@ -850,6 +867,11 @@ where
EmbedderMsg::Keyboard(key_event) => {
self.handle_key_from_servo(webview_id, key_event);
},
EmbedderMsg::ClearClipboardContents => {
self.clipboard
.as_mut()
.and_then(|clipboard| clipboard.clear().ok());
},
EmbedderMsg::GetClipboardContents(sender) => {
let contents = self
.clipboard

View file

@ -644,7 +644,8 @@ impl ServoGlue {
EmbedderMsg::ReportProfile(..) |
EmbedderMsg::EventDelivered(..) |
EmbedderMsg::PlayGamepadHapticEffect(..) |
EmbedderMsg::StopGamepadHapticEffect(..) => {},
EmbedderMsg::StopGamepadHapticEffect(..) |
EmbedderMsg::ClearClipboardContents => {},
}
}

View file

@ -7,6 +7,7 @@
"dom_bluetooth_testing_enabled": false,
"dom_canvas_capture_enabled": false,
"dom_canvas_text_enabled": true,
"dom_clipboardevent_enabled": true,
"dom_compositionevent_enabled": false,
"dom_crypto_subtle_enabled": true,
"dom_customelements_enabled": true,

View file

@ -13499,7 +13499,7 @@
]
],
"interfaces.https.html": [
"75b7d9bc3e68a1147cf2d86b2a7af2b14f45597a",
"71d2291cb162143e4abf298f0a23e6a06fe5c1bc",
[
null,
{}

View file

@ -38,6 +38,7 @@ test_interfaces([
"ChannelMergerNode",
"ChannelSplitterNode",
"CharacterData",
"ClipboardEvent",
"CloseEvent",
"ConstantSourceNode",
"CryptoKey",