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