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