1 /*
2  * Copyright (C) 2012 Nokia Corporation and/or its subsidiary(-ies)
3  *
4  * This library is free software; you can redistribute it and/or
5  * modify it under the terms of the GNU Library General Public
6  * License as published by the Free Software Foundation; either
7  * version 2 of the License, or (at your option) any later version.
8  *
9  * This library is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
12  * Library General Public License for more details.
13  *
14  * You should have received a copy of the GNU Library General Public License
15  * along with this library; see the file COPYING.LIB.  If not, write to
16  * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
17  * Boston, MA 02110-1301, USA.
18  */
19 
20 #include "third_party/blink/renderer/core/page/touch_adjustment.h"
21 
22 #include "third_party/blink/public/common/widget/screen_info.h"
23 #include "third_party/blink/renderer/core/dom/container_node.h"
24 #include "third_party/blink/renderer/core/dom/node.h"
25 #include "third_party/blink/renderer/core/dom/node_computed_style.h"
26 #include "third_party/blink/renderer/core/dom/text.h"
27 #include "third_party/blink/renderer/core/editing/editing_behavior.h"
28 #include "third_party/blink/renderer/core/editing/editing_utilities.h"
29 #include "third_party/blink/renderer/core/editing/editor.h"
30 #include "third_party/blink/renderer/core/editing/frame_selection.h"
31 #include "third_party/blink/renderer/core/frame/local_frame.h"
32 #include "third_party/blink/renderer/core/frame/local_frame_view.h"
33 #include "third_party/blink/renderer/core/html/html_frame_owner_element.h"
34 #include "third_party/blink/renderer/core/layout/layout_box.h"
35 #include "third_party/blink/renderer/core/layout/layout_object.h"
36 #include "third_party/blink/renderer/core/layout/layout_text.h"
37 #include "third_party/blink/renderer/core/page/chrome_client.h"
38 #include "third_party/blink/renderer/core/page/page.h"
39 #include "third_party/blink/renderer/core/style/computed_style.h"
40 #include "third_party/blink/renderer/platform/geometry/float_point.h"
41 #include "third_party/blink/renderer/platform/geometry/float_quad.h"
42 #include "third_party/blink/renderer/platform/geometry/int_size.h"
43 #include "third_party/blink/renderer/platform/text/text_break_iterator.h"
44 
45 namespace blink {
46 
47 namespace touch_adjustment {
48 
49 const float kZeroTolerance = 1e-6f;
50 // The touch adjustment range (diameters) in dip, using same as the value in
51 // gesture_configuration_android.cc
52 constexpr float kMaxAdjustmentSizeDip = 32.f;
53 constexpr float kMinAdjustmentSizeDip = 20.f;
54 
55 // Class for remembering absolute quads of a target node and what node they
56 // represent.
57 class SubtargetGeometry {
58   DISALLOW_NEW();
59 
60  public:
SubtargetGeometry(Node * node,const FloatQuad & quad)61   SubtargetGeometry(Node* node, const FloatQuad& quad)
62       : node_(node), quad_(quad) {}
Trace(Visitor * visitor) const63   void Trace(Visitor* visitor) const { visitor->Trace(node_); }
64 
GetNode() const65   Node* GetNode() const { return node_; }
Quad() const66   FloatQuad Quad() const { return quad_; }
BoundingBox() const67   IntRect BoundingBox() const { return quad_.EnclosingBoundingBox(); }
68 
69  private:
70   Member<Node> node_;
71   FloatQuad quad_;
72 };
73 
74 }  // namespace touch_adjustment
75 
76 }  // namespace blink
77 
78 WTF_ALLOW_MOVE_INIT_AND_COMPARE_WITH_MEM_FUNCTIONS(
79     blink::touch_adjustment::SubtargetGeometry)
80 
81 namespace blink {
82 
83 namespace touch_adjustment {
84 
85 typedef HeapVector<SubtargetGeometry> SubtargetGeometryList;
86 typedef bool (*NodeFilter)(Node*);
87 typedef void (*AppendSubtargetsForNode)(Node*, SubtargetGeometryList&);
88 typedef float (*DistanceFunction)(const IntPoint&,
89                                   const IntRect&,
90                                   const SubtargetGeometry&);
91 
92 // Takes non-const Node* because isContentEditable is a non-const function.
NodeRespondsToTapGesture(Node * node)93 bool NodeRespondsToTapGesture(Node* node) {
94   if (node->WillRespondToMouseClickEvents() ||
95       node->WillRespondToMouseMoveEvents())
96     return true;
97   if (auto* element = DynamicTo<Element>(node)) {
98     // Tapping on a text field or other focusable item should trigger
99     // adjustment, except that iframe elements are hard-coded to support focus
100     // but the effect is often invisible so they should be excluded.
101     if (element->IsMouseFocusable() && !IsA<HTMLIFrameElement>(element))
102       return true;
103     // Accept nodes that has a CSS effect when touched.
104     if (element->ChildrenOrSiblingsAffectedByActive() ||
105         element->ChildrenOrSiblingsAffectedByHover())
106       return true;
107   }
108   if (const ComputedStyle* computed_style = node->GetComputedStyle()) {
109     if (computed_style->AffectedByActive() || computed_style->AffectedByHover())
110       return true;
111   }
112   return false;
113 }
114 
NodeIsZoomTarget(Node * node)115 bool NodeIsZoomTarget(Node* node) {
116   if (node->IsTextNode() || node->IsShadowRoot())
117     return false;
118 
119   DCHECK(node->GetLayoutObject());
120   return node->GetLayoutObject()->IsBox();
121 }
122 
ProvidesContextMenuItems(Node * node)123 bool ProvidesContextMenuItems(Node* node) {
124   // This function tries to match the nodes that receive special context-menu
125   // items in ContextMenuController::populate(), and should be kept up to date
126   // with those.
127   DCHECK(node->GetLayoutObject() || node->IsShadowRoot());
128   if (!node->GetLayoutObject())
129     return false;
130   node->GetDocument().UpdateStyleAndLayoutTree();
131   if (HasEditableStyle(*node))
132     return true;
133   if (node->IsLink())
134     return true;
135   if (node->GetLayoutObject()->IsImage())
136     return true;
137   if (node->GetLayoutObject()->IsMedia())
138     return true;
139   if (node->GetLayoutObject()->CanBeSelectionLeaf()) {
140     // If the context menu gesture will trigger a selection all selectable nodes
141     // are valid targets.
142     if (node->GetLayoutObject()
143             ->GetFrame()
144             ->GetEditor()
145             .Behavior()
146             .ShouldSelectOnContextualMenuClick())
147       return true;
148     // Only the selected part of the layoutObject is a valid target, but this
149     // will be corrected in appendContextSubtargetsForNode.
150     if (node->GetLayoutObject()->IsSelected())
151       return true;
152   }
153   return false;
154 }
155 
AppendQuadsToSubtargetList(Vector<FloatQuad> & quads,Node * node,SubtargetGeometryList & subtargets)156 static inline void AppendQuadsToSubtargetList(
157     Vector<FloatQuad>& quads,
158     Node* node,
159     SubtargetGeometryList& subtargets) {
160   Vector<FloatQuad>::const_iterator it = quads.begin();
161   const Vector<FloatQuad>::const_iterator end = quads.end();
162   for (; it != end; ++it)
163     subtargets.push_back(SubtargetGeometry(node, *it));
164 }
165 
AppendBasicSubtargetsForNode(Node * node,SubtargetGeometryList & subtargets)166 static inline void AppendBasicSubtargetsForNode(
167     Node* node,
168     SubtargetGeometryList& subtargets) {
169   // Node guaranteed to have layoutObject due to check in node filter.
170   DCHECK(node->GetLayoutObject());
171 
172   Vector<FloatQuad> quads;
173   node->GetLayoutObject()->AbsoluteQuads(quads);
174 
175   AppendQuadsToSubtargetList(quads, node, subtargets);
176 }
177 
AppendContextSubtargetsForNode(Node * node,SubtargetGeometryList & subtargets)178 static inline void AppendContextSubtargetsForNode(
179     Node* node,
180     SubtargetGeometryList& subtargets) {
181   // This is a variant of appendBasicSubtargetsForNode that adds special
182   // subtargets for selected or auto-selectable parts of text nodes.
183   DCHECK(node->GetLayoutObject());
184 
185   auto* text_node = DynamicTo<Text>(node);
186   if (!text_node)
187     return AppendBasicSubtargetsForNode(node, subtargets);
188 
189   LayoutText* text_layout_object = text_node->GetLayoutObject();
190 
191   if (text_layout_object->GetFrame()
192           ->GetEditor()
193           .Behavior()
194           .ShouldSelectOnContextualMenuClick()) {
195     // Make subtargets out of every word.
196     String text_value = text_node->data();
197     TextBreakIterator* word_iterator =
198         WordBreakIterator(text_value, 0, text_value.length());
199     int last_offset = word_iterator->first();
200     if (last_offset == -1)
201       return;
202     int offset;
203     while ((offset = word_iterator->next()) != -1) {
204       if (IsWordTextBreak(word_iterator)) {
205         Vector<FloatQuad> quads;
206         text_layout_object->AbsoluteQuadsForRange(quads, last_offset, offset);
207         AppendQuadsToSubtargetList(quads, text_node, subtargets);
208       }
209       last_offset = offset;
210     }
211   } else {
212     if (!text_layout_object->IsSelected())
213       return AppendBasicSubtargetsForNode(node, subtargets);
214     const FrameSelection& frame_selection =
215         text_layout_object->GetFrame()->Selection();
216     const LayoutTextSelectionStatus& selection_status =
217         frame_selection.ComputeLayoutSelectionStatus(*text_layout_object);
218     // If selected, make subtargets out of only the selected part of the text.
219     Vector<FloatQuad> quads;
220     text_layout_object->AbsoluteQuadsForRange(quads, selection_status.start,
221                                               selection_status.end);
222     AppendQuadsToSubtargetList(quads, text_node, subtargets);
223   }
224 }
225 
ParentShadowHostOrOwner(const Node * node)226 static inline Node* ParentShadowHostOrOwner(const Node* node) {
227   if (Node* ancestor = node->ParentOrShadowHostNode())
228     return ancestor;
229   if (auto* document = DynamicTo<Document>(node))
230     return document->LocalOwner();
231   return nullptr;
232 }
233 
234 // Compiles a list of subtargets of all the relevant target nodes.
CompileSubtargetList(const HeapVector<Member<Node>> & intersected_nodes,SubtargetGeometryList & subtargets,NodeFilter node_filter,AppendSubtargetsForNode append_subtargets_for_node)235 void CompileSubtargetList(const HeapVector<Member<Node>>& intersected_nodes,
236                           SubtargetGeometryList& subtargets,
237                           NodeFilter node_filter,
238                           AppendSubtargetsForNode append_subtargets_for_node) {
239   // Find candidates responding to tap gesture events in O(n) time.
240   HeapHashMap<Member<Node>, Member<Node>> responder_map;
241   HeapHashSet<Member<Node>> ancestors_to_responders_set;
242   HeapVector<Member<Node>> candidates;
243   HeapHashSet<Member<Node>> editable_ancestors;
244 
245   // A node matching the NodeFilter is called a responder. Candidate nodes must
246   // either be a responder or have an ancestor that is a responder.  This
247   // iteration tests all ancestors at most once by caching earlier results.
248   for (unsigned i = 0; i < intersected_nodes.size(); ++i) {
249     Node* node = intersected_nodes[i].Get();
250     HeapVector<Member<Node>> visited_nodes;
251     Node* responding_node = nullptr;
252     for (Node* visited_node = node; visited_node;
253          visited_node = visited_node->ParentOrShadowHostNode()) {
254       // Check if we already have a result for a common ancestor from another
255       // candidate.
256       responding_node = responder_map.at(visited_node);
257       if (responding_node)
258         break;
259       visited_nodes.push_back(visited_node);
260       // Check if the node filter applies, which would mean we have found a
261       // responding node.
262       if (node_filter(visited_node)) {
263         responding_node = visited_node;
264         // Continue the iteration to collect the ancestors of the responder,
265         // which we will need later.
266         for (visited_node = ParentShadowHostOrOwner(visited_node); visited_node;
267              visited_node = ParentShadowHostOrOwner(visited_node)) {
268           HeapHashSet<Member<Node>>::AddResult add_result =
269               ancestors_to_responders_set.insert(visited_node);
270           if (!add_result.is_new_entry)
271             break;
272         }
273         break;
274       }
275     }
276     // Insert the detected responder for all the visited nodes.
277     for (unsigned j = 0; j < visited_nodes.size(); j++)
278       responder_map.insert(visited_nodes[j], responding_node);
279 
280     if (responding_node)
281       candidates.push_back(node);
282   }
283 
284   // We compile the list of component absolute quads instead of using the
285   // bounding rect to be able to perform better hit-testing on inline links on
286   // line-breaks.
287   for (unsigned i = 0; i < candidates.size(); i++) {
288     Node* candidate = candidates[i];
289     // Skip nodes who's responders are ancestors of other responders. This gives
290     // preference to the inner-most event-handlers. So that a link is always
291     // preferred even when contained in an element that monitors all
292     // click-events.
293     Node* responding_node = responder_map.at(candidate);
294     DCHECK(responding_node);
295     if (ancestors_to_responders_set.Contains(responding_node))
296       continue;
297     // Consolidate bounds for editable content.
298     if (editable_ancestors.Contains(candidate))
299       continue;
300     candidate->GetDocument().UpdateStyleAndLayoutTree();
301     if (HasEditableStyle(*candidate)) {
302       Node* replacement = candidate;
303       Node* parent = candidate->ParentOrShadowHostNode();
304       while (parent && HasEditableStyle(*parent)) {
305         replacement = parent;
306         if (editable_ancestors.Contains(replacement)) {
307           replacement = nullptr;
308           break;
309         }
310         editable_ancestors.insert(replacement);
311         parent = parent->ParentOrShadowHostNode();
312       }
313       candidate = replacement;
314     }
315     if (candidate)
316       append_subtargets_for_node(candidate, subtargets);
317   }
318 }
319 
320 // This returns quotient of the target area and its intersection with the touch
321 // area.  This will prioritize largest intersection and smallest area, while
322 // balancing the two against each other.
ZoomableIntersectionQuotient(const IntPoint & touch_hotspot,const IntRect & touch_area,const SubtargetGeometry & subtarget)323 float ZoomableIntersectionQuotient(const IntPoint& touch_hotspot,
324                                    const IntRect& touch_area,
325                                    const SubtargetGeometry& subtarget) {
326   IntRect rect = subtarget.GetNode()->GetDocument().View()->ConvertToRootFrame(
327       subtarget.BoundingBox());
328 
329   // Check the rectangle is meaningful zoom target. It should at least contain
330   // the hotspot.
331   if (!rect.Contains(touch_hotspot))
332     return std::numeric_limits<float>::infinity();
333   IntRect intersection = rect;
334   intersection.Intersect(touch_area);
335 
336   // Return the quotient of the intersection.
337   return rect.Size().Area() / (float)intersection.Size().Area();
338 }
339 
340 // Uses a hybrid of distance to adjust and intersect ratio, normalizing each
341 // score between 0 and 1 and combining them. The distance to adjust works best
342 // for disambiguating clicks on targets such as links, where the width may be
343 // significantly larger than the touch width.  Using area of overlap in such
344 // cases can lead to a bias towards shorter links. Conversely, percentage of
345 // overlap can provide strong confidence in tapping on a small target, where the
346 // overlap is often quite high, and works well for tightly packed controls.
HybridDistanceFunction(const IntPoint & touch_hotspot,const IntRect & touch_rect,const SubtargetGeometry & subtarget)347 float HybridDistanceFunction(const IntPoint& touch_hotspot,
348                              const IntRect& touch_rect,
349                              const SubtargetGeometry& subtarget) {
350   IntRect rect = subtarget.GetNode()->GetDocument().View()->ConvertToRootFrame(
351       subtarget.BoundingBox());
352 
353   float radius_squared = 0.25f * (touch_rect.Size().DiagonalLengthSquared());
354   float distance_to_adjust_score =
355       rect.DistanceSquaredToPoint(touch_hotspot) / radius_squared;
356 
357   int max_overlap_width = std::min(touch_rect.Width(), rect.Width());
358   int max_overlap_height = std::min(touch_rect.Height(), rect.Height());
359   float max_overlap_area = std::max(max_overlap_width * max_overlap_height, 1);
360   rect.Intersect(touch_rect);
361   float intersect_area = rect.Size().Area();
362   float intersection_score = 1 - intersect_area / max_overlap_area;
363 
364   float hybrid_score = intersection_score + distance_to_adjust_score;
365 
366   return hybrid_score;
367 }
368 
ConvertToRootFrame(LocalFrameView * view,FloatPoint pt)369 FloatPoint ConvertToRootFrame(LocalFrameView* view, FloatPoint pt) {
370   int x = static_cast<int>(pt.X() + 0.5f);
371   int y = static_cast<int>(pt.Y() + 0.5f);
372   IntPoint adjusted = view->ConvertToRootFrame(IntPoint(x, y));
373   return FloatPoint(adjusted.X(), adjusted.Y());
374 }
375 
376 // Adjusts 'point' to the nearest point inside rect, and leaves it unchanged if
377 // already inside.
AdjustPointToRect(FloatPoint & point,const IntRect & rect)378 void AdjustPointToRect(FloatPoint& point, const IntRect& rect) {
379   if (point.X() < rect.X())
380     point.SetX(rect.X());
381   else if (point.X() > rect.MaxX())
382     point.SetX(rect.MaxX());
383 
384   if (point.Y() < rect.Y())
385     point.SetY(rect.Y());
386   else if (point.Y() > rect.MaxY())
387     point.SetY(rect.MaxY());
388 }
389 
SnapTo(const SubtargetGeometry & geom,const IntPoint & touch_point,const IntRect & touch_area,IntPoint & adjusted_point)390 bool SnapTo(const SubtargetGeometry& geom,
391             const IntPoint& touch_point,
392             const IntRect& touch_area,
393             IntPoint& adjusted_point) {
394   LocalFrameView* view = geom.GetNode()->GetDocument().View();
395   FloatQuad quad = geom.Quad();
396 
397   if (quad.IsRectilinear()) {
398     IntRect bounds = view->ConvertToRootFrame(geom.BoundingBox());
399     if (bounds.Contains(touch_point)) {
400       adjusted_point = touch_point;
401       return true;
402     }
403     if (bounds.Intersects(touch_area)) {
404       bounds.Intersect(touch_area);
405       adjusted_point = bounds.Center();
406       return true;
407     }
408     return false;
409   }
410 
411   // The following code tries to adjust the point to place inside a both the
412   // touchArea and the non-rectilinear quad.
413   // FIXME: This will return the point inside the touch area that is the closest
414   // to the quad center, but does not guarantee that the point will be inside
415   // the quad. Corner-cases exist where the quad will intersect but this will
416   // fail to adjust the point to somewhere in the intersection.
417 
418   FloatPoint p1 = ConvertToRootFrame(view, quad.P1());
419   FloatPoint p2 = ConvertToRootFrame(view, quad.P2());
420   FloatPoint p3 = ConvertToRootFrame(view, quad.P3());
421   FloatPoint p4 = ConvertToRootFrame(view, quad.P4());
422   quad = FloatQuad(p1, p2, p3, p4);
423 
424   if (quad.ContainsPoint(FloatPoint(touch_point))) {
425     adjusted_point = touch_point;
426     return true;
427   }
428 
429   // Pull point towards the center of the element.
430   FloatPoint center = quad.Center();
431 
432   AdjustPointToRect(center, touch_area);
433   adjusted_point = RoundedIntPoint(center);
434 
435   return quad.ContainsPoint(FloatPoint(adjusted_point));
436 }
437 
438 // A generic function for finding the target node with the lowest distance
439 // metric. A distance metric here is the result of a distance-like function,
440 // that computes how well the touch hits the node.  Distance functions could for
441 // instance be distance squared or area of intersection.
FindNodeWithLowestDistanceMetric(Node * & target_node,IntPoint & target_point,IntRect & target_area,const IntPoint & touch_hotspot,const IntRect & touch_area,SubtargetGeometryList & subtargets,DistanceFunction distance_function)442 bool FindNodeWithLowestDistanceMetric(Node*& target_node,
443                                       IntPoint& target_point,
444                                       IntRect& target_area,
445                                       const IntPoint& touch_hotspot,
446                                       const IntRect& touch_area,
447                                       SubtargetGeometryList& subtargets,
448                                       DistanceFunction distance_function) {
449   target_node = nullptr;
450   float best_distance_metric = std::numeric_limits<float>::infinity();
451   SubtargetGeometryList::const_iterator it = subtargets.begin();
452   const SubtargetGeometryList::const_iterator end = subtargets.end();
453   IntPoint adjusted_point;
454 
455   for (; it != end; ++it) {
456     Node* node = it->GetNode();
457     float distance_metric = distance_function(touch_hotspot, touch_area, *it);
458     if (distance_metric < best_distance_metric) {
459       if (SnapTo(*it, touch_hotspot, touch_area, adjusted_point)) {
460         target_point = adjusted_point;
461         target_area = it->BoundingBox();
462         target_node = node;
463         best_distance_metric = distance_metric;
464       }
465     } else if (distance_metric - best_distance_metric < kZeroTolerance) {
466       if (SnapTo(*it, touch_hotspot, touch_area, adjusted_point)) {
467         if (node->IsDescendantOf(target_node)) {
468           // Try to always return the inner-most element.
469           target_point = adjusted_point;
470           target_node = node;
471           target_area = it->BoundingBox();
472         }
473       }
474     }
475   }
476 
477   // As for HitTestResult.innerNode, we skip over pseudo elements.
478   if (target_node && target_node->IsPseudoElement())
479     target_node = target_node->ParentOrShadowHostNode();
480 
481   if (target_node) {
482     target_area =
483         target_node->GetDocument().View()->ConvertToRootFrame(target_area);
484   }
485 
486   return (target_node);
487 }
488 
489 }  // namespace touch_adjustment
490 
FindBestClickableCandidate(Node * & target_node,IntPoint & target_point,const IntPoint & touch_hotspot,const IntRect & touch_area,const HeapVector<Member<Node>> & nodes)491 bool FindBestClickableCandidate(Node*& target_node,
492                                 IntPoint& target_point,
493                                 const IntPoint& touch_hotspot,
494                                 const IntRect& touch_area,
495                                 const HeapVector<Member<Node>>& nodes) {
496   IntRect target_area;
497   touch_adjustment::SubtargetGeometryList subtargets;
498   touch_adjustment::CompileSubtargetList(
499       nodes, subtargets, touch_adjustment::NodeRespondsToTapGesture,
500       touch_adjustment::AppendBasicSubtargetsForNode);
501   return touch_adjustment::FindNodeWithLowestDistanceMetric(
502       target_node, target_point, target_area, touch_hotspot, touch_area,
503       subtargets, touch_adjustment::HybridDistanceFunction);
504 }
505 
FindBestContextMenuCandidate(Node * & target_node,IntPoint & target_point,const IntPoint & touch_hotspot,const IntRect & touch_area,const HeapVector<Member<Node>> & nodes)506 bool FindBestContextMenuCandidate(Node*& target_node,
507                                   IntPoint& target_point,
508                                   const IntPoint& touch_hotspot,
509                                   const IntRect& touch_area,
510                                   const HeapVector<Member<Node>>& nodes) {
511   IntRect target_area;
512   touch_adjustment::SubtargetGeometryList subtargets;
513   touch_adjustment::CompileSubtargetList(
514       nodes, subtargets, touch_adjustment::ProvidesContextMenuItems,
515       touch_adjustment::AppendContextSubtargetsForNode);
516   return touch_adjustment::FindNodeWithLowestDistanceMetric(
517       target_node, target_point, target_area, touch_hotspot, touch_area,
518       subtargets, touch_adjustment::HybridDistanceFunction);
519 }
520 
GetHitTestRectForAdjustment(LocalFrame & frame,const LayoutSize & touch_area)521 LayoutSize GetHitTestRectForAdjustment(LocalFrame& frame,
522                                        const LayoutSize& touch_area) {
523   ChromeClient& chrome_client = frame.GetChromeClient();
524   float device_scale_factor =
525       chrome_client.GetScreenInfo(frame).device_scale_factor;
526   // Check if zoom-for-dsf is enabled. If not, touch_area is in dip, so we don't
527   // need to convert max_size_in_dip to physical pixel.
528   if (frame.GetPage()->DeviceScaleFactorDeprecated() != 1)
529     device_scale_factor = 1;
530 
531   float page_scale_factor = frame.GetPage()->PageScaleFactor();
532   const LayoutSize max_size_in_dip(touch_adjustment::kMaxAdjustmentSizeDip,
533                                    touch_adjustment::kMaxAdjustmentSizeDip);
534 
535   const LayoutSize min_size_in_dip(touch_adjustment::kMinAdjustmentSizeDip,
536                                    touch_adjustment::kMinAdjustmentSizeDip);
537   // (when use-zoom-for-dsf enabled) touch_area is in physical pixel scaled,
538   // max_size_in_dip should be converted to physical pixel and scale too.
539   return touch_area
540       .ShrunkTo(max_size_in_dip * (device_scale_factor / page_scale_factor))
541       .ExpandedTo(min_size_in_dip * (device_scale_factor / page_scale_factor));
542 }
543 
544 }  // namespace blink
545