From 1b73824161e6cd2f5d2f63bc323b4e8a607207d0 Mon Sep 17 00:00:00 2001 From: Moonchild Date: Wed, 16 Sep 2020 16:39:15 +0000 Subject: Issue #1643 - Part 2: Implement ResizeObserver API Implements ResizeObserver, ResizeObserverEntry and ResizeObservation --- dom/base/ResizeObserver.cpp | 304 +++++++++++++++++++++++++++++++++++++++ dom/base/ResizeObserver.h | 254 ++++++++++++++++++++++++++++++++ dom/base/moz.build | 2 + dom/bindings/Bindings.conf | 15 ++ dom/webidl/ResizeObserver.webidl | 39 +++++ dom/webidl/moz.build | 1 + 6 files changed, 615 insertions(+) create mode 100644 dom/base/ResizeObserver.cpp create mode 100644 dom/base/ResizeObserver.h create mode 100644 dom/webidl/ResizeObserver.webidl (limited to 'dom') diff --git a/dom/base/ResizeObserver.cpp b/dom/base/ResizeObserver.cpp new file mode 100644 index 0000000000..aefcddd9d4 --- /dev/null +++ b/dom/base/ResizeObserver.cpp @@ -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::Constructor(const GlobalObject& aGlobal, + ResizeObserverCallback& aCb, + ErrorResult& aRv) +{ + nsCOMPtr window = + do_QueryInterface(aGlobal.GetAsSupports()); + + if (!window) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + nsCOMPtr document = window->GetExtantDoc(); + + if (!document) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + RefPtr 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 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 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> entries; + + for (auto observation : mActiveTargets) { + RefPtr 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::Constructor(const GlobalObject& aGlobal, + Element* aTarget, + ErrorResult& aRv) +{ + RefPtr observerEntry = + new ResizeObserverEntry(aGlobal.GetAsSupports(), aTarget); + return observerEntry.forget(); +} + +void +ResizeObserverEntry::SetContentRect(nsRect aRect) +{ + RefPtr 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::Constructor(const GlobalObject& aGlobal, + Element* aTarget, + ErrorResult& aRv) +{ + RefPtr 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 diff --git a/dom/base/ResizeObserver.h b/dom/base/ResizeObserver.h new file mode 100644 index 0000000000..2f56c580f4 --- /dev/null +++ b/dom/base/ResizeObserver.h @@ -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&& aOwner, + ResizeObserverCallback& aCb) + : mOwner(aOwner) + , mCallback(&aCb) + { + MOZ_ASSERT(mOwner, "Need a non-null owner window"); + } + + static already_AddRefed + Constructor(const GlobalObject& aGlobal, + ResizeObserverCallback& aCb, + ErrorResult& aRv); + + JSObject* WrapObject(JSContext* aCx, + JS::Handle 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 mOwner; + RefPtr mCallback; + nsTArray> 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, ResizeObservation> mObservationMap; + LinkedList 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 + Constructor(const GlobalObject& aGlobal, + Element* aTarget, + ErrorResult& aRv); + + JSObject* WrapObject(JSContext* aCx, + JS::Handle 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 mOwner; + nsCOMPtr mTarget; + RefPtr 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 +{ +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 + Constructor(const GlobalObject& aGlobal, + Element* aTarget, + ErrorResult& aRv); + + JSObject* WrapObject(JSContext* aCx, + JS::Handle 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 mOwner; + nsCOMPtr 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 + diff --git a/dom/base/moz.build b/dom/base/moz.build index fe65453fe9..27322475ed 100755 --- a/dom/base/moz.build +++ b/dom/base/moz.build @@ -204,6 +204,7 @@ EXPORTS.mozilla.dom += [ 'PartialSHistory.h', 'Pose.h', 'ProcessGlobal.h', + 'ResizeObserver.h', 'ResponsiveImageSelector.h', 'SameProcessMessageQueue.h', 'ScreenOrientation.h', @@ -350,6 +351,7 @@ SOURCES += [ 'Pose.cpp', 'PostMessageEvent.cpp', 'ProcessGlobal.cpp', + 'ResizeObserver.cpp', 'ResponsiveImageSelector.cpp', 'SameProcessMessageQueue.cpp', 'ScreenOrientation.cpp', diff --git a/dom/bindings/Bindings.conf b/dom/bindings/Bindings.conf index 0b075069d5..7972f53115 100644 --- a/dom/bindings/Bindings.conf +++ b/dom/bindings/Bindings.conf @@ -717,6 +717,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_' }, }, diff --git a/dom/webidl/ResizeObserver.webidl b/dom/webidl/ResizeObserver.webidl new file mode 100644 index 0000000000..98700f53c6 --- /dev/null +++ b/dom/webidl/ResizeObserver.webidl @@ -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 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(); +}; diff --git a/dom/webidl/moz.build b/dom/webidl/moz.build index c56a9b984e..7dc30a897f 100644 --- a/dom/webidl/moz.build +++ b/dom/webidl/moz.build @@ -365,6 +365,7 @@ WEBIDL_FILES = [ 'Range.webidl', 'Rect.webidl', 'Request.webidl', + 'ResizeObserver.webidl', 'Response.webidl', 'RGBColor.webidl', 'RTCStatsReport.webidl', -- cgit v1.2.3