1 /* 2 * Copyright (C) 2006, 2007, 2008, 2009 Apple Inc. All rights reserved. 3 * Copyright (C) 2010 Google Inc. All rights reserved. 4 * 5 * Redistribution and use in source and binary forms, with or without 6 * modification, are permitted provided that the following conditions are 7 * met: 8 * 9 * * Redistributions of source code must retain the above copyright 10 * notice, this list of conditions and the following disclaimer. 11 * * Redistributions in binary form must reproduce the above 12 * copyright notice, this list of conditions and the following disclaimer 13 * in the documentation and/or other materials provided with the 14 * distribution. 15 * * Neither the name of Google Inc. nor the names of its 16 * contributors may be used to endorse or promote products derived from 17 * this software without specific prior written permission. 18 * 19 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 20 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 21 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 22 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 23 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 24 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 25 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 26 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 27 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 */ 31 32 33 #include "config.h" 34 #include "core/html/shadow/SliderThumbElement.h" 35 36 #include "core/dom/Event.h" 37 #include "core/dom/MouseEvent.h" 38 #include "core/dom/shadow/ShadowRoot.h" 39 #include "core/html/HTMLInputElement.h" 40 #include "core/html/StepRange.h" 41 #include "core/html/parser/HTMLParserIdioms.h" 42 #include "core/page/EventHandler.h" 43 #include "core/page/Frame.h" 44 #include "core/rendering/RenderFlexibleBox.h" 45 #include "core/rendering/RenderSlider.h" 46 #include "core/rendering/RenderTheme.h" 47 48 using namespace std; 49 50 namespace WebCore { 51 52 using namespace HTMLNames; 53 54 inline static Decimal sliderPosition(HTMLInputElement* element) 55 { 56 const StepRange stepRange(element->createStepRange(RejectAny)); 57 const Decimal oldValue = parseToDecimalForNumberType(element->value(), stepRange.defaultValue()); 58 return stepRange.proportionFromValue(stepRange.clampValue(oldValue)); 59 } 60 61 inline static bool hasVerticalAppearance(HTMLInputElement* input) 62 { 63 ASSERT(input->renderer()); 64 RenderStyle* sliderStyle = input->renderer()->style(); 65 66 if (sliderStyle->appearance() == MediaVolumeSliderPart && input->renderer()->theme()->usesVerticalVolumeSlider()) 67 return true; 68 69 return sliderStyle->appearance() == SliderVerticalPart; 70 } 71 72 SliderThumbElement* sliderThumbElementOf(Node* node) 73 { 74 RELEASE_ASSERT(node->hasTagName(inputTag)); 75 ShadowRoot* shadow = toHTMLInputElement(node)->userAgentShadowRoot(); 76 ASSERT(shadow); 77 Node* thumb = shadow->firstChild()->firstChild()->firstChild(); 78 ASSERT(thumb); 79 return toSliderThumbElement(thumb); 80 } 81 82 HTMLElement* sliderTrackElementOf(Node* node) 83 { 84 RELEASE_ASSERT(node->hasTagName(inputTag)); 85 ShadowRoot* shadow = toHTMLInputElement(node)->userAgentShadowRoot(); 86 ASSERT(shadow); 87 Node* track = shadow->firstChild()->firstChild(); 88 ASSERT(track); 89 return toHTMLElement(track); 90 } 91 92 // -------------------------------- 93 94 RenderSliderThumb::RenderSliderThumb(SliderThumbElement* element) 95 : RenderBlock(element) 96 { 97 } 98 99 void RenderSliderThumb::updateAppearance(RenderStyle* parentStyle) 100 { 101 if (parentStyle->appearance() == SliderVerticalPart) 102 style()->setAppearance(SliderThumbVerticalPart); 103 else if (parentStyle->appearance() == SliderHorizontalPart) 104 style()->setAppearance(SliderThumbHorizontalPart); 105 else if (parentStyle->appearance() == MediaSliderPart) 106 style()->setAppearance(MediaSliderThumbPart); 107 else if (parentStyle->appearance() == MediaVolumeSliderPart) 108 style()->setAppearance(MediaVolumeSliderThumbPart); 109 else if (parentStyle->appearance() == MediaFullScreenVolumeSliderPart) 110 style()->setAppearance(MediaFullScreenVolumeSliderThumbPart); 111 if (style()->hasAppearance()) 112 theme()->adjustSliderThumbSize(style(), toElement(node())); 113 } 114 115 bool RenderSliderThumb::isSliderThumb() const 116 { 117 return true; 118 } 119 120 // -------------------------------- 121 122 // FIXME: Find a way to cascade appearance and adjust heights, and get rid of this class. 123 // http://webkit.org/b/62535 124 class RenderSliderContainer : public RenderFlexibleBox { 125 public: 126 RenderSliderContainer(SliderContainerElement* element) 127 : RenderFlexibleBox(element) { } 128 public: 129 virtual void computeLogicalHeight(LayoutUnit logicalHeight, LayoutUnit logicalTop, LogicalExtentComputedValues&) const OVERRIDE; 130 131 private: 132 virtual void layout() OVERRIDE; 133 }; 134 135 void RenderSliderContainer::computeLogicalHeight(LayoutUnit logicalHeight, LayoutUnit logicalTop, LogicalExtentComputedValues& computedValues) const 136 { 137 HTMLInputElement* input = toHTMLInputElement(node()->shadowHost()); 138 bool isVertical = hasVerticalAppearance(input); 139 140 if (input->renderer()->isSlider() && !isVertical && input->list()) { 141 int offsetFromCenter = theme()->sliderTickOffsetFromTrackCenter(); 142 LayoutUnit trackHeight = 0; 143 if (offsetFromCenter < 0) 144 trackHeight = -2 * offsetFromCenter; 145 else { 146 int tickLength = theme()->sliderTickSize().height(); 147 trackHeight = 2 * (offsetFromCenter + tickLength); 148 } 149 float zoomFactor = style()->effectiveZoom(); 150 if (zoomFactor != 1.0) 151 trackHeight *= zoomFactor; 152 153 RenderBox::computeLogicalHeight(trackHeight, logicalTop, computedValues); 154 return; 155 } 156 if (isVertical) 157 logicalHeight = RenderSlider::defaultTrackLength; 158 RenderBox::computeLogicalHeight(logicalHeight, logicalTop, computedValues); 159 } 160 161 void RenderSliderContainer::layout() 162 { 163 HTMLInputElement* input = toHTMLInputElement(node()->shadowHost()); 164 bool isVertical = hasVerticalAppearance(input); 165 style()->setFlexDirection(isVertical ? FlowColumn : FlowRow); 166 TextDirection oldTextDirection = style()->direction(); 167 if (isVertical) { 168 // FIXME: Work around rounding issues in RTL vertical sliders. We want them to 169 // render identically to LTR vertical sliders. We can remove this work around when 170 // subpixel rendering is enabled on all ports. 171 style()->setDirection(LTR); 172 } 173 174 RenderBox* thumb = input->sliderThumbElement() ? input->sliderThumbElement()->renderBox() : 0; 175 RenderBox* track = input->sliderTrackElement() ? input->sliderTrackElement()->renderBox() : 0; 176 // Force a layout to reset the position of the thumb so the code below doesn't move the thumb to the wrong place. 177 // FIXME: Make a custom Render class for the track and move the thumb positioning code there. 178 if (track) 179 track->setChildNeedsLayout(MarkOnlyThis); 180 181 RenderFlexibleBox::layout(); 182 183 style()->setDirection(oldTextDirection); 184 // These should always exist, unless someone mutates the shadow DOM (e.g., in the inspector). 185 if (!thumb || !track) 186 return; 187 188 double percentageOffset = sliderPosition(input).toDouble(); 189 LayoutUnit availableExtent = isVertical ? track->contentHeight() : track->contentWidth(); 190 availableExtent -= isVertical ? thumb->height() : thumb->width(); 191 LayoutUnit offset = percentageOffset * availableExtent; 192 LayoutPoint thumbLocation = thumb->location(); 193 if (isVertical) 194 thumbLocation.setY(thumbLocation.y() + track->contentHeight() - thumb->height() - offset); 195 else if (style()->isLeftToRightDirection()) 196 thumbLocation.setX(thumbLocation.x() + offset); 197 else 198 thumbLocation.setX(thumbLocation.x() - offset); 199 thumb->setLocation(thumbLocation); 200 if (checkForRepaintDuringLayout() && parent() 201 && (parent()->style()->appearance() == MediaVolumeSliderPart || parent()->style()->appearance() == MediaSliderPart)) { 202 // This will sometimes repaint too much. However, it is necessary to 203 // correctly repaint media controls (volume and timeline sliders) - 204 // they have special painting code in RenderMediaControls.cpp:paintMediaVolumeSlider 205 // and paintMediaSlider that gets called via -webkit-appearance and RenderTheme, 206 // so nothing else would otherwise invalidate the slider. 207 repaint(); 208 } 209 } 210 211 // -------------------------------- 212 213 void SliderThumbElement::setPositionFromValue() 214 { 215 // Since the code to calculate position is in the RenderSliderThumb layout 216 // path, we don't actually update the value here. Instead, we poke at the 217 // renderer directly to trigger layout. 218 if (renderer()) 219 renderer()->setNeedsLayout(); 220 } 221 222 RenderObject* SliderThumbElement::createRenderer(RenderStyle*) 223 { 224 return new RenderSliderThumb(this); 225 } 226 227 bool SliderThumbElement::isDisabledFormControl() const 228 { 229 return hostInput() && hostInput()->isDisabledFormControl(); 230 } 231 232 bool SliderThumbElement::matchesReadOnlyPseudoClass() const 233 { 234 return hostInput()->matchesReadOnlyPseudoClass(); 235 } 236 237 bool SliderThumbElement::matchesReadWritePseudoClass() const 238 { 239 return hostInput()->matchesReadWritePseudoClass(); 240 } 241 242 Node* SliderThumbElement::focusDelegate() 243 { 244 return hostInput(); 245 } 246 247 void SliderThumbElement::dragFrom(const LayoutPoint& point) 248 { 249 RefPtr<SliderThumbElement> protector(this); 250 setPositionFromPoint(point); 251 startDragging(); 252 } 253 254 void SliderThumbElement::setPositionFromPoint(const LayoutPoint& point) 255 { 256 RefPtr<HTMLInputElement> input(hostInput()); 257 HTMLElement* trackElement = sliderTrackElementOf(input.get()); 258 259 if (!input->renderer() || !renderBox() || !trackElement->renderBox()) 260 return; 261 262 input->setTextAsOfLastFormControlChangeEvent(input->value()); 263 LayoutPoint offset = roundedLayoutPoint(input->renderer()->absoluteToLocal(point, UseTransforms)); 264 bool isVertical = hasVerticalAppearance(input.get()); 265 bool isLeftToRightDirection = renderBox()->style()->isLeftToRightDirection(); 266 LayoutUnit trackSize; 267 LayoutUnit position; 268 LayoutUnit currentPosition; 269 // We need to calculate currentPosition from absolute points becaue the 270 // renderer for this node is usually on a layer and renderBox()->x() and 271 // y() are unusable. 272 // FIXME: This should probably respect transforms. 273 LayoutPoint absoluteThumbOrigin = renderBox()->absoluteBoundingBoxRectIgnoringTransforms().location(); 274 LayoutPoint absoluteSliderContentOrigin = roundedLayoutPoint(input->renderer()->localToAbsolute()); 275 IntRect trackBoundingBox = trackElement->renderer()->absoluteBoundingBoxRectIgnoringTransforms(); 276 IntRect inputBoundingBox = input->renderer()->absoluteBoundingBoxRectIgnoringTransforms(); 277 if (isVertical) { 278 trackSize = trackElement->renderBox()->contentHeight() - renderBox()->height(); 279 position = offset.y() - renderBox()->height() / 2 - trackBoundingBox.y() + inputBoundingBox.y() - renderBox()->marginBottom(); 280 currentPosition = absoluteThumbOrigin.y() - absoluteSliderContentOrigin.y(); 281 } else { 282 trackSize = trackElement->renderBox()->contentWidth() - renderBox()->width(); 283 position = offset.x() - renderBox()->width() / 2 - trackBoundingBox.x() + inputBoundingBox.x(); 284 position -= isLeftToRightDirection ? renderBox()->marginLeft() : renderBox()->marginRight(); 285 currentPosition = absoluteThumbOrigin.x() - absoluteSliderContentOrigin.x(); 286 } 287 position = max<LayoutUnit>(0, min(position, trackSize)); 288 const Decimal ratio = Decimal::fromDouble(static_cast<double>(position) / trackSize); 289 const Decimal fraction = isVertical || !isLeftToRightDirection ? Decimal(1) - ratio : ratio; 290 StepRange stepRange(input->createStepRange(RejectAny)); 291 Decimal value = stepRange.clampValue(stepRange.valueFromProportion(fraction)); 292 293 const LayoutUnit snappingThreshold = renderer()->theme()->sliderTickSnappingThreshold(); 294 if (snappingThreshold > 0) { 295 Decimal closest = input->findClosestTickMarkValue(value); 296 if (closest.isFinite()) { 297 double closestFraction = stepRange.proportionFromValue(closest).toDouble(); 298 double closestRatio = isVertical || !isLeftToRightDirection ? 1.0 - closestFraction : closestFraction; 299 LayoutUnit closestPosition = trackSize * closestRatio; 300 if ((closestPosition - position).abs() <= snappingThreshold) 301 value = closest; 302 } 303 } 304 305 String valueString = serializeForNumberType(value); 306 if (valueString == input->value()) 307 return; 308 309 // FIXME: This is no longer being set from renderer. Consider updating the method name. 310 input->setValueFromRenderer(valueString); 311 if (renderer()) 312 renderer()->setNeedsLayout(); 313 input->dispatchFormControlChangeEvent(); 314 } 315 316 void SliderThumbElement::startDragging() 317 { 318 if (Frame* frame = document()->frame()) { 319 frame->eventHandler()->setCapturingMouseEventsNode(this); 320 m_inDragMode = true; 321 } 322 } 323 324 void SliderThumbElement::stopDragging() 325 { 326 if (!m_inDragMode) 327 return; 328 329 if (Frame* frame = document()->frame()) 330 frame->eventHandler()->setCapturingMouseEventsNode(0); 331 m_inDragMode = false; 332 if (renderer()) 333 renderer()->setNeedsLayout(); 334 } 335 336 void SliderThumbElement::defaultEventHandler(Event* event) 337 { 338 if (!event->isMouseEvent()) { 339 HTMLDivElement::defaultEventHandler(event); 340 return; 341 } 342 343 // FIXME: Should handle this readonly/disabled check in more general way. 344 // Missing this kind of check is likely to occur elsewhere if adding it in each shadow element. 345 HTMLInputElement* input = hostInput(); 346 if (!input || input->isDisabledOrReadOnly()) { 347 stopDragging(); 348 HTMLDivElement::defaultEventHandler(event); 349 return; 350 } 351 352 MouseEvent* mouseEvent = toMouseEvent(event); 353 bool isLeftButton = mouseEvent->button() == LeftButton; 354 const AtomicString& eventType = event->type(); 355 356 // We intentionally do not call event->setDefaultHandled() here because 357 // MediaControlTimelineElement::defaultEventHandler() wants to handle these 358 // mouse events. 359 if (eventType == eventNames().mousedownEvent && isLeftButton) { 360 startDragging(); 361 return; 362 } else if (eventType == eventNames().mouseupEvent && isLeftButton) { 363 stopDragging(); 364 return; 365 } else if (eventType == eventNames().mousemoveEvent) { 366 if (m_inDragMode) 367 setPositionFromPoint(mouseEvent->absoluteLocation()); 368 return; 369 } 370 371 HTMLDivElement::defaultEventHandler(event); 372 } 373 374 bool SliderThumbElement::willRespondToMouseMoveEvents() 375 { 376 const HTMLInputElement* input = hostInput(); 377 if (input && !input->isDisabledOrReadOnly() && m_inDragMode) 378 return true; 379 380 return HTMLDivElement::willRespondToMouseMoveEvents(); 381 } 382 383 bool SliderThumbElement::willRespondToMouseClickEvents() 384 { 385 const HTMLInputElement* input = hostInput(); 386 if (input && !input->isDisabledOrReadOnly()) 387 return true; 388 389 return HTMLDivElement::willRespondToMouseClickEvents(); 390 } 391 392 void SliderThumbElement::detach(const AttachContext& context) 393 { 394 if (m_inDragMode) { 395 if (Frame* frame = document()->frame()) 396 frame->eventHandler()->setCapturingMouseEventsNode(0); 397 } 398 HTMLDivElement::detach(context); 399 } 400 401 HTMLInputElement* SliderThumbElement::hostInput() const 402 { 403 // Only HTMLInputElement creates SliderThumbElement instances as its shadow nodes. 404 // So, shadowHost() must be an HTMLInputElement. 405 return toHTMLInputElement(shadowHost()); 406 } 407 408 static const AtomicString& sliderThumbShadowPartId() 409 { 410 DEFINE_STATIC_LOCAL(const AtomicString, sliderThumb, ("-webkit-slider-thumb", AtomicString::ConstructFromLiteral)); 411 return sliderThumb; 412 } 413 414 static const AtomicString& mediaSliderThumbShadowPartId() 415 { 416 DEFINE_STATIC_LOCAL(const AtomicString, mediaSliderThumb, ("-webkit-media-slider-thumb", AtomicString::ConstructFromLiteral)); 417 return mediaSliderThumb; 418 } 419 420 const AtomicString& SliderThumbElement::part() const 421 { 422 HTMLInputElement* input = hostInput(); 423 if (!input) 424 return sliderThumbShadowPartId(); 425 426 RenderStyle* sliderStyle = input->renderer()->style(); 427 switch (sliderStyle->appearance()) { 428 case MediaSliderPart: 429 case MediaSliderThumbPart: 430 case MediaVolumeSliderPart: 431 case MediaVolumeSliderThumbPart: 432 case MediaFullScreenVolumeSliderPart: 433 case MediaFullScreenVolumeSliderThumbPart: 434 return mediaSliderThumbShadowPartId(); 435 default: 436 return sliderThumbShadowPartId(); 437 } 438 } 439 440 // -------------------------------- 441 442 inline SliderContainerElement::SliderContainerElement(Document* document) 443 : HTMLDivElement(HTMLNames::divTag, document) 444 { 445 } 446 447 PassRefPtr<SliderContainerElement> SliderContainerElement::create(Document* document) 448 { 449 return adoptRef(new SliderContainerElement(document)); 450 } 451 452 RenderObject* SliderContainerElement::createRenderer(RenderStyle*) 453 { 454 return new RenderSliderContainer(this); 455 } 456 457 const AtomicString& SliderContainerElement::part() const 458 { 459 DEFINE_STATIC_LOCAL(const AtomicString, mediaSliderContainer, ("-webkit-media-slider-container", AtomicString::ConstructFromLiteral)); 460 DEFINE_STATIC_LOCAL(const AtomicString, sliderContainer, ("-webkit-slider-container", AtomicString::ConstructFromLiteral)); 461 462 if (!shadowHost()->hasTagName(inputTag)) 463 return sliderContainer; 464 465 RenderStyle* sliderStyle = toHTMLInputElement(shadowHost())->renderer()->style(); 466 switch (sliderStyle->appearance()) { 467 case MediaSliderPart: 468 case MediaSliderThumbPart: 469 case MediaVolumeSliderPart: 470 case MediaVolumeSliderThumbPart: 471 case MediaFullScreenVolumeSliderPart: 472 case MediaFullScreenVolumeSliderThumbPart: 473 return mediaSliderContainer; 474 default: 475 return sliderContainer; 476 } 477 } 478 479 } 480