ScrollbarThemeWin.cpp   [plain text]


/*
 * Copyright (C) 2008, 2013 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 "ScrollbarThemeWin.h"

#include "GDIUtilities.h"
#include "GraphicsContext.h"
#include "HWndDC.h"
#include "LocalWindowsContext.h"
#include "PlatformMouseEvent.h"
#include "Scrollbar.h"
#include "SystemInfo.h"
#include <wtf/SoftLinking.h>
#include <wtf/win/GDIObject.h>

// Generic state constants
#define TS_NORMAL    1
#define TS_HOVER     2
#define TS_ACTIVE    3
#define TS_DISABLED  4

#define SP_BUTTON          1
#define SP_THUMBHOR        2
#define SP_THUMBVERT       3
#define SP_TRACKSTARTHOR   4
#define SP_TRACKENDHOR     5
#define SP_TRACKSTARTVERT  6
#define SP_TRACKENDVERT    7
#define SP_GRIPPERHOR      8
#define SP_GRIPPERVERT     9

#define TS_UP_BUTTON       0
#define TS_DOWN_BUTTON     4
#define TS_LEFT_BUTTON     8
#define TS_RIGHT_BUTTON    12
#define TS_UP_BUTTON_HOVER   17
#define TS_DOWN_BUTTON_HOVER  18
#define TS_LEFT_BUTTON_HOVER  19
#define TS_RIGHT_BUTTON_HOVER   20


namespace WebCore {

static HANDLE scrollbarTheme;
static bool runningVista;

// FIXME:  Refactor the soft-linking code so that it can be shared with RenderThemeWin
SOFT_LINK_LIBRARY(uxtheme)
SOFT_LINK(uxtheme, OpenThemeData, HANDLE, WINAPI, (HWND hwnd, LPCWSTR pszClassList), (hwnd, pszClassList))
SOFT_LINK(uxtheme, CloseThemeData, HRESULT, WINAPI, (HANDLE hTheme), (hTheme))
SOFT_LINK(uxtheme, DrawThemeBackground, HRESULT, WINAPI, (HANDLE hTheme, HDC hdc, int iPartId, int iStateId, const RECT* pRect, const RECT* pClipRect), (hTheme, hdc, iPartId, iStateId, pRect, pClipRect))
SOFT_LINK(uxtheme, IsThemeActive, BOOL, WINAPI, (), ())
SOFT_LINK(uxtheme, IsThemeBackgroundPartiallyTransparent, BOOL, WINAPI, (HANDLE hTheme, int iPartId, int iStateId), (hTheme, iPartId, iStateId))

// Constants used to figure the drag rect outside which we should snap the
// scrollbar thumb back to its origin.  These calculations are based on
// observing the behavior of the MSVC8 main window scrollbar + some
// guessing/extrapolation.
static const int kOffEndMultiplier = 3;
static const int kOffSideMultiplier = 8;

static void checkAndInitScrollbarTheme()
{
    if (uxthemeLibrary() && !scrollbarTheme && IsThemeActive())
        scrollbarTheme = OpenThemeData(0, L"Scrollbar");
}

ScrollbarTheme& ScrollbarTheme::nativeTheme()
{
    static ScrollbarThemeWin winTheme;
    return winTheme;
}

ScrollbarThemeWin::ScrollbarThemeWin()
{
    static bool initialized;
    if (!initialized) {
        initialized = true;
        checkAndInitScrollbarTheme();
        runningVista = (windowsVersion() >= WindowsVista);
    }
}

ScrollbarThemeWin::~ScrollbarThemeWin() = default;

static int scrollbarThicknessInPixels()
{
    static int thickness = ::GetSystemMetrics(SM_CXVSCROLL);
    return thickness;
}

int ScrollbarThemeWin::scrollbarThickness(ScrollbarControlSize, ScrollbarExpansionState)
{
    float inverseScaleFactor = 1.0f / deviceScaleFactorForWindow(0);
    return clampTo<int>(inverseScaleFactor * scrollbarThicknessInPixels());
}

void ScrollbarThemeWin::themeChanged()
{
    if (!scrollbarTheme)
        return;

    CloseThemeData(scrollbarTheme);
    scrollbarTheme = 0;
}

bool ScrollbarThemeWin::invalidateOnMouseEnterExit()
{
    return runningVista;
}

bool ScrollbarThemeWin::hasThumb(Scrollbar& scrollbar)
{
    return thumbLength(scrollbar) > 0;
}

IntRect ScrollbarThemeWin::backButtonRect(Scrollbar& scrollbar, ScrollbarPart part, bool)
{
    // Windows just has single arrows.
    if (part == BackButtonEndPart)
        return IntRect();

    // Our desired rect is essentially 17x17.
    
    // Our actual rect will shrink to half the available space when
    // we have < 34 pixels left.  This allows the scrollbar
    // to scale down and function even at tiny sizes.
    int thickness = scrollbarThickness();
    if (scrollbar.orientation() == HorizontalScrollbar)
        return IntRect(scrollbar.x(), scrollbar.y(),
                       scrollbar.width() < 2 * thickness ? scrollbar.width() / 2 : thickness, thickness);
    return IntRect(scrollbar.x(), scrollbar.y(),
                   thickness, scrollbar.height() < 2 * thickness ? scrollbar.height() / 2 : thickness);
}

IntRect ScrollbarThemeWin::forwardButtonRect(Scrollbar& scrollbar, ScrollbarPart part, bool)
{
    // Windows just has single arrows.
    if (part == ForwardButtonStartPart)
        return IntRect();
    
    // Our desired rect is essentially 17x17.
    
    // Our actual rect will shrink to half the available space when
    // we have < 34 pixels left.  This allows the scrollbar
    // to scale down and function even at tiny sizes.
    int thickness = scrollbarThickness();
    if (scrollbar.orientation() == HorizontalScrollbar) {
        int w = scrollbar.width() < 2 * thickness ? scrollbar.width() / 2 : thickness;
        return IntRect(scrollbar.x() + scrollbar.width() - w, scrollbar.y(), w, thickness);
    }
    
    int h = scrollbar.height() < 2 * thickness ? scrollbar.height() / 2 : thickness;
    return IntRect(scrollbar.x(), scrollbar.y() + scrollbar.height() - h, thickness, h);
}

IntRect ScrollbarThemeWin::trackRect(Scrollbar& scrollbar, bool)
{
    int thickness = scrollbarThickness();
    if (scrollbar.orientation() == HorizontalScrollbar) {
        if (scrollbar.width() < 2 * thickness)
            return IntRect();
        return IntRect(scrollbar.x() + thickness, scrollbar.y(), scrollbar.width() - 2 * thickness, thickness);
    }
    if (scrollbar.height() < 2 * thickness)
        return IntRect();
    return IntRect(scrollbar.x(), scrollbar.y() + thickness, thickness, scrollbar.height() - 2 * thickness);
}

ScrollbarButtonPressAction ScrollbarThemeWin::handleMousePressEvent(Scrollbar&, const PlatformMouseEvent& event, ScrollbarPart pressedPart)
{
    if (event.button() == RightButton)
        return ScrollbarButtonPressAction::None;

    switch (pressedPart) {
    case BackTrackPart:
    case ForwardTrackPart:
        if (event.shiftKey() && event.button() == LeftButton)
            return ScrollbarButtonPressAction::CenterOnThumb;
        break;
    case ThumbPart:
        return ScrollbarButtonPressAction::StartDrag;
    default:
        break;
    }

    return ScrollbarButtonPressAction::Scroll;
}

bool ScrollbarThemeWin::shouldSnapBackToDragOrigin(Scrollbar& scrollbar, const PlatformMouseEvent& evt)
{
    // Find the rect within which we shouldn't snap, by expanding the track rect
    // in both dimensions.
    IntRect rect = trackRect(scrollbar);
    const bool horz = scrollbar.orientation() == HorizontalScrollbar;
    const int thickness = scrollbarThickness(scrollbar.controlSize());
    rect.inflateX((horz ? kOffEndMultiplier : kOffSideMultiplier) * thickness);
    rect.inflateY((horz ? kOffSideMultiplier : kOffEndMultiplier) * thickness);

    // Convert the event to local coordinates.
    IntPoint mousePosition = scrollbar.convertFromContainingWindow(evt.position());
    mousePosition.move(scrollbar.x(), scrollbar.y());

    // We should snap iff the event is outside our calculated rect.
    return !rect.contains(mousePosition);
}

void ScrollbarThemeWin::paintTrackBackground(GraphicsContext& context, Scrollbar& scrollbar, const IntRect& rect)
{
    // Just assume a forward track part.  We only paint the track as a single piece when there is no thumb.
    if (!hasThumb(scrollbar))
        paintTrackPiece(context, scrollbar, rect, ForwardTrackPart);
}

void ScrollbarThemeWin::paintTrackPiece(GraphicsContext& context, Scrollbar& scrollbar, const IntRect& rect, ScrollbarPart partType)
{
    checkAndInitScrollbarTheme();

    bool start = partType == BackTrackPart;
    int part;
    if (scrollbar.orientation() == HorizontalScrollbar)
        part = start ? SP_TRACKSTARTHOR : SP_TRACKENDHOR;
    else
        part = start ? SP_TRACKSTARTVERT : SP_TRACKENDVERT;

    int state;
    if (!scrollbar.enabled())
        state = TS_DISABLED;
    else if ((scrollbar.hoveredPart() == BackTrackPart && start) ||
             (scrollbar.hoveredPart() == ForwardTrackPart && !start))
        state = (scrollbar.pressedPart() == scrollbar.hoveredPart() ? TS_ACTIVE : TS_HOVER);
    else
        state = TS_NORMAL;

    bool alphaBlend = false;
    if (scrollbarTheme)
        alphaBlend = IsThemeBackgroundPartiallyTransparent(scrollbarTheme, part, state);

    LocalWindowsContext windowsContext(context, rect, alphaBlend);
    RECT themeRect(rect);

    if (scrollbarTheme)
        DrawThemeBackground(scrollbarTheme, windowsContext.hdc(), part, state, &themeRect, 0);
    else {
        DWORD color3DFace = ::GetSysColor(COLOR_3DFACE);
        DWORD colorScrollbar = ::GetSysColor(COLOR_SCROLLBAR);
        DWORD colorWindow = ::GetSysColor(COLOR_WINDOW);
        HDC hdc = windowsContext.hdc();
        if ((color3DFace != colorScrollbar) && (colorWindow != colorScrollbar))
            ::FillRect(hdc, &themeRect, HBRUSH(COLOR_SCROLLBAR+1));
        else {
            static WORD patternBits[8] = { 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55 };
            auto patternBitmap = adoptGDIObject(::CreateBitmap(8, 8, 1, 1, patternBits));
            auto brush = adoptGDIObject(::CreatePatternBrush(patternBitmap.get()));
            SaveDC(hdc);
            ::SetTextColor(hdc, ::GetSysColor(COLOR_3DHILIGHT));
            ::SetBkColor(hdc, ::GetSysColor(COLOR_3DFACE));
            ::SetBrushOrgEx(hdc, rect.x(), rect.y(), NULL);
            ::SelectObject(hdc, brush.get());
            ::FillRect(hdc, &themeRect, brush.get());
            ::RestoreDC(hdc, -1);
        }
    }

    if (!alphaBlend && !context.isInTransparencyLayer())
        DIBPixelData::setRGBABitmapAlpha(windowsContext.hdc(), rect, 255);
}

void ScrollbarThemeWin::paintButton(GraphicsContext& context, Scrollbar& scrollbar, const IntRect& rect, ScrollbarPart part)
{
    checkAndInitScrollbarTheme();

    bool start = (part == BackButtonStartPart);
    int xpState = 0;
    int classicState = 0;
    if (scrollbar.orientation() == HorizontalScrollbar)
        xpState = start ? TS_LEFT_BUTTON : TS_RIGHT_BUTTON;
    else
        xpState = start ? TS_UP_BUTTON : TS_DOWN_BUTTON;
    classicState = xpState / 4;

    if (!scrollbar.enabled()) {
        xpState += TS_DISABLED;
        classicState |= DFCS_INACTIVE;
    } else if ((scrollbar.hoveredPart() == BackButtonStartPart && start) ||
               (scrollbar.hoveredPart() == ForwardButtonEndPart && !start)) {
        if (scrollbar.pressedPart() == scrollbar.hoveredPart()) {
            xpState += TS_ACTIVE;
            classicState |= DFCS_PUSHED;
            classicState |= DFCS_FLAT;
        } else
            xpState += TS_HOVER;
    } else {
        if (scrollbar.hoveredPart() == NoPart || !runningVista)
            xpState += TS_NORMAL;
        else {
            if (scrollbar.orientation() == HorizontalScrollbar)
                xpState = start ? TS_LEFT_BUTTON_HOVER : TS_RIGHT_BUTTON_HOVER;
            else
                xpState = start ? TS_UP_BUTTON_HOVER : TS_DOWN_BUTTON_HOVER;
        }
    }

    bool alphaBlend = false;
    if (scrollbarTheme)
        alphaBlend = IsThemeBackgroundPartiallyTransparent(scrollbarTheme, SP_BUTTON, xpState);

    // There seems to be a bug in DrawThemeBackground when the device context is scaled.
    // We can work around this by scaling the drawing rectangle instead.
    auto scaleFactor = context.scaleFactor().width();
    auto scaledRect = rect;
    scaledRect.scale(scaleFactor);
    context.save();
    context.scale(FloatSize(1.0f / scaleFactor, 1.0f / scaleFactor));

    {
        LocalWindowsContext windowsContext(context, scaledRect, alphaBlend);
        RECT themeRect(scaledRect);
        if (scrollbarTheme)
            DrawThemeBackground(scrollbarTheme, windowsContext.hdc(), SP_BUTTON, xpState, &themeRect, 0);
        else
            ::DrawFrameControl(windowsContext.hdc(), &themeRect, DFC_SCROLL, classicState);

        if (!alphaBlend && !context.isInTransparencyLayer())
            DIBPixelData::setRGBABitmapAlpha(windowsContext.hdc(), scaledRect, 255);
    }
    context.restore();
}

static IntRect gripperRect(int thickness, const IntRect& thumbRect)
{
    // Center in the thumb.
    int gripperThickness = thickness / 2;
    return IntRect(thumbRect.x() + (thumbRect.width() - gripperThickness) / 2,
                   thumbRect.y() + (thumbRect.height() - gripperThickness) / 2,
                   gripperThickness, gripperThickness);
}

static void paintGripper(Scrollbar& scrollbar, HDC hdc, const IntRect& rect)
{
    if (!scrollbarTheme)
        return;  // Classic look has no gripper.
   
    int state;
    if (!scrollbar.enabled())
        state = TS_DISABLED;
    else if (scrollbar.pressedPart() == ThumbPart)
        state = TS_ACTIVE; // Thumb always stays active once pressed.
    else if (scrollbar.hoveredPart() == ThumbPart)
        state = TS_HOVER;
    else
        state = TS_NORMAL;

    RECT themeRect(rect);
    DrawThemeBackground(scrollbarTheme, hdc, scrollbar.orientation() == HorizontalScrollbar ? SP_GRIPPERHOR : SP_GRIPPERVERT, state, &themeRect, 0);
}

void ScrollbarThemeWin::paintThumb(GraphicsContext& context, Scrollbar& scrollbar, const IntRect& rect)
{
    checkAndInitScrollbarTheme();

    int state;
    if (!scrollbar.enabled())
        state = TS_DISABLED;
    else if (scrollbar.pressedPart() == ThumbPart)
        state = TS_ACTIVE; // Thumb always stays active once pressed.
    else if (scrollbar.hoveredPart() == ThumbPart)
        state = TS_HOVER;
    else
        state = TS_NORMAL;

    bool alphaBlend = false;
    if (scrollbarTheme)
        alphaBlend = IsThemeBackgroundPartiallyTransparent(scrollbarTheme, scrollbar.orientation() == HorizontalScrollbar ? SP_THUMBHOR : SP_THUMBVERT, state);
    LocalWindowsContext windowsContext(context, rect, alphaBlend);
    RECT themeRect(rect);
    if (scrollbarTheme) {
        DrawThemeBackground(scrollbarTheme, windowsContext.hdc(), scrollbar.orientation() == HorizontalScrollbar ? SP_THUMBHOR : SP_THUMBVERT, state, &themeRect, 0);
        paintGripper(scrollbar, windowsContext.hdc(), gripperRect(scrollbarThickness(), rect));
    } else
        ::DrawEdge(windowsContext.hdc(), &themeRect, EDGE_RAISED, BF_RECT | BF_MIDDLE);

    if (!alphaBlend && !context.isInTransparencyLayer())
        DIBPixelData::setRGBABitmapAlpha(windowsContext.hdc(), rect, 255);
}

}