DXR is a code search and navigation tool aimed at making sense of large projects. It supports full-text and regex searches as well as structural queries.

Header

Mercurial (b6d82b1a6b02)

VCS Links

Line Code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim:set ts=2 sw=2 sts=2 et cindent: */
/* 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 "mozilla/dom/Document.h"
#include "nsIContent.h"
#include "nsIContentInlines.h"
#include "nsSVGUtils.h"
#include <limits>

namespace mozilla {
namespace dom {

/**
 * Returns the length of the parent-traversal path (in terms of the number of
 * nodes) to an unparented/root node from aNode. An unparented/root node is
 * considered to have a depth of 1, its children have a depth of 2, etc.
 * aNode is expected to be non-null.
 * Note: The shadow root is not part of the calculation because the caller,
 * ResizeObserver, doesn't observe the shadow root, and only needs relative
 * depths among all the observed targets. In other words, we calculate the
 * depth of the flattened tree.
 *
 * However, these is a spec issue about how to handle shadow DOM case. We
 * may need to update this function later:
 *   https://github.com/w3c/csswg-drafts/issues/3840
 *
 * https://drafts.csswg.org/resize-observer/#calculate-depth-for-node-h
 */
static uint32_t GetNodeDepth(nsINode* aNode) {
  uint32_t depth = 1;

  MOZ_ASSERT(aNode, "Node shouldn't be null");

  // Use GetFlattenedTreeParentNode to bypass the shadow root and cross the
  // shadow boundary to calculate the node depth without the shadow root.
  while ((aNode = aNode->GetFlattenedTreeParentNode())) {
    ++depth;
  }

  return depth;
}

/**
 * Returns |aTarget|'s size in the form of nsSize.
 * If the target is SVG, width and height are determined from bounding box.
 */
static nsSize GetTargetSize(Element* aTarget, ResizeObserverBoxOptions aBox) {
  nsSize size;
  nsIFrame* frame = aTarget->GetPrimaryFrame();

  if (!frame) {
    return size;
  }

  if (aTarget->IsSVGElement()) {
    // Per the spec, SVG size is always its bounding box size no matter what
    // box option you choose, because SVG elements do not use standard CSS box
    // model.
    gfxRect bbox = nsSVGUtils::GetBBox(frame);
    size.width = NSFloatPixelsToAppUnits(bbox.width, AppUnitsPerCSSPixel());
    size.height = NSFloatPixelsToAppUnits(bbox.height, AppUnitsPerCSSPixel());
  } else {
    // Per the spec, non-replaced inline Elements will always have an empty
    // content rect. Therefore, we always use the same trivially-empty size
    // for non-replaced inline elements here, and their IsActive() will
    // always return false. (So its observation won't be fired.)
    if (!frame->IsFrameOfType(nsIFrame::eReplaced) &&
        frame->IsFrameOfType(nsIFrame::eLineParticipant)) {
      return size;
    }

    switch (aBox) {
      case ResizeObserverBoxOptions::Border_box:
        // GetSize() includes the content area, borders, and padding.
        size = frame->GetSize();
        break;
      case ResizeObserverBoxOptions::Content_box:
      default:
        size = frame->GetContentRectRelativeToSelf().Size();
    }
  }

  return size;
}

NS_IMPL_CYCLE_COLLECTION(ResizeObservation, mTarget)
NS_IMPL_CYCLE_COLLECTION_ROOT_NATIVE(ResizeObservation, AddRef)
NS_IMPL_CYCLE_COLLECTION_UNROOT_NATIVE(ResizeObservation, Release)

bool ResizeObservation::IsActive() const {
  nsIFrame* frame = mTarget->GetPrimaryFrame();
  const WritingMode wm = frame ? frame->GetWritingMode() : WritingMode();
  const LogicalSize size(wm, GetTargetSize(mTarget, mObservedBox));
  return mLastReportedSize.ISize(mLastReportedWM) != size.ISize(wm) ||
         mLastReportedSize.BSize(mLastReportedWM) != size.BSize(wm);
}

void ResizeObservation::UpdateLastReportedSize(const nsSize& aSize) {
  nsIFrame* frame = mTarget->GetPrimaryFrame();
  mLastReportedWM = frame ? frame->GetWritingMode() : WritingMode();
  mLastReportedSize = LogicalSize(mLastReportedWM, aSize);
}

// Only needed for refcounted objects.
NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(ResizeObserver, mOwner, mCallback,
                                      mActiveTargets, mObservationMap)
NS_IMPL_CYCLE_COLLECTING_ADDREF(ResizeObserver)
NS_IMPL_CYCLE_COLLECTING_RELEASE(ResizeObserver)
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ResizeObserver)
  NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
  NS_INTERFACE_MAP_ENTRY(nsISupports)
NS_INTERFACE_MAP_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;
  }

  Document* document = window->GetExtantDoc();

  if (!document) {
    aRv.Throw(NS_ERROR_FAILURE);
    return nullptr;
  }

  RefPtr<ResizeObserver> observer = new ResizeObserver(window.forget(), aCb);
  document->AddResizeObserver(observer);

  return observer.forget();
}

void ResizeObserver::Observe(Element& aTarget,
                             const ResizeObserverOptions& aOptions,
                             ErrorResult& aRv) {
  RefPtr<ResizeObservation> observation;

  if (mObservationMap.Get(&aTarget, getter_AddRefs(observation))) {
    if (observation->BoxOptions() == aOptions.mBox) {
      // Already observed this target and the observed box is the same, so
      // return.
      // Note: Based on the spec, we should unobserve it first. However,
      // calling Unobserve() when we observe the same box will remove original
      // ResizeObservation and then add a new one, this may cause an unexpected
      // result because ResizeObservation stores the mLastReportedSize which
      // should be kept to make sure IsActive() returns the correct result.
      return;
    }
    Unobserve(aTarget, aRv);
  }

  nsIFrame* frame = aTarget.GetPrimaryFrame();
  observation = new ResizeObservation(
      aTarget, aOptions.mBox, frame ? frame->GetWritingMode() : WritingMode());

  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.
  aTarget.OwnerDoc()->ScheduleResizeObserversNotification();
}

void ResizeObserver::Unobserve(Element& aTarget, ErrorResult& aRv) {
  RefPtr<ResizeObservation> observation;
  if (!mObservationMap.Remove(&aTarget, getter_AddRefs(observation))) {
    return;
  }

  MOZ_ASSERT(!mObservationList.isEmpty(),
             "If ResizeObservation found for an element, observation list "
             "must be not empty.");
  observation->remove();
}

void ResizeObserver::Disconnect() {
  mObservationList.clear();
  mObservationMap.Clear();
  mActiveTargets.Clear();
}

void ResizeObserver::GatherActiveObservations(uint32_t aDepth) {
  mActiveTargets.Clear();
  mHasSkippedTargets = false;

  for (auto observation : mObservationList) {
    if (!observation->IsActive()) {
      continue;
    }

    uint32_t targetDepth = GetNodeDepth(observation->Target());

    if (targetDepth > aDepth) {
      mActiveTargets.AppendElement(observation);
    } else {
      mHasSkippedTargets = true;
    }
  }
}

uint32_t ResizeObserver::BroadcastActiveObservations() {
  uint32_t shallowestTargetDepth = std::numeric_limits<uint32_t>::max();

  if (!HasActiveObservations()) {
    return shallowestTargetDepth;
  }

  Sequence<OwningNonNull<ResizeObserverEntry>> entries;

  for (auto& observation : mActiveTargets) {
    Element* target = observation->Target();

    nsSize borderBoxSize =
        GetTargetSize(target, ResizeObserverBoxOptions::Border_box);
    nsSize contentBoxSize =
        GetTargetSize(target, ResizeObserverBoxOptions::Content_box);
    RefPtr<ResizeObserverEntry> entry =
        new ResizeObserverEntry(this, *target, borderBoxSize, contentBoxSize);

    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.
    switch (observation->BoxOptions()) {
      case ResizeObserverBoxOptions::Border_box:
        observation->UpdateLastReportedSize(borderBoxSize);
        break;
      case ResizeObserverBoxOptions::Content_box:
      default:
        observation->UpdateLastReportedSize(contentBoxSize);
    }

    uint32_t targetDepth = GetNodeDepth(observation->Target());

    if (targetDepth < shallowestTargetDepth) {
      shallowestTargetDepth = targetDepth;
    }
  }

  RefPtr<ResizeObserverCallback> callback(mCallback);
  callback->Call(this, entries, *this);

  mActiveTargets.Clear();
  mHasSkippedTargets = false;

  return shallowestTargetDepth;
}

NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(ResizeObserverEntry, mOwner, mTarget,
                                      mContentRect, mBorderBoxSize,
                                      mContentBoxSize)
NS_IMPL_CYCLE_COLLECTING_ADDREF(ResizeObserverEntry)
NS_IMPL_CYCLE_COLLECTING_RELEASE(ResizeObserverEntry)
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ResizeObserverEntry)
  NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
  NS_INTERFACE_MAP_ENTRY(nsISupports)
NS_INTERFACE_MAP_END

void ResizeObserverEntry::SetBorderBoxSize(const nsSize& aSize) {
  nsIFrame* frame = mTarget->GetPrimaryFrame();
  const WritingMode wm = frame ? frame->GetWritingMode() : WritingMode();
  mBorderBoxSize = new ResizeObserverSize(this, aSize, wm);
}

void ResizeObserverEntry::SetContentRectAndSize(const nsSize& aSize) {
  nsIFrame* frame = mTarget->GetPrimaryFrame();

  // 1. Update mContentRect.
  nsMargin padding = frame ? frame->GetUsedPadding() : nsMargin();
  // Per the spec, we need to use the top-left padding offset as the origin of
  // our contentRect.
  nsRect rect(nsPoint(padding.left, padding.top), aSize);
  RefPtr<DOMRect> contentRect = new DOMRect(this);
  contentRect->SetLayoutRect(rect);
  mContentRect = contentRect.forget();

  // 2. Update mContentBoxSize.
  const WritingMode wm = frame ? frame->GetWritingMode() : WritingMode();
  mContentBoxSize = new ResizeObserverSize(this, aSize, wm);
}

NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(ResizeObserverSize, mOwner)
NS_IMPL_CYCLE_COLLECTING_ADDREF(ResizeObserverSize)
NS_IMPL_CYCLE_COLLECTING_RELEASE(ResizeObserverSize)
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ResizeObserverSize)
  NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
  NS_INTERFACE_MAP_ENTRY(nsISupports)
NS_INTERFACE_MAP_END

}  // namespace dom
}  // namespace mozilla