1 /*
2 * Copyright (C) 2009 Nokia Corporation and/or its subsidiary(-ies)
3 * Copyright (C) 2009 Antonio Gomes <tonikitoo@webkit.org>
4 *
5 * All rights reserved.
6 *
7 * Redistribution and use in source and binary forms, with or without
8 * modification, are permitted provided that the following conditions
9 * are met:
10 * 1. Redistributions of source code must retain the above copyright
11 * notice, this list of conditions and the following disclaimer.
12 * 2. Redistributions in binary form must reproduce the above copyright
13 * notice, this list of conditions and the following disclaimer in the
14 * documentation and/or other materials provided with the distribution.
15 *
16 * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
17 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
18 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
20 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
21 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
22 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
23 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
24 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27 */
28
29 #include "third_party/blink/renderer/core/page/spatial_navigation.h"
30
31 #include "third_party/blink/public/mojom/scroll/scrollbar_mode.mojom-blink.h"
32 #include "third_party/blink/renderer/core/dom/node_traversal.h"
33 #include "third_party/blink/renderer/core/frame/local_frame.h"
34 #include "third_party/blink/renderer/core/frame/local_frame_view.h"
35 #include "third_party/blink/renderer/core/frame/settings.h"
36 #include "third_party/blink/renderer/core/frame/visual_viewport.h"
37 #include "third_party/blink/renderer/core/html/html_area_element.h"
38 #include "third_party/blink/renderer/core/html/html_frame_owner_element.h"
39 #include "third_party/blink/renderer/core/html/html_image_element.h"
40 #include "third_party/blink/renderer/core/html_names.h"
41 #include "third_party/blink/renderer/core/input/event_handler.h"
42 #include "third_party/blink/renderer/core/layout/layout_box.h"
43 #include "third_party/blink/renderer/core/layout/layout_inline.h"
44 #include "third_party/blink/renderer/core/layout/layout_view.h"
45 #include "third_party/blink/renderer/core/page/frame_tree.h"
46 #include "third_party/blink/renderer/core/page/page.h"
47 #include "third_party/blink/renderer/core/paint/paint_layer.h"
48 #include "third_party/blink/renderer/core/paint/paint_layer_scrollable_area.h"
49 #include "third_party/blink/renderer/platform/geometry/int_rect.h"
50
51 namespace blink {
52
53 // A small integer that easily fits into a double with a good margin for
54 // arithmetic. In particular, we don't want to use
55 // std::numeric_limits<double>::lowest() because, if subtracted, it becomes
56 // NaN which will make all following arithmetic NaN too (an unusable number).
57 constexpr double kMinDistance = std::numeric_limits<int>::lowest();
58 constexpr double kPriorityClassA = kMinDistance;
59 constexpr double kPriorityClassB = kMinDistance / 2;
60
61 constexpr int kFudgeFactor = 2;
62
FocusCandidate(Node * node,SpatialNavigationDirection direction)63 FocusCandidate::FocusCandidate(Node* node, SpatialNavigationDirection direction)
64 : visible_node(nullptr), focusable_node(nullptr), is_offscreen(true) {
65 DCHECK(node);
66 DCHECK(node->IsElementNode());
67
68 if (auto* area = DynamicTo<HTMLAreaElement>(*node)) {
69 HTMLImageElement* image = area->ImageElement();
70 if (!image || !image->GetLayoutObject())
71 return;
72
73 visible_node = image;
74 rect_in_root_frame = StartEdgeForAreaElement(*area, direction);
75 } else {
76 if (!node->GetLayoutObject())
77 return;
78
79 visible_node = node;
80 rect_in_root_frame = NodeRectInRootFrame(node);
81
82 // Remove any overlap with line boxes *above* the search origin.
83 rect_in_root_frame =
84 ShrinkInlineBoxToLineBox(*node->GetLayoutObject(), rect_in_root_frame);
85 }
86
87 focusable_node = node;
88 is_offscreen = IsOffscreen(visible_node);
89 }
90
IsSpatialNavigationEnabled(const LocalFrame * frame)91 bool IsSpatialNavigationEnabled(const LocalFrame* frame) {
92 return (frame && frame->GetSettings() &&
93 frame->GetSettings()->GetSpatialNavigationEnabled());
94 }
95
RectsIntersectOnOrthogonalAxis(SpatialNavigationDirection direction,const PhysicalRect & a,const PhysicalRect & b)96 static bool RectsIntersectOnOrthogonalAxis(SpatialNavigationDirection direction,
97 const PhysicalRect& a,
98 const PhysicalRect& b) {
99 switch (direction) {
100 case SpatialNavigationDirection::kLeft:
101 case SpatialNavigationDirection::kRight:
102 return a.Bottom() > b.Y() && a.Y() < b.Bottom();
103 case SpatialNavigationDirection::kUp:
104 case SpatialNavigationDirection::kDown:
105 return a.Right() > b.X() && a.X() < b.Right();
106 default:
107 NOTREACHED();
108 return false;
109 }
110 }
111
112 // Return true if rect |a| is below |b|. False otherwise.
113 // For overlapping rects, |a| is considered to be below |b|
114 // if both edges of |a| are below the respective ones of |b|.
Below(const PhysicalRect & a,const PhysicalRect & b)115 static inline bool Below(const PhysicalRect& a, const PhysicalRect& b) {
116 return a.Y() >= b.Bottom() || (a.Y() >= b.Y() && a.Bottom() > b.Bottom() &&
117 a.X() < b.Right() && a.Right() > b.X());
118 }
119
120 // Return true if rect |a| is on the right of |b|. False otherwise.
121 // For overlapping rects, |a| is considered to be on the right of |b|
122 // if both edges of |a| are on the right of the respective ones of |b|.
RightOf(const PhysicalRect & a,const PhysicalRect & b)123 static inline bool RightOf(const PhysicalRect& a, const PhysicalRect& b) {
124 return a.X() >= b.Right() || (a.X() >= b.X() && a.Right() > b.Right() &&
125 a.Y() < b.Bottom() && a.Bottom() > b.Y());
126 }
127
IsRectInDirection(SpatialNavigationDirection direction,const PhysicalRect & cur_rect,const PhysicalRect & target_rect)128 static bool IsRectInDirection(SpatialNavigationDirection direction,
129 const PhysicalRect& cur_rect,
130 const PhysicalRect& target_rect) {
131 switch (direction) {
132 case SpatialNavigationDirection::kLeft:
133 return RightOf(cur_rect, target_rect);
134 case SpatialNavigationDirection::kRight:
135 return RightOf(target_rect, cur_rect);
136 case SpatialNavigationDirection::kUp:
137 return Below(cur_rect, target_rect);
138 case SpatialNavigationDirection::kDown:
139 return Below(target_rect, cur_rect);
140 default:
141 NOTREACHED();
142 return false;
143 }
144 }
145
LineBoxes(const LayoutObject & layout_object)146 int LineBoxes(const LayoutObject& layout_object) {
147 if (!layout_object.IsInline() || layout_object.IsAtomicInlineLevel())
148 return 1;
149
150 // If it has empty quads, it's most likely not a line broken ("fragmented")
151 // text. <a><div></div></a> has for example one empty rect.
152 Vector<FloatQuad> quads;
153 layout_object.AbsoluteQuads(quads);
154 for (const FloatQuad& quad : quads) {
155 if (quad.IsEmpty())
156 return 1;
157 }
158
159 return quads.size();
160 }
161
IsFragmentedInline(const LayoutObject & layout_object)162 bool IsFragmentedInline(const LayoutObject& layout_object) {
163 return LineBoxes(layout_object) > 1;
164 }
165
RectInViewport(const Node & node)166 FloatRect RectInViewport(const Node& node) {
167 LocalFrameView* frame_view = node.GetDocument().View();
168 if (!frame_view)
169 return FloatRect();
170
171 DCHECK(!frame_view->NeedsLayout());
172
173 LayoutObject* object = node.GetLayoutObject();
174 if (!object)
175 return FloatRect();
176
177 PhysicalRect rect_in_root_frame = NodeRectInRootFrame(&node);
178
179 // Convert to the visual viewport which will account for pinch zoom.
180 VisualViewport& visual_viewport =
181 object->GetDocument().GetPage()->GetVisualViewport();
182 FloatRect rect_in_viewport =
183 visual_viewport.RootFrameToViewport(FloatRect(rect_in_root_frame));
184
185 // RootFrameToViewport doesn't clip so manually apply the viewport clip here.
186 FloatRect viewport_rect =
187 FloatRect(FloatPoint(), FloatSize(visual_viewport.Size()));
188 rect_in_viewport.Intersect(viewport_rect);
189
190 return rect_in_viewport;
191 }
192
193 // Answers true if |node| is completely outside the user's (visual) viewport.
194 // This logic is used by spatnav to rule out offscreen focus candidates and an
195 // offscreen activeElement. When activeElement is offscreen, spatnav doesn't use
196 // it as the search origin; the search will start at an edge of the visual
197 // viewport instead.
198 // TODO(crbug.com/889840): Fix VisibleBoundsInVisualViewport().
199 // If VisibleBoundsInVisualViewport() would have taken "element-clips" into
200 // account, spatnav could have called it directly; no need to check the
201 // LayoutObject's VisibleContentRect.
IsOffscreen(const Node * node)202 bool IsOffscreen(const Node* node) {
203 DCHECK(node);
204 return RectInViewport(*node).IsEmpty();
205 }
206
ScrollableAreaFor(const Node * node)207 ScrollableArea* ScrollableAreaFor(const Node* node) {
208 if (node->IsDocumentNode()) {
209 LocalFrameView* view = node->GetDocument().View();
210 if (!view)
211 return nullptr;
212
213 return view->GetScrollableArea();
214 }
215
216 LayoutObject* object = node->GetLayoutObject();
217 if (!object || !object->IsBox())
218 return nullptr;
219
220 return To<LayoutBox>(object)->GetScrollableArea();
221 }
222
IsUnobscured(const FocusCandidate & candidate)223 bool IsUnobscured(const FocusCandidate& candidate) {
224 DCHECK(candidate.visible_node);
225
226 const LocalFrame* local_main_frame = DynamicTo<LocalFrame>(
227 candidate.visible_node->GetDocument().GetPage()->MainFrame());
228 if (!local_main_frame)
229 return false;
230
231 // TODO(crbug.com/955952): We cannot evaluate visibility for media element
232 // using hit test since attached media controls cover media element.
233 if (candidate.visible_node->IsMediaElement())
234 return true;
235
236 PhysicalRect viewport_rect(
237 local_main_frame->GetPage()->GetVisualViewport().VisibleContentRect());
238 PhysicalRect interesting_rect =
239 Intersection(candidate.rect_in_root_frame, viewport_rect);
240
241 if (interesting_rect.IsEmpty())
242 return false;
243
244 HitTestLocation location(interesting_rect);
245 HitTestResult result =
246 local_main_frame->GetEventHandler().HitTestResultAtLocation(
247 location, HitTestRequest::kReadOnly | HitTestRequest::kListBased |
248 HitTestRequest::kIgnoreZeroOpacityObjects |
249 HitTestRequest::kAllowChildFrameContent);
250
251 const HitTestResult::NodeSet& nodes = result.ListBasedTestResult();
252 for (auto hit_node = nodes.rbegin(); hit_node != nodes.rend(); ++hit_node) {
253 if (candidate.visible_node->ContainsIncludingHostElements(**hit_node))
254 return true;
255
256 if (FrameOwnerElement(candidate) &&
257 FrameOwnerElement(candidate)
258 ->contentDocument()
259 ->ContainsIncludingHostElements(**hit_node))
260 return true;
261 }
262
263 return false;
264 }
265
HasRemoteFrame(const Node * node)266 bool HasRemoteFrame(const Node* node) {
267 auto* frame_owner_element = DynamicTo<HTMLFrameOwnerElement>(node);
268 if (!frame_owner_element)
269 return false;
270
271 return frame_owner_element->ContentFrame() &&
272 frame_owner_element->ContentFrame()->IsRemoteFrame();
273 }
274
ScrollInDirection(Node * container,SpatialNavigationDirection direction)275 bool ScrollInDirection(Node* container, SpatialNavigationDirection direction) {
276 DCHECK(container);
277
278 if (!CanScrollInDirection(container, direction))
279 return false;
280
281 int dx = 0;
282 int dy = 0;
283 int pixels_per_line_step =
284 ScrollableArea::PixelsPerLineStep(container->GetDocument().GetFrame());
285 switch (direction) {
286 case SpatialNavigationDirection::kLeft:
287 dx = -pixels_per_line_step;
288 break;
289 case SpatialNavigationDirection::kRight:
290 // TODO(bokan, https://crbug.com/952326): Fix this DCHECK.
291 // DCHECK_GT(container->GetLayoutBox()->ScrollWidth(),
292 // container->GetLayoutBoxForScrolling()
293 // ->GetScrollableArea()
294 // ->ScrollPosition()
295 // .X() +
296 // container->GetLayoutBox()->ClientWidth());
297 dx = pixels_per_line_step;
298 break;
299 case SpatialNavigationDirection::kUp:
300 dy = -pixels_per_line_step;
301 break;
302 case SpatialNavigationDirection::kDown:
303 // TODO(bokan, https://crbug.com/952326): Fix this DCHECK.
304 // DCHECK_GT(container->GetLayoutBox()->ScrollHeight(),
305 // container->GetLayoutBoxForScrolling()
306 // ->GetScrollableArea()
307 // ->ScrollPosition()
308 // .Y() +
309 // container->GetLayoutBox()->ClientHeight());
310 dy = pixels_per_line_step;
311 break;
312 default:
313 NOTREACHED();
314 return false;
315 }
316
317 // TODO(crbug.com/914775): Use UserScroll() instead. UserScroll() does a
318 // smooth, animated scroll which might make it easier for users to understand
319 // spatnav's moves. Another advantage of using ScrollableArea::UserScroll() is
320 // that it returns a ScrollResult so we don't need to call
321 // CanScrollInDirection(). Regular arrow-key scrolling (without
322 // --enable-spatial-navigation) already uses smooth scrolling by default.
323 ScrollableArea* scroller = ScrollableAreaFor(container);
324 if (!scroller)
325 return false;
326
327 scroller->ScrollBy(ScrollOffset(dx, dy), mojom::blink::ScrollType::kUser);
328 return true;
329 }
330
IsScrollableNode(const Node * node)331 bool IsScrollableNode(const Node* node) {
332 if (!node)
333 return false;
334
335 if (node->IsDocumentNode())
336 return true;
337
338 if (auto* box = DynamicTo<LayoutBox>(node->GetLayoutObject()))
339 return box->CanBeScrolledAndHasScrollableArea();
340 return false;
341 }
342
ScrollableAreaOrDocumentOf(Node * node)343 Node* ScrollableAreaOrDocumentOf(Node* node) {
344 DCHECK(node);
345 Node* parent = node;
346 do {
347 // FIXME: Spatial navigation is broken for OOPI.
348 if (auto* document = DynamicTo<Document>(parent))
349 parent = document->GetFrame()->DeprecatedLocalOwner();
350 else
351 parent = parent->ParentOrShadowHostNode();
352 } while (parent && !IsScrollableAreaOrDocument(parent));
353
354 return parent;
355 }
356
IsScrollableAreaOrDocument(const Node * node)357 bool IsScrollableAreaOrDocument(const Node* node) {
358 if (!node)
359 return false;
360
361 auto* frame_owner_element = DynamicTo<HTMLFrameOwnerElement>(node);
362 return (frame_owner_element && frame_owner_element->ContentFrame()) ||
363 IsScrollableNode(node);
364 }
365
CanScrollInDirection(const Node * container,SpatialNavigationDirection direction)366 bool CanScrollInDirection(const Node* container,
367 SpatialNavigationDirection direction) {
368 DCHECK(container);
369 if (auto* document = DynamicTo<Document>(container))
370 return CanScrollInDirection(document->GetFrame(), direction);
371
372 if (!IsScrollableNode(container))
373 return false;
374
375 const Element* container_element = DynamicTo<Element>(container);
376 if (!container_element)
377 return false;
378 LayoutBox* box = container_element->GetLayoutBoxForScrolling();
379 if (!box)
380 return false;
381 auto* scrollable_area = box->GetScrollableArea();
382 if (!scrollable_area)
383 return false;
384
385 DCHECK(container->GetLayoutObject());
386 switch (direction) {
387 case SpatialNavigationDirection::kLeft:
388 return (container->GetLayoutObject()->Style()->OverflowX() !=
389 EOverflow::kHidden &&
390 scrollable_area->ScrollPosition().X() > 0);
391 case SpatialNavigationDirection::kUp:
392 return (container->GetLayoutObject()->Style()->OverflowY() !=
393 EOverflow::kHidden &&
394 scrollable_area->ScrollPosition().Y() > 0);
395 case SpatialNavigationDirection::kRight:
396 return (container->GetLayoutObject()->Style()->OverflowX() !=
397 EOverflow::kHidden &&
398 LayoutUnit(scrollable_area->ScrollPosition().X()) +
399 container->GetLayoutBox()->ClientWidth() <
400 container->GetLayoutBox()->ScrollWidth());
401 case SpatialNavigationDirection::kDown:
402 return (container->GetLayoutObject()->Style()->OverflowY() !=
403 EOverflow::kHidden &&
404 LayoutUnit(scrollable_area->ScrollPosition().Y()) +
405 container->GetLayoutBox()->ClientHeight() <
406 container->GetLayoutBox()->ScrollHeight());
407 default:
408 NOTREACHED();
409 return false;
410 }
411 }
412
CanScrollInDirection(const LocalFrame * frame,SpatialNavigationDirection direction)413 bool CanScrollInDirection(const LocalFrame* frame,
414 SpatialNavigationDirection direction) {
415 if (!frame->View())
416 return false;
417 LayoutView* layoutView = frame->ContentLayoutObject();
418 if (!layoutView)
419 return false;
420 mojom::blink::ScrollbarMode vertical_mode;
421 mojom::blink::ScrollbarMode horizontal_mode;
422 layoutView->CalculateScrollbarModes(horizontal_mode, vertical_mode);
423 if ((direction == SpatialNavigationDirection::kLeft ||
424 direction == SpatialNavigationDirection::kRight) &&
425 mojom::blink::ScrollbarMode::kAlwaysOff == horizontal_mode)
426 return false;
427 if ((direction == SpatialNavigationDirection::kUp ||
428 direction == SpatialNavigationDirection::kDown) &&
429 mojom::blink::ScrollbarMode::kAlwaysOff == vertical_mode)
430 return false;
431 ScrollableArea* scrollable_area = frame->View()->GetScrollableArea();
432 LayoutSize size(scrollable_area->ContentsSize());
433 LayoutSize offset(scrollable_area->ScrollOffsetInt());
434 PhysicalRect rect(scrollable_area->VisibleContentRect(kIncludeScrollbars));
435
436 switch (direction) {
437 case SpatialNavigationDirection::kLeft:
438 return offset.Width() > 0;
439 case SpatialNavigationDirection::kUp:
440 return offset.Height() > 0;
441 case SpatialNavigationDirection::kRight:
442 return rect.Width() + offset.Width() < size.Width();
443 case SpatialNavigationDirection::kDown:
444 return rect.Height() + offset.Height() < size.Height();
445 default:
446 NOTREACHED();
447 return false;
448 }
449 }
450
NodeRectInRootFrame(const Node * node)451 PhysicalRect NodeRectInRootFrame(const Node* node) {
452 DCHECK(node);
453 DCHECK(node->GetLayoutObject());
454 DCHECK(!node->GetDocument().View()->NeedsLayout());
455
456 LayoutObject* object = node->GetLayoutObject();
457
458 PhysicalRect rect = PhysicalRect::EnclosingRect(
459 object->LocalBoundingBoxRectForAccessibility());
460
461 // Inset the bounding box by the border.
462 // TODO(bokan): As far as I can tell, this is to work around empty iframes
463 // that have a border. It's unclear if that's still useful.
464 rect.ContractEdges(LayoutUnit(object->StyleRef().BorderTopWidth()),
465 LayoutUnit(object->StyleRef().BorderRightWidth()),
466 LayoutUnit(object->StyleRef().BorderBottomWidth()),
467 LayoutUnit(object->StyleRef().BorderLeftWidth()));
468
469 object->MapToVisualRectInAncestorSpace(/*ancestor=*/nullptr, rect);
470 return rect;
471 }
472
473 // This method calculates the exitPoint from the startingRect and the entryPoint
474 // into the candidate rect. The line between those 2 points is the closest
475 // distance between the 2 rects. Takes care of overlapping rects, defining
476 // points so that the distance between them is zero where necessary.
EntryAndExitPointsForDirection(SpatialNavigationDirection direction,const PhysicalRect & starting_rect,const PhysicalRect & potential_rect,LayoutPoint & exit_point,LayoutPoint & entry_point)477 void EntryAndExitPointsForDirection(SpatialNavigationDirection direction,
478 const PhysicalRect& starting_rect,
479 const PhysicalRect& potential_rect,
480 LayoutPoint& exit_point,
481 LayoutPoint& entry_point) {
482 switch (direction) {
483 case SpatialNavigationDirection::kLeft:
484 exit_point.SetX(starting_rect.X());
485 if (potential_rect.Right() < starting_rect.X())
486 entry_point.SetX(potential_rect.Right());
487 else
488 entry_point.SetX(starting_rect.X());
489 break;
490 case SpatialNavigationDirection::kUp:
491 exit_point.SetY(starting_rect.Y());
492 if (potential_rect.Bottom() < starting_rect.Y())
493 entry_point.SetY(potential_rect.Bottom());
494 else
495 entry_point.SetY(starting_rect.Y());
496 break;
497 case SpatialNavigationDirection::kRight:
498 exit_point.SetX(starting_rect.Right());
499 if (potential_rect.X() > starting_rect.Right())
500 entry_point.SetX(potential_rect.X());
501 else
502 entry_point.SetX(starting_rect.Right());
503 break;
504 case SpatialNavigationDirection::kDown:
505 exit_point.SetY(starting_rect.Bottom());
506 if (potential_rect.Y() > starting_rect.Bottom())
507 entry_point.SetY(potential_rect.Y());
508 else
509 entry_point.SetY(starting_rect.Bottom());
510 break;
511 default:
512 NOTREACHED();
513 }
514
515 switch (direction) {
516 case SpatialNavigationDirection::kLeft:
517 case SpatialNavigationDirection::kRight:
518 if (Below(starting_rect, potential_rect)) {
519 exit_point.SetY(starting_rect.Y());
520 if (potential_rect.Bottom() < starting_rect.Y())
521 entry_point.SetY(potential_rect.Bottom());
522 else
523 entry_point.SetY(starting_rect.Y());
524 } else if (Below(potential_rect, starting_rect)) {
525 exit_point.SetY(starting_rect.Bottom());
526 if (potential_rect.Y() > starting_rect.Bottom())
527 entry_point.SetY(potential_rect.Y());
528 else
529 entry_point.SetY(starting_rect.Bottom());
530 } else {
531 exit_point.SetY(max(starting_rect.Y(), potential_rect.Y()));
532 entry_point.SetY(exit_point.Y());
533 }
534 break;
535 case SpatialNavigationDirection::kUp:
536 case SpatialNavigationDirection::kDown:
537 if (RightOf(starting_rect, potential_rect)) {
538 exit_point.SetX(starting_rect.X());
539 if (potential_rect.Right() < starting_rect.X())
540 entry_point.SetX(potential_rect.Right());
541 else
542 entry_point.SetX(starting_rect.X());
543 } else if (RightOf(potential_rect, starting_rect)) {
544 exit_point.SetX(starting_rect.Right());
545 if (potential_rect.X() > starting_rect.Right())
546 entry_point.SetX(potential_rect.X());
547 else
548 entry_point.SetX(starting_rect.Right());
549 } else {
550 exit_point.SetX(max(starting_rect.X(), potential_rect.X()));
551 entry_point.SetX(exit_point.X());
552 }
553 break;
554 default:
555 NOTREACHED();
556 }
557 }
558
ProjectedOverlap(SpatialNavigationDirection direction,PhysicalRect current,PhysicalRect candidate)559 double ProjectedOverlap(SpatialNavigationDirection direction,
560 PhysicalRect current,
561 PhysicalRect candidate) {
562 switch (direction) {
563 case SpatialNavigationDirection::kLeft:
564 case SpatialNavigationDirection::kRight:
565 current.SetWidth(LayoutUnit(1));
566 candidate.SetX(current.X());
567 current.Intersect(candidate);
568 return current.Height();
569 case SpatialNavigationDirection::kUp:
570 case SpatialNavigationDirection::kDown:
571 current.SetHeight(LayoutUnit(1));
572 candidate.SetY(current.Y());
573 current.Intersect(candidate);
574 return current.Width();
575 default:
576 NOTREACHED();
577 return kMaxDistance;
578 }
579 }
580
Alignment(SpatialNavigationDirection direction,PhysicalRect current,PhysicalRect candidate)581 double Alignment(SpatialNavigationDirection direction,
582 PhysicalRect current,
583 PhysicalRect candidate) {
584 // The formula and constants for "alignment" are experimental and
585 // come from https://drafts.csswg.org/css-nav-1/#heuristics.
586 const int kAlignWeight = 5;
587
588 double projected_overlap = ProjectedOverlap(direction, current, candidate);
589 switch (direction) {
590 case SpatialNavigationDirection::kLeft:
591 case SpatialNavigationDirection::kRight:
592 return (kAlignWeight * projected_overlap) / current.Height();
593 case SpatialNavigationDirection::kUp:
594 case SpatialNavigationDirection::kDown:
595 return (kAlignWeight * projected_overlap) / current.Width();
596 default:
597 NOTREACHED();
598 return kMaxDistance;
599 }
600 }
601
BothOnTopmostPaintLayerInStackingContext(const FocusCandidate & current_interest,const FocusCandidate & candidate)602 bool BothOnTopmostPaintLayerInStackingContext(
603 const FocusCandidate& current_interest,
604 const FocusCandidate& candidate) {
605 if (!current_interest.visible_node)
606 return false;
607
608 const LayoutObject* origin = current_interest.visible_node->GetLayoutObject();
609 const PaintLayer* focused_layer = origin->PaintingLayer();
610 if (!focused_layer || focused_layer->IsRootLayer())
611 return false;
612
613 const LayoutObject* next = candidate.visible_node->GetLayoutObject();
614 const PaintLayer* candidate_layer = next->PaintingLayer();
615 if (focused_layer != candidate_layer)
616 return false;
617
618 return !candidate_layer->HasVisibleDescendant();
619 }
620
ComputeDistanceDataForNode(SpatialNavigationDirection direction,const FocusCandidate & current_interest,const FocusCandidate & candidate)621 double ComputeDistanceDataForNode(SpatialNavigationDirection direction,
622 const FocusCandidate& current_interest,
623 const FocusCandidate& candidate) {
624 double distance = 0.0;
625 PhysicalRect node_rect = candidate.rect_in_root_frame;
626 PhysicalRect current_rect = current_interest.rect_in_root_frame;
627 if (node_rect.Contains(current_rect)) {
628 // When leaving an "insider", don't focus its underlaying container box.
629 // Go directly to the outside world. This avoids focus from being trapped
630 // inside a container.
631 return kMaxDistance;
632 }
633
634 if (current_rect.Contains(node_rect)) {
635 // We give highest priority to "insiders", candidates that are completely
636 // inside the current focus rect, by giving them a negative, < 0, distance
637 // number.
638 distance = kPriorityClassA;
639
640 // For insiders we cannot meassure the distance from the outer box. Instead,
641 // we meassure distance _from_ the focused container's rect's "opposite
642 // edge" in the navigated direction, just like we do when we look for
643 // candidates inside a focused scroll container.
644 current_rect = OppositeEdge(direction, current_rect);
645
646 // This candidate fully overlaps the current focus rect so we can omit the
647 // overlap term of the equation. An "insider" will always win against an
648 // "outsider".
649 } else if (!IsRectInDirection(direction, current_rect, node_rect)) {
650 return kMaxDistance;
651 } else if (BothOnTopmostPaintLayerInStackingContext(current_interest,
652 candidate)) {
653 // Prioritize "popup candidates" over other candidates by giving them a
654 // negative, < 0, distance number.
655 distance = kPriorityClassB;
656 }
657
658 LayoutPoint exit_point;
659 LayoutPoint entry_point;
660 EntryAndExitPointsForDirection(direction, current_rect, node_rect, exit_point,
661 entry_point);
662
663 LayoutUnit x_axis = (exit_point.X() - entry_point.X()).Abs();
664 LayoutUnit y_axis = (exit_point.Y() - entry_point.Y()).Abs();
665 double euclidian_distance =
666 sqrt((x_axis * x_axis + y_axis * y_axis).ToDouble());
667 distance += euclidian_distance;
668
669 LayoutUnit navigation_axis_distance;
670 LayoutUnit weighted_orthogonal_axis_distance;
671
672 // Bias and weights are put to the orthogonal axis distance calculation
673 // so aligned candidates would have advantage over partially-aligned ones
674 // and then over not-aligned candidates. The bias is given to not-aligned
675 // candidates with respect to size of the current rect. The weight for
676 // left/right direction is given a higher value to allow navigation on
677 // common horizonally-aligned elements. The hardcoded values are based on
678 // tests and experiments.
679 const int kOrthogonalWeightForLeftRight = 30;
680 const int kOrthogonalWeightForUpDown = 2;
681 int orthogonal_bias = 0;
682
683 switch (direction) {
684 case SpatialNavigationDirection::kLeft:
685 case SpatialNavigationDirection::kRight:
686 navigation_axis_distance = x_axis;
687 if (!RectsIntersectOnOrthogonalAxis(direction, current_rect, node_rect))
688 orthogonal_bias = (current_rect.Height() / 2).ToInt();
689 weighted_orthogonal_axis_distance =
690 (y_axis + orthogonal_bias) * kOrthogonalWeightForLeftRight;
691 break;
692 case SpatialNavigationDirection::kUp:
693 case SpatialNavigationDirection::kDown:
694 navigation_axis_distance = y_axis;
695 if (!RectsIntersectOnOrthogonalAxis(direction, current_rect, node_rect))
696 orthogonal_bias = (current_rect.Width() / 2).ToInt();
697 weighted_orthogonal_axis_distance =
698 (x_axis + orthogonal_bias) * kOrthogonalWeightForUpDown;
699 break;
700 default:
701 NOTREACHED();
702 return kMaxDistance;
703 }
704
705 // We try to formalize this distance calculation at
706 // https://drafts.csswg.org/css-nav-1/.
707 distance += weighted_orthogonal_axis_distance.ToDouble() +
708 navigation_axis_distance.ToDouble();
709 return distance - Alignment(direction, current_rect, node_rect);
710 }
711
712 // Returns a thin rectangle that represents one of |box|'s edges.
713 // To not intersect elements that are positioned inside |box|, we add one
714 // LayoutUnit of margin that puts the returned slice "just outside" |box|.
OppositeEdge(SpatialNavigationDirection side,const PhysicalRect & box,LayoutUnit thickness)715 PhysicalRect OppositeEdge(SpatialNavigationDirection side,
716 const PhysicalRect& box,
717 LayoutUnit thickness) {
718 PhysicalRect thin_rect = box;
719 switch (side) {
720 case SpatialNavigationDirection::kLeft:
721 thin_rect.SetX(thin_rect.Right() - thickness);
722 thin_rect.SetWidth(thickness);
723 thin_rect.offset.left += 1;
724 break;
725 case SpatialNavigationDirection::kRight:
726 thin_rect.SetWidth(thickness);
727 thin_rect.offset.left -= 1;
728 break;
729 case SpatialNavigationDirection::kDown:
730 thin_rect.SetHeight(thickness);
731 thin_rect.offset.top -= 1;
732 break;
733 case SpatialNavigationDirection::kUp:
734 thin_rect.SetY(thin_rect.Bottom() - thickness);
735 thin_rect.SetHeight(thickness);
736 thin_rect.offset.top += 1;
737 break;
738 default:
739 NOTREACHED();
740 }
741
742 return thin_rect;
743 }
744
StartEdgeForAreaElement(const HTMLAreaElement & area,SpatialNavigationDirection direction)745 PhysicalRect StartEdgeForAreaElement(const HTMLAreaElement& area,
746 SpatialNavigationDirection direction) {
747 DCHECK(area.ImageElement());
748 // Area elements tend to overlap more than other focusable elements. We
749 // flatten the rect of the area elements to minimize the effect of overlapping
750 // areas.
751 PhysicalRect rect = OppositeEdge(
752 direction,
753 area.GetDocument().GetFrame()->View()->ConvertToRootFrame(
754 area.ComputeAbsoluteRect(area.ImageElement()->GetLayoutObject())),
755 LayoutUnit(kFudgeFactor) /* snav-imagemap-overlapped-areas.html */);
756 return rect;
757 }
758
FrameOwnerElement(const FocusCandidate & candidate)759 HTMLFrameOwnerElement* FrameOwnerElement(const FocusCandidate& candidate) {
760 return DynamicTo<HTMLFrameOwnerElement>(candidate.visible_node);
761 }
762
763 // The visual viewport's rect (given in the root frame's coordinate space).
RootViewport(const LocalFrame * current_frame)764 PhysicalRect RootViewport(const LocalFrame* current_frame) {
765 return PhysicalRect::EnclosingRect(
766 current_frame->GetPage()->GetVisualViewport().VisibleRect());
767 }
768
769 // Ignores fragments that are completely offscreen.
770 // Returns the first one that is not offscreen, in the given iterator range.
771 template <class Iterator>
FirstVisibleFragment(const PhysicalRect & visibility,Iterator fragment,Iterator end)772 PhysicalRect FirstVisibleFragment(const PhysicalRect& visibility,
773 Iterator fragment,
774 Iterator end) {
775 while (fragment != end) {
776 PhysicalRect physical_fragment =
777 PhysicalRect::EnclosingRect(fragment->BoundingBox());
778 physical_fragment.Intersect(visibility);
779 if (!physical_fragment.IsEmpty())
780 return physical_fragment;
781 ++fragment;
782 }
783 return visibility;
784 }
785
GetLogicalHeight(const PhysicalRect & rect,const LayoutObject & layout_object)786 LayoutUnit GetLogicalHeight(const PhysicalRect& rect,
787 const LayoutObject& layout_object) {
788 if (layout_object.IsHorizontalWritingMode())
789 return rect.Height();
790 else
791 return rect.Width();
792 }
793
SetLogicalHeight(PhysicalRect & rect,const LayoutObject & layout_object,LayoutUnit height)794 void SetLogicalHeight(PhysicalRect& rect,
795 const LayoutObject& layout_object,
796 LayoutUnit height) {
797 if (layout_object.IsHorizontalWritingMode())
798 rect.SetHeight(height);
799 else
800 rect.SetWidth(height);
801 }
802
TallestInlineAtomicChild(const LayoutObject & layout_object)803 LayoutUnit TallestInlineAtomicChild(const LayoutObject& layout_object) {
804 LayoutUnit max_child_size(0);
805
806 if (!layout_object.IsLayoutInline())
807 return max_child_size;
808
809 for (LayoutObject* child = layout_object.SlowFirstChild(); child;
810 child = child->NextSibling()) {
811 if (child->IsOutOfFlowPositioned())
812 continue;
813
814 if (child->IsAtomicInlineLevel()) {
815 max_child_size =
816 std::max(To<LayoutBox>(child)->LogicalHeight(), max_child_size);
817 }
818 }
819
820 return max_child_size;
821 }
822
823 // "Although margins, borders, and padding of non-replaced elements do not
824 // enter into the line box calculation, they are still rendered around
825 // inline boxes. This means that if the height specified by line-height is
826 // less than the content height of contained boxes, backgrounds and colors
827 // of padding and borders may "bleed" into adjoining line boxes". [1]
828 // [1] https://drafts.csswg.org/css2/#leading
829 // [2] https://drafts.csswg.org/css2/#line-box
830 // [3] https://drafts.csswg.org/css2/#atomic-inline-level-boxes
831 //
832 // If an inline box is "bleeding", ShrinkInlineBoxToLineBox shrinks its
833 // rect to the size of of its "line box" [2]. We need to do so because
834 // "bleeding" can make links intersect vertically. We need to avoid that
835 // overlap because it could make links on the same line (to the left or right)
836 // unreachable as SpatNav's distance formula favors intersecting rects (on the
837 // line below or above).
ShrinkInlineBoxToLineBox(const LayoutObject & layout_object,PhysicalRect node_rect,int line_boxes)838 PhysicalRect ShrinkInlineBoxToLineBox(const LayoutObject& layout_object,
839 PhysicalRect node_rect,
840 int line_boxes) {
841 if (!layout_object.IsInline() || layout_object.IsLayoutReplaced() ||
842 layout_object.IsButtonIncludingNG())
843 return node_rect;
844
845 // If actual line-height is bigger than the inline box, we shouldn't change
846 // anything. This is, for example, needed to not break
847 // snav-stay-in-overflow-div.html where the link's inline box doesn't fill
848 // the entire line box vertically.
849 LayoutUnit line_height = layout_object.StyleRef().ComputedLineHeightAsFixed();
850 LayoutUnit current_height = GetLogicalHeight(node_rect, layout_object);
851 if (line_height >= current_height)
852 return node_rect;
853
854 // Handle focusables like <a><img><a> (a LayoutInline that carries atomic
855 // inline boxes [3]). Despite a small line-height on the <a>, <a>'s line box
856 // will still fit the <img>.
857 line_height = std::max(TallestInlineAtomicChild(layout_object), line_height);
858 if (line_height >= current_height)
859 return node_rect;
860
861 // Cap the box at its line height to avoid overlapping inline links.
862 // Links can overlap vertically when CSS line-height < font-size, see
863 // snav-line-height_less_font-size.html.
864 line_boxes = line_boxes == -1 ? LineBoxes(layout_object) : line_boxes;
865 line_height = line_height * line_boxes;
866 if (line_height >= current_height)
867 return node_rect;
868 SetLogicalHeight(node_rect, layout_object, line_height);
869 return node_rect;
870 }
871
872 // TODO(crbug.com/1131419): Add tests and support for other writing-modes.
SearchOriginFragment(const PhysicalRect & visible_part,const LayoutObject & fragmented,const SpatialNavigationDirection direction)873 PhysicalRect SearchOriginFragment(const PhysicalRect& visible_part,
874 const LayoutObject& fragmented,
875 const SpatialNavigationDirection direction) {
876 // For accuracy, use the first visible fragment (not the fragmented element's
877 // entire bounding rect which is a union of all fragments) as search origin.
878 Vector<FloatQuad> fragments;
879 fragmented.AbsoluteQuads(
880 fragments, kTraverseDocumentBoundaries | kApplyRemoteMainFrameTransform);
881 switch (direction) {
882 case SpatialNavigationDirection::kLeft:
883 case SpatialNavigationDirection::kDown:
884 // Search from the topmost fragment.
885 return FirstVisibleFragment(visible_part, fragments.begin(),
886 fragments.end());
887 case SpatialNavigationDirection::kRight:
888 case SpatialNavigationDirection::kUp:
889 // Search from the bottommost fragment.
890 return FirstVisibleFragment(visible_part, fragments.rbegin(),
891 fragments.rend());
892 case SpatialNavigationDirection::kNone:
893 break;
894 // Nothing to do.
895 }
896 return visible_part;
897 }
898
899 // Spatnav uses this rectangle to measure distances to focus candidates.
900 // The search origin is either activeElement F itself, if it's being at least
901 // partially visible, or else, its first [partially] visible scroller. If both
902 // F and its enclosing scroller are completely off-screen, we recurse to the
903 // scroller’s scroller ... all the way up until the root frame's document.
904 // The root frame's document is a good base case because it's, per definition,
905 // a visible scrollable area.
SearchOrigin(const PhysicalRect & viewport_rect_of_root_frame,Node * focus_node,const SpatialNavigationDirection direction)906 PhysicalRect SearchOrigin(const PhysicalRect& viewport_rect_of_root_frame,
907 Node* focus_node,
908 const SpatialNavigationDirection direction) {
909 if (!focus_node) {
910 // Search from one of the visual viewport's edges towards the navigated
911 // direction. For example, UP makes spatnav search upwards, starting at the
912 // visual viewport's bottom.
913 return OppositeEdge(direction, viewport_rect_of_root_frame);
914 }
915
916 auto* area_element = DynamicTo<HTMLAreaElement>(focus_node);
917 if (area_element)
918 focus_node = area_element->ImageElement();
919
920 if (!IsOffscreen(focus_node)) {
921 if (area_element)
922 return StartEdgeForAreaElement(*area_element, direction);
923
924 PhysicalRect box_in_root_frame = NodeRectInRootFrame(focus_node);
925 PhysicalRect visible_part =
926 Intersection(box_in_root_frame, viewport_rect_of_root_frame);
927
928 const LayoutObject* const layout_object = focus_node->GetLayoutObject();
929 if (IsFragmentedInline(*layout_object)) {
930 visible_part =
931 SearchOriginFragment(visible_part, *layout_object, direction);
932 }
933
934 // Remove any overlap with line boxes *below* the search origin.
935 // The search origin is always only one line (because if |focus_node| is
936 // line broken, SearchOriginFragment picks the first or last line's box).
937 visible_part = ShrinkInlineBoxToLineBox(*layout_object, visible_part, 1);
938
939 return visible_part;
940 }
941
942 Node* container = ScrollableAreaOrDocumentOf(focus_node);
943 while (container) {
944 if (!IsOffscreen(container)) {
945 // The first scroller that encloses focus and is [partially] visible.
946 PhysicalRect box_in_root_frame = NodeRectInRootFrame(container);
947 return OppositeEdge(direction, Intersection(box_in_root_frame,
948 viewport_rect_of_root_frame));
949 }
950 container = ScrollableAreaOrDocumentOf(container);
951 }
952 return OppositeEdge(direction, viewport_rect_of_root_frame);
953 }
954
955 } // namespace blink
956