Bug 1746794 - Make more nsLayoutUtils functions SVG-text aware. r=layout-reviewers,jfkthame

This makes TransformPoint and TransformRect deal with SVG text
correctly. We need TransformPoint support so that GeometryUtils works as
expected, since we need to transform the points individually so that
stuff like transforms report the expected result (a rect doesn't cut it
for those).

Differential Revision: https://phabricator.services.mozilla.com/D135152
This commit is contained in:
Emilio Cobos Álvarez 2022-01-28 16:50:14 +00:00
Родитель e5314ec7e8
Коммит 4eea923e01
6 изменённых файлов: 219 добавлений и 179 удалений

Просмотреть файл

@ -191,7 +191,7 @@ class AccumulateQuadCallback : public nsLayoutUtils::BoxCallback {
}
}
virtual void AddBox(nsIFrame* aFrame) override {
void AddBox(nsIFrame* aFrame) override {
nsIFrame* f = aFrame;
if (mBoxType == CSSBoxType::Margin && f->IsTableFrame()) {
// Margin boxes for table frames should be taken from the table wrapper
@ -207,8 +207,8 @@ class AccumulateQuadCallback : public nsLayoutUtils::BoxCallback {
CSSPoint(nsPresContext::AppUnitsToFloatCSSPixels(appUnits[i].x),
nsPresContext::AppUnitsToFloatCSSPixels(appUnits[i].y));
}
nsLayoutUtils::TransformResult rv =
nsLayoutUtils::TransformPoints(f, mRelativeToFrame, 4, points);
nsLayoutUtils::TransformResult rv = nsLayoutUtils::TransformPoints(
RelativeTo{f}, RelativeTo{mRelativeToFrame}, 4, points);
if (rv == nsLayoutUtils::TRANSFORM_SUCCEEDED) {
CSSPoint delta(
nsPresContext::AppUnitsToFloatCSSPixels(mRelativeToBoxTopLeft.x),
@ -422,8 +422,8 @@ static void TransformPoints(nsINode* aTo, const GeometryNode& aFrom,
for (uint32_t i = 0; i < aPointCount; ++i) {
aPoints[i] += fromOffsetGfx;
}
nsLayoutUtils::TransformResult rv =
nsLayoutUtils::TransformPoints(fromFrame, toFrame, aPointCount, aPoints);
nsLayoutUtils::TransformResult rv = nsLayoutUtils::TransformPoints(
RelativeTo{fromFrame}, RelativeTo{toFrame}, aPointCount, aPoints);
if (rv == nsLayoutUtils::TRANSFORM_SUCCEEDED) {
CSSPoint toOffsetGfx(nsPresContext::AppUnitsToFloatCSSPixels(toOffset.x),
nsPresContext::AppUnitsToFloatCSSPixels(toOffset.y));

Просмотреть файл

@ -2221,46 +2221,120 @@ bool nsLayoutUtils::AuthorSpecifiedBorderBackgroundDisablesTheming(
aAppearance == StyleAppearance::MenulistButton;
}
nsLayoutUtils::TransformResult nsLayoutUtils::TransformPoints(
nsIFrame* aFromFrame, nsIFrame* aToFrame, uint32_t aPointCount,
CSSPoint* aPoints) {
const nsIFrame* nearestCommonAncestor =
FindNearestCommonAncestorFrame(aFromFrame, aToFrame);
if (!nearestCommonAncestor) {
return NO_COMMON_ANCESTOR;
static SVGTextFrame* GetContainingSVGTextFrame(const nsIFrame* aFrame) {
if (!SVGUtils::IsInSVGTextSubtree(aFrame)) {
return nullptr;
}
Matrix4x4Flagged downToDest = GetTransformToAncestor(
RelativeTo{aToFrame}, RelativeTo{nearestCommonAncestor});
if (downToDest.IsSingular()) {
return NONINVERTIBLE_TRANSFORM;
}
downToDest.Invert();
Matrix4x4Flagged upToAncestor = GetTransformToAncestor(
RelativeTo{aFromFrame}, RelativeTo{nearestCommonAncestor});
CSSToLayoutDeviceScale devPixelsPerCSSPixelFromFrame =
aFromFrame->PresContext()->CSSToDevPixelScale();
CSSToLayoutDeviceScale devPixelsPerCSSPixelToFrame =
aToFrame->PresContext()->CSSToDevPixelScale();
for (uint32_t i = 0; i < aPointCount; ++i) {
LayoutDevicePoint devPixels = aPoints[i] * devPixelsPerCSSPixelFromFrame;
// What should the behaviour be if some of the points aren't invertible
// and others are? Just assume all points are for now.
Point toDevPixels =
downToDest
.ProjectPoint(
(upToAncestor.TransformPoint(Point(devPixels.x, devPixels.y))))
.As2DPoint();
// Divide here so that when the devPixelsPerCSSPixels are the same, we get
// the correct answer instead of some inaccuracy multiplying a number by its
// reciprocal.
aPoints[i] = LayoutDevicePoint(toDevPixels.x, toDevPixels.y) /
devPixelsPerCSSPixelToFrame;
}
return TRANSFORM_SUCCEEDED;
return static_cast<SVGTextFrame*>(nsLayoutUtils::GetClosestFrameOfType(
aFrame->GetParent(), LayoutFrameType::SVGText));
}
nsLayoutUtils::TransformResult nsLayoutUtils::TransformPoint(
RelativeTo aFromFrame, RelativeTo aToFrame, nsPoint& aPoint) {
static bool TransformGfxPointFromAncestor(RelativeTo aFrame,
const Point& aPoint,
RelativeTo aAncestor,
Maybe<Matrix4x4Flagged>& aMatrixCache,
Point* aOut) {
SVGTextFrame* text = GetContainingSVGTextFrame(aFrame.mFrame);
if (!aMatrixCache) {
auto matrix = nsLayoutUtils::GetTransformToAncestor(
RelativeTo{text ? text : aFrame.mFrame, aFrame.mViewportType},
aAncestor);
if (matrix.IsSingular()) {
return false;
}
matrix.Invert();
aMatrixCache.emplace(matrix);
}
const Matrix4x4Flagged& ctm = *aMatrixCache;
Point4D point = ctm.ProjectPoint(aPoint);
if (!point.HasPositiveWCoord()) {
return false;
}
*aOut = point.As2DPoint();
if (text) {
*aOut = text->TransformFramePointToTextChild(*aOut, aFrame.mFrame);
}
return true;
}
static Point TransformGfxPointToAncestor(
RelativeTo aFrame, const Point& aPoint, RelativeTo aAncestor,
Maybe<Matrix4x4Flagged>& aMatrixCache) {
if (SVGTextFrame* text = GetContainingSVGTextFrame(aFrame.mFrame)) {
Point result =
text->TransformFramePointFromTextChild(aPoint, aFrame.mFrame);
return TransformGfxPointToAncestor(RelativeTo{text}, result, aAncestor,
aMatrixCache);
}
if (!aMatrixCache) {
aMatrixCache.emplace(
nsLayoutUtils::GetTransformToAncestor(aFrame, aAncestor));
}
return aMatrixCache->ProjectPoint(aPoint).As2DPoint();
}
static Rect TransformGfxRectToAncestor(
RelativeTo aFrame, const Rect& aRect, RelativeTo aAncestor,
bool* aPreservesAxisAlignedRectangles = nullptr,
Maybe<Matrix4x4Flagged>* aMatrixCache = nullptr,
bool aStopAtStackingContextAndDisplayPortAndOOFFrame = false,
nsIFrame** aOutAncestor = nullptr) {
Rect result;
Matrix4x4Flagged ctm;
if (SVGTextFrame* text = GetContainingSVGTextFrame(aFrame.mFrame)) {
result = text->TransformFrameRectFromTextChild(aRect, aFrame.mFrame);
result = TransformGfxRectToAncestor(
RelativeTo{text}, result, aAncestor, nullptr, aMatrixCache,
aStopAtStackingContextAndDisplayPortAndOOFFrame, aOutAncestor);
if (aPreservesAxisAlignedRectangles) {
// TransformFrameRectFromTextChild could involve any kind of transform, we
// could drill down into it to get an answer out of it but we don't yet.
*aPreservesAxisAlignedRectangles = false;
}
return result;
}
if (aMatrixCache && *aMatrixCache) {
// We are given a matrix to use, so use it
ctm = aMatrixCache->value();
} else {
// Else, compute it
uint32_t flags = 0;
if (aStopAtStackingContextAndDisplayPortAndOOFFrame) {
flags |= nsIFrame::STOP_AT_STACKING_CONTEXT_AND_DISPLAY_PORT;
}
ctm = nsLayoutUtils::GetTransformToAncestor(aFrame, aAncestor, flags,
aOutAncestor);
if (aMatrixCache) {
// and put it in the cache, if provided
*aMatrixCache = Some(ctm);
}
}
// Fill out the axis-alignment flag
if (aPreservesAxisAlignedRectangles) {
// TransformFrameRectFromTextChild could involve any kind of transform, we
// could drill down into it to get an answer out of it but we don't yet.
Matrix matrix2d;
*aPreservesAxisAlignedRectangles =
ctm.Is2D(&matrix2d) && matrix2d.PreservesAxisAlignedRectangles();
}
const nsIFrame* ancestor = aOutAncestor ? *aOutAncestor : aAncestor.mFrame;
float factor = ancestor->PresContext()->AppUnitsPerDevPixel();
Rect maxBounds =
Rect(float(nscoord_MIN) / factor * 0.5, float(nscoord_MIN) / factor * 0.5,
float(nscoord_MAX) / factor, float(nscoord_MAX) / factor);
return ctm.TransformAndClipBounds(aRect, maxBounds);
}
nsLayoutUtils::TransformResult nsLayoutUtils::TransformPoints(
RelativeTo aFromFrame, RelativeTo aToFrame, uint32_t aPointCount,
CSSPoint* aPoints) {
// Conceptually, {ViewportFrame, Visual} is an ancestor of
// {ViewportFrame, Layout}, so factor that into the nearest ancestor
// computation.
@ -2273,32 +2347,43 @@ nsLayoutUtils::TransformResult nsLayoutUtils::TransformPoint(
if (!nearestCommonAncestor.mFrame) {
return NO_COMMON_ANCESTOR;
}
Matrix4x4Flagged downToDest =
GetTransformToAncestor(aToFrame, nearestCommonAncestor);
if (downToDest.IsSingular()) {
return NONINVERTIBLE_TRANSFORM;
CSSToLayoutDeviceScale devPixelsPerCSSPixelFromFrame =
aFromFrame.mFrame->PresContext()->CSSToDevPixelScale();
CSSToLayoutDeviceScale devPixelsPerCSSPixelToFrame =
aToFrame.mFrame->PresContext()->CSSToDevPixelScale();
Maybe<Matrix4x4Flagged> cacheTo;
Maybe<Matrix4x4Flagged> cacheFrom;
for (uint32_t i = 0; i < aPointCount; ++i) {
LayoutDevicePoint devPixels = aPoints[i] * devPixelsPerCSSPixelFromFrame;
// What should the behaviour be if some of the points aren't invertible
// and others are? Just assume all points are for now.
Point toDevPixels =
TransformGfxPointToAncestor(aFromFrame, Point(devPixels.x, devPixels.y),
nearestCommonAncestor, cacheTo);
Point result;
if (!TransformGfxPointFromAncestor(
aToFrame, toDevPixels, nearestCommonAncestor, cacheFrom, &result)) {
return NONINVERTIBLE_TRANSFORM;
}
// Divide here so that when the devPixelsPerCSSPixels are the same, we get
// the correct answer instead of some inaccuracy multiplying a number by its
// reciprocal.
aPoints[i] =
LayoutDevicePoint(result.x, result.y) / devPixelsPerCSSPixelToFrame;
}
downToDest.Invert();
Matrix4x4Flagged upToAncestor =
GetTransformToAncestor(aFromFrame, nearestCommonAncestor);
float devPixelsPerAppUnitFromFrame =
1.0f / aFromFrame.mFrame->PresContext()->AppUnitsPerDevPixel();
float devPixelsPerAppUnitToFrame =
1.0f / aToFrame.mFrame->PresContext()->AppUnitsPerDevPixel();
Point4D toDevPixels = downToDest.ProjectPoint(upToAncestor.TransformPoint(
Point(aPoint.x * devPixelsPerAppUnitFromFrame,
aPoint.y * devPixelsPerAppUnitFromFrame)));
if (!toDevPixels.HasPositiveWCoord()) {
// Not strictly true, but we failed to get a valid point in this
// coordinate space.
return NONINVERTIBLE_TRANSFORM;
}
aPoint.x = NSToCoordRound(toDevPixels.x / devPixelsPerAppUnitToFrame);
aPoint.y = NSToCoordRound(toDevPixels.y / devPixelsPerAppUnitToFrame);
return TRANSFORM_SUCCEEDED;
}
nsLayoutUtils::TransformResult nsLayoutUtils::TransformPoint(
RelativeTo aFromFrame, RelativeTo aToFrame, nsPoint& aPoint) {
CSSPoint point = CSSPoint::FromAppUnits(aPoint);
auto result = TransformPoints(aFromFrame, aToFrame, 1, &point);
if (result == TRANSFORM_SUCCEEDED) {
aPoint = CSSPoint::ToAppUnits(point);
}
return result;
}
nsLayoutUtils::TransformResult nsLayoutUtils::TransformRect(
const nsIFrame* aFromFrame, const nsIFrame* aToFrame, nsRect& aRect) {
const nsIFrame* nearestCommonAncestor =
@ -2312,23 +2397,18 @@ nsLayoutUtils::TransformResult nsLayoutUtils::TransformRect(
return NONINVERTIBLE_TRANSFORM;
}
downToDest.Invert();
Matrix4x4Flagged upToAncestor = GetTransformToAncestor(
RelativeTo{aFromFrame}, RelativeTo{nearestCommonAncestor});
aRect = TransformFrameRectToAncestor(aFromFrame, aRect,
RelativeTo{nearestCommonAncestor});
float devPixelsPerAppUnitFromFrame =
1.0f / aFromFrame->PresContext()->AppUnitsPerDevPixel();
1.0f / nearestCommonAncestor->PresContext()->AppUnitsPerDevPixel();
float devPixelsPerAppUnitToFrame =
1.0f / aToFrame->PresContext()->AppUnitsPerDevPixel();
gfx::Rect toDevPixels = downToDest.ProjectRectBounds(
upToAncestor.ProjectRectBounds(
gfx::Rect(aRect.x * devPixelsPerAppUnitFromFrame,
aRect.y * devPixelsPerAppUnitFromFrame,
aRect.width * devPixelsPerAppUnitFromFrame,
aRect.height * devPixelsPerAppUnitFromFrame),
Rect(-std::numeric_limits<Float>::max() * 0.5f,
-std::numeric_limits<Float>::max() * 0.5f,
std::numeric_limits<Float>::max(),
std::numeric_limits<Float>::max())),
gfx::Rect(aRect.x * devPixelsPerAppUnitFromFrame,
aRect.y * devPixelsPerAppUnitFromFrame,
aRect.width * devPixelsPerAppUnitFromFrame,
aRect.height * devPixelsPerAppUnitFromFrame),
Rect(-std::numeric_limits<Float>::max() * devPixelsPerAppUnitFromFrame *
0.5f,
-std::numeric_limits<Float>::max() * devPixelsPerAppUnitFromFrame *
@ -2436,85 +2516,19 @@ bool nsLayoutUtils::GetLayerTransformForFrame(nsIFrame* aFrame,
return true;
}
static bool TransformGfxPointFromAncestor(RelativeTo aFrame,
const Point& aPoint,
RelativeTo aAncestor, Point* aOut) {
Matrix4x4Flagged ctm =
nsLayoutUtils::GetTransformToAncestor(aFrame, aAncestor);
ctm.Invert();
Point4D point = ctm.ProjectPoint(aPoint);
if (!point.HasPositiveWCoord()) {
return false;
}
*aOut = point.As2DPoint();
return true;
}
static Rect TransformGfxRectToAncestor(
RelativeTo aFrame, const Rect& aRect, RelativeTo aAncestor,
bool* aPreservesAxisAlignedRectangles = nullptr,
Maybe<Matrix4x4Flagged>* aMatrixCache = nullptr,
bool aStopAtStackingContextAndDisplayPortAndOOFFrame = false,
nsIFrame** aOutAncestor = nullptr) {
Matrix4x4Flagged ctm;
if (aMatrixCache && *aMatrixCache) {
// We are given a matrix to use, so use it
ctm = aMatrixCache->value();
} else {
// Else, compute it
uint32_t flags = 0;
if (aStopAtStackingContextAndDisplayPortAndOOFFrame) {
flags |= nsIFrame::STOP_AT_STACKING_CONTEXT_AND_DISPLAY_PORT;
}
ctm = nsLayoutUtils::GetTransformToAncestor(aFrame, aAncestor, flags,
aOutAncestor);
if (aMatrixCache) {
// and put it in the cache, if provided
*aMatrixCache = Some(ctm);
}
}
// Fill out the axis-alignment flag
if (aPreservesAxisAlignedRectangles) {
Matrix matrix2d;
*aPreservesAxisAlignedRectangles =
ctm.Is2D(&matrix2d) && matrix2d.PreservesAxisAlignedRectangles();
}
const nsIFrame* ancestor = aOutAncestor ? *aOutAncestor : aAncestor.mFrame;
float factor = ancestor->PresContext()->AppUnitsPerDevPixel();
Rect maxBounds =
Rect(float(nscoord_MIN) / factor * 0.5, float(nscoord_MIN) / factor * 0.5,
float(nscoord_MAX) / factor, float(nscoord_MAX) / factor);
return ctm.TransformAndClipBounds(aRect, maxBounds);
}
static SVGTextFrame* GetContainingSVGTextFrame(const nsIFrame* aFrame) {
if (!SVGUtils::IsInSVGTextSubtree(aFrame)) {
return nullptr;
}
return static_cast<SVGTextFrame*>(nsLayoutUtils::GetClosestFrameOfType(
aFrame->GetParent(), LayoutFrameType::SVGText));
}
nsPoint nsLayoutUtils::TransformAncestorPointToFrame(RelativeTo aFrame,
const nsPoint& aPoint,
RelativeTo aAncestor) {
SVGTextFrame* text = GetContainingSVGTextFrame(aFrame.mFrame);
float factor = aFrame.mFrame->PresContext()->AppUnitsPerDevPixel();
Point result(NSAppUnitsToFloatPixels(aPoint.x, factor),
NSAppUnitsToFloatPixels(aPoint.y, factor));
if (!TransformGfxPointFromAncestor(
text ? RelativeTo{text, aFrame.mViewportType} : aFrame, result,
aAncestor, &result)) {
Maybe<Matrix4x4Flagged> matrixCache;
if (!TransformGfxPointFromAncestor(aFrame, result, aAncestor, matrixCache,
&result)) {
return nsPoint(NS_UNCONSTRAINEDSIZE, NS_UNCONSTRAINEDSIZE);
}
if (text) {
result = text->TransformFramePointToTextChild(result, aFrame.mFrame);
}
return nsPoint(NSFloatPixelsToAppUnits(float(result.x), factor),
NSFloatPixelsToAppUnits(float(result.y), factor));
}
@ -2527,39 +2541,15 @@ nsRect nsLayoutUtils::TransformFrameRectToAncestor(
nsIFrame** aOutAncestor /* = nullptr */) {
MOZ_ASSERT(IsAncestorFrameCrossDocInProcess(aAncestor.mFrame, aFrame),
"Fix the caller");
SVGTextFrame* text = GetContainingSVGTextFrame(aFrame);
float srcAppUnitsPerDevPixel = aFrame->PresContext()->AppUnitsPerDevPixel();
Rect result;
if (text) {
result = ToRect(text->TransformFrameRectFromTextChild(aRect, aFrame));
// |result| from TransformFrameRectFromTextChild() is in user space (css
// pixel), should convert to device pixel
float devPixelPerCSSPixel =
float(AppUnitsPerCSSPixel()) / srcAppUnitsPerDevPixel;
result.Scale(devPixelPerCSSPixel);
result = TransformGfxRectToAncestor(
RelativeTo{text}, result, aAncestor, nullptr, aMatrixCache,
aStopAtStackingContextAndDisplayPortAndOOFFrame, aOutAncestor);
// TransformFrameRectFromTextChild could involve any kind of transform, we
// could drill down into it to get an answer out of it but we don't yet.
if (aPreservesAxisAlignedRectangles)
*aPreservesAxisAlignedRectangles = false;
} else {
result =
Rect(NSAppUnitsToFloatPixels(aRect.x, srcAppUnitsPerDevPixel),
NSAppUnitsToFloatPixels(aRect.y, srcAppUnitsPerDevPixel),
NSAppUnitsToFloatPixels(aRect.width, srcAppUnitsPerDevPixel),
NSAppUnitsToFloatPixels(aRect.height, srcAppUnitsPerDevPixel));
result = TransformGfxRectToAncestor(
RelativeTo{aFrame}, result, aAncestor, aPreservesAxisAlignedRectangles,
aMatrixCache, aStopAtStackingContextAndDisplayPortAndOOFFrame,
aOutAncestor);
}
Rect result(NSAppUnitsToFloatPixels(aRect.x, srcAppUnitsPerDevPixel),
NSAppUnitsToFloatPixels(aRect.y, srcAppUnitsPerDevPixel),
NSAppUnitsToFloatPixels(aRect.width, srcAppUnitsPerDevPixel),
NSAppUnitsToFloatPixels(aRect.height, srcAppUnitsPerDevPixel));
result = TransformGfxRectToAncestor(
RelativeTo{aFrame}, result, aAncestor, aPreservesAxisAlignedRectangles,
aMatrixCache, aStopAtStackingContextAndDisplayPortAndOOFFrame,
aOutAncestor);
float destAppUnitsPerDevPixel =
aAncestor.mFrame->PresContext()->AppUnitsPerDevPixel();

Просмотреть файл

@ -994,8 +994,8 @@ class nsLayoutUtils {
NO_COMMON_ANCESTOR,
NONINVERTIBLE_TRANSFORM
};
static TransformResult TransformPoints(nsIFrame* aFromFrame,
nsIFrame* aToFrame,
static TransformResult TransformPoints(RelativeTo aFromFrame,
RelativeTo aToFrame,
uint32_t aPointCount,
CSSPoint* aPoints);

Просмотреть файл

@ -5362,6 +5362,24 @@ gfxRect SVGTextFrame::TransformFrameRectFromTextChild(
return result - framePosition;
}
Rect SVGTextFrame::TransformFrameRectFromTextChild(
const Rect& aRect, const nsIFrame* aChildFrame) {
nscoord appUnitsPerDevPixel = PresContext()->AppUnitsPerDevPixel();
nsRect r = LayoutDevicePixel::ToAppUnits(
LayoutDeviceRect::FromUnknownRect(aRect), appUnitsPerDevPixel);
gfxRect resultCssUnits = TransformFrameRectFromTextChild(r, aChildFrame);
float devPixelPerCSSPixel =
float(AppUnitsPerCSSPixel()) / appUnitsPerDevPixel;
resultCssUnits.Scale(devPixelPerCSSPixel);
return ToRect(resultCssUnits);
}
Point SVGTextFrame::TransformFramePointFromTextChild(
const Point& aPoint, const nsIFrame* aChildFrame) {
return TransformFrameRectFromTextChild(Rect(aPoint, Size(1, 1)), aChildFrame)
.TopLeft();
}
void SVGTextFrame::AppendDirectlyOwnedAnonBoxes(
nsTArray<OwnedAnonBox>& aResult) {
MOZ_ASSERT(PrincipalChildList().FirstChild(), "Must have our anon box");

Просмотреть файл

@ -179,6 +179,7 @@ class SVGTextFrame final : public SVGDisplayContainerFrame {
using DrawTarget = gfx::DrawTarget;
using Path = gfx::Path;
using Point = gfx::Point;
using Rect = gfx::Rect;
protected:
explicit SVGTextFrame(ComputedStyle* aStyle, nsPresContext* aPresContext)
@ -330,6 +331,14 @@ class SVGTextFrame final : public SVGDisplayContainerFrame {
gfxRect TransformFrameRectFromTextChild(const nsRect& aRect,
const nsIFrame* aChildFrame);
/** As above, but taking and returning a device px rect. */
Rect TransformFrameRectFromTextChild(const Rect& aRect,
const nsIFrame* aChildFrame);
/** As above, but with a single point */
Point TransformFramePointFromTextChild(const Point& aPoint,
const nsIFrame* aChildFrame);
// Return our ::-moz-svg-text anonymous box.
void AppendDirectlyOwnedAnonBoxes(nsTArray<OwnedAnonBox>& aResult) override;

Просмотреть файл

@ -0,0 +1,23 @@
<!DOCTYPE html>
<title>CSSOM View - getBoxQuads() returns consistent box for SVG test</title>
<link rel="help" href="https://drafts.csswg.org/cssom-view/#the-geometryutils-interface">
<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1746794">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<svg width="200" height="200">
<text>
<tspan id="t1" x="50 60 70 80 90 100" y="50">hello1</tspan>
<tspan id="t2" x="50 60 70 80 90 100" y="100">hello2</tspan>
</text>
</svg>
<script>
test(function() {
for (let element of document.querySelectorAll("tspan")) {
let bcr = element.getBoundingClientRect();
let quad = element.getBoxQuads()[0].getBounds();
for (let prop of ["width", "height", "left", "top"]) {
assert_equals(bcr[prop], quad[prop], `${element.id} ${prop}: getBoxQuads should be consistent with getBoundingClientRect`);
}
}
});
</script>