/* * 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 "Event.h" #include "EventHandler.h" #include "Frame.h" #include "HTMLInputElement.h" #include "HTMLParserIdioms.h" #include "MouseEvent.h" #include "RenderFlexibleBox.h" #include "RenderSlider.h" #include "RenderTheme.h" #include "ShadowRoot.h" #if ENABLE(IOS_TOUCH_EVENTS) #include "Document.h" #include "Page.h" #include "TouchEvent.h" #endif 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()); const RenderStyle& sliderStyle = input.renderer()->style(); #if ENABLE(VIDEO) if (sliderStyle.appearance() == MediaVolumeSliderPart && input.renderer()->theme().usesVerticalVolumeSlider()) return true; #endif return sliderStyle.appearance() == SliderVerticalPart; } // -------------------------------- RenderSliderThumb::RenderSliderThumb(SliderThumbElement& element, Ref&& style) : RenderBlockFlow(element, WTFMove(style)) { } 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()) { ASSERT(element()); theme().adjustSliderThumbSize(style(), element()); } } bool RenderSliderThumb::isSliderThumb() const { return true; } // -------------------------------- // FIXME: Find a way to cascade appearance and adjust heights, and get rid of this class. // http://webkit.org/b/62535 class RenderSliderContainer final : public RenderFlexibleBox { public: RenderSliderContainer(SliderContainerElement& element, Ref&& style) : RenderFlexibleBox(element, WTFMove(style)) { } public: virtual void computeLogicalHeight(LayoutUnit logicalHeight, LayoutUnit logicalTop, LogicalExtentComputedValues&) const override; private: virtual void layout() override; bool isFlexibleBoxImpl() const override { return true; } }; void RenderSliderContainer::computeLogicalHeight(LayoutUnit logicalHeight, LayoutUnit logicalTop, LogicalExtentComputedValues& computedValues) const { ASSERT(element()->shadowHost()); auto& input = downcast(*element()->shadowHost()); bool isVertical = hasVerticalAppearance(input); #if ENABLE(DATALIST_ELEMENT) if (input.renderer()->isSlider() && !isVertical && input.list()) { int offsetFromCenter = theme().sliderTickOffsetFromTrackCenter(); LayoutUnit 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; RenderBox::computeLogicalHeight(trackHeight, logicalTop, computedValues); return; } #endif if (isVertical) logicalHeight = RenderSlider::defaultTrackLength; RenderBox::computeLogicalHeight(logicalHeight, logicalTop, computedValues); } void RenderSliderContainer::layout() { ASSERT(element()->shadowHost()); auto& input = downcast(*element()->shadowHost()); bool isVertical = hasVerticalAppearance(input); style().setFlexDirection(isVertical ? FlowColumn : FlowRow); TextDirection oldTextDirection = style().direction(); if (isVertical) { // FIXME: Work around rounding issues in RTL vertical sliders. We want them to // render identically to LTR vertical sliders. We can remove this work around when // subpixel rendering is enabled on all ports. style().setDirection(LTR); } RenderBox* thumb = input.sliderThumbElement() ? input.sliderThumbElement()->renderBox() : nullptr; RenderBox* track = input.sliderTrackElement() ? input.sliderTrackElement()->renderBox() : nullptr; // Force a layout to reset the position of the thumb so the code below doesn't move the thumb to the wrong place. // FIXME: Make a custom Render class for the track and move the thumb positioning code there. if (track) track->setChildNeedsLayout(MarkOnlyThis); RenderFlexibleBox::layout(); style().setDirection(oldTextDirection); // These should always exist, unless someone mutates the shadow DOM (e.g., in the inspector). if (!thumb || !track) return; double percentageOffset = sliderPosition(input).toDouble(); LayoutUnit availableExtent = isVertical ? track->contentHeight() : track->contentWidth(); availableExtent -= isVertical ? thumb->height() : thumb->width(); LayoutUnit offset = percentageOffset * availableExtent; LayoutPoint thumbLocation = thumb->location(); if (isVertical) thumbLocation.setY(thumbLocation.y() + track->contentHeight() - thumb->height() - offset); else if (style().isLeftToRightDirection()) thumbLocation.setX(thumbLocation.x() + offset); else thumbLocation.setX(thumbLocation.x() - offset); thumb->setLocation(thumbLocation); thumb->repaint(); } // -------------------------------- SliderThumbElement::SliderThumbElement(Document& document) : HTMLDivElement(HTMLNames::divTag, document) , m_inDragMode(false) #if ENABLE(IOS_TOUCH_EVENTS) , m_exclusiveTouchIdentifier(NoIdentifier) , m_isRegisteredAsTouchEventListener(false) #endif { setHasCustomStyleResolveCallbacks(); } 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(); } RenderPtr SliderThumbElement::createElementRenderer(Ref&& style, const RenderTreePosition&) { return createRenderer(*this, WTFMove(style)); } bool SliderThumbElement::isDisabledFormControl() const { HTMLInputElement* input = hostInput(); return !input || input->isDisabledFormControl(); } bool SliderThumbElement::matchesReadWritePseudoClass() const { HTMLInputElement* input = hostInput(); return input && input->matchesReadWritePseudoClass(); } Element* SliderThumbElement::focusDelegate() { return hostInput(); } void SliderThumbElement::dragFrom(const LayoutPoint& point) { Ref protect(*this); setPositionFromPoint(point); #if !PLATFORM(IOS) startDragging(); #endif } void SliderThumbElement::setPositionFromPoint(const LayoutPoint& absolutePoint) { RefPtr input = hostInput(); if (!input || !input->renderer() || !renderBox()) return; HTMLElement* trackElement = input->sliderTrackElement(); if (!trackElement->renderBox()) return; // Do all the tracking math relative to the input's renderer's box. RenderBox& inputRenderer = downcast(*input->renderer()); RenderBox& trackRenderer = *trackElement->renderBox(); bool isVertical = hasVerticalAppearance(*input); bool isLeftToRightDirection = renderBox()->style().isLeftToRightDirection(); LayoutPoint offset(inputRenderer.absoluteToLocal(absolutePoint, UseTransforms)); FloatRect trackBoundingBox = trackRenderer.localToContainerQuad(FloatRect(0, 0, trackRenderer.width(), trackRenderer.height()), &inputRenderer).enclosingBoundingBox(); LayoutUnit trackLength; LayoutUnit position; if (isVertical) { trackLength = trackRenderer.contentHeight() - renderBox()->height(); position = offset.y() - renderBox()->height() / 2 - trackBoundingBox.y() - renderBox()->marginBottom(); } else { trackLength = trackRenderer.contentWidth() - renderBox()->width(); position = offset.x() - renderBox()->width() / 2 - trackBoundingBox.x(); position -= isLeftToRightDirection ? renderBox()->marginLeft() : renderBox()->marginRight(); } position = std::max(0, std::min(position, trackLength)); const Decimal ratio = Decimal::fromDouble(static_cast(position) / trackLength); 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) { if (Optional closest = input->findClosestTickMarkValue(value)) { double closestFraction = stepRange.proportionFromValue(*closest).toDouble(); double closestRatio = isVertical || !isLeftToRightDirection ? 1.0 - closestFraction : closestFraction; LayoutUnit closestPosition = trackLength * closestRatio; if ((closestPosition - position).abs() <= snappingThreshold) value = *closest; } } #endif String valueString = serializeForNumberType(value); if (valueString == input->value()) return; // FIXME: This is no longer being set from renderer. Consider updating the method name. input->setValueFromRenderer(valueString); if (renderer()) renderer()->setNeedsLayout(); } void SliderThumbElement::startDragging() { if (Frame* frame = document().frame()) { frame->eventHandler().setCapturingMouseEventsElement(this); m_inDragMode = true; } } void SliderThumbElement::stopDragging() { if (!m_inDragMode) return; if (Frame* frame = document().frame()) frame->eventHandler().setCapturingMouseEventsElement(nullptr); m_inDragMode = false; if (renderer()) renderer()->setNeedsLayout(); RefPtr input = hostInput(); if (input) input->dispatchFormControlChangeEvent(); } #if !PLATFORM(IOS) void SliderThumbElement::defaultEventHandler(Event* event) { if (!is(*event)) { 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->isDisabledOrReadOnly()) { stopDragging(); HTMLDivElement::defaultEventHandler(event); return; } MouseEvent& mouseEvent = downcast(*event); bool isLeftButton = mouseEvent.button() == LeftButton; const AtomicString& eventType = mouseEvent.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(&mouseEvent); } #endif #if !PLATFORM(IOS) bool SliderThumbElement::willRespondToMouseMoveEvents() { const HTMLInputElement* input = hostInput(); if (input && !input->isDisabledOrReadOnly() && m_inDragMode) return true; return HTMLDivElement::willRespondToMouseMoveEvents(); } bool SliderThumbElement::willRespondToMouseClickEvents() { const HTMLInputElement* input = hostInput(); if (input && !input->isDisabledOrReadOnly()) return true; return HTMLDivElement::willRespondToMouseClickEvents(); } #endif // !PLATFORM(IOS) void SliderThumbElement::willDetachRenderers() { if (m_inDragMode) { if (Frame* frame = document().frame()) frame->eventHandler().setCapturingMouseEventsElement(nullptr); } #if ENABLE(IOS_TOUCH_EVENTS) unregisterForTouchEvents(); #endif } #if ENABLE(IOS_TOUCH_EVENTS) unsigned SliderThumbElement::exclusiveTouchIdentifier() const { return m_exclusiveTouchIdentifier; } void SliderThumbElement::setExclusiveTouchIdentifier(unsigned identifier) { ASSERT(m_exclusiveTouchIdentifier == NoIdentifier); m_exclusiveTouchIdentifier = identifier; } void SliderThumbElement::clearExclusiveTouchIdentifier() { m_exclusiveTouchIdentifier = NoIdentifier; } static Touch* findTouchWithIdentifier(TouchList& list, unsigned identifier) { unsigned length = list.length(); for (unsigned i = 0; i < length; ++i) { Touch* touch = list.item(i); if (touch->identifier() == identifier) return touch; } return nullptr; } void SliderThumbElement::handleTouchStart(TouchEvent* touchEvent) { TouchList* targetTouches = touchEvent->targetTouches(); if (!targetTouches) return; if (targetTouches->length() != 1) return; Touch* touch = targetTouches->item(0); if (!renderer()) return; IntRect boundingBox = renderer()->absoluteBoundingBoxRect(); // Ignore the touch if it is not really inside the thumb. if (!boundingBox.contains(touch->pageX(), touch->pageY())) return; setExclusiveTouchIdentifier(touch->identifier()); startDragging(); touchEvent->setDefaultHandled(); } void SliderThumbElement::handleTouchMove(TouchEvent* touchEvent) { unsigned identifier = exclusiveTouchIdentifier(); if (identifier == NoIdentifier) return; TouchList* targetTouches = touchEvent->targetTouches(); if (!targetTouches) return; Touch* touch = findTouchWithIdentifier(*targetTouches, identifier); if (!touch) return; if (m_inDragMode) setPositionFromPoint(IntPoint(touch->pageX(), touch->pageY())); touchEvent->setDefaultHandled(); } void SliderThumbElement::handleTouchEndAndCancel(TouchEvent* touchEvent) { unsigned identifier = exclusiveTouchIdentifier(); if (identifier == NoIdentifier) return; TouchList* targetTouches = touchEvent->targetTouches(); if (!targetTouches) return; // If our exclusive touch still exists, it was not the touch // that ended, so we should not stop dragging. Touch* exclusiveTouch = findTouchWithIdentifier(*targetTouches, identifier); if (exclusiveTouch) return; clearExclusiveTouchIdentifier(); stopDragging(); } void SliderThumbElement::didAttachRenderers() { if (shouldAcceptTouchEvents()) registerForTouchEvents(); } void SliderThumbElement::handleTouchEvent(TouchEvent* touchEvent) { HTMLInputElement* input = hostInput(); ASSERT(input); if (input->isReadOnly() || input->isDisabledFormControl()) { clearExclusiveTouchIdentifier(); stopDragging(); touchEvent->setDefaultHandled(); HTMLDivElement::defaultEventHandler(touchEvent); return; } const AtomicString& eventType = touchEvent->type(); if (eventType == eventNames().touchstartEvent) { handleTouchStart(touchEvent); return; } if (eventType == eventNames().touchendEvent || eventType == eventNames().touchcancelEvent) { handleTouchEndAndCancel(touchEvent); return; } if (eventType == eventNames().touchmoveEvent) { handleTouchMove(touchEvent); return; } HTMLDivElement::defaultEventHandler(touchEvent); } bool SliderThumbElement::shouldAcceptTouchEvents() { return renderer() && !isDisabledFormControl(); } void SliderThumbElement::registerForTouchEvents() { if (m_isRegisteredAsTouchEventListener) return; ASSERT(shouldAcceptTouchEvents()); document().addTouchEventListener(this); m_isRegisteredAsTouchEventListener = true; } void SliderThumbElement::unregisterForTouchEvents() { if (!m_isRegisteredAsTouchEventListener) return; clearExclusiveTouchIdentifier(); stopDragging(); document().removeTouchEventListener(this); m_isRegisteredAsTouchEventListener = false; } void SliderThumbElement::disabledAttributeChanged() { if (shouldAcceptTouchEvents()) registerForTouchEvents(); else unregisterForTouchEvents(); } #endif // ENABLE(IOS_TOUCH_EVENTS) HTMLInputElement* SliderThumbElement::hostInput() const { // Only HTMLInputElement creates SliderThumbElement instances as its shadow nodes. // So, shadowHost() must be an HTMLInputElement. return downcast(shadowHost()); } static const AtomicString& sliderThumbShadowPseudoId() { static NeverDestroyed sliderThumb("-webkit-slider-thumb", AtomicString::ConstructFromLiteral); return sliderThumb; } static const AtomicString& mediaSliderThumbShadowPseudoId() { static NeverDestroyed mediaSliderThumb("-webkit-media-slider-thumb", AtomicString::ConstructFromLiteral); return mediaSliderThumb; } const AtomicString& SliderThumbElement::shadowPseudoId() const { // FIXME: this code needs to go away, it is very very wrong. // The value of shadowPseudoId() is needed to resolve the style of the shadow tree. In this case, // that value depends on the style, which means the style needs to be computed twice to get // a correct value: once to get the Input's appearance, then a second time to style the shadow tree correctly. HTMLInputElement* input = hostInput(); if (!input) return sliderThumbShadowPseudoId(); if (!input->renderer()) return emptyAtom; const 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(); } } Ref SliderThumbElement::cloneElementWithoutAttributesAndChildren(Document& targetDocument) { return create(targetDocument); } // -------------------------------- inline SliderContainerElement::SliderContainerElement(Document& document) : HTMLDivElement(HTMLNames::divTag, document) { } Ref SliderContainerElement::create(Document& document) { return adoptRef(*new SliderContainerElement(document)); } RenderPtr SliderContainerElement::createElementRenderer(Ref&& style, const RenderTreePosition&) { return createRenderer(*this, WTFMove(style)); } const AtomicString& SliderContainerElement::shadowPseudoId() const { // FIXME: this code needs to go away, it is very very wrong. // The value of shadowPseudoId() is needed to resolve the style of the shadow tree. In this case, // that value depends on the style, which means the style needs to be computed twice to get // a correct value: once to get the Input's appearance, then a second time to style the shadow tree correctly. static NeverDestroyed mediaSliderContainer("-webkit-media-slider-container", AtomicString::ConstructFromLiteral); static NeverDestroyed sliderContainer("-webkit-slider-container", AtomicString::ConstructFromLiteral); if (!is(*shadowHost())) return sliderContainer; auto& input = downcast(*shadowHost()); if (!input.renderer()) return emptyAtom; const 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; } } }