/* * Copyright (C) 2004, 2006, 2008, 2015 Apple 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: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. 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. * * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``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 APPLE INC. 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 "Scrollbar.h" #include "FrameView.h" #include "GraphicsContext.h" #include "PlatformMouseEvent.h" #include "ScrollAnimator.h" #include "ScrollView.h" #include "ScrollableArea.h" #include "ScrollbarTheme.h" #include #if ENABLE(QT_GESTURE_EVENTS) #include "PlatformGestureEvent.h" #endif #if PLATFORM(GTK) // The position of the scrollbar thumb affects the appearance of the steppers, so // when the thumb moves, we have to invalidate them for painting. #define THUMB_POSITION_AFFECTS_BUTTONS #endif namespace WebCore { PassRefPtr Scrollbar::createNativeScrollbar(ScrollableArea& scrollableArea, ScrollbarOrientation orientation, ScrollbarControlSize size) { return adoptRef(new Scrollbar(scrollableArea, orientation, size)); } int Scrollbar::maxOverlapBetweenPages() { static int maxOverlapBetweenPages = ScrollbarTheme::theme().maxOverlapBetweenPages(); return maxOverlapBetweenPages; } Scrollbar::Scrollbar(ScrollableArea& scrollableArea, ScrollbarOrientation orientation, ScrollbarControlSize controlSize, ScrollbarTheme* customTheme, bool isCustomScrollbar) : m_scrollableArea(scrollableArea) , m_orientation(orientation) , m_controlSize(controlSize) , m_theme(customTheme ? *customTheme : ScrollbarTheme::theme()) , m_visibleSize(0) , m_totalSize(0) , m_currentPos(0) , m_dragOrigin(0) , m_lineStep(0) , m_pageStep(0) , m_pixelStep(1) , m_hoveredPart(NoPart) , m_pressedPart(NoPart) , m_pressedPos(0) , m_scrollPos(0) , m_draggingDocument(false) , m_documentDragPos(0) , m_enabled(true) , m_scrollTimer(*this, &Scrollbar::autoscrollTimerFired) , m_suppressInvalidation(false) , m_isAlphaLocked(false) , m_isCustomScrollbar(isCustomScrollbar) , m_weakPtrFactory(this) { theme().registerScrollbar(*this); // FIXME: This is ugly and would not be necessary if we fix cross-platform code to actually query for // scrollbar thickness and use it when sizing scrollbars (rather than leaving one dimension of the scrollbar // alone when sizing). int thickness = theme().scrollbarThickness(controlSize); Widget::setFrameRect(IntRect(0, 0, thickness, thickness)); m_currentPos = static_cast(m_scrollableArea.scrollOffset(m_orientation)); } Scrollbar::~Scrollbar() { stopTimerIfNeeded(); theme().unregisterScrollbar(*this); } int Scrollbar::occupiedWidth() const { return isOverlayScrollbar() ? 0 : width(); } int Scrollbar::occupiedHeight() const { return isOverlayScrollbar() ? 0 : height(); } void Scrollbar::offsetDidChange() { float position = static_cast(m_scrollableArea.scrollOffset(m_orientation)); if (position == m_currentPos) return; int oldThumbPosition = theme().thumbPosition(*this); m_currentPos = position; updateThumbPosition(); if (m_pressedPart == ThumbPart) setPressedPos(m_pressedPos + theme().thumbPosition(*this) - oldThumbPosition); } void Scrollbar::setProportion(int visibleSize, int totalSize) { if (visibleSize == m_visibleSize && totalSize == m_totalSize) return; m_visibleSize = visibleSize; m_totalSize = totalSize; updateThumbProportion(); } void Scrollbar::setSteps(int lineStep, int pageStep, int pixelsPerStep) { m_lineStep = lineStep; m_pageStep = pageStep; m_pixelStep = 1.0f / pixelsPerStep; } void Scrollbar::updateThumb() { #ifdef THUMB_POSITION_AFFECTS_BUTTONS invalidate(); #else theme().invalidateParts(*this, ForwardTrackPart | BackTrackPart | ThumbPart); #endif } void Scrollbar::updateThumbPosition() { updateThumb(); } void Scrollbar::updateThumbProportion() { updateThumb(); } void Scrollbar::paint(GraphicsContext& context, const IntRect& damageRect) { if (context.updatingControlTints() && theme().supportsControlTints()) { invalidate(); return; } if (context.paintingDisabled() || !frameRect().intersects(damageRect)) return; if (!theme().paint(*this, context, damageRect)) Widget::paint(context, damageRect); } void Scrollbar::autoscrollTimerFired() { autoscrollPressedPart(theme().autoscrollTimerDelay()); } static bool thumbUnderMouse(Scrollbar* scrollbar) { int thumbPos = scrollbar->theme().trackPosition(*scrollbar) + scrollbar->theme().thumbPosition(*scrollbar); int thumbLength = scrollbar->theme().thumbLength(*scrollbar); return scrollbar->pressedPos() >= thumbPos && scrollbar->pressedPos() < thumbPos + thumbLength; } void Scrollbar::autoscrollPressedPart(double delay) { // Don't do anything for the thumb or if nothing was pressed. if (m_pressedPart == ThumbPart || m_pressedPart == NoPart) return; // Handle the track. if ((m_pressedPart == BackTrackPart || m_pressedPart == ForwardTrackPart) && thumbUnderMouse(this)) { theme().invalidatePart(*this, m_pressedPart); setHoveredPart(ThumbPart); return; } // Handle the arrows and track. if (m_scrollableArea.scroll(pressedPartScrollDirection(), pressedPartScrollGranularity())) startTimerIfNeeded(delay); } void Scrollbar::startTimerIfNeeded(double delay) { // Don't do anything for the thumb. if (m_pressedPart == ThumbPart) return; // Handle the track. We halt track scrolling once the thumb is level // with us. if ((m_pressedPart == BackTrackPart || m_pressedPart == ForwardTrackPart) && thumbUnderMouse(this)) { theme().invalidatePart(*this, m_pressedPart); setHoveredPart(ThumbPart); return; } // We can't scroll if we've hit the beginning or end. ScrollDirection dir = pressedPartScrollDirection(); if (dir == ScrollUp || dir == ScrollLeft) { if (m_currentPos == 0) return; } else { if (m_currentPos == maximum()) return; } m_scrollTimer.startOneShot(delay); } void Scrollbar::stopTimerIfNeeded() { if (m_scrollTimer.isActive()) m_scrollTimer.stop(); } ScrollDirection Scrollbar::pressedPartScrollDirection() { if (m_orientation == HorizontalScrollbar) { if (m_pressedPart == BackButtonStartPart || m_pressedPart == BackButtonEndPart || m_pressedPart == BackTrackPart) return ScrollLeft; return ScrollRight; } else { if (m_pressedPart == BackButtonStartPart || m_pressedPart == BackButtonEndPart || m_pressedPart == BackTrackPart) return ScrollUp; return ScrollDown; } } ScrollGranularity Scrollbar::pressedPartScrollGranularity() { if (m_pressedPart == BackButtonStartPart || m_pressedPart == BackButtonEndPart || m_pressedPart == ForwardButtonStartPart || m_pressedPart == ForwardButtonEndPart) return ScrollByLine; return ScrollByPage; } void Scrollbar::moveThumb(int pos, bool draggingDocument) { int delta = pos - m_pressedPos; if (draggingDocument) { if (m_draggingDocument) delta = pos - m_documentDragPos; m_draggingDocument = true; FloatPoint currentPosition = m_scrollableArea.scrollAnimator().currentPosition(); int destinationPosition = (m_orientation == HorizontalScrollbar ? currentPosition.x() : currentPosition.y()) + delta; if (delta > 0) destinationPosition = std::min(destinationPosition + delta, maximum()); else if (delta < 0) destinationPosition = std::max(destinationPosition + delta, 0); m_scrollableArea.scrollToOffsetWithoutAnimation(m_orientation, destinationPosition); m_documentDragPos = pos; return; } if (m_draggingDocument) { delta += m_pressedPos - m_documentDragPos; m_draggingDocument = false; } // Drag the thumb. int thumbPos = theme().thumbPosition(*this); int thumbLen = theme().thumbLength(*this); int trackLen = theme().trackLength(*this); int maxPos = trackLen - thumbLen; if (delta > 0) delta = std::min(maxPos - thumbPos, delta); else if (delta < 0) delta = std::max(-thumbPos, delta); if (delta) { float newPosition = static_cast(thumbPos + delta) * maximum() / (trackLen - thumbLen); m_scrollableArea.scrollToOffsetWithoutAnimation(m_orientation, newPosition); } } void Scrollbar::setHoveredPart(ScrollbarPart part) { if (part == m_hoveredPart) return; if ((m_hoveredPart == NoPart || part == NoPart) && theme().invalidateOnMouseEnterExit()) invalidate(); // Just invalidate the whole scrollbar, since the buttons at either end change anyway. else if (m_pressedPart == NoPart) { // When there's a pressed part, we don't draw a hovered state, so there's no reason to invalidate. theme().invalidatePart(*this, part); theme().invalidatePart(*this, m_hoveredPart); } m_hoveredPart = part; } void Scrollbar::setPressedPart(ScrollbarPart part) { if (m_pressedPart != NoPart) theme().invalidatePart(*this, m_pressedPart); m_pressedPart = part; if (m_pressedPart != NoPart) theme().invalidatePart(*this, m_pressedPart); else if (m_hoveredPart != NoPart) // When we no longer have a pressed part, we can start drawing a hovered state on the hovered part. theme().invalidatePart(*this, m_hoveredPart); } #if ENABLE(QT_GESTURE_EVENTS) bool Scrollbar::gestureEvent(const PlatformGestureEvent& evt) { bool handled = false; switch (evt.type()) { case PlatformEvent::GestureTap: if (m_pressedPart != ThumbPart && m_pressedPart != NoPart) handled = m_scrollableArea.scroll(pressedPartScrollDirection(), pressedPartScrollGranularity()); break; default: break; } setPressedPart(NoPart); m_pressedPos = 0; return handled; } #endif #if !PLATFORM(IOS) bool Scrollbar::mouseMoved(const PlatformMouseEvent& evt) { if (m_pressedPart == ThumbPart) { if (theme().shouldSnapBackToDragOrigin(*this, evt)) m_scrollableArea.scrollToOffsetWithoutAnimation(m_orientation, m_dragOrigin); else { moveThumb(m_orientation == HorizontalScrollbar ? convertFromContainingWindow(evt.position()).x() : convertFromContainingWindow(evt.position()).y(), theme().shouldDragDocumentInsteadOfThumb(*this, evt)); } return true; } if (m_pressedPart != NoPart) m_pressedPos = (orientation() == HorizontalScrollbar ? convertFromContainingWindow(evt.position()).x() : convertFromContainingWindow(evt.position()).y()); ScrollbarPart part = theme().hitTest(*this, evt.position()); if (part != m_hoveredPart) { if (m_pressedPart != NoPart) { if (part == m_pressedPart) { // The mouse is moving back over the pressed part. We // need to start up the timer action again. startTimerIfNeeded(theme().autoscrollTimerDelay()); theme().invalidatePart(*this, m_pressedPart); } else if (m_hoveredPart == m_pressedPart) { // The mouse is leaving the pressed part. Kill our timer // if needed. stopTimerIfNeeded(); theme().invalidatePart(*this, m_pressedPart); } } setHoveredPart(part); } return true; } #endif void Scrollbar::mouseEntered() { m_scrollableArea.mouseEnteredScrollbar(this); } bool Scrollbar::mouseExited() { m_scrollableArea.mouseExitedScrollbar(this); setHoveredPart(NoPart); return true; } bool Scrollbar::mouseUp(const PlatformMouseEvent& mouseEvent) { setPressedPart(NoPart); m_pressedPos = 0; m_draggingDocument = false; stopTimerIfNeeded(); m_scrollableArea.mouseIsDownInScrollbar(this, false); // m_hoveredPart won't be updated until the next mouseMoved or mouseDown, so we have to hit test // to really know if the mouse has exited the scrollbar on a mouseUp. ScrollbarPart part = theme().hitTest(*this, mouseEvent.position()); if (part == NoPart) m_scrollableArea.mouseExitedScrollbar(this); return true; } bool Scrollbar::mouseDown(const PlatformMouseEvent& evt) { ScrollbarPart pressedPart = theme().hitTest(*this, evt.position()); auto action = theme().handleMousePressEvent(*this, evt, pressedPart); if (action == ScrollbarButtonPressAction::None) return true; m_scrollableArea.mouseIsDownInScrollbar(this, true); setPressedPart(pressedPart); int pressedPosition = (orientation() == HorizontalScrollbar ? convertFromContainingWindow(evt.position()).x() : convertFromContainingWindow(evt.position()).y()); if (action == ScrollbarButtonPressAction::CenterOnThumb) { setHoveredPart(ThumbPart); setPressedPart(ThumbPart); m_dragOrigin = m_currentPos; // Set the pressed position to the middle of the thumb so that when we do the move, the delta // will be from the current pixel position of the thumb to the new desired position for the thumb. m_pressedPos = theme().trackPosition(*this) + theme().thumbPosition(*this) + theme().thumbLength(*this) / 2; moveThumb(pressedPosition); return true; } m_pressedPos = pressedPosition; if (action == ScrollbarButtonPressAction::StartDrag) m_dragOrigin = m_currentPos; if (action == ScrollbarButtonPressAction::Scroll) autoscrollPressedPart(theme().initialAutoscrollTimerDelay()); return true; } void Scrollbar::setEnabled(bool e) { if (m_enabled == e) return; m_enabled = e; theme().updateEnabledState(*this); invalidate(); } bool Scrollbar::isOverlayScrollbar() const { return theme().usesOverlayScrollbars(); } bool Scrollbar::shouldParticipateInHitTesting() { // Non-overlay scrollbars should always participate in hit testing. if (!isOverlayScrollbar()) return true; return m_scrollableArea.scrollAnimator().shouldScrollbarParticipateInHitTesting(this); } bool Scrollbar::isWindowActive() const { return m_scrollableArea.isActive(); } void Scrollbar::invalidateRect(const IntRect& rect) { if (suppressInvalidation()) return; m_scrollableArea.invalidateScrollbar(this, rect); } IntRect Scrollbar::convertToContainingView(const IntRect& localRect) const { return m_scrollableArea.convertFromScrollbarToContainingView(this, localRect); } IntRect Scrollbar::convertFromContainingView(const IntRect& parentRect) const { return m_scrollableArea.convertFromContainingViewToScrollbar(this, parentRect); } IntPoint Scrollbar::convertToContainingView(const IntPoint& localPoint) const { return m_scrollableArea.convertFromScrollbarToContainingView(this, localPoint); } IntPoint Scrollbar::convertFromContainingView(const IntPoint& parentPoint) const { return m_scrollableArea.convertFromContainingViewToScrollbar(this, parentPoint); } bool Scrollbar::supportsUpdateOnSecondaryThread() const { // It's unfortunate that this needs to be done with an ifdef. Ideally there would be a way to feature-detect // the necessary support within AppKit. #if ENABLE(ASYNC_SCROLLING) && PLATFORM(MAC) return !m_scrollableArea.forceUpdateScrollbarsOnMainThreadForPerformanceTesting() && (m_scrollableArea.hasLayerForVerticalScrollbar() || m_scrollableArea.hasLayerForHorizontalScrollbar()) && m_scrollableArea.usesAsyncScrolling(); #else return false; #endif } } // namespace WebCore