f54ccb815e68d543a26caa6b23096d0214da1522
[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     double valueFromElement(HTMLInputElement*, bool* wasClamped = 0);
58 };
59
60 SliderRange::SliderRange(HTMLInputElement* element)
61 {
62     // FIXME: What's the right way to handle an integral range with non-integral minimum and maximum?
63     // Currently values are guaranteed to be integral but could be outside the range in that case.
64
65     isIntegral = !equalIgnoringCase(element->getAttribute(precisionAttr), "float");
66
67     // FIXME: This treats maximum strings that can't be parsed as 0, but perhaps 100 would be more appropriate.
68     const AtomicString& maxString = element->getAttribute(maxAttr);
69     maximum = maxString.isNull() ? 100.0 : maxString.toDouble();
70
71     // If the maximum is smaller, use it as the minimum.
72     minimum = min(element->getAttribute(minAttr).toDouble(), maximum);
73 }
74
75 double SliderRange::clampValue(double value)
76 {
77     double clampedValue = max(minimum, min(value, maximum));
78     return isIntegral ? round(clampedValue) : clampedValue;
79 }
80
81 double SliderRange::valueFromElement(HTMLInputElement* element, bool* wasClamped)
82 {
83     String valueString = element->value();
84     double oldValue = valueString.isNull() ? (minimum + maximum) / 2 : valueString.toDouble();
85     double newValue = clampValue(oldValue);
86
87     if (wasClamped)
88         *wasClamped = valueString.isNull() || newValue != oldValue;
89
90     return newValue;
91 }
92
93 // Returns a value between 0 and 1.
94 // As with SliderRange, this could be on HTMLInputElement instead of here.
95 static double sliderPosition(HTMLInputElement* element)
96 {
97     SliderRange range(element);
98     double value = range.valueFromElement(element);
99     return (value - range.minimum) / (range.maximum - range.minimum);
100 }
101
102 class SliderThumbElement : public HTMLDivElement {
103 public:
104     SliderThumbElement(Document*, Node* shadowParent);
105     
106     bool inDragMode() const { return m_inDragMode; }
107
108     virtual void defaultEventHandler(Event*);
109
110 private:        
111     virtual bool isShadowNode() const { return true; }
112     virtual Node* shadowParentNode() { return m_shadowParent; }
113
114     Node* m_shadowParent;
115     FloatPoint m_initialClickPoint;       // initial click point in RenderSlider-local coordinates
116     int m_initialPosition;
117     bool m_inDragMode;
118 };
119
120 SliderThumbElement::SliderThumbElement(Document* document, Node* shadowParent)
121     : HTMLDivElement(divTag, document)
122     , m_shadowParent(shadowParent)
123     , m_initialPosition(0)
124     , m_inDragMode(false)
125 {
126 }
127
128 void SliderThumbElement::defaultEventHandler(Event* event)
129 {
130     const AtomicString& eventType = event->type();
131     if (eventType == eventNames().mousedownEvent && event->isMouseEvent() && static_cast<MouseEvent*>(event)->button() == LeftButton) {
132         MouseEvent* mouseEvent = static_cast<MouseEvent*>(event);
133         RenderSlider* slider;
134         if (document()->frame() && renderer() &&
135                 (slider = static_cast<RenderSlider*>(renderer()->parent())) &&
136                 slider->mouseEventIsInThumb(mouseEvent)) {
137             
138             // Cache the initial point where the mouse down occurred, in slider coordinates
139             m_initialClickPoint = slider->absoluteToLocal(mouseEvent->absoluteLocation(), false, true);
140             // Cache the initial position of the thumb.
141             m_initialPosition = slider->currentPosition();
142             m_inDragMode = true;
143             
144             document()->frame()->eventHandler()->setCapturingMouseEventsNode(m_shadowParent);
145             
146             event->setDefaultHandled();
147             return;
148         }
149     } else if (eventType == eventNames().mouseupEvent && event->isMouseEvent() && static_cast<MouseEvent*>(event)->button() == LeftButton) {
150         if (m_inDragMode) {
151             if (Frame* frame = document()->frame())
152                 frame->eventHandler()->setCapturingMouseEventsNode(0);      
153             m_inDragMode = false;
154             event->setDefaultHandled();
155             return;
156         }
157     } else if (eventType == eventNames().mousemoveEvent && event->isMouseEvent()) {
158         if (m_inDragMode && renderer() && renderer()->parent()) {
159             // Move the slider
160             MouseEvent* mouseEvent = static_cast<MouseEvent*>(event);
161             RenderSlider* slider = static_cast<RenderSlider*>(renderer()->parent());
162
163             FloatPoint curPoint = slider->absoluteToLocal(mouseEvent->absoluteLocation(), false, true);
164             IntPoint eventOffset(m_initialPosition + curPoint.x() - m_initialClickPoint.x() + renderBox()->width() / 2, 
165                 m_initialPosition + curPoint.y() - m_initialClickPoint.y() + renderBox()->height() / 2);
166             slider->setValueForPosition(slider->positionForOffset(eventOffset));
167             event->setDefaultHandled();
168             return;
169         }
170     }
171
172     HTMLDivElement::defaultEventHandler(event);
173 }
174
175 RenderSlider::RenderSlider(HTMLInputElement* element)
176     : RenderBlock(element)
177 {
178 }
179
180 RenderSlider::~RenderSlider()
181 {
182     if (m_thumb)
183         m_thumb->detach();
184 }
185
186 int RenderSlider::baselinePosition(bool, bool) const
187 {
188     return height() + marginTop();
189 }
190
191 void RenderSlider::calcPrefWidths()
192 {
193     m_minPrefWidth = 0;
194     m_maxPrefWidth = 0;
195
196     if (style()->width().isFixed() && style()->width().value() > 0)
197         m_minPrefWidth = m_maxPrefWidth = calcContentBoxWidth(style()->width().value());
198     else
199         m_maxPrefWidth = defaultTrackLength * style()->effectiveZoom();
200
201     if (style()->minWidth().isFixed() && style()->minWidth().value() > 0) {
202         m_maxPrefWidth = max(m_maxPrefWidth, calcContentBoxWidth(style()->minWidth().value()));
203         m_minPrefWidth = max(m_minPrefWidth, calcContentBoxWidth(style()->minWidth().value()));
204     } else if (style()->width().isPercent() || (style()->width().isAuto() && style()->height().isPercent()))
205         m_minPrefWidth = 0;
206     else
207         m_minPrefWidth = m_maxPrefWidth;
208     
209     if (style()->maxWidth().isFixed() && style()->maxWidth().value() != undefinedLength) {
210         m_maxPrefWidth = min(m_maxPrefWidth, calcContentBoxWidth(style()->maxWidth().value()));
211         m_minPrefWidth = min(m_minPrefWidth, calcContentBoxWidth(style()->maxWidth().value()));
212     }
213
214     int toAdd = paddingLeft() + paddingRight() + borderLeft() + borderRight();
215     m_minPrefWidth += toAdd;
216     m_maxPrefWidth += toAdd;
217
218     setPrefWidthsDirty(false); 
219 }
220
221 void RenderSlider::styleDidChange(StyleDifference diff, const RenderStyle* oldStyle)
222 {
223     RenderBlock::styleDidChange(diff, oldStyle);
224
225     if (m_thumb)
226         m_thumb->renderer()->setStyle(createThumbStyle(style()));
227
228     setReplaced(isInline());
229 }
230
231 PassRefPtr<RenderStyle> RenderSlider::createThumbStyle(const RenderStyle* parentStyle)
232 {
233     RefPtr<RenderStyle> style;
234     RenderStyle* pseudoStyle = getCachedPseudoStyle(SLIDER_THUMB);
235     if (pseudoStyle)
236         // We may be sharing style with another slider, but we must not share the thumb style.
237         style = RenderStyle::clone(pseudoStyle);
238     else
239         style = RenderStyle::create();
240
241     if (parentStyle)
242         style->inheritFrom(parentStyle);
243
244     style->setDisplay(BLOCK);
245
246     if (parentStyle->appearance() == SliderVerticalPart)
247         style->setAppearance(SliderThumbVerticalPart);
248     else if (parentStyle->appearance() == SliderHorizontalPart)
249         style->setAppearance(SliderThumbHorizontalPart);
250     else if (parentStyle->appearance() == MediaSliderPart)
251         style->setAppearance(MediaSliderThumbPart);
252
253     return style.release();
254 }
255
256 void RenderSlider::layout()
257 {
258     ASSERT(needsLayout());
259
260     RenderBox* thumb = m_thumb ? toRenderBox(m_thumb->renderer()) : 0;
261
262     IntSize baseSize(borderLeft() + paddingLeft() + paddingRight() + borderRight(),
263         borderTop() + paddingTop() + paddingBottom() + borderBottom());
264
265     if (thumb) {
266         // Allow the theme to set the size of the thumb.
267         if (thumb->style()->hasAppearance()) {
268             // FIXME: This should pass the style, not the renderer, to the theme.
269             theme()->adjustSliderThumbSize(thumb);
270         }
271
272         baseSize.expand(thumb->style()->width().calcMinValue(0), thumb->style()->height().calcMinValue(0));
273     }
274
275     LayoutRepainter repainter(*this, checkForRepaintDuringLayout());
276
277     IntSize oldSize = size();
278
279     setSize(baseSize);
280     calcWidth();
281     calcHeight();
282
283     IntRect overflowRect(IntPoint(), size());
284
285     if (thumb) {
286         if (oldSize != size())
287             thumb->setChildNeedsLayout(true, false);
288
289         LayoutStateMaintainer statePusher(view(), this, size());
290
291         IntRect oldThumbRect = thumb->frameRect();
292
293         thumb->layoutIfNeeded();
294
295         IntRect thumbRect;
296
297         thumbRect.setWidth(thumb->style()->width().calcMinValue(contentWidth()));
298         thumbRect.setHeight(thumb->style()->height().calcMinValue(contentHeight()));
299
300         double fraction = sliderPosition(static_cast<HTMLInputElement*>(node()));
301         IntRect contentRect = contentBoxRect();
302         if (style()->appearance() == SliderVerticalPart) {
303             thumbRect.setX(contentRect.x() + (contentRect.width() - thumbRect.width()) / 2);
304             thumbRect.setY(contentRect.y() + static_cast<int>(nextafter((contentRect.height() - thumbRect.height()) + 1, 0) * (1 - fraction)));
305         } else {
306             thumbRect.setX(contentRect.x() + static_cast<int>(nextafter((contentRect.width() - thumbRect.width()) + 1, 0) * fraction));
307             thumbRect.setY(contentRect.y() + (contentRect.height() - thumbRect.height()) / 2);
308         }
309
310         thumb->setFrameRect(thumbRect);
311
312         if (thumb->checkForRepaintDuringLayout())
313             thumb->repaintDuringLayoutIfMoved(oldThumbRect);
314
315         statePusher.pop();
316
317         IntRect thumbOverflowRect = thumb->overflowRect();
318         thumbOverflowRect.move(thumb->x(), thumb->y());
319         overflowRect.unite(thumbOverflowRect);
320     }
321
322     // FIXME: m_overflowWidth and m_overflowHeight should be renamed
323     // m_overflowRight and m_overflowBottom.
324     m_overflowLeft = overflowRect.x();
325     m_overflowTop = overflowRect.y();
326     m_overflowWidth = overflowRect.right();
327     m_overflowHeight = overflowRect.bottom();
328
329     repainter.repaintAfterLayout();    
330
331     setNeedsLayout(false);
332 }
333
334 void RenderSlider::updateFromElement()
335 {
336     HTMLInputElement* element = static_cast<HTMLInputElement*>(node());
337
338     // Send the value back to the element if the range changes it.
339     SliderRange range(element);
340     bool clamped;
341     double value = range.valueFromElement(element, &clamped);
342     if (clamped)
343         element->setValueFromRenderer(String::number(value));
344
345     // Layout will take care of the thumb's size and position.
346     if (!m_thumb) {
347         m_thumb = new SliderThumbElement(document(), node());
348         RefPtr<RenderStyle> thumbStyle = createThumbStyle(style());
349         m_thumb->setRenderer(m_thumb->createRenderer(renderArena(), thumbStyle.get()));
350         m_thumb->renderer()->setStyle(thumbStyle.release());
351         m_thumb->setAttached();
352         m_thumb->setInDocument(true);
353         addChild(m_thumb->renderer());
354     }
355     setNeedsLayout(true);
356 }
357
358 bool RenderSlider::mouseEventIsInThumb(MouseEvent* evt)
359 {
360     if (!m_thumb || !m_thumb->renderer())
361         return false;
362
363 #if ENABLE(VIDEO)
364     if (style()->appearance() == MediaSliderPart) {
365         MediaControlInputElement *sliderThumb = static_cast<MediaControlInputElement*>(m_thumb->renderer()->node());
366         return sliderThumb->hitTest(evt->absoluteLocation());
367     }
368 #endif
369
370     FloatPoint localPoint = m_thumb->renderBox()->absoluteToLocal(evt->absoluteLocation(), false, true);
371     IntRect thumbBounds = m_thumb->renderBox()->borderBoxRect();
372     return thumbBounds.contains(roundedIntPoint(localPoint));
373 }
374
375 void RenderSlider::setValueForPosition(int position)
376 {
377     if (!m_thumb || !m_thumb->renderer())
378         return;
379
380     HTMLInputElement* element = static_cast<HTMLInputElement*>(node());
381
382     // Calculate the new value based on the position, and send it to the element.
383     SliderRange range(element);
384     double fraction = static_cast<double>(position) / trackSize();
385     if (style()->appearance() == SliderVerticalPart)
386         fraction = 1 - fraction;
387     double value = range.clampValue(range.minimum + fraction * (range.maximum - range.minimum));
388     element->setValueFromRenderer(String::number(value));
389
390     // Also update the position if appropriate.
391     if (position != currentPosition()) {
392         setNeedsLayout(true);
393
394         // FIXME: It seems like this could send extra change events if the same value is set
395         // multiple times with no layout in between.
396         element->onChange();
397     }
398 }
399
400 int RenderSlider::positionForOffset(const IntPoint& p)
401 {
402     if (!m_thumb || !m_thumb->renderer())
403         return 0;
404
405     int position;
406     if (style()->appearance() == SliderVerticalPart)
407         position = p.y() - m_thumb->renderBox()->height() / 2;
408     else
409         position = p.x() - m_thumb->renderBox()->width() / 2;
410     
411     return max(0, min(position, trackSize()));
412 }
413
414 int RenderSlider::currentPosition()
415 {
416     ASSERT(m_thumb);
417     ASSERT(m_thumb->renderer());
418
419     if (style()->appearance() == SliderVerticalPart)
420         return toRenderBox(m_thumb->renderer())->y() - contentBoxRect().y();
421     return toRenderBox(m_thumb->renderer())->x() - contentBoxRect().x();
422 }
423
424 int RenderSlider::trackSize()
425 {
426     ASSERT(m_thumb);
427     ASSERT(m_thumb->renderer());
428
429     if (style()->appearance() == SliderVerticalPart)
430         return contentHeight() - m_thumb->renderBox()->height();
431     return contentWidth() - m_thumb->renderBox()->width();
432 }
433
434 void RenderSlider::forwardEvent(Event* event)
435 {
436     if (event->isMouseEvent()) {
437         MouseEvent* mouseEvent = static_cast<MouseEvent*>(event);
438         if (event->type() == eventNames().mousedownEvent && mouseEvent->button() == LeftButton) {
439             if (!mouseEventIsInThumb(mouseEvent)) {
440                 IntPoint eventOffset = roundedIntPoint(absoluteToLocal(mouseEvent->absoluteLocation(), false, true));
441                 setValueForPosition(positionForOffset(eventOffset));
442             }
443         }
444     }
445
446     m_thumb->defaultEventHandler(event);
447 }
448
449 bool RenderSlider::inDragMode() const
450 {
451     return m_thumb && m_thumb->inDragMode();
452 }
453
454 } // namespace WebCore