WebCore: https://bugs.webkit.org/show_bug.cgi?id=20329, shadows and reflections incor...
[WebKit-https.git] / WebCore / rendering / RenderSlider.cpp
1 /*
2  * Copyright (C) 2006, 2007, 2008, 2009 Apple Inc. All rights reserved.
3  *
4  * This library is free software; you can redistribute it and/or
5  * modify it under the terms of the GNU Library General Public
6  * License as published by the Free Software Foundation; either
7  * version 2 of the License, or (at your option) any later version.
8  *
9  * This library is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
12  * Library General Public License for more details.
13  *
14  * You should have received a copy of the GNU Library General Public License
15  * along with this library; see the file COPYING.LIB.  If not, write to
16  * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
17  * Boston, MA 02110-1301, USA.
18  *
19  */
20
21 #include "config.h"
22 #include "RenderSlider.h"
23
24 #include "CSSPropertyNames.h"
25 #include "Document.h"
26 #include "Event.h"
27 #include "EventHandler.h"
28 #include "EventNames.h"
29 #include "Frame.h"
30 #include "HTMLInputElement.h"
31 #include "HTMLDivElement.h"
32 #include "HTMLNames.h"
33 #include "MediaControlElements.h"
34 #include "MouseEvent.h"
35 #include "RenderLayer.h"
36 #include "RenderTheme.h"
37 #include "RenderView.h"
38 #include <wtf/MathExtras.h>
39
40 using std::min;
41
42 namespace WebCore {
43
44 using namespace HTMLNames;
45
46 static const int defaultTrackLength = 129;
47
48 // FIXME: The SliderRange class and functions are entirely based on the DOM,
49 // and could be put with HTMLInputElement (possibly with a new name) instead of here.
50 struct SliderRange {
51     bool isIntegral;
52     double minimum;
53     double maximum;
54
55     explicit SliderRange(HTMLInputElement*);
56     double clampValue(double value);
57
58     // Map value into 0-1 range
59     double proportionFromValue(double value)
60     {
61         if (minimum == maximum)
62             return 0;
63
64         return (value - minimum) / (maximum - minimum);
65     }
66     
67     // Map from 0-1 range to value
68     double valueFromProportion(double proportion)
69     {
70         return minimum + proportion * (maximum - minimum);
71     }
72     
73     double valueFromElement(HTMLInputElement*, bool* wasClamped = 0);
74 };
75
76 SliderRange::SliderRange(HTMLInputElement* element)
77 {
78     // FIXME: What's the right way to handle an integral range with non-integral minimum and maximum?
79     // Currently values are guaranteed to be integral but could be outside the range in that case.
80
81     isIntegral = !equalIgnoringCase(element->getAttribute(precisionAttr), "float");
82
83     // FIXME: This treats maximum strings that can't be parsed as 0, but perhaps 100 would be more appropriate.
84     const AtomicString& maxString = element->getAttribute(maxAttr);
85     maximum = maxString.isNull() ? 100.0 : maxString.toDouble();
86
87     // If the maximum is smaller, use it as the minimum.
88     minimum = min(element->getAttribute(minAttr).toDouble(), maximum);
89 }
90
91 double SliderRange::clampValue(double value)
92 {
93     double clampedValue = max(minimum, min(value, maximum));
94     return isIntegral ? round(clampedValue) : clampedValue;
95 }
96
97 double SliderRange::valueFromElement(HTMLInputElement* element, bool* wasClamped)
98 {
99     String valueString = element->value();
100     double oldValue = valueString.isNull() ? (minimum + maximum) / 2 : valueString.toDouble();
101     double newValue = clampValue(oldValue);
102
103     if (wasClamped)
104         *wasClamped = valueString.isNull() || newValue != oldValue;
105
106     return newValue;
107 }
108
109 // Returns a value between 0 and 1.
110 // As with SliderRange, this could be on HTMLInputElement instead of here.
111 static double sliderPosition(HTMLInputElement* element)
112 {
113     SliderRange range(element);
114     return range.proportionFromValue(range.valueFromElement(element));
115 }
116
117 class SliderThumbElement : public HTMLDivElement {
118 public:
119     SliderThumbElement(Document*, Node* shadowParent);
120     
121     bool inDragMode() const { return m_inDragMode; }
122
123     virtual void defaultEventHandler(Event*);
124     virtual void detach();
125
126 private:        
127     virtual bool isShadowNode() const { return true; }
128     virtual Node* shadowParentNode() { return m_shadowParent; }
129
130     FloatPoint m_offsetToThumb;
131     Node* m_shadowParent;
132     bool m_inDragMode;
133 };
134
135 SliderThumbElement::SliderThumbElement(Document* document, Node* shadowParent)
136     : HTMLDivElement(divTag, document)
137     , m_shadowParent(shadowParent)
138     , m_inDragMode(false)
139 {
140 }
141
142 void SliderThumbElement::defaultEventHandler(Event* event)
143 {
144     if (!event->isMouseEvent()) {
145         HTMLDivElement::defaultEventHandler(event);
146         return;
147     }
148
149     MouseEvent* mouseEvent = static_cast<MouseEvent*>(event);
150     bool isLeftButton = mouseEvent->button() == LeftButton;
151     const AtomicString& eventType = event->type();
152
153     if (eventType == eventNames().mousedownEvent && isLeftButton) {
154         if (document()->frame() && renderer()) {
155             RenderSlider* slider = toRenderSlider(renderer()->parent());
156             if (slider) {
157                 if (slider->mouseEventIsInThumb(mouseEvent)) {
158                     // We selected the thumb, we want the cursor to always stay at
159                     // the same position relative to the thumb.
160                     m_offsetToThumb = slider->mouseEventOffsetToThumb(mouseEvent);
161                 } else {
162                     // We are outside the thumb, move the thumb to the point were
163                     // we clicked. We'll be exactly at the center of the thumb.
164                     m_offsetToThumb.setX(0);
165                     m_offsetToThumb.setY(0);
166                 }
167
168                 m_inDragMode = true;
169                 document()->frame()->eventHandler()->setCapturingMouseEventsNode(m_shadowParent);
170                 event->setDefaultHandled();
171                 return;
172             }
173         }
174     } else if (eventType == eventNames().mouseupEvent && isLeftButton) {
175         if (m_inDragMode) {
176             if (Frame* frame = document()->frame())
177                 frame->eventHandler()->setCapturingMouseEventsNode(0);      
178             m_inDragMode = false;
179             event->setDefaultHandled();
180             return;
181         }
182     } else if (eventType == eventNames().mousemoveEvent) {
183         if (m_inDragMode && renderer() && renderer()->parent()) {
184             RenderSlider* slider = toRenderSlider(renderer()->parent());
185             if (slider) {
186                 FloatPoint curPoint = slider->absoluteToLocal(mouseEvent->absoluteLocation(), false, true);
187                 IntPoint eventOffset(curPoint.x() + m_offsetToThumb.x(), curPoint.y() + m_offsetToThumb.y());
188                 slider->setValueForPosition(slider->positionForOffset(eventOffset));
189                 event->setDefaultHandled();
190                 return;
191             }
192         }
193     }
194
195     HTMLDivElement::defaultEventHandler(event);
196 }
197
198 void SliderThumbElement::detach()
199 {
200     if (m_inDragMode) {
201         if (Frame* frame = document()->frame())
202             frame->eventHandler()->setCapturingMouseEventsNode(0);      
203     }
204     HTMLDivElement::detach();
205 }
206
207 RenderSlider::RenderSlider(HTMLInputElement* element)
208     : RenderBlock(element)
209 {
210 }
211
212 RenderSlider::~RenderSlider()
213 {
214     if (m_thumb)
215         m_thumb->detach();
216 }
217
218 int RenderSlider::baselinePosition(bool, bool) const
219 {
220     return height() + marginTop();
221 }
222
223 void RenderSlider::calcPrefWidths()
224 {
225     m_minPrefWidth = 0;
226     m_maxPrefWidth = 0;
227
228     if (style()->width().isFixed() && style()->width().value() > 0)
229         m_minPrefWidth = m_maxPrefWidth = calcContentBoxWidth(style()->width().value());
230     else
231         m_maxPrefWidth = defaultTrackLength * style()->effectiveZoom();
232
233     if (style()->minWidth().isFixed() && style()->minWidth().value() > 0) {
234         m_maxPrefWidth = max(m_maxPrefWidth, calcContentBoxWidth(style()->minWidth().value()));
235         m_minPrefWidth = max(m_minPrefWidth, calcContentBoxWidth(style()->minWidth().value()));
236     } else if (style()->width().isPercent() || (style()->width().isAuto() && style()->height().isPercent()))
237         m_minPrefWidth = 0;
238     else
239         m_minPrefWidth = m_maxPrefWidth;
240     
241     if (style()->maxWidth().isFixed() && style()->maxWidth().value() != undefinedLength) {
242         m_maxPrefWidth = min(m_maxPrefWidth, calcContentBoxWidth(style()->maxWidth().value()));
243         m_minPrefWidth = min(m_minPrefWidth, calcContentBoxWidth(style()->maxWidth().value()));
244     }
245
246     int toAdd = paddingLeft() + paddingRight() + borderLeft() + borderRight();
247     m_minPrefWidth += toAdd;
248     m_maxPrefWidth += toAdd;
249
250     setPrefWidthsDirty(false); 
251 }
252
253 void RenderSlider::styleDidChange(StyleDifference diff, const RenderStyle* oldStyle)
254 {
255     RenderBlock::styleDidChange(diff, oldStyle);
256
257     if (m_thumb)
258         m_thumb->renderer()->setStyle(createThumbStyle(style()));
259
260     setReplaced(isInline());
261 }
262
263 PassRefPtr<RenderStyle> RenderSlider::createThumbStyle(const RenderStyle* parentStyle)
264 {
265     RefPtr<RenderStyle> style;
266     RenderStyle* pseudoStyle = getCachedPseudoStyle(SLIDER_THUMB);
267     if (pseudoStyle)
268         // We may be sharing style with another slider, but we must not share the thumb style.
269         style = RenderStyle::clone(pseudoStyle);
270     else
271         style = RenderStyle::create();
272
273     if (parentStyle)
274         style->inheritFrom(parentStyle);
275
276     style->setDisplay(BLOCK);
277
278     if (parentStyle->appearance() == SliderVerticalPart)
279         style->setAppearance(SliderThumbVerticalPart);
280     else if (parentStyle->appearance() == SliderHorizontalPart)
281         style->setAppearance(SliderThumbHorizontalPart);
282     else if (parentStyle->appearance() == MediaSliderPart)
283         style->setAppearance(MediaSliderThumbPart);
284
285     return style.release();
286 }
287
288 IntRect RenderSlider::thumbRect()
289 {
290     if (!m_thumb)
291         return IntRect();
292
293     IntRect thumbRect;
294     RenderBox* thumb = toRenderBox(m_thumb->renderer());
295
296     thumbRect.setWidth(thumb->style()->width().calcMinValue(contentWidth()));
297     thumbRect.setHeight(thumb->style()->height().calcMinValue(contentHeight()));
298
299     double fraction = sliderPosition(static_cast<HTMLInputElement*>(node()));
300     IntRect contentRect = contentBoxRect();
301     if (style()->appearance() == SliderVerticalPart) {
302         thumbRect.setX(contentRect.x() + (contentRect.width() - thumbRect.width()) / 2);
303         thumbRect.setY(contentRect.y() + static_cast<int>(nextafter((contentRect.height() - thumbRect.height()) + 1, 0) * (1 - fraction)));
304     } else {
305         thumbRect.setX(contentRect.x() + static_cast<int>(nextafter((contentRect.width() - thumbRect.width()) + 1, 0) * fraction));
306         thumbRect.setY(contentRect.y() + (contentRect.height() - thumbRect.height()) / 2);
307     }
308
309     return thumbRect;
310 }
311
312 void RenderSlider::layout()
313 {
314     ASSERT(needsLayout());
315
316     RenderBox* thumb = m_thumb ? toRenderBox(m_thumb->renderer()) : 0;
317
318     IntSize baseSize(borderLeft() + paddingLeft() + paddingRight() + borderRight(),
319         borderTop() + paddingTop() + paddingBottom() + borderBottom());
320
321     if (thumb) {
322         // Allow the theme to set the size of the thumb.
323         if (thumb->style()->hasAppearance()) {
324             // FIXME: This should pass the style, not the renderer, to the theme.
325             theme()->adjustSliderThumbSize(thumb);
326         }
327
328         baseSize.expand(thumb->style()->width().calcMinValue(0), thumb->style()->height().calcMinValue(0));
329     }
330
331     LayoutRepainter repainter(*this, checkForRepaintDuringLayout());
332
333     IntSize oldSize = size();
334
335     setSize(baseSize);
336     calcWidth();
337     calcHeight();
338
339     if (thumb) {
340         if (oldSize != size())
341             thumb->setChildNeedsLayout(true, false);
342
343         LayoutStateMaintainer statePusher(view(), this, size());
344
345         IntRect oldThumbRect = thumb->frameRect();
346
347         thumb->layoutIfNeeded();
348
349         IntRect rect = thumbRect();
350         thumb->setFrameRect(rect);
351         if (thumb->checkForRepaintDuringLayout())
352             thumb->repaintDuringLayoutIfMoved(oldThumbRect);
353
354         statePusher.pop();
355     }
356
357     addOverflowFromChild(thumb);
358
359     repainter.repaintAfterLayout();    
360
361     setNeedsLayout(false);
362 }
363
364 void RenderSlider::updateFromElement()
365 {
366     HTMLInputElement* element = static_cast<HTMLInputElement*>(node());
367
368     // Send the value back to the element if the range changes it.
369     SliderRange range(element);
370     bool clamped;
371     double value = range.valueFromElement(element, &clamped);
372     if (clamped)
373         element->setValueFromRenderer(String::number(value));
374
375     // Layout will take care of the thumb's size and position.
376     if (!m_thumb) {
377         m_thumb = new SliderThumbElement(document(), node());
378         RefPtr<RenderStyle> thumbStyle = createThumbStyle(style());
379         m_thumb->setRenderer(m_thumb->createRenderer(renderArena(), thumbStyle.get()));
380         m_thumb->renderer()->setStyle(thumbStyle.release());
381         m_thumb->setAttached();
382         m_thumb->setInDocument(true);
383         addChild(m_thumb->renderer());
384     }
385     setNeedsLayout(true);
386 }
387
388 bool RenderSlider::mouseEventIsInThumb(MouseEvent* evt)
389 {
390     if (!m_thumb || !m_thumb->renderer())
391         return false;
392
393 #if ENABLE(VIDEO)
394     if (style()->appearance() == MediaSliderPart) {
395         MediaControlInputElement *sliderThumb = static_cast<MediaControlInputElement*>(m_thumb->renderer()->node());
396         return sliderThumb->hitTest(evt->absoluteLocation());
397     }
398 #endif
399
400     FloatPoint localPoint = m_thumb->renderBox()->absoluteToLocal(evt->absoluteLocation(), false, true);
401     IntRect thumbBounds = m_thumb->renderBox()->borderBoxRect();
402     return thumbBounds.contains(roundedIntPoint(localPoint));
403 }
404
405 FloatPoint RenderSlider::mouseEventOffsetToThumb(MouseEvent* evt)
406 {
407     ASSERT(m_thumb && m_thumb->renderer());
408     FloatPoint localPoint = m_thumb->renderBox()->absoluteToLocal(evt->absoluteLocation(), false, true);
409     IntRect thumbBounds = m_thumb->renderBox()->borderBoxRect();
410     FloatPoint offset;
411     offset.setX(thumbBounds.x() + thumbBounds.width() / 2 - localPoint.x());
412     offset.setY(thumbBounds.y() + thumbBounds.height() / 2 - localPoint.y());
413     return offset;
414 }
415
416 void RenderSlider::setValueForPosition(int position)
417 {
418     if (!m_thumb || !m_thumb->renderer())
419         return;
420
421     HTMLInputElement* element = static_cast<HTMLInputElement*>(node());
422
423     // Calculate the new value based on the position, and send it to the element.
424     SliderRange range(element);
425     double fraction = static_cast<double>(position) / trackSize();
426     if (style()->appearance() == SliderVerticalPart)
427         fraction = 1 - fraction;
428     double value = range.clampValue(range.valueFromProportion(fraction));
429     element->setValueFromRenderer(String::number(value));
430
431     // Also update the position if appropriate.
432     if (position != currentPosition()) {
433         setNeedsLayout(true);
434
435         // FIXME: It seems like this could send extra change events if the same value is set
436         // multiple times with no layout in between.
437         element->dispatchFormControlChangeEvent();
438     }
439 }
440
441 int RenderSlider::positionForOffset(const IntPoint& p)
442 {
443     if (!m_thumb || !m_thumb->renderer())
444         return 0;
445
446     int position;
447     if (style()->appearance() == SliderVerticalPart)
448         position = p.y() - m_thumb->renderBox()->height() / 2;
449     else
450         position = p.x() - m_thumb->renderBox()->width() / 2;
451     
452     return max(0, min(position, trackSize()));
453 }
454
455 int RenderSlider::currentPosition()
456 {
457     ASSERT(m_thumb);
458     ASSERT(m_thumb->renderer());
459
460     if (style()->appearance() == SliderVerticalPart)
461         return toRenderBox(m_thumb->renderer())->y() - contentBoxRect().y();
462     return toRenderBox(m_thumb->renderer())->x() - contentBoxRect().x();
463 }
464
465 int RenderSlider::trackSize()
466 {
467     ASSERT(m_thumb);
468     ASSERT(m_thumb->renderer());
469
470     if (style()->appearance() == SliderVerticalPart)
471         return contentHeight() - m_thumb->renderBox()->height();
472     return contentWidth() - m_thumb->renderBox()->width();
473 }
474
475 void RenderSlider::forwardEvent(Event* event)
476 {
477     if (event->isMouseEvent()) {
478         MouseEvent* mouseEvent = static_cast<MouseEvent*>(event);
479         if (event->type() == eventNames().mousedownEvent && mouseEvent->button() == LeftButton) {
480             if (!mouseEventIsInThumb(mouseEvent)) {
481                 IntPoint eventOffset = roundedIntPoint(absoluteToLocal(mouseEvent->absoluteLocation(), false, true));
482                 setValueForPosition(positionForOffset(eventOffset));
483             }
484         }
485     }
486
487     m_thumb->defaultEventHandler(event);
488 }
489
490 bool RenderSlider::inDragMode() const
491 {
492     return m_thumb && m_thumb->inDragMode();
493 }
494
495 } // namespace WebCore