blob: 4bf5ee54e340b184d73fb1cb7693cc4503380875 [file] [log] [blame] [edit]
/* 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 std::array::from_ref;
use std::cell::{Cell, RefCell};
use std::f64::consts::PI;
use std::mem;
use std::rc::Rc;
use std::time::{Duration, Instant};
use base::generic_channel;
use constellation_traits::ScriptToConstellationMessage;
use embedder_traits::{
Cursor, EditingActionEvent, EmbedderMsg, GamepadEvent as EmbedderGamepadEvent,
GamepadSupportedHapticEffects, GamepadUpdateType, ImeEvent, InputEvent,
KeyboardEvent as EmbedderKeyboardEvent, MouseButton, MouseButtonAction, MouseButtonEvent,
MouseLeftViewportEvent, ScrollEvent, TouchEvent as EmbedderTouchEvent, TouchEventType, TouchId,
UntrustedNodeAddress, WheelEvent as EmbedderWheelEvent,
};
use euclid::Point2D;
use ipc_channel::ipc;
use keyboard_types::{Code, Key, KeyState, Modifiers, NamedKey};
use layout_api::node_id_from_scroll_id;
use script_bindings::codegen::GenericBindings::DocumentBinding::DocumentMethods;
use script_bindings::codegen::GenericBindings::EventBinding::EventMethods;
use script_bindings::codegen::GenericBindings::NavigatorBinding::NavigatorMethods;
use script_bindings::codegen::GenericBindings::NodeBinding::NodeMethods;
use script_bindings::codegen::GenericBindings::PerformanceBinding::PerformanceMethods;
use script_bindings::codegen::GenericBindings::TouchBinding::TouchMethods;
use script_bindings::codegen::GenericBindings::WindowBinding::WindowMethods;
use script_bindings::inheritance::Castable;
use script_bindings::num::Finite;
use script_bindings::root::{Dom, DomRoot, DomSlice};
use script_bindings::script_runtime::CanGc;
use script_bindings::str::DOMString;
use script_traits::ConstellationInputEvent;
use servo_config::pref;
use style_traits::CSSPixel;
use xml5ever::{local_name, ns};
use crate::dom::bindings::cell::DomRefCell;
use crate::dom::bindings::refcounted::Trusted;
use crate::dom::bindings::root::MutNullableDom;
use crate::dom::clipboardevent::ClipboardEventType;
use crate::dom::document::{FireMouseEventType, FocusInitiator, TouchEventResult};
use crate::dom::event::{EventBubbles, EventCancelable, EventDefault};
use crate::dom::gamepad::gamepad::{Gamepad, contains_user_gesture};
use crate::dom::gamepad::gamepadevent::GamepadEventType;
use crate::dom::inputevent::HitTestResult;
use crate::dom::node::{self, Node, ShadowIncluding};
use crate::dom::pointerevent::PointerId;
use crate::dom::types::{
ClipboardEvent, CompositionEvent, DataTransfer, Element, Event, EventTarget, GlobalScope,
HTMLAnchorElement, KeyboardEvent, MouseEvent, PointerEvent, Touch, TouchEvent, TouchList,
WheelEvent, Window,
};
use crate::drag_data_store::{DragDataStore, Kind, Mode};
use crate::realms::enter_realm;
/// The [`DocumentEventHandler`] is a structure responsible for handling input events for
/// the [`crate::Document`] and storing data related to event handling. It exists to
/// decrease the size of the [`crate::Document`] structure.
#[derive(JSTraceable, MallocSizeOf)]
#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
pub(crate) struct DocumentEventHandler {
/// The [`Window`] element for this [`DocumentEventHandler`].
window: Dom<Window>,
/// Pending input events, to be handled at the next rendering opportunity.
#[no_trace]
#[ignore_malloc_size_of = "CompositorEvent contains data from outside crates"]
pending_input_events: DomRefCell<Vec<ConstellationInputEvent>>,
/// The index of the last mouse move event in the pending compositor events queue.
mouse_move_event_index: DomRefCell<Option<usize>>,
/// <https://w3c.github.io/uievents/#event-type-dblclick>
#[ignore_malloc_size_of = "Defined in std"]
#[no_trace]
last_click_info: DomRefCell<Option<(Instant, Point2D<f32, CSSPixel>)>>,
/// The element that is currently hovered by the cursor.
current_hover_target: MutNullableDom<Element>,
/// The most recent mouse movement point, used for processing `mouseleave` events.
#[no_trace]
most_recent_mousemove_point: Cell<Option<Point2D<f32, CSSPixel>>>,
/// The currently set [`Cursor`] or `None` if the `Document` isn't being hovered
/// by the cursor.
#[no_trace]
current_cursor: Cell<Option<Cursor>>,
/// <http://w3c.github.io/touch-events/#dfn-active-touch-point>
active_touch_points: DomRefCell<Vec<Dom<Touch>>>,
/// The active keyboard modifiers for the WebView. This is updated when receiving any input event.
#[no_trace]
active_keyboard_modifiers: Cell<Modifiers>,
}
impl DocumentEventHandler {
pub(crate) fn new(window: &Window) -> Self {
Self {
window: Dom::from_ref(window),
pending_input_events: Default::default(),
mouse_move_event_index: Default::default(),
last_click_info: Default::default(),
current_hover_target: Default::default(),
most_recent_mousemove_point: Default::default(),
current_cursor: Default::default(),
active_touch_points: Default::default(),
active_keyboard_modifiers: Default::default(),
}
}
/// 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();
if matches!(event.event, InputEvent::MouseMove(..)) {
// First try to replace any existing mouse move event.
if let Some(mouse_move_event) = self
.mouse_move_event_index
.borrow()
.and_then(|index| pending_compositor_events.get_mut(index))
{
*mouse_move_event = event;
return;
}
*self.mouse_move_event_index.borrow_mut() = Some(pending_compositor_events.len());
}
pending_compositor_events.push(event);
}
/// Whether or not this [`Document`] has any pending input events to be processed during
/// "update the rendering."
pub(crate) fn has_pending_input_events(&self) -> bool {
!self.pending_input_events.borrow().is_empty()
}
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)
}
}
pub(crate) fn handle_pending_input_events(&self, can_gc: CanGc) {
let _realm = enter_realm(&*self.window);
// Reset the mouse event index.
*self.mouse_move_event_index.borrow_mut() = None;
let pending_input_events = mem::take(&mut *self.pending_input_events.borrow_mut());
for event in pending_input_events {
self.active_keyboard_modifiers
.set(event.active_keyboard_modifiers);
match event.event.clone() {
InputEvent::MouseButton(mouse_button_event) => {
self.handle_native_mouse_button_event(mouse_button_event, &event, can_gc);
},
InputEvent::MouseMove(_) => {
self.handle_native_mouse_move_event(&event, can_gc);
},
InputEvent::MouseLeftViewport(mouse_leave_event) => {
self.handle_mouse_left_viewport_event(&event, &mouse_leave_event, can_gc);
},
InputEvent::Touch(touch_event) => {
self.handle_touch_event(touch_event, &event, can_gc);
},
InputEvent::Wheel(wheel_event) => {
self.handle_wheel_event(wheel_event, &event, can_gc);
},
InputEvent::Keyboard(keyboard_event) => {
self.handle_keyboard_event(keyboard_event, can_gc);
},
InputEvent::Ime(ime_event) => {
self.handle_ime_event(ime_event, can_gc);
},
InputEvent::Gamepad(gamepad_event) => {
self.handle_gamepad_event(gamepad_event);
},
InputEvent::EditingAction(editing_action_event) => {
self.handle_editing_action(editing_action_event, can_gc);
},
InputEvent::Scroll(scroll_event) => {
self.handle_embedder_scroll_event(scroll_event);
},
}
self.notify_webdriver_input_event_completed(event.event);
}
}
fn notify_webdriver_input_event_completed(&self, event: InputEvent) {
let Some(id) = event.webdriver_message_id() else {
return;
};
// Webdriver should be notified once all current dom events have been processed.
let trusted_window = Trusted::new(&*self.window);
self.window
.as_global_scope()
.task_manager()
.dom_manipulation_task_source()
.queue(task!(notify_webdriver_input_event_completed: move || {
let window = trusted_window.root();
window.send_to_constellation(ScriptToConstellationMessage::WebDriverInputComplete(id));
}));
}
pub(crate) fn set_cursor(&self, cursor: Option<Cursor>) {
if cursor == self.current_cursor.get() {
return;
}
self.current_cursor.set(cursor);
self.window.send_to_embedder(EmbedderMsg::SetCursor(
self.window.webview_id(),
cursor.unwrap_or_default(),
));
}
fn handle_mouse_left_viewport_event(
&self,
input_event: &ConstellationInputEvent,
mouse_leave_event: &MouseLeftViewportEvent,
can_gc: CanGc,
) {
if let Some(current_hover_target) = self.current_hover_target.get() {
let current_hover_target = current_hover_target.upcast::<Node>();
for element in current_hover_target
.inclusive_ancestors(ShadowIncluding::No)
.filter_map(DomRoot::downcast::<Element>)
{
element.set_hover_state(false);
element.set_active_state(false);
}
if let Some(hit_test_result) = self
.most_recent_mousemove_point
.get()
.and_then(|point| self.window.hit_test_from_point_in_viewport(point))
{
MouseEvent::new_simple(
&self.window,
FireMouseEventType::Out,
EventBubbles::Bubbles,
EventCancelable::Cancelable,
&hit_test_result,
input_event,
can_gc,
)
.upcast::<Event>()
.fire(current_hover_target.upcast(), can_gc);
self.handle_mouse_enter_leave_event(
DomRoot::from_ref(current_hover_target),
None,
FireMouseEventType::Leave,
&hit_test_result,
input_event,
can_gc,
);
}
}
// We do not want to always inform the embedder that cursor has been set to the
// default cursor, in order to avoid a timing issue when moving between `<iframe>`
// elements. There is currently no way to control which `SetCursor` message will
// reach the embedder first. This is safer when leaving the `WebView` entirely.
if !mouse_leave_event.focus_moving_to_another_iframe {
// If focus is moving to another frame, it will decide what the new status
// text is, but if this mouse leave event is leaving the WebView entirely,
// then clear it.
self.window
.send_to_embedder(EmbedderMsg::Status(self.window.webview_id(), None));
self.set_cursor(None);
} else {
self.current_cursor.set(None);
}
self.current_hover_target.set(None);
self.most_recent_mousemove_point.set(None);
}
fn handle_mouse_enter_leave_event(
&self,
event_target: DomRoot<Node>,
related_target: Option<DomRoot<Node>>,
event_type: FireMouseEventType,
hit_test_result: &HitTestResult,
input_event: &ConstellationInputEvent,
can_gc: CanGc,
) {
assert!(matches!(
event_type,
FireMouseEventType::Enter | FireMouseEventType::Leave
));
let common_ancestor = match related_target.as_ref() {
Some(related_target) => event_target
.common_ancestor(related_target, ShadowIncluding::No)
.unwrap_or_else(|| DomRoot::from_ref(&*event_target)),
None => DomRoot::from_ref(&*event_target),
};
// We need to create a target chain in case the event target shares
// its boundaries with its ancestors.
let mut targets = vec![];
let mut current = Some(event_target);
while let Some(node) = current {
if node == common_ancestor {
break;
}
current = node.GetParentNode();
targets.push(node);
}
// The order for dispatching mouseenter events starts from the topmost
// common ancestor of the event target and the related target.
if event_type == FireMouseEventType::Enter {
targets = targets.into_iter().rev().collect();
}
for target in targets {
MouseEvent::new_simple(
&self.window,
event_type,
EventBubbles::DoesNotBubble,
EventCancelable::NotCancelable,
hit_test_result,
input_event,
can_gc,
)
.upcast::<Event>()
.fire(target.upcast(), can_gc);
}
}
/// <https://w3c.github.io/uievents/#handle-native-mouse-move>
fn handle_native_mouse_move_event(&self, input_event: &ConstellationInputEvent, can_gc: CanGc) {
// Ignore all incoming events without a hit test.
let Some(hit_test_result) = self.window.hit_test_from_input_event(input_event) else {
return;
};
// Update the cursor when the mouse moves, if it has changed.
self.set_cursor(Some(hit_test_result.cursor));
let Some(new_target) = hit_test_result
.node
.inclusive_ancestors(ShadowIncluding::No)
.filter_map(DomRoot::downcast::<Element>)
.next()
else {
return;
};
let target_has_changed = self
.current_hover_target
.get()
.is_none_or(|old_target| old_target != new_target);
// Here we know the target has changed, so we must update the state,
// dispatch mouseout to the previous one, mouseover to the new one.
if target_has_changed {
// Dispatch mouseout and mouseleave to previous target.
if let Some(old_target) = self.current_hover_target.get() {
let old_target_is_ancestor_of_new_target = old_target
.upcast::<Node>()
.is_ancestor_of(new_target.upcast::<Node>());
// If the old target is an ancestor of the new target, this can be skipped
// completely, since the node's hover state will be reset below.
if !old_target_is_ancestor_of_new_target {
for element in old_target
.upcast::<Node>()
.inclusive_ancestors(ShadowIncluding::No)
.filter_map(DomRoot::downcast::<Element>)
{
element.set_hover_state(false);
element.set_active_state(false);
}
}
MouseEvent::new_simple(
&self.window,
FireMouseEventType::Out,
EventBubbles::Bubbles,
EventCancelable::Cancelable,
&hit_test_result,
input_event,
can_gc,
)
.upcast::<Event>()
.fire(old_target.upcast(), can_gc);
if !old_target_is_ancestor_of_new_target {
let event_target = DomRoot::from_ref(old_target.upcast::<Node>());
let moving_into = Some(DomRoot::from_ref(new_target.upcast::<Node>()));
self.handle_mouse_enter_leave_event(
event_target,
moving_into,
FireMouseEventType::Leave,
&hit_test_result,
input_event,
can_gc,
);
}
}
// Dispatch mouseover and mouseenter to new target.
for element in new_target
.upcast::<Node>()
.inclusive_ancestors(ShadowIncluding::No)
.filter_map(DomRoot::downcast::<Element>)
{
element.set_hover_state(true);
}
MouseEvent::new_simple(
&self.window,
FireMouseEventType::Over,
EventBubbles::Bubbles,
EventCancelable::Cancelable,
&hit_test_result,
input_event,
can_gc,
)
.upcast::<Event>()
.fire(new_target.upcast(), can_gc);
let moving_from = self
.current_hover_target
.get()
.map(|old_target| DomRoot::from_ref(old_target.upcast::<Node>()));
let event_target = DomRoot::from_ref(new_target.upcast::<Node>());
self.handle_mouse_enter_leave_event(
event_target,
moving_from,
FireMouseEventType::Enter,
&hit_test_result,
input_event,
can_gc,
);
}
// Send mousemove event to topmost target, unless it's an iframe, in which case the
// compositor should have also sent an event to the inner document.
MouseEvent::new_simple(
&self.window,
FireMouseEventType::Move,
EventBubbles::Bubbles,
EventCancelable::Cancelable,
&hit_test_result,
input_event,
can_gc,
)
.upcast::<Event>()
.fire(new_target.upcast(), can_gc);
self.update_current_hover_target_and_status(Some(new_target));
self.most_recent_mousemove_point
.set(Some(hit_test_result.point_in_frame));
}
fn update_current_hover_target_and_status(&self, new_hover_target: Option<DomRoot<Element>>) {
let current_hover_target = self.current_hover_target.get();
if current_hover_target == new_hover_target {
return;
}
let previous_hover_target = self.current_hover_target.get();
self.current_hover_target.set(new_hover_target.as_deref());
// If the new hover target is an anchor with a status value, inform the embedder
// of the new value.
if let Some(target) = self.current_hover_target.get() {
if let Some(anchor) = target
.upcast::<Node>()
.inclusive_ancestors(ShadowIncluding::No)
.filter_map(DomRoot::downcast::<HTMLAnchorElement>)
.next()
{
let status = anchor
.upcast::<Element>()
.get_attribute(&ns!(), &local_name!("href"))
.and_then(|href| {
let value = href.value();
let url = self.window.get_url();
url.join(&value).map(|url| url.to_string()).ok()
});
self.window
.send_to_embedder(EmbedderMsg::Status(self.window.webview_id(), status));
return;
}
}
// No state was set above, which means that the new value of the status in the embedder
// should be `None`. Set that now. If `previous_hover_target` is `None` that means this
// is the first mouse move event we are seeing after getting the cursor. In that case,
// we also clear the status.
if previous_hover_target.is_none_or(|previous_hover_target| {
previous_hover_target
.upcast::<Node>()
.inclusive_ancestors(ShadowIncluding::No)
.filter_map(DomRoot::downcast::<HTMLAnchorElement>)
.next()
.is_some()
}) {
self.window
.send_to_embedder(EmbedderMsg::Status(self.window.webview_id(), None));
}
}
pub(crate) fn handle_refresh_cursor(&self) {
let Some(most_recent_mousemove_point) = self.most_recent_mousemove_point.get() else {
return;
};
let Some(hit_test_result) = self
.window
.hit_test_from_point_in_viewport(most_recent_mousemove_point)
else {
return;
};
self.set_cursor(Some(hit_test_result.cursor));
}
/// <https://w3c.github.io/uievents/#mouseevent-algorithms>
/// Handles native mouse down, mouse up, mouse click.
fn handle_native_mouse_button_event(
&self,
event: MouseButtonEvent,
input_event: &ConstellationInputEvent,
can_gc: CanGc,
) {
// Ignore all incoming events without a hit test.
let Some(hit_test_result) = self.window.hit_test_from_input_event(input_event) else {
return;
};
debug!(
"{:?}: at {:?}",
event.action, hit_test_result.point_in_frame
);
let Some(el) = hit_test_result
.node
.inclusive_ancestors(ShadowIncluding::Yes)
.filter_map(DomRoot::downcast::<Element>)
.next()
else {
return;
};
let node = el.upcast::<Node>();
debug!("{:?} on {:?}", event.action, node.debug_str());
// https://w3c.github.io/uievents/#hit-test
// Prevent mouse event if element is disabled.
// TODO: also inert.
if el.is_actually_disabled() {
return;
}
let dom_event = DomRoot::upcast::<Event>(MouseEvent::for_platform_mouse_event(
event,
input_event.pressed_mouse_buttons,
&self.window,
&hit_test_result,
input_event.active_keyboard_modifiers,
can_gc,
));
let activatable = el.as_maybe_activatable();
match event.action {
// https://w3c.github.io/uievents/#handle-native-mouse-click
MouseButtonAction::Click => {
el.set_click_in_progress(true);
dom_event.dispatch(node.upcast(), false, can_gc);
el.set_click_in_progress(false);
self.maybe_fire_dblclick(node, &hit_test_result, input_event, can_gc);
},
// https://w3c.github.io/uievents/#handle-native-mouse-down
MouseButtonAction::Down => {
if let Some(a) = activatable {
a.enter_formal_activation_state();
}
// (TODO) Step 6. Maybe send pointerdown event with `dom_event`.
// For a node within a text input UA shadow DOM,
// delegate the focus target into its shadow host.
// TODO: This focus delegation should be done
// with shadow DOM delegateFocus attribute.
let target_el = el.find_focusable_shadow_host_if_necessary();
let document = self.window.Document();
document.begin_focus_transaction();
// Try to focus `el`. If it's not focusable, focus the document instead.
document.request_focus(None, FocusInitiator::Local, can_gc);
document.request_focus(target_el.as_deref(), FocusInitiator::Local, can_gc);
// Step 7. Let result = dispatch event at target
let result = dom_event.dispatch(node.upcast(), false, can_gc);
// Step 8. If result is true and target is a focusable area
// that is click focusable, then Run the focusing steps at target.
if result && document.has_focus_transaction() {
document.commit_focus_transaction(FocusInitiator::Local, can_gc);
}
// Step 9. If mbutton is the secondary mouse button, then
// Maybe show context menu with native, target.
if let MouseButton::Right = event.button {
self.maybe_show_context_menu(
node.upcast(),
&hit_test_result,
input_event,
can_gc,
);
}
},
// https://w3c.github.io/uievents/#handle-native-mouse-up
MouseButtonAction::Up => {
if let Some(a) = activatable {
a.exit_formal_activation_state();
}
// (TODO) Step 6. Maybe send pointerup event with `dom_event``.
// Step 7. dispatch event at target.
dom_event.dispatch(node.upcast(), false, can_gc);
},
}
}
/// <https://www.w3.org/TR/uievents/#maybe-show-context-menu>
fn maybe_show_context_menu(
&self,
target: &EventTarget,
hit_test_result: &HitTestResult,
input_event: &ConstellationInputEvent,
can_gc: CanGc,
) {
// <https://w3c.github.io/uievents/#contextmenu>
let menu_event = PointerEvent::new(
&self.window, // window
DOMString::from("contextmenu"), // type
EventBubbles::Bubbles, // can_bubble
EventCancelable::Cancelable, // cancelable
Some(&self.window), // view
0, // detail
hit_test_result.point_in_frame.to_i32(),
hit_test_result.point_in_frame.to_i32(),
hit_test_result
.point_relative_to_initial_containing_block
.to_i32(),
input_event.active_keyboard_modifiers,
2i16, // button, right mouse button
input_event.pressed_mouse_buttons,
None, // related_target
None, // point_in_target
PointerId::Mouse as i32, // pointer_id
1, // width
1, // height
0.5, // pressure
0.0, // tangential_pressure
0, // tilt_x
0, // tilt_y
0, // twist
PI / 2.0, // altitude_angle
0.0, // azimuth_angle
DOMString::from("mouse"), // pointer_type
true, // is_primary
vec![], // coalesced_events
vec![], // predicted_events
can_gc,
);
// Step 3. Let result = dispatch menuevent at target.
let result = menu_event.upcast::<Event>().fire(target, can_gc);
// Step 4. If result is true, then show the UA context menu
if result {
let (sender, receiver) =
generic_channel::channel().expect("Failed to create IPC channel.");
self.window.send_to_embedder(EmbedderMsg::ShowContextMenu(
self.window.webview_id(),
sender,
None,
vec![],
));
let _ = receiver.recv().unwrap();
};
}
fn maybe_fire_dblclick(
&self,
target: &Node,
hit_test_result: &HitTestResult,
input_event: &ConstellationInputEvent,
can_gc: CanGc,
) {
// https://w3c.github.io/uievents/#event-type-dblclick
let now = Instant::now();
let point_in_frame = hit_test_result.point_in_frame;
let opt = self.last_click_info.borrow_mut().take();
if let Some((last_time, last_pos)) = opt {
let double_click_timeout =
Duration::from_millis(pref!(dom_document_dblclick_timeout) as u64);
let double_click_distance_threshold = pref!(dom_document_dblclick_dist) as u64;
// Calculate distance between this click and the previous click.
let line = point_in_frame - last_pos;
let dist = (line.dot(line) as f64).sqrt();
if now.duration_since(last_time) < double_click_timeout &&
dist < double_click_distance_threshold as f64
{
// A double click has occurred if this click is within a certain time and dist. of previous click.
let click_count = 2;
let event = MouseEvent::new(
&self.window,
DOMString::from("dblclick"),
EventBubbles::Bubbles,
EventCancelable::Cancelable,
Some(&self.window),
click_count,
point_in_frame.to_i32(),
point_in_frame.to_i32(),
hit_test_result
.point_relative_to_initial_containing_block
.to_i32(),
input_event.active_keyboard_modifiers,
0i16,
input_event.pressed_mouse_buttons,
None,
None,
can_gc,
);
event.upcast::<Event>().fire(target.upcast(), can_gc);
// When a double click occurs, self.last_click_info is left as None so that a
// third sequential click will not cause another double click.
return;
}
}
// Update last_click_info with the time and position of the click.
*self.last_click_info.borrow_mut() = Some((now, point_in_frame));
}
fn handle_touch_event(
&self,
event: EmbedderTouchEvent,
input_event: &ConstellationInputEvent,
can_gc: CanGc,
) {
let result = self.handle_touch_event_inner(event, input_event, can_gc);
if let (TouchEventResult::Processed(handled), true) = (result, event.is_cancelable()) {
let sequence_id = event.expect_sequence_id();
let result = if handled {
embedder_traits::TouchEventResult::DefaultAllowed(sequence_id, event.event_type)
} else {
embedder_traits::TouchEventResult::DefaultPrevented(sequence_id, event.event_type)
};
self.window
.send_to_constellation(ScriptToConstellationMessage::TouchEventProcessed(result));
}
}
fn handle_touch_event_inner(
&self,
event: EmbedderTouchEvent,
input_event: &ConstellationInputEvent,
can_gc: CanGc,
) -> TouchEventResult {
// Ignore all incoming events without a hit test.
let Some(hit_test_result) = self.window.hit_test_from_input_event(input_event) else {
self.update_active_touch_points_when_early_return(event);
return TouchEventResult::Forwarded;
};
let TouchId(identifier) = event.id;
let event_name = match event.event_type {
TouchEventType::Down => "touchstart",
TouchEventType::Move => "touchmove",
TouchEventType::Up => "touchend",
TouchEventType::Cancel => "touchcancel",
};
let Some(el) = hit_test_result
.node
.inclusive_ancestors(ShadowIncluding::No)
.filter_map(DomRoot::downcast::<Element>)
.next()
else {
self.update_active_touch_points_when_early_return(event);
return TouchEventResult::Forwarded;
};
let target = DomRoot::upcast::<EventTarget>(el);
let window = &*self.window;
let client_x = Finite::wrap(hit_test_result.point_in_frame.x as f64);
let client_y = Finite::wrap(hit_test_result.point_in_frame.y as f64);
let page_x =
Finite::wrap(hit_test_result.point_in_frame.x as f64 + window.PageXOffset() as f64);
let page_y =
Finite::wrap(hit_test_result.point_in_frame.y as f64 + window.PageYOffset() as f64);
let touch = Touch::new(
window, identifier, &target, client_x,
client_y, // TODO: Get real screen coordinates?
client_x, client_y, page_x, page_y, can_gc,
);
match event.event_type {
TouchEventType::Down => {
// Add a new touch point
self.active_touch_points
.borrow_mut()
.push(Dom::from_ref(&*touch));
},
TouchEventType::Move => {
// Replace an existing touch point
let mut active_touch_points = self.active_touch_points.borrow_mut();
match active_touch_points
.iter_mut()
.find(|t| t.Identifier() == identifier)
{
Some(t) => *t = Dom::from_ref(&*touch),
None => warn!("Got a touchmove event for a non-active touch point"),
}
},
TouchEventType::Up | TouchEventType::Cancel => {
// Remove an existing touch point
let mut active_touch_points = self.active_touch_points.borrow_mut();
match active_touch_points
.iter()
.position(|t| t.Identifier() == identifier)
{
Some(i) => {
active_touch_points.swap_remove(i);
},
None => warn!("Got a touchend event for a non-active touch point"),
}
},
}
rooted_vec!(let mut target_touches);
let touches = {
let touches = self.active_touch_points.borrow();
target_touches.extend(touches.iter().filter(|t| t.Target() == target).cloned());
TouchList::new(window, touches.r(), can_gc)
};
let event = TouchEvent::new(
window,
DOMString::from(event_name),
EventBubbles::Bubbles,
EventCancelable::from(event.is_cancelable()),
Some(window),
0i32,
&touches,
&TouchList::new(window, from_ref(&&*touch), can_gc),
&TouchList::new(window, target_touches.r(), can_gc),
// FIXME: modifier keys
false,
false,
false,
false,
can_gc,
);
TouchEventResult::Processed(event.upcast::<Event>().fire(&target, can_gc))
}
// If hittest fails, we still need to update the active point information.
fn update_active_touch_points_when_early_return(&self, event: EmbedderTouchEvent) {
match event.event_type {
TouchEventType::Down => {
// If the touchdown fails, we don't need to do anything.
// When a touchmove or touchdown occurs at that touch point,
// a warning is triggered: Got a touchmove/touchend event for a non-active touch point
},
TouchEventType::Move => {
// The failure of touchmove does not affect the number of active points.
// Since there is no position information when it fails, we do not need to update.
},
TouchEventType::Up | TouchEventType::Cancel => {
// Remove an existing touch point
let mut active_touch_points = self.active_touch_points.borrow_mut();
match active_touch_points
.iter()
.position(|t| t.Identifier() == event.id.0)
{
Some(i) => {
active_touch_points.swap_remove(i);
},
None => warn!("Got a touchend event for a non-active touch point"),
}
},
}
}
/// The entry point for all key processing for web content
fn handle_keyboard_event(&self, keyboard_event: EmbedderKeyboardEvent, can_gc: CanGc) {
let document = self.window.Document();
let focused = document.get_focused_element();
let body = document.GetBody();
let target = match (&focused, &body) {
(Some(focused), _) => focused.upcast(),
(&None, Some(body)) => body.upcast(),
(&None, &None) => self.window.upcast(),
};
let keyevent = KeyboardEvent::new(
&self.window,
DOMString::from(keyboard_event.event.state.event_type()),
true,
true,
Some(&self.window),
0,
keyboard_event.event.key.clone(),
DOMString::from(keyboard_event.event.code.to_string()),
keyboard_event.event.location as u32,
keyboard_event.event.repeat,
keyboard_event.event.is_composing,
keyboard_event.event.modifiers,
0,
keyboard_event.event.key.legacy_keycode(),
can_gc,
);
let event = keyevent.upcast::<Event>();
event.fire(target, can_gc);
let mut cancel_state = event.get_cancel_state();
// https://w3c.github.io/uievents/#keys-cancelable-keys
// it MUST prevent the respective beforeinput and input
// (and keypress if supported) events from being generated
// TODO: keypress should be deprecated and superceded by beforeinput
let is_character_value_key = matches!(
keyboard_event.event.key,
Key::Character(_) | Key::Named(NamedKey::Enter)
);
if keyboard_event.event.state == KeyState::Down &&
is_character_value_key &&
!keyboard_event.event.is_composing &&
cancel_state != EventDefault::Prevented
{
// https://w3c.github.io/uievents/#keypress-event-order
let event = KeyboardEvent::new(
&self.window,
DOMString::from("keypress"),
true,
true,
Some(&self.window),
0,
keyboard_event.event.key.clone(),
DOMString::from(keyboard_event.event.code.to_string()),
keyboard_event.event.location as u32,
keyboard_event.event.repeat,
keyboard_event.event.is_composing,
keyboard_event.event.modifiers,
keyboard_event.event.key.legacy_charcode(),
0,
can_gc,
);
let ev = event.upcast::<Event>();
ev.fire(target, can_gc);
cancel_state = ev.get_cancel_state();
}
if cancel_state == EventDefault::Allowed {
self.window.send_to_embedder(EmbedderMsg::Keyboard(
self.window.webview_id(),
keyboard_event.clone(),
));
// This behavior is unspecced
// We are supposed to dispatch synthetic click activation for Space and/or Return,
// however *when* we do it is up to us.
// Here, we're dispatching it after the key event so the script has a chance to cancel it
// https://www.w3.org/Bugs/Public/show_bug.cgi?id=27337
if (keyboard_event.event.key == Key::Named(NamedKey::Enter) ||
keyboard_event.event.code == Code::Space) &&
keyboard_event.event.state == KeyState::Up
{
if let Some(elem) = target.downcast::<Element>() {
elem.upcast::<Node>()
.fire_synthetic_pointer_event_not_trusted(DOMString::from("click"), can_gc);
}
}
}
}
fn handle_ime_event(&self, event: ImeEvent, can_gc: CanGc) {
let document = self.window.Document();
let composition_event = match event {
ImeEvent::Dismissed => {
document.request_focus(
document.GetBody().as_ref().map(|e| e.upcast()),
FocusInitiator::Local,
can_gc,
);
return;
},
ImeEvent::Composition(composition_event) => composition_event,
};
// spec: https://w3c.github.io/uievents/#compositionstart
// spec: https://w3c.github.io/uievents/#compositionupdate
// spec: https://w3c.github.io/uievents/#compositionend
// > Event.target : focused element processing the composition
let focused = document.get_focused_element();
let target = if let Some(elem) = &focused {
elem.upcast()
} else {
// Event is only dispatched if there is a focused element.
return;
};
let cancelable = composition_event.state == keyboard_types::CompositionState::Start;
CompositionEvent::new(
&self.window,
DOMString::from(composition_event.state.event_type()),
true,
cancelable,
Some(&self.window),
0,
DOMString::from(composition_event.data),
can_gc,
)
.upcast::<Event>()
.fire(target, can_gc);
}
fn handle_wheel_event(
&self,
event: EmbedderWheelEvent,
input_event: &ConstellationInputEvent,
can_gc: CanGc,
) {
// Ignore all incoming events without a hit test.
let Some(hit_test_result) = self.window.hit_test_from_input_event(input_event) else {
return;
};
let Some(el) = hit_test_result
.node
.inclusive_ancestors(ShadowIncluding::No)
.filter_map(DomRoot::downcast::<Element>)
.next()
else {
return;
};
let node = el.upcast::<Node>();
let wheel_event_type_string = "wheel".to_owned();
debug!(
"{}: on {:?} at {:?}",
wheel_event_type_string,
node.debug_str(),
hit_test_result.point_in_frame
);
// https://w3c.github.io/uievents/#event-wheelevents
let dom_event = WheelEvent::new(
&self.window,
DOMString::from(wheel_event_type_string),
EventBubbles::Bubbles,
EventCancelable::Cancelable,
Some(&self.window),
0i32,
hit_test_result.point_in_frame.to_i32(),
hit_test_result.point_in_frame.to_i32(),
hit_test_result
.point_relative_to_initial_containing_block
.to_i32(),
input_event.active_keyboard_modifiers,
0i16,
input_event.pressed_mouse_buttons,
None,
None,
// winit defines positive wheel delta values as revealing more content left/up.
// https://docs.rs/winit-gtk/latest/winit/event/enum.MouseScrollDelta.html
// This is the opposite of wheel delta in uievents
// https://w3c.github.io/uievents/#dom-wheeleventinit-deltaz
Finite::wrap(-event.delta.x),
Finite::wrap(-event.delta.y),
Finite::wrap(-event.delta.z),
event.delta.mode as u32,
can_gc,
);
let dom_event = dom_event.upcast::<Event>();
dom_event.set_trusted(true);
let target = node.upcast();
dom_event.fire(target, can_gc);
}
fn handle_gamepad_event(&self, gamepad_event: EmbedderGamepadEvent) {
match gamepad_event {
EmbedderGamepadEvent::Connected(index, name, bounds, supported_haptic_effects) => {
self.handle_gamepad_connect(
index.0,
name,
bounds.axis_bounds,
bounds.button_bounds,
supported_haptic_effects,
);
},
EmbedderGamepadEvent::Disconnected(index) => {
self.handle_gamepad_disconnect(index.0);
},
EmbedderGamepadEvent::Updated(index, update_type) => {
self.receive_new_gamepad_button_or_axis(index.0, update_type);
},
};
}
/// <https://www.w3.org/TR/gamepad/#dfn-gamepadconnected>
fn handle_gamepad_connect(
&self,
// As the spec actually defines how to set the gamepad index, the GilRs index
// is currently unused, though in practice it will almost always be the same.
// More infra is currently needed to track gamepads across windows.
_index: usize,
name: String,
axis_bounds: (f64, f64),
button_bounds: (f64, f64),
supported_haptic_effects: GamepadSupportedHapticEffects,
) {
// TODO: 2. If document is not null and is not allowed to use the "gamepad" permission,
// then abort these steps.
let trusted_window = Trusted::new(&*self.window);
self.window
.upcast::<GlobalScope>()
.task_manager()
.gamepad_task_source()
.queue(task!(gamepad_connected: move || {
let window = trusted_window.root();
let navigator = window.Navigator();
let selected_index = navigator.select_gamepad_index();
let gamepad = Gamepad::new(
&window,
selected_index,
name,
"standard".into(),
axis_bounds,
button_bounds,
supported_haptic_effects,
false,
CanGc::note(),
);
navigator.set_gamepad(selected_index as usize, &gamepad, CanGc::note());
}));
}
/// <https://www.w3.org/TR/gamepad/#dfn-gamepaddisconnected>
fn handle_gamepad_disconnect(&self, index: usize) {
let trusted_window = Trusted::new(&*self.window);
self.window
.upcast::<GlobalScope>()
.task_manager()
.gamepad_task_source()
.queue(task!(gamepad_disconnected: move || {
let window = trusted_window.root();
let navigator = window.Navigator();
if let Some(gamepad) = navigator.get_gamepad(index) {
if window.Document().is_fully_active() {
gamepad.update_connected(false, gamepad.exposed(), CanGc::note());
navigator.remove_gamepad(index);
}
}
}));
}
/// <https://www.w3.org/TR/gamepad/#receiving-inputs>
fn receive_new_gamepad_button_or_axis(&self, index: usize, update_type: GamepadUpdateType) {
let trusted_window = Trusted::new(&*self.window);
// <https://w3c.github.io/gamepad/#dfn-update-gamepad-state>
self.window.upcast::<GlobalScope>().task_manager().gamepad_task_source().queue(
task!(update_gamepad_state: move || {
let window = trusted_window.root();
let navigator = window.Navigator();
if let Some(gamepad) = navigator.get_gamepad(index) {
let current_time = window.Performance().Now();
gamepad.update_timestamp(*current_time);
match update_type {
GamepadUpdateType::Axis(index, value) => {
gamepad.map_and_normalize_axes(index, value);
},
GamepadUpdateType::Button(index, value) => {
gamepad.map_and_normalize_buttons(index, value);
}
};
if !navigator.has_gamepad_gesture() && contains_user_gesture(update_type) {
navigator.set_has_gamepad_gesture(true);
navigator.GetGamepads()
.iter()
.filter_map(|g| g.as_ref())
.for_each(|gamepad| {
gamepad.set_exposed(true);
gamepad.update_timestamp(*current_time);
let new_gamepad = Trusted::new(&**gamepad);
if window.Document().is_fully_active() {
window.upcast::<GlobalScope>().task_manager().gamepad_task_source().queue(
task!(update_gamepad_connect: move || {
let gamepad = new_gamepad.root();
gamepad.notify_event(GamepadEventType::Connected, CanGc::note());
})
);
}
});
}
}
})
);
}
/// <https://www.w3.org/TR/clipboard-apis/#clipboard-actions>
fn handle_editing_action(&self, action: EditingActionEvent, can_gc: CanGc) -> bool {
let clipboard_event_type = match action {
EditingActionEvent::Copy => ClipboardEventType::Copy,
EditingActionEvent::Cut => ClipboardEventType::Cut,
EditingActionEvent::Paste => ClipboardEventType::Paste,
};
// 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(clipboard_event_type.as_str()),
EventBubbles::Bubbles,
EventCancelable::Cancelable,
None,
can_gc,
);
self.fire_clipboard_event(&event, clipboard_event_type, 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 document = self.window.Document();
let focused = document.get_focused_element();
let body = document.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 => {
let (sender, receiver) = ipc::channel().unwrap();
self.window.send_to_embedder(EmbedderMsg::GetClipboardText(
self.window.webview_id(),
sender,
));
let text_contents = receiver
.recv()
.map(Result::unwrap_or_default)
.unwrap_or_default();
// 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 data = DOMString::from(text_contents.to_string());
let type_ = DOMString::from("text/plain");
let _ = drag_data_store.add(Kind::Text { data, type_ });
// 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.
event.set_composed(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.window
.send_to_embedder(EmbedderMsg::ClearClipboard(self.window.webview_id()));
// Step 1.2
for item in drag_data_store.iter_item_list() {
match item {
Kind::Text { data, .. } => {
// 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.window.send_to_embedder(EmbedderMsg::SetClipboardText(
self.window.webview_id(),
data.to_string(),
));
},
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.window
.send_to_embedder(EmbedderMsg::ClearClipboard(self.window.webview_id()));
// 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
}
}
}
/// Handle scroll event triggered by user interactions from embedder side.
/// <https://drafts.csswg.org/cssom-view/#scrolling-events>
#[allow(unsafe_code)]
fn handle_embedder_scroll_event(&self, event: ScrollEvent) {
// If it is a viewport scroll.
let document = self.window.Document();
if event.external_id.is_root() {
document.handle_viewport_scroll_event();
} else {
// Otherwise, check whether it is for a relevant element within the document.
let Some(node_id) = node_id_from_scroll_id(event.external_id.0 as usize) else {
return;
};
let node = unsafe {
node::from_untrusted_node_address(UntrustedNodeAddress::from_id(node_id))
};
let Some(element) = node
.inclusive_ancestors(ShadowIncluding::No)
.filter_map(DomRoot::downcast::<Element>)
.next()
else {
return;
};
document.handle_element_scroll_event(&element);
}
}
}