Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(1054)

Unified Diff: Source/core/html/canvas/CanvasHitRegion.cpp

Issue 287163007: WIP: <canvas> hit regions (Closed) Base URL: svn://svn.chromium.org/blink/trunk
Patch Set: Created 6 years, 7 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
« no previous file with comments | « Source/core/html/canvas/CanvasHitRegion.h ('k') | Source/core/html/canvas/CanvasRenderingContext2D.h » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
Index: Source/core/html/canvas/CanvasHitRegion.cpp
diff --git a/Source/core/html/canvas/CanvasHitRegion.cpp b/Source/core/html/canvas/CanvasHitRegion.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..0841a4128d5763ec27b8f0133e9ff27737f57196
--- /dev/null
+++ b/Source/core/html/canvas/CanvasHitRegion.cpp
@@ -0,0 +1,378 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "config.h"
+#include "core/html/canvas/CanvasHitRegion.h"
+
+#include "bindings/v8/Dictionary.h"
+#include "bindings/v8/ExceptionState.h"
+#include "core/dom/Element.h"
+#include "core/dom/ElementTraversal.h"
+#include "core/html/HTMLImageElement.h"
+#include "core/html/HTMLInputElement.h"
+#include "core/html/HTMLSelectElement.h"
+#include "core/html/canvas/Path2D.h"
+
+namespace WebCore {
+
+// FIXME: De-dupe.
+static WindRule parseWinding(const String& windingRuleString, WindRule defaultValue = RULE_EVENODD)
+{
+ if (windingRuleString == "nonzero")
+ return RULE_NONZERO;
+ if (windingRuleString == "evenodd")
+ return RULE_EVENODD;
+
+ return defaultValue;
+}
+
+DecodedHitRegionOptions::DecodedHitRegionOptions()
+ : hasPath(false)
+ , fillRule(RULE_NONZERO)
+{
+}
+
+DecodedHitRegionOptions::DecodedHitRegionOptions(const Dictionary& options)
+ : hasPath(false)
+ , fillRule(RULE_NONZERO)
+{
+ RefPtr<Path2D> pathFromDictionary;
+ if (options.get("path", pathFromDictionary) && pathFromDictionary) {
+ path = pathFromDictionary->path();
+ hasPath = true;
+ }
+ String windRuleString;
+ if (options.get("fillRule", windRuleString))
+ fillRule = parseWinding(windRuleString, RULE_NONZERO);
+ if (options.get("id", id) && id.isEmpty())
+ id = String();
+ if (options.getWithUndefinedOrNullCheck("parentID", parentId) && parentId.isEmpty())
+ parentId = String();
+ options.get("cursor", cursor);
+ options.get("control", control);
+ if (options.getWithUndefinedOrNullCheck("label", label) && label.isEmpty())
+ label = String();
+ if (options.getWithUndefinedOrNullCheck("role", role) && role.isEmpty())
+ role = String();
+}
+
+void DecodedHitRegionOptions::resolvePath(const Path& currentPath, const AffineTransform& currentTransform)
+{
+ if (!hasPath)
+ path = currentPath;
+ path.transform(currentTransform);
+}
+
+void DecodedHitRegionOptions::resolveIds(const CanvasHitRegionManager* hitRegionManager)
+{
+ parentHitRegion = !parentId.isEmpty() ? hitRegionManager->getHitRegionById(parentId) : 0;
+ previousHitRegion = !id.isEmpty() ? hitRegionManager->getHitRegionById(id) : 0;
+}
+
+static bool isSupportedInteractiveFallbackElement(Element& element)
+{
+ // "An element is a supported interactive canvas fallback element if it is one of the following:"
+
+ // "an 'a' element that represents a hyperlink and that does not have any img descendants"
+ if (element.hasTagName(HTMLNames::aTag))
+ return !Traversal<HTMLImageElement>::firstWithin(element);
+
+ // "a button element"
+ if (element.hasTagName(HTMLNames::buttonTag))
+ return true;
+
+ if (isHTMLInputElement(element)) {
+ // "an input element whose type attribute is in one of the Checkbox or Radio Button states"
+ const HTMLInputElement& inputElement = toHTMLInputElement(element);
+ if (inputElement.isCheckbox() || inputElement.isRadioButton())
+ return true;
+
+ // "an input element that is a button but its type attribute is not in the Image Button state"
+ if (inputElement.isTextButton() && !inputElement.isImageButton())
+ return true;
+ }
+
+ // "a select element with a multiple attribute or a display size greater than 1"
+ if (isHTMLSelectElement(element)) {
+ const HTMLSelectElement& selectElement = toHTMLSelectElement(element);
+ if (selectElement.multiple() || selectElement.size() > 1)
+ return true;
+ }
+
+ // "an option element that is in a list of options of a select element with
+ // a multiple attribute or a display size greater than 1"
+ // FIXME: Only direct descendant?
+ if (isHTMLOptionElement(element) && element.parentNode() && isHTMLSelectElement(*element.parentNode())) {
+ const HTMLSelectElement& selectElement = toHTMLSelectElement(*element.parentNode());
+ if (selectElement.multiple() || selectElement.size() > 1)
+ return true;
+ }
+
+ // "a sorting interface th element"
+ // Note: This seems redundant with the last condition.
+ if (element.hasTagName(HTMLNames::thTag) && element.fastHasAttribute(HTMLNames::sortableAttr))
+ return true;
+
+ // "an element that would not be interactive content except for having the
+ // tabindex attribute specified"
+ // FIXME: Does not test the "interactive content" part.
+ if (element.fastHasAttribute(HTMLNames::tabindexAttr))
+ return true;
+
+ // "a non-interactive table, caption, thead, tbody, tfoot, tr, td, or th element"
+ if (element.hasTagName(HTMLNames::tableTag)
+ || element.hasTagName(HTMLNames::captionTag)
+ || element.hasTagName(HTMLNames::theadTag)
+ || element.hasTagName(HTMLNames::tbodyTag)
+ || element.hasTagName(HTMLNames::tfootTag)
+ || element.hasTagName(HTMLNames::trTag)
+ || element.hasTagName(HTMLNames::tdTag)
+ || element.hasTagName(HTMLNames::thTag))
+ return true;
+
+ return false;
+}
+
+static bool isValidCSSCursor(const String& cursor)
+{
+ // FIXME: Implement.
+ return false;
+}
+
+static bool isValidCursorValue(const String& cursor)
+{
+ return cursor == "inherit" || isValidCSSCursor(cursor);
+}
+
+static bool hasValidAriaRoles(const String& roleList)
+{
+ // FIXME: Implement.
+ return false;
+}
+
+bool DecodedHitRegionOptions::validate(ExceptionState& exceptionState) const
+{
+ // 11. "If any of the following conditions are met, throw a NotSupportedError exception and abort these steps."
+
+ // "The arguments object's control and label members are both non-null."
+ if (control && !label.isNull())
+ exceptionState.throwDOMException(NotSupportedError, "cannot specify both a control and a label");
+
+ // "The arguments object's control and role members are both non-null."
+ if (control && !role.isNull())
+ exceptionState.throwDOMException(NotSupportedError, "cannot specify both a control and a role");
+
+ // "The arguments object's role member's value is the empty string, and the
+ // label member's value is either null or the empty string."
+ if (role.isEmpty() && (label.isNull() || label.isEmpty()))
+ exceptionState.throwDOMException(NotSupportedError, "both role and label cannot be empty");
+
+ // FIXME: Include clip.
+ // "The specified pixels has no pixels." (s/pixels/area/)
+ if (path.isEmpty() || path.boundingRect().isEmpty())
+ exceptionState.throwDOMException(NotSupportedError, "the specified region cannot be empty");
+
+ // "The arguments object's control member is neither null nor a supported
+ // interactive canvas fallback element."
+ if (control && !isSupportedInteractiveFallbackElement(*control))
+ exceptionState.throwDOMException(NotSupportedError, "the specified control is not supported");
+
+ // "The parent region is not null but has a control."
+ if (parentHitRegion && parentHitRegion->control())
+ exceptionState.throwDOMException(NotSupportedError, "the specified parent hit-region is associated with a control");
+
+ // "The previous region for this ID is the same hit region as the parent region."
+ if (previousHitRegion && parentHitRegion && previousHitRegion == parentHitRegion)
+ exceptionState.throwDOMException(NotSupportedError, "the previous region is the same as the parent region");
+
+ // "The previous region for this ID is an ancestor region of the parent region."
+ if (previousHitRegion && parentHitRegion && previousHitRegion->isAncestorOf(parentHitRegion.get()))
+ exceptionState.throwDOMException(NotSupportedError, "the previous region is an ancestor of the paretn region");
+
+ // 12. "If the parent member is not null but parent region is null, then
+ // throw a NotFoundError exception and abort these steps."
+ if (!parentId.isNull() && !parentHitRegion)
+ exceptionState.throwDOMException(NotFoundError, "the specified parent region does not exist");
+
+ // 13. "If any of the following conditions are met, throw a SyntaxError exception and abort these steps."
+
+ // "The arguments object's cursor member is not null but is neither an
+ // ASCII case-insensitive match for the string "inherit", nor a valid CSS
+ // 'cursor' property value."
+ if (!cursor.isNull() && !isValidCursorValue(cursor))
+ exceptionState.throwDOMException(SyntaxError, "the cursor value (' + cursor + ') is not recognized");
+
+ // "The arguments object's role member is not null but its value is not an
+ // ordered set of unique space-separated tokens whose tokens are all
+ // case-sensitive matches for names of non-abstract WAI-ARIA roles."
+ if (!role.isNull() && !hasValidAriaRoles(role))
+ exceptionState.throwDOMException(SyntaxError, "the specified role(s) not valid ARIA role(s)");
+
+ return !exceptionState.hadException();
+}
+
+CanvasHitRegion::CanvasHitRegion(const DecodedHitRegionOptions& options)
+ : m_geometryInfo(GeometryInfo::create(options.path, options.fillRule, this))
+ , m_parent(options.parentHitRegion)
+ , m_control(options.control)
+ , m_id(options.id)
+ , m_cursor(options.cursor)
+ , m_label(options.label)
+ , m_ariaRole(options.role)
+{
+}
+
+CanvasHitRegion::~CanvasHitRegion()
+{
+ // Detach ourselves from our GeometryInfo, rendering it unable to produce a
+ // matching result on a hit test (but still occupying the area.)
+ m_geometryInfo->detachHitRegion();
+}
+
+CanvasHitRegion::GeometryInfo::GeometryInfo(const Path& path, WindRule fillRule, CanvasHitRegion* hitRegion)
+ : m_path(path)
+ , m_fillRule(fillRule)
+ , m_hitRegion(hitRegion)
+{
+}
+
+bool CanvasHitRegion::GeometryInfo::contains(const LayoutPoint& point) const
+{
+ if (!m_path.boundingRect().contains(point))
+ return false;
+ return m_path.contains(point, m_fillRule);
+}
+
+bool CanvasHitRegion::GeometryInfo::isEnclosed(const FloatRect& queryRect, const AffineTransform& queryTransform) const
+{
+ // Pessimistic 'enclosure' test.
+ FloatRect testRect;
+ if (queryTransform.isIdentity()) {
+ testRect = m_path.boundingRect();
+ } else {
+ Path inverseTransformedPath = m_path;
+ inverseTransformedPath.transform(queryTransform);
+ testRect = inverseTransformedPath.boundingRect();
+ }
+ return queryRect.contains(testRect);
+}
+
+bool CanvasHitRegion::isAncestorOf(const CanvasHitRegion* hitRegion) const
+{
+ if (!hitRegion || hitRegion == this)
+ return false;
+
+ while ((hitRegion = hitRegion->parent())) {
+ if (hitRegion == this)
+ return true;
+ }
+ return false;
+}
+
+void CanvasHitRegion::trace(Visitor* visitor)
+{
+#if ENABLE(OILPAN)
+ visitor->trace(m_parent);
+ visitor->trace(m_control);
+#endif
+}
+
+void CanvasHitRegionManager::add(PassRefPtrWillBeRawPtr<CanvasHitRegion> passHitRegion)
+{
+ RefPtrWillBeRawPtr<CanvasHitRegion> hitRegion = passHitRegion;
+ ASSERT(!m_hitRegions.contains(hitRegion));
+ m_hitRegions.add(hitRegion);
+ // Add to the id -> hit region map.
+ if (!hitRegion->id().isEmpty()) {
+ ASSERT(!m_hitRegionIdMap.contains(hitRegion->id()));
+ m_hitRegionIdMap.set(hitRegion->id(), hitRegion);
+ }
+ // Add the hit region to the spatial query structure.
+ m_hitRegionGeometry.append(hitRegion->geometryInfo());
+}
+
+void CanvasHitRegionManager::remove(CanvasHitRegion* hitRegion)
+{
+ if (!hitRegion)
+ return;
+
+ if (!hitRegion->id().isEmpty()) {
+ ASSERT(m_hitRegionIdMap.get(hitRegion->id()) == hitRegion);
+ m_hitRegionIdMap.remove(hitRegion->id());
+ }
+ ASSERT(m_hitRegions.contains(hitRegion));
+ m_hitRegions.remove(hitRegion);
+}
+
+void CanvasHitRegionManager::clear()
+{
+ m_hitRegionGeometry.clear();
+ m_hitRegionIdMap.clear();
+ m_hitRegions.clear();
+}
+
+void CanvasHitRegionManager::removeEnclosed(const FloatRect& rect, const AffineTransform& currentTransform)
+{
+ size_t geometryCount = m_hitRegionGeometry.size();
+ if (!geometryCount)
+ return;
+
+ FloatRect queryRect;
+ AffineTransform queryTransform;
+ if (currentTransform.preservesAxisAlignment()) {
+ queryRect = currentTransform.mapRect(rect);
+ // |queryTransform| is identity.
+ } else {
+ queryRect = rect;
+ queryTransform = currentTransform.inverse();
+ }
+
+ for (size_t i = geometryCount - 1; i >= 0; --i) {
+ CanvasHitRegion::GeometryInfo* geometryInfo = m_hitRegionGeometry[i].get();
+ if (!geometryInfo->isEnclosed(queryRect, queryTransform))
+ continue;
+
+ // Drop the associated hit region (if any.)
+ remove(geometryInfo->hitRegion());
+ // Drop the geometry info.
+ m_hitRegionGeometry.remove(i);
+ }
+
+ // If there's still hit regions referenced from the geometry list, then add
+ // a query rectangle that will consume any queries to that area.
+ if (!m_hitRegions.isEmpty()) {
+ Path exclusionPath;
+ exclusionPath.addRect(rect);
+ exclusionPath.transform(currentTransform);
+ m_hitRegionGeometry.append(CanvasHitRegion::GeometryInfo::create(exclusionPath, RULE_NONZERO, 0));
+ } else {
+ // Clear the geometry list, since it will only contain exclusions.
+ m_hitRegionGeometry.clear();
+ }
+}
+
+CanvasHitRegion* CanvasHitRegionManager::getHitRegionById(const String& id) const
+{
+ return m_hitRegionIdMap.get(id);
+}
+
+CanvasHitRegion* CanvasHitRegionManager::getHitRegionAtPoint(const LayoutPoint& point) const
+{
+ HitRegionGeometry::const_reverse_iterator itEnd = m_hitRegionGeometry.rend();
+ for (HitRegionGeometry::const_reverse_iterator it = m_hitRegionGeometry.rbegin(); it != itEnd; ++it) {
+ if ((*it)->contains(point))
+ return (*it)->hitRegion();
+ }
+ return 0;
+}
+
+void CanvasHitRegionManager::trace(Visitor* visitor)
+{
+#if ENABLE(OILPAN)
+ visitor->trace(m_hitRegions);
+ visitor->trace(m_hitRegionIdMap);
+#endif
+}
+
+} // namespace WebCore
« no previous file with comments | « Source/core/html/canvas/CanvasHitRegion.h ('k') | Source/core/html/canvas/CanvasRenderingContext2D.h » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698