diff --git a/components/layout/query.rs b/components/layout/query.rs
index 348a3ad556d..df45301106a 100644
--- a/components/layout/query.rs
+++ b/components/layout/query.rs
@@ -16,7 +16,7 @@ use layout_thread::LayoutThreadData;
use msg::constellation_msg::ConstellationChan;
use opaque_node::OpaqueNodeMethods;
use script::layout_interface::{ContentBoxResponse, ContentBoxesResponse, NodeGeometryResponse};
-use script::layout_interface::{HitTestResponse, LayoutRPC, MouseOverResponse, OffsetParentResponse};
+use script::layout_interface::{HitTestResponse, LayoutRPC, OffsetParentResponse};
use script::layout_interface::{ResolvedStyleResponse, ScriptLayoutChan, MarginStyleResponse};
use script_traits::LayoutMsg as ConstellationMsg;
use sequential;
@@ -67,51 +67,29 @@ impl LayoutRPC for LayoutRPCImpl {
}
/// Requests the node containing the point of interest.
- fn hit_test(&self, point: Point2D) -> Result {
+ fn hit_test(&self, point: Point2D, update_cursor: bool) -> Result {
let point = Point2D::new(Au::from_f32_px(point.x), Au::from_f32_px(point.y));
let &LayoutRPCImpl(ref rw_data) = self;
let rw_data = rw_data.lock().unwrap();
- let result = match rw_data.display_list {
- None => panic!("Tried to hit test without a DisplayList"),
- Some(ref display_list) => display_list.hit_test(point),
- };
+ let display_list = rw_data.display_list.as_ref().expect("Tried to hit test without a DisplayList!");
- if result.is_empty() {
- return Err(());
- }
+ let result = display_list.hit_test(point);
- Ok(HitTestResponse(result[0].node.to_untrusted_node_address()))
- }
-
- fn mouse_over(&self, point: Point2D) -> Result {
- let point = Point2D::new(Au::from_f32_px(point.x), Au::from_f32_px(point.y));
- let mouse_over_list = {
- let &LayoutRPCImpl(ref rw_data) = self;
- let rw_data = rw_data.lock().unwrap();
- let result = match rw_data.display_list {
- None => panic!("Tried to hit test without a DisplayList"),
- Some(ref display_list) => display_list.hit_test(point),
- };
-
- // Compute the new cursor.
+ if update_cursor {
let cursor = if !result.is_empty() {
result[0].pointing.unwrap()
} else {
Cursor::DefaultCursor
};
+
let ConstellationChan(ref constellation_chan) = rw_data.constellation_chan;
constellation_chan.send(ConstellationMsg::SetCursor(cursor)).unwrap();
- result
};
- if mouse_over_list.is_empty() {
- Err(())
+ if !result.is_empty() {
+ Ok(HitTestResponse(result[0].node.to_untrusted_node_address()))
} else {
- let response_list =
- mouse_over_list.iter()
- .map(|metadata| metadata.node.to_untrusted_node_address())
- .collect();
- Ok(MouseOverResponse(response_list))
+ Err(())
}
}
diff --git a/components/script/dom/document.rs b/components/script/dom/document.rs
index 501ad242305..ec0e78fafc2 100644
--- a/components/script/dom/document.rs
+++ b/components/script/dom/document.rs
@@ -80,7 +80,7 @@ use html5ever::tree_builder::{LimitedQuirks, NoQuirks, Quirks, QuirksMode};
use ipc_channel::ipc::{self, IpcSender};
use js::jsapi::JS_GetRuntime;
use js::jsapi::{JSContext, JSObject, JSRuntime};
-use layout_interface::{HitTestResponse, MouseOverResponse};
+use layout_interface::HitTestResponse;
use layout_interface::{LayoutChan, Msg, ReflowQueryType};
use msg::constellation_msg::{ALT, CONTROL, SHIFT, SUPER};
use msg::constellation_msg::{ConstellationChan, Key, KeyModifiers, KeyState};
@@ -563,9 +563,9 @@ impl Document {
.map(Root::upcast)
}
- pub fn hit_test(&self, page_point: &Point2D) -> Option {
+ pub fn hit_test(&self, page_point: &Point2D, update_cursor: bool) -> Option {
assert!(self.GetDocumentElement().is_some());
- match self.window.layout().hit_test(*page_point) {
+ match self.window.layout().hit_test(*page_point, update_cursor) {
Ok(HitTestResponse(node_address)) => Some(node_address),
Err(()) => {
debug!("layout query error");
@@ -574,14 +574,6 @@ impl Document {
}
}
- pub fn get_nodes_under_mouse(&self, page_point: &Point2D) -> Vec {
- assert!(self.GetDocumentElement().is_some());
- match self.window.layout().mouse_over(*page_point) {
- Ok(MouseOverResponse(node_address)) => node_address,
- Err(()) => vec![],
- }
- }
-
// https://html.spec.whatwg.org/multipage/#current-document-readiness
pub fn set_ready_state(&self, state: DocumentReadyState) {
match state {
@@ -693,7 +685,7 @@ impl Document {
let page_point = Point2D::new(client_point.x + self.window.PageXOffset() as f32,
client_point.y + self.window.PageYOffset() as f32);
- let node = match self.hit_test(&page_point) {
+ let node = match self.hit_test(&page_point, false) {
Some(node_address) => {
debug!("node address is {:?}", node_address);
node::from_untrusted_node_address(js_runtime, node_address)
@@ -806,63 +798,33 @@ impl Document {
pub fn handle_mouse_move_event(&self,
js_runtime: *mut JSRuntime,
client_point: Option>,
- prev_mouse_over_targets: &mut RootedVec>) {
- // Build a list of elements that are currently under the mouse.
- let mouse_over_addresses = client_point.as_ref().map(|client_point| {
- let page_point = Point2D::new(client_point.x + self.window.PageXOffset() as f32,
- client_point.y + self.window.PageYOffset() as f32);
- self.get_nodes_under_mouse(&page_point)
- }).unwrap_or(vec![]);
- let mut mouse_over_targets = mouse_over_addresses.iter().map(|node_address| {
- node::from_untrusted_node_address(js_runtime, *node_address)
- .inclusive_ancestors()
+ prev_mouse_over_target: &MutNullableHeap>) {
+ let page_point = match client_point {
+ None => {
+ // If there's no point, there's no target under the mouse
+ // FIXME: dispatch mouseout here. We have no point.
+ prev_mouse_over_target.set(None);
+ return;
+ }
+ Some(ref client_point) => {
+ Point2D::new(client_point.x + self.window.PageXOffset() as f32,
+ client_point.y + self.window.PageYOffset() as f32)
+ }
+ };
+
+ let client_point = client_point.unwrap();
+
+ let maybe_new_target = self.hit_test(&page_point, true).and_then(|address| {
+ let node = node::from_untrusted_node_address(js_runtime, address);
+ node.inclusive_ancestors()
.filter_map(Root::downcast::)
.next()
- .unwrap()
- }).collect::>>();
-
- // Remove hover from any elements in the previous list that are no longer
- // under the mouse.
- for target in prev_mouse_over_targets.iter() {
- if !mouse_over_targets.contains(target) {
- let target_ref = &**target;
- if target_ref.get_hover_state() {
- target_ref.set_hover_state(false);
-
- let target = target_ref.upcast();
-
- // FIXME: we should be dispatching this event but we lack an actual
- // point to pass to it.
- if let Some(client_point) = client_point {
- self.fire_mouse_event(client_point, &target, "mouseout".to_owned());
- }
- }
- }
- }
-
- // Set hover state for any elements in the current mouse over list.
- // Check if any of them changed state to determine whether to
- // force a reflow below.
- for target in mouse_over_targets.r() {
- if !target.get_hover_state() {
- target.set_hover_state(true);
-
- let target = target.upcast();
-
- if let Some(client_point) = client_point {
- self.fire_mouse_event(client_point, target, "mouseover".to_owned());
- }
- }
- }
-
- // Send mousemove event to topmost target
- if mouse_over_addresses.len() > 0 {
- let top_most_node = node::from_untrusted_node_address(js_runtime,
- mouse_over_addresses[0]);
- let client_point = client_point.unwrap(); // Must succeed because hit test succeeded.
+ });
+ // Send mousemove event to topmost target, and forward it if it's an iframe
+ if let Some(ref new_target) = maybe_new_target {
// If the target is an iframe, forward the event to the child document.
- if let Some(iframe) = top_most_node.downcast::() {
+ if let Some(iframe) = new_target.downcast::() {
if let Some(pipeline_id) = iframe.pipeline_id() {
let rect = iframe.upcast::().GetBoundingClientRect();
let child_origin = Point2D::new(rect.X() as f32, rect.Y() as f32);
@@ -874,13 +836,59 @@ impl Document {
return;
}
- let target = top_most_node.upcast();
- self.fire_mouse_event(client_point, target, "mousemove".to_owned());
+ self.fire_mouse_event(client_point, new_target.upcast(), "mousemove".to_owned());
}
- // Store the current mouse over targets for next frame
- prev_mouse_over_targets.clear();
- prev_mouse_over_targets.append(&mut *mouse_over_targets);
+ // Nothing more to do here, mousemove is sent,
+ // and the element under the mouse hasn't changed.
+ if maybe_new_target == prev_mouse_over_target.get() {
+ return;
+ }
+
+ let old_target_is_ancestor_of_new_target = match (prev_mouse_over_target.get(), maybe_new_target.as_ref()) {
+ (Some(old_target), Some(new_target))
+ => old_target.upcast::().is_ancestor_of(new_target.upcast::()),
+ _ => false,
+ };
+
+ // 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 let Some(old_target) = prev_mouse_over_target.get() {
+ // If the old target is an ancestor of the new target, this can be skipped
+ // completely, since the node's hover state will be reseted below.
+ if !old_target_is_ancestor_of_new_target {
+ for element in old_target.upcast::()
+ .inclusive_ancestors()
+ .filter_map(Root::downcast::) {
+ element.set_hover_state(false);
+ }
+ }
+
+ // Remove hover state to old target and its parents
+ self.fire_mouse_event(client_point, old_target.upcast(), "mouseout".to_owned());
+
+ // TODO: Fire mouseleave here only if the old target is
+ // not an ancestor of the new target.
+ }
+
+ if let Some(ref new_target) = maybe_new_target {
+ for element in new_target.upcast::()
+ .inclusive_ancestors()
+ .filter_map(Root::downcast::) {
+ if element.get_hover_state() {
+ break;
+ }
+
+ element.set_hover_state(true);
+ }
+
+ self.fire_mouse_event(client_point, &new_target.upcast(), "mouseover".to_owned());
+
+ // TODO: Fire mouseenter here.
+ }
+
+ // Store the current mouse over target for next frame.
+ prev_mouse_over_target.set(maybe_new_target.as_ref().map(|target| target.r()));
self.window.reflow(ReflowGoal::ForDisplay,
ReflowQueryType::NoQuery,
@@ -900,7 +908,7 @@ impl Document {
TouchEventType::Cancel => "touchcancel",
};
- let node = match self.hit_test(&point) {
+ let node = match self.hit_test(&point, false) {
Some(node_address) => node::from_untrusted_node_address(js_runtime, node_address),
None => return false,
};
@@ -2567,7 +2575,7 @@ impl DocumentMethods for Document {
let js_runtime = unsafe { JS_GetRuntime(window.get_cx()) };
- match self.hit_test(point) {
+ match self.hit_test(point, false) {
Some(untrusted_node_address) => {
let node = node::from_untrusted_node_address(js_runtime, untrusted_node_address);
let parent_node = node.GetParentNode().unwrap();
diff --git a/components/script/dom/node.rs b/components/script/dom/node.rs
index ec72b6eed2f..717be0331d5 100644
--- a/components/script/dom/node.rs
+++ b/components/script/dom/node.rs
@@ -518,7 +518,11 @@ impl Node {
}
pub fn is_inclusive_ancestor_of(&self, parent: &Node) -> bool {
- self == parent || parent.ancestors().any(|ancestor| ancestor.r() == self)
+ self == parent || self.is_ancestor_of(parent)
+ }
+
+ pub fn is_ancestor_of(&self, parent: &Node) -> bool {
+ parent.ancestors().any(|ancestor| ancestor.r() == self)
}
pub fn following_siblings(&self) -> NodeSiblingIterator {
diff --git a/components/script/layout_interface.rs b/components/script/layout_interface.rs
index 17f553d43e8..3976f0a8d20 100644
--- a/components/script/layout_interface.rs
+++ b/components/script/layout_interface.rs
@@ -105,8 +105,7 @@ pub trait LayoutRPC {
/// Requests the geometry of this node. Used by APIs such as `clientTop`.
fn node_geometry(&self) -> NodeGeometryResponse;
/// Requests the node containing the point of interest
- fn hit_test(&self, point: Point2D) -> Result;
- fn mouse_over(&self, point: Point2D) -> Result;
+ fn hit_test(&self, point: Point2D, update_cursor: bool) -> Result;
/// Query layout for the resolved value of a given CSS property
fn resolved_style(&self) -> ResolvedStyleResponse;
fn offset_parent(&self) -> OffsetParentResponse;
@@ -139,7 +138,6 @@ pub struct NodeGeometryResponse {
pub client_rect: Rect,
}
pub struct HitTestResponse(pub UntrustedNodeAddress);
-pub struct MouseOverResponse(pub Vec);
pub struct ResolvedStyleResponse(pub Option);
#[derive(Clone)]
diff --git a/components/script/script_thread.rs b/components/script/script_thread.rs
index ddf32f85406..a735603431a 100644
--- a/components/script/script_thread.rs
+++ b/components/script/script_thread.rs
@@ -27,10 +27,10 @@ use dom::bindings::codegen::Bindings::DocumentBinding::{DocumentMethods, Documen
use dom::bindings::conversions::{FromJSValConvertible, StringificationBehavior};
use dom::bindings::global::GlobalRef;
use dom::bindings::inheritance::Castable;
-use dom::bindings::js::{JS, RootCollection, trace_roots};
+use dom::bindings::js::{JS, MutNullableHeap, Root, RootCollection, trace_roots};
use dom::bindings::js::{RootCollectionPtr, RootedReference};
use dom::bindings::refcounted::{LiveDOMReferences, Trusted, TrustedReference, trace_refcounted_objects};
-use dom::bindings::trace::{JSTraceable, RootedVec, trace_traceables};
+use dom::bindings::trace::{JSTraceable, trace_traceables};
use dom::bindings::utils::{DOM_CALLBACKS, WRAP_CALLBACKS};
use dom::browsingcontext::BrowsingContext;
use dom::document::{Document, DocumentProgressHandler, DocumentSource, FocusType, IsHTMLDocument};
@@ -91,7 +91,6 @@ use std::cell::{Cell, RefCell};
use std::collections::HashSet;
use std::io::{Write, stdout};
use std::marker::PhantomData;
-use std::mem as std_mem;
use std::option::Option;
use std::ptr;
use std::rc::Rc;
@@ -543,7 +542,8 @@ pub struct ScriptThread {
/// The JavaScript runtime.
js_runtime: Rc,
- mouse_over_targets: DOMRefCell>>,
+ /// The topmost element over the mouse.
+ topmost_mouse_over_target: MutNullableHeap>,
/// List of pipelines that have been owned and closed by this script thread.
closed_pipelines: DOMRefCell>,
@@ -789,7 +789,7 @@ impl ScriptThread {
devtools_sender: ipc_devtools_sender,
js_runtime: Rc::new(runtime),
- mouse_over_targets: DOMRefCell::new(vec!()),
+ topmost_mouse_over_target: MutNullableHeap::new(Default::default()),
closed_pipelines: DOMRefCell::new(HashSet::new()),
scheduler_chan: state.scheduler_chan,
@@ -1979,46 +1979,55 @@ impl ScriptThread {
let page = get_page(&self.root_page(), pipeline_id);
let document = page.document();
- let mut prev_mouse_over_targets: RootedVec> = RootedVec::new();
- for target in &*self.mouse_over_targets.borrow_mut() {
- prev_mouse_over_targets.push(target.clone());
+ // Get the previous target temporarily
+ let prev_mouse_over_target = self.topmost_mouse_over_target.get();
+
+ document.handle_mouse_move_event(self.js_runtime.rt(), point,
+ &self.topmost_mouse_over_target);
+
+ // Short-circuit if nothing changed
+ if self.topmost_mouse_over_target.get() == prev_mouse_over_target {
+ return;
}
- // We temporarily steal the list of targets over which the mouse is to pass it to
- // handle_mouse_move_event() in a safe RootedVec container.
- let mut mouse_over_targets = RootedVec::new();
- std_mem::swap(&mut *self.mouse_over_targets.borrow_mut(), &mut *mouse_over_targets);
- document.handle_mouse_move_event(self.js_runtime.rt(), point, &mut mouse_over_targets);
-
- // Notify Constellation about anchors that are no longer mouse over targets.
- for target in &*prev_mouse_over_targets {
- if !mouse_over_targets.contains(target) && target.is::() {
- let event = ConstellationMsg::NodeStatus(None);
- let ConstellationChan(ref chan) = self.constellation_chan;
- chan.send(event).unwrap();
- break;
- }
- }
+ let mut state_already_changed = false;
// Notify Constellation about the topmost anchor mouse over target.
- for target in &*mouse_over_targets {
- if target.is::() {
- let status = target.get_attribute(&ns!(), &atom!("href"))
- .and_then(|href| {
- let value = href.value();
- let url = document.url();
- url.join(&value).map(|url| url.serialize()).ok()
- });
+ if let Some(target) = self.topmost_mouse_over_target.get() {
+ if let Some(anchor) = target.upcast::()
+ .inclusive_ancestors()
+ .filter_map(Root::downcast::)
+ .next() {
+ let status = anchor.upcast::()
+ .get_attribute(&ns!(), &atom!("href"))
+ .and_then(|href| {
+ let value = href.value();
+ let url = document.url();
+ url.join(&value).map(|url| url.serialize()).ok()
+ });
+
let event = ConstellationMsg::NodeStatus(status);
let ConstellationChan(ref chan) = self.constellation_chan;
chan.send(event).unwrap();
- break;
+
+ state_already_changed = true;
}
}
- std_mem::swap(&mut *self.mouse_over_targets.borrow_mut(), &mut *mouse_over_targets);
+ // We might have to reset the anchor state
+ if !state_already_changed {
+ if let Some(target) = prev_mouse_over_target {
+ if let Some(_) = target.upcast::()
+ .inclusive_ancestors()
+ .filter_map(Root::downcast::)
+ .next() {
+ let event = ConstellationMsg::NodeStatus(None);
+ let ConstellationChan(ref chan) = self.constellation_chan;
+ chan.send(event).unwrap();
+ }
+ }
+ }
}
-
TouchEvent(event_type, identifier, point) => {
let handled = self.handle_touch_event(pipeline_id, event_type, identifier, point);
match event_type {
diff --git a/tests/wpt/metadata/MANIFEST.json b/tests/wpt/metadata/MANIFEST.json
index 98aaa6d001e..9885853d2ec 100644
--- a/tests/wpt/metadata/MANIFEST.json
+++ b/tests/wpt/metadata/MANIFEST.json
@@ -34513,6 +34513,14 @@
"local_changes": {
"deleted": [],
"items": {
+ "manual": {
+ "uievents/order-of-events/mouse-events/mouseover-out-manual.html": [
+ {
+ "path": "uievents/order-of-events/mouse-events/mouseover-out-manual.html",
+ "url": "/uievents/order-of-events/mouse-events/mouseover-out-manual.html"
+ }
+ ]
+ },
"testharness": {
"dom/lists/DOMTokenList-value.html": [
{
diff --git a/tests/wpt/web-platform-tests/uievents/order-of-events/mouse-events/mouseover-out-manual.html b/tests/wpt/web-platform-tests/uievents/order-of-events/mouse-events/mouseover-out-manual.html
new file mode 100644
index 00000000000..1d96fd303ad
--- /dev/null
+++ b/tests/wpt/web-platform-tests/uievents/order-of-events/mouse-events/mouseover-out-manual.html
@@ -0,0 +1,125 @@
+
+
+Mouseover/mouseout handling
+
+
+
+
+ Steps:
+
+
+ - Move your mouse over the blue
<div>
element, later
+ over the green one, later over the yellow one.
+ - Move the mouse from the yellow element to the green one, later to the
+ blue one, and later over this paragraph.
+
+
+
+
+
+
+
+
+
+