blob: 40aa52baa6b737d97d5cd12620f6166da61caf91 [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 "config.h"
#include "SpatialNavigation.h"
#include "Frame.h"
#include "FrameTree.h"
#include "FrameView.h"
#include "HTMLFrameOwnerElement.h"
#include "IntRect.h"
#include "Node.h"
#include "RenderLayer.h"
#include "Page.h"
namespace WebCore {
static long long spatialDistance(FocusDirection, const IntRect&, const IntRect&);
static IntRect renderRectRelativeToRootDocument(RenderObject*);
static RectsAlignment alignmentForRects(FocusDirection, const IntRect&, const IntRect&);
static bool areRectsFullyAligned(FocusDirection, const IntRect&, const IntRect&);
static bool areRectsPartiallyAligned(FocusDirection, const IntRect&, const IntRect&);
static bool isRectInDirection(FocusDirection, const IntRect&, const IntRect&);
static void deflateIfOverlapped(IntRect&, IntRect&);
static bool checkNegativeCoordsForNode(Node*, const IntRect&);
void distanceDataForNode(FocusDirection direction, Node* start, FocusCandidate& candidate)
{
RenderObject* startRender = start->renderer();
if (!startRender) {
candidate.distance = maxDistance();
return;
}
RenderObject* destRender = candidate.node->renderer();
if (!destRender) {
candidate.distance = maxDistance();
return;
}
IntRect curRect = renderRectRelativeToRootDocument(startRender);
IntRect targetRect = renderRectRelativeToRootDocument(destRender);
// The bounding rectangle of two consecutive nodes can overlap. In such cases,
// deflate both.
deflateIfOverlapped(curRect, targetRect);
// If empty rects or negative width or height, bail out.
if (curRect.isEmpty() || targetRect.isEmpty()
|| targetRect.width() <= 0 || targetRect.height() <= 0) {
candidate.distance = maxDistance();
return;
}
// Negative coordinates can be used if node is scrolled up offscreen.
if (!checkNegativeCoordsForNode(start, curRect)) {
candidate.distance = maxDistance();
return;
}
if (!checkNegativeCoordsForNode(candidate.node, targetRect)) {
candidate.distance = maxDistance();
return;
}
if (!isRectInDirection(direction, curRect, targetRect)) {
candidate.distance = maxDistance();
return;
}
// The distance between two nodes is not to be considered alone when evaluating/looking
// for the best focus candidate node. Alignment of rects can be also a good point to be
// considered in order to make the algorithm to behavior in a more intuitive way.
candidate.alignment = alignmentForRects(direction, curRect, targetRect);
candidate.distance = spatialDistance(direction, curRect, targetRect);
}
// FIXME: This function does not behave correctly with transformed frames.
static IntRect renderRectRelativeToRootDocument(RenderObject* render)
{
ASSERT(render && render->node());
IntRect rect = render->node()->getRect();
// In cases when the |render|'s associated node is in a scrollable inner
// document, we only consider its scrollOffset if it is not offscreen.
Node* node = render->node();
Document* mainDocument = node->document()->page()->mainFrame()->document();
bool considerScrollOffset = !(hasOffscreenRect(node) && node->document() != mainDocument);
if (considerScrollOffset) {
if (FrameView* frameView = render->node()->document()->view())
rect.move(-frameView->scrollOffset());
}
// Handle nested frames.
for (Frame* frame = render->document()->frame(); frame; frame = frame->tree()->parent()) {
if (Element* element = static_cast<Element*>(frame->ownerElement())) {
do {
rect.move(element->offsetLeft(), element->offsetTop());
} while ((element = element->offsetParent()));
}
}
return rect;
}
static RectsAlignment alignmentForRects(FocusDirection direction, const IntRect& curRect, const IntRect& targetRect)
{
if (areRectsFullyAligned(direction, curRect, targetRect))
return Full;
if (areRectsPartiallyAligned(direction, curRect, targetRect))
return Partial;
return None;
}
static inline bool isHorizontalMove(FocusDirection direction)
{
return direction == FocusDirectionLeft || direction == FocusDirectionRight;
}
static inline int start(FocusDirection direction, const IntRect& rect)
{
return isHorizontalMove(direction) ? rect.y() : rect.x();
}
static inline int middle(FocusDirection direction, const IntRect& rect)
{
IntPoint center(rect.center());
return isHorizontalMove(direction) ? center.y(): center.x();
}
static inline int end(FocusDirection direction, const IntRect& rect)
{
return isHorizontalMove(direction) ? rect.bottom() : rect.right();
}
// This method checks if rects |a| and |b| are fully aligned either vertically or
// horizontally. In general, rects whose central point falls between the top or
// bottom of each other are considered fully aligned.
// Rects that match this criteria are preferable target nodes in move focus changing
// operations.
// * a = Current focused node's rect.
// * b = Focus candidate node's rect.
static bool areRectsFullyAligned(FocusDirection direction, const IntRect& a, const IntRect& b)
{
int aStart, bStart, aEnd, bEnd;
switch (direction) {
case FocusDirectionLeft:
aStart = a.x();
bEnd = b.right();
break;
case FocusDirectionRight:
aStart = b.x();
bEnd = a.right();
break;
case FocusDirectionUp:
aStart = a.y();
bEnd = b.y();
break;
case FocusDirectionDown:
aStart = b.y();
bEnd = a.y();
break;
default:
ASSERT_NOT_REACHED();
return false;
}
if (aStart < bEnd)
return false;
aStart = start(direction, a);
bStart = start(direction, b);
int aMiddle = middle(direction, a);
int bMiddle = middle(direction, b);
aEnd = end(direction, a);
bEnd = end(direction, b);
// Picture of the totally aligned logic:
//
// Horizontal Vertical Horizontal Vertical
// **************************** *****************************
// * _ * _ _ _ _ * * _ * _ _ *
// * |_| _ * |_|_|_|_| * * _ |_| * |_|_| *
// * |_|....|_| * . * * |_|....|_| * . *
// * |_| |_| (1) . * * |_| |_| (2) . *
// * |_| * _._ * * |_| * _ _._ _ *
// * * |_|_| * * * |_|_|_|_| *
// * * * * * *
// **************************** *****************************
// Horizontal Vertical Horizontal Vertical
// **************************** *****************************
// * _......_ * _ _ _ _ * * _ * _ _ _ _ *
// * |_| |_| * |_|_|_|_| * * |_| _ * |_|_|_|_| *
// * |_| |_| * . * * |_| |_| * . *
// * |_| (3) . * * |_|....|_| (4) . *
// * * ._ _ * * * _ _. *
// * * |_|_| * * * |_|_| *
// * * * * * *
// **************************** *****************************
return ((bMiddle >= aStart && bMiddle <= aEnd) // (1)
|| (aMiddle >= bStart && aMiddle <= bEnd) // (2)
|| (bStart == aStart) // (3)
|| (bEnd == aEnd)); // (4)
}
// This method checks if |start| and |dest| have a partial intersection, either
// horizontally or vertically.
// * a = Current focused node's rect.
// * b = Focus candidate node's rect.
static bool areRectsPartiallyAligned(FocusDirection direction, const IntRect& a, const IntRect& b)
{
int aStart = start(direction, a);
int bStart = start(direction, b);
int bMiddle = middle(direction, b);
int aEnd = end(direction, a);
int bEnd = end(direction, b);
// Picture of the partially aligned logic:
//
// Horizontal Vertical
// ********************************
// * _ * _ _ _ *
// * |_| * |_|_|_| *
// * |_|.... _ * . . *
// * |_| |_| * . . *
// * |_|....|_| * ._._ _ *
// * |_| * |_|_|_| *
// * |_| * *
// * * *
// ********************************
//
// ... and variants of the above cases.
return ((bStart >= aStart && bStart <= aEnd)
|| (bStart >= aStart && bStart <= aEnd)
|| (bEnd >= aStart && bEnd <= aEnd)
|| (bMiddle >= aStart && bMiddle <= aEnd)
|| (bEnd >= aStart && bEnd <= aEnd));
}
// Return true if rect |a| is below |b|. False otherwise.
static inline bool below(const IntRect& a, const IntRect& b)
{
return a.y() > b.bottom();
}
// Return true if rect |a| is on the right of |b|. False otherwise.
static inline bool rightOf(const IntRect& a, const IntRect& b)
{
return a.x() > b.right();
}
// * a = Current focused node's rect.
// * b = Focus candidate node's rect.
static long long spatialDistance(FocusDirection direction, const IntRect& a, const IntRect& b)
{
int x1 = 0, y1 = 0, x2 = 0, y2 = 0;
if (direction == FocusDirectionLeft) {
// #1 |--|
//
// #2 |--| |--|
//
// #3 |--|
x1 = a.x();
x2 = b.right();
if (below(a, b)) {
// #1 The a rect is below b.
y1 = a.y();
y2 = b.bottom();
} else if (below(b, a)) {
// #3 The b rect is below a.
y1 = a.bottom();
y2 = b.y();
} else {
// #2 Both b and a share some common y's.
y1 = 0;
y2 = 0;
}
} else if (direction == FocusDirectionRight) {
// |--| #1
//
// |--| |--| #2
//
// |--| #3
x1 = a.right();
x2 = b.x();
if (below(a, b)) {
// #1 The b rect is above a.
y1 = a.y();
y2 = b.bottom();
} else if (below(b, a)) {
// #3 The b rect is below a.
y1 = a.bottom();
y2 = b.y();
} else {
// #2 Both b and a share some common y's.
y1 = 0;
y2 = 0;
}
} else if (direction == FocusDirectionUp) {
//
// #1 #2 #3
//
// |--| |--| |--|
//
// |--|
y1 = a.y();
y2 = b.bottom();
if (rightOf(a, b)) {
// #1 The b rect is to the left of a.
x1 = a.x();
x2 = b.right();
} else if (rightOf(b, a)) {
// #3 The b rect is to the right of a.
x1 = a.right();
x2 = b.x();
} else {
// #2 Both b and a share some common x's.
x1 = 0;
x2 = 0;
}
} else if (direction == FocusDirectionDown) {
// |--|
//
// |--| |--| |--|
//
// #1 #2 #3
y1 = a.bottom();
y2 = b.y();
if (rightOf(a, b)) {
// #1 The b rect is to the left of a.
x1 = a.x();
x2 = b.right();
} else if (rightOf(b, a)) {
// #3 The b rect is to the right of a
x1 = a.right();
x2 = b.x();
} else {
// #2 Both b and a share some common x's.
x1 = 0;
x2 = 0;
}
}
long long dx = x1 - x2;
long long dy = y1 - y2;
long long distance = (dx * dx) + (dy * dy);
if (distance < 0)
distance *= -1;
return distance;
}
static bool isRectInDirection(FocusDirection direction, const IntRect& curRect, const IntRect& targetRect)
{
IntPoint center(targetRect.center());
int targetMiddle = isHorizontalMove(direction) ? center.x() : center.y();
switch (direction) {
case FocusDirectionLeft:
return targetMiddle < curRect.x();
case FocusDirectionRight:
return targetMiddle > curRect.right();
case FocusDirectionUp:
return targetMiddle < curRect.y();
case FocusDirectionDown:
return targetMiddle > curRect.bottom();
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)
{
// 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;
IntRect containerViewportRect = frameView->visibleContentRect();
RenderObject* render = node->renderer();
if (!render)
return true;
IntRect rect(render->absoluteClippedOverflowRect());
if (rect.isEmpty())
return true;
return !containerViewportRect.intersects(rect);
}
// In a bottom-up way, this method tries to scroll |frame| in a given direction
// |direction|, going up in the frame tree hierarchy in case it does not succeed.
bool scrollInDirection(Frame* frame, FocusDirection direction, const FocusCandidate& candidate)
{
if (!frame)
return false;
ScrollDirection scrollDirection;
switch (direction) {
case FocusDirectionLeft:
scrollDirection = ScrollLeft;
break;
case FocusDirectionRight:
scrollDirection = ScrollRight;
break;
case FocusDirectionUp:
scrollDirection = ScrollUp;
break;
case FocusDirectionDown:
scrollDirection = ScrollDown;
break;
default:
return false;
}
if (!candidate.isNull() && isScrollableContainerNode(candidate.enclosingScrollableBox))
return frame->eventHandler()->scrollRecursively(scrollDirection, ScrollByLine, candidate.enclosingScrollableBox);
return frame->eventHandler()->scrollRecursively(scrollDirection, ScrollByLine);
}
void scrollIntoView(Element* element)
{
// NOTE: Element's scrollIntoView method could had been used here, but
// it is preferable to inflate |element|'s bounding rect a bit before
// scrolling it for accurate reason.
// Element's scrollIntoView method does not provide this flexibility.
IntRect bounds = element->getRect();
bounds.inflate(fudgeFactor());
element->renderer()->enclosingLayer()->scrollRectToVisible(bounds);
}
bool isInRootDocument(Node* node)
{
if (!node)
return false;
Document* rootDocument = node->document()->page()->mainFrame()->document();
return node->document() == rootDocument;
}
static void deflateIfOverlapped(IntRect& a, IntRect& b)
{
if (!a.intersects(b) || a.contains(b) || b.contains(a))
return;
int deflateFactor = -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);
}
static bool checkNegativeCoordsForNode(Node* node, const IntRect& curRect)
{
ASSERT(node || node->renderer());
if (curRect.x() >= 0 && curRect.y() >= 0)
return true;
bool canBeScrolled = false;
RenderObject* renderer = node->renderer();
for (; renderer; renderer = renderer->parent()) {
if (renderer->isBox() && toRenderBox(renderer)->canBeScrolledAndHasScrollableArea()) {
canBeScrolled = true;
break;
}
}
return canBeScrolled;
}
bool isScrollableContainerNode(Node* node)
{
if (!node)
return false;
if (RenderObject* renderer = node->renderer()) {
return (renderer->isBox() && toRenderBox(renderer)->canBeScrolledAndHasScrollableArea()
&& node->hasChildNodes() && !node->isDocumentNode());
}
return false;
}
bool isNodeDeepDescendantOfDocument(Node* node, Document* baseDocument)
{
if (!node || !baseDocument)
return false;
bool descendant = baseDocument == node->document();
Element* currentElement = static_cast<Element*>(node);
while (!descendant) {
Element* documentOwner = currentElement->document()->ownerElement();
if (!documentOwner)
break;
descendant = documentOwner->document() == baseDocument;
currentElement = documentOwner;
}
return descendant;
}
} // namespace WebCore