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/events/Event.h" 37 #include "core/events/MouseEvent.h" 38 #include "core/dom/shadow/ShadowRoot.h" 39 #include "core/frame/LocalFrame.h" 40 #include "core/html/HTMLInputElement.h" 41 #include "core/html/forms/StepRange.h" 42 #include "core/html/parser/HTMLParserIdioms.h" 43 #include "core/html/shadow/ShadowElementNames.h" 44 #include "core/page/EventHandler.h" 45 #include "core/rendering/RenderFlexibleBox.h" 46 #include "core/rendering/RenderSlider.h" 47 #include "core/rendering/RenderTheme.h" 48 49 namespace WebCore { 50 51 using namespace HTMLNames; 52 53 inline static Decimal sliderPosition(HTMLInputElement* element) 54 { 55 const StepRange stepRange(element->createStepRange(RejectAny)); 56 const Decimal oldValue = parseToDecimalForNumberType(element->value(), stepRange.defaultValue()); 57 return stepRange.proportionFromValue(stepRange.clampValue(oldValue)); 58 } 59 60 inline static bool hasVerticalAppearance(HTMLInputElement* input) 61 { 62 ASSERT(input->renderer()); 63 RenderStyle* sliderStyle = input->renderer()->style(); 64 65 return sliderStyle->appearance() == SliderVerticalPart; 66 } 67 68 // -------------------------------- 69 70 RenderSliderThumb::RenderSliderThumb(SliderThumbElement* element) 71 : RenderBlockFlow(element) 72 { 73 } 74 75 void RenderSliderThumb::updateAppearance(RenderStyle* parentStyle) 76 { 77 if (parentStyle->appearance() == SliderVerticalPart) 78 style()->setAppearance(SliderThumbVerticalPart); 79 else if (parentStyle->appearance() == SliderHorizontalPart) 80 style()->setAppearance(SliderThumbHorizontalPart); 81 else if (parentStyle->appearance() == MediaSliderPart) 82 style()->setAppearance(MediaSliderThumbPart); 83 else if (parentStyle->appearance() == MediaVolumeSliderPart) 84 style()->setAppearance(MediaVolumeSliderThumbPart); 85 else if (parentStyle->appearance() == MediaFullScreenVolumeSliderPart) 86 style()->setAppearance(MediaFullScreenVolumeSliderThumbPart); 87 if (style()->hasAppearance()) 88 RenderTheme::theme().adjustSliderThumbSize(style(), toElement(node())); 89 } 90 91 bool RenderSliderThumb::isSliderThumb() const 92 { 93 return true; 94 } 95 96 // -------------------------------- 97 98 // FIXME: Find a way to cascade appearance and adjust heights, and get rid of this class. 99 // http://webkit.org/b/62535 100 class RenderSliderContainer : public RenderFlexibleBox { 101 public: 102 RenderSliderContainer(SliderContainerElement* element) 103 : RenderFlexibleBox(element) { } 104 public: 105 virtual void computeLogicalHeight(LayoutUnit logicalHeight, LayoutUnit logicalTop, LogicalExtentComputedValues&) const OVERRIDE; 106 107 private: 108 virtual void layout() OVERRIDE; 109 }; 110 111 void RenderSliderContainer::computeLogicalHeight(LayoutUnit logicalHeight, LayoutUnit logicalTop, LogicalExtentComputedValues& computedValues) const 112 { 113 HTMLInputElement* input = toHTMLInputElement(node()->shadowHost()); 114 bool isVertical = hasVerticalAppearance(input); 115 116 if (input->renderer()->isSlider() && !isVertical && input->list()) { 117 int offsetFromCenter = RenderTheme::theme().sliderTickOffsetFromTrackCenter(); 118 LayoutUnit trackHeight = 0; 119 if (offsetFromCenter < 0) 120 trackHeight = -2 * offsetFromCenter; 121 else { 122 int tickLength = RenderTheme::theme().sliderTickSize().height(); 123 trackHeight = 2 * (offsetFromCenter + tickLength); 124 } 125 float zoomFactor = style()->effectiveZoom(); 126 if (zoomFactor != 1.0) 127 trackHeight *= zoomFactor; 128 129 // FIXME: The trackHeight should have been added before updateLogicalHeight was called to avoid this hack. 130 updateIntrinsicContentLogicalHeight(trackHeight); 131 132 RenderBox::computeLogicalHeight(trackHeight, logicalTop, computedValues); 133 return; 134 } 135 if (isVertical) 136 logicalHeight = RenderSlider::defaultTrackLength; 137 138 // FIXME: The trackHeight should have been added before updateLogicalHeight was called to avoid this hack. 139 updateIntrinsicContentLogicalHeight(logicalHeight); 140 141 RenderBox::computeLogicalHeight(logicalHeight, logicalTop, computedValues); 142 } 143 144 void RenderSliderContainer::layout() 145 { 146 HTMLInputElement* input = toHTMLInputElement(node()->shadowHost()); 147 bool isVertical = hasVerticalAppearance(input); 148 style()->setFlexDirection(isVertical ? FlowColumn : FlowRow); 149 TextDirection oldTextDirection = style()->direction(); 150 if (isVertical) { 151 // FIXME: Work around rounding issues in RTL vertical sliders. We want them to 152 // render identically to LTR vertical sliders. We can remove this work around when 153 // subpixel rendering is enabled on all ports. 154 style()->setDirection(LTR); 155 } 156 157 Element* thumbElement = input->userAgentShadowRoot()->getElementById(ShadowElementNames::sliderThumb()); 158 Element* trackElement = input->userAgentShadowRoot()->getElementById(ShadowElementNames::sliderTrack()); 159 RenderBox* thumb = thumbElement ? thumbElement->renderBox() : 0; 160 RenderBox* track = trackElement ? trackElement->renderBox() : 0; 161 162 SubtreeLayoutScope layoutScope(*this); 163 // Force a layout to reset the position of the thumb so the code below doesn't move the thumb to the wrong place. 164 // FIXME: Make a custom Render class for the track and move the thumb positioning code there. 165 if (track) 166 layoutScope.setChildNeedsLayout(track); 167 168 RenderFlexibleBox::layout(); 169 170 style()->setDirection(oldTextDirection); 171 // These should always exist, unless someone mutates the shadow DOM (e.g., in the inspector). 172 if (!thumb || !track) 173 return; 174 175 double percentageOffset = sliderPosition(input).toDouble(); 176 LayoutUnit availableExtent = isVertical ? track->contentHeight() : track->contentWidth(); 177 availableExtent -= isVertical ? thumb->height() : thumb->width(); 178 LayoutUnit offset = percentageOffset * availableExtent; 179 LayoutPoint thumbLocation = thumb->location(); 180 if (isVertical) 181 thumbLocation.setY(thumbLocation.y() + track->contentHeight() - thumb->height() - offset); 182 else if (style()->isLeftToRightDirection()) 183 thumbLocation.setX(thumbLocation.x() + offset); 184 else 185 thumbLocation.setX(thumbLocation.x() - offset); 186 thumb->setLocation(thumbLocation); 187 if (checkForPaintInvalidationDuringLayout() && parent() 188 && (parent()->style()->appearance() == MediaVolumeSliderPart || parent()->style()->appearance() == MediaSliderPart)) { 189 // This will sometimes repaint too much. However, it is necessary to 190 // correctly repaint media controls (volume and timeline sliders) - 191 // they have special painting code in RenderMediaControls.cpp:paintMediaVolumeSlider 192 // and paintMediaSlider that gets called via -webkit-appearance and RenderTheme, 193 // so nothing else would otherwise invalidate the slider. 194 paintInvalidationForWholeRenderer(); 195 } 196 197 // We need one-off invalidation code here because painting of the timeline element does not go through style. 198 // Instead it has a custom implementation in C++ code. 199 // Therefore the style system cannot understand when it needs to be repainted. 200 setShouldDoFullPaintInvalidationAfterLayout(true); 201 } 202 203 // -------------------------------- 204 205 inline SliderThumbElement::SliderThumbElement(Document& document) 206 : HTMLDivElement(document) 207 , m_inDragMode(false) 208 { 209 } 210 211 PassRefPtrWillBeRawPtr<SliderThumbElement> SliderThumbElement::create(Document& document) 212 { 213 RefPtrWillBeRawPtr<SliderThumbElement> element = adoptRefWillBeNoop(new SliderThumbElement(document)); 214 element->setAttribute(idAttr, ShadowElementNames::sliderThumb()); 215 return element.release(); 216 } 217 218 void SliderThumbElement::setPositionFromValue() 219 { 220 // Since the code to calculate position is in the RenderSliderThumb layout 221 // path, we don't actually update the value here. Instead, we poke at the 222 // renderer directly to trigger layout. 223 if (renderer()) 224 renderer()->setNeedsLayoutAndFullPaintInvalidation(); 225 } 226 227 RenderObject* SliderThumbElement::createRenderer(RenderStyle*) 228 { 229 return new RenderSliderThumb(this); 230 } 231 232 bool SliderThumbElement::isDisabledFormControl() const 233 { 234 return hostInput() && hostInput()->isDisabledFormControl(); 235 } 236 237 bool SliderThumbElement::matchesReadOnlyPseudoClass() const 238 { 239 return hostInput() && hostInput()->matchesReadOnlyPseudoClass(); 240 } 241 242 bool SliderThumbElement::matchesReadWritePseudoClass() const 243 { 244 return hostInput() && hostInput()->matchesReadWritePseudoClass(); 245 } 246 247 Node* SliderThumbElement::focusDelegate() 248 { 249 return hostInput(); 250 } 251 252 void SliderThumbElement::dragFrom(const LayoutPoint& point) 253 { 254 RefPtrWillBeRawPtr<SliderThumbElement> protector(this); 255 startDragging(); 256 setPositionFromPoint(point); 257 } 258 259 void SliderThumbElement::setPositionFromPoint(const LayoutPoint& point) 260 { 261 RefPtrWillBeRawPtr<HTMLInputElement> input(hostInput()); 262 Element* trackElement = input->userAgentShadowRoot()->getElementById(ShadowElementNames::sliderTrack()); 263 264 if (!input->renderer() || !renderBox() || !trackElement->renderBox()) 265 return; 266 267 LayoutPoint offset = roundedLayoutPoint(input->renderer()->absoluteToLocal(point, UseTransforms)); 268 bool isVertical = hasVerticalAppearance(input.get()); 269 bool isLeftToRightDirection = renderBox()->style()->isLeftToRightDirection(); 270 LayoutUnit trackSize; 271 LayoutUnit position; 272 LayoutUnit currentPosition; 273 // We need to calculate currentPosition from absolute points becaue the 274 // renderer for this node is usually on a layer and renderBox()->x() and 275 // y() are unusable. 276 // FIXME: This should probably respect transforms. 277 LayoutPoint absoluteThumbOrigin = renderBox()->absoluteBoundingBoxRectIgnoringTransforms().location(); 278 LayoutPoint absoluteSliderContentOrigin = roundedLayoutPoint(input->renderer()->localToAbsolute()); 279 IntRect trackBoundingBox = trackElement->renderer()->absoluteBoundingBoxRectIgnoringTransforms(); 280 IntRect inputBoundingBox = input->renderer()->absoluteBoundingBoxRectIgnoringTransforms(); 281 if (isVertical) { 282 trackSize = trackElement->renderBox()->contentHeight() - renderBox()->height(); 283 position = offset.y() - renderBox()->height() / 2 - trackBoundingBox.y() + inputBoundingBox.y() - renderBox()->marginBottom(); 284 currentPosition = absoluteThumbOrigin.y() - absoluteSliderContentOrigin.y(); 285 } else { 286 trackSize = trackElement->renderBox()->contentWidth() - renderBox()->width(); 287 position = offset.x() - renderBox()->width() / 2 - trackBoundingBox.x() + inputBoundingBox.x(); 288 position -= isLeftToRightDirection ? renderBox()->marginLeft() : renderBox()->marginRight(); 289 currentPosition = absoluteThumbOrigin.x() - absoluteSliderContentOrigin.x(); 290 } 291 position = std::max<LayoutUnit>(0, std::min(position, trackSize)); 292 const Decimal ratio = Decimal::fromDouble(static_cast<double>(position) / trackSize); 293 const Decimal fraction = isVertical || !isLeftToRightDirection ? Decimal(1) - ratio : ratio; 294 StepRange stepRange(input->createStepRange(RejectAny)); 295 Decimal value = stepRange.clampValue(stepRange.valueFromProportion(fraction)); 296 297 Decimal closest = input->findClosestTickMarkValue(value); 298 if (closest.isFinite()) { 299 double closestFraction = stepRange.proportionFromValue(closest).toDouble(); 300 double closestRatio = isVertical || !isLeftToRightDirection ? 1.0 - closestFraction : closestFraction; 301 LayoutUnit closestPosition = trackSize * closestRatio; 302 const LayoutUnit snappingThreshold = 5; 303 if ((closestPosition - position).abs() <= snappingThreshold) 304 value = closest; 305 } 306 307 String valueString = serializeForNumberType(value); 308 if (valueString == input->value()) 309 return; 310 311 // FIXME: This is no longer being set from renderer. Consider updating the method name. 312 input->setValueFromRenderer(valueString); 313 if (renderer()) 314 renderer()->setNeedsLayoutAndFullPaintInvalidation(); 315 } 316 317 void SliderThumbElement::startDragging() 318 { 319 if (LocalFrame* frame = document().frame()) { 320 frame->eventHandler().setCapturingMouseEventsNode(this); 321 m_inDragMode = true; 322 } 323 } 324 325 void SliderThumbElement::stopDragging() 326 { 327 if (!m_inDragMode) 328 return; 329 330 if (LocalFrame* frame = document().frame()) 331 frame->eventHandler().setCapturingMouseEventsNode(nullptr); 332 m_inDragMode = false; 333 if (renderer()) 334 renderer()->setNeedsLayoutAndFullPaintInvalidation(); 335 if (hostInput()) 336 hostInput()->dispatchFormControlChangeEvent(); 337 } 338 339 void SliderThumbElement::defaultEventHandler(Event* event) 340 { 341 if (!event->isMouseEvent()) { 342 HTMLDivElement::defaultEventHandler(event); 343 return; 344 } 345 346 // FIXME: Should handle this readonly/disabled check in more general way. 347 // Missing this kind of check is likely to occur elsewhere if adding it in each shadow element. 348 HTMLInputElement* input = hostInput(); 349 if (!input || input->isDisabledOrReadOnly()) { 350 stopDragging(); 351 HTMLDivElement::defaultEventHandler(event); 352 return; 353 } 354 355 MouseEvent* mouseEvent = toMouseEvent(event); 356 bool isLeftButton = mouseEvent->button() == LeftButton; 357 const AtomicString& eventType = event->type(); 358 359 // We intentionally do not call event->setDefaultHandled() here because 360 // MediaControlTimelineElement::defaultEventHandler() wants to handle these 361 // mouse events. 362 if (eventType == EventTypeNames::mousedown && isLeftButton) { 363 startDragging(); 364 return; 365 } else if (eventType == EventTypeNames::mouseup && isLeftButton) { 366 stopDragging(); 367 return; 368 } else if (eventType == EventTypeNames::mousemove) { 369 if (m_inDragMode) 370 setPositionFromPoint(mouseEvent->absoluteLocation()); 371 return; 372 } 373 374 HTMLDivElement::defaultEventHandler(event); 375 } 376 377 bool SliderThumbElement::willRespondToMouseMoveEvents() 378 { 379 const HTMLInputElement* input = hostInput(); 380 if (input && !input->isDisabledOrReadOnly() && m_inDragMode) 381 return true; 382 383 return HTMLDivElement::willRespondToMouseMoveEvents(); 384 } 385 386 bool SliderThumbElement::willRespondToMouseClickEvents() 387 { 388 const HTMLInputElement* input = hostInput(); 389 if (input && !input->isDisabledOrReadOnly()) 390 return true; 391 392 return HTMLDivElement::willRespondToMouseClickEvents(); 393 } 394 395 void SliderThumbElement::detach(const AttachContext& context) 396 { 397 if (m_inDragMode) { 398 if (LocalFrame* frame = document().frame()) 399 frame->eventHandler().setCapturingMouseEventsNode(nullptr); 400 } 401 HTMLDivElement::detach(context); 402 } 403 404 HTMLInputElement* SliderThumbElement::hostInput() const 405 { 406 // Only HTMLInputElement creates SliderThumbElement instances as its shadow nodes. 407 // So, shadowHost() must be an HTMLInputElement. 408 return toHTMLInputElement(shadowHost()); 409 } 410 411 static const AtomicString& sliderThumbShadowPartId() 412 { 413 DEFINE_STATIC_LOCAL(const AtomicString, sliderThumb, ("-webkit-slider-thumb", AtomicString::ConstructFromLiteral)); 414 return sliderThumb; 415 } 416 417 static const AtomicString& mediaSliderThumbShadowPartId() 418 { 419 DEFINE_STATIC_LOCAL(const AtomicString, mediaSliderThumb, ("-webkit-media-slider-thumb", AtomicString::ConstructFromLiteral)); 420 return mediaSliderThumb; 421 } 422 423 const AtomicString& SliderThumbElement::shadowPseudoId() const 424 { 425 HTMLInputElement* input = hostInput(); 426 if (!input || !input->renderer()) 427 return sliderThumbShadowPartId(); 428 429 RenderStyle* sliderStyle = input->renderer()->style(); 430 switch (sliderStyle->appearance()) { 431 case MediaSliderPart: 432 case MediaSliderThumbPart: 433 case MediaVolumeSliderPart: 434 case MediaVolumeSliderThumbPart: 435 case MediaFullScreenVolumeSliderPart: 436 case MediaFullScreenVolumeSliderThumbPart: 437 return mediaSliderThumbShadowPartId(); 438 default: 439 return sliderThumbShadowPartId(); 440 } 441 } 442 443 // -------------------------------- 444 445 inline SliderContainerElement::SliderContainerElement(Document& document) 446 : HTMLDivElement(document) 447 { 448 } 449 450 DEFINE_NODE_FACTORY(SliderContainerElement) 451 452 RenderObject* SliderContainerElement::createRenderer(RenderStyle*) 453 { 454 return new RenderSliderContainer(this); 455 } 456 457 const AtomicString& SliderContainerElement::shadowPseudoId() 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() || !shadowHost()->renderer()) 463 return sliderContainer; 464 465 RenderStyle* sliderStyle = 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