GeometryUtils.cpp (17801B)
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */ 3 /* This Source Code Form is subject to the terms of the Mozilla Public 4 * License, v. 2.0. If a copy of the MPL was not distributed with this 5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 7 #include "GeometryUtils.h" 8 9 #include "mozilla/PresShell.h" 10 #include "mozilla/SVGUtils.h" 11 #include "mozilla/dom/BrowserChild.h" 12 #include "mozilla/dom/CharacterData.h" 13 #include "mozilla/dom/DOMPoint.h" 14 #include "mozilla/dom/DOMPointBinding.h" 15 #include "mozilla/dom/DOMQuad.h" 16 #include "mozilla/dom/DOMRect.h" 17 #include "mozilla/dom/DocumentInlines.h" 18 #include "mozilla/dom/Element.h" 19 #include "mozilla/dom/GeometryUtilsBinding.h" 20 #include "mozilla/dom/Text.h" 21 #include "nsCSSFrameConstructor.h" 22 #include "nsContainerFrame.h" 23 #include "nsContentUtils.h" 24 #include "nsIFrame.h" 25 #include "nsLayoutUtils.h" 26 27 using namespace mozilla; 28 using namespace mozilla::dom; 29 30 namespace mozilla { 31 32 enum GeometryNodeType { 33 GEOMETRY_NODE_ELEMENT, 34 GEOMETRY_NODE_TEXT, 35 GEOMETRY_NODE_DOCUMENT 36 }; 37 38 static nsIFrame* GetFrameForNode(nsINode* aNode, GeometryNodeType aType, 39 const GeometryUtilsOptions& aOptions) { 40 RefPtr<Document> doc = aNode->GetComposedDoc(); 41 if (!doc) { 42 return nullptr; 43 } 44 45 if (aOptions.mFlush) { 46 if (aType == GEOMETRY_NODE_TEXT && 47 aOptions.mCreateFramesForSuppressedWhitespace) { 48 if (PresShell* presShell = doc->GetPresShell()) { 49 presShell->FrameConstructor() 50 ->EnsureFrameForTextNodeIsCreatedAfterFlush( 51 static_cast<CharacterData*>(aNode)); 52 } 53 } 54 doc->FlushPendingNotifications(FlushType::Layout); 55 } 56 57 switch (aType) { 58 case GEOMETRY_NODE_TEXT: 59 case GEOMETRY_NODE_ELEMENT: 60 return aNode->AsContent()->GetPrimaryFrame(); 61 case GEOMETRY_NODE_DOCUMENT: { 62 PresShell* presShell = doc->GetPresShell(); 63 return presShell ? presShell->GetRootFrame() : nullptr; 64 } 65 default: 66 MOZ_ASSERT(false, "Unknown GeometryNodeType"); 67 return nullptr; 68 } 69 } 70 71 static nsIFrame* GetFrameForGeometryNode( 72 const Optional<OwningGeometryNode>& aGeometryNode, nsINode* aDefaultNode, 73 const GeometryUtilsOptions& aOptions) { 74 if (!aGeometryNode.WasPassed()) { 75 return GetFrameForNode(aDefaultNode->OwnerDoc(), GEOMETRY_NODE_DOCUMENT, 76 aOptions); 77 } 78 79 const OwningGeometryNode& value = aGeometryNode.Value(); 80 if (value.IsElement()) { 81 return GetFrameForNode(value.GetAsElement(), GEOMETRY_NODE_ELEMENT, 82 aOptions); 83 } 84 if (value.IsDocument()) { 85 return GetFrameForNode(value.GetAsDocument(), GEOMETRY_NODE_DOCUMENT, 86 aOptions); 87 } 88 return GetFrameForNode(value.GetAsText(), GEOMETRY_NODE_TEXT, aOptions); 89 } 90 91 static nsIFrame* GetFrameForGeometryNode(const GeometryNode& aGeometryNode, 92 const GeometryUtilsOptions& aOptions) { 93 if (aGeometryNode.IsElement()) { 94 return GetFrameForNode(&aGeometryNode.GetAsElement(), GEOMETRY_NODE_ELEMENT, 95 aOptions); 96 } 97 if (aGeometryNode.IsDocument()) { 98 return GetFrameForNode(&aGeometryNode.GetAsDocument(), 99 GEOMETRY_NODE_DOCUMENT, aOptions); 100 } 101 return GetFrameForNode(&aGeometryNode.GetAsText(), GEOMETRY_NODE_TEXT, 102 aOptions); 103 } 104 105 static nsIFrame* GetFrameForNode(nsINode* aNode, 106 const GeometryUtilsOptions& aOptions) { 107 if (aNode->IsElement()) { 108 return GetFrameForNode(aNode, GEOMETRY_NODE_ELEMENT, aOptions); 109 } 110 if (aNode == aNode->OwnerDoc()) { 111 return GetFrameForNode(aNode, GEOMETRY_NODE_DOCUMENT, aOptions); 112 } 113 NS_ASSERTION(aNode->IsText(), "Unknown node type"); 114 return GetFrameForNode(aNode, GEOMETRY_NODE_TEXT, aOptions); 115 } 116 117 static nsIFrame* GetFirstNonAnonymousFrameForGeometryNode( 118 const Optional<OwningGeometryNode>& aNode, nsINode* aDefaultNode, 119 const GeometryUtilsOptions& aOptions) { 120 if (nsIFrame* f = GetFrameForGeometryNode(aNode, aDefaultNode, aOptions)) { 121 return nsLayoutUtils::GetFirstNonAnonymousFrame(f); 122 } 123 return nullptr; 124 } 125 126 static nsIFrame* GetFirstNonAnonymousFrameForGeometryNode( 127 const GeometryNode& aNode, const GeometryUtilsOptions& aOptions) { 128 if (nsIFrame* f = GetFrameForGeometryNode(aNode, aOptions)) { 129 return nsLayoutUtils::GetFirstNonAnonymousFrame(f); 130 } 131 return nullptr; 132 } 133 134 static nsIFrame* GetFirstNonAnonymousFrameForNode( 135 nsINode* aNode, const GeometryUtilsOptions& aOptions) { 136 if (nsIFrame* f = GetFrameForNode(aNode, aOptions)) { 137 return nsLayoutUtils::GetFirstNonAnonymousFrame(f); 138 } 139 return nullptr; 140 } 141 142 /** 143 * This can modify aFrame to point to a different frame. This is needed to 144 * handle SVG, where SVG elements can only compute a rect that's valid with 145 * respect to the "outer SVG" frame. 146 */ 147 static nsRect GetBoxRectForFrame(nsIFrame** aFrame, CSSBoxType aType) { 148 nsRect r; 149 nsIFrame* f = SVGUtils::GetOuterSVGFrameAndCoveredRegion(*aFrame, &r); 150 if (f && f != *aFrame) { 151 // For non-outer SVG frames, the BoxType is ignored. 152 *aFrame = f; 153 return r; 154 } 155 156 f = *aFrame; 157 switch (aType) { 158 case CSSBoxType::Content: 159 r = f->GetContentRectRelativeToSelf(); 160 break; 161 case CSSBoxType::Padding: 162 r = f->GetPaddingRectRelativeToSelf(); 163 break; 164 case CSSBoxType::Border: 165 r = nsRect(nsPoint(0, 0), f->GetSize()); 166 break; 167 case CSSBoxType::Margin: 168 r = f->GetMarginRectRelativeToSelf(); 169 break; 170 default: 171 MOZ_ASSERT(false, "unknown box type"); 172 return r; 173 } 174 175 return r; 176 } 177 178 class MOZ_STACK_CLASS AccumulateQuadCallback final 179 : public nsLayoutUtils::BoxCallback { 180 public: 181 AccumulateQuadCallback(Document* aParentObject, 182 nsTArray<RefPtr<DOMQuad>>& aResult, 183 nsIFrame* aRelativeToFrame, 184 const nsPoint& aRelativeToBoxTopLeft, 185 const BoxQuadOptions& aOptions) 186 : mParentObject(ToSupports(aParentObject)), 187 mResult(aResult), 188 mRelativeToFrame(aRelativeToFrame), 189 mRelativeToBoxTopLeft(aRelativeToBoxTopLeft), 190 mOptions(aOptions) { 191 if (mOptions.mBox == CSSBoxType::Margin) { 192 // Don't include the caption margin when computing margins for a 193 // table 194 mIncludeCaptionBoxForTable = false; 195 } 196 } 197 198 void AddBox(nsIFrame* aFrame) override { 199 nsIFrame* f = aFrame; 200 if (mOptions.mBox == CSSBoxType::Margin && f->IsTableFrame()) { 201 // Margin boxes for table frames should be taken from the table wrapper 202 // frame, since that has the margin. 203 f = f->GetParent(); 204 } 205 const nsRect box = GetBoxRectForFrame(&f, mOptions.mBox); 206 CSSPoint points[4] = {CSSPoint::FromAppUnits(box.TopLeft()), 207 CSSPoint::FromAppUnits(box.TopRight()), 208 CSSPoint::FromAppUnits(box.BottomRight()), 209 CSSPoint::FromAppUnits(box.BottomLeft())}; 210 const auto delta = [&]() -> Maybe<CSSPoint> { 211 if (mOptions.mIgnoreTransforms) { 212 return Some(CSSPoint::FromAppUnits(f->GetOffsetTo(mRelativeToFrame) - 213 mRelativeToBoxTopLeft)); 214 } 215 nsLayoutUtils::TransformResult rv = nsLayoutUtils::TransformPoints( 216 RelativeTo{f}, RelativeTo{mRelativeToFrame}, 4, points); 217 if (rv != nsLayoutUtils::TRANSFORM_SUCCEEDED) { 218 return Nothing(); 219 } 220 return Some(CSSPoint::FromAppUnits(-mRelativeToBoxTopLeft)); 221 }(); 222 if (delta) { 223 for (auto& point : points) { 224 point += *delta; 225 } 226 } else { 227 PodArrayZero(points); 228 } 229 mResult.AppendElement(new DOMQuad(mParentObject, points)); 230 } 231 232 nsISupports* const mParentObject; 233 nsTArray<RefPtr<DOMQuad>>& mResult; 234 nsIFrame* const mRelativeToFrame; 235 const nsPoint mRelativeToBoxTopLeft; 236 const BoxQuadOptions& mOptions; 237 }; 238 239 static nsPresContext* FindTopLevelPresContext(nsPresContext* aPC) { 240 bool isChrome = aPC->IsChrome(); 241 nsPresContext* pc = aPC; 242 for (;;) { 243 nsPresContext* parent = pc->GetParentPresContext(); 244 if (!parent || parent->IsChrome() != isChrome) { 245 return pc; 246 } 247 pc = parent; 248 } 249 } 250 251 static bool CheckFramesInSameTopLevelBrowsingContext(nsIFrame* aFrame1, 252 nsIFrame* aFrame2, 253 CallerType aCallerType) { 254 nsPresContext* pc1 = aFrame1->PresContext(); 255 nsPresContext* pc2 = aFrame2->PresContext(); 256 if (pc1 == pc2) { 257 return true; 258 } 259 if (aCallerType == CallerType::System) { 260 return true; 261 } 262 if (FindTopLevelPresContext(pc1) == FindTopLevelPresContext(pc2)) { 263 return true; 264 } 265 return false; 266 } 267 268 void GetBoxQuads(nsINode* aNode, const dom::BoxQuadOptions& aOptions, 269 nsTArray<RefPtr<DOMQuad>>& aResult, CallerType aCallerType, 270 ErrorResult& aRv) { 271 nsIFrame* frame = GetFrameForNode(aNode, aOptions); 272 if (!frame) { 273 // No boxes to return 274 return; 275 } 276 AutoWeakFrame weakFrame(frame); 277 Document* ownerDoc = aNode->OwnerDoc(); 278 nsIFrame* relativeToFrame = GetFirstNonAnonymousFrameForGeometryNode( 279 aOptions.mRelativeTo, ownerDoc, aOptions); 280 // The first frame might be destroyed now if the above call lead to an 281 // EnsureFrameForTextNode call. We need to get the first frame again 282 // when that happens and re-check it. 283 if (!weakFrame.IsAlive()) { 284 frame = GetFrameForNode(aNode, aOptions); 285 if (!frame) { 286 // No boxes to return 287 return; 288 } 289 } 290 if (!relativeToFrame) { 291 // XXXbz There's no spec for this. 292 return aRv.ThrowNotFoundError("No box to get quads relative to"); 293 } 294 if (!CheckFramesInSameTopLevelBrowsingContext(frame, relativeToFrame, 295 aCallerType)) { 296 aRv.ThrowNotFoundError( 297 "Can't get quads relative to a box in a different toplevel browsing " 298 "context"); 299 return; 300 } 301 // GetBoxRectForFrame can modify relativeToFrame so call it first. 302 nsPoint relativeToTopLeft = 303 GetBoxRectForFrame(&relativeToFrame, CSSBoxType::Border).TopLeft(); 304 AccumulateQuadCallback callback(ownerDoc, aResult, relativeToFrame, 305 relativeToTopLeft, aOptions); 306 307 // Bug 1624653: Refactor this to get boxes in layer pixels, which we will 308 // then convert into CSS units. 309 nsLayoutUtils::GetAllInFlowBoxes(frame, &callback); 310 } 311 312 void GetBoxQuadsFromWindowOrigin(nsINode* aNode, 313 const dom::BoxQuadOptions& aOptions, 314 nsTArray<RefPtr<DOMQuad>>& aResult, 315 ErrorResult& aRv) { 316 // We want the quads relative to the window. To do this, we ignore the 317 // provided aOptions.mRelativeTo and instead use the document node of 318 // the top-most in-process document. Later, we'll check if there is a 319 // browserChild associated with that document, and if so, transform the 320 // calculated quads with the browserChild's to-parent matrix, which 321 // will get us to top-level coordinates. 322 if (aOptions.mRelativeTo.WasPassed()) { 323 return aRv.ThrowNotSupportedError( 324 "Can't request quads in window origin space relative to another " 325 "node."); 326 } 327 328 // We're going to call GetBoxQuads with our parameters, but we supply 329 // a new BoxQuadOptions object that uses the top in-process document 330 // as the relativeTo target. 331 BoxQuadOptions bqo(aOptions); 332 333 RefPtr<Document> topInProcessDoc = 334 nsContentUtils::GetInProcessSubtreeRootDocument(aNode->OwnerDoc()); 335 336 OwningGeometryNode ogn; 337 ogn.SetAsDocument() = topInProcessDoc; 338 bqo.mRelativeTo.Construct(ogn); 339 340 // Bug 1624653: Refactor this to get boxes in layer pixels, which we can 341 // transform directly with the GetChildToParentConversionMatrix below, 342 // and convert to CSS units as a final step. 343 GetBoxQuads(aNode, bqo, aResult, CallerType::System, aRv); 344 if (aRv.Failed()) { 345 return; 346 } 347 348 // Now we have aResult filled with DOMQuads with values relative to the 349 // top in-process document. See if topInProcessDoc is associated with a 350 // BrowserChild, and if it is, get its transformation matrix and use that 351 // to transform the DOMQuads in place to make them relative to the window 352 // origin. 353 nsIDocShell* docShell = topInProcessDoc->GetDocShell(); 354 if (!docShell) { 355 return aRv.ThrowInvalidStateError( 356 "Returning untranslated quads because top in process document has " 357 "no docshell."); 358 } 359 360 BrowserChild* browserChild = BrowserChild::GetFrom(docShell); 361 if (!browserChild) { 362 return; 363 } 364 365 nsPresContext* presContext = docShell->GetPresContext(); 366 if (!presContext) { 367 return; 368 } 369 int32_t appUnitsPerDevPixel = presContext->AppUnitsPerDevPixel(); 370 371 LayoutDeviceToLayoutDeviceMatrix4x4 matrix = 372 browserChild->GetChildToParentConversionMatrix(); 373 374 // For each DOMQuad in aResult, change the css units into layer pixels, 375 // then transform them by matrix, then change them back into css units 376 // and overwrite the original points. 377 LayoutDeviceToCSSScale ld2cScale((float)appUnitsPerDevPixel / 378 (float)AppUnitsPerCSSPixel()); 379 CSSToLayoutDeviceScale c2ldScale = ld2cScale.Inverse(); 380 381 for (auto& quad : aResult) { 382 for (uint32_t i = 0; i < 4; i++) { 383 DOMPoint* p = quad->Point(i); 384 CSSPoint cp(p->X(), p->Y()); 385 386 LayoutDevicePoint windowLdp = matrix.TransformPoint(cp * c2ldScale); 387 388 CSSPoint windowCp = windowLdp * ld2cScale; 389 p->SetX(windowCp.x); 390 p->SetY(windowCp.y); 391 } 392 } 393 } 394 395 static void TransformPoints(nsINode* aTo, const GeometryNode& aFrom, 396 uint32_t aPointCount, CSSPoint* aPoints, 397 const ConvertCoordinateOptions& aOptions, 398 CallerType aCallerType, ErrorResult& aRv) { 399 nsIFrame* fromFrame = 400 GetFirstNonAnonymousFrameForGeometryNode(aFrom, aOptions); 401 AutoWeakFrame weakFrame(fromFrame); 402 nsIFrame* toFrame = GetFirstNonAnonymousFrameForNode(aTo, aOptions); 403 // The first frame might be destroyed now if the above call lead to an 404 // EnsureFrameForTextNode call. We need to get the first frame again 405 // when that happens. 406 if (fromFrame && !weakFrame.IsAlive()) { 407 fromFrame = GetFirstNonAnonymousFrameForGeometryNode(aFrom, aOptions); 408 } 409 if (!fromFrame || !toFrame) { 410 aRv.ThrowNotFoundError( 411 "Can't transform coordinates between nonexistent boxes"); 412 return; 413 } 414 if (!CheckFramesInSameTopLevelBrowsingContext(fromFrame, toFrame, 415 aCallerType)) { 416 aRv.ThrowNotFoundError( 417 "Can't transform coordinates between boxes in different toplevel " 418 "browsing contexts"); 419 return; 420 } 421 422 nsPoint fromOffset = 423 GetBoxRectForFrame(&fromFrame, aOptions.mFromBox).TopLeft(); 424 nsPoint toOffset = GetBoxRectForFrame(&toFrame, aOptions.mToBox).TopLeft(); 425 CSSPoint fromOffsetGfx(nsPresContext::AppUnitsToFloatCSSPixels(fromOffset.x), 426 nsPresContext::AppUnitsToFloatCSSPixels(fromOffset.y)); 427 for (uint32_t i = 0; i < aPointCount; ++i) { 428 aPoints[i] += fromOffsetGfx; 429 } 430 nsLayoutUtils::TransformResult rv = nsLayoutUtils::TransformPoints( 431 RelativeTo{fromFrame}, RelativeTo{toFrame}, aPointCount, aPoints); 432 if (rv == nsLayoutUtils::TRANSFORM_SUCCEEDED) { 433 CSSPoint toOffsetGfx(nsPresContext::AppUnitsToFloatCSSPixels(toOffset.x), 434 nsPresContext::AppUnitsToFloatCSSPixels(toOffset.y)); 435 for (uint32_t i = 0; i < aPointCount; ++i) { 436 aPoints[i] -= toOffsetGfx; 437 } 438 } else { 439 PodZero(aPoints, aPointCount); 440 } 441 } 442 443 already_AddRefed<DOMQuad> ConvertQuadFromNode( 444 nsINode* aTo, dom::DOMQuad& aQuad, const GeometryNode& aFrom, 445 const dom::ConvertCoordinateOptions& aOptions, CallerType aCallerType, 446 ErrorResult& aRv) { 447 CSSPoint points[4]; 448 for (uint32_t i = 0; i < 4; ++i) { 449 DOMPoint* p = aQuad.Point(i); 450 if (p->W() != 1.0 || p->Z() != 0.0) { 451 aRv.ThrowInvalidStateError("Point is not 2d"); 452 return nullptr; 453 } 454 points[i] = CSSPoint(p->X(), p->Y()); 455 } 456 TransformPoints(aTo, aFrom, 4, points, aOptions, aCallerType, aRv); 457 if (aRv.Failed()) { 458 return nullptr; 459 } 460 return MakeAndAddRef<DOMQuad>(aTo->GetParentObject().mObject, points); 461 } 462 463 already_AddRefed<DOMQuad> ConvertRectFromNode( 464 nsINode* aTo, dom::DOMRectReadOnly& aRect, const GeometryNode& aFrom, 465 const dom::ConvertCoordinateOptions& aOptions, CallerType aCallerType, 466 ErrorResult& aRv) { 467 CSSPoint points[4]; 468 double x = aRect.X(), y = aRect.Y(), w = aRect.Width(), h = aRect.Height(); 469 points[0] = CSSPoint(x, y); 470 points[1] = CSSPoint(x + w, y); 471 points[2] = CSSPoint(x + w, y + h); 472 points[3] = CSSPoint(x, y + h); 473 TransformPoints(aTo, aFrom, 4, points, aOptions, aCallerType, aRv); 474 if (aRv.Failed()) { 475 return nullptr; 476 } 477 return MakeAndAddRef<DOMQuad>(aTo->GetParentObject().mObject, points); 478 } 479 480 already_AddRefed<DOMPoint> ConvertPointFromNode( 481 nsINode* aTo, const dom::DOMPointInit& aPoint, const GeometryNode& aFrom, 482 const dom::ConvertCoordinateOptions& aOptions, CallerType aCallerType, 483 ErrorResult& aRv) { 484 if (aPoint.mW != 1.0 || aPoint.mZ != 0.0) { 485 aRv.ThrowInvalidStateError("Point is not 2d"); 486 return nullptr; 487 } 488 CSSPoint point(aPoint.mX, aPoint.mY); 489 TransformPoints(aTo, aFrom, 1, &point, aOptions, aCallerType, aRv); 490 if (aRv.Failed()) { 491 return nullptr; 492 } 493 return MakeAndAddRef<DOMPoint>(aTo->GetParentObject().mObject, point.x, 494 point.y); 495 } 496 497 } // namespace mozilla