blob: d8bbd2bbb4ab83fca3721f69e6502b7a732bbdf8 [file] [log] [blame]
/*
* Copyright (C) 2006, 2007, 2008, 2009 Apple Inc. All rights reserved.
* Copyright (C) 2010 Google Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
* * Neither the name of Google Inc. nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "config.h"
#include "SliderThumbElement.h"
#include "CSSValueKeywords.h"
#include "ElementShadow.h"
#include "Event.h"
#include "Frame.h"
#include "HTMLInputElement.h"
#include "HTMLParserIdioms.h"
#include "MouseEvent.h"
#include "RenderDeprecatedFlexibleBox.h"
#include "RenderSlider.h"
#include "RenderTheme.h"
#include "ShadowRoot.h"
#include "StepRange.h"
#include <wtf/MathExtras.h>
using namespace std;
namespace WebCore {
using namespace HTMLNames;
inline static Decimal sliderPosition(HTMLInputElement* element)
{
const StepRange stepRange(element->createStepRange(RejectAny));
const Decimal oldValue = parseToDecimalForNumberType(element->value(), stepRange.defaultValue());
return stepRange.proportionFromValue(stepRange.clampValue(oldValue));
}
inline static bool hasVerticalAppearance(HTMLInputElement* input)
{
ASSERT(input->renderer());
RenderStyle* sliderStyle = input->renderer()->style();
#if ENABLE(VIDEO)
if (sliderStyle->appearance() == MediaVolumeSliderPart && input->renderer()->theme()->usesVerticalVolumeSlider())
return true;
#endif
return sliderStyle->appearance() == SliderVerticalPart;
}
SliderThumbElement* sliderThumbElementOf(Node* node)
{
ASSERT(node);
ShadowRoot* shadow = node->toInputElement()->userAgentShadowRoot();
ASSERT(shadow);
Node* thumb = shadow->firstChild()->firstChild()->firstChild();
ASSERT(thumb);
return toSliderThumbElement(thumb);
}
HTMLElement* sliderTrackElementOf(Node* node)
{
ASSERT(node);
ShadowRoot* shadow = node->toInputElement()->userAgentShadowRoot();
ASSERT(shadow);
Node* track = shadow->firstChild()->firstChild();
ASSERT(track);
return toHTMLElement(track);
}
// --------------------------------
RenderSliderThumb::RenderSliderThumb(Node* node)
: RenderBlock(node)
{
}
void RenderSliderThumb::updateAppearance(RenderStyle* parentStyle)
{
if (parentStyle->appearance() == SliderVerticalPart)
style()->setAppearance(SliderThumbVerticalPart);
else if (parentStyle->appearance() == SliderHorizontalPart)
style()->setAppearance(SliderThumbHorizontalPart);
else if (parentStyle->appearance() == MediaSliderPart)
style()->setAppearance(MediaSliderThumbPart);
else if (parentStyle->appearance() == MediaVolumeSliderPart)
style()->setAppearance(MediaVolumeSliderThumbPart);
else if (parentStyle->appearance() == MediaFullScreenVolumeSliderPart)
style()->setAppearance(MediaFullScreenVolumeSliderThumbPart);
if (style()->hasAppearance())
theme()->adjustSliderThumbSize(style(), toElement(node()));
}
bool RenderSliderThumb::isSliderThumb() const
{
return true;
}
void RenderSliderThumb::layout()
{
// Do not cast node() to SliderThumbElement. This renderer is used for
// TrackLimitElement too.
HTMLInputElement* input = node()->shadowHost()->toInputElement();
bool isVertical = hasVerticalAppearance(input);
double fraction = (sliderPosition(input) * 100).toDouble();
if (isVertical)
style()->setTop(Length(100 - fraction, Percent));
else if (style()->isLeftToRightDirection())
style()->setLeft(Length(fraction, Percent));
else
style()->setRight(Length(fraction, Percent));
RenderBlock::layout();
}
// --------------------------------
// FIXME: Find a way to cascade appearance and adjust heights, and get rid of this class.
// http://webkit.org/b/62535
class RenderSliderContainer : public RenderDeprecatedFlexibleBox {
public:
RenderSliderContainer(Node* node)
: RenderDeprecatedFlexibleBox(node) { }
private:
virtual void layout();
};
void RenderSliderContainer::layout()
{
HTMLInputElement* input = node()->shadowHost()->toInputElement();
bool isVertical = hasVerticalAppearance(input);
style()->setBoxOrient(isVertical ? VERTICAL : HORIZONTAL);
// Sets the concrete height if the height of the <input> is not fixed or a
// percentage value because a percentage height value of this box won't be
// based on the <input> height in such case.
if (input->renderer()->isSlider()) {
if (!isVertical) {
RenderObject* trackRenderer = node()->firstChild()->renderer();
Length inputHeight = input->renderer()->style()->height();
if (!inputHeight.isSpecified()) {
RenderObject* thumbRenderer = input->sliderThumbElement()->renderer();
if (thumbRenderer) {
Length height = thumbRenderer->style()->height();
#if ENABLE(DATALIST_ELEMENT)
if (input && input->list()) {
int offsetFromCenter = theme()->sliderTickOffsetFromTrackCenter();
int trackHeight = 0;
if (offsetFromCenter < 0)
trackHeight = -2 * offsetFromCenter;
else {
int tickLength = theme()->sliderTickSize().height();
trackHeight = 2 * (offsetFromCenter + tickLength);
}
float zoomFactor = style()->effectiveZoom();
if (zoomFactor != 1.0)
trackHeight *= zoomFactor;
height = Length(trackHeight, Fixed);
}
#endif
style()->setHeight(height);
}
} else {
style()->setHeight(Length(100, Percent));
if (trackRenderer)
trackRenderer->style()->setHeight(Length());
}
}
}
RenderDeprecatedFlexibleBox::layout();
// Percentage 'top' for the thumb doesn't work if the parent style has no
// concrete height.
Node* track = node()->firstChild();
if (track && track->renderer()->isBox()) {
RenderBox* trackBox = track->renderBox();
trackBox->style()->setHeight(Length(trackBox->height() - trackBox->borderAndPaddingHeight(), Fixed));
}
}
// --------------------------------
void SliderThumbElement::setPositionFromValue()
{
// Since the code to calculate position is in the RenderSliderThumb layout
// path, we don't actually update the value here. Instead, we poke at the
// renderer directly to trigger layout.
if (renderer())
renderer()->setNeedsLayout(true);
}
RenderObject* SliderThumbElement::createRenderer(RenderArena* arena, RenderStyle*)
{
return new (arena) RenderSliderThumb(this);
}
bool SliderThumbElement::isEnabledFormControl() const
{
return hostInput()->isEnabledFormControl();
}
bool SliderThumbElement::shouldMatchReadOnlySelector() const
{
return hostInput()->shouldMatchReadOnlySelector();
}
bool SliderThumbElement::shouldMatchReadWriteSelector() const
{
return hostInput()->shouldMatchReadWriteSelector();
}
Node* SliderThumbElement::focusDelegate()
{
return hostInput();
}
void SliderThumbElement::dragFrom(const LayoutPoint& point)
{
setPositionFromPoint(point);
startDragging();
}
void SliderThumbElement::setPositionFromPoint(const LayoutPoint& point)
{
HTMLInputElement* input = hostInput();
HTMLElement* trackElement = sliderTrackElementOf(input);
if (!input->renderer() || !renderer() || !trackElement->renderer())
return;
input->setTextAsOfLastFormControlChangeEvent(input->value());
LayoutPoint offset = roundedLayoutPoint(input->renderer()->absoluteToLocal(point, false, true));
bool isVertical = hasVerticalAppearance(input);
bool isLeftToRightDirection = renderBox()->style()->isLeftToRightDirection();
LayoutUnit trackSize;
LayoutUnit position;
LayoutUnit currentPosition;
// We need to calculate currentPosition from absolute points becaue the
// renderer for this node is usually on a layer and renderBox()->x() and
// y() are unusable.
// FIXME: This should probably respect transforms.
LayoutPoint absoluteThumbOrigin = renderBox()->absoluteBoundingBoxRectIgnoringTransforms().location();
LayoutPoint absoluteSliderContentOrigin = roundedLayoutPoint(input->renderer()->localToAbsolute());
IntRect trackBoundingBox = trackElement->renderer()->absoluteBoundingBoxRectIgnoringTransforms();
IntRect inputBoundingBox = input->renderer()->absoluteBoundingBoxRectIgnoringTransforms();
if (isVertical) {
trackSize = trackElement->renderBox()->contentHeight();
position = offset.y() - renderBox()->height() / 2 - trackBoundingBox.y() + inputBoundingBox.y() - renderBox()->marginBottom();
currentPosition = absoluteThumbOrigin.y() - absoluteSliderContentOrigin.y();
} else {
trackSize = trackElement->renderBox()->contentWidth();
position = offset.x() - renderBox()->width() / 2 - trackBoundingBox.x() + inputBoundingBox.x();
if (isLeftToRightDirection)
position -= renderBox()->marginLeft();
else
position += renderBox()->width() - renderBox()->marginRight();
currentPosition = absoluteThumbOrigin.x() - absoluteSliderContentOrigin.x();
}
position = max<LayoutUnit>(0, min(position, trackSize));
if (position == currentPosition)
return;
const Decimal ratio = Decimal::fromDouble(static_cast<double>(position) / trackSize);
const Decimal fraction = isVertical || !isLeftToRightDirection ? Decimal(1) - ratio : ratio;
StepRange stepRange(input->createStepRange(RejectAny));
Decimal value = stepRange.clampValue(stepRange.valueFromProportion(fraction));
#if ENABLE(DATALIST_ELEMENT)
const LayoutUnit snappingThreshold = renderer()->theme()->sliderTickSnappingThreshold();
if (snappingThreshold > 0) {
Decimal closest = input->findClosestTickMarkValue(value);
if (closest.isFinite()) {
double closestFraction = stepRange.proportionFromValue(closest).toDouble();
double closestRatio = isVertical || !isLeftToRightDirection ? 1.0 - closestFraction : closestFraction;
LayoutUnit closestPosition = trackSize * closestRatio;
if ((closestPosition - position).abs() <= snappingThreshold)
value = closest;
}
}
#endif
// FIXME: This is no longer being set from renderer. Consider updating the method name.
input->setValueFromRenderer(serializeForNumberType(value));
renderer()->setNeedsLayout(true);
input->dispatchFormControlChangeEvent();
}
void SliderThumbElement::startDragging()
{
if (Frame* frame = document()->frame()) {
frame->eventHandler()->setCapturingMouseEventsNode(this);
m_inDragMode = true;
}
}
void SliderThumbElement::stopDragging()
{
if (!m_inDragMode)
return;
if (Frame* frame = document()->frame())
frame->eventHandler()->setCapturingMouseEventsNode(0);
m_inDragMode = false;
if (renderer())
renderer()->setNeedsLayout(true);
}
void SliderThumbElement::defaultEventHandler(Event* event)
{
if (!event->isMouseEvent()) {
HTMLDivElement::defaultEventHandler(event);
return;
}
// FIXME: Should handle this readonly/disabled check in more general way.
// Missing this kind of check is likely to occur elsewhere if adding it in each shadow element.
HTMLInputElement* input = hostInput();
if (!input || input->readOnly() || !input->isEnabledFormControl()) {
stopDragging();
HTMLDivElement::defaultEventHandler(event);
return;
}
MouseEvent* mouseEvent = static_cast<MouseEvent*>(event);
bool isLeftButton = mouseEvent->button() == LeftButton;
const AtomicString& eventType = event->type();
// We intentionally do not call event->setDefaultHandled() here because
// MediaControlTimelineElement::defaultEventHandler() wants to handle these
// mouse events.
if (eventType == eventNames().mousedownEvent && isLeftButton) {
startDragging();
return;
} else if (eventType == eventNames().mouseupEvent && isLeftButton) {
stopDragging();
return;
} else if (eventType == eventNames().mousemoveEvent) {
if (m_inDragMode)
setPositionFromPoint(mouseEvent->absoluteLocation());
return;
}
HTMLDivElement::defaultEventHandler(event);
}
bool SliderThumbElement::willRespondToMouseMoveEvents()
{
const HTMLInputElement* input = hostInput();
if (input && !input->readOnly() && input->isEnabledFormControl() && m_inDragMode)
return true;
return HTMLDivElement::willRespondToMouseMoveEvents();
}
bool SliderThumbElement::willRespondToMouseClickEvents()
{
const HTMLInputElement* input = hostInput();
if (input && !input->readOnly() && input->isEnabledFormControl())
return true;
return HTMLDivElement::willRespondToMouseClickEvents();
}
void SliderThumbElement::detach()
{
if (m_inDragMode) {
if (Frame* frame = document()->frame())
frame->eventHandler()->setCapturingMouseEventsNode(0);
}
HTMLDivElement::detach();
}
HTMLInputElement* SliderThumbElement::hostInput() const
{
// Only HTMLInputElement creates SliderThumbElement instances as its shadow nodes.
// So, shadowHost() must be an HTMLInputElement.
return shadowHost()->toInputElement();
}
static const AtomicString& sliderThumbShadowPseudoId()
{
DEFINE_STATIC_LOCAL(const AtomicString, sliderThumb, ("-webkit-slider-thumb"));
return sliderThumb;
}
static const AtomicString& mediaSliderThumbShadowPseudoId()
{
DEFINE_STATIC_LOCAL(const AtomicString, mediaSliderThumb, ("-webkit-media-slider-thumb"));
return mediaSliderThumb;
}
const AtomicString& SliderThumbElement::shadowPseudoId() const
{
HTMLInputElement* input = hostInput();
if (!input)
return sliderThumbShadowPseudoId();
RenderStyle* sliderStyle = input->renderer()->style();
switch (sliderStyle->appearance()) {
case MediaSliderPart:
case MediaSliderThumbPart:
case MediaVolumeSliderPart:
case MediaVolumeSliderThumbPart:
case MediaFullScreenVolumeSliderPart:
case MediaFullScreenVolumeSliderThumbPart:
return mediaSliderThumbShadowPseudoId();
default:
return sliderThumbShadowPseudoId();
}
}
// --------------------------------
inline TrackLimiterElement::TrackLimiterElement(Document* document)
: HTMLDivElement(HTMLNames::divTag, document)
{
}
PassRefPtr<TrackLimiterElement> TrackLimiterElement::create(Document* document)
{
RefPtr<TrackLimiterElement> element = adoptRef(new TrackLimiterElement(document));
element->setInlineStyleProperty(CSSPropertyVisibility, CSSValueHidden);
element->setInlineStyleProperty(CSSPropertyPosition, CSSValueStatic);
return element.release();
}
RenderObject* TrackLimiterElement::createRenderer(RenderArena* arena, RenderStyle*)
{
return new (arena) RenderSliderThumb(this);
}
const AtomicString& TrackLimiterElement::shadowPseudoId() const
{
HTMLInputElement* input = shadowHost()->toInputElement();
if (!input)
return sliderThumbShadowPseudoId();
RenderStyle* sliderStyle = input->renderer()->style();
switch (sliderStyle->appearance()) {
case MediaSliderPart:
case MediaSliderThumbPart:
case MediaVolumeSliderPart:
case MediaVolumeSliderThumbPart:
case MediaFullScreenVolumeSliderPart:
case MediaFullScreenVolumeSliderThumbPart:
return mediaSliderThumbShadowPseudoId();
default:
return sliderThumbShadowPseudoId();
}
}
TrackLimiterElement* trackLimiterElementOf(Node* node)
{
ASSERT(node);
ShadowRoot* shadow = node->toInputElement()->userAgentShadowRoot();
ASSERT(shadow);
Node* limiter = shadow->firstChild()->lastChild();
ASSERT(limiter);
return static_cast<TrackLimiterElement*>(limiter);
}
// --------------------------------
inline SliderContainerElement::SliderContainerElement(Document* document)
: HTMLDivElement(HTMLNames::divTag, document)
{
}
PassRefPtr<SliderContainerElement> SliderContainerElement::create(Document* document)
{
return adoptRef(new SliderContainerElement(document));
}
RenderObject* SliderContainerElement::createRenderer(RenderArena* arena, RenderStyle*)
{
return new (arena) RenderSliderContainer(this);
}
const AtomicString& SliderContainerElement::shadowPseudoId() const
{
DEFINE_STATIC_LOCAL(const AtomicString, mediaSliderContainer, ("-webkit-media-slider-container"));
DEFINE_STATIC_LOCAL(const AtomicString, sliderContainer, ("-webkit-slider-container"));
HTMLInputElement* input = shadowHost()->toInputElement();
if (!input)
return sliderContainer;
RenderStyle* sliderStyle = input->renderer()->style();
switch (sliderStyle->appearance()) {
case MediaSliderPart:
case MediaSliderThumbPart:
case MediaVolumeSliderPart:
case MediaVolumeSliderThumbPart:
case MediaFullScreenVolumeSliderPart:
case MediaFullScreenVolumeSliderThumbPart:
return mediaSliderContainer;
default:
return sliderContainer;
}
}
}