import from UXP: Issue #1643 - Part 2: Implement ResizeObserver API (abb36309)

This commit is contained in:
2022-04-16 00:48:24 +08:00
parent 3092aeef0c
commit ecb025cd3a
7 changed files with 618 additions and 0 deletions
+304
View File
@@ -0,0 +1,304 @@
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* 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 "mozilla/dom/ResizeObserver.h"
#include "mozilla/dom/DOMRect.h"
#include "nsContentUtils.h"
#include "nsIFrame.h"
#include "nsSVGUtils.h"
namespace mozilla {
namespace dom {
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ResizeObserver)
NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
NS_INTERFACE_MAP_ENTRY(nsISupports)
NS_INTERFACE_MAP_END
NS_IMPL_CYCLE_COLLECTING_ADDREF(ResizeObserver)
NS_IMPL_CYCLE_COLLECTING_RELEASE(ResizeObserver)
NS_IMPL_CYCLE_COLLECTION_CLASS(ResizeObserver)
NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(ResizeObserver)
NS_IMPL_CYCLE_COLLECTION_TRACE_PRESERVED_WRAPPER
NS_IMPL_CYCLE_COLLECTION_TRACE_END
NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(ResizeObserver)
NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER
NS_IMPL_CYCLE_COLLECTION_UNLINK(mOwner)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mCallback)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mObservationMap)
NS_IMPL_CYCLE_COLLECTION_UNLINK_END
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(ResizeObserver)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mOwner)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mCallback)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mObservationMap)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
already_AddRefed<ResizeObserver>
ResizeObserver::Constructor(const GlobalObject& aGlobal,
ResizeObserverCallback& aCb,
ErrorResult& aRv)
{
nsCOMPtr<nsPIDOMWindowInner> window =
do_QueryInterface(aGlobal.GetAsSupports());
if (!window) {
aRv.Throw(NS_ERROR_FAILURE);
return nullptr;
}
nsCOMPtr<nsIDocument> document = window->GetExtantDoc();
if (!document) {
aRv.Throw(NS_ERROR_FAILURE);
return nullptr;
}
RefPtr<ResizeObserver> observer = new ResizeObserver(window.forget(), aCb);
// TODO: Add the new ResizeObserver to document here in the later patch.
return observer.forget();
}
void
ResizeObserver::Observe(Element* aTarget,
ErrorResult& aRv)
{
if (!aTarget) {
aRv.Throw(NS_ERROR_DOM_NOT_FOUND_ERR);
return;
}
RefPtr<ResizeObservation> observation;
if (!mObservationMap.Get(aTarget, getter_AddRefs(observation))) {
observation = new ResizeObservation(this, aTarget);
mObservationMap.Put(aTarget, observation);
mObservationList.insertBack(observation);
// Per the spec, we need to trigger notification in event loop that
// contains ResizeObserver observe call even when resize/reflow does
// not happen.
// TODO: Implement the notification scheduling in the later patch.
}
}
void
ResizeObserver::Unobserve(Element* aTarget,
ErrorResult& aRv)
{
if (!aTarget) {
aRv.Throw(NS_ERROR_DOM_NOT_FOUND_ERR);
return;
}
RefPtr<ResizeObservation> observation;
if (mObservationMap.Get(aTarget, getter_AddRefs(observation))) {
mObservationMap.Remove(aTarget);
MOZ_ASSERT(!mObservationList.isEmpty(),
"If ResizeObservation found for an element, observation list "
"must be not empty.");
observation->remove();
}
}
void
ResizeObserver::Disconnect()
{
mObservationMap.Clear();
mObservationList.clear();
mActiveTargets.Clear();
}
void
ResizeObserver::GatherActiveObservations(uint32_t aDepth)
{
mActiveTargets.Clear();
mHasSkippedTargets = false;
for (auto observation : mObservationList) {
if (observation->IsActive()) {
uint32_t targetDepth =
nsContentUtils::GetNodeDepth(observation->Target());
if (targetDepth > aDepth) {
mActiveTargets.AppendElement(observation);
} else {
mHasSkippedTargets = true;
}
}
}
}
bool
ResizeObserver::HasActiveObservations() const
{
return !mActiveTargets.IsEmpty();
}
bool
ResizeObserver::HasSkippedObservations() const
{
return mHasSkippedTargets;
}
uint32_t
ResizeObserver::BroadcastActiveObservations()
{
uint32_t shallowestTargetDepth = UINT32_MAX;
if (HasActiveObservations()) {
Sequence<OwningNonNull<ResizeObserverEntry>> entries;
for (auto observation : mActiveTargets) {
RefPtr<ResizeObserverEntry> entry =
new ResizeObserverEntry(this, observation->Target());
nsRect rect = observation->GetTargetRect();
entry->SetContentRect(rect);
if (!entries.AppendElement(entry.forget(), fallible)) {
// Out of memory.
break;
}
// Sync the broadcast size of observation so the next size inspection
// will be based on the updated size from last delivered observations.
observation->UpdateBroadcastSize(rect);
uint32_t targetDepth =
nsContentUtils::GetNodeDepth(observation->Target());
if (targetDepth < shallowestTargetDepth) {
shallowestTargetDepth = targetDepth;
}
}
mCallback->Call(this, entries, *this);
mActiveTargets.Clear();
mHasSkippedTargets = false;
}
return shallowestTargetDepth;
}
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ResizeObserverEntry)
NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
NS_INTERFACE_MAP_ENTRY(nsISupports)
NS_INTERFACE_MAP_END
NS_IMPL_CYCLE_COLLECTING_ADDREF(ResizeObserverEntry)
NS_IMPL_CYCLE_COLLECTING_RELEASE(ResizeObserverEntry)
NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(ResizeObserverEntry,
mTarget, mContentRect,
mOwner)
already_AddRefed<ResizeObserverEntry>
ResizeObserverEntry::Constructor(const GlobalObject& aGlobal,
Element* aTarget,
ErrorResult& aRv)
{
RefPtr<ResizeObserverEntry> observerEntry =
new ResizeObserverEntry(aGlobal.GetAsSupports(), aTarget);
return observerEntry.forget();
}
void
ResizeObserverEntry::SetContentRect(nsRect aRect)
{
RefPtr<DOMRect> contentRect = new DOMRect(mTarget);
nsIFrame* frame = mTarget->GetPrimaryFrame();
if (frame) {
nsMargin padding = frame->GetUsedPadding();
// Per the spec, we need to include padding in contentRect of
// ResizeObserverEntry.
aRect.x = padding.left;
aRect.y = padding.top;
}
contentRect->SetLayoutRect(aRect);
mContentRect = contentRect.forget();
}
ResizeObserverEntry::~ResizeObserverEntry()
{
}
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ResizeObservation)
NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
NS_INTERFACE_MAP_ENTRY(nsISupports)
NS_INTERFACE_MAP_END
NS_IMPL_CYCLE_COLLECTING_ADDREF(ResizeObservation)
NS_IMPL_CYCLE_COLLECTING_RELEASE(ResizeObservation)
NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(ResizeObservation,
mTarget, mOwner)
already_AddRefed<ResizeObservation>
ResizeObservation::Constructor(const GlobalObject& aGlobal,
Element* aTarget,
ErrorResult& aRv)
{
RefPtr<ResizeObservation> observation =
new ResizeObservation(aGlobal.GetAsSupports(), aTarget);
return observation.forget();
}
bool
ResizeObservation::IsActive() const
{
nsRect rect = GetTargetRect();
return (rect.width != mBroadcastWidth || rect.height != mBroadcastHeight);
}
void
ResizeObservation::UpdateBroadcastSize(nsRect aRect)
{
mBroadcastWidth = aRect.width;
mBroadcastHeight = aRect.height;
}
nsRect
ResizeObservation::GetTargetRect() const
{
nsRect rect;
nsIFrame* frame = mTarget->GetPrimaryFrame();
if (frame) {
if (mTarget->IsSVGElement()) {
gfxRect bbox = nsSVGUtils::GetBBox(frame);
rect.width = NSFloatPixelsToAppUnits(bbox.width, AppUnitsPerCSSPixel());
rect.height = NSFloatPixelsToAppUnits(bbox.height, AppUnitsPerCSSPixel());
} else {
// Per the spec, non-replaced inline Elements will always have an empty
// content rect.
if (frame->IsFrameOfType(nsIFrame::eReplaced) ||
!frame->IsFrameOfType(nsIFrame::eLineParticipant)) {
rect = frame->GetContentRectRelativeToSelf();
}
}
}
return rect;
}
ResizeObservation::~ResizeObservation()
{
}
} // namespace dom
} // namespace mozilla
+254
View File
@@ -0,0 +1,254 @@
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* 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/. */
#ifndef mozilla_dom_ResizeObserver_h
#define mozilla_dom_ResizeObserver_h
#include "mozilla/dom/ResizeObserverBinding.h"
namespace mozilla {
namespace dom {
/**
* ResizeObserver interfaces and algorithms are based on
* https://wicg.github.io/ResizeObserver/#api
*/
class ResizeObserver final
: public nsISupports
, public nsWrapperCache
{
public:
NS_DECL_CYCLE_COLLECTING_ISUPPORTS
NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(ResizeObserver)
ResizeObserver(already_AddRefed<nsPIDOMWindowInner>&& aOwner,
ResizeObserverCallback& aCb)
: mOwner(aOwner)
, mCallback(&aCb)
{
MOZ_ASSERT(mOwner, "Need a non-null owner window");
}
static already_AddRefed<ResizeObserver>
Constructor(const GlobalObject& aGlobal,
ResizeObserverCallback& aCb,
ErrorResult& aRv);
JSObject* WrapObject(JSContext* aCx,
JS::Handle<JSObject*> aGivenProto) override
{
return ResizeObserverBinding::Wrap(aCx, this, aGivenProto);
}
nsISupports* GetParentObject() const
{
return mOwner;
}
void Observe(Element* aTarget, ErrorResult& aRv);
void Unobserve(Element* aTarget, ErrorResult& aRv);
void Disconnect();
/*
* Gather all observations which have an observed target with size changed
* since last BroadcastActiveObservations() in this ResizeObserver.
* An observation will be skipped if the depth of its observed target is less
* or equal than aDepth. All gathered observations will be added to
* mActiveTargets.
*/
void GatherActiveObservations(uint32_t aDepth);
/*
* Returns whether this ResizeObserver has any active observations
* since last GatherActiveObservations().
*/
bool HasActiveObservations() const;
/*
* Returns whether this ResizeObserver has any skipped observations
* since last GatherActiveObservations().
*/
bool HasSkippedObservations() const;
/*
* Deliver the callback function in JavaScript for all active observations
* and pass the sequence of ResizeObserverEntry so JavaScript can access them.
* The broadcast size of observations will be updated and mActiveTargets will
* be cleared. It also returns the shallowest depth of elements from active
* observations or UINT32_MAX if there is no any active observations.
*/
uint32_t BroadcastActiveObservations();
protected:
~ResizeObserver()
{
mObservationList.clear();
}
nsCOMPtr<nsPIDOMWindowInner> mOwner;
RefPtr<ResizeObserverCallback> mCallback;
nsTArray<RefPtr<ResizeObservation>> mActiveTargets;
bool mHasSkippedTargets;
// Combination of HashTable and LinkedList so we can iterate through
// the elements of HashTable in order of insertion time.
// Will be nice if we have our own data structure for this in the future.
nsRefPtrHashtable<nsPtrHashKey<Element>, ResizeObservation> mObservationMap;
LinkedList<ResizeObservation> mObservationList;
};
/**
* ResizeObserverEntry is the entry that contains the information for observed
* elements. This object is the one that visible to JavaScript in callback
* function that is fired by ResizeObserver.
*/
class ResizeObserverEntry final
: public nsISupports
, public nsWrapperCache
{
public:
NS_DECL_CYCLE_COLLECTING_ISUPPORTS
NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(ResizeObserverEntry)
ResizeObserverEntry(nsISupports* aOwner, Element* aTarget)
: mOwner(aOwner)
, mTarget(aTarget)
{
MOZ_ASSERT(mOwner, "Need a non-null owner");
MOZ_ASSERT(mTarget, "Need a non-null target element");
}
static already_AddRefed<ResizeObserverEntry>
Constructor(const GlobalObject& aGlobal,
Element* aTarget,
ErrorResult& aRv);
JSObject* WrapObject(JSContext* aCx,
JS::Handle<JSObject*> aGivenProto) override
{
return ResizeObserverEntryBinding::Wrap(aCx, this,
aGivenProto);
}
nsISupports* GetParentObject() const
{
return mOwner;
}
Element* Target() const
{
return mTarget;
}
/*
* Returns the DOMRectReadOnly of target's content rect so it can be
* accessed from JavaScript in callback function of ResizeObserver.
*/
DOMRectReadOnly* GetContentRect() const
{
return mContentRect;
}
void SetContentRect(nsRect aRect);
protected:
~ResizeObserverEntry();
nsCOMPtr<nsISupports> mOwner;
nsCOMPtr<Element> mTarget;
RefPtr<DOMRectReadOnly> mContentRect;
};
/**
* We use ResizeObservation to store and sync the size information of one
* observed element so we can decide whether an observation should be fired
* or not.
*/
class ResizeObservation final
: public nsISupports
, public nsWrapperCache
, public LinkedListElement<ResizeObservation>
{
public:
NS_DECL_CYCLE_COLLECTING_ISUPPORTS
NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(ResizeObservation)
ResizeObservation(nsISupports* aOwner, Element* aTarget)
: mOwner(aOwner)
, mTarget(aTarget)
, mBroadcastWidth(0)
, mBroadcastHeight(0)
{
MOZ_ASSERT(mOwner, "Need a non-null owner");
MOZ_ASSERT(mTarget, "Need a non-null target element");
}
static already_AddRefed<ResizeObservation>
Constructor(const GlobalObject& aGlobal,
Element* aTarget,
ErrorResult& aRv);
JSObject* WrapObject(JSContext* aCx,
JS::Handle<JSObject*> aGivenProto) override
{
return ResizeObservationBinding::Wrap(aCx, this, aGivenProto);
}
nsISupports* GetParentObject() const
{
return mOwner;
}
Element* Target() const
{
return mTarget;
}
nscoord BroadcastWidth() const
{
return mBroadcastWidth;
}
nscoord BroadcastHeight() const
{
return mBroadcastHeight;
}
/*
* Returns whether the observed target element size differs from current
* BroadcastWidth and BroadcastHeight
*/
bool IsActive() const;
/*
* Update current BroadcastWidth and BroadcastHeight with size from aRect.
*/
void UpdateBroadcastSize(nsRect aRect);
/*
* Returns the target's rect in the form of nsRect.
* If the target is SVG, width and height are determined from bounding box.
*/
nsRect GetTargetRect() const;
protected:
~ResizeObservation();
nsCOMPtr<nsISupports> mOwner;
nsCOMPtr<Element> mTarget;
// Broadcast width and broadcast height are the latest recorded size
// of observed target.
nscoord mBroadcastWidth;
nscoord mBroadcastHeight;
};
} // namespace dom
} // namespace mozilla
#endif // mozilla_dom_ResizeObserver_h
+2
View File
@@ -193,6 +193,7 @@ EXPORTS.mozilla.dom += [
'PartialSHistory.h',
'Pose.h',
'ProcessGlobal.h',
'ResizeObserver.h',
'ResponsiveImageSelector.h',
'SameProcessMessageQueue.h',
'ScreenOrientation.h',
@@ -332,6 +333,7 @@ UNIFIED_SOURCES += [
'Pose.cpp',
'PostMessageEvent.cpp',
'ProcessGlobal.cpp',
'ResizeObserver.cpp',
'ResponsiveImageSelector.cpp',
'SameProcessMessageQueue.cpp',
'ScreenOrientation.cpp',
+15
View File
@@ -796,6 +796,21 @@ DOMInterfaces = {
},
},
'ResizeObservation': {
'nativeType': 'mozilla::dom::ResizeObservation',
'headerFile': 'mozilla/dom/ResizeObserver.h',
},
'ResizeObserver': {
'nativeType': 'mozilla::dom::ResizeObserver',
'headerFile': 'mozilla/dom/ResizeObserver.h',
},
'ResizeObserverEntry': {
'nativeType': 'mozilla::dom::ResizeObserverEntry',
'headerFile': 'mozilla/dom/ResizeObserver.h',
},
'Response': {
'binaryNames': { 'headers': 'headers_' },
},
+39
View File
@@ -0,0 +1,39 @@
/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* 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/.
*
* The origin of this IDL file is
* https://wicg.github.io/ResizeObserver/
*/
[Constructor(ResizeObserverCallback callback),
Exposed=Window,
Pref="layout.css.resizeobserver.enabled"]
interface ResizeObserver {
[Throws]
void observe(Element? target);
[Throws]
void unobserve(Element? target);
void disconnect();
};
callback ResizeObserverCallback = void (sequence<ResizeObserverEntry> entries, ResizeObserver observer);
[Constructor(Element? target),
ChromeOnly,
Pref="layout.css.resizeobserver.enabled"]
interface ResizeObserverEntry {
readonly attribute Element target;
readonly attribute DOMRectReadOnly? contentRect;
};
[Constructor(Element? target),
ChromeOnly,
Pref="layout.css.resizeobserver.enabled"]
interface ResizeObservation {
readonly attribute Element target;
readonly attribute long broadcastWidth;
readonly attribute long broadcastHeight;
boolean isActive();
};
+1
View File
@@ -401,6 +401,7 @@ WEBIDL_FILES = [
'Range.webidl',
'Rect.webidl',
'Request.webidl',
'ResizeObserver.webidl',
'Response.webidl',
'RGBColor.webidl',
'RTCStatsReport.webidl',
+3
View File
@@ -2722,6 +2722,9 @@ pref("layout.css.control-characters.visible", false);
pref("layout.css.control-characters.visible", true);
#endif
// Is support for ResizeObservers enabled?
pref("layout.css.resizeobserver.enabled", true);
// pref for which side vertical scrollbars should be on
// 0 = end-side in UI direction
// 1 = end-side in document/content direction