mirror of
https://github.com/servo/servo.git
synced 2025-09-27 15:20:09 +01:00
When an `<iframe>` cannot scroll because the size of the frame is greater than or equal to the size of page contents, chain up the keyboard scroll operation to the parent frame. Testing: A new Servo-only WPT tests is added, though needs to be manually run with `--product servodriver`. Signed-off-by: Martin Robinson <mrobinson@igalia.com> Co-authored-by: Delan Azabani <dazabani@igalia.com>
1583 lines
64 KiB
Rust
1583 lines
64 KiB
Rust
/* 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::{KeyboardScroll, 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, Vector2D};
|
||
use ipc_channel::ipc;
|
||
use js::jsapi::JSAutoRealm;
|
||
use keyboard_types::{Code, Key, KeyState, Modifiers, NamedKey};
|
||
use layout_api::{ScrollContainerQueryFlags, 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::{ScrollBehavior, WindowMethods};
|
||
use script_bindings::inheritance::Castable;
|
||
use script_bindings::num::Finite;
|
||
use script_bindings::reflector::DomObject;
|
||
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, EventComposed, 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, NodeTraits, ShadowIncluding};
|
||
use crate::dom::pointerevent::PointerId;
|
||
use crate::dom::scrolling_box::ScrollingBoxAxis;
|
||
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 element that was most recently clicked.
|
||
most_recently_clicked_element: 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_recently_clicked_element: 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 => {
|
||
self.most_recently_clicked_element.set(Some(&el));
|
||
|
||
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()),
|
||
EventComposed::Composed,
|
||
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);
|
||
}
|
||
}
|
||
|
||
pub(crate) fn run_default_keyboard_event_handler(&self, event: &KeyboardEvent) {
|
||
if event.upcast::<Event>().type_() != atom!("keydown") {
|
||
return;
|
||
}
|
||
if !event.modifiers().is_empty() {
|
||
return;
|
||
}
|
||
let scroll = match event.key() {
|
||
Key::Named(NamedKey::ArrowDown) => KeyboardScroll::Down,
|
||
Key::Named(NamedKey::ArrowLeft) => KeyboardScroll::Left,
|
||
Key::Named(NamedKey::ArrowRight) => KeyboardScroll::Right,
|
||
Key::Named(NamedKey::ArrowUp) => KeyboardScroll::Up,
|
||
Key::Named(NamedKey::End) => KeyboardScroll::End,
|
||
Key::Named(NamedKey::Home) => KeyboardScroll::Home,
|
||
Key::Named(NamedKey::PageDown) => KeyboardScroll::PageDown,
|
||
Key::Named(NamedKey::PageUp) => KeyboardScroll::PageUp,
|
||
_ => return,
|
||
};
|
||
self.do_keyboard_scroll(scroll);
|
||
}
|
||
|
||
pub(crate) fn do_keyboard_scroll(&self, scroll: KeyboardScroll) {
|
||
let scroll_axis = match scroll {
|
||
KeyboardScroll::Left | KeyboardScroll::Right => ScrollingBoxAxis::X,
|
||
_ => ScrollingBoxAxis::Y,
|
||
};
|
||
|
||
let document = self.window.Document();
|
||
let mut scrolling_box = document
|
||
.get_focused_element()
|
||
.or(self.most_recently_clicked_element.get())
|
||
.and_then(|element| element.scrolling_box(ScrollContainerQueryFlags::Inclusive))
|
||
.unwrap_or_else(|| {
|
||
document.viewport_scrolling_box(ScrollContainerQueryFlags::Inclusive)
|
||
});
|
||
|
||
while !scrolling_box.can_keyboard_scroll_in_axis(scroll_axis) {
|
||
// Always fall back to trying to scroll the entire document.
|
||
if scrolling_box.is_viewport() {
|
||
break;
|
||
}
|
||
let parent = scrolling_box.parent().unwrap_or_else(|| {
|
||
document.viewport_scrolling_box(ScrollContainerQueryFlags::Inclusive)
|
||
});
|
||
scrolling_box = parent;
|
||
}
|
||
|
||
// If this is the viewport and we cannot scroll, try to ask a parent viewport to scroll,
|
||
// if we are inside an `<iframe>`.
|
||
if !scrolling_box.can_keyboard_scroll_in_axis(scroll_axis) {
|
||
assert!(scrolling_box.is_viewport());
|
||
|
||
let window_proxy = document.window().window_proxy();
|
||
if let Some(iframe) = window_proxy.frame_element() {
|
||
// When the `<iframe>` is local (in this ScriptThread), we can
|
||
// synchronously chain up the keyboard scrolling event.
|
||
let cx = GlobalScope::get_cx();
|
||
let iframe_window = iframe.owner_window();
|
||
let _ac = JSAutoRealm::new(*cx, iframe_window.reflector().get_jsobject().get());
|
||
iframe_window
|
||
.Document()
|
||
.event_handler()
|
||
.do_keyboard_scroll(scroll);
|
||
} else if let Some(parent_pipeline) = self.window.parent_info() {
|
||
// Otherwise, if we have a parent (presumably from a different origin)
|
||
// asynchronously ask the Constellation to forward the event to the parent
|
||
// pipeline, if we have one.
|
||
document.window().send_to_constellation(
|
||
ScriptToConstellationMessage::ForwardKeyboardScroll(parent_pipeline, scroll),
|
||
);
|
||
};
|
||
return;
|
||
}
|
||
|
||
const LINE_HEIGHT: f32 = 76.0;
|
||
const LINE_WIDTH: f32 = 76.0;
|
||
|
||
let current_scroll_offset = scrolling_box.scroll_position();
|
||
let delta = match scroll {
|
||
KeyboardScroll::Home => Vector2D::new(0.0, -current_scroll_offset.y),
|
||
KeyboardScroll::End => Vector2D::new(
|
||
0.0,
|
||
-current_scroll_offset.y + scrolling_box.content_size().height -
|
||
scrolling_box.size().height,
|
||
),
|
||
KeyboardScroll::PageDown => {
|
||
Vector2D::new(0.0, scrolling_box.size().height - 2.0 * LINE_HEIGHT)
|
||
},
|
||
KeyboardScroll::PageUp => {
|
||
Vector2D::new(0.0, 2.0 * LINE_HEIGHT - scrolling_box.size().height)
|
||
},
|
||
KeyboardScroll::Up => Vector2D::new(0.0, -LINE_HEIGHT),
|
||
KeyboardScroll::Down => Vector2D::new(0.0, LINE_HEIGHT),
|
||
KeyboardScroll::Left => Vector2D::new(-LINE_WIDTH, 0.0),
|
||
KeyboardScroll::Right => Vector2D::new(LINE_WIDTH, 0.0),
|
||
};
|
||
|
||
scrolling_box.scroll_to(delta + current_scroll_offset, ScrollBehavior::Auto);
|
||
}
|
||
}
|