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