Home | History | Annotate | Download | only in shadow
      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