Commit b27a05ca authored by Hugo Holgersson's avatar Hugo Holgersson Committed by Commit Bot

[SpatNav] Add projected overlap to SpatNav's distance formula

When two candidates are equally close to our current focus F,
SpatNav now prioritizes the candidate that can "absorb" most
of F's horizontal or vertical projection. The spec calls this
"projected overlap" [1].

When a document is focused (or when activeElement is null),
SpatNav projects the document's edge onto each candidate.
More generally, when looking inside (and outside) any focused
box, we project the box's _edge_ onto each candidate.

Note: The current spec [1] suggests us to remove
|navigation_axis_distance| from the formula but if we do,
snav-symmetrically-positioned.html will fail. So let's keep
it until we find a simpler/more robust formula that can
handle this pattern.

[1] https://drafts.csswg.org/css-nav-1/

Bug: 958845
Change-Id: Id90bce78c9e9d985e6936716b8dafce883b07540
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1594724Reviewed-by: default avatarDavid Bokan <bokan@chromium.org>
Reviewed-by: default avatarFredrik Söderquist <fs@opera.com>
Commit-Queue: Hugo Holgersson <hholgersson@fb.com>
Cr-Commit-Position: refs/heads/master@{#665845}
parent 48327f50
...@@ -530,6 +530,49 @@ void EntryAndExitPointsForDirection(SpatialNavigationDirection direction, ...@@ -530,6 +530,49 @@ void EntryAndExitPointsForDirection(SpatialNavigationDirection direction,
} }
} }
double ProjectedOverlap(SpatialNavigationDirection direction,
LayoutRect current,
LayoutRect candidate) {
switch (direction) {
case SpatialNavigationDirection::kLeft:
case SpatialNavigationDirection::kRight:
current.SetWidth(LayoutUnit(1));
candidate.SetX(current.X());
current.Intersect(candidate);
return current.Height();
case SpatialNavigationDirection::kUp:
case SpatialNavigationDirection::kDown:
current.SetHeight(LayoutUnit(1));
candidate.SetY(current.Y());
current.Intersect(candidate);
return current.Width();
default:
NOTREACHED();
return kMaxDistance;
}
}
double Alignment(SpatialNavigationDirection direction,
LayoutRect current,
LayoutRect candidate) {
// The formula and constants for "alignment" are experimental and
// come from https://drafts.csswg.org/css-nav-1/#heuristics.
const int kAlignWeight = 5;
double projected_overlap = ProjectedOverlap(direction, current, candidate);
switch (direction) {
case SpatialNavigationDirection::kLeft:
case SpatialNavigationDirection::kRight:
return (kAlignWeight * projected_overlap) / current.Height();
case SpatialNavigationDirection::kUp:
case SpatialNavigationDirection::kDown:
return (kAlignWeight * projected_overlap) / current.Width();
default:
NOTREACHED();
return kMaxDistance;
}
}
double ComputeDistanceDataForNode(SpatialNavigationDirection direction, double ComputeDistanceDataForNode(SpatialNavigationDirection direction,
const FocusCandidate& current_interest, const FocusCandidate& current_interest,
const FocusCandidate& candidate) { const FocusCandidate& candidate) {
...@@ -614,8 +657,9 @@ double ComputeDistanceDataForNode(SpatialNavigationDirection direction, ...@@ -614,8 +657,9 @@ double ComputeDistanceDataForNode(SpatialNavigationDirection direction,
return kMaxDistance; return kMaxDistance;
} }
// Distance calculation is based on http://www.w3.org/TR/WICD/#focus-handling // Distance calculation is based on https://drafts.csswg.org/css-nav-1/.
return distance + navigation_axis_distance + return distance + navigation_axis_distance -
Alignment(direction, current_rect, node_rect) +
weighted_orthogonal_axis_distance - sqrt(overlap); weighted_orthogonal_axis_distance - sqrt(overlap);
} }
......
<!doctype html>
<style>
div {height: 30px;}
div.green {background: green}
div.red {background: red}
*:focus {outline: blue solid 2px;}
</style>
<div id="container" href="www" style="top: 10px; left: 40px; width: 300px; height: 300px; background: yellow; position: relative; margin: 60px" tabindex="0">
<div class="red" id="redA" style="position: absolute; top: 10px; left: 240px; width: 40px;" tabindex="0"></div>
<div class="red" id="redB" style="position: absolute; top: 50px; left: 10px; width: 40px;" tabindex="0"></div>
<div class="red" id="redC" style="position: absolute; top: 50px; left: 240px; width: 40px;" tabindex="0"></div>
<div class="red" id="redD" style="position: absolute; top: 260px; left: 240px; width: 40px;" tabindex="0"></div>
<div class="green" id="greenC" style="position: absolute; top: 85px; left: 240px; width: 40px; height: 160px" tabindex="0"></div>
<div class="green" id="greenB" style="position: absolute; top: 260px; left: 10px; width: 220px;" tabindex="0"></div>
<div class="green" id="greenD" style="position: absolute; top: 85px; left: 10px; width: 40px; height: 160px" tabindex="0"></div>
<div class="green" id="greenA" style="position: absolute; top: 10px; left: 10px; width: 220px;" tabindex="0"></div>
<a id="linkA" href="www" style="position: absolute; top: 160px; left: 320px;">linkA</a>
<a id="linkB" href="www" style="position: absolute; top: 160px; left: -40px;">linkB</a>
<a id="linkC" href="www" style="position: absolute; top: -40px; left: 125px;">linkC</a>
<a id="linkD" href="www" style="position: absolute; top: 330px; left: 125px;">linkD</a>
</div>
<p><em>Manual test instruction: When moving into the yellow div, ensure
that SpatNav picks "more aligned" candidates first.</em></p>
<p>Note: In DOM order, the green candidates are <em>after</em> the red candidates.
Here SpatNav must ignore DOM order because the green candidates have bigger
projection surface.</p>
<script src="../../resources/testharness.js"></script>
<script src="../../resources/testharnessreport.js"></script>
<script src="resources/snav-testharness.js"></script>
<script>
container.focus();
var resultMap = [
["Down", "greenA"],
["Down", "redB"],
["Down", "greenD"],
["Left", "linkB"],
["Right", "container"],
["Right", "greenD"],
["Right", "greenC"],
["Down", "redD"],
["Down", "linkD"],
];
snav.assertFocusMoves(resultMap);
</script>
\ No newline at end of file
<!doctype html> <!doctype html>
<p>Here are two links that (due to their parent divs' limited width) split over two lines:</p> <p>Here are two links that (due to their parent divs' limited width) split over two lines:</p>
<div style="font: 6px Ahem;"> <div style="font: 6px Ahem; line-height: 9px;">
<a id="start" href="www">Random start link</a>. <a id="start" href="www">Random start link</a>.
<div style="width: 230px; background: yellow; position: relative; margin-left: 20px;"> <div style="width: 230px; background: yellow; position: relative; margin-left: 20px;">
<a id="fragmentedA" href="www">This is a long bla bla bla fragmented link.</a> <a id="fragmentedA" href="www">This is a long bla bla bla fragmented link.</a>
...@@ -50,11 +50,13 @@ This bounding box covers another link. Here we test that SpatNav can reach that ...@@ -50,11 +50,13 @@ This bounding box covers another link. Here we test that SpatNav can reach that
["Left", "B"], // We don't go to the enclosing #fragmentedA. If we did, holding ["Left", "B"], // We don't go to the enclosing #fragmentedA. If we did, holding
// Left would cause a loop inside #fragmentedA's bounding box. // Left would cause a loop inside #fragmentedA's bounding box.
["Right", "fragmentedA"], ["Right", "fragmentedA"],
["Down", "now_reachableA"], // We searched *inside* #fragmentedA's box. // Down searches *inside* #fragmentedA's box.
["Down", "short"], ["Down", "now_reachableB"], // #now_reachableB is wider than #now_reachableA.
["Right", "now_reachableC"], ["Left", "now_reachableA"],
["Right", "anotherC"], // We don't go to #fragmentedB because Right from #fragmentedB goes back to #short. ["Right", "now_reachableB"],
["Right", "anotherA"], // We don't go to #fragmentedB because Right from #fragmentedB goes back to #short.
// This avoids an endless loop between #fragmentedB and #short if Right is held. // This avoids an endless loop between #fragmentedB and #short if Right is held.
["Down", "anotherC"],
["Left", "fragmentedB"], ["Left", "fragmentedB"],
["Right", "short"], // We searched *inside* #fragmentedB's box. ["Right", "short"], // We searched *inside* #fragmentedB's box.
["Down", "end"], // This avoids an endless loop between #fragmentedB and #short if Down is held. ["Down", "end"], // This avoids an endless loop between #fragmentedB and #short if Down is held.
......
<!doctype html>
<map name="map" title="map" id="firstmap"> <map name="map" title="map" id="firstmap">
<area shape="circle" coords="45,45,25" href="#" id="1"> <area shape="circle" coords="45,45,25" href="#" id="circle">
<area shape="rect" coords="45,60,95,110" href="#" id="2"> <area shape="rect" coords="45,60,95,110" href="#" id="rect">
<area shape="poly" coords="80,20,130,20,130,180,30,180,30,130,80,130" href="#" id="3"> <area shape="poly" coords="80,20,130,20,130,180,30,180,30,130,80,130" href="#" id="poly">
<area shape="default" href="#" id="4"> <area shape="default" href="#" id="default">
</map> </map>
<a id="start" href="a"><img src="resources/green.png" width=50px height=50px></a> <a id="first" href="a"><img src="resources/green.png" width=50px height=50px></a>
<br><br> <br><br>
<div> <div>
<img src="resources/green.png" width=200px height=200px usemap="#map"> <img src="resources/green.png" width=200px height=200px usemap="#map">
</div> </div>
<a id="5" href="a"><img src="resources/green.png" width=50px height=50px></a> <a id="last" href="a"><img src="resources/green.png" width=50px height=50px></a>
<p>This test tests that areas of an imagemap can be reached with spatial navigation even if they are overlapped.</p> <p>All areas of an image map should be reachable with spatial navigation, even if they overlap.</p>
<script src="../../resources/testharness.js"></script> <script src="../../resources/testharness.js"></script>
<script src="../../resources/testharnessreport.js"></script> <script src="../../resources/testharnessreport.js"></script>
<script src="resources/snav-testharness.js"></script> <script src="resources/snav-testharness.js"></script>
<script> <script>
var resultMap = [ var resultMap = [
["Down", "4"], ["Down", "default"],
["Down", "1"], ["Down", "poly"],
["Down", "2"], ["Left", "rect"],
["Down", "5"], ["Left", "circle"],
["Up", "4"], ["Down", "rect"],
["Up", "3"], ["Down", "last"],
["Up", "2"], ["Up", "default"],
["Left", "1"], ["Up", "poly"],
["Right", "3"], ["Up", "rect"],
["Left", "2"], ["Up", "circle"],
["Up", "1"], ["Up", "first"],
["Up", "start"],
]; ];
// Start at a known place. // Start at a known place.
document.getElementById("start").focus(); document.getElementById("first").focus();
snav.assertFocusMoves(resultMap); snav.assertFocusMoves(resultMap);
</script> </script>
<p><em>Manual test instruction: Ensure the img's default area gets visited first since it is the
outermost area and closest to #first. From there, ensure that all internal areas are reachable too.
</em></p>
<p>In general, if two areas are on the same distance from current focus F, SpatNav should prioritize
the area that can absorb most of F's projection.</p>
<p>When looking for the closest "insider" (here, areas inside the focused img), SpatNav projects
he img's edge onto each area. Here #poly wins over #circle because #poly's area absorbs more
of the projection.</p>
\ No newline at end of file
<!doctype html>
<style>
#more-aligned {background: green; padding: 100px;}
#less-aligned {background: red; padding: 99px;}
</style>
<span id="less-aligned" class="more" tabindex="0">x</span><span id="more-aligned" class="more" tabindex="0">x</span>
<script src="../../resources/testharness.js"></script>
<script src="../../resources/testharnessreport.js"></script>
<script src="resources/snav-testharness.js"></script>
<script>
snav.assertFocusMoves([["Down", "more-aligned"]]);
</script>
<p><em>Manual test instruction: Ensure SpatNav goes to the, projection wise,
"more aligned" element first, even though it comes second in DOM order.</em></p>
<p>When two rects are on the same distance from current focus F, SpatNav
should prioritize the rect that can absorb most of F's projection.</p>
<p>When looking for the closest "insider" (here, focusables inside the
document), SpatNav projects the document's edge onto each candidate.</p>
\ No newline at end of file
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment