blob: a5b8eb3e3644fbf34c9155644f78575b6c0a8bb9 [file] [log] [blame]
/*
* Copyright (C) 2012 Nokia Corporation and/or its subsidiary(-ies)
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Library General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Library General Public License for more details.
*
* You should have received a copy of the GNU Library General Public License
* along with this library; see the file COPYING.LIB. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
* Boston, MA 02110-1301, USA.
*/
#include "third_party/blink/renderer/core/page/touch_adjustment.h"
#include "third_party/blink/public/platform/web_screen_info.h"
#include "third_party/blink/renderer/core/dom/container_node.h"
#include "third_party/blink/renderer/core/dom/node.h"
#include "third_party/blink/renderer/core/dom/node_computed_style.h"
#include "third_party/blink/renderer/core/dom/text.h"
#include "third_party/blink/renderer/core/editing/editing_behavior.h"
#include "third_party/blink/renderer/core/editing/editing_utilities.h"
#include "third_party/blink/renderer/core/editing/editor.h"
#include "third_party/blink/renderer/core/editing/frame_selection.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/core/frame/local_frame_view.h"
#include "third_party/blink/renderer/core/html/html_frame_owner_element.h"
#include "third_party/blink/renderer/core/layout/layout_box.h"
#include "third_party/blink/renderer/core/layout/layout_object.h"
#include "third_party/blink/renderer/core/layout/layout_text.h"
#include "third_party/blink/renderer/core/page/chrome_client.h"
#include "third_party/blink/renderer/core/page/page.h"
#include "third_party/blink/renderer/core/style/computed_style.h"
#include "third_party/blink/renderer/platform/geometry/float_point.h"
#include "third_party/blink/renderer/platform/geometry/float_quad.h"
#include "third_party/blink/renderer/platform/geometry/int_size.h"
#include "third_party/blink/renderer/platform/text/text_break_iterator.h"
namespace blink {
namespace touch_adjustment {
const float kZeroTolerance = 1e-6f;
// The maximum adjustment range (diameters) in dip.
constexpr float kMaxAdjustmentSizeDip = 32.f;
// Class for remembering absolute quads of a target node and what node they
// represent.
class SubtargetGeometry {
DISALLOW_NEW();
public:
SubtargetGeometry(Node* node, const FloatQuad& quad)
: node_(node), quad_(quad) {}
void Trace(blink::Visitor* visitor) { visitor->Trace(node_); }
Node* GetNode() const { return node_; }
FloatQuad Quad() const { return quad_; }
IntRect BoundingBox() const { return quad_.EnclosingBoundingBox(); }
private:
Member<Node> node_;
FloatQuad quad_;
};
} // namespace touch_adjustment
} // namespace blink
WTF_ALLOW_MOVE_INIT_AND_COMPARE_WITH_MEM_FUNCTIONS(
blink::touch_adjustment::SubtargetGeometry)
namespace blink {
namespace touch_adjustment {
typedef HeapVector<SubtargetGeometry> SubtargetGeometryList;
typedef bool (*NodeFilter)(Node*);
typedef void (*AppendSubtargetsForNode)(Node*, SubtargetGeometryList&);
typedef float (*DistanceFunction)(const IntPoint&,
const IntRect&,
const SubtargetGeometry&);
// Takes non-const Node* because isContentEditable is a non-const function.
bool NodeRespondsToTapGesture(Node* node) {
if (node->WillRespondToMouseClickEvents() ||
node->WillRespondToMouseMoveEvents())
return true;
if (node->IsElementNode()) {
Element* element = ToElement(node);
// Tapping on a text field or other focusable item should trigger
// adjustment, except that iframe elements are hard-coded to support focus
// but the effect is often invisible so they should be excluded.
if (element->IsMouseFocusable() && !IsHTMLIFrameElement(element))
return true;
// Accept nodes that has a CSS effect when touched.
if (element->ChildrenOrSiblingsAffectedByActive() ||
element->ChildrenOrSiblingsAffectedByHover())
return true;
}
if (const ComputedStyle* computed_style = node->GetComputedStyle()) {
if (computed_style->AffectedByActive() || computed_style->AffectedByHover())
return true;
}
return false;
}
bool NodeIsZoomTarget(Node* node) {
if (node->IsTextNode() || node->IsShadowRoot())
return false;
DCHECK(node->GetLayoutObject());
return node->GetLayoutObject()->IsBox();
}
bool ProvidesContextMenuItems(Node* node) {
// This function tries to match the nodes that receive special context-menu
// items in ContextMenuController::populate(), and should be kept uptodate
// with those.
DCHECK(node->GetLayoutObject() || node->IsShadowRoot());
if (!node->GetLayoutObject())
return false;
node->GetDocument().UpdateStyleAndLayoutTree();
if (HasEditableStyle(*node))
return true;
if (node->IsLink())
return true;
if (node->GetLayoutObject()->IsImage())
return true;
if (node->GetLayoutObject()->IsMedia())
return true;
if (node->GetLayoutObject()->CanBeSelectionLeaf()) {
// If the context menu gesture will trigger a selection all selectable nodes
// are valid targets.
if (node->GetLayoutObject()
->GetFrame()
->GetEditor()
.Behavior()
.ShouldSelectOnContextualMenuClick())
return true;
// Only the selected part of the layoutObject is a valid target, but this
// will be corrected in appendContextSubtargetsForNode.
if (node->GetLayoutObject()->IsSelected())
return true;
}
return false;
}
static inline void AppendQuadsToSubtargetList(
Vector<FloatQuad>& quads,
Node* node,
SubtargetGeometryList& subtargets) {
Vector<FloatQuad>::const_iterator it = quads.begin();
const Vector<FloatQuad>::const_iterator end = quads.end();
for (; it != end; ++it)
subtargets.push_back(SubtargetGeometry(node, *it));
}
static inline void AppendBasicSubtargetsForNode(
Node* node,
SubtargetGeometryList& subtargets) {
// Node guaranteed to have layoutObject due to check in node filter.
DCHECK(node->GetLayoutObject());
Vector<FloatQuad> quads;
node->GetLayoutObject()->AbsoluteQuads(quads);
AppendQuadsToSubtargetList(quads, node, subtargets);
}
static inline void AppendContextSubtargetsForNode(
Node* node,
SubtargetGeometryList& subtargets) {
// This is a variant of appendBasicSubtargetsForNode that adds special
// subtargets for selected or auto-selectable parts of text nodes.
DCHECK(node->GetLayoutObject());
if (!node->IsTextNode())
return AppendBasicSubtargetsForNode(node, subtargets);
Text* text_node = ToText(node);
LayoutText* text_layout_object = text_node->GetLayoutObject();
if (text_layout_object->GetFrame()
->GetEditor()
.Behavior()
.ShouldSelectOnContextualMenuClick()) {
// Make subtargets out of every word.
String text_value = text_node->data();
TextBreakIterator* word_iterator =
WordBreakIterator(text_value, 0, text_value.length());
int last_offset = word_iterator->first();
if (last_offset == -1)
return;
int offset;
while ((offset = word_iterator->next()) != -1) {
if (IsWordTextBreak(word_iterator)) {
Vector<FloatQuad> quads;
text_layout_object->AbsoluteQuadsForRange(quads, last_offset, offset);
AppendQuadsToSubtargetList(quads, text_node, subtargets);
}
last_offset = offset;
}
} else {
if (!text_layout_object->IsSelected())
return AppendBasicSubtargetsForNode(node, subtargets);
const FrameSelection& frame_selection =
text_layout_object->GetFrame()->Selection();
const LayoutTextSelectionStatus& selection_status =
frame_selection.ComputeLayoutSelectionStatus(*text_layout_object);
// If selected, make subtargets out of only the selected part of the text.
Vector<FloatQuad> quads;
text_layout_object->AbsoluteQuadsForRange(quads, selection_status.start,
selection_status.end);
AppendQuadsToSubtargetList(quads, text_node, subtargets);
}
}
static inline Node* ParentShadowHostOrOwner(const Node* node) {
if (Node* ancestor = node->ParentOrShadowHostNode())
return ancestor;
if (auto* document = DynamicTo<Document>(node))
return document->LocalOwner();
return nullptr;
}
// Compiles a list of subtargets of all the relevant target nodes.
void CompileSubtargetList(const HeapVector<Member<Node>>& intersected_nodes,
SubtargetGeometryList& subtargets,
NodeFilter node_filter,
AppendSubtargetsForNode append_subtargets_for_node) {
// Find candidates responding to tap gesture events in O(n) time.
HeapHashMap<Member<Node>, Member<Node>> responder_map;
HeapHashSet<Member<Node>> ancestors_to_responders_set;
HeapVector<Member<Node>> candidates;
HeapHashSet<Member<Node>> editable_ancestors;
// A node matching the NodeFilter is called a responder. Candidate nodes must
// either be a responder or have an ancestor that is a responder. This
// iteration tests all ancestors at most once by caching earlier results.
for (unsigned i = 0; i < intersected_nodes.size(); ++i) {
Node* node = intersected_nodes[i].Get();
HeapVector<Member<Node>> visited_nodes;
Node* responding_node = nullptr;
for (Node* visited_node = node; visited_node;
visited_node = visited_node->ParentOrShadowHostNode()) {
// Check if we already have a result for a common ancestor from another
// candidate.
responding_node = responder_map.at(visited_node);
if (responding_node)
break;
visited_nodes.push_back(visited_node);
// Check if the node filter applies, which would mean we have found a
// responding node.
if (node_filter(visited_node)) {
responding_node = visited_node;
// Continue the iteration to collect the ancestors of the responder,
// which we will need later.
for (visited_node = ParentShadowHostOrOwner(visited_node); visited_node;
visited_node = ParentShadowHostOrOwner(visited_node)) {
HeapHashSet<Member<Node>>::AddResult add_result =
ancestors_to_responders_set.insert(visited_node);
if (!add_result.is_new_entry)
break;
}
break;
}
}
// Insert the detected responder for all the visited nodes.
for (unsigned j = 0; j < visited_nodes.size(); j++)
responder_map.insert(visited_nodes[j], responding_node);
if (responding_node)
candidates.push_back(node);
}
// We compile the list of component absolute quads instead of using the
// bounding rect to be able to perform better hit-testing on inline links on
// line-breaks.
for (unsigned i = 0; i < candidates.size(); i++) {
Node* candidate = candidates[i];
// Skip nodes who's responders are ancestors of other responders. This gives
// preference to the inner-most event-handlers. So that a link is always
// preferred even when contained in an element that monitors all
// click-events.
Node* responding_node = responder_map.at(candidate);
DCHECK(responding_node);
if (ancestors_to_responders_set.Contains(responding_node))
continue;
// Consolidate bounds for editable content.
if (editable_ancestors.Contains(candidate))
continue;
candidate->GetDocument().UpdateStyleAndLayoutTree();
if (HasEditableStyle(*candidate)) {
Node* replacement = candidate;
Node* parent = candidate->ParentOrShadowHostNode();
while (parent && HasEditableStyle(*parent)) {
replacement = parent;
if (editable_ancestors.Contains(replacement)) {
replacement = nullptr;
break;
}
editable_ancestors.insert(replacement);
parent = parent->ParentOrShadowHostNode();
}
candidate = replacement;
}
if (candidate)
append_subtargets_for_node(candidate, subtargets);
}
}
// This returns quotient of the target area and its intersection with the touch
// area. This will prioritize largest intersection and smallest area, while
// balancing the two against each other.
float ZoomableIntersectionQuotient(const IntPoint& touch_hotspot,
const IntRect& touch_area,
const SubtargetGeometry& subtarget) {
IntRect rect = subtarget.GetNode()->GetDocument().View()->ConvertToRootFrame(
subtarget.BoundingBox());
// Check the rectangle is meaningful zoom target. It should at least contain
// the hotspot.
if (!rect.Contains(touch_hotspot))
return std::numeric_limits<float>::infinity();
IntRect intersection = rect;
intersection.Intersect(touch_area);
// Return the quotient of the intersection.
return rect.Size().Area() / (float)intersection.Size().Area();
}
// Uses a hybrid of distance to adjust and intersect ratio, normalizing each
// score between 0 and 1 and combining them. The distance to adjust works best
// for disambiguating clicks on targets such as links, where the width may be
// significantly larger than the touch width. Using area of overlap in such
// cases can lead to a bias towards shorter links. Conversely, percentage of
// overlap can provide strong confidence in tapping on a small target, where the
// overlap is often quite high, and works well for tightly packed controls.
float HybridDistanceFunction(const IntPoint& touch_hotspot,
const IntRect& touch_rect,
const SubtargetGeometry& subtarget) {
IntRect rect = subtarget.GetNode()->GetDocument().View()->ConvertToRootFrame(
subtarget.BoundingBox());
float radius_squared = 0.25f * (touch_rect.Size().DiagonalLengthSquared());
float distance_to_adjust_score =
rect.DistanceSquaredToPoint(touch_hotspot) / radius_squared;
int max_overlap_width = std::min(touch_rect.Width(), rect.Width());
int max_overlap_height = std::min(touch_rect.Height(), rect.Height());
float max_overlap_area = std::max(max_overlap_width * max_overlap_height, 1);
rect.Intersect(touch_rect);
float intersect_area = rect.Size().Area();
float intersection_score = 1 - intersect_area / max_overlap_area;
float hybrid_score = intersection_score + distance_to_adjust_score;
return hybrid_score;
}
FloatPoint ConvertToRootFrame(LocalFrameView* view, FloatPoint pt) {
int x = static_cast<int>(pt.X() + 0.5f);
int y = static_cast<int>(pt.Y() + 0.5f);
IntPoint adjusted = view->ConvertToRootFrame(IntPoint(x, y));
return FloatPoint(adjusted.X(), adjusted.Y());
}
// Adjusts 'point' to the nearest point inside rect, and leaves it unchanged if
// already inside.
void AdjustPointToRect(FloatPoint& point, const IntRect& rect) {
if (point.X() < rect.X())
point.SetX(rect.X());
else if (point.X() > rect.MaxX())
point.SetX(rect.MaxX());
if (point.Y() < rect.Y())
point.SetY(rect.Y());
else if (point.Y() > rect.MaxY())
point.SetY(rect.MaxY());
}
bool SnapTo(const SubtargetGeometry& geom,
const IntPoint& touch_point,
const IntRect& touch_area,
IntPoint& adjusted_point) {
LocalFrameView* view = geom.GetNode()->GetDocument().View();
FloatQuad quad = geom.Quad();
if (quad.IsRectilinear()) {
IntRect bounds = view->ConvertToRootFrame(geom.BoundingBox());
if (bounds.Contains(touch_point)) {
adjusted_point = touch_point;
return true;
}
if (bounds.Intersects(touch_area)) {
bounds.Intersect(touch_area);
adjusted_point = bounds.Center();
return true;
}
return false;
}
// The following code tries to adjust the point to place inside a both the
// touchArea and the non-rectilinear quad.
// FIXME: This will return the point inside the touch area that is the closest
// to the quad center, but does not guarantee that the point will be inside
// the quad. Corner-cases exist where the quad will intersect but this will
// fail to adjust the point to somewhere in the intersection.
FloatPoint p1 = ConvertToRootFrame(view, quad.P1());
FloatPoint p2 = ConvertToRootFrame(view, quad.P2());
FloatPoint p3 = ConvertToRootFrame(view, quad.P3());
FloatPoint p4 = ConvertToRootFrame(view, quad.P4());
quad = FloatQuad(p1, p2, p3, p4);
if (quad.ContainsPoint(FloatPoint(touch_point))) {
adjusted_point = touch_point;
return true;
}
// Pull point towards the center of the element.
FloatPoint center = quad.Center();
AdjustPointToRect(center, touch_area);
adjusted_point = RoundedIntPoint(center);
return quad.ContainsPoint(FloatPoint(adjusted_point));
}
// A generic function for finding the target node with the lowest distance
// metric. A distance metric here is the result of a distance-like function,
// that computes how well the touch hits the node. Distance functions could for
// instance be distance squared or area of intersection.
bool FindNodeWithLowestDistanceMetric(Node*& target_node,
IntPoint& target_point,
IntRect& target_area,
const IntPoint& touch_hotspot,
const IntRect& touch_area,
SubtargetGeometryList& subtargets,
DistanceFunction distance_function) {
target_node = nullptr;
float best_distance_metric = std::numeric_limits<float>::infinity();
SubtargetGeometryList::const_iterator it = subtargets.begin();
const SubtargetGeometryList::const_iterator end = subtargets.end();
IntPoint adjusted_point;
for (; it != end; ++it) {
Node* node = it->GetNode();
float distance_metric = distance_function(touch_hotspot, touch_area, *it);
if (distance_metric < best_distance_metric) {
if (SnapTo(*it, touch_hotspot, touch_area, adjusted_point)) {
target_point = adjusted_point;
target_area = it->BoundingBox();
target_node = node;
best_distance_metric = distance_metric;
}
} else if (distance_metric - best_distance_metric < kZeroTolerance) {
if (SnapTo(*it, touch_hotspot, touch_area, adjusted_point)) {
if (node->IsDescendantOf(target_node)) {
// Try to always return the inner-most element.
target_point = adjusted_point;
target_node = node;
target_area = it->BoundingBox();
}
}
}
}
// As for HitTestResult.innerNode, we skip over pseudo elements.
if (target_node && target_node->IsPseudoElement())
target_node = target_node->ParentOrShadowHostNode();
if (target_node) {
target_area =
target_node->GetDocument().View()->ConvertToRootFrame(target_area);
}
return (target_node);
}
} // namespace touch_adjustment
bool FindBestClickableCandidate(Node*& target_node,
IntPoint& target_point,
const IntPoint& touch_hotspot,
const IntRect& touch_area,
const HeapVector<Member<Node>>& nodes) {
IntRect target_area;
touch_adjustment::SubtargetGeometryList subtargets;
touch_adjustment::CompileSubtargetList(
nodes, subtargets, touch_adjustment::NodeRespondsToTapGesture,
touch_adjustment::AppendBasicSubtargetsForNode);
return touch_adjustment::FindNodeWithLowestDistanceMetric(
target_node, target_point, target_area, touch_hotspot, touch_area,
subtargets, touch_adjustment::HybridDistanceFunction);
}
bool FindBestContextMenuCandidate(Node*& target_node,
IntPoint& target_point,
const IntPoint& touch_hotspot,
const IntRect& touch_area,
const HeapVector<Member<Node>>& nodes) {
IntRect target_area;
touch_adjustment::SubtargetGeometryList subtargets;
touch_adjustment::CompileSubtargetList(
nodes, subtargets, touch_adjustment::ProvidesContextMenuItems,
touch_adjustment::AppendContextSubtargetsForNode);
return touch_adjustment::FindNodeWithLowestDistanceMetric(
target_node, target_point, target_area, touch_hotspot, touch_area,
subtargets, touch_adjustment::HybridDistanceFunction);
}
LayoutSize GetHitTestRectForAdjustment(const LocalFrame& frame,
const LayoutSize& touch_area) {
float device_scale_factor =
frame.GetPage()->GetChromeClient().GetScreenInfo().device_scale_factor;
// Check if zoom-for-dsf is enabled. If not, touch_area is in dip, so we don't
// need to convert max_size_in_dip to physical pixel.
if (frame.GetPage()->DeviceScaleFactorDeprecated() != 1)
device_scale_factor = 1;
float page_scale_factor = frame.GetPage()->PageScaleFactor();
const LayoutSize max_size_in_dip(touch_adjustment::kMaxAdjustmentSizeDip,
touch_adjustment::kMaxAdjustmentSizeDip);
// (when use-zoom-for-dsf enabled) touch_area is in physical pixel scaled,
// max_size_in_dip should be converted to physical pixel and scale too.
return touch_area.ShrunkTo(max_size_in_dip *
(device_scale_factor / page_scale_factor));
}
} // namespace blink