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

Side by Side 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 unified diff | Download patch | Annotate | Revision Log
OLDNEW
(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
OLDNEW
« 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