/* * 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 "EventNames.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" #include "StyleResolver.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, RenderStyle&& style) : RenderBlockFlow(element, WTFMove(style)) { } void RenderSliderThumb::updateAppearance(const RenderStyle* parentStyle) { if (parentStyle->appearance() == SliderVerticalPart) mutableStyle().setAppearance(SliderThumbVerticalPart); else if (parentStyle->appearance() == SliderHorizontalPart) mutableStyle().setAppearance(SliderThumbHorizontalPart); else if (parentStyle->appearance() == MediaSliderPart) mutableStyle().setAppearance(MediaSliderThumbPart); else if (parentStyle->appearance() == MediaVolumeSliderPart) mutableStyle().setAppearance(MediaVolumeSliderThumbPart); else if (parentStyle->appearance() == MediaFullScreenVolumeSliderPart) mutableStyle().setAppearance(MediaFullScreenVolumeSliderThumbPart); if (style().hasAppearance()) { ASSERT(element()); theme().adjustSliderThumbSize(mutableStyle(), 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, RenderStyle&& style) : RenderFlexibleBox(element, WTFMove(style)) { } public: RenderBox::LogicalExtentComputedValues computeLogicalHeight(LayoutUnit logicalHeight, LayoutUnit logicalTop) const override; private: void layout() override; bool isFlexibleBoxImpl() const override { return true; } }; RenderBox::LogicalExtentComputedValues RenderSliderContainer::computeLogicalHeight(LayoutUnit logicalHeight, LayoutUnit logicalTop) 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; return RenderBox::computeLogicalHeight(trackHeight, logicalTop); } #endif if (isVertical) logicalHeight = RenderSlider::defaultTrackLength; return RenderBox::computeLogicalHeight(logicalHeight, logicalTop); } void RenderSliderContainer::layout() { ASSERT(element()->shadowHost()); auto& input = downcast(*element()->shadowHost()); bool isVertical = hasVerticalAppearance(input); mutableStyle().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. mutableStyle().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(); mutableStyle().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(RenderStyle&& 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 protectedThis(*this); setPositionFromPoint(point); #if !PLATFORM(IOS) startDragging(); #endif } void SliderThumbElement::setPositionFromPoint(const LayoutPoint& absolutePoint) { RefPtr input = hostInput(); if (!input) return; auto* inputRenderer = input->renderBox(); if (!inputRenderer) return; auto* thumbRenderer = renderBox(); if (!thumbRenderer) return; ASSERT(input->sliderTrackElement()); auto* trackRenderer = input->sliderTrackElement()->renderBox(); if (!trackRenderer) return; // Do all the tracking math relative to the input's renderer's box. bool isVertical = hasVerticalAppearance(*input); bool isLeftToRightDirection = thumbRenderer->style().isLeftToRightDirection(); auto offset = inputRenderer->absoluteToLocal(absolutePoint, UseTransforms); auto trackBoundingBox = trackRenderer->localToContainerQuad(FloatRect { { }, trackRenderer->size() }, inputRenderer).enclosingBoundingBox(); LayoutUnit trackLength; LayoutUnit position; if (isVertical) { trackLength = trackRenderer->contentHeight() - thumbRenderer->height(); position = offset.y() - thumbRenderer->height() / 2 - trackBoundingBox.y() - thumbRenderer->marginBottom(); } else { trackLength = trackRenderer->contentWidth() - thumbRenderer->width(); position = offset.x() - thumbRenderer->width() / 2 - trackBoundingBox.x(); position -= isLeftToRightDirection ? thumbRenderer->marginLeft() : thumbRenderer->marginRight(); } position = std::max(0, std::min(position, trackLength)); auto ratio = Decimal::fromDouble(static_cast(position) / trackLength); auto fraction = isVertical || !isLeftToRightDirection ? Decimal(1) - ratio : ratio; auto stepRange = input->createStepRange(RejectAny); auto value = stepRange.clampValue(stepRange.valueFromProportion(fraction)); #if ENABLE(DATALIST_ELEMENT) const LayoutUnit snappingThreshold = renderer()->theme().sliderTickSnappingThreshold(); if (snappingThreshold > 0) { if (std::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(); } #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->isDisabledFormControl()) { 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) { input->dispatchFormControlChangeEvent(); 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->isDisabledFormControl() && m_inDragMode) return true; return HTMLDivElement::willRespondToMouseMoveEvents(); } bool SliderThumbElement::willRespondToMouseClickEvents() { const HTMLInputElement* input = hostInput(); if (input && !input->isDisabledFormControl()) 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(); RefPtr input = hostInput(); if (input) input->dispatchFormControlChangeEvent(); 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().addTouchEventHandler(this); m_isRegisteredAsTouchEventListener = true; } void SliderThumbElement::unregisterForTouchEvents() { if (!m_isRegisteredAsTouchEventListener) return; clearExclusiveTouchIdentifier(); stopDragging(); document().removeTouchEventHandler(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()); } std::optional SliderThumbElement::resolveCustomStyle(const RenderStyle&, const RenderStyle* hostStyle) { // This doesn't actually compute style. This is just a hack to pick shadow pseudo id when host style is known. static NeverDestroyed sliderThumbShadowPseudoId("-webkit-slider-thumb", AtomicString::ConstructFromLiteral); static NeverDestroyed mediaSliderThumbShadowPseudoId("-webkit-media-slider-thumb", AtomicString::ConstructFromLiteral); if (!hostStyle) return std::nullopt; switch (hostStyle->appearance()) { case MediaSliderPart: case MediaSliderThumbPart: case MediaVolumeSliderPart: case MediaVolumeSliderThumbPart: case MediaFullScreenVolumeSliderPart: case MediaFullScreenVolumeSliderThumbPart: m_shadowPseudoId = mediaSliderThumbShadowPseudoId; break; default: m_shadowPseudoId = sliderThumbShadowPseudoId; } return std::nullopt; } const AtomicString& SliderThumbElement::shadowPseudoId() const { return m_shadowPseudoId; } Ref SliderThumbElement::cloneElementWithoutAttributesAndChildren(Document& targetDocument) { return create(targetDocument); } // -------------------------------- inline SliderContainerElement::SliderContainerElement(Document& document) : HTMLDivElement(HTMLNames::divTag, document) { setHasCustomStyleResolveCallbacks(); } Ref SliderContainerElement::create(Document& document) { return adoptRef(*new SliderContainerElement(document)); } RenderPtr SliderContainerElement::createElementRenderer(RenderStyle&& style, const RenderTreePosition&) { return createRenderer(*this, WTFMove(style)); } std::optional SliderContainerElement::resolveCustomStyle(const RenderStyle&, const RenderStyle* hostStyle) { // This doesn't actually compute style. This is just a hack to pick shadow pseudo id when host style is known. static NeverDestroyed mediaSliderContainer("-webkit-media-slider-container", AtomicString::ConstructFromLiteral); static NeverDestroyed sliderContainer("-webkit-slider-container", AtomicString::ConstructFromLiteral); if (!hostStyle) return std::nullopt; switch (hostStyle->appearance()) { case MediaSliderPart: case MediaSliderThumbPart: case MediaVolumeSliderPart: case MediaVolumeSliderThumbPart: case MediaFullScreenVolumeSliderPart: case MediaFullScreenVolumeSliderThumbPart: m_shadowPseudoId = mediaSliderContainer; break; default: m_shadowPseudoId = sliderContainer; } return std::nullopt; } const AtomicString& SliderContainerElement::shadowPseudoId() const { return m_shadowPseudoId; } }