OLD | NEW |
(Empty) | |
| 1 // Copyright 2014 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. |
| 4 |
| 5 #include "config.h" |
| 6 #include "core/html/canvas/CanvasHitRegion.h" |
| 7 |
| 8 #include "bindings/v8/Dictionary.h" |
| 9 #include "bindings/v8/ExceptionState.h" |
| 10 #include "core/dom/Element.h" |
| 11 #include "core/dom/ElementTraversal.h" |
| 12 #include "core/html/HTMLImageElement.h" |
| 13 #include "core/html/HTMLInputElement.h" |
| 14 #include "core/html/HTMLSelectElement.h" |
| 15 #include "core/html/canvas/Path2D.h" |
| 16 |
| 17 namespace WebCore { |
| 18 |
| 19 // FIXME: De-dupe. |
| 20 static WindRule parseWinding(const String& windingRuleString, WindRule defaultVa
lue = RULE_EVENODD) |
| 21 { |
| 22 if (windingRuleString == "nonzero") |
| 23 return RULE_NONZERO; |
| 24 if (windingRuleString == "evenodd") |
| 25 return RULE_EVENODD; |
| 26 |
| 27 return defaultValue; |
| 28 } |
| 29 |
| 30 DecodedHitRegionOptions::DecodedHitRegionOptions() |
| 31 : hasPath(false) |
| 32 , fillRule(RULE_NONZERO) |
| 33 { |
| 34 } |
| 35 |
| 36 DecodedHitRegionOptions::DecodedHitRegionOptions(const Dictionary& options) |
| 37 : hasPath(false) |
| 38 , fillRule(RULE_NONZERO) |
| 39 { |
| 40 RefPtr<Path2D> pathFromDictionary; |
| 41 if (options.get("path", pathFromDictionary) && pathFromDictionary) { |
| 42 path = pathFromDictionary->path(); |
| 43 hasPath = true; |
| 44 } |
| 45 String windRuleString; |
| 46 if (options.get("fillRule", windRuleString)) |
| 47 fillRule = parseWinding(windRuleString, RULE_NONZERO); |
| 48 if (options.get("id", id) && id.isEmpty()) |
| 49 id = String(); |
| 50 if (options.getWithUndefinedOrNullCheck("parentID", parentId) && parentId.is
Empty()) |
| 51 parentId = String(); |
| 52 options.get("cursor", cursor); |
| 53 options.get("control", control); |
| 54 if (options.getWithUndefinedOrNullCheck("label", label) && label.isEmpty()) |
| 55 label = String(); |
| 56 if (options.getWithUndefinedOrNullCheck("role", role) && role.isEmpty()) |
| 57 role = String(); |
| 58 } |
| 59 |
| 60 void DecodedHitRegionOptions::resolvePath(const Path& currentPath, const AffineT
ransform& currentTransform) |
| 61 { |
| 62 if (!hasPath) |
| 63 path = currentPath; |
| 64 path.transform(currentTransform); |
| 65 } |
| 66 |
| 67 void DecodedHitRegionOptions::resolveIds(const CanvasHitRegionManager* hitRegion
Manager) |
| 68 { |
| 69 parentHitRegion = !parentId.isEmpty() ? hitRegionManager->getHitRegionById(p
arentId) : 0; |
| 70 previousHitRegion = !id.isEmpty() ? hitRegionManager->getHitRegionById(id) :
0; |
| 71 } |
| 72 |
| 73 static bool isSupportedInteractiveFallbackElement(Element& element) |
| 74 { |
| 75 // "An element is a supported interactive canvas fallback element if it is o
ne of the following:" |
| 76 |
| 77 // "an 'a' element that represents a hyperlink and that does not have any im
g descendants" |
| 78 if (element.hasTagName(HTMLNames::aTag)) |
| 79 return !Traversal<HTMLImageElement>::firstWithin(element); |
| 80 |
| 81 // "a button element" |
| 82 if (element.hasTagName(HTMLNames::buttonTag)) |
| 83 return true; |
| 84 |
| 85 if (isHTMLInputElement(element)) { |
| 86 // "an input element whose type attribute is in one of the Checkbox or R
adio Button states" |
| 87 const HTMLInputElement& inputElement = toHTMLInputElement(element); |
| 88 if (inputElement.isCheckbox() || inputElement.isRadioButton()) |
| 89 return true; |
| 90 |
| 91 // "an input element that is a button but its type attribute is not in t
he Image Button state" |
| 92 if (inputElement.isTextButton() && !inputElement.isImageButton()) |
| 93 return true; |
| 94 } |
| 95 |
| 96 // "a select element with a multiple attribute or a display size greater tha
n 1" |
| 97 if (isHTMLSelectElement(element)) { |
| 98 const HTMLSelectElement& selectElement = toHTMLSelectElement(element); |
| 99 if (selectElement.multiple() || selectElement.size() > 1) |
| 100 return true; |
| 101 } |
| 102 |
| 103 // "an option element that is in a list of options of a select element with |
| 104 // a multiple attribute or a display size greater than 1" |
| 105 // FIXME: Only direct descendant? |
| 106 if (isHTMLOptionElement(element) && element.parentNode() && isHTMLSelectElem
ent(*element.parentNode())) { |
| 107 const HTMLSelectElement& selectElement = toHTMLSelectElement(*element.pa
rentNode()); |
| 108 if (selectElement.multiple() || selectElement.size() > 1) |
| 109 return true; |
| 110 } |
| 111 |
| 112 // "a sorting interface th element" |
| 113 // Note: This seems redundant with the last condition. |
| 114 if (element.hasTagName(HTMLNames::thTag) && element.fastHasAttribute(HTMLNam
es::sortableAttr)) |
| 115 return true; |
| 116 |
| 117 // "an element that would not be interactive content except for having the |
| 118 // tabindex attribute specified" |
| 119 // FIXME: Does not test the "interactive content" part. |
| 120 if (element.fastHasAttribute(HTMLNames::tabindexAttr)) |
| 121 return true; |
| 122 |
| 123 // "a non-interactive table, caption, thead, tbody, tfoot, tr, td, or th ele
ment" |
| 124 if (element.hasTagName(HTMLNames::tableTag) |
| 125 || element.hasTagName(HTMLNames::captionTag) |
| 126 || element.hasTagName(HTMLNames::theadTag) |
| 127 || element.hasTagName(HTMLNames::tbodyTag) |
| 128 || element.hasTagName(HTMLNames::tfootTag) |
| 129 || element.hasTagName(HTMLNames::trTag) |
| 130 || element.hasTagName(HTMLNames::tdTag) |
| 131 || element.hasTagName(HTMLNames::thTag)) |
| 132 return true; |
| 133 |
| 134 return false; |
| 135 } |
| 136 |
| 137 static bool isValidCSSCursor(const String& cursor) |
| 138 { |
| 139 // FIXME: Implement. |
| 140 return false; |
| 141 } |
| 142 |
| 143 static bool isValidCursorValue(const String& cursor) |
| 144 { |
| 145 return cursor == "inherit" || isValidCSSCursor(cursor); |
| 146 } |
| 147 |
| 148 static bool hasValidAriaRoles(const String& roleList) |
| 149 { |
| 150 // FIXME: Implement. |
| 151 return false; |
| 152 } |
| 153 |
| 154 bool DecodedHitRegionOptions::validate(ExceptionState& exceptionState) const |
| 155 { |
| 156 // 11. "If any of the following conditions are met, throw a NotSupportedErro
r exception and abort these steps." |
| 157 |
| 158 // "The arguments object's control and label members are both non-null." |
| 159 if (control && !label.isNull()) |
| 160 exceptionState.throwDOMException(NotSupportedError, "cannot specify both
a control and a label"); |
| 161 |
| 162 // "The arguments object's control and role members are both non-null." |
| 163 if (control && !role.isNull()) |
| 164 exceptionState.throwDOMException(NotSupportedError, "cannot specify both
a control and a role"); |
| 165 |
| 166 // "The arguments object's role member's value is the empty string, and the |
| 167 // label member's value is either null or the empty string." |
| 168 if (role.isEmpty() && (label.isNull() || label.isEmpty())) |
| 169 exceptionState.throwDOMException(NotSupportedError, "both role and label
cannot be empty"); |
| 170 |
| 171 // FIXME: Include clip. |
| 172 // "The specified pixels has no pixels." (s/pixels/area/) |
| 173 if (path.isEmpty() || path.boundingRect().isEmpty()) |
| 174 exceptionState.throwDOMException(NotSupportedError, "the specified regio
n cannot be empty"); |
| 175 |
| 176 // "The arguments object's control member is neither null nor a supported |
| 177 // interactive canvas fallback element." |
| 178 if (control && !isSupportedInteractiveFallbackElement(*control)) |
| 179 exceptionState.throwDOMException(NotSupportedError, "the specified contr
ol is not supported"); |
| 180 |
| 181 // "The parent region is not null but has a control." |
| 182 if (parentHitRegion && parentHitRegion->control()) |
| 183 exceptionState.throwDOMException(NotSupportedError, "the specified paren
t hit-region is associated with a control"); |
| 184 |
| 185 // "The previous region for this ID is the same hit region as the parent reg
ion." |
| 186 if (previousHitRegion && parentHitRegion && previousHitRegion == parentHitRe
gion) |
| 187 exceptionState.throwDOMException(NotSupportedError, "the previous region
is the same as the parent region"); |
| 188 |
| 189 // "The previous region for this ID is an ancestor region of the parent regi
on." |
| 190 if (previousHitRegion && parentHitRegion && previousHitRegion->isAncestorOf(
parentHitRegion.get())) |
| 191 exceptionState.throwDOMException(NotSupportedError, "the previous region
is an ancestor of the paretn region"); |
| 192 |
| 193 // 12. "If the parent member is not null but parent region is null, then |
| 194 // throw a NotFoundError exception and abort these steps." |
| 195 if (!parentId.isNull() && !parentHitRegion) |
| 196 exceptionState.throwDOMException(NotFoundError, "the specified parent re
gion does not exist"); |
| 197 |
| 198 // 13. "If any of the following conditions are met, throw a SyntaxError exce
ption and abort these steps." |
| 199 |
| 200 // "The arguments object's cursor member is not null but is neither an |
| 201 // ASCII case-insensitive match for the string "inherit", nor a valid CSS |
| 202 // 'cursor' property value." |
| 203 if (!cursor.isNull() && !isValidCursorValue(cursor)) |
| 204 exceptionState.throwDOMException(SyntaxError, "the cursor value (' + cur
sor + ') is not recognized"); |
| 205 |
| 206 // "The arguments object's role member is not null but its value is not an |
| 207 // ordered set of unique space-separated tokens whose tokens are all |
| 208 // case-sensitive matches for names of non-abstract WAI-ARIA roles." |
| 209 if (!role.isNull() && !hasValidAriaRoles(role)) |
| 210 exceptionState.throwDOMException(SyntaxError, "the specified role(s) not
valid ARIA role(s)"); |
| 211 |
| 212 return !exceptionState.hadException(); |
| 213 } |
| 214 |
| 215 CanvasHitRegion::CanvasHitRegion(const DecodedHitRegionOptions& options) |
| 216 : m_geometryInfo(GeometryInfo::create(options.path, options.fillRule, this)) |
| 217 , m_parent(options.parentHitRegion) |
| 218 , m_control(options.control) |
| 219 , m_id(options.id) |
| 220 , m_cursor(options.cursor) |
| 221 , m_label(options.label) |
| 222 , m_ariaRole(options.role) |
| 223 { |
| 224 } |
| 225 |
| 226 CanvasHitRegion::~CanvasHitRegion() |
| 227 { |
| 228 // Detach ourselves from our GeometryInfo, rendering it unable to produce a |
| 229 // matching result on a hit test (but still occupying the area.) |
| 230 m_geometryInfo->detachHitRegion(); |
| 231 } |
| 232 |
| 233 CanvasHitRegion::GeometryInfo::GeometryInfo(const Path& path, WindRule fillRule,
CanvasHitRegion* hitRegion) |
| 234 : m_path(path) |
| 235 , m_fillRule(fillRule) |
| 236 , m_hitRegion(hitRegion) |
| 237 { |
| 238 } |
| 239 |
| 240 bool CanvasHitRegion::GeometryInfo::contains(const LayoutPoint& point) const |
| 241 { |
| 242 if (!m_path.boundingRect().contains(point)) |
| 243 return false; |
| 244 return m_path.contains(point, m_fillRule); |
| 245 } |
| 246 |
| 247 bool CanvasHitRegion::GeometryInfo::isEnclosed(const FloatRect& queryRect, const
AffineTransform& queryTransform) const |
| 248 { |
| 249 // Pessimistic 'enclosure' test. |
| 250 FloatRect testRect; |
| 251 if (queryTransform.isIdentity()) { |
| 252 testRect = m_path.boundingRect(); |
| 253 } else { |
| 254 Path inverseTransformedPath = m_path; |
| 255 inverseTransformedPath.transform(queryTransform); |
| 256 testRect = inverseTransformedPath.boundingRect(); |
| 257 } |
| 258 return queryRect.contains(testRect); |
| 259 } |
| 260 |
| 261 bool CanvasHitRegion::isAncestorOf(const CanvasHitRegion* hitRegion) const |
| 262 { |
| 263 if (!hitRegion || hitRegion == this) |
| 264 return false; |
| 265 |
| 266 while ((hitRegion = hitRegion->parent())) { |
| 267 if (hitRegion == this) |
| 268 return true; |
| 269 } |
| 270 return false; |
| 271 } |
| 272 |
| 273 void CanvasHitRegion::trace(Visitor* visitor) |
| 274 { |
| 275 #if ENABLE(OILPAN) |
| 276 visitor->trace(m_parent); |
| 277 visitor->trace(m_control); |
| 278 #endif |
| 279 } |
| 280 |
| 281 void CanvasHitRegionManager::add(PassRefPtrWillBeRawPtr<CanvasHitRegion> passHit
Region) |
| 282 { |
| 283 RefPtrWillBeRawPtr<CanvasHitRegion> hitRegion = passHitRegion; |
| 284 ASSERT(!m_hitRegions.contains(hitRegion)); |
| 285 m_hitRegions.add(hitRegion); |
| 286 // Add to the id -> hit region map. |
| 287 if (!hitRegion->id().isEmpty()) { |
| 288 ASSERT(!m_hitRegionIdMap.contains(hitRegion->id())); |
| 289 m_hitRegionIdMap.set(hitRegion->id(), hitRegion); |
| 290 } |
| 291 // Add the hit region to the spatial query structure. |
| 292 m_hitRegionGeometry.append(hitRegion->geometryInfo()); |
| 293 } |
| 294 |
| 295 void CanvasHitRegionManager::remove(CanvasHitRegion* hitRegion) |
| 296 { |
| 297 if (!hitRegion) |
| 298 return; |
| 299 |
| 300 if (!hitRegion->id().isEmpty()) { |
| 301 ASSERT(m_hitRegionIdMap.get(hitRegion->id()) == hitRegion); |
| 302 m_hitRegionIdMap.remove(hitRegion->id()); |
| 303 } |
| 304 ASSERT(m_hitRegions.contains(hitRegion)); |
| 305 m_hitRegions.remove(hitRegion); |
| 306 } |
| 307 |
| 308 void CanvasHitRegionManager::clear() |
| 309 { |
| 310 m_hitRegionGeometry.clear(); |
| 311 m_hitRegionIdMap.clear(); |
| 312 m_hitRegions.clear(); |
| 313 } |
| 314 |
| 315 void CanvasHitRegionManager::removeEnclosed(const FloatRect& rect, const AffineT
ransform& currentTransform) |
| 316 { |
| 317 size_t geometryCount = m_hitRegionGeometry.size(); |
| 318 if (!geometryCount) |
| 319 return; |
| 320 |
| 321 FloatRect queryRect; |
| 322 AffineTransform queryTransform; |
| 323 if (currentTransform.preservesAxisAlignment()) { |
| 324 queryRect = currentTransform.mapRect(rect); |
| 325 // |queryTransform| is identity. |
| 326 } else { |
| 327 queryRect = rect; |
| 328 queryTransform = currentTransform.inverse(); |
| 329 } |
| 330 |
| 331 for (size_t i = geometryCount - 1; i >= 0; --i) { |
| 332 CanvasHitRegion::GeometryInfo* geometryInfo = m_hitRegionGeometry[i].get
(); |
| 333 if (!geometryInfo->isEnclosed(queryRect, queryTransform)) |
| 334 continue; |
| 335 |
| 336 // Drop the associated hit region (if any.) |
| 337 remove(geometryInfo->hitRegion()); |
| 338 // Drop the geometry info. |
| 339 m_hitRegionGeometry.remove(i); |
| 340 } |
| 341 |
| 342 // If there's still hit regions referenced from the geometry list, then add |
| 343 // a query rectangle that will consume any queries to that area. |
| 344 if (!m_hitRegions.isEmpty()) { |
| 345 Path exclusionPath; |
| 346 exclusionPath.addRect(rect); |
| 347 exclusionPath.transform(currentTransform); |
| 348 m_hitRegionGeometry.append(CanvasHitRegion::GeometryInfo::create(exclusi
onPath, RULE_NONZERO, 0)); |
| 349 } else { |
| 350 // Clear the geometry list, since it will only contain exclusions. |
| 351 m_hitRegionGeometry.clear(); |
| 352 } |
| 353 } |
| 354 |
| 355 CanvasHitRegion* CanvasHitRegionManager::getHitRegionById(const String& id) cons
t |
| 356 { |
| 357 return m_hitRegionIdMap.get(id); |
| 358 } |
| 359 |
| 360 CanvasHitRegion* CanvasHitRegionManager::getHitRegionAtPoint(const LayoutPoint&
point) const |
| 361 { |
| 362 HitRegionGeometry::const_reverse_iterator itEnd = m_hitRegionGeometry.rend()
; |
| 363 for (HitRegionGeometry::const_reverse_iterator it = m_hitRegionGeometry.rbeg
in(); it != itEnd; ++it) { |
| 364 if ((*it)->contains(point)) |
| 365 return (*it)->hitRegion(); |
| 366 } |
| 367 return 0; |
| 368 } |
| 369 |
| 370 void CanvasHitRegionManager::trace(Visitor* visitor) |
| 371 { |
| 372 #if ENABLE(OILPAN) |
| 373 visitor->trace(m_hitRegions); |
| 374 visitor->trace(m_hitRegionIdMap); |
| 375 #endif |
| 376 } |
| 377 |
| 378 } // namespace WebCore |
OLD | NEW |