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 #ifdef ANDROID_LAYOUT
41 #include "Settings.h"
42 #endif
43
44 using std::min;
45
46 namespace WebCore {
47
48 using namespace HTMLNames;
49
50 static const int defaultTrackLength = 129;
51
52 // FIXME: The SliderRange class and functions are entirely based on the DOM,
53 // and could be put with HTMLInputElement (possibly with a new name) instead of here.
54 struct SliderRange {
55 bool hasStep;
56 double step;
57 double minimum;
58 double maximum; // maximum must be >= minimum.
59
60 explicit SliderRange(HTMLInputElement*);
61 double clampValue(double value);
62
63 // Map value into 0-1 range
proportionFromValueWebCore::SliderRange64 double proportionFromValue(double value)
65 {
66 if (minimum == maximum)
67 return 0;
68
69 return (value - minimum) / (maximum - minimum);
70 }
71
72 // Map from 0-1 range to value
valueFromProportionWebCore::SliderRange73 double valueFromProportion(double proportion)
74 {
75 return minimum + proportion * (maximum - minimum);
76 }
77
78 double valueFromElement(HTMLInputElement*, bool* wasClamped = 0);
79 };
80
SliderRange(HTMLInputElement * element)81 SliderRange::SliderRange(HTMLInputElement* element)
82 {
83 if (element->hasAttribute(precisionAttr)) {
84 step = 1.0;
85 hasStep = !equalIgnoringCase(element->getAttribute(precisionAttr), "float");
86 } else
87 hasStep = element->getAllowedValueStep(&step);
88
89 maximum = element->maximum();
90 minimum = element->minimum();
91 }
92
clampValue(double value)93 double SliderRange::clampValue(double value)
94 {
95 double clampedValue = max(minimum, min(value, maximum));
96 if (!hasStep)
97 return clampedValue;
98 // Rounds clampedValue to minimum + N * step.
99 clampedValue = minimum + round((clampedValue - minimum) / step) * step;
100 if (clampedValue > maximum)
101 clampedValue -= step;
102 ASSERT(clampedValue >= minimum);
103 ASSERT(clampedValue <= maximum);
104 return clampedValue;
105 }
106
valueFromElement(HTMLInputElement * element,bool * wasClamped)107 double SliderRange::valueFromElement(HTMLInputElement* element, bool* wasClamped)
108 {
109 double oldValue;
110 bool parseSuccess = HTMLInputElement::formStringToDouble(element->value(), &oldValue);
111 if (!parseSuccess)
112 oldValue = (minimum + maximum) / 2;
113 double newValue = clampValue(oldValue);
114
115 if (wasClamped)
116 *wasClamped = !parseSuccess || newValue != oldValue;
117
118 return newValue;
119 }
120
121 // Returns a value between 0 and 1.
122 // As with SliderRange, this could be on HTMLInputElement instead of here.
sliderPosition(HTMLInputElement * element)123 static double sliderPosition(HTMLInputElement* element)
124 {
125 SliderRange range(element);
126 return range.proportionFromValue(range.valueFromElement(element));
127 }
128
129 class SliderThumbElement : public HTMLDivElement {
130 public:
131 SliderThumbElement(Document*, Node* shadowParent);
132
inDragMode() const133 bool inDragMode() const { return m_inDragMode; }
134
135 virtual void defaultEventHandler(Event*);
136 virtual void detach();
137
138 private:
isShadowNode() const139 virtual bool isShadowNode() const { return true; }
shadowParentNode()140 virtual Node* shadowParentNode() { return m_shadowParent; }
141
142 FloatPoint m_offsetToThumb;
143 Node* m_shadowParent;
144 bool m_inDragMode;
145 };
146
SliderThumbElement(Document * document,Node * shadowParent)147 SliderThumbElement::SliderThumbElement(Document* document, Node* shadowParent)
148 : HTMLDivElement(divTag, document)
149 , m_shadowParent(shadowParent)
150 , m_inDragMode(false)
151 {
152 }
153
defaultEventHandler(Event * event)154 void SliderThumbElement::defaultEventHandler(Event* event)
155 {
156 if (!event->isMouseEvent()) {
157 HTMLDivElement::defaultEventHandler(event);
158 return;
159 }
160
161 MouseEvent* mouseEvent = static_cast<MouseEvent*>(event);
162 bool isLeftButton = mouseEvent->button() == LeftButton;
163 const AtomicString& eventType = event->type();
164
165 if (eventType == eventNames().mousedownEvent && isLeftButton) {
166 if (document()->frame() && renderer()) {
167 RenderSlider* slider = toRenderSlider(renderer()->parent());
168 if (slider) {
169 if (slider->mouseEventIsInThumb(mouseEvent)) {
170 // We selected the thumb, we want the cursor to always stay at
171 // the same position relative to the thumb.
172 m_offsetToThumb = slider->mouseEventOffsetToThumb(mouseEvent);
173 } else {
174 // We are outside the thumb, move the thumb to the point were
175 // we clicked. We'll be exactly at the center of the thumb.
176 m_offsetToThumb.setX(0);
177 m_offsetToThumb.setY(0);
178 }
179
180 m_inDragMode = true;
181 document()->frame()->eventHandler()->setCapturingMouseEventsNode(m_shadowParent);
182 event->setDefaultHandled();
183 return;
184 }
185 }
186 } else if (eventType == eventNames().mouseupEvent && isLeftButton) {
187 if (m_inDragMode) {
188 if (Frame* frame = document()->frame())
189 frame->eventHandler()->setCapturingMouseEventsNode(0);
190 m_inDragMode = false;
191 event->setDefaultHandled();
192 return;
193 }
194 } else if (eventType == eventNames().mousemoveEvent) {
195 if (m_inDragMode && renderer() && renderer()->parent()) {
196 RenderSlider* slider = toRenderSlider(renderer()->parent());
197 if (slider) {
198 FloatPoint curPoint = slider->absoluteToLocal(mouseEvent->absoluteLocation(), false, true);
199 IntPoint eventOffset(curPoint.x() + m_offsetToThumb.x(), curPoint.y() + m_offsetToThumb.y());
200 slider->setValueForPosition(slider->positionForOffset(eventOffset));
201 event->setDefaultHandled();
202 return;
203 }
204 }
205 }
206
207 HTMLDivElement::defaultEventHandler(event);
208 }
209
detach()210 void SliderThumbElement::detach()
211 {
212 if (m_inDragMode) {
213 if (Frame* frame = document()->frame())
214 frame->eventHandler()->setCapturingMouseEventsNode(0);
215 }
216 HTMLDivElement::detach();
217 }
218
RenderSlider(HTMLInputElement * element)219 RenderSlider::RenderSlider(HTMLInputElement* element)
220 : RenderBlock(element)
221 {
222 }
223
~RenderSlider()224 RenderSlider::~RenderSlider()
225 {
226 if (m_thumb)
227 m_thumb->detach();
228 }
229
baselinePosition(bool,bool) const230 int RenderSlider::baselinePosition(bool, bool) const
231 {
232 return height() + marginTop();
233 }
234
calcPrefWidths()235 void RenderSlider::calcPrefWidths()
236 {
237 m_minPrefWidth = 0;
238 m_maxPrefWidth = 0;
239
240 if (style()->width().isFixed() && style()->width().value() > 0)
241 m_minPrefWidth = m_maxPrefWidth = calcContentBoxWidth(style()->width().value());
242 else
243 m_maxPrefWidth = defaultTrackLength * style()->effectiveZoom();
244
245 if (style()->minWidth().isFixed() && style()->minWidth().value() > 0) {
246 m_maxPrefWidth = max(m_maxPrefWidth, calcContentBoxWidth(style()->minWidth().value()));
247 m_minPrefWidth = max(m_minPrefWidth, calcContentBoxWidth(style()->minWidth().value()));
248 } else if (style()->width().isPercent() || (style()->width().isAuto() && style()->height().isPercent()))
249 m_minPrefWidth = 0;
250 else
251 m_minPrefWidth = m_maxPrefWidth;
252
253 if (style()->maxWidth().isFixed() && style()->maxWidth().value() != undefinedLength) {
254 m_maxPrefWidth = min(m_maxPrefWidth, calcContentBoxWidth(style()->maxWidth().value()));
255 m_minPrefWidth = min(m_minPrefWidth, calcContentBoxWidth(style()->maxWidth().value()));
256 }
257
258 int toAdd = paddingLeft() + paddingRight() + borderLeft() + borderRight();
259 m_minPrefWidth += toAdd;
260 m_maxPrefWidth += toAdd;
261
262 setPrefWidthsDirty(false);
263 }
264
styleDidChange(StyleDifference diff,const RenderStyle * oldStyle)265 void RenderSlider::styleDidChange(StyleDifference diff, const RenderStyle* oldStyle)
266 {
267 RenderBlock::styleDidChange(diff, oldStyle);
268
269 if (m_thumb)
270 m_thumb->renderer()->setStyle(createThumbStyle(style()));
271
272 setReplaced(isInline());
273 }
274
createThumbStyle(const RenderStyle * parentStyle)275 PassRefPtr<RenderStyle> RenderSlider::createThumbStyle(const RenderStyle* parentStyle)
276 {
277 RefPtr<RenderStyle> style;
278 RenderStyle* pseudoStyle = getCachedPseudoStyle(SLIDER_THUMB);
279 if (pseudoStyle)
280 // We may be sharing style with another slider, but we must not share the thumb style.
281 style = RenderStyle::clone(pseudoStyle);
282 else
283 style = RenderStyle::create();
284
285 if (parentStyle)
286 style->inheritFrom(parentStyle);
287
288 style->setDisplay(BLOCK);
289
290 if (parentStyle->appearance() == SliderVerticalPart)
291 style->setAppearance(SliderThumbVerticalPart);
292 else if (parentStyle->appearance() == SliderHorizontalPart)
293 style->setAppearance(SliderThumbHorizontalPart);
294 else if (parentStyle->appearance() == MediaSliderPart)
295 style->setAppearance(MediaSliderThumbPart);
296 else if (parentStyle->appearance() == MediaVolumeSliderPart)
297 style->setAppearance(MediaVolumeSliderThumbPart);
298
299 return style.release();
300 }
301
thumbRect()302 IntRect RenderSlider::thumbRect()
303 {
304 if (!m_thumb)
305 return IntRect();
306
307 IntRect thumbRect;
308 RenderBox* thumb = toRenderBox(m_thumb->renderer());
309
310 thumbRect.setWidth(thumb->style()->width().calcMinValue(contentWidth()));
311 thumbRect.setHeight(thumb->style()->height().calcMinValue(contentHeight()));
312
313 double fraction = sliderPosition(static_cast<HTMLInputElement*>(node()));
314 IntRect contentRect = contentBoxRect();
315 if (style()->appearance() == SliderVerticalPart || style()->appearance() == MediaVolumeSliderPart) {
316 thumbRect.setX(contentRect.x() + (contentRect.width() - thumbRect.width()) / 2);
317 thumbRect.setY(contentRect.y() + static_cast<int>(nextafter((contentRect.height() - thumbRect.height()) + 1, 0) * (1 - fraction)));
318 } else {
319 thumbRect.setX(contentRect.x() + static_cast<int>(nextafter((contentRect.width() - thumbRect.width()) + 1, 0) * fraction));
320 thumbRect.setY(contentRect.y() + (contentRect.height() - thumbRect.height()) / 2);
321 }
322
323 return thumbRect;
324 }
325
layout()326 void RenderSlider::layout()
327 {
328 ASSERT(needsLayout());
329
330 RenderBox* thumb = m_thumb ? toRenderBox(m_thumb->renderer()) : 0;
331
332 IntSize baseSize(borderLeft() + paddingLeft() + paddingRight() + borderRight(),
333 borderTop() + paddingTop() + paddingBottom() + borderBottom());
334
335 if (thumb) {
336 // Allow the theme to set the size of the thumb.
337 if (thumb->style()->hasAppearance()) {
338 // FIXME: This should pass the style, not the renderer, to the theme.
339 theme()->adjustSliderThumbSize(thumb);
340 }
341
342 baseSize.expand(thumb->style()->width().calcMinValue(0), thumb->style()->height().calcMinValue(0));
343 }
344
345 LayoutRepainter repainter(*this, checkForRepaintDuringLayout());
346
347 IntSize oldSize = size();
348
349 setSize(baseSize);
350 calcWidth();
351 calcHeight();
352
353 if (thumb) {
354 if (oldSize != size())
355 thumb->setChildNeedsLayout(true, false);
356
357 LayoutStateMaintainer statePusher(view(), this, size());
358
359 IntRect oldThumbRect = thumb->frameRect();
360
361 thumb->layoutIfNeeded();
362
363 IntRect rect = thumbRect();
364 thumb->setFrameRect(rect);
365 if (thumb->checkForRepaintDuringLayout())
366 thumb->repaintDuringLayoutIfMoved(oldThumbRect);
367
368 statePusher.pop();
369 addOverflowFromChild(thumb);
370 }
371
372 repainter.repaintAfterLayout();
373
374 setNeedsLayout(false);
375 }
376
updateFromElement()377 void RenderSlider::updateFromElement()
378 {
379 HTMLInputElement* element = static_cast<HTMLInputElement*>(node());
380
381 // Send the value back to the element if the range changes it.
382 SliderRange range(element);
383 bool clamped;
384 double value = range.valueFromElement(element, &clamped);
385 if (clamped)
386 element->setValueFromRenderer(HTMLInputElement::formStringFromDouble(value));
387
388 // Layout will take care of the thumb's size and position.
389 if (!m_thumb) {
390 m_thumb = new SliderThumbElement(document(), node());
391 RefPtr<RenderStyle> thumbStyle = createThumbStyle(style());
392 m_thumb->setRenderer(m_thumb->createRenderer(renderArena(), thumbStyle.get()));
393 m_thumb->renderer()->setStyle(thumbStyle.release());
394 m_thumb->setAttached();
395 m_thumb->setInDocument(true);
396 addChild(m_thumb->renderer());
397 }
398 setNeedsLayout(true);
399 }
400
mouseEventIsInThumb(MouseEvent * evt)401 bool RenderSlider::mouseEventIsInThumb(MouseEvent* evt)
402 {
403 if (!m_thumb || !m_thumb->renderer())
404 return false;
405
406 #if ENABLE(VIDEO)
407 if (style()->appearance() == MediaSliderPart || style()->appearance() == MediaVolumeSliderPart) {
408 MediaControlInputElement *sliderThumb = static_cast<MediaControlInputElement*>(m_thumb->renderer()->node());
409 return sliderThumb->hitTest(evt->absoluteLocation());
410 }
411 #endif
412
413 FloatPoint localPoint = m_thumb->renderBox()->absoluteToLocal(evt->absoluteLocation(), false, true);
414 IntRect thumbBounds = m_thumb->renderBox()->borderBoxRect();
415 return thumbBounds.contains(roundedIntPoint(localPoint));
416 }
417
mouseEventOffsetToThumb(MouseEvent * evt)418 FloatPoint RenderSlider::mouseEventOffsetToThumb(MouseEvent* evt)
419 {
420 ASSERT(m_thumb && m_thumb->renderer());
421 FloatPoint localPoint = m_thumb->renderBox()->absoluteToLocal(evt->absoluteLocation(), false, true);
422 IntRect thumbBounds = m_thumb->renderBox()->borderBoxRect();
423 FloatPoint offset;
424 offset.setX(thumbBounds.x() + thumbBounds.width() / 2 - localPoint.x());
425 offset.setY(thumbBounds.y() + thumbBounds.height() / 2 - localPoint.y());
426 return offset;
427 }
428
setValueForPosition(int position)429 void RenderSlider::setValueForPosition(int position)
430 {
431 if (!m_thumb || !m_thumb->renderer())
432 return;
433
434 HTMLInputElement* element = static_cast<HTMLInputElement*>(node());
435
436 // Calculate the new value based on the position, and send it to the element.
437 SliderRange range(element);
438 double fraction = static_cast<double>(position) / trackSize();
439 if (style()->appearance() == SliderVerticalPart || style()->appearance() == MediaVolumeSliderPart)
440 fraction = 1 - fraction;
441 double value = range.clampValue(range.valueFromProportion(fraction));
442 element->setValueFromRenderer(HTMLInputElement::formStringFromDouble(value));
443
444 // Also update the position if appropriate.
445 if (position != currentPosition()) {
446 setNeedsLayout(true);
447
448 // FIXME: It seems like this could send extra change events if the same value is set
449 // multiple times with no layout in between.
450 element->dispatchFormControlChangeEvent();
451 }
452 }
453
positionForOffset(const IntPoint & p)454 int RenderSlider::positionForOffset(const IntPoint& p)
455 {
456 if (!m_thumb || !m_thumb->renderer())
457 return 0;
458
459 int position;
460 if (style()->appearance() == SliderVerticalPart || style()->appearance() == MediaVolumeSliderPart)
461 position = p.y() - m_thumb->renderBox()->height() / 2;
462 else
463 position = p.x() - m_thumb->renderBox()->width() / 2;
464
465 return max(0, min(position, trackSize()));
466 }
467
currentPosition()468 int RenderSlider::currentPosition()
469 {
470 ASSERT(m_thumb);
471 ASSERT(m_thumb->renderer());
472
473 if (style()->appearance() == SliderVerticalPart || style()->appearance() == MediaVolumeSliderPart)
474 return toRenderBox(m_thumb->renderer())->y() - contentBoxRect().y();
475 return toRenderBox(m_thumb->renderer())->x() - contentBoxRect().x();
476 }
477
trackSize()478 int RenderSlider::trackSize()
479 {
480 ASSERT(m_thumb);
481 ASSERT(m_thumb->renderer());
482
483 if (style()->appearance() == SliderVerticalPart || style()->appearance() == MediaVolumeSliderPart)
484 return contentHeight() - m_thumb->renderBox()->height();
485 return contentWidth() - m_thumb->renderBox()->width();
486 }
487
forwardEvent(Event * event)488 void RenderSlider::forwardEvent(Event* event)
489 {
490 if (event->isMouseEvent()) {
491 MouseEvent* mouseEvent = static_cast<MouseEvent*>(event);
492 if (event->type() == eventNames().mousedownEvent && mouseEvent->button() == LeftButton) {
493 if (!mouseEventIsInThumb(mouseEvent)) {
494 IntPoint eventOffset = roundedIntPoint(absoluteToLocal(mouseEvent->absoluteLocation(), false, true));
495 setValueForPosition(positionForOffset(eventOffset));
496 }
497 }
498 }
499
500 m_thumb->defaultEventHandler(event);
501 }
502
inDragMode() const503 bool RenderSlider::inDragMode() const
504 {
505 return m_thumb && m_thumb->inDragMode();
506 }
507
508 } // namespace WebCore
509