blob: bfc71f874126777cb6d34609a427a99cdec4ed5f [file] [log] [blame]
/*
* Copyright (C) 2009 Nokia Corporation and/or its subsidiary(-ies)
* Copyright (C) 2009 Antonio Gomes <tonikitoo@webkit.org>
*
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
* OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "core/page/SpatialNavigation.h"
#include "core/HTMLNames.h"
#include "core/dom/NodeTraversal.h"
#include "core/frame/FrameView.h"
#include "core/frame/LocalFrame.h"
#include "core/frame/Settings.h"
#include "core/html/HTMLAreaElement.h"
#include "core/html/HTMLFrameOwnerElement.h"
#include "core/html/HTMLImageElement.h"
#include "core/layout/LayoutBox.h"
#include "core/page/FrameTree.h"
#include "core/page/Page.h"
#include "platform/geometry/IntRect.h"
namespace blink {
using namespace HTMLNames;
static void deflateIfOverlapped(LayoutRect&, LayoutRect&);
static LayoutRect rectToAbsoluteCoordinates(LocalFrame* initialFrame,
const LayoutRect&);
static bool isScrollableNode(const Node*);
FocusCandidate::FocusCandidate(Node* node, WebFocusType type)
: visibleNode(nullptr),
focusableNode(nullptr),
enclosingScrollableBox(nullptr),
distance(maxDistance()),
isOffscreen(true),
isOffscreenAfterScrolling(true) {
ASSERT(node);
ASSERT(node->isElementNode());
if (isHTMLAreaElement(*node)) {
HTMLAreaElement& area = toHTMLAreaElement(*node);
HTMLImageElement* image = area.imageElement();
if (!image || !image->layoutObject())
return;
visibleNode = image;
rect = virtualRectForAreaElementAndDirection(area, type);
} else {
if (!node->layoutObject())
return;
visibleNode = node;
rect = nodeRectInAbsoluteCoordinates(node, true /* ignore border */);
}
focusableNode = node;
isOffscreen = hasOffscreenRect(visibleNode);
isOffscreenAfterScrolling = hasOffscreenRect(visibleNode, type);
}
bool isSpatialNavigationEnabled(const LocalFrame* frame) {
return (frame && frame->settings() &&
frame->settings()->spatialNavigationEnabled());
}
bool spatialNavigationIgnoresEventHandlers(const LocalFrame* frame) {
return (frame && frame->settings() &&
frame->settings()->deviceSupportsTouch());
}
static bool rectsIntersectOnOrthogonalAxis(WebFocusType type,
const LayoutRect& a,
const LayoutRect& b) {
switch (type) {
case WebFocusTypeLeft:
case WebFocusTypeRight:
return a.maxY() > b.y() && a.y() < b.maxY();
case WebFocusTypeUp:
case WebFocusTypeDown:
return a.maxX() > b.x() && a.x() < b.maxX();
default:
ASSERT_NOT_REACHED();
return false;
}
}
// Return true if rect |a| is below |b|. False otherwise.
// For overlapping rects, |a| is considered to be below |b|
// if both edges of |a| are below the respective ones of |b|
static inline bool below(const LayoutRect& a, const LayoutRect& b) {
return a.y() >= b.maxY() || (a.y() >= b.y() && a.maxY() > b.maxY() &&
a.x() < b.maxX() && a.maxX() > b.x());
}
// Return true if rect |a| is on the right of |b|. False otherwise.
// For overlapping rects, |a| is considered to be on the right of |b|
// if both edges of |a| are on the right of the respective ones of |b|
static inline bool rightOf(const LayoutRect& a, const LayoutRect& b) {
return a.x() >= b.maxX() || (a.x() >= b.x() && a.maxX() > b.maxX() &&
a.y() < b.maxY() && a.maxY() > b.y());
}
static bool isRectInDirection(WebFocusType type,
const LayoutRect& curRect,
const LayoutRect& targetRect) {
switch (type) {
case WebFocusTypeLeft:
return rightOf(curRect, targetRect);
case WebFocusTypeRight:
return rightOf(targetRect, curRect);
case WebFocusTypeUp:
return below(curRect, targetRect);
case WebFocusTypeDown:
return below(targetRect, curRect);
default:
ASSERT_NOT_REACHED();
return false;
}
}
// Checks if |node| is offscreen the visible area (viewport) of its container
// document. In case it is, one can scroll in direction or take any different
// desired action later on.
bool hasOffscreenRect(Node* node, WebFocusType type) {
// Get the FrameView in which |node| is (which means the current viewport if
// |node| is not in an inner document), so we can check if its content rect is
// visible before we actually move the focus to it.
FrameView* frameView = node->document().view();
if (!frameView)
return true;
ASSERT(!frameView->needsLayout());
LayoutRect containerViewportRect(frameView->visibleContentRect());
// We want to select a node if it is currently off screen, but will be
// exposed after we scroll. Adjust the viewport to post-scrolling position.
// If the container has overflow:hidden, we cannot scroll, so we do not pass
// direction and we do not adjust for scrolling.
int pixelsPerLineStep =
ScrollableArea::pixelsPerLineStep(frameView->getHostWindow());
switch (type) {
case WebFocusTypeLeft:
containerViewportRect.setX(containerViewportRect.x() - pixelsPerLineStep);
containerViewportRect.setWidth(containerViewportRect.width() +
pixelsPerLineStep);
break;
case WebFocusTypeRight:
containerViewportRect.setWidth(containerViewportRect.width() +
pixelsPerLineStep);
break;
case WebFocusTypeUp:
containerViewportRect.setY(containerViewportRect.y() - pixelsPerLineStep);
containerViewportRect.setHeight(containerViewportRect.height() +
pixelsPerLineStep);
break;
case WebFocusTypeDown:
containerViewportRect.setHeight(containerViewportRect.height() +
pixelsPerLineStep);
break;
default:
break;
}
LayoutObject* layoutObject = node->layoutObject();
if (!layoutObject)
return true;
LayoutRect rect(layoutObject->absoluteVisualRect());
if (rect.isEmpty())
return true;
return !containerViewportRect.intersects(rect);
}
bool scrollInDirection(LocalFrame* frame, WebFocusType type) {
ASSERT(frame);
if (frame && canScrollInDirection(frame->document(), type)) {
int dx = 0;
int dy = 0;
int pixelsPerLineStep =
ScrollableArea::pixelsPerLineStep(frame->view()->getHostWindow());
switch (type) {
case WebFocusTypeLeft:
dx = -pixelsPerLineStep;
break;
case WebFocusTypeRight:
dx = pixelsPerLineStep;
break;
case WebFocusTypeUp:
dy = -pixelsPerLineStep;
break;
case WebFocusTypeDown:
dy = pixelsPerLineStep;
break;
default:
ASSERT_NOT_REACHED();
return false;
}
frame->view()->scrollBy(ScrollOffset(dx, dy), UserScroll);
return true;
}
return false;
}
bool scrollInDirection(Node* container, WebFocusType type) {
ASSERT(container);
if (container->isDocumentNode())
return scrollInDirection(toDocument(container)->frame(), type);
if (!container->layoutBox())
return false;
if (canScrollInDirection(container, type)) {
int dx = 0;
int dy = 0;
// TODO(leviw): Why are these values truncated (toInt) instead of rounding?
FrameView* frameView = container->document().view();
int pixelsPerLineStep = ScrollableArea::pixelsPerLineStep(
frameView ? frameView->getHostWindow() : nullptr);
switch (type) {
case WebFocusTypeLeft:
dx = -pixelsPerLineStep;
break;
case WebFocusTypeRight:
ASSERT(container->layoutBox()->scrollWidth() >
(container->layoutBox()->scrollLeft() +
container->layoutBox()->clientWidth()));
dx = pixelsPerLineStep;
break;
case WebFocusTypeUp:
dy = -pixelsPerLineStep;
break;
case WebFocusTypeDown:
ASSERT(container->layoutBox()->scrollHeight() -
(container->layoutBox()->scrollTop() +
container->layoutBox()->clientHeight()));
dy = pixelsPerLineStep;
break;
default:
ASSERT_NOT_REACHED();
return false;
}
container->layoutBox()->scrollByRecursively(ScrollOffset(dx, dy));
return true;
}
return false;
}
static void deflateIfOverlapped(LayoutRect& a, LayoutRect& b) {
if (!a.intersects(b) || a.contains(b) || b.contains(a))
return;
LayoutUnit deflateFactor = LayoutUnit(-fudgeFactor());
// Avoid negative width or height values.
if ((a.width() + 2 * deflateFactor > 0) &&
(a.height() + 2 * deflateFactor > 0))
a.inflate(deflateFactor);
if ((b.width() + 2 * deflateFactor > 0) &&
(b.height() + 2 * deflateFactor > 0))
b.inflate(deflateFactor);
}
bool isScrollableNode(const Node* node) {
ASSERT(!node->isDocumentNode());
if (!node)
return false;
if (LayoutObject* layoutObject = node->layoutObject())
return layoutObject->isBox() &&
toLayoutBox(layoutObject)->canBeScrolledAndHasScrollableArea() &&
node->hasChildren();
return false;
}
Node* scrollableEnclosingBoxOrParentFrameForNodeInDirection(WebFocusType type,
Node* node) {
ASSERT(node);
Node* parent = node;
do {
// FIXME: Spatial navigation is broken for OOPI.
if (parent->isDocumentNode())
parent = toDocument(parent)->frame()->deprecatedLocalOwner();
else
parent = parent->parentOrShadowHostNode();
} while (parent && !canScrollInDirection(parent, type) &&
!parent->isDocumentNode());
return parent;
}
bool canScrollInDirection(const Node* container, WebFocusType type) {
ASSERT(container);
if (container->isDocumentNode())
return canScrollInDirection(toDocument(container)->frame(), type);
if (!isScrollableNode(container))
return false;
switch (type) {
case WebFocusTypeLeft:
return (container->layoutObject()->style()->overflowX() !=
EOverflow::Hidden &&
container->layoutBox()->scrollLeft() > 0);
case WebFocusTypeUp:
return (container->layoutObject()->style()->overflowY() !=
EOverflow::Hidden &&
container->layoutBox()->scrollTop() > 0);
case WebFocusTypeRight:
return (container->layoutObject()->style()->overflowX() !=
EOverflow::Hidden &&
container->layoutBox()->scrollLeft() +
container->layoutBox()->clientWidth() <
container->layoutBox()->scrollWidth());
case WebFocusTypeDown:
return (container->layoutObject()->style()->overflowY() !=
EOverflow::Hidden &&
container->layoutBox()->scrollTop() +
container->layoutBox()->clientHeight() <
container->layoutBox()->scrollHeight());
default:
ASSERT_NOT_REACHED();
return false;
}
}
bool canScrollInDirection(const LocalFrame* frame, WebFocusType type) {
if (!frame->view())
return false;
ScrollbarMode verticalMode;
ScrollbarMode horizontalMode;
frame->view()->calculateScrollbarModes(horizontalMode, verticalMode);
if ((type == WebFocusTypeLeft || type == WebFocusTypeRight) &&
ScrollbarAlwaysOff == horizontalMode)
return false;
if ((type == WebFocusTypeUp || type == WebFocusTypeDown) &&
ScrollbarAlwaysOff == verticalMode)
return false;
LayoutSize size(frame->view()->contentsSize());
LayoutSize offset(frame->view()->scrollOffsetInt());
LayoutRect rect(frame->view()->visibleContentRect(IncludeScrollbars));
switch (type) {
case WebFocusTypeLeft:
return offset.width() > 0;
case WebFocusTypeUp:
return offset.height() > 0;
case WebFocusTypeRight:
return rect.width() + offset.width() < size.width();
case WebFocusTypeDown:
return rect.height() + offset.height() < size.height();
default:
ASSERT_NOT_REACHED();
return false;
}
}
static LayoutRect rectToAbsoluteCoordinates(LocalFrame* initialFrame,
const LayoutRect& initialRect) {
LayoutRect rect = initialRect;
for (Frame* frame = initialFrame; frame; frame = frame->tree().parent()) {
if (!frame->isLocalFrame())
continue;
// FIXME: Spatial navigation is broken for OOPI.
Element* element = frame->deprecatedLocalOwner();
if (element) {
do {
rect.move(element->offsetLeft(), element->offsetTop());
LayoutObject* layoutObject = element->layoutObject();
element = layoutObject ? layoutObject->offsetParent() : nullptr;
} while (element);
rect.move((-toLocalFrame(frame)->view()->scrollOffsetInt()));
}
}
return rect;
}
LayoutRect nodeRectInAbsoluteCoordinates(Node* node, bool ignoreBorder) {
ASSERT(node && node->layoutObject() &&
!node->document().view()->needsLayout());
if (node->isDocumentNode())
return frameRectInAbsoluteCoordinates(toDocument(node)->frame());
LayoutRect rect =
rectToAbsoluteCoordinates(node->document().frame(), node->boundingBox());
// For authors that use border instead of outline in their CSS, we compensate
// by ignoring the border when calculating the rect of the focused element.
if (ignoreBorder) {
rect.move(node->layoutObject()->style()->borderLeftWidth(),
node->layoutObject()->style()->borderTopWidth());
rect.setWidth(rect.width() -
node->layoutObject()->style()->borderLeftWidth() -
node->layoutObject()->style()->borderRightWidth());
rect.setHeight(rect.height() -
node->layoutObject()->style()->borderTopWidth() -
node->layoutObject()->style()->borderBottomWidth());
}
return rect;
}
LayoutRect frameRectInAbsoluteCoordinates(LocalFrame* frame) {
return rectToAbsoluteCoordinates(
frame, LayoutRect(frame->view()->visibleContentRect()));
}
// This method calculates the exitPoint from the startingRect and the entryPoint
// into the candidate rect. The line between those 2 points is the closest
// distance between the 2 rects. Takes care of overlapping rects, defining
// points so that the distance between them is zero where necessary
void entryAndExitPointsForDirection(WebFocusType type,
const LayoutRect& startingRect,
const LayoutRect& potentialRect,
LayoutPoint& exitPoint,
LayoutPoint& entryPoint) {
switch (type) {
case WebFocusTypeLeft:
exitPoint.setX(startingRect.x());
if (potentialRect.maxX() < startingRect.x())
entryPoint.setX(potentialRect.maxX());
else
entryPoint.setX(startingRect.x());
break;
case WebFocusTypeUp:
exitPoint.setY(startingRect.y());
if (potentialRect.maxY() < startingRect.y())
entryPoint.setY(potentialRect.maxY());
else
entryPoint.setY(startingRect.y());
break;
case WebFocusTypeRight:
exitPoint.setX(startingRect.maxX());
if (potentialRect.x() > startingRect.maxX())
entryPoint.setX(potentialRect.x());
else
entryPoint.setX(startingRect.maxX());
break;
case WebFocusTypeDown:
exitPoint.setY(startingRect.maxY());
if (potentialRect.y() > startingRect.maxY())
entryPoint.setY(potentialRect.y());
else
entryPoint.setY(startingRect.maxY());
break;
default:
ASSERT_NOT_REACHED();
}
switch (type) {
case WebFocusTypeLeft:
case WebFocusTypeRight:
if (below(startingRect, potentialRect)) {
exitPoint.setY(startingRect.y());
if (potentialRect.maxY() < startingRect.y())
entryPoint.setY(potentialRect.maxY());
else
entryPoint.setY(startingRect.y());
} else if (below(potentialRect, startingRect)) {
exitPoint.setY(startingRect.maxY());
if (potentialRect.y() > startingRect.maxY())
entryPoint.setY(potentialRect.y());
else
entryPoint.setY(startingRect.maxY());
} else {
exitPoint.setY(max(startingRect.y(), potentialRect.y()));
entryPoint.setY(exitPoint.y());
}
break;
case WebFocusTypeUp:
case WebFocusTypeDown:
if (rightOf(startingRect, potentialRect)) {
exitPoint.setX(startingRect.x());
if (potentialRect.maxX() < startingRect.x())
entryPoint.setX(potentialRect.maxX());
else
entryPoint.setX(startingRect.x());
} else if (rightOf(potentialRect, startingRect)) {
exitPoint.setX(startingRect.maxX());
if (potentialRect.x() > startingRect.maxX())
entryPoint.setX(potentialRect.x());
else
entryPoint.setX(startingRect.maxX());
} else {
exitPoint.setX(max(startingRect.x(), potentialRect.x()));
entryPoint.setX(exitPoint.x());
}
break;
default:
ASSERT_NOT_REACHED();
}
}
bool areElementsOnSameLine(const FocusCandidate& firstCandidate,
const FocusCandidate& secondCandidate) {
if (firstCandidate.isNull() || secondCandidate.isNull())
return false;
if (!firstCandidate.visibleNode->layoutObject() ||
!secondCandidate.visibleNode->layoutObject())
return false;
if (!firstCandidate.rect.intersects(secondCandidate.rect))
return false;
if (isHTMLAreaElement(*firstCandidate.focusableNode) ||
isHTMLAreaElement(*secondCandidate.focusableNode))
return false;
if (!firstCandidate.visibleNode->layoutObject()->isLayoutInline() ||
!secondCandidate.visibleNode->layoutObject()->isLayoutInline())
return false;
if (firstCandidate.visibleNode->layoutObject()->containingBlock() !=
secondCandidate.visibleNode->layoutObject()->containingBlock())
return false;
return true;
}
void distanceDataForNode(WebFocusType type,
const FocusCandidate& current,
FocusCandidate& candidate) {
if (!isRectInDirection(type, current.rect, candidate.rect))
return;
if (areElementsOnSameLine(current, candidate)) {
if ((type == WebFocusTypeUp && current.rect.y() > candidate.rect.y()) ||
(type == WebFocusTypeDown && candidate.rect.y() > current.rect.y())) {
candidate.distance = 0;
return;
}
}
LayoutRect nodeRect = candidate.rect;
LayoutRect currentRect = current.rect;
deflateIfOverlapped(currentRect, nodeRect);
LayoutPoint exitPoint;
LayoutPoint entryPoint;
entryAndExitPointsForDirection(type, currentRect, nodeRect, exitPoint,
entryPoint);
LayoutUnit xAxis = (exitPoint.x() - entryPoint.x()).abs();
LayoutUnit yAxis = (exitPoint.y() - entryPoint.y()).abs();
LayoutUnit navigationAxisDistance;
LayoutUnit weightedOrthogonalAxisDistance;
// Bias and weights are put to the orthogonal axis distance calculation
// so aligned candidates would have advantage over partially-aligned ones
// and then over not-aligned candidates. The bias is given to not-aligned
// candidates with respect to size of the current rect. The weight for
// left/right direction is given a higher value to allow navigation on
// common horizonally-aligned elements. The hardcoded values are based on
// tests and experiments.
const int orthogonalWeightForLeftRight = 30;
const int orthogonalWeightForUpDown = 2;
int orthogonalBias = 0;
switch (type) {
case WebFocusTypeLeft:
case WebFocusTypeRight:
navigationAxisDistance = xAxis;
if (!rectsIntersectOnOrthogonalAxis(type, currentRect, nodeRect))
orthogonalBias = (currentRect.height() / 2).toInt();
weightedOrthogonalAxisDistance =
(yAxis + orthogonalBias) * orthogonalWeightForLeftRight;
break;
case WebFocusTypeUp:
case WebFocusTypeDown:
navigationAxisDistance = yAxis;
if (!rectsIntersectOnOrthogonalAxis(type, currentRect, nodeRect))
orthogonalBias = (currentRect.width() / 2).toInt();
weightedOrthogonalAxisDistance =
(xAxis + orthogonalBias) * orthogonalWeightForUpDown;
break;
default:
ASSERT_NOT_REACHED();
return;
}
double euclidianDistancePow2 = (xAxis * xAxis + yAxis * yAxis).toDouble();
LayoutRect intersectionRect = intersection(currentRect, nodeRect);
double overlap =
(intersectionRect.width() * intersectionRect.height()).toDouble();
// Distance calculation is based on http://www.w3.org/TR/WICD/#focus-handling
candidate.distance = sqrt(euclidianDistancePow2) + navigationAxisDistance +
weightedOrthogonalAxisDistance - sqrt(overlap);
}
bool canBeScrolledIntoView(WebFocusType type, const FocusCandidate& candidate) {
ASSERT(candidate.visibleNode && candidate.isOffscreen);
LayoutRect candidateRect = candidate.rect;
for (Node& parentNode : NodeTraversal::ancestorsOf(*candidate.visibleNode)) {
LayoutRect parentRect = nodeRectInAbsoluteCoordinates(&parentNode);
if (!candidateRect.intersects(parentRect)) {
if (((type == WebFocusTypeLeft || type == WebFocusTypeRight) &&
parentNode.layoutObject()->style()->overflowX() ==
EOverflow::Hidden) ||
((type == WebFocusTypeUp || type == WebFocusTypeDown) &&
parentNode.layoutObject()->style()->overflowY() ==
EOverflow::Hidden))
return false;
}
if (parentNode == candidate.enclosingScrollableBox)
return canScrollInDirection(&parentNode, type);
}
return true;
}
// The starting rect is the rect of the focused node, in document coordinates.
// Compose a virtual starting rect if there is no focused node or if it is off
// screen. The virtual rect is the edge of the container or frame. We select
// which edge depending on the direction of the navigation.
LayoutRect virtualRectForDirection(WebFocusType type,
const LayoutRect& startingRect,
LayoutUnit width) {
LayoutRect virtualStartingRect = startingRect;
switch (type) {
case WebFocusTypeLeft:
virtualStartingRect.setX(virtualStartingRect.maxX() - width);
virtualStartingRect.setWidth(width);
break;
case WebFocusTypeUp:
virtualStartingRect.setY(virtualStartingRect.maxY() - width);
virtualStartingRect.setHeight(width);
break;
case WebFocusTypeRight:
virtualStartingRect.setWidth(width);
break;
case WebFocusTypeDown:
virtualStartingRect.setHeight(width);
break;
default:
ASSERT_NOT_REACHED();
}
return virtualStartingRect;
}
LayoutRect virtualRectForAreaElementAndDirection(HTMLAreaElement& area,
WebFocusType type) {
ASSERT(area.imageElement());
// Area elements tend to overlap more than other focusable elements. We
// flatten the rect of the area elements to minimize the effect of overlapping
// areas.
LayoutRect rect = virtualRectForDirection(
type, rectToAbsoluteCoordinates(
area.document().frame(),
area.computeAbsoluteRect(area.imageElement()->layoutObject())),
LayoutUnit(1));
return rect;
}
HTMLFrameOwnerElement* frameOwnerElement(FocusCandidate& candidate) {
return candidate.isFrameOwnerElement()
? toHTMLFrameOwnerElement(candidate.visibleNode)
: nullptr;
};
} // namespace blink