<https://webkit.org/b/119930> input[type=range]: Fix a crash by changing input type...
[WebKit-https.git] / Source / WebCore / html / shadow / SliderThumbElement.cpp
1 /*
2  * Copyright (C) 2006, 2007, 2008, 2009 Apple Inc. All rights reserved.
3  * Copyright (C) 2010 Google Inc. All rights reserved.
4  *
5  * Redistribution and use in source and binary forms, with or without
6  * modification, are permitted provided that the following conditions are
7  * met:
8  *
9  *     * Redistributions of source code must retain the above copyright
10  * notice, this list of conditions and the following disclaimer.
11  *     * Redistributions in binary form must reproduce the above
12  * copyright notice, this list of conditions and the following disclaimer
13  * in the documentation and/or other materials provided with the
14  * distribution.
15  *     * Neither the name of Google Inc. nor the names of its
16  * contributors may be used to endorse or promote products derived from
17  * this software without specific prior written permission.
18  *
19  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
20  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
21  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
22  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
23  * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
25  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
26  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
27  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30  */
31
32
33 #include "config.h"
34 #include "SliderThumbElement.h"
35
36 #include "CSSValueKeywords.h"
37 #include "Event.h"
38 #include "EventHandler.h"
39 #include "Frame.h"
40 #include "HTMLInputElement.h"
41 #include "HTMLParserIdioms.h"
42 #include "MouseEvent.h"
43 #include "RenderFlexibleBox.h"
44 #include "RenderSlider.h"
45 #include "RenderTheme.h"
46 #include "ShadowRoot.h"
47 #include "StepRange.h"
48
49 using namespace std;
50
51 namespace WebCore {
52
53 using namespace HTMLNames;
54
55 inline static Decimal sliderPosition(HTMLInputElement* element)
56 {
57     const StepRange stepRange(element->createStepRange(RejectAny));
58     const Decimal oldValue = parseToDecimalForNumberType(element->value(), stepRange.defaultValue());
59     return stepRange.proportionFromValue(stepRange.clampValue(oldValue));
60 }
61
62 inline static bool hasVerticalAppearance(HTMLInputElement* input)
63 {
64     ASSERT(input->renderer());
65     RenderStyle* sliderStyle = input->renderer()->style();
66
67 #if ENABLE(VIDEO)
68     if (sliderStyle->appearance() == MediaVolumeSliderPart && input->renderer()->theme()->usesVerticalVolumeSlider())
69         return true;
70 #endif
71
72     return sliderStyle->appearance() == SliderVerticalPart;
73 }
74
75 SliderThumbElement* sliderThumbElementOf(Node* node)
76 {
77     ASSERT(node);
78     ShadowRoot* shadow = node->toInputElement()->userAgentShadowRoot();
79     ASSERT(shadow);
80     Node* thumb = shadow->firstChild()->firstChild()->firstChild();
81     ASSERT(thumb);
82     return toSliderThumbElement(thumb);
83 }
84
85 HTMLElement* sliderTrackElementOf(Node* node)
86 {
87     ASSERT(node);
88     ShadowRoot* shadow = node->toInputElement()->userAgentShadowRoot();
89     ASSERT(shadow);
90     Node* track = shadow->firstChild()->firstChild();
91     ASSERT(track);
92     return toHTMLElement(track);
93 }
94
95 // --------------------------------
96
97 RenderSliderThumb::RenderSliderThumb(SliderThumbElement* element)
98     : RenderBlock(element)
99 {
100 }
101
102 void RenderSliderThumb::updateAppearance(RenderStyle* parentStyle)
103 {
104     if (parentStyle->appearance() == SliderVerticalPart)
105         style()->setAppearance(SliderThumbVerticalPart);
106     else if (parentStyle->appearance() == SliderHorizontalPart)
107         style()->setAppearance(SliderThumbHorizontalPart);
108     else if (parentStyle->appearance() == MediaSliderPart)
109         style()->setAppearance(MediaSliderThumbPart);
110     else if (parentStyle->appearance() == MediaVolumeSliderPart)
111         style()->setAppearance(MediaVolumeSliderThumbPart);
112     else if (parentStyle->appearance() == MediaFullScreenVolumeSliderPart)
113         style()->setAppearance(MediaFullScreenVolumeSliderThumbPart);
114     if (style()->hasAppearance())
115         theme()->adjustSliderThumbSize(style(), toElement(node()));
116 }
117
118 bool RenderSliderThumb::isSliderThumb() const
119 {
120     return true;
121 }
122
123 // --------------------------------
124
125 // FIXME: Find a way to cascade appearance and adjust heights, and get rid of this class.
126 // http://webkit.org/b/62535
127 class RenderSliderContainer : public RenderFlexibleBox {
128 public:
129     RenderSliderContainer(SliderContainerElement* element)
130         : RenderFlexibleBox(element) { }
131 public:
132     virtual void computeLogicalHeight(LayoutUnit logicalHeight, LayoutUnit logicalTop, LogicalExtentComputedValues&) const OVERRIDE;
133
134 private:
135     virtual void layout() OVERRIDE;
136 };
137
138 void RenderSliderContainer::computeLogicalHeight(LayoutUnit logicalHeight, LayoutUnit logicalTop, LogicalExtentComputedValues& computedValues) const
139 {
140     HTMLInputElement* input = node()->shadowHost()->toInputElement();
141     bool isVertical = hasVerticalAppearance(input);
142
143 #if ENABLE(DATALIST_ELEMENT)
144     if (input->renderer()->isSlider() && !isVertical && input->list()) {
145         int offsetFromCenter = theme()->sliderTickOffsetFromTrackCenter();
146         LayoutUnit trackHeight = 0;
147         if (offsetFromCenter < 0)
148             trackHeight = -2 * offsetFromCenter;
149         else {
150             int tickLength = theme()->sliderTickSize().height();
151             trackHeight = 2 * (offsetFromCenter + tickLength);
152         }
153         float zoomFactor = style()->effectiveZoom();
154         if (zoomFactor != 1.0)
155             trackHeight *= zoomFactor;
156
157         RenderBox::computeLogicalHeight(trackHeight, logicalTop, computedValues);
158         return;
159     }
160 #endif
161     if (isVertical)
162         logicalHeight = RenderSlider::defaultTrackLength;
163     RenderBox::computeLogicalHeight(logicalHeight, logicalTop, computedValues);
164 }
165
166 void RenderSliderContainer::layout()
167 {
168     HTMLInputElement* input = node()->shadowHost()->toInputElement();
169     bool isVertical = hasVerticalAppearance(input);
170     style()->setFlexDirection(isVertical ? FlowColumn : FlowRow);
171     TextDirection oldTextDirection = style()->direction();
172     if (isVertical) {
173         // FIXME: Work around rounding issues in RTL vertical sliders. We want them to
174         // render identically to LTR vertical sliders. We can remove this work around when
175         // subpixel rendering is enabled on all ports.
176         style()->setDirection(LTR);
177     }
178
179     RenderBox* thumb = input->sliderThumbElement() ? input->sliderThumbElement()->renderBox() : 0;
180     RenderBox* track = input->sliderTrackElement() ? input->sliderTrackElement()->renderBox() : 0;
181     // Force a layout to reset the position of the thumb so the code below doesn't move the thumb to the wrong place.
182     // FIXME: Make a custom Render class for the track and move the thumb positioning code there.
183     if (track)
184         track->setChildNeedsLayout(true, MarkOnlyThis);
185
186     RenderFlexibleBox::layout();
187
188     style()->setDirection(oldTextDirection);
189     // These should always exist, unless someone mutates the shadow DOM (e.g., in the inspector).
190     if (!thumb || !track)
191         return;
192
193     double percentageOffset = sliderPosition(input).toDouble();
194     LayoutUnit availableExtent = isVertical ? track->contentHeight() : track->contentWidth();
195     availableExtent -= isVertical ? thumb->height() : thumb->width();
196     LayoutUnit offset = percentageOffset * availableExtent;
197     LayoutPoint thumbLocation = thumb->location();
198     if (isVertical)
199         thumbLocation.setY(thumbLocation.y() + track->contentHeight() - thumb->height() - offset);
200     else if (style()->isLeftToRightDirection())
201         thumbLocation.setX(thumbLocation.x() + offset);
202     else
203         thumbLocation.setX(thumbLocation.x() - offset);
204     thumb->setLocation(thumbLocation);
205 }
206
207 // --------------------------------
208
209 SliderThumbElement::SliderThumbElement(Document* document)
210     : HTMLDivElement(HTMLNames::divTag, document)
211     , m_inDragMode(false)
212 {
213     setHasCustomStyleResolveCallbacks();
214 }
215
216 void SliderThumbElement::setPositionFromValue()
217 {
218     // Since the code to calculate position is in the RenderSliderThumb layout
219     // path, we don't actually update the value here. Instead, we poke at the
220     // renderer directly to trigger layout.
221     if (renderer())
222         renderer()->setNeedsLayout(true);
223 }
224
225 RenderObject* SliderThumbElement::createRenderer(RenderArena* arena, RenderStyle*)
226 {
227     return new (arena) RenderSliderThumb(this);
228 }
229
230 bool SliderThumbElement::isDisabledFormControl() const
231 {
232     HTMLInputElement* input = hostInput();
233     return !input || input->isDisabledFormControl();
234 }
235
236 bool SliderThumbElement::matchesReadOnlyPseudoClass() const
237 {
238     HTMLInputElement* input = hostInput();
239     return input && input->matchesReadOnlyPseudoClass();
240 }
241
242 bool SliderThumbElement::matchesReadWritePseudoClass() const
243 {
244     HTMLInputElement* input = hostInput();
245     return input && input->matchesReadWritePseudoClass();
246 }
247
248 Element* SliderThumbElement::focusDelegate()
249 {
250     return hostInput();
251 }
252
253 void SliderThumbElement::dragFrom(const LayoutPoint& point)
254 {
255     setPositionFromPoint(point);
256     startDragging();
257 }
258
259 void SliderThumbElement::setPositionFromPoint(const LayoutPoint& point)
260 {
261     RefPtr<HTMLInputElement> input(hostInput());
262     HTMLElement* trackElement = sliderTrackElementOf(input.get());
263
264     if (!input->renderer() || !renderBox() || !trackElement->renderBox())
265         return;
266
267     input->setTextAsOfLastFormControlChangeEvent(input->value());
268     LayoutPoint offset = roundedLayoutPoint(input->renderer()->absoluteToLocal(point, UseTransforms));
269     bool isVertical = hasVerticalAppearance(input.get());
270     bool isLeftToRightDirection = renderBox()->style()->isLeftToRightDirection();
271     LayoutUnit trackSize;
272     LayoutUnit position;
273     LayoutUnit currentPosition;
274     // We need to calculate currentPosition from absolute points becaue the
275     // renderer for this node is usually on a layer and renderBox()->x() and
276     // y() are unusable.
277     // FIXME: This should probably respect transforms.
278     LayoutPoint absoluteThumbOrigin = renderBox()->absoluteBoundingBoxRectIgnoringTransforms().location();
279     LayoutPoint absoluteSliderContentOrigin = roundedLayoutPoint(input->renderer()->localToAbsolute());
280     IntRect trackBoundingBox = trackElement->renderer()->absoluteBoundingBoxRectIgnoringTransforms();
281     IntRect inputBoundingBox = input->renderer()->absoluteBoundingBoxRectIgnoringTransforms();
282     if (isVertical) {
283         trackSize = trackElement->renderBox()->contentHeight() - renderBox()->height();
284         position = offset.y() - renderBox()->height() / 2 - trackBoundingBox.y() + inputBoundingBox.y() - renderBox()->marginBottom();
285         currentPosition = absoluteThumbOrigin.y() - absoluteSliderContentOrigin.y();
286     } else {
287         trackSize = trackElement->renderBox()->contentWidth() - renderBox()->width();
288         position = offset.x() - renderBox()->width() / 2 - trackBoundingBox.x() + inputBoundingBox.x();
289         position -= isLeftToRightDirection ? renderBox()->marginLeft() : renderBox()->marginRight();
290         currentPosition = absoluteThumbOrigin.x() - absoluteSliderContentOrigin.x();
291     }
292     position = max<LayoutUnit>(0, min(position, trackSize));
293     const Decimal ratio = Decimal::fromDouble(static_cast<double>(position) / trackSize);
294     const Decimal fraction = isVertical || !isLeftToRightDirection ? Decimal(1) - ratio : ratio;
295     StepRange stepRange(input->createStepRange(RejectAny));
296     Decimal value = stepRange.clampValue(stepRange.valueFromProportion(fraction));
297
298 #if ENABLE(DATALIST_ELEMENT)
299     const LayoutUnit snappingThreshold = renderer()->theme()->sliderTickSnappingThreshold();
300     if (snappingThreshold > 0) {
301         Decimal closest = input->findClosestTickMarkValue(value);
302         if (closest.isFinite()) {
303             double closestFraction = stepRange.proportionFromValue(closest).toDouble();
304             double closestRatio = isVertical || !isLeftToRightDirection ? 1.0 - closestFraction : closestFraction;
305             LayoutUnit closestPosition = trackSize * closestRatio;
306             if ((closestPosition - position).abs() <= snappingThreshold)
307                 value = closest;
308         }
309     }
310 #endif
311
312     String valueString = serializeForNumberType(value);
313     if (valueString == input->value())
314         return;
315
316     // FIXME: This is no longer being set from renderer. Consider updating the method name.
317     input->setValueFromRenderer(valueString);
318     if (renderer())
319         renderer()->setNeedsLayout(true);
320     input->dispatchFormControlChangeEvent();
321 }
322
323 void SliderThumbElement::startDragging()
324 {
325     if (Frame* frame = document()->frame()) {
326         frame->eventHandler().setCapturingMouseEventsNode(this);
327         m_inDragMode = true;
328     }
329 }
330
331 void SliderThumbElement::stopDragging()
332 {
333     if (!m_inDragMode)
334         return;
335
336     if (Frame* frame = document()->frame())
337         frame->eventHandler().setCapturingMouseEventsNode(0);
338     m_inDragMode = false;
339     if (renderer())
340         renderer()->setNeedsLayout(true);
341 }
342
343 void SliderThumbElement::defaultEventHandler(Event* event)
344 {
345     if (!event->isMouseEvent()) {
346         HTMLDivElement::defaultEventHandler(event);
347         return;
348     }
349
350     // FIXME: Should handle this readonly/disabled check in more general way.
351     // Missing this kind of check is likely to occur elsewhere if adding it in each shadow element.
352     HTMLInputElement* input = hostInput();
353     if (!input || input->isDisabledOrReadOnly()) {
354         stopDragging();
355         HTMLDivElement::defaultEventHandler(event);
356         return;
357     }
358
359     MouseEvent* mouseEvent = static_cast<MouseEvent*>(event);
360     bool isLeftButton = mouseEvent->button() == LeftButton;
361     const AtomicString& eventType = event->type();
362
363     // We intentionally do not call event->setDefaultHandled() here because
364     // MediaControlTimelineElement::defaultEventHandler() wants to handle these
365     // mouse events.
366     if (eventType == eventNames().mousedownEvent && isLeftButton) {
367         startDragging();
368         return;
369     } else if (eventType == eventNames().mouseupEvent && isLeftButton) {
370         stopDragging();
371         return;
372     } else if (eventType == eventNames().mousemoveEvent) {
373         if (m_inDragMode)
374             setPositionFromPoint(mouseEvent->absoluteLocation());
375         return;
376     }
377
378     HTMLDivElement::defaultEventHandler(event);
379 }
380
381 bool SliderThumbElement::willRespondToMouseMoveEvents()
382 {
383     const HTMLInputElement* input = hostInput();
384     if (input && !input->isDisabledOrReadOnly() && m_inDragMode)
385         return true;
386
387     return HTMLDivElement::willRespondToMouseMoveEvents();
388 }
389
390 bool SliderThumbElement::willRespondToMouseClickEvents()
391 {
392     const HTMLInputElement* input = hostInput();
393     if (input && !input->isDisabledOrReadOnly())
394         return true;
395
396     return HTMLDivElement::willRespondToMouseClickEvents();
397 }
398
399 void SliderThumbElement::willDetachRenderers()
400 {
401     if (m_inDragMode) {
402         if (Frame* frame = document()->frame())
403             frame->eventHandler().setCapturingMouseEventsNode(0);
404     }
405 }
406
407 HTMLInputElement* SliderThumbElement::hostInput() const
408 {
409     // Only HTMLInputElement creates SliderThumbElement instances as its shadow nodes.
410     // So, shadowHost() must be an HTMLInputElement.
411     Element* host = shadowHost();
412     return host ? host->toInputElement() : 0;
413 }
414
415 static const AtomicString& sliderThumbShadowPseudoId()
416 {
417     DEFINE_STATIC_LOCAL(const AtomicString, sliderThumb, ("-webkit-slider-thumb", AtomicString::ConstructFromLiteral));
418     return sliderThumb;
419 }
420
421 static const AtomicString& mediaSliderThumbShadowPseudoId()
422 {
423     DEFINE_STATIC_LOCAL(const AtomicString, mediaSliderThumb, ("-webkit-media-slider-thumb", AtomicString::ConstructFromLiteral));
424     return mediaSliderThumb;
425 }
426
427 const AtomicString& SliderThumbElement::shadowPseudoId() const
428 {
429     HTMLInputElement* input = hostInput();
430     if (!input)
431         return sliderThumbShadowPseudoId();
432
433     RenderStyle* sliderStyle = input->renderer()->style();
434     switch (sliderStyle->appearance()) {
435     case MediaSliderPart:
436     case MediaSliderThumbPart:
437     case MediaVolumeSliderPart:
438     case MediaVolumeSliderThumbPart:
439     case MediaFullScreenVolumeSliderPart:
440     case MediaFullScreenVolumeSliderThumbPart:
441         return mediaSliderThumbShadowPseudoId();
442     default:
443         return sliderThumbShadowPseudoId();
444     }
445 }
446
447 // --------------------------------
448
449 inline SliderContainerElement::SliderContainerElement(Document* document)
450     : HTMLDivElement(HTMLNames::divTag, document)
451 {
452 }
453
454 PassRefPtr<SliderContainerElement> SliderContainerElement::create(Document* document)
455 {
456     return adoptRef(new SliderContainerElement(document));
457 }
458
459 RenderObject* SliderContainerElement::createRenderer(RenderArena* arena, RenderStyle*)
460 {
461     return new (arena) RenderSliderContainer(this);
462 }
463
464 const AtomicString& SliderContainerElement::shadowPseudoId() const
465 {
466     DEFINE_STATIC_LOCAL(const AtomicString, mediaSliderContainer, ("-webkit-media-slider-container", AtomicString::ConstructFromLiteral));
467     DEFINE_STATIC_LOCAL(const AtomicString, sliderContainer, ("-webkit-slider-container", AtomicString::ConstructFromLiteral));
468
469     HTMLInputElement* input = shadowHost()->toInputElement();
470     if (!input)
471         return sliderContainer;
472
473     RenderStyle* sliderStyle = input->renderer()->style();
474     switch (sliderStyle->appearance()) {
475     case MediaSliderPart:
476     case MediaSliderThumbPart:
477     case MediaVolumeSliderPart:
478     case MediaVolumeSliderThumbPart:
479     case MediaFullScreenVolumeSliderPart:
480     case MediaFullScreenVolumeSliderThumbPart:
481         return mediaSliderContainer;
482     default:
483         return sliderContainer;
484     }
485 }
486
487 }