| // Copyright 2017 The Cobalt Authors. All Rights Reserved. |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| #include "cobalt/layout/topmost_event_target.h" |
| |
| #include "base/optional.h" |
| #include "base/trace_event/trace_event.h" |
| #include "cobalt/base/token.h" |
| #include "cobalt/base/tokens.h" |
| #include "cobalt/cssom/keyword_value.h" |
| #include "cobalt/dom/document.h" |
| #include "cobalt/dom/event.h" |
| #include "cobalt/dom/html_element.h" |
| #include "cobalt/dom/html_html_element.h" |
| #include "cobalt/dom/lottie_player.h" |
| #include "cobalt/dom/mouse_event.h" |
| #include "cobalt/dom/mouse_event_init.h" |
| #include "cobalt/dom/pointer_event.h" |
| #include "cobalt/dom/pointer_event_init.h" |
| #include "cobalt/dom/pointer_state.h" |
| #include "cobalt/dom/ui_event.h" |
| #include "cobalt/dom/wheel_event.h" |
| #include "cobalt/math/vector2d.h" |
| #include "cobalt/math/vector2d_f.h" |
| |
| namespace cobalt { |
| namespace layout { |
| |
| scoped_refptr<dom::HTMLElement> TopmostEventTarget::FindTopmostEventTarget( |
| const scoped_refptr<dom::Document>& document, |
| const math::Vector2dF& coordinate) { |
| TRACE_EVENT0("cobalt::layout", |
| "TopmostEventTarget::FindTopmostEventTarget()"); |
| DCHECK(document); |
| DCHECK(!box_); |
| DCHECK(render_sequence_.empty()); |
| |
| // Make sure the document's layout box tree is up-to-date. |
| document->DoSynchronousLayout(); |
| |
| html_element_ = document->html(); |
| ConsiderElement(html_element_, coordinate); |
| box_ = NULL; |
| render_sequence_.clear(); |
| scoped_refptr<dom::HTMLElement> topmost_element; |
| topmost_element.swap(html_element_); |
| DCHECK(!html_element_); |
| return topmost_element; |
| } |
| |
| namespace { |
| |
| LayoutBoxes* GetLayoutBoxesIfNotEmpty(dom::Element* element) { |
| dom::HTMLElement* html_element = element->AsHTMLElement(); |
| if (html_element && html_element->computed_style()) { |
| dom::LayoutBoxes* dom_layout_boxes = html_element->layout_boxes(); |
| if (dom_layout_boxes && |
| dom_layout_boxes->type() == dom::LayoutBoxes::kLayoutLayoutBoxes) { |
| LayoutBoxes* layout_boxes = |
| base::polymorphic_downcast<LayoutBoxes*>(dom_layout_boxes); |
| if (!layout_boxes->boxes().empty()) { |
| return layout_boxes; |
| } |
| } |
| } |
| return NULL; |
| } |
| |
| } // namespace |
| void TopmostEventTarget::ConsiderElement(dom::Element* element, |
| const math::Vector2dF& coordinate) { |
| if (!element) return; |
| math::Vector2dF element_coordinate(coordinate); |
| LayoutBoxes* layout_boxes = GetLayoutBoxesIfNotEmpty(element); |
| if (layout_boxes) { |
| const Box* box = layout_boxes->boxes().front(); |
| if (box->computed_style() && box->IsTransformed()) { |
| // Early out if the transform cannot be applied. This can occur if the |
| // transform matrix is not invertible. |
| if (!box->ApplyTransformActionToCoordinate(Box::kEnterTransform, |
| &element_coordinate)) { |
| return; |
| } |
| } |
| |
| scoped_refptr<dom::HTMLElement> html_element = element->AsHTMLElement(); |
| if (html_element && html_element->CanbeDesignatedByPointerIfDisplayed()) { |
| ConsiderBoxes(html_element, layout_boxes, element_coordinate); |
| } |
| } |
| |
| for (dom::Element* child_element = element->first_element_child(); |
| child_element; child_element = child_element->next_element_sibling()) { |
| ConsiderElement(child_element, element_coordinate); |
| } |
| } |
| |
| void TopmostEventTarget::ConsiderBoxes( |
| const scoped_refptr<dom::HTMLElement>& html_element, |
| LayoutBoxes* layout_boxes, const math::Vector2dF& coordinate) { |
| const Boxes& boxes = layout_boxes->boxes(); |
| Vector2dLayoutUnit layout_coordinate(LayoutUnit(coordinate.x()), |
| LayoutUnit(coordinate.y())); |
| for (Boxes::const_iterator box_iterator = boxes.begin(); |
| box_iterator != boxes.end(); ++box_iterator) { |
| Box* box = *box_iterator; |
| do { |
| if (box->IsUnderCoordinate(layout_coordinate)) { |
| Box::RenderSequence render_sequence = box->GetRenderSequence(); |
| if (Box::IsRenderedLater(render_sequence, render_sequence_)) { |
| html_element_ = html_element; |
| box_ = box; |
| render_sequence_.swap(render_sequence); |
| } |
| } |
| box = box->GetSplitSibling(); |
| } while (box != NULL); |
| } |
| } |
| |
| namespace { |
| // Return the nearest common ancestor of previous_element and target_element |
| scoped_refptr<dom::Element> GetNearestCommonAncestor( |
| scoped_refptr<dom::HTMLElement> previous_element, |
| scoped_refptr<dom::HTMLElement> target_element) { |
| scoped_refptr<dom::Element> nearest_common_ancestor; |
| if (previous_element == target_element) { |
| nearest_common_ancestor = target_element; |
| } else { |
| if (previous_element && target_element) { |
| // Find the nearest common ancestor, if there is any. |
| dom::Document* previous_document = previous_element->node_document(); |
| // The elements only have a common ancestor if they are both in the same |
| // document. |
| if (previous_document && |
| previous_document == target_element->node_document()) { |
| // The nearest ancestor of the target element that is already |
| // designated is the nearest common ancestor of it and the previous |
| // element. |
| nearest_common_ancestor = target_element; |
| while (nearest_common_ancestor && |
| nearest_common_ancestor->AsHTMLElement() && |
| !nearest_common_ancestor->AsHTMLElement()->IsDesignated()) { |
| nearest_common_ancestor = nearest_common_ancestor->parent_element(); |
| } |
| } |
| } |
| } |
| return nearest_common_ancestor; |
| } |
| |
| void SendStateChangeLeaveEvents( |
| bool is_pointer_event, scoped_refptr<dom::HTMLElement> previous_element, |
| scoped_refptr<dom::HTMLElement> target_element, |
| scoped_refptr<dom::Element> nearest_common_ancestor, |
| dom::PointerEventInit* event_init) { |
| // Send enter/leave/over/out (status change) events when needed. |
| if (previous_element != target_element) { |
| const scoped_refptr<dom::Window>& view = event_init->view(); |
| |
| // Send out and leave events. |
| if (previous_element) { |
| // LottiePlayer elements may change playback state. |
| if (previous_element->AsLottiePlayer()) { |
| previous_element->AsLottiePlayer()->OnUnHover(); |
| } |
| |
| dom::Document* previous_document = previous_element->node_document(); |
| |
| event_init->set_related_target(target_element); |
| if (is_pointer_event) { |
| previous_element->DispatchEvent(new dom::PointerEvent( |
| base::Tokens::pointerout(), view, *event_init)); |
| if (previous_document) { |
| for (scoped_refptr<dom::Element> element = previous_element; |
| element && element != nearest_common_ancestor; |
| element = element->parent_element()) { |
| DCHECK(element->AsHTMLElement()->IsDesignated()); |
| element->DispatchEvent(new dom::PointerEvent( |
| base::Tokens::pointerleave(), dom::Event::kNotBubbles, |
| dom::Event::kNotCancelable, view, *event_init)); |
| } |
| } |
| } |
| |
| // Send compatibility mapping mouse events for state changes. |
| // https://www.w3.org/TR/pointerevents/#mapping-for-devices-that-do-not-support-hover |
| previous_element->DispatchEvent( |
| new dom::MouseEvent(base::Tokens::mouseout(), view, *event_init)); |
| |
| if (previous_document) { |
| for (scoped_refptr<dom::Element> element = previous_element; |
| element && element != nearest_common_ancestor; |
| element = element->parent_element()) { |
| DCHECK(element->AsHTMLElement()->IsDesignated()); |
| element->DispatchEvent(new dom::MouseEvent( |
| base::Tokens::mouseleave(), dom::Event::kNotBubbles, |
| dom::Event::kNotCancelable, view, *event_init)); |
| } |
| |
| if (!target_element || |
| previous_document != target_element->node_document()) { |
| previous_document->SetIndicatedElement(NULL); |
| } |
| } |
| } |
| } |
| } |
| |
| void SendStateChangeEnterEvents( |
| bool is_pointer_event, scoped_refptr<dom::HTMLElement> previous_element, |
| scoped_refptr<dom::HTMLElement> target_element, |
| scoped_refptr<dom::Element> nearest_common_ancestor, |
| dom::PointerEventInit* event_init) { |
| // Send enter/leave/over/out (status change) events when needed. |
| if (previous_element != target_element) { |
| const scoped_refptr<dom::Window>& view = event_init->view(); |
| |
| // Send over and enter events. |
| if (target_element) { |
| // LottiePlayer elements may change playback state. |
| if (target_element->AsLottiePlayer()) { |
| target_element->AsLottiePlayer()->OnHover(); |
| } |
| |
| event_init->set_related_target(previous_element); |
| if (is_pointer_event) { |
| target_element->DispatchEvent(new dom::PointerEvent( |
| base::Tokens::pointerover(), view, *event_init)); |
| for (scoped_refptr<dom::Element> element = target_element; |
| element != nearest_common_ancestor; |
| element = element->parent_element()) { |
| if (element) { |
| element->DispatchEvent(new dom::PointerEvent( |
| base::Tokens::pointerenter(), dom::Event::kNotBubbles, |
| dom::Event::kNotCancelable, view, *event_init)); |
| } |
| } |
| } |
| |
| // Send compatibility mapping mouse events for state changes. |
| // https://www.w3.org/TR/pointerevents/#mapping-for-devices-that-do-not-support-hover |
| target_element->DispatchEvent( |
| new dom::MouseEvent(base::Tokens::mouseover(), view, *event_init)); |
| for (scoped_refptr<dom::Element> element = target_element; |
| element != nearest_common_ancestor; |
| element = element->parent_element()) { |
| if (element) { |
| element->DispatchEvent(new dom::MouseEvent( |
| base::Tokens::mouseenter(), dom::Event::kNotBubbles, |
| dom::Event::kNotCancelable, view, *event_init)); |
| } |
| } |
| } |
| } |
| } |
| |
| void SendCompatibilityMappingMouseEvent( |
| const scoped_refptr<dom::HTMLElement>& target_element, |
| const scoped_refptr<dom::Event>& event, |
| const dom::PointerEvent* pointer_event, |
| const dom::PointerEventInit& event_init, |
| std::set<std::string>* mouse_event_prevent_flags) { |
| // Send compatibility mapping mouse event if needed. |
| // https://www.w3.org/TR/2015/REC-pointerevents-20150224/#compatibility-mapping-with-mouse-events |
| bool has_compatibility_mouse_event = true; |
| base::Token type = pointer_event->type(); |
| if (type == base::Tokens::pointerdown()) { |
| // If the pointer event dispatched was pointerdown and the event was |
| // canceled, then set the PREVENT MOUSE EVENT flag for this pointerType. |
| if (event->default_prevented()) { |
| mouse_event_prevent_flags->insert(pointer_event->pointer_type()); |
| has_compatibility_mouse_event = false; |
| } else { |
| type = base::Tokens::mousedown(); |
| } |
| } else { |
| has_compatibility_mouse_event = |
| mouse_event_prevent_flags->find(pointer_event->pointer_type()) == |
| mouse_event_prevent_flags->end(); |
| if (type == base::Tokens::pointerup()) { |
| // If the pointer event dispatched was pointerup, clear the PREVENT |
| // MOUSE EVENT flag for this pointerType. |
| mouse_event_prevent_flags->erase(pointer_event->pointer_type()); |
| type = base::Tokens::mouseup(); |
| } else if (type == base::Tokens::pointermove()) { |
| type = base::Tokens::mousemove(); |
| } else { |
| has_compatibility_mouse_event = false; |
| } |
| } |
| if (has_compatibility_mouse_event) { |
| target_element->DispatchEvent( |
| new dom::MouseEvent(type, event_init.view(), event_init)); |
| } |
| } |
| |
| void InitializePointerEventInitFromEvent( |
| const dom::MouseEvent* const mouse_event, |
| const dom::PointerEvent* pointer_event, dom::PointerEventInit* event_init) { |
| // For EventInit |
| event_init->set_bubbles(mouse_event->bubbles()); |
| event_init->set_cancelable(mouse_event->cancelable()); |
| |
| // For UIEventInit |
| event_init->set_view(mouse_event->view()); |
| event_init->set_detail(mouse_event->detail()); |
| event_init->set_which(mouse_event->which()); |
| |
| // For EventModifierInit |
| event_init->set_ctrl_key(mouse_event->ctrl_key()); |
| event_init->set_shift_key(mouse_event->shift_key()); |
| event_init->set_alt_key(mouse_event->alt_key()); |
| event_init->set_meta_key(mouse_event->meta_key()); |
| |
| // For MouseEventInit |
| event_init->set_screen_x(mouse_event->screen_x()); |
| event_init->set_screen_y(mouse_event->screen_y()); |
| event_init->set_client_x(mouse_event->screen_x()); |
| event_init->set_client_y(mouse_event->screen_y()); |
| event_init->set_button(mouse_event->button()); |
| event_init->set_buttons(mouse_event->buttons()); |
| event_init->set_related_target(mouse_event->related_target()); |
| if (pointer_event) { |
| // For PointerEventInit |
| event_init->set_pointer_id(pointer_event->pointer_id()); |
| event_init->set_width(pointer_event->width()); |
| event_init->set_height(pointer_event->height()); |
| event_init->set_pressure(pointer_event->pressure()); |
| event_init->set_tilt_x(pointer_event->tilt_x()); |
| event_init->set_tilt_y(pointer_event->tilt_y()); |
| event_init->set_pointer_type(pointer_event->pointer_type()); |
| event_init->set_is_primary(pointer_event->is_primary()); |
| } |
| } |
| } // namespace |
| |
| void TopmostEventTarget::MaybeSendPointerEvents( |
| const scoped_refptr<dom::Event>& event) { |
| TRACE_EVENT0("cobalt::layout", |
| "TopmostEventTarget::MaybeSendPointerEvents()"); |
| |
| const dom::MouseEvent* const mouse_event = |
| base::polymorphic_downcast<const dom::MouseEvent* const>(event.get()); |
| DCHECK(mouse_event); |
| DCHECK(!html_element_); |
| const dom::PointerEvent* pointer_event = |
| (event->GetWrappableType() == base::GetTypeId<dom::PointerEvent>()) |
| ? base::polymorphic_downcast<const dom::PointerEvent* const>( |
| event.get()) |
| : NULL; |
| bool is_touchpad_event = false; |
| |
| // The target override element for the pointer event. This may not be the same |
| // as the hit test target, and it also may not be set. |
| scoped_refptr<dom::HTMLElement> target_override_element; |
| |
| // Store the data for the status change and pointer capture event(s). |
| dom::PointerEventInit event_init; |
| InitializePointerEventInitFromEvent(mouse_event, pointer_event, &event_init); |
| const scoped_refptr<dom::Window>& view = event_init.view(); |
| if (!view) { |
| return; |
| } |
| dom::PointerState* pointer_state = view->document()->pointer_state(); |
| if (pointer_event) { |
| pointer_state->SetActiveButtonsState(pointer_event->pointer_id(), |
| pointer_event->buttons()); |
| is_touchpad_event = pointer_event->pointer_type() == "touchpad"; |
| if (is_touchpad_event) { |
| if (pointer_event->type() == base::Tokens::pointerdown()) { |
| pointer_state->SetActive(pointer_event->pointer_id()); |
| // Implicitly capture the pointer to the active element. |
| // https://www.w3.org/TR/pointerevents/#implicit-pointer-capture |
| scoped_refptr<dom::HTMLElement> html_element; |
| if (view->document()->active_element()) { |
| html_element = view->document()->active_element()->AsHTMLElement(); |
| } |
| if (html_element) { |
| pointer_state->SetPendingPointerCaptureTargetOverride( |
| pointer_event->pointer_id(), html_element); |
| } |
| } |
| } else { |
| pointer_state->SetActive(pointer_event->pointer_id()); |
| } |
| target_override_element = pointer_state->GetPointerCaptureOverrideElement( |
| pointer_event->pointer_id(), &event_init); |
| } |
| |
| scoped_refptr<dom::HTMLElement> target_element; |
| if (target_override_element) { |
| target_element = target_override_element; |
| } else { |
| // Do a hit test if there is no target override element. |
| math::Vector2dF coordinate(static_cast<float>(event_init.client_x()), |
| static_cast<float>(event_init.client_y())); |
| target_element = FindTopmostEventTarget(view->document(), coordinate); |
| } |
| |
| scoped_refptr<dom::HTMLElement> previous_html_element( |
| previous_html_element_weak_); |
| |
| // The enter/leave status change events apply to all ancestors up to the |
| // nearest common ancestor between the previous and current element. |
| scoped_refptr<dom::Element> nearest_common_ancestor( |
| GetNearestCommonAncestor(previous_html_element, target_element)); |
| |
| SendStateChangeLeaveEvents(pointer_event, previous_html_element, |
| target_element, nearest_common_ancestor, |
| &event_init); |
| |
| if (target_element) { |
| target_element->DispatchEvent(event); |
| } |
| |
| if (pointer_event) { |
| if (pointer_event->type() == base::Tokens::pointerup()) { |
| if (is_touchpad_event) { |
| // A touchpad becomes inactive after a pointerup. |
| pointer_state->ClearActive(pointer_event->pointer_id()); |
| } |
| // Implicit release of pointer capture. |
| // https://www.w3.org/TR/pointerevents/#implicit-release-of-pointer-capture |
| pointer_state->ClearPendingPointerCaptureTargetOverride( |
| pointer_event->pointer_id()); |
| } |
| if (target_element && !is_touchpad_event) { |
| SendCompatibilityMappingMouseEvent(target_element, event, pointer_event, |
| event_init, |
| &mouse_event_prevent_flags_); |
| } |
| } |
| |
| if (event_init.button() == 0 && |
| ((mouse_event->type() == base::Tokens::pointerup()) || |
| (mouse_event->type() == base::Tokens::mouseup()))) { |
| // This is an 'up' event for the last pressed button indicating that no |
| // more buttons are pressed. |
| if (target_element && !is_touchpad_event) { |
| // Send the click event if needed, which is not prevented by canceling |
| // the pointerdown event. |
| // https://www.w3.org/TR/uievents/#event-type-click |
| // https://www.w3.org/TR/pointerevents/#compatibility-mapping-with-mouse-events |
| target_element->DispatchEvent( |
| new dom::MouseEvent(base::Tokens::click(), view, event_init)); |
| } |
| if (target_element && (pointer_event->pointer_type() != "mouse")) { |
| // If it's not a mouse event, then releasing the last button means |
| // that there is no longer an indicated element. |
| dom::Document* document = target_element->node_document(); |
| if (document) { |
| document->SetIndicatedElement(NULL); |
| target_element = NULL; |
| } |
| } |
| } |
| |
| SendStateChangeEnterEvents(pointer_event, previous_html_element, |
| target_element, nearest_common_ancestor, |
| &event_init); |
| |
| if (target_element) { |
| // Touchpad input never indicates document elements. |
| if (!is_touchpad_event) { |
| dom::Document* document = target_element->node_document(); |
| if (document) { |
| document->SetIndicatedElement(target_element); |
| } |
| } |
| previous_html_element_weak_ = base::AsWeakPtr(target_element.get()); |
| } else { |
| previous_html_element_weak_.reset(); |
| } |
| DCHECK(!html_element_); |
| } |
| |
| } // namespace layout |
| } // namespace cobalt |