blob: 34f2ab9fb0412c9ad1c1bcabd3ff532b2ce65215 [file] [log] [blame]
tonikitoo@webkit.org660f8ff2010-03-04 20:19:44 +00001/*
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 "config.h"
30#include "SpatialNavigation.h"
31
32#include "Frame.h"
33#include "FrameTree.h"
34#include "FrameView.h"
yael.aharon@nokia.comf1b5ab42010-12-07 19:04:10 +000035#include "HTMLAreaElement.h"
yael.aharon@nokia.comf1b5ab42010-12-07 19:04:10 +000036#include "HTMLImageElement.h"
37#include "HTMLMapElement.h"
tonikitoo@webkit.orga2bb1222010-12-09 18:53:47 +000038#include "HTMLNames.h"
tonikitoo@webkit.org660f8ff2010-03-04 20:19:44 +000039#include "IntRect.h"
40#include "Node.h"
41#include "Page.h"
yael.aharon@nokia.com4a4c87b2011-02-18 21:05:38 +000042#include "RenderInline.h"
chang.shu@nokia.com2883b772010-11-07 09:24:27 +000043#include "RenderLayer.h"
44#include "Settings.h"
tonikitoo@webkit.org660f8ff2010-03-04 20:19:44 +000045
46namespace WebCore {
47
eae@chromium.org4ecf4182011-08-18 00:49:01 +000048static RectsAlignment alignmentForRects(FocusDirection, const LayoutRect&, const LayoutRect&, const LayoutSize& viewSize);
49static bool areRectsFullyAligned(FocusDirection, const LayoutRect&, const LayoutRect&);
50static bool areRectsPartiallyAligned(FocusDirection, const LayoutRect&, const LayoutRect&);
51static bool areRectsMoreThanFullScreenApart(FocusDirection, const LayoutRect& curRect, const LayoutRect& targetRect, const LayoutSize& viewSize);
52static bool isRectInDirection(FocusDirection, const LayoutRect&, const LayoutRect&);
53static void deflateIfOverlapped(LayoutRect&, LayoutRect&);
54static LayoutRect rectToAbsoluteCoordinates(Frame* initialFrame, const LayoutRect&);
55static void entryAndExitPointsForDirection(FocusDirection, const LayoutRect& startingRect, const LayoutRect& potentialRect, LayoutPoint& exitPoint, LayoutPoint& entryPoint);
tonikitoo@webkit.org3d623722010-12-28 21:57:11 +000056static bool isScrollableNode(const Node*);
yael.aharon@nokia.com0a818c22010-11-22 13:49:09 +000057
tonikitoo@webkit.org237c1ea2010-12-13 05:52:40 +000058FocusCandidate::FocusCandidate(Node* node, FocusDirection direction)
yael.aharon@nokia.comf1b5ab42010-12-07 19:04:10 +000059 : visibleNode(0)
60 , focusableNode(0)
61 , enclosingScrollableBox(0)
62 , distance(maxDistance())
63 , parentDistance(maxDistance())
64 , alignment(None)
65 , parentAlignment(None)
66 , isOffscreen(true)
67 , isOffscreenAfterScrolling(true)
68{
tonikitoo@webkit.org237c1ea2010-12-13 05:52:40 +000069 ASSERT(node);
tonikitoo@webkit.org3d623722010-12-28 21:57:11 +000070 ASSERT(node->isElementNode());
71
tonikitoo@webkit.org237c1ea2010-12-13 05:52:40 +000072 if (node->hasTagName(HTMLNames::areaTag)) {
73 HTMLAreaElement* area = static_cast<HTMLAreaElement*>(node);
tonikitoo@webkit.orga2bb1222010-12-09 18:53:47 +000074 HTMLImageElement* image = area->imageElement();
75 if (!image || !image->renderer())
76 return;
yael.aharon@nokia.comf1b5ab42010-12-07 19:04:10 +000077
tonikitoo@webkit.orga2bb1222010-12-09 18:53:47 +000078 visibleNode = image;
tonikitoo@webkit.org3d623722010-12-28 21:57:11 +000079 rect = virtualRectForAreaElementAndDirection(area, direction);
tonikitoo@webkit.orga2bb1222010-12-09 18:53:47 +000080 } else {
tonikitoo@webkit.org237c1ea2010-12-13 05:52:40 +000081 if (!node->renderer())
tonikitoo@webkit.orga2bb1222010-12-09 18:53:47 +000082 return;
83
tonikitoo@webkit.org237c1ea2010-12-13 05:52:40 +000084 visibleNode = node;
85 rect = nodeRectInAbsoluteCoordinates(node, true /* ignore border */);
tonikitoo@webkit.orga2bb1222010-12-09 18:53:47 +000086 }
87
tonikitoo@webkit.org237c1ea2010-12-13 05:52:40 +000088 focusableNode = node;
tonikitoo@webkit.orga2bb1222010-12-09 18:53:47 +000089 isOffscreen = hasOffscreenRect(visibleNode);
90 isOffscreenAfterScrolling = hasOffscreenRect(visibleNode, direction);
yael.aharon@nokia.comf1b5ab42010-12-07 19:04:10 +000091}
92
chang.shu@nokia.com2883b772010-11-07 09:24:27 +000093bool isSpatialNavigationEnabled(const Frame* frame)
94{
95 return (frame && frame->settings() && frame->settings()->isSpatialNavigationEnabled());
96}
97
eae@chromium.org4ecf4182011-08-18 00:49:01 +000098static RectsAlignment alignmentForRects(FocusDirection direction, const LayoutRect& curRect, const LayoutRect& targetRect, const LayoutSize& viewSize)
tonikitoo@webkit.org660f8ff2010-03-04 20:19:44 +000099{
yael.aharon@nokia.com0a818c22010-11-22 13:49:09 +0000100 // If we found a node in full alignment, but it is too far away, ignore it.
101 if (areRectsMoreThanFullScreenApart(direction, curRect, targetRect, viewSize))
102 return None;
103
tonikitoo@webkit.org660f8ff2010-03-04 20:19:44 +0000104 if (areRectsFullyAligned(direction, curRect, targetRect))
105 return Full;
106
107 if (areRectsPartiallyAligned(direction, curRect, targetRect))
108 return Partial;
109
110 return None;
111}
112
113static inline bool isHorizontalMove(FocusDirection direction)
114{
115 return direction == FocusDirectionLeft || direction == FocusDirectionRight;
116}
117
eae@chromium.org4ecf4182011-08-18 00:49:01 +0000118static inline LayoutUnit start(FocusDirection direction, const LayoutRect& rect)
tonikitoo@webkit.org660f8ff2010-03-04 20:19:44 +0000119{
120 return isHorizontalMove(direction) ? rect.y() : rect.x();
121}
122
eae@chromium.org4ecf4182011-08-18 00:49:01 +0000123static inline LayoutUnit middle(FocusDirection direction, const LayoutRect& rect)
tonikitoo@webkit.org660f8ff2010-03-04 20:19:44 +0000124{
eae@chromium.org4ecf4182011-08-18 00:49:01 +0000125 LayoutPoint center(rect.center());
tonikitoo@webkit.org660f8ff2010-03-04 20:19:44 +0000126 return isHorizontalMove(direction) ? center.y(): center.x();
127}
128
eae@chromium.org4ecf4182011-08-18 00:49:01 +0000129static inline LayoutUnit end(FocusDirection direction, const LayoutRect& rect)
tonikitoo@webkit.org660f8ff2010-03-04 20:19:44 +0000130{
hyatt@apple.comaa0cba02011-02-01 21:39:47 +0000131 return isHorizontalMove(direction) ? rect.maxY() : rect.maxX();
tonikitoo@webkit.org660f8ff2010-03-04 20:19:44 +0000132}
133
134// This method checks if rects |a| and |b| are fully aligned either vertically or
135// horizontally. In general, rects whose central point falls between the top or
136// bottom of each other are considered fully aligned.
137// Rects that match this criteria are preferable target nodes in move focus changing
138// operations.
139// * a = Current focused node's rect.
140// * b = Focus candidate node's rect.
eae@chromium.org4ecf4182011-08-18 00:49:01 +0000141static bool areRectsFullyAligned(FocusDirection direction, const LayoutRect& a, const LayoutRect& b)
tonikitoo@webkit.org660f8ff2010-03-04 20:19:44 +0000142{
eae@chromium.org4ecf4182011-08-18 00:49:01 +0000143 LayoutUnit aStart, bStart, aEnd, bEnd;
tonikitoo@webkit.org660f8ff2010-03-04 20:19:44 +0000144
145 switch (direction) {
146 case FocusDirectionLeft:
147 aStart = a.x();
hyatt@apple.comaa0cba02011-02-01 21:39:47 +0000148 bEnd = b.maxX();
tonikitoo@webkit.org660f8ff2010-03-04 20:19:44 +0000149 break;
150 case FocusDirectionRight:
151 aStart = b.x();
hyatt@apple.comaa0cba02011-02-01 21:39:47 +0000152 bEnd = a.maxX();
tonikitoo@webkit.org660f8ff2010-03-04 20:19:44 +0000153 break;
154 case FocusDirectionUp:
155 aStart = a.y();
156 bEnd = b.y();
157 break;
158 case FocusDirectionDown:
159 aStart = b.y();
160 bEnd = a.y();
161 break;
162 default:
163 ASSERT_NOT_REACHED();
164 return false;
165 }
166
167 if (aStart < bEnd)
168 return false;
169
170 aStart = start(direction, a);
171 bStart = start(direction, b);
172
eae@chromium.org4ecf4182011-08-18 00:49:01 +0000173 LayoutUnit aMiddle = middle(direction, a);
174 LayoutUnit bMiddle = middle(direction, b);
tonikitoo@webkit.org660f8ff2010-03-04 20:19:44 +0000175
176 aEnd = end(direction, a);
177 bEnd = end(direction, b);
178
179 // Picture of the totally aligned logic:
180 //
181 // Horizontal Vertical Horizontal Vertical
182 // **************************** *****************************
183 // * _ * _ _ _ _ * * _ * _ _ *
184 // * |_| _ * |_|_|_|_| * * _ |_| * |_|_| *
185 // * |_|....|_| * . * * |_|....|_| * . *
186 // * |_| |_| (1) . * * |_| |_| (2) . *
187 // * |_| * _._ * * |_| * _ _._ _ *
188 // * * |_|_| * * * |_|_|_|_| *
189 // * * * * * *
190 // **************************** *****************************
191
192 // Horizontal Vertical Horizontal Vertical
193 // **************************** *****************************
194 // * _......_ * _ _ _ _ * * _ * _ _ _ _ *
195 // * |_| |_| * |_|_|_|_| * * |_| _ * |_|_|_|_| *
196 // * |_| |_| * . * * |_| |_| * . *
197 // * |_| (3) . * * |_|....|_| (4) . *
198 // * * ._ _ * * * _ _. *
199 // * * |_|_| * * * |_|_| *
200 // * * * * * *
201 // **************************** *****************************
202
203 return ((bMiddle >= aStart && bMiddle <= aEnd) // (1)
204 || (aMiddle >= bStart && aMiddle <= bEnd) // (2)
205 || (bStart == aStart) // (3)
206 || (bEnd == aEnd)); // (4)
207}
208
209// This method checks if |start| and |dest| have a partial intersection, either
210// horizontally or vertically.
211// * a = Current focused node's rect.
212// * b = Focus candidate node's rect.
eae@chromium.org4ecf4182011-08-18 00:49:01 +0000213static bool areRectsPartiallyAligned(FocusDirection direction, const LayoutRect& a, const LayoutRect& b)
tonikitoo@webkit.org660f8ff2010-03-04 20:19:44 +0000214{
eae@chromium.org4ecf4182011-08-18 00:49:01 +0000215 LayoutUnit aStart = start(direction, a);
216 LayoutUnit bStart = start(direction, b);
217 LayoutUnit bMiddle = middle(direction, b);
218 LayoutUnit aEnd = end(direction, a);
219 LayoutUnit bEnd = end(direction, b);
tonikitoo@webkit.org660f8ff2010-03-04 20:19:44 +0000220
221 // Picture of the partially aligned logic:
222 //
223 // Horizontal Vertical
224 // ********************************
225 // * _ * _ _ _ *
226 // * |_| * |_|_|_| *
227 // * |_|.... _ * . . *
228 // * |_| |_| * . . *
229 // * |_|....|_| * ._._ _ *
230 // * |_| * |_|_|_| *
231 // * |_| * *
232 // * * *
233 // ********************************
234 //
235 // ... and variants of the above cases.
236 return ((bStart >= aStart && bStart <= aEnd)
tonikitoo@webkit.org660f8ff2010-03-04 20:19:44 +0000237 || (bEnd >= aStart && bEnd <= aEnd)
238 || (bMiddle >= aStart && bMiddle <= aEnd)
239 || (bEnd >= aStart && bEnd <= aEnd));
240}
241
eae@chromium.org4ecf4182011-08-18 00:49:01 +0000242static bool areRectsMoreThanFullScreenApart(FocusDirection direction, const LayoutRect& curRect, const LayoutRect& targetRect, const LayoutSize& viewSize)
yael.aharon@nokia.com0a818c22010-11-22 13:49:09 +0000243{
244 ASSERT(isRectInDirection(direction, curRect, targetRect));
245
246 switch (direction) {
247 case FocusDirectionLeft:
hyatt@apple.comaa0cba02011-02-01 21:39:47 +0000248 return curRect.x() - targetRect.maxX() > viewSize.width();
yael.aharon@nokia.com0a818c22010-11-22 13:49:09 +0000249 case FocusDirectionRight:
hyatt@apple.comaa0cba02011-02-01 21:39:47 +0000250 return targetRect.x() - curRect.maxX() > viewSize.width();
yael.aharon@nokia.com0a818c22010-11-22 13:49:09 +0000251 case FocusDirectionUp:
hyatt@apple.comaa0cba02011-02-01 21:39:47 +0000252 return curRect.y() - targetRect.maxY() > viewSize.height();
yael.aharon@nokia.com0a818c22010-11-22 13:49:09 +0000253 case FocusDirectionDown:
hyatt@apple.comaa0cba02011-02-01 21:39:47 +0000254 return targetRect.y() - curRect.maxY() > viewSize.height();
yael.aharon@nokia.com0a818c22010-11-22 13:49:09 +0000255 default:
256 ASSERT_NOT_REACHED();
257 return true;
258 }
259}
260
tonikitoo@webkit.org660f8ff2010-03-04 20:19:44 +0000261// Return true if rect |a| is below |b|. False otherwise.
eae@chromium.org4ecf4182011-08-18 00:49:01 +0000262static inline bool below(const LayoutRect& a, const LayoutRect& b)
tonikitoo@webkit.org660f8ff2010-03-04 20:19:44 +0000263{
hyatt@apple.comaa0cba02011-02-01 21:39:47 +0000264 return a.y() > b.maxY();
tonikitoo@webkit.org660f8ff2010-03-04 20:19:44 +0000265}
266
267// Return true if rect |a| is on the right of |b|. False otherwise.
eae@chromium.org4ecf4182011-08-18 00:49:01 +0000268static inline bool rightOf(const LayoutRect& a, const LayoutRect& b)
tonikitoo@webkit.org660f8ff2010-03-04 20:19:44 +0000269{
hyatt@apple.comaa0cba02011-02-01 21:39:47 +0000270 return a.x() > b.maxX();
tonikitoo@webkit.org660f8ff2010-03-04 20:19:44 +0000271}
272
eae@chromium.org4ecf4182011-08-18 00:49:01 +0000273static bool isRectInDirection(FocusDirection direction, const LayoutRect& curRect, const LayoutRect& targetRect)
tonikitoo@webkit.org660f8ff2010-03-04 20:19:44 +0000274{
tonikitoo@webkit.org660f8ff2010-03-04 20:19:44 +0000275 switch (direction) {
276 case FocusDirectionLeft:
hyatt@apple.comaa0cba02011-02-01 21:39:47 +0000277 return targetRect.maxX() <= curRect.x();
tonikitoo@webkit.org660f8ff2010-03-04 20:19:44 +0000278 case FocusDirectionRight:
hyatt@apple.comaa0cba02011-02-01 21:39:47 +0000279 return targetRect.x() >= curRect.maxX();
tonikitoo@webkit.org660f8ff2010-03-04 20:19:44 +0000280 case FocusDirectionUp:
hyatt@apple.comaa0cba02011-02-01 21:39:47 +0000281 return targetRect.maxY() <= curRect.y();
tonikitoo@webkit.org660f8ff2010-03-04 20:19:44 +0000282 case FocusDirectionDown:
hyatt@apple.comaa0cba02011-02-01 21:39:47 +0000283 return targetRect.y() >= curRect.maxY();
tonikitoo@webkit.org660f8ff2010-03-04 20:19:44 +0000284 default:
285 ASSERT_NOT_REACHED();
yael.aharon@nokia.com0a818c22010-11-22 13:49:09 +0000286 return false;
tonikitoo@webkit.org660f8ff2010-03-04 20:19:44 +0000287 }
tonikitoo@webkit.org660f8ff2010-03-04 20:19:44 +0000288}
289
290// Checks if |node| is offscreen the visible area (viewport) of its container
291// document. In case it is, one can scroll in direction or take any different
292// desired action later on.
yael.aharon@nokia.com0a818c22010-11-22 13:49:09 +0000293bool hasOffscreenRect(Node* node, FocusDirection direction)
tonikitoo@webkit.org660f8ff2010-03-04 20:19:44 +0000294{
295 // Get the FrameView in which |node| is (which means the current viewport if |node|
296 // is not in an inner document), so we can check if its content rect is visible
297 // before we actually move the focus to it.
298 FrameView* frameView = node->document()->view();
299 if (!frameView)
300 return true;
301
hyatt@apple.coma61b8a32011-04-06 18:20:52 +0000302 ASSERT(!frameView->needsLayout());
303
eae@chromium.org4ecf4182011-08-18 00:49:01 +0000304 LayoutRect containerViewportRect = frameView->visibleContentRect();
yael.aharon@nokia.com0a818c22010-11-22 13:49:09 +0000305 // We want to select a node if it is currently off screen, but will be
306 // exposed after we scroll. Adjust the viewport to post-scrolling position.
307 // If the container has overflow:hidden, we cannot scroll, so we do not pass direction
308 // and we do not adjust for scrolling.
309 switch (direction) {
310 case FocusDirectionLeft:
311 containerViewportRect.setX(containerViewportRect.x() - Scrollbar::pixelsPerLineStep());
312 containerViewportRect.setWidth(containerViewportRect.width() + Scrollbar::pixelsPerLineStep());
313 break;
314 case FocusDirectionRight:
315 containerViewportRect.setWidth(containerViewportRect.width() + Scrollbar::pixelsPerLineStep());
316 break;
317 case FocusDirectionUp:
318 containerViewportRect.setY(containerViewportRect.y() - Scrollbar::pixelsPerLineStep());
319 containerViewportRect.setHeight(containerViewportRect.height() + Scrollbar::pixelsPerLineStep());
320 break;
321 case FocusDirectionDown:
322 containerViewportRect.setHeight(containerViewportRect.height() + Scrollbar::pixelsPerLineStep());
323 break;
324 default:
325 break;
326 }
tonikitoo@webkit.org660f8ff2010-03-04 20:19:44 +0000327
328 RenderObject* render = node->renderer();
329 if (!render)
330 return true;
331
eae@chromium.org4ecf4182011-08-18 00:49:01 +0000332 LayoutRect rect(render->absoluteClippedOverflowRect());
tonikitoo@webkit.org453ceea2010-04-15 19:33:59 +0000333 if (rect.isEmpty())
334 return true;
335
tonikitoo@webkit.org660f8ff2010-03-04 20:19:44 +0000336 return !containerViewportRect.intersects(rect);
337}
338
yael.aharon@nokia.com0a818c22010-11-22 13:49:09 +0000339bool scrollInDirection(Frame* frame, FocusDirection direction)
tonikitoo@webkit.org660f8ff2010-03-04 20:19:44 +0000340{
tonikitoo@webkit.orgaf84fda02010-12-14 06:51:52 +0000341 ASSERT(frame);
tonikitoo@webkit.org660f8ff2010-03-04 20:19:44 +0000342
tonikitoo@webkit.org3d623722010-12-28 21:57:11 +0000343 if (frame && canScrollInDirection(frame->document(), direction)) {
eae@chromium.org4ecf4182011-08-18 00:49:01 +0000344 LayoutUnit dx = 0;
345 LayoutUnit dy = 0;
yael.aharon@nokia.com0a818c22010-11-22 13:49:09 +0000346 switch (direction) {
347 case FocusDirectionLeft:
348 dx = - Scrollbar::pixelsPerLineStep();
349 break;
350 case FocusDirectionRight:
351 dx = Scrollbar::pixelsPerLineStep();
352 break;
353 case FocusDirectionUp:
354 dy = - Scrollbar::pixelsPerLineStep();
355 break;
356 case FocusDirectionDown:
357 dy = Scrollbar::pixelsPerLineStep();
358 break;
359 default:
360 ASSERT_NOT_REACHED();
361 return false;
362 }
tonikitoo@webkit.org660f8ff2010-03-04 20:19:44 +0000363
leviw@chromium.orga0fe0852011-11-12 02:53:52 +0000364 frame->view()->scrollBy(IntSize(dx, dy));
yael.aharon@nokia.com0a818c22010-11-22 13:49:09 +0000365 return true;
tonikitoo@webkit.org660f8ff2010-03-04 20:19:44 +0000366 }
yael.aharon@nokia.com0a818c22010-11-22 13:49:09 +0000367 return false;
368}
tonikitoo@webkit.org660f8ff2010-03-04 20:19:44 +0000369
yael.aharon@nokia.com0a818c22010-11-22 13:49:09 +0000370bool scrollInDirection(Node* container, FocusDirection direction)
371{
tonikitoo@webkit.orgaf84fda02010-12-14 06:51:52 +0000372 ASSERT(container);
yael.aharon@nokia.com0a818c22010-11-22 13:49:09 +0000373 if (container->isDocumentNode())
374 return scrollInDirection(static_cast<Document*>(container)->frame(), direction);
tonikitoo@webkit.org8f86fed2010-06-17 00:57:15 +0000375
yael.aharon@nokia.com0a818c22010-11-22 13:49:09 +0000376 if (!container->renderBox())
377 return false;
378
tonikitoo@webkit.org3d623722010-12-28 21:57:11 +0000379 if (canScrollInDirection(container, direction)) {
eae@chromium.org4ecf4182011-08-18 00:49:01 +0000380 LayoutUnit dx = 0;
381 LayoutUnit dy = 0;
yael.aharon@nokia.com0a818c22010-11-22 13:49:09 +0000382 switch (direction) {
383 case FocusDirectionLeft:
eae@chromium.org4ecf4182011-08-18 00:49:01 +0000384 dx = - min<LayoutUnit>(Scrollbar::pixelsPerLineStep(), container->renderBox()->scrollLeft());
yael.aharon@nokia.com0a818c22010-11-22 13:49:09 +0000385 break;
386 case FocusDirectionRight:
387 ASSERT(container->renderBox()->scrollWidth() > (container->renderBox()->scrollLeft() + container->renderBox()->clientWidth()));
eae@chromium.org4ecf4182011-08-18 00:49:01 +0000388 dx = min<LayoutUnit>(Scrollbar::pixelsPerLineStep(), container->renderBox()->scrollWidth() - (container->renderBox()->scrollLeft() + container->renderBox()->clientWidth()));
yael.aharon@nokia.com0a818c22010-11-22 13:49:09 +0000389 break;
390 case FocusDirectionUp:
eae@chromium.org4ecf4182011-08-18 00:49:01 +0000391 dy = - min<LayoutUnit>(Scrollbar::pixelsPerLineStep(), container->renderBox()->scrollTop());
yael.aharon@nokia.com0a818c22010-11-22 13:49:09 +0000392 break;
393 case FocusDirectionDown:
394 ASSERT(container->renderBox()->scrollHeight() - (container->renderBox()->scrollTop() + container->renderBox()->clientHeight()));
eae@chromium.org4ecf4182011-08-18 00:49:01 +0000395 dy = min<LayoutUnit>(Scrollbar::pixelsPerLineStep(), container->renderBox()->scrollHeight() - (container->renderBox()->scrollTop() + container->renderBox()->clientHeight()));
yael.aharon@nokia.com0a818c22010-11-22 13:49:09 +0000396 break;
397 default:
398 ASSERT_NOT_REACHED();
399 return false;
400 }
401
402 container->renderBox()->enclosingLayer()->scrollByRecursively(dx, dy);
403 return true;
404 }
tonikitoo@webkit.orgaf84fda02010-12-14 06:51:52 +0000405
yael.aharon@nokia.com0a818c22010-11-22 13:49:09 +0000406 return false;
tonikitoo@webkit.org660f8ff2010-03-04 20:19:44 +0000407}
408
eae@chromium.org4ecf4182011-08-18 00:49:01 +0000409static void deflateIfOverlapped(LayoutRect& a, LayoutRect& b)
tonikitoo@webkit.org660f8ff2010-03-04 20:19:44 +0000410{
411 if (!a.intersects(b) || a.contains(b) || b.contains(a))
412 return;
413
eae@chromium.org4ecf4182011-08-18 00:49:01 +0000414 LayoutUnit deflateFactor = -fudgeFactor();
tonikitoo@webkit.org660f8ff2010-03-04 20:19:44 +0000415
416 // Avoid negative width or height values.
tonikitoo@webkit.org346f8c02010-05-09 03:05:46 +0000417 if ((a.width() + 2 * deflateFactor > 0) && (a.height() + 2 * deflateFactor > 0))
418 a.inflate(deflateFactor);
tonikitoo@webkit.org660f8ff2010-03-04 20:19:44 +0000419
tonikitoo@webkit.org346f8c02010-05-09 03:05:46 +0000420 if ((b.width() + 2 * deflateFactor > 0) && (b.height() + 2 * deflateFactor > 0))
421 b.inflate(deflateFactor);
tonikitoo@webkit.org660f8ff2010-03-04 20:19:44 +0000422}
423
tonikitoo@webkit.org3d623722010-12-28 21:57:11 +0000424bool isScrollableNode(const Node* node)
tonikitoo@webkit.orgf5586a52010-06-14 19:10:59 +0000425{
tonikitoo@webkit.org3d623722010-12-28 21:57:11 +0000426 ASSERT(!node->isDocumentNode());
427
tonikitoo@webkit.orgf5586a52010-06-14 19:10:59 +0000428 if (!node)
429 return false;
430
tonikitoo@webkit.org3d623722010-12-28 21:57:11 +0000431 if (RenderObject* renderer = node->renderer())
432 return renderer->isBox() && toRenderBox(renderer)->canBeScrolledAndHasScrollableArea() && node->hasChildNodes();
tonikitoo@webkit.orgf5586a52010-06-14 19:10:59 +0000433
434 return false;
435}
436
yael.aharon@nokia.com0a818c22010-11-22 13:49:09 +0000437Node* scrollableEnclosingBoxOrParentFrameForNodeInDirection(FocusDirection direction, Node* node)
438{
439 ASSERT(node);
440 Node* parent = node;
441 do {
442 if (parent->isDocumentNode())
443 parent = static_cast<Document*>(parent)->document()->frame()->ownerElement();
444 else
445 parent = parent->parentNode();
tonikitoo@webkit.org3d623722010-12-28 21:57:11 +0000446 } while (parent && !canScrollInDirection(parent, direction) && !parent->isDocumentNode());
yael.aharon@nokia.com0a818c22010-11-22 13:49:09 +0000447
448 return parent;
449}
450
tonikitoo@webkit.org3d623722010-12-28 21:57:11 +0000451bool canScrollInDirection(const Node* container, FocusDirection direction)
yael.aharon@nokia.com0a818c22010-11-22 13:49:09 +0000452{
453 ASSERT(container);
454 if (container->isDocumentNode())
tonikitoo@webkit.org3d623722010-12-28 21:57:11 +0000455 return canScrollInDirection(static_cast<const Document*>(container)->frame(), direction);
yael.aharon@nokia.com0a818c22010-11-22 13:49:09 +0000456
tonikitoo@webkit.org3d623722010-12-28 21:57:11 +0000457 if (!isScrollableNode(container))
yael.aharon@nokia.com0a818c22010-11-22 13:49:09 +0000458 return false;
459
460 switch (direction) {
461 case FocusDirectionLeft:
462 return (container->renderer()->style()->overflowX() != OHIDDEN && container->renderBox()->scrollLeft() > 0);
463 case FocusDirectionUp:
464 return (container->renderer()->style()->overflowY() != OHIDDEN && container->renderBox()->scrollTop() > 0);
465 case FocusDirectionRight:
466 return (container->renderer()->style()->overflowX() != OHIDDEN && container->renderBox()->scrollLeft() + container->renderBox()->clientWidth() < container->renderBox()->scrollWidth());
467 case FocusDirectionDown:
468 return (container->renderer()->style()->overflowY() != OHIDDEN && container->renderBox()->scrollTop() + container->renderBox()->clientHeight() < container->renderBox()->scrollHeight());
469 default:
470 ASSERT_NOT_REACHED();
471 return false;
472 }
473}
474
tonikitoo@webkit.org3d623722010-12-28 21:57:11 +0000475bool canScrollInDirection(const Frame* frame, FocusDirection direction)
yael.aharon@nokia.com0a818c22010-11-22 13:49:09 +0000476{
477 if (!frame->view())
478 return false;
479 ScrollbarMode verticalMode;
480 ScrollbarMode horizontalMode;
481 frame->view()->calculateScrollbarModesForLayout(horizontalMode, verticalMode);
482 if ((direction == FocusDirectionLeft || direction == FocusDirectionRight) && ScrollbarAlwaysOff == horizontalMode)
483 return false;
484 if ((direction == FocusDirectionUp || direction == FocusDirectionDown) && ScrollbarAlwaysOff == verticalMode)
485 return false;
eae@chromium.org4ecf4182011-08-18 00:49:01 +0000486 LayoutSize size = frame->view()->contentsSize();
487 LayoutSize offset = frame->view()->scrollOffset();
488 LayoutRect rect = frame->view()->visibleContentRect(true);
yael.aharon@nokia.com0a818c22010-11-22 13:49:09 +0000489
490 switch (direction) {
491 case FocusDirectionLeft:
492 return offset.width() > 0;
493 case FocusDirectionUp:
494 return offset.height() > 0;
495 case FocusDirectionRight:
496 return rect.width() + offset.width() < size.width();
497 case FocusDirectionDown:
498 return rect.height() + offset.height() < size.height();
499 default:
500 ASSERT_NOT_REACHED();
501 return false;
502 }
503}
504
eae@chromium.org4ecf4182011-08-18 00:49:01 +0000505static LayoutRect rectToAbsoluteCoordinates(Frame* initialFrame, const LayoutRect& initialRect)
yael.aharon@nokia.com0a818c22010-11-22 13:49:09 +0000506{
eae@chromium.org4ecf4182011-08-18 00:49:01 +0000507 LayoutRect rect = initialRect;
yael.aharon@nokia.com0a818c22010-11-22 13:49:09 +0000508 for (Frame* frame = initialFrame; frame; frame = frame->tree()->parent()) {
509 if (Element* element = static_cast<Element*>(frame->ownerElement())) {
510 do {
511 rect.move(element->offsetLeft(), element->offsetTop());
512 } while ((element = element->offsetParent()));
513 rect.move((-frame->view()->scrollOffset()));
514 }
515 }
516 return rect;
517}
518
eae@chromium.org4ecf4182011-08-18 00:49:01 +0000519LayoutRect nodeRectInAbsoluteCoordinates(Node* node, bool ignoreBorder)
yael.aharon@nokia.com0a818c22010-11-22 13:49:09 +0000520{
hyatt@apple.coma61b8a32011-04-06 18:20:52 +0000521 ASSERT(node && node->renderer() && !node->document()->view()->needsLayout());
yael.aharon@nokia.com0a818c22010-11-22 13:49:09 +0000522
523 if (node->isDocumentNode())
524 return frameRectInAbsoluteCoordinates(static_cast<Document*>(node)->frame());
eae@chromium.org4ecf4182011-08-18 00:49:01 +0000525 LayoutRect rect = rectToAbsoluteCoordinates(node->document()->frame(), node->getRect());
yael.aharon@nokia.com0a818c22010-11-22 13:49:09 +0000526
527 // For authors that use border instead of outline in their CSS, we compensate by ignoring the border when calculating
528 // the rect of the focused element.
529 if (ignoreBorder) {
530 rect.move(node->renderer()->style()->borderLeftWidth(), node->renderer()->style()->borderTopWidth());
531 rect.setWidth(rect.width() - node->renderer()->style()->borderLeftWidth() - node->renderer()->style()->borderRightWidth());
532 rect.setHeight(rect.height() - node->renderer()->style()->borderTopWidth() - node->renderer()->style()->borderBottomWidth());
533 }
534 return rect;
535}
536
eae@chromium.org4ecf4182011-08-18 00:49:01 +0000537LayoutRect frameRectInAbsoluteCoordinates(Frame* frame)
yael.aharon@nokia.com0a818c22010-11-22 13:49:09 +0000538{
539 return rectToAbsoluteCoordinates(frame, frame->view()->visibleContentRect());
540}
541
542// This method calculates the exitPoint from the startingRect and the entryPoint into the candidate rect.
543// The line between those 2 points is the closest distance between the 2 rects.
eae@chromium.org4ecf4182011-08-18 00:49:01 +0000544void entryAndExitPointsForDirection(FocusDirection direction, const LayoutRect& startingRect, const LayoutRect& potentialRect, LayoutPoint& exitPoint, LayoutPoint& entryPoint)
yael.aharon@nokia.com0a818c22010-11-22 13:49:09 +0000545{
546 switch (direction) {
547 case FocusDirectionLeft:
548 exitPoint.setX(startingRect.x());
hyatt@apple.comaa0cba02011-02-01 21:39:47 +0000549 entryPoint.setX(potentialRect.maxX());
yael.aharon@nokia.com0a818c22010-11-22 13:49:09 +0000550 break;
551 case FocusDirectionUp:
552 exitPoint.setY(startingRect.y());
hyatt@apple.comaa0cba02011-02-01 21:39:47 +0000553 entryPoint.setY(potentialRect.maxY());
yael.aharon@nokia.com0a818c22010-11-22 13:49:09 +0000554 break;
555 case FocusDirectionRight:
hyatt@apple.comaa0cba02011-02-01 21:39:47 +0000556 exitPoint.setX(startingRect.maxX());
yael.aharon@nokia.com0a818c22010-11-22 13:49:09 +0000557 entryPoint.setX(potentialRect.x());
558 break;
559 case FocusDirectionDown:
hyatt@apple.comaa0cba02011-02-01 21:39:47 +0000560 exitPoint.setY(startingRect.maxY());
yael.aharon@nokia.com0a818c22010-11-22 13:49:09 +0000561 entryPoint.setY(potentialRect.y());
562 break;
563 default:
564 ASSERT_NOT_REACHED();
565 }
566
567 switch (direction) {
568 case FocusDirectionLeft:
569 case FocusDirectionRight:
570 if (below(startingRect, potentialRect)) {
571 exitPoint.setY(startingRect.y());
hyatt@apple.comaa0cba02011-02-01 21:39:47 +0000572 entryPoint.setY(potentialRect.maxY());
yael.aharon@nokia.com0a818c22010-11-22 13:49:09 +0000573 } else if (below(potentialRect, startingRect)) {
hyatt@apple.comaa0cba02011-02-01 21:39:47 +0000574 exitPoint.setY(startingRect.maxY());
yael.aharon@nokia.com0a818c22010-11-22 13:49:09 +0000575 entryPoint.setY(potentialRect.y());
576 } else {
577 exitPoint.setY(max(startingRect.y(), potentialRect.y()));
578 entryPoint.setY(exitPoint.y());
579 }
580 break;
581 case FocusDirectionUp:
582 case FocusDirectionDown:
583 if (rightOf(startingRect, potentialRect)) {
584 exitPoint.setX(startingRect.x());
hyatt@apple.comaa0cba02011-02-01 21:39:47 +0000585 entryPoint.setX(potentialRect.maxX());
yael.aharon@nokia.com0a818c22010-11-22 13:49:09 +0000586 } else if (rightOf(potentialRect, startingRect)) {
hyatt@apple.comaa0cba02011-02-01 21:39:47 +0000587 exitPoint.setX(startingRect.maxX());
yael.aharon@nokia.com0a818c22010-11-22 13:49:09 +0000588 entryPoint.setX(potentialRect.x());
589 } else {
590 exitPoint.setX(max(startingRect.x(), potentialRect.x()));
591 entryPoint.setX(exitPoint.x());
592 }
593 break;
594 default:
595 ASSERT_NOT_REACHED();
596 }
597}
598
yael.aharon@nokia.com4a4c87b2011-02-18 21:05:38 +0000599bool areElementsOnSameLine(const FocusCandidate& firstCandidate, const FocusCandidate& secondCandidate)
600{
601 if (firstCandidate.isNull() || secondCandidate.isNull())
602 return false;
603
604 if (!firstCandidate.visibleNode->renderer() || !secondCandidate.visibleNode->renderer())
605 return false;
606
607 if (!firstCandidate.rect.intersects(secondCandidate.rect))
608 return false;
609
610 if (firstCandidate.focusableNode->hasTagName(HTMLNames::areaTag) || secondCandidate.focusableNode->hasTagName(HTMLNames::areaTag))
611 return false;
612
613 if (!firstCandidate.visibleNode->renderer()->isRenderInline() || !secondCandidate.visibleNode->renderer()->isRenderInline())
614 return false;
615
616 if (firstCandidate.visibleNode->renderer()->containingBlock() != secondCandidate.visibleNode->renderer()->containingBlock())
617 return false;
618
619 return true;
620}
621
tonikitoo@webkit.orgd13935e2010-12-14 07:17:23 +0000622void distanceDataForNode(FocusDirection direction, const FocusCandidate& current, FocusCandidate& candidate)
yael.aharon@nokia.com0a818c22010-11-22 13:49:09 +0000623{
yael.aharon@nokia.com4a4c87b2011-02-18 21:05:38 +0000624 if (areElementsOnSameLine(current, candidate)) {
625 if ((direction == FocusDirectionUp && current.rect.y() > candidate.rect.y()) || (direction == FocusDirectionDown && candidate.rect.y() > current.rect.y())) {
626 candidate.distance = 0;
627 candidate.alignment = Full;
628 return;
629 }
630 }
631
eae@chromium.org4ecf4182011-08-18 00:49:01 +0000632 LayoutRect nodeRect = candidate.rect;
633 LayoutRect currentRect = current.rect;
yael.aharon@nokia.com0a818c22010-11-22 13:49:09 +0000634 deflateIfOverlapped(currentRect, nodeRect);
635
636 if (!isRectInDirection(direction, currentRect, nodeRect))
637 return;
638
eae@chromium.org4ecf4182011-08-18 00:49:01 +0000639 LayoutPoint exitPoint;
640 LayoutPoint entryPoint;
641 LayoutUnit sameAxisDistance = 0;
642 LayoutUnit otherAxisDistance = 0;
yael.aharon@nokia.com0a818c22010-11-22 13:49:09 +0000643 entryAndExitPointsForDirection(direction, currentRect, nodeRect, exitPoint, entryPoint);
644
645 switch (direction) {
646 case FocusDirectionLeft:
647 sameAxisDistance = exitPoint.x() - entryPoint.x();
648 otherAxisDistance = abs(exitPoint.y() - entryPoint.y());
649 break;
650 case FocusDirectionUp:
651 sameAxisDistance = exitPoint.y() - entryPoint.y();
652 otherAxisDistance = abs(exitPoint.x() - entryPoint.x());
653 break;
654 case FocusDirectionRight:
655 sameAxisDistance = entryPoint.x() - exitPoint.x();
656 otherAxisDistance = abs(entryPoint.y() - exitPoint.y());
657 break;
658 case FocusDirectionDown:
659 sameAxisDistance = entryPoint.y() - exitPoint.y();
660 otherAxisDistance = abs(entryPoint.x() - exitPoint.x());
661 break;
662 default:
663 ASSERT_NOT_REACHED();
664 return;
665 }
666
eae@chromium.orgfdb2f382012-03-19 21:05:07 +0000667 float x = (entryPoint.x() - exitPoint.x()) * (entryPoint.x() - exitPoint.x());
668 float y = (entryPoint.y() - exitPoint.y()) * (entryPoint.y() - exitPoint.y());
yael.aharon@nokia.com0a818c22010-11-22 13:49:09 +0000669
eae@chromium.orgfdb2f382012-03-19 21:05:07 +0000670 float euclidianDistance = sqrt(x + y);
yael.aharon@nokia.com0a818c22010-11-22 13:49:09 +0000671
672 // Loosely based on http://www.w3.org/TR/WICD/#focus-handling
673 // df = dotDist + dx + dy + 2 * (xdisplacement + ydisplacement) - sqrt(Overlap)
674
675 float distance = euclidianDistance + sameAxisDistance + 2 * otherAxisDistance;
676 candidate.distance = roundf(distance);
eae@chromium.org4ecf4182011-08-18 00:49:01 +0000677 LayoutSize viewSize = candidate.visibleNode->document()->page()->mainFrame()->view()->visibleContentRect().size();
yael.aharon@nokia.com0a818c22010-11-22 13:49:09 +0000678 candidate.alignment = alignmentForRects(direction, currentRect, nodeRect, viewSize);
679}
680
yael.aharon@nokia.coma886edf2010-11-23 12:57:17 +0000681bool canBeScrolledIntoView(FocusDirection direction, const FocusCandidate& candidate)
yael.aharon@nokia.com0a818c22010-11-22 13:49:09 +0000682{
yael.aharon@nokia.comf1b5ab42010-12-07 19:04:10 +0000683 ASSERT(candidate.visibleNode && candidate.isOffscreen);
eae@chromium.org4ecf4182011-08-18 00:49:01 +0000684 LayoutRect candidateRect = candidate.rect;
yael.aharon@nokia.comf1b5ab42010-12-07 19:04:10 +0000685 for (Node* parentNode = candidate.visibleNode->parentNode(); parentNode; parentNode = parentNode->parentNode()) {
eae@chromium.org4ecf4182011-08-18 00:49:01 +0000686 LayoutRect parentRect = nodeRectInAbsoluteCoordinates(parentNode);
yael.aharon@nokia.com0a818c22010-11-22 13:49:09 +0000687 if (!candidateRect.intersects(parentRect)) {
688 if (((direction == FocusDirectionLeft || direction == FocusDirectionRight) && parentNode->renderer()->style()->overflowX() == OHIDDEN)
689 || ((direction == FocusDirectionUp || direction == FocusDirectionDown) && parentNode->renderer()->style()->overflowY() == OHIDDEN))
690 return false;
691 }
692 if (parentNode == candidate.enclosingScrollableBox)
tonikitoo@webkit.org3d623722010-12-28 21:57:11 +0000693 return canScrollInDirection(parentNode, direction);
yael.aharon@nokia.com0a818c22010-11-22 13:49:09 +0000694 }
695 return true;
696}
697
698// The starting rect is the rect of the focused node, in document coordinates.
699// Compose a virtual starting rect if there is no focused node or if it is off screen.
700// The virtual rect is the edge of the container or frame. We select which
701// edge depending on the direction of the navigation.
eae@chromium.org4ecf4182011-08-18 00:49:01 +0000702LayoutRect virtualRectForDirection(FocusDirection direction, const LayoutRect& startingRect, LayoutUnit width)
yael.aharon@nokia.com0a818c22010-11-22 13:49:09 +0000703{
eae@chromium.org4ecf4182011-08-18 00:49:01 +0000704 LayoutRect virtualStartingRect = startingRect;
yael.aharon@nokia.com0a818c22010-11-22 13:49:09 +0000705 switch (direction) {
706 case FocusDirectionLeft:
hyatt@apple.comaa0cba02011-02-01 21:39:47 +0000707 virtualStartingRect.setX(virtualStartingRect.maxX() - width);
yael.aharon@nokia.comf1b5ab42010-12-07 19:04:10 +0000708 virtualStartingRect.setWidth(width);
yael.aharon@nokia.com0a818c22010-11-22 13:49:09 +0000709 break;
710 case FocusDirectionUp:
hyatt@apple.comaa0cba02011-02-01 21:39:47 +0000711 virtualStartingRect.setY(virtualStartingRect.maxY() - width);
yael.aharon@nokia.comf1b5ab42010-12-07 19:04:10 +0000712 virtualStartingRect.setHeight(width);
yael.aharon@nokia.com0a818c22010-11-22 13:49:09 +0000713 break;
714 case FocusDirectionRight:
yael.aharon@nokia.comf1b5ab42010-12-07 19:04:10 +0000715 virtualStartingRect.setWidth(width);
yael.aharon@nokia.com0a818c22010-11-22 13:49:09 +0000716 break;
717 case FocusDirectionDown:
yael.aharon@nokia.comf1b5ab42010-12-07 19:04:10 +0000718 virtualStartingRect.setHeight(width);
yael.aharon@nokia.com0a818c22010-11-22 13:49:09 +0000719 break;
720 default:
721 ASSERT_NOT_REACHED();
722 }
723
724 return virtualStartingRect;
725}
726
eae@chromium.org4ecf4182011-08-18 00:49:01 +0000727LayoutRect virtualRectForAreaElementAndDirection(HTMLAreaElement* area, FocusDirection direction)
yael.aharon@nokia.comf1b5ab42010-12-07 19:04:10 +0000728{
tonikitoo@webkit.org237c1ea2010-12-13 05:52:40 +0000729 ASSERT(area);
730 ASSERT(area->imageElement());
yael.aharon@nokia.comf1b5ab42010-12-07 19:04:10 +0000731 // Area elements tend to overlap more than other focusable elements. We flatten the rect of the area elements
732 // to minimize the effect of overlapping areas.
eae@chromium.org4ecf4182011-08-18 00:49:01 +0000733 LayoutRect rect = virtualRectForDirection(direction, rectToAbsoluteCoordinates(area->document()->frame(), area->computeRect(area->imageElement()->renderer())), 1);
yael.aharon@nokia.comf1b5ab42010-12-07 19:04:10 +0000734 return rect;
735}
736
tonikitoo@webkit.org36a7cfb2010-12-14 06:51:41 +0000737HTMLFrameOwnerElement* frameOwnerElement(FocusCandidate& candidate)
738{
739 return candidate.isFrameOwnerElement() ? static_cast<HTMLFrameOwnerElement*>(candidate.visibleNode) : 0;
740};
yael.aharon@nokia.com0a818c22010-11-22 13:49:09 +0000741
tonikitoo@webkit.org660f8ff2010-03-04 20:19:44 +0000742} // namespace WebCore