2 * Copyright (C) 2006, 2007, 2008, 2009 Apple Inc. All rights reserved.
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.
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.
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.
22 #include "RenderSlider.h"
24 #include "CSSPropertyNames.h"
27 #include "EventHandler.h"
28 #include "EventNames.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>
44 using namespace HTMLNames;
46 static const int defaultTrackLength = 129;
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.
55 explicit SliderRange(HTMLInputElement*);
56 double clampValue(double value);
57 double valueFromElement(HTMLInputElement*, bool* wasClamped = 0);
60 SliderRange::SliderRange(HTMLInputElement* element)
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.
65 isIntegral = !equalIgnoringCase(element->getAttribute(precisionAttr), "float");
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();
71 // If the maximum is smaller, use it as the minimum.
72 minimum = min(element->getAttribute(minAttr).toDouble(), maximum);
75 double SliderRange::clampValue(double value)
77 double clampedValue = max(minimum, min(value, maximum));
78 return isIntegral ? round(clampedValue) : clampedValue;
81 double SliderRange::valueFromElement(HTMLInputElement* element, bool* wasClamped)
83 String valueString = element->value();
84 double oldValue = valueString.isNull() ? (minimum + maximum) / 2 : valueString.toDouble();
85 double newValue = clampValue(oldValue);
88 *wasClamped = valueString.isNull() || newValue != oldValue;
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)
97 SliderRange range(element);
98 double value = range.valueFromElement(element);
99 return (value - range.minimum) / (range.maximum - range.minimum);
102 class SliderThumbElement : public HTMLDivElement {
104 SliderThumbElement(Document*, Node* shadowParent);
106 bool inDragMode() const { return m_inDragMode; }
108 virtual void defaultEventHandler(Event*);
111 virtual bool isShadowNode() const { return true; }
112 virtual Node* shadowParentNode() { return m_shadowParent; }
114 Node* m_shadowParent;
115 FloatPoint m_initialClickPoint; // initial click point in RenderSlider-local coordinates
116 int m_initialPosition;
120 SliderThumbElement::SliderThumbElement(Document* document, Node* shadowParent)
121 : HTMLDivElement(divTag, document)
122 , m_shadowParent(shadowParent)
123 , m_initialPosition(0)
124 , m_inDragMode(false)
128 void SliderThumbElement::defaultEventHandler(Event* event)
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)) {
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();
144 document()->frame()->eventHandler()->setCapturingMouseEventsNode(m_shadowParent);
146 event->setDefaultHandled();
149 } else if (eventType == eventNames().mouseupEvent && event->isMouseEvent() && static_cast<MouseEvent*>(event)->button() == LeftButton) {
151 if (Frame* frame = document()->frame())
152 frame->eventHandler()->setCapturingMouseEventsNode(0);
153 m_inDragMode = false;
154 event->setDefaultHandled();
157 } else if (eventType == eventNames().mousemoveEvent && event->isMouseEvent()) {
158 if (m_inDragMode && renderer() && renderer()->parent()) {
160 MouseEvent* mouseEvent = static_cast<MouseEvent*>(event);
161 RenderSlider* slider = static_cast<RenderSlider*>(renderer()->parent());
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();
172 HTMLDivElement::defaultEventHandler(event);
175 RenderSlider::RenderSlider(HTMLInputElement* element)
176 : RenderBlock(element)
180 RenderSlider::~RenderSlider()
186 int RenderSlider::baselinePosition(bool, bool) const
188 return height() + marginTop();
191 void RenderSlider::calcPrefWidths()
196 if (style()->width().isFixed() && style()->width().value() > 0)
197 m_minPrefWidth = m_maxPrefWidth = calcContentBoxWidth(style()->width().value());
199 m_maxPrefWidth = defaultTrackLength * style()->effectiveZoom();
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()))
207 m_minPrefWidth = m_maxPrefWidth;
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()));
214 int toAdd = paddingLeft() + paddingRight() + borderLeft() + borderRight();
215 m_minPrefWidth += toAdd;
216 m_maxPrefWidth += toAdd;
218 setPrefWidthsDirty(false);
221 void RenderSlider::styleDidChange(StyleDifference diff, const RenderStyle* oldStyle)
223 RenderBlock::styleDidChange(diff, oldStyle);
226 m_thumb->renderer()->setStyle(createThumbStyle(style()));
228 setReplaced(isInline());
231 PassRefPtr<RenderStyle> RenderSlider::createThumbStyle(const RenderStyle* parentStyle)
233 RefPtr<RenderStyle> style;
234 RenderStyle* pseudoStyle = getCachedPseudoStyle(SLIDER_THUMB);
236 // We may be sharing style with another slider, but we must not share the thumb style.
237 style = RenderStyle::clone(pseudoStyle);
239 style = RenderStyle::create();
242 style->inheritFrom(parentStyle);
244 style->setDisplay(BLOCK);
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);
253 return style.release();
256 void RenderSlider::layout()
258 ASSERT(needsLayout());
260 RenderBox* thumb = m_thumb ? toRenderBox(m_thumb->renderer()) : 0;
262 IntSize baseSize(borderLeft() + paddingLeft() + paddingRight() + borderRight(),
263 borderTop() + paddingTop() + paddingBottom() + borderBottom());
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);
272 baseSize.expand(thumb->style()->width().calcMinValue(0), thumb->style()->height().calcMinValue(0));
275 LayoutRepainter repainter(*this, checkForRepaintDuringLayout());
277 IntSize oldSize = size();
283 IntRect overflowRect(IntPoint(), size());
286 if (oldSize != size())
287 thumb->setChildNeedsLayout(true, false);
289 LayoutStateMaintainer statePusher(view(), this, size());
291 IntRect oldThumbRect = thumb->frameRect();
293 thumb->layoutIfNeeded();
297 thumbRect.setWidth(thumb->style()->width().calcMinValue(contentWidth()));
298 thumbRect.setHeight(thumb->style()->height().calcMinValue(contentHeight()));
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)));
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);
310 thumb->setFrameRect(thumbRect);
312 if (thumb->checkForRepaintDuringLayout())
313 thumb->repaintDuringLayoutIfMoved(oldThumbRect);
317 IntRect thumbOverflowRect = thumb->overflowRect();
318 thumbOverflowRect.move(thumb->x(), thumb->y());
319 overflowRect.unite(thumbOverflowRect);
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();
329 repainter.repaintAfterLayout();
331 setNeedsLayout(false);
334 void RenderSlider::updateFromElement()
336 HTMLInputElement* element = static_cast<HTMLInputElement*>(node());
338 // Send the value back to the element if the range changes it.
339 SliderRange range(element);
341 double value = range.valueFromElement(element, &clamped);
343 element->setValueFromRenderer(String::number(value));
345 // Layout will take care of the thumb's size and position.
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());
355 setNeedsLayout(true);
358 bool RenderSlider::mouseEventIsInThumb(MouseEvent* evt)
360 if (!m_thumb || !m_thumb->renderer())
364 if (style()->appearance() == MediaSliderPart) {
365 MediaControlInputElement *sliderThumb = static_cast<MediaControlInputElement*>(m_thumb->renderer()->node());
366 return sliderThumb->hitTest(evt->absoluteLocation());
370 FloatPoint localPoint = m_thumb->renderBox()->absoluteToLocal(evt->absoluteLocation(), false, true);
371 IntRect thumbBounds = m_thumb->renderBox()->borderBoxRect();
372 return thumbBounds.contains(roundedIntPoint(localPoint));
375 void RenderSlider::setValueForPosition(int position)
377 if (!m_thumb || !m_thumb->renderer())
380 HTMLInputElement* element = static_cast<HTMLInputElement*>(node());
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));
390 // Also update the position if appropriate.
391 if (position != currentPosition()) {
392 setNeedsLayout(true);
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.
400 int RenderSlider::positionForOffset(const IntPoint& p)
402 if (!m_thumb || !m_thumb->renderer())
406 if (style()->appearance() == SliderVerticalPart)
407 position = p.y() - m_thumb->renderBox()->height() / 2;
409 position = p.x() - m_thumb->renderBox()->width() / 2;
411 return max(0, min(position, trackSize()));
414 int RenderSlider::currentPosition()
417 ASSERT(m_thumb->renderer());
419 if (style()->appearance() == SliderVerticalPart)
420 return toRenderBox(m_thumb->renderer())->y() - contentBoxRect().y();
421 return toRenderBox(m_thumb->renderer())->x() - contentBoxRect().x();
424 int RenderSlider::trackSize()
427 ASSERT(m_thumb->renderer());
429 if (style()->appearance() == SliderVerticalPart)
430 return contentHeight() - m_thumb->renderBox()->height();
431 return contentWidth() - m_thumb->renderBox()->width();
434 void RenderSlider::forwardEvent(Event* event)
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));
446 m_thumb->defaultEventHandler(event);
449 bool RenderSlider::inDragMode() const
451 return m_thumb && m_thumb->inDragMode();
454 } // namespace WebCore