| /* |
| * 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 <algorithm> |
| |
| #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 { |
| |
| Ref<Scrollbar> 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_isCustomScrollbar(isCustomScrollbar) |
| , m_scrollTimer(*this, &Scrollbar::autoscrollTimerFired) |
| { |
| 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<float>(offsetForOrientation(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<float>(offsetForOrientation(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, Widget::SecurityOriginPaintPolicy) |
| { |
| if (context.invalidatingControlTints() && 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(Seconds 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(Seconds 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<float>(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 !PLATFORM(IOS_FAMILY) |
| 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 |