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 64 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 73 double valueFromProportion(double proportion) 74 { 75 return minimum + proportion * (maximum - minimum); 76 } 77 78 double valueFromElement(HTMLInputElement*, bool* wasClamped = 0); 79 }; 80 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 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 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. 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 133 bool inDragMode() const { return m_inDragMode; } 134 135 virtual void defaultEventHandler(Event*); 136 virtual void detach(); 137 138 private: 139 virtual bool isShadowNode() const { return true; } 140 virtual Node* shadowParentNode() { return m_shadowParent; } 141 142 FloatPoint m_offsetToThumb; 143 Node* m_shadowParent; 144 bool m_inDragMode; 145 }; 146 147 SliderThumbElement::SliderThumbElement(Document* document, Node* shadowParent) 148 : HTMLDivElement(divTag, document) 149 , m_shadowParent(shadowParent) 150 , m_inDragMode(false) 151 { 152 } 153 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 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 219 RenderSlider::RenderSlider(HTMLInputElement* element) 220 : RenderBlock(element) 221 { 222 } 223 224 RenderSlider::~RenderSlider() 225 { 226 if (m_thumb) 227 m_thumb->detach(); 228 } 229 230 int RenderSlider::baselinePosition(bool, bool) const 231 { 232 return height() + marginTop(); 233 } 234 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 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 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 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 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 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 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 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 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 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 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 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 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 503 bool RenderSlider::inDragMode() const 504 { 505 return m_thumb && m_thumb->inDragMode(); 506 } 507 508 } // namespace WebCore 509