Bug 1958965 - Change scroll event setup to match the spec better. r=smaug

This matches the spec (plus resolution at
https://github.com/w3c/csswg-drafts/issues/11164) better, by running the
events per document.

Same comments as D244655 regarding the delayed event stuff that's going
away.

Differential Revision: https://phabricator.services.mozilla.com/D244666
This commit is contained in:
Emilio Cobos Álvarez 2025-04-10 01:07:15 +00:00
parent e0f0653a49
commit abf15a9165
9 changed files with 57 additions and 106 deletions

View file

@ -1424,7 +1424,6 @@ Document::Document(const char* aContentType)
mParserAborted(false), mParserAborted(false),
mReportedDocumentUseCounters(false), mReportedDocumentUseCounters(false),
mHasReportedShadowDOMUsage(false), mHasReportedShadowDOMUsage(false),
mHasDelayedRefreshEvent(false),
mLoadEventFiring(false), mLoadEventFiring(false),
mSkipLoadEventAfterClose(false), mSkipLoadEventAfterClose(false),
mDisableCookieAccess(false), mDisableCookieAccess(false),
@ -13219,19 +13218,6 @@ void Document::UnsuppressEventHandlingAndFireEvents(bool aFireEvents) {
for (net::ChannelEventQueue* queue : queues) { for (net::ChannelEventQueue* queue : queues) {
queue->Resume(); queue->Resume();
} }
// If there have been any events driven by the refresh driver which were
// delayed due to events being suppressed in this document, make sure
// there is a refresh scheduled soon so the events will run.
if (doc->mHasDelayedRefreshEvent) {
doc->mHasDelayedRefreshEvent = false;
if (doc->mPresShell) {
nsRefreshDriver* rd =
doc->mPresShell->GetPresContext()->RefreshDriver();
rd->RunDelayedEventsSoon();
}
}
} }
} }

View file

@ -2810,8 +2810,6 @@ class Document : public nsINode,
*/ */
void FireOrClearPostMessageEvents(bool aFireEvents); void FireOrClearPostMessageEvents(bool aFireEvents);
void SetHasDelayedRefreshEvent() { mHasDelayedRefreshEvent = true; }
/** /**
* Flag whether we're about to fire the window's load event for this document. * Flag whether we're about to fire the window's load event for this document.
*/ */
@ -4920,10 +4918,6 @@ class Document : public nsINode,
bool mHasReportedShadowDOMUsage : 1; bool mHasReportedShadowDOMUsage : 1;
// Whether an event triggered by the refresh driver was delayed because this
// document has suppressed events.
bool mHasDelayedRefreshEvent : 1;
// The HTML spec has a "iframe load in progress" flag, but that doesn't seem // The HTML spec has a "iframe load in progress" flag, but that doesn't seem
// to have the right semantics. See // to have the right semantics. See
// <https://github.com/whatwg/html/issues/4292>. What we have instead is a // <https://github.com/whatwg/html/issues/4292>. What we have instead is a

View file

@ -216,7 +216,7 @@ VisualViewport::VisualViewportScrollEvent::VisualViewportScrollEvent(
mPrevLayoutOffset(aPrevLayoutOffset) { mPrevLayoutOffset(aPrevLayoutOffset) {
VVP_LOG("%p: Registering PostScroll on %p %p\n", aViewport, aPresContext, VVP_LOG("%p: Registering PostScroll on %p %p\n", aViewport, aPresContext,
aPresContext->RefreshDriver()); aPresContext->RefreshDriver());
aPresContext->RefreshDriver()->PostScrollEvent(this); aPresContext->PresShell()->PostScrollEvent(this);
} }
bool VisualViewport::VisualViewportScrollEvent::HasPresContext( bool VisualViewport::VisualViewportScrollEvent::HasPresContext(

View file

@ -1997,6 +1997,16 @@ bool PresShell::CanHandleUserInputEvents(WidgetGUIEvent* aGUIEvent) {
return true; return true;
} }
void PresShell::PostScrollEvent(Runnable* aEvent) {
MOZ_ASSERT(aEvent);
const bool hadEvents = !mPendingScrollEvents.IsEmpty();
mPendingScrollEvents.AppendElement(aEvent);
if (!hadEvents) {
mPresContext->RefreshDriver()->ScheduleRenderingPhase(
RenderingPhase::ScrollSteps);
}
}
void PresShell::ScheduleResizeEventIfNeeded(ResizeEventKind aKind) { void PresShell::ScheduleResizeEventIfNeeded(ResizeEventKind aKind) {
if (mIsDestroying) { if (mIsDestroying) {
return; return;
@ -2011,7 +2021,7 @@ void PresShell::ScheduleResizeEventIfNeeded(ResizeEventKind aKind) {
mVisualViewportResizeEventPending = true; mVisualViewportResizeEventPending = true;
} }
mPresContext->RefreshDriver()->ScheduleRenderingPhase( mPresContext->RefreshDriver()->ScheduleRenderingPhase(
mozilla::RenderingPhase::ResizeSteps); RenderingPhase::ResizeSteps);
} }
bool PresShell::ResizeReflowIgnoreOverride(nscoord aWidth, nscoord aHeight, bool PresShell::ResizeReflowIgnoreOverride(nscoord aWidth, nscoord aHeight,
@ -2165,6 +2175,19 @@ void PresShell::RunResizeSteps() {
} }
} }
// https://drafts.csswg.org/cssom-view/#document-run-the-scroll-steps
// But note: https://github.com/w3c/csswg-drafts/issues/11164
void PresShell::RunScrollSteps() {
// Scroll events are one-shot, so after running them we can drop them.
// However, dispatching a scroll event can potentially cause more scroll
// events to be posted, so we move the initial set into a temporary array
// first. (Newly posted scroll events will be dispatched on the next tick.)
auto events = std::move(mPendingScrollEvents);
for (auto& event : events) {
event->Run();
}
}
static nsIContent* GetNativeAnonymousSubtreeRoot(nsIContent* aContent) { static nsIContent* GetNativeAnonymousSubtreeRoot(nsIContent* aContent) {
if (!aContent) { if (!aContent) {
return nullptr; return nullptr;

View file

@ -361,6 +361,8 @@ class PresShell final : public nsStubDocumentObserver,
enum class ResizeEventKind : uint8_t { Regular, Visual }; enum class ResizeEventKind : uint8_t { Regular, Visual };
void ScheduleResizeEventIfNeeded(ResizeEventKind = ResizeEventKind::Regular); void ScheduleResizeEventIfNeeded(ResizeEventKind = ResizeEventKind::Regular);
void PostScrollEvent(mozilla::Runnable*);
/** /**
* Returns true if the document hosted by this presShell is in a devtools * Returns true if the document hosted by this presShell is in a devtools
* Responsive Design Mode browsing context. * Responsive Design Mode browsing context.
@ -1175,6 +1177,7 @@ class PresShell final : public nsStubDocumentObserver,
bool HasHandledUserInput() const { return mHasHandledUserInput; } bool HasHandledUserInput() const { return mHasHandledUserInput; }
MOZ_CAN_RUN_SCRIPT void RunResizeSteps(); MOZ_CAN_RUN_SCRIPT void RunResizeSteps();
MOZ_CAN_RUN_SCRIPT void RunScrollSteps();
void NativeAnonymousContentWillBeRemoved(nsIContent* aAnonContent); void NativeAnonymousContentWillBeRemoved(nsIContent* aAnonContent);
@ -3101,6 +3104,8 @@ class PresShell final : public nsStubDocumentObserver,
nsTHashSet<ScrollContainerFrame*> mPendingScrollAnchorSelection; nsTHashSet<ScrollContainerFrame*> mPendingScrollAnchorSelection;
nsTHashSet<ScrollContainerFrame*> mPendingScrollAnchorAdjustment; nsTHashSet<ScrollContainerFrame*> mPendingScrollAnchorAdjustment;
nsTHashSet<ScrollContainerFrame*> mPendingScrollResnap; nsTHashSet<ScrollContainerFrame*> mPendingScrollResnap;
// Pending list of scroll/scrollend/etc events.
nsTArray<RefPtr<Runnable>> mPendingScrollEvents;
nsTHashSet<nsIContent*> mHiddenContentInForcedLayout; nsTHashSet<nsIContent*> mHiddenContentInForcedLayout;

View file

@ -1556,27 +1556,6 @@ bool nsRefreshDriver::RemoveRefreshObserver(nsARefreshObserver* aObserver,
return true; return true;
} }
void nsRefreshDriver::PostScrollEvent(mozilla::Runnable* aScrollEvent,
bool aDelayed) {
if (aDelayed) {
mDelayedScrollEvents.AppendElement(aScrollEvent);
} else {
mScrollEvents.AppendElement(aScrollEvent);
ScheduleRenderingPhase(RenderingPhase::ScrollSteps);
}
}
void nsRefreshDriver::DispatchScrollEvents() {
// Scroll events are one-shot, so after running them we can drop them.
// However, dispatching a scroll event can potentially cause more scroll
// events to be posted, so we move the initial set into a temporary array
// first. (Newly posted scroll events will be dispatched on the next tick.)
ScrollEventArray events = std::move(mScrollEvents);
for (auto& event : events) {
event->Run();
}
}
void nsRefreshDriver::AddPostRefreshObserver( void nsRefreshDriver::AddPostRefreshObserver(
nsAPostRefreshObserver* aObserver) { nsAPostRefreshObserver* aObserver) {
MOZ_ASSERT(!mPostRefreshObservers.Contains(aObserver)); MOZ_ASSERT(!mPostRefreshObservers.Contains(aObserver));
@ -1654,17 +1633,6 @@ void nsRefreshDriver::FlushForceNotifyContentfulPaintPresContext() {
} }
} }
void nsRefreshDriver::RunDelayedEventsSoon() {
// Place entries for delayed events into their corresponding normal list,
// and schedule a refresh. When these delayed events run, if their document
// still has events suppressed then they will be readded to the delayed
// events list.
mScrollEvents.AppendElements(mDelayedScrollEvents);
mDelayedScrollEvents.Clear();
ScheduleRenderingPhase(RenderingPhase::ScrollSteps);
}
bool nsRefreshDriver::CanDoCatchUpTick() { bool nsRefreshDriver::CanDoCatchUpTick() {
if (mTestControllingRefreshes || !mActiveTimer) { if (mTestControllingRefreshes || !mActiveTimer) {
return false; return false;
@ -2584,9 +2552,12 @@ void nsRefreshDriver::Tick(VsyncId aId, TimeStamp aNowTime,
}); });
// Step 9. For each doc of docs, run the scroll steps for doc. // Step 9. For each doc of docs, run the scroll steps for doc.
RunRenderingPhaseLegacy( RunRenderingPhase(RenderingPhase::ScrollSteps,
RenderingPhase::ScrollSteps, [](Document& aDoc) MOZ_CAN_RUN_SCRIPT_BOUNDARY_LAMBDA {
[&]() MOZ_CAN_RUN_SCRIPT_BOUNDARY_LAMBDA { DispatchScrollEvents(); }); if (RefPtr<PresShell> ps = aDoc.GetPresShell()) {
ps->RunScrollSteps();
}
});
// Step 10. For each doc of docs, evaluate media queries and report changes // Step 10. For each doc of docs, evaluate media queries and report changes
// for doc. // for doc.

View file

@ -26,7 +26,6 @@
#include "nsThreadUtils.h" #include "nsThreadUtils.h"
#include "mozilla/Attributes.h" #include "mozilla/Attributes.h"
#include "mozilla/Maybe.h" #include "mozilla/Maybe.h"
#include "mozilla/dom/VisualViewport.h"
#include "mozilla/layers/TransactionIdAllocator.h" #include "mozilla/layers/TransactionIdAllocator.h"
#include "LayersTypes.h" #include "LayersTypes.h"
@ -46,14 +45,17 @@ class PresShell;
class RefreshDriverTimer; class RefreshDriverTimer;
class Runnable; class Runnable;
class Task; class Task;
namespace dom {
class Document;
}
} // namespace mozilla } // namespace mozilla
class nsRefreshDriver final : public mozilla::layers::TransactionIdAllocator, class nsRefreshDriver final : public mozilla::layers::TransactionIdAllocator,
public nsARefreshObserver { public nsARefreshObserver {
using Document = mozilla::dom::Document; using Document = mozilla::dom::Document;
using TransactionId = mozilla::layers::TransactionId; using TransactionId = mozilla::layers::TransactionId;
using VVPScrollEvent =
mozilla::dom::VisualViewport::VisualViewportScrollEvent;
using LogPresShellObserver = mozilla::LogPresShellObserver; using LogPresShellObserver = mozilla::LogPresShellObserver;
public: public:
@ -105,8 +107,6 @@ class nsRefreshDriver final : public mozilla::layers::TransactionIdAllocator,
mozilla::FlushType aFlushType); mozilla::FlushType aFlushType);
void PostScrollEvent(mozilla::Runnable* aScrollEvent, bool aDelayed = false);
void DispatchScrollEvents();
MOZ_CAN_RUN_SCRIPT void FlushLayoutOnPendingDocsAndFixUpFocus(); MOZ_CAN_RUN_SCRIPT void FlushLayoutOnPendingDocsAndFixUpFocus();
/** /**
@ -341,9 +341,6 @@ class nsRefreshDriver final : public mozilla::layers::TransactionIdAllocator,
static void DispatchIdleTaskAfterTickUnlessExists(mozilla::Task* aTask); static void DispatchIdleTaskAfterTickUnlessExists(mozilla::Task* aTask);
static void CancelIdleTask(mozilla::Task* aTask); static void CancelIdleTask(mozilla::Task* aTask);
// Schedule a refresh so that any delayed events will run soon.
void RunDelayedEventsSoon();
void InitializeTimer() { void InitializeTimer() {
MOZ_ASSERT(!mActiveTimer); MOZ_ASSERT(!mActiveTimer);
EnsureTimerStarted(); EnsureTimerStarted();
@ -420,7 +417,6 @@ class nsRefreshDriver final : public mozilla::layers::TransactionIdAllocator,
void FinishedVsyncTick() { mAttemptedExtraTickSinceLastVsync = false; } void FinishedVsyncTick() { mAttemptedExtraTickSinceLastVsync = false; }
private: private:
using ScrollEventArray = nsTArray<RefPtr<mozilla::Runnable>>;
using RequestTable = nsTHashSet<RefPtr<imgIRequest>>; using RequestTable = nsTHashSet<RefPtr<imgIRequest>>;
struct ImageStartData { struct ImageStartData {
ImageStartData() = default; ImageStartData() = default;
@ -450,10 +446,10 @@ class nsRefreshDriver final : public mozilla::layers::TransactionIdAllocator,
MOZ_CAN_RUN_SCRIPT MOZ_CAN_RUN_SCRIPT
void RunVideoAndFrameRequestCallbacks(mozilla::TimeStamp aNowTime); void RunVideoAndFrameRequestCallbacks(mozilla::TimeStamp aNowTime);
MOZ_CAN_RUN_SCRIPT MOZ_CAN_RUN_SCRIPT
void RunVideoFrameCallbacks(const nsTArray<RefPtr<mozilla::dom::Document>>&, void RunVideoFrameCallbacks(const nsTArray<RefPtr<Document>>&,
mozilla::TimeStamp aNowTime); mozilla::TimeStamp aNowTime);
MOZ_CAN_RUN_SCRIPT MOZ_CAN_RUN_SCRIPT
void RunFrameRequestCallbacks(const nsTArray<RefPtr<mozilla::dom::Document>>&, void RunFrameRequestCallbacks(const nsTArray<RefPtr<Document>>&,
mozilla::TimeStamp aNowTime); mozilla::TimeStamp aNowTime);
void UpdateRemoteFrameEffects(); void UpdateRemoteFrameEffects();
void UpdateRelevancyOfContentVisibilityAutoFrames(); void UpdateRelevancyOfContentVisibilityAutoFrames();
@ -645,9 +641,6 @@ class nsRefreshDriver final : public mozilla::layers::TransactionIdAllocator,
RequestTable mRequests; RequestTable mRequests;
ImageStartTable mStartTable; ImageStartTable mStartTable;
AutoTArray<nsCOMPtr<nsIRunnable>, 16> mEarlyRunners; AutoTArray<nsCOMPtr<nsIRunnable>, 16> mEarlyRunners;
ScrollEventArray mScrollEvents;
// Scroll events on documents that might have events suppressed.
ScrollEventArray mDelayedScrollEvents;
AutoTArray<mozilla::PresShell*, 16> mStyleFlushObservers; AutoTArray<mozilla::PresShell*, 16> mStyleFlushObservers;
nsTObserverArray<nsAPostRefreshObserver*> mPostRefreshObservers; nsTObserverArray<nsAPostRefreshObserver*> mPostRefreshObservers;
nsTArray<mozilla::UniquePtr<mozilla::PendingFullscreenEvent>> nsTArray<mozilla::UniquePtr<mozilla::PendingFullscreenEvent>>

View file

@ -159,7 +159,7 @@ static ScrollDirections GetOverflowChange(const nsRect& aCurScrolledRect,
class ScrollContainerFrame::ScrollEvent : public Runnable { class ScrollContainerFrame::ScrollEvent : public Runnable {
public: public:
NS_DECL_NSIRUNNABLE NS_DECL_NSIRUNNABLE
explicit ScrollEvent(ScrollContainerFrame* aHelper, bool aDelayed); explicit ScrollEvent(ScrollContainerFrame* aHelper);
void Revoke() { mHelper = nullptr; } void Revoke() { mHelper = nullptr; }
private: private:
@ -169,7 +169,7 @@ class ScrollContainerFrame::ScrollEvent : public Runnable {
class ScrollContainerFrame::ScrollEndEvent : public Runnable { class ScrollContainerFrame::ScrollEndEvent : public Runnable {
public: public:
NS_DECL_NSIRUNNABLE NS_DECL_NSIRUNNABLE
explicit ScrollEndEvent(ScrollContainerFrame* aHelper, bool aDelayed); explicit ScrollEndEvent(ScrollContainerFrame* aHelper);
void Revoke() { mHelper = nullptr; } void Revoke() { mHelper = nullptr; }
private: private:
@ -5382,30 +5382,20 @@ nsresult ScrollContainerFrame::FireScrollPortEvent() {
return EventDispatcher::Dispatch(content, presContext, &event); return EventDispatcher::Dispatch(content, presContext, &event);
} }
void ScrollContainerFrame::PostScrollEndEvent(bool aDelayed) { void ScrollContainerFrame::PostScrollEndEvent() {
if (mScrollEndEvent) { if (mScrollEndEvent) {
return; return;
} }
// The ScrollEndEvent constructor registers itself with the refresh driver. // The ScrollEndEvent constructor registers itself.
mScrollEndEvent = new ScrollEndEvent(this, aDelayed); mScrollEndEvent = new ScrollEndEvent(this);
} }
void ScrollContainerFrame::FireScrollEndEvent() { void ScrollContainerFrame::FireScrollEndEvent() {
RefPtr<nsIContent> content = GetContent();
MOZ_ASSERT(content);
MOZ_ASSERT(mScrollEndEvent); MOZ_ASSERT(mScrollEndEvent);
mScrollEndEvent->Revoke(); mScrollEndEvent->Revoke();
mScrollEndEvent = nullptr; mScrollEndEvent = nullptr;
if (content->GetComposedDoc() &&
content->GetComposedDoc()->EventHandlingSuppressed()) {
content->GetComposedDoc()->SetHasDelayedRefreshEvent();
PostScrollEndEvent(/* aDelayed = */ true);
return;
}
RefPtr<nsPresContext> presContext = PresContext(); RefPtr<nsPresContext> presContext = PresContext();
nsEventStatus status = nsEventStatus_eIgnore; nsEventStatus status = nsEventStatus_eIgnore;
WidgetGUIEvent event(true, eScrollend, nullptr); WidgetGUIEvent event(true, eScrollend, nullptr);
@ -5825,10 +5815,9 @@ void ScrollContainerFrame::CurPosAttributeChangedInternal(nsIContent* aContent,
/* ============= Scroll events ========== */ /* ============= Scroll events ========== */
ScrollContainerFrame::ScrollEvent::ScrollEvent(ScrollContainerFrame* aHelper, ScrollContainerFrame::ScrollEvent::ScrollEvent(ScrollContainerFrame* aHelper)
bool aDelayed)
: Runnable("ScrollContainerFrame::ScrollEvent"), mHelper(aHelper) { : Runnable("ScrollContainerFrame::ScrollEvent"), mHelper(aHelper) {
mHelper->PresContext()->RefreshDriver()->PostScrollEvent(this, aDelayed); mHelper->PresShell()->PostScrollEvent(this);
} }
// TODO: Convert this to MOZ_CAN_RUN_SCRIPT (bug 1415230, bug 1535398) // TODO: Convert this to MOZ_CAN_RUN_SCRIPT (bug 1415230, bug 1535398)
@ -5841,9 +5830,9 @@ ScrollContainerFrame::ScrollEvent::Run() {
} }
ScrollContainerFrame::ScrollEndEvent::ScrollEndEvent( ScrollContainerFrame::ScrollEndEvent::ScrollEndEvent(
ScrollContainerFrame* aHelper, bool aDelayed) ScrollContainerFrame* aHelper)
: Runnable("ScrollContainerFrame::ScrollEndEvent"), mHelper(aHelper) { : Runnable("ScrollContainerFrame::ScrollEndEvent"), mHelper(aHelper) {
mHelper->PresContext()->RefreshDriver()->PostScrollEvent(this, aDelayed); mHelper->PresShell()->PostScrollEvent(this);
} }
MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHODIMP MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHODIMP
@ -5864,16 +5853,6 @@ void ScrollContainerFrame::FireScrollEvent() {
mScrollEvent->Revoke(); mScrollEvent->Revoke();
mScrollEvent = nullptr; mScrollEvent = nullptr;
// If event handling is suppressed, keep posting the scroll event to the
// refresh driver until it is unsuppressed. The event is marked as delayed so
// that the refresh driver does not continue ticking.
if (content->GetComposedDoc() &&
content->GetComposedDoc()->EventHandlingSuppressed()) {
content->GetComposedDoc()->SetHasDelayedRefreshEvent();
PostScrollEvent(/* aDelayed = */ true);
return;
}
bool oldProcessing = mProcessingScrollEvent; bool oldProcessing = mProcessingScrollEvent;
AutoWeakFrame weakFrame(this); AutoWeakFrame weakFrame(this);
auto RestoreProcessingScrollEvent = mozilla::MakeScopeExit([&] { auto RestoreProcessingScrollEvent = mozilla::MakeScopeExit([&] {
@ -5903,13 +5882,13 @@ void ScrollContainerFrame::FireScrollEvent() {
} }
} }
void ScrollContainerFrame::PostScrollEvent(bool aDelayed) { void ScrollContainerFrame::PostScrollEvent() {
if (mScrollEvent) { if (mScrollEvent) {
return; return;
} }
// The ScrollEvent constructor registers itself with the refresh driver. // The ScrollEvent constructor registers itself.
mScrollEvent = new ScrollEvent(this, aDelayed); mScrollEvent = new ScrollEvent(this);
} }
// TODO: Convert this to MOZ_CAN_RUN_SCRIPT (bug 1415230, bug 1535398) // TODO: Convert this to MOZ_CAN_RUN_SCRIPT (bug 1415230, bug 1535398)

View file

@ -1083,7 +1083,7 @@ class ScrollContainerFrame : public nsContainerFrame,
PhysicalAxes GetOverflowAxes() const; PhysicalAxes GetOverflowAxes() const;
MOZ_CAN_RUN_SCRIPT nsresult FireScrollPortEvent(); MOZ_CAN_RUN_SCRIPT nsresult FireScrollPortEvent();
void PostScrollEndEvent(bool aDelayed = false); void PostScrollEndEvent();
MOZ_CAN_RUN_SCRIPT void FireScrollEndEvent(); MOZ_CAN_RUN_SCRIPT void FireScrollEndEvent();
void PostOverflowEvent(); void PostOverflowEvent();
@ -1111,7 +1111,7 @@ class ScrollContainerFrame : public nsContainerFrame,
*/ */
void CurPosAttributeChangedInternal(nsIContent*, bool aDoScroll = true); void CurPosAttributeChangedInternal(nsIContent*, bool aDoScroll = true);
void PostScrollEvent(bool aDelayed = false); void PostScrollEvent();
MOZ_CAN_RUN_SCRIPT void FireScrollEvent(); MOZ_CAN_RUN_SCRIPT void FireScrollEvent();
void PostScrolledAreaEvent(); void PostScrolledAreaEvent();
MOZ_CAN_RUN_SCRIPT void FireScrolledAreaEvent(); MOZ_CAN_RUN_SCRIPT void FireScrolledAreaEvent();