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