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