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/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