forked from mirrors/gecko-dev
This rewrites scrollbar layout to work with regular reflow rather than box layout. Overall it's about the same amount of code (mostly because nsScrollbarFrame::Reflow is sorta hand-rolled), but it cleans up a bit and it is progress towards removing XUL layout altogether, without getting into much deeper refactoring. This also blocks some other performance improvements and refactorings I want to make in this code. We make some assumptions to simplify the code that to some extent were made already before, both explicitly and by virtue of using XUL layout. In particular, we assume that scrollbar / slider / thumb has no border or padding and that the writing-mode is horizontal ltr. Differential Revision: https://phabricator.services.mozilla.com/D173489
431 lines
14 KiB
C++
431 lines
14 KiB
C++
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
|
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
|
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
#include "ScrollbarActivity.h"
|
|
#include "nsIScrollbarMediator.h"
|
|
#include "nsIContent.h"
|
|
#include "nsICSSDeclaration.h"
|
|
#include "nsIFrame.h"
|
|
#include "nsContentUtils.h"
|
|
#include "nsAString.h"
|
|
#include "nsQueryFrame.h"
|
|
#include "nsIScrollableFrame.h"
|
|
#include "PresShell.h"
|
|
#include "nsLayoutUtils.h"
|
|
#include "nsScrollbarFrame.h"
|
|
#include "nsRefreshDriver.h"
|
|
#include "nsComponentManagerUtils.h"
|
|
#include "nsStyledElement.h"
|
|
#include "mozilla/dom/Element.h"
|
|
#include "mozilla/dom/Event.h"
|
|
#include "mozilla/LookAndFeel.h"
|
|
#include "mozilla/StaticPrefs_layout.h"
|
|
|
|
namespace mozilla {
|
|
namespace layout {
|
|
|
|
using mozilla::dom::Element;
|
|
|
|
NS_IMPL_ISUPPORTS(ScrollbarActivity, nsIDOMEventListener)
|
|
|
|
void ScrollbarActivity::QueryLookAndFeelVals() {
|
|
// Fade animation constants
|
|
mScrollbarFadeBeginDelay =
|
|
LookAndFeel::GetInt(LookAndFeel::IntID::ScrollbarFadeBeginDelay);
|
|
mScrollbarFadeDuration =
|
|
LookAndFeel::GetInt(LookAndFeel::IntID::ScrollbarFadeDuration);
|
|
// Controls whether we keep the mouse move listener so we can display the
|
|
// scrollbars whenever the user moves the mouse within the scroll area.
|
|
mDisplayOnMouseMove =
|
|
LookAndFeel::GetInt(LookAndFeel::IntID::ScrollbarDisplayOnMouseMove);
|
|
}
|
|
|
|
void ScrollbarActivity::Destroy() {
|
|
StopListeningForScrollbarEvents();
|
|
StopListeningForScrollAreaEvents();
|
|
UnregisterFromRefreshDriver();
|
|
CancelFadeBeginTimer();
|
|
}
|
|
|
|
void ScrollbarActivity::ActivityOccurred() {
|
|
ActivityStarted();
|
|
ActivityStopped();
|
|
}
|
|
|
|
void ScrollbarActivity::ActivityStarted() {
|
|
mNestedActivityCounter++;
|
|
CancelFadeBeginTimer();
|
|
if (!SetIsFading(false)) {
|
|
return;
|
|
}
|
|
UnregisterFromRefreshDriver();
|
|
StartListeningForScrollbarEvents();
|
|
StartListeningForScrollAreaEvents();
|
|
SetIsActive(true);
|
|
|
|
NS_ASSERTION(mIsActive, "need to be active during activity");
|
|
NS_ASSERTION(!mIsFading, "must not be fading during activity");
|
|
}
|
|
|
|
void ScrollbarActivity::ActivityStopped() {
|
|
if (!IsActivityOngoing()) {
|
|
// This can happen if there was a frame reconstruction while the activity
|
|
// was ongoing. In this case we just do nothing. We should probably handle
|
|
// this case better.
|
|
return;
|
|
}
|
|
NS_ASSERTION(mIsActive, "need to be active during activity");
|
|
NS_ASSERTION(!mIsFading, "must not be fading during ongoing activity");
|
|
|
|
mNestedActivityCounter--;
|
|
|
|
if (!IsActivityOngoing()) {
|
|
StartFadeBeginTimer();
|
|
|
|
NS_ASSERTION(mIsActive, "need to be active right after activity");
|
|
NS_ASSERTION(!mIsFading, "must not be fading right after activity");
|
|
}
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
ScrollbarActivity::HandleEvent(dom::Event* aEvent) {
|
|
if (!mDisplayOnMouseMove && !mIsActive) return NS_OK;
|
|
|
|
nsAutoString type;
|
|
aEvent->GetType(type);
|
|
|
|
if (type.EqualsLiteral("mousemove")) {
|
|
// Mouse motions anywhere in the scrollable frame should keep the
|
|
// scrollbars visible, but we have to be careful as content descendants of
|
|
// our scrollable content aren't necessarily scrolled by our scroll frame
|
|
// (if they are out of flow and their containing block is not a descendant
|
|
// of our scroll frame) and we don't want those to activate us.
|
|
nsIFrame* scrollFrame = do_QueryFrame(mScrollableFrame);
|
|
MOZ_ASSERT(scrollFrame);
|
|
nsIScrollableFrame* scrollableFrame = do_QueryFrame(mScrollableFrame);
|
|
nsCOMPtr<nsIContent> targetContent =
|
|
do_QueryInterface(aEvent->GetOriginalTarget());
|
|
nsIFrame* targetFrame =
|
|
targetContent ? targetContent->GetPrimaryFrame() : nullptr;
|
|
if ((scrollableFrame && scrollableFrame->IsRootScrollFrameOfDocument()) ||
|
|
!targetFrame ||
|
|
nsLayoutUtils::IsAncestorFrameCrossDocInProcess(
|
|
scrollFrame, targetFrame,
|
|
scrollFrame->PresShell()->GetRootFrame())) {
|
|
ActivityOccurred();
|
|
}
|
|
return NS_OK;
|
|
}
|
|
|
|
nsCOMPtr<nsIContent> targetContent =
|
|
do_QueryInterface(aEvent->GetOriginalTarget());
|
|
|
|
HandleEventForScrollbar(type, targetContent, GetHorizontalScrollbar(),
|
|
&mHScrollbarHovered);
|
|
HandleEventForScrollbar(type, targetContent, GetVerticalScrollbar(),
|
|
&mVScrollbarHovered);
|
|
|
|
return NS_OK;
|
|
}
|
|
|
|
void ScrollbarActivity::WillRefresh(TimeStamp aTime) {
|
|
NS_ASSERTION(mIsActive, "should only fade while scrollbars are visible");
|
|
NS_ASSERTION(!IsActivityOngoing(),
|
|
"why weren't we unregistered from the refresh driver when "
|
|
"scrollbar activity started?");
|
|
NS_ASSERTION(mIsFading, "should only animate fading during fade");
|
|
|
|
if (!UpdateOpacity(aTime)) {
|
|
return;
|
|
}
|
|
|
|
if (!IsStillFading(aTime)) {
|
|
EndFade();
|
|
}
|
|
}
|
|
|
|
bool ScrollbarActivity::IsStillFading(TimeStamp aTime) {
|
|
return !mFadeBeginTime.IsNull() && (aTime - mFadeBeginTime < FadeDuration());
|
|
}
|
|
|
|
void ScrollbarActivity::HandleEventForScrollbar(const nsAString& aType,
|
|
nsIContent* aTarget,
|
|
Element* aScrollbar,
|
|
bool* aStoredHoverState) {
|
|
if (!aTarget || !aScrollbar || !aTarget->IsInclusiveDescendantOf(aScrollbar))
|
|
return;
|
|
|
|
if (aType.EqualsLiteral("mousedown")) {
|
|
ActivityStarted();
|
|
} else if (aType.EqualsLiteral("mouseup")) {
|
|
ActivityStopped();
|
|
} else if (aType.EqualsLiteral("mouseover") ||
|
|
aType.EqualsLiteral("mouseout")) {
|
|
bool newHoveredState = aType.EqualsLiteral("mouseover");
|
|
if (newHoveredState && !*aStoredHoverState) {
|
|
ActivityStarted();
|
|
HoveredScrollbar(aScrollbar);
|
|
} else if (*aStoredHoverState && !newHoveredState) {
|
|
ActivityStopped();
|
|
// Don't call HoveredScrollbar(nullptr) here because we want the hover
|
|
// attribute to stick until the scrollbars are hidden.
|
|
}
|
|
*aStoredHoverState = newHoveredState;
|
|
}
|
|
}
|
|
|
|
void ScrollbarActivity::StartListeningForScrollbarEvents() {
|
|
if (mListeningForScrollbarEvents) return;
|
|
|
|
mHorizontalScrollbar = GetHorizontalScrollbar();
|
|
mVerticalScrollbar = GetVerticalScrollbar();
|
|
|
|
AddScrollbarEventListeners(mHorizontalScrollbar);
|
|
AddScrollbarEventListeners(mVerticalScrollbar);
|
|
|
|
mListeningForScrollbarEvents = true;
|
|
}
|
|
|
|
void ScrollbarActivity::StopListeningForScrollbarEvents() {
|
|
if (!mListeningForScrollbarEvents) return;
|
|
|
|
RemoveScrollbarEventListeners(mHorizontalScrollbar);
|
|
RemoveScrollbarEventListeners(mVerticalScrollbar);
|
|
|
|
mHorizontalScrollbar = nullptr;
|
|
mVerticalScrollbar = nullptr;
|
|
mListeningForScrollbarEvents = false;
|
|
}
|
|
|
|
void ScrollbarActivity::StartListeningForScrollAreaEvents() {
|
|
if (mListeningForScrollAreaEvents) return;
|
|
|
|
nsIFrame* scrollArea = do_QueryFrame(mScrollableFrame);
|
|
scrollArea->GetContent()->AddEventListener(u"mousemove"_ns, this, true);
|
|
mListeningForScrollAreaEvents = true;
|
|
}
|
|
|
|
void ScrollbarActivity::StopListeningForScrollAreaEvents() {
|
|
if (!mListeningForScrollAreaEvents) return;
|
|
|
|
nsIFrame* scrollArea = do_QueryFrame(mScrollableFrame);
|
|
scrollArea->GetContent()->RemoveEventListener(u"mousemove"_ns, this, true);
|
|
mListeningForScrollAreaEvents = false;
|
|
}
|
|
|
|
void ScrollbarActivity::AddScrollbarEventListeners(
|
|
dom::EventTarget* aScrollbar) {
|
|
if (aScrollbar) {
|
|
aScrollbar->AddEventListener(u"mousedown"_ns, this, true);
|
|
aScrollbar->AddEventListener(u"mouseup"_ns, this, true);
|
|
aScrollbar->AddEventListener(u"mouseover"_ns, this, true);
|
|
aScrollbar->AddEventListener(u"mouseout"_ns, this, true);
|
|
}
|
|
}
|
|
|
|
void ScrollbarActivity::RemoveScrollbarEventListeners(
|
|
dom::EventTarget* aScrollbar) {
|
|
if (aScrollbar) {
|
|
aScrollbar->RemoveEventListener(u"mousedown"_ns, this, true);
|
|
aScrollbar->RemoveEventListener(u"mouseup"_ns, this, true);
|
|
aScrollbar->RemoveEventListener(u"mouseover"_ns, this, true);
|
|
aScrollbar->RemoveEventListener(u"mouseout"_ns, this, true);
|
|
}
|
|
}
|
|
|
|
void ScrollbarActivity::BeginFade() {
|
|
NS_ASSERTION(mIsActive, "can't begin fade when we're already inactive");
|
|
NS_ASSERTION(!IsActivityOngoing(),
|
|
"why wasn't the fade begin timer cancelled when scrollbar "
|
|
"activity started?");
|
|
NS_ASSERTION(!mIsFading, "shouldn't be fading just yet");
|
|
|
|
CancelFadeBeginTimer();
|
|
mFadeBeginTime = TimeStamp::Now();
|
|
if (!SetIsFading(true)) {
|
|
return;
|
|
}
|
|
RegisterWithRefreshDriver();
|
|
|
|
NS_ASSERTION(mIsActive, "only fade while scrollbars are visible");
|
|
NS_ASSERTION(mIsFading, "should be fading now");
|
|
}
|
|
|
|
void ScrollbarActivity::EndFade() {
|
|
NS_ASSERTION(mIsActive, "still need to be active at this point");
|
|
NS_ASSERTION(!IsActivityOngoing(),
|
|
"why wasn't the fade end timer cancelled when scrollbar "
|
|
"activity started?");
|
|
|
|
if (!SetIsFading(false)) {
|
|
return;
|
|
}
|
|
SetIsActive(false);
|
|
UnregisterFromRefreshDriver();
|
|
StopListeningForScrollbarEvents();
|
|
if (!mDisplayOnMouseMove) {
|
|
StopListeningForScrollAreaEvents();
|
|
}
|
|
|
|
NS_ASSERTION(!mIsActive, "should have gone inactive after fade end");
|
|
NS_ASSERTION(!mIsFading, "shouldn't be fading anymore");
|
|
}
|
|
|
|
void ScrollbarActivity::RegisterWithRefreshDriver() {
|
|
nsRefreshDriver* refreshDriver = GetRefreshDriver();
|
|
if (refreshDriver) {
|
|
refreshDriver->AddRefreshObserver(this, FlushType::Style,
|
|
"Scrollbar fade animation");
|
|
}
|
|
}
|
|
|
|
void ScrollbarActivity::UnregisterFromRefreshDriver() {
|
|
nsRefreshDriver* refreshDriver = GetRefreshDriver();
|
|
if (refreshDriver) {
|
|
refreshDriver->RemoveRefreshObserver(this, FlushType::Style);
|
|
}
|
|
}
|
|
|
|
static void SetBooleanAttribute(Element* aElement, nsAtom* aAttribute,
|
|
bool aValue) {
|
|
if (aElement) {
|
|
if (aValue) {
|
|
aElement->SetAttr(kNameSpaceID_None, aAttribute, u"true"_ns, true);
|
|
} else {
|
|
aElement->UnsetAttr(kNameSpaceID_None, aAttribute, true);
|
|
}
|
|
}
|
|
}
|
|
|
|
void ScrollbarActivity::SetIsActive(bool aNewActive) {
|
|
if (mIsActive == aNewActive) return;
|
|
|
|
mIsActive = aNewActive;
|
|
if (!mIsActive) {
|
|
// Clear sticky scrollbar hover status.
|
|
HoveredScrollbar(nullptr);
|
|
}
|
|
|
|
SetBooleanAttribute(GetHorizontalScrollbar(), nsGkAtoms::active, mIsActive);
|
|
SetBooleanAttribute(GetVerticalScrollbar(), nsGkAtoms::active, mIsActive);
|
|
}
|
|
|
|
static void SetOpacityOnElement(nsIContent* aContent, double aOpacity) {
|
|
if (RefPtr<nsStyledElement> inlineStyleContent =
|
|
nsStyledElement::FromNodeOrNull(aContent)) {
|
|
nsICSSDeclaration* decl = inlineStyleContent->Style();
|
|
nsAutoCString str;
|
|
str.AppendFloat(aOpacity);
|
|
decl->SetProperty("opacity"_ns, str, EmptyCString(), IgnoreErrors());
|
|
}
|
|
}
|
|
|
|
bool ScrollbarActivity::UpdateOpacity(TimeStamp aTime) {
|
|
// Avoid division by zero if mScrollbarFadeDuration is zero, just jump
|
|
// to the end of the fade animation
|
|
double progress = mScrollbarFadeDuration
|
|
? ((aTime - mFadeBeginTime) / FadeDuration())
|
|
: 1.0;
|
|
double opacity = 1.0 - std::max(0.0, std::min(1.0, progress));
|
|
|
|
// 'this' may be getting destroyed during SetOpacityOnElement calls.
|
|
AutoWeakFrame weakFrame((do_QueryFrame(mScrollableFrame)));
|
|
SetOpacityOnElement(GetHorizontalScrollbar(), opacity);
|
|
if (!weakFrame.IsAlive()) {
|
|
return false;
|
|
}
|
|
SetOpacityOnElement(GetVerticalScrollbar(), opacity);
|
|
if (!weakFrame.IsAlive()) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static void UnsetOpacityOnElement(nsIContent* aContent) {
|
|
if (RefPtr<nsStyledElement> inlineStyleContent =
|
|
nsStyledElement::FromNodeOrNull(aContent)) {
|
|
nsICSSDeclaration* decl = inlineStyleContent->Style();
|
|
nsAutoCString dummy;
|
|
decl->RemoveProperty("opacity"_ns, dummy, IgnoreErrors());
|
|
}
|
|
}
|
|
|
|
bool ScrollbarActivity::SetIsFading(bool aNewFading) {
|
|
if (mIsFading == aNewFading) return true;
|
|
|
|
mIsFading = aNewFading;
|
|
if (!mIsFading) {
|
|
mFadeBeginTime = TimeStamp();
|
|
// 'this' may be getting destroyed during UnsetOpacityOnElement calls.
|
|
AutoWeakFrame weakFrame((do_QueryFrame(mScrollableFrame)));
|
|
UnsetOpacityOnElement(GetHorizontalScrollbar());
|
|
if (!weakFrame.IsAlive()) {
|
|
return false;
|
|
}
|
|
UnsetOpacityOnElement(GetVerticalScrollbar());
|
|
if (!weakFrame.IsAlive()) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void ScrollbarActivity::StartFadeBeginTimer() {
|
|
if (StaticPrefs::layout_testing_overlay_scrollbars_always_visible()) {
|
|
return;
|
|
}
|
|
if (!mFadeBeginTimer) {
|
|
mFadeBeginTimer = NS_NewTimer();
|
|
}
|
|
mFadeBeginTimer->InitWithNamedFuncCallback(
|
|
FadeBeginTimerFired, this, mScrollbarFadeBeginDelay,
|
|
nsITimer::TYPE_ONE_SHOT, "ScrollbarActivity::FadeBeginTimerFired");
|
|
}
|
|
|
|
void ScrollbarActivity::CancelFadeBeginTimer() {
|
|
if (mFadeBeginTimer) {
|
|
mFadeBeginTimer->Cancel();
|
|
}
|
|
}
|
|
|
|
static void MaybeInvalidateScrollbarForHover(
|
|
Element* aScrollbarToInvalidate, Element* aScrollbarAboutToGetHover) {
|
|
if (aScrollbarToInvalidate) {
|
|
bool hasHover =
|
|
aScrollbarToInvalidate->HasAttr(kNameSpaceID_None, nsGkAtoms::hover);
|
|
bool willHaveHover = (aScrollbarAboutToGetHover == aScrollbarToInvalidate);
|
|
|
|
if (hasHover != willHaveHover) {
|
|
if (nsIFrame* f = aScrollbarToInvalidate->GetPrimaryFrame()) {
|
|
f->SchedulePaint();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void ScrollbarActivity::HoveredScrollbar(Element* aScrollbar) {
|
|
Element* vertScrollbar = GetVerticalScrollbar();
|
|
Element* horzScrollbar = GetHorizontalScrollbar();
|
|
MaybeInvalidateScrollbarForHover(vertScrollbar, aScrollbar);
|
|
MaybeInvalidateScrollbarForHover(horzScrollbar, aScrollbar);
|
|
|
|
SetBooleanAttribute(horzScrollbar, nsGkAtoms::hover, false);
|
|
SetBooleanAttribute(vertScrollbar, nsGkAtoms::hover, false);
|
|
SetBooleanAttribute(aScrollbar, nsGkAtoms::hover, true);
|
|
}
|
|
|
|
nsRefreshDriver* ScrollbarActivity::GetRefreshDriver() {
|
|
nsIFrame* scrollableFrame = do_QueryFrame(mScrollableFrame);
|
|
return scrollableFrame->PresContext()->RefreshDriver();
|
|
}
|
|
|
|
Element* ScrollbarActivity::GetScrollbarContent(bool aVertical) {
|
|
nsIFrame* box = mScrollableFrame->GetScrollbarBox(aVertical);
|
|
return box ? box->GetContent()->AsElement() : nullptr;
|
|
}
|
|
|
|
} // namespace layout
|
|
} // namespace mozilla
|