зеркало из https://github.com/mozilla/gecko-dev.git
Bug 878346 - Make transform a mapped attribute for SVG. r=longsonr,firefox-style-system-reviewers,zrhoffman
The tricky bit is rotate() which in SVG means something different if there's an origin (you translate-then-untranslate it). But this seems to work off-hand, and fix the reminder of bug 1906261. Differential Revision: https://phabricator.services.mozilla.com/D215788
This commit is contained in:
Родитель
189251a906
Коммит
4aa1ad81ba
|
@ -1560,8 +1560,7 @@ function testTransformSVG() {
|
|||
animation.effect.getProperties(),
|
||||
[ {
|
||||
property: subtest.property,
|
||||
runningOnCompositor: false,
|
||||
warning: 'CompositorAnimationWarningTransformSVG'
|
||||
runningOnCompositor: true,
|
||||
} ]);
|
||||
svg.removeAttribute('transform');
|
||||
await waitForFrame();
|
||||
|
|
|
@ -98,7 +98,7 @@ class AttrArray {
|
|||
// called when a mapped attribute is changed (regardless of connectedness).
|
||||
bool MarkAsPendingPresAttributeEvaluation() {
|
||||
// It'd be great to be able to assert that mImpl is non-null or we're the
|
||||
// <body> element.
|
||||
// <body> or <svg> elements.
|
||||
if (MOZ_UNLIKELY(!mImpl) && !GrowBy(1)) {
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -248,8 +248,7 @@ nsresult SVGElement::CopyInnerTo(mozilla::dom::Element* aDest) {
|
|||
if (const auto* pathSegList = GetAnimPathSegList()) {
|
||||
*dest->GetAnimPathSegList() = *pathSegList;
|
||||
if (pathSegList->IsAnimating()) {
|
||||
dest->SMILOverrideStyle()->SetSMILValue(nsCSSPropertyID::eCSSProperty_d,
|
||||
*pathSegList);
|
||||
dest->SMILOverrideStyle()->SetSMILValue(eCSSProperty_d, *pathSegList);
|
||||
}
|
||||
}
|
||||
if (const auto* transformList = GetAnimatedTransformList()) {
|
||||
|
@ -1116,6 +1115,93 @@ bool SVGElement::UpdateDeclarationBlockFromPath(
|
|||
return true;
|
||||
}
|
||||
|
||||
template <typename Float>
|
||||
static StyleTransformOperation MatrixToTransformOperation(
|
||||
const gfx::BaseMatrix<Float>& aMatrix) {
|
||||
return StyleTransformOperation::Matrix(StyleGenericMatrix<float>{
|
||||
.a = float(aMatrix._11),
|
||||
.b = float(aMatrix._12),
|
||||
.c = float(aMatrix._21),
|
||||
.d = float(aMatrix._22),
|
||||
.e = float(aMatrix._31),
|
||||
.f = float(aMatrix._32),
|
||||
});
|
||||
}
|
||||
|
||||
static void SVGTransformToCSS(const SVGTransform& aTransform,
|
||||
nsTArray<StyleTransformOperation>& aOut) {
|
||||
switch (aTransform.Type()) {
|
||||
case dom::SVGTransform_Binding::SVG_TRANSFORM_SCALE: {
|
||||
const auto& m = aTransform.GetMatrix();
|
||||
aOut.AppendElement(StyleTransformOperation::Scale(m._11, m._22));
|
||||
return;
|
||||
}
|
||||
case dom::SVGTransform_Binding::SVG_TRANSFORM_TRANSLATE: {
|
||||
auto p = aTransform.GetMatrix().GetTranslation();
|
||||
aOut.AppendElement(StyleTransformOperation::Translate(
|
||||
LengthPercentage::FromPixels(CSSCoord(p.x)),
|
||||
LengthPercentage::FromPixels(CSSCoord(p.y))));
|
||||
return;
|
||||
}
|
||||
case dom::SVGTransform_Binding::SVG_TRANSFORM_ROTATE: {
|
||||
float cx, cy;
|
||||
aTransform.GetRotationOrigin(cx, cy);
|
||||
const StyleAngle angle{aTransform.Angle()};
|
||||
const bool hasOrigin = cx != 0.0f || cy != 0.0f;
|
||||
if (hasOrigin) {
|
||||
aOut.AppendElement(StyleTransformOperation::Translate(
|
||||
LengthPercentage::FromPixels(cx),
|
||||
LengthPercentage::FromPixels(cy)));
|
||||
}
|
||||
aOut.AppendElement(StyleTransformOperation::Rotate(angle));
|
||||
if (hasOrigin) {
|
||||
aOut.AppendElement(StyleTransformOperation::Translate(
|
||||
LengthPercentage::FromPixels(-cx),
|
||||
LengthPercentage::FromPixels(-cy)));
|
||||
}
|
||||
return;
|
||||
}
|
||||
case dom::SVGTransform_Binding::SVG_TRANSFORM_SKEWX:
|
||||
aOut.AppendElement(StyleTransformOperation::SkewX({aTransform.Angle()}));
|
||||
return;
|
||||
case dom::SVGTransform_Binding::SVG_TRANSFORM_SKEWY:
|
||||
aOut.AppendElement(StyleTransformOperation::SkewY({aTransform.Angle()}));
|
||||
return;
|
||||
case dom::SVGTransform_Binding::SVG_TRANSFORM_MATRIX: {
|
||||
aOut.AppendElement(MatrixToTransformOperation(aTransform.GetMatrix()));
|
||||
return;
|
||||
}
|
||||
case dom::SVGTransform_Binding::SVG_TRANSFORM_UNKNOWN:
|
||||
default:
|
||||
MOZ_CRASH("Bad SVGTransform?");
|
||||
}
|
||||
}
|
||||
|
||||
/* static */
|
||||
bool SVGElement::UpdateDeclarationBlockFromTransform(
|
||||
StyleLockedDeclarationBlock& aBlock,
|
||||
const SVGAnimatedTransformList* aTransform,
|
||||
const gfx::Matrix* aAnimateMotionTransform, ValToUse aValToUse) {
|
||||
MOZ_ASSERT(aTransform || aAnimateMotionTransform);
|
||||
AutoTArray<StyleTransformOperation, 5> operations;
|
||||
if (aAnimateMotionTransform) {
|
||||
operations.AppendElement(
|
||||
MatrixToTransformOperation(*aAnimateMotionTransform));
|
||||
}
|
||||
if (aTransform) {
|
||||
const SVGTransformList& transforms = aValToUse == ValToUse::Anim
|
||||
? aTransform->GetAnimValue()
|
||||
: aTransform->GetBaseValue();
|
||||
// TODO: Maybe make SVGTransform use StyleTransformOperation directly?
|
||||
for (size_t i = 0, len = transforms.Length(); i < len; ++i) {
|
||||
SVGTransformToCSS(transforms[i], operations);
|
||||
}
|
||||
}
|
||||
Servo_DeclarationBlock_SetTransform(&aBlock, eCSSProperty_transform,
|
||||
&operations);
|
||||
return true;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------
|
||||
// Helper class: MappedAttrParser, for parsing values of mapped attributes
|
||||
|
||||
|
@ -1142,7 +1228,8 @@ class MOZ_STACK_CLASS MappedAttrParser {
|
|||
|
||||
void TellStyleAlreadyParsedResult(nsAtom const* aAtom,
|
||||
SVGAnimatedLength const& aLength);
|
||||
void TellStyleAlreadyParsedResult(const SVGAnimatedPathSegList& aPath);
|
||||
void TellStyleAlreadyParsedResult(const SVGAnimatedPathSegList&);
|
||||
void TellStyleAlreadyParsedResult(const SVGAnimatedTransformList&);
|
||||
|
||||
// If we've parsed any values for mapped attributes, this method returns the
|
||||
// already_AddRefed declaration block that incorporates the parsed values.
|
||||
|
@ -1228,6 +1315,13 @@ void MappedAttrParser::TellStyleAlreadyParsedResult(
|
|||
SVGElement::ValToUse::Base);
|
||||
}
|
||||
|
||||
void MappedAttrParser::TellStyleAlreadyParsedResult(
|
||||
const SVGAnimatedTransformList& aTransform) {
|
||||
SVGElement::UpdateDeclarationBlockFromTransform(EnsureDeclarationBlock(),
|
||||
&aTransform, nullptr,
|
||||
SVGElement::ValToUse::Base);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
//----------------------------------------------------------------------
|
||||
|
@ -1239,6 +1333,7 @@ void SVGElement::UpdateMappedDeclarationBlock() {
|
|||
|
||||
const bool lengthAffectsStyle =
|
||||
SVGGeometryProperty::ElementMapsLengthsToStyle(this);
|
||||
bool sawTransform = false;
|
||||
|
||||
uint32_t i = 0;
|
||||
while (BorrowedAttrInfo info = GetAttrInfoAt(i++)) {
|
||||
|
@ -1267,7 +1362,18 @@ void SVGElement::UpdateMappedDeclarationBlock() {
|
|||
}
|
||||
}
|
||||
|
||||
if (attrName->Equals(nsGkAtoms::d, kNameSpaceID_None)) {
|
||||
if (attrName->Atom() == nsGkAtoms::transform) {
|
||||
sawTransform = true;
|
||||
const auto* transform = GetAnimatedTransformList();
|
||||
MOZ_ASSERT(GetTransformListAttrName() == nsGkAtoms::transform);
|
||||
MOZ_ASSERT(transform);
|
||||
// We want to go through the optimized path to tell the style system the
|
||||
// result directly, rather than let it parse the same thing again.
|
||||
mappedAttrParser.TellStyleAlreadyParsedResult(*transform);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (attrName->Atom() == nsGkAtoms::d) {
|
||||
const auto* path = GetAnimPathSegList();
|
||||
// Note: Only SVGPathElement has d attribute.
|
||||
MOZ_ASSERT(
|
||||
|
@ -1295,6 +1401,14 @@ void SVGElement::UpdateMappedDeclarationBlock() {
|
|||
info.mValue->ToString(value);
|
||||
mappedAttrParser.ParseMappedAttrValue(attrName->Atom(), value);
|
||||
}
|
||||
|
||||
// We need to map the SVG view's transform if we haven't mapped it already.
|
||||
if (NodeInfo()->NameAtom() == nsGkAtoms::svg && !sawTransform) {
|
||||
if (const auto* transform = GetAnimatedTransformList()) {
|
||||
mappedAttrParser.TellStyleAlreadyParsedResult(*transform);
|
||||
}
|
||||
}
|
||||
|
||||
mAttrs.SetMappedDeclarationBlock(mappedAttrParser.TakeDeclarationBlock());
|
||||
}
|
||||
|
||||
|
@ -1713,10 +1827,9 @@ void SVGElement::DidAnimatePathSegList() {
|
|||
if (name == nsGkAtoms::d) {
|
||||
auto* animPathSegList = GetAnimPathSegList();
|
||||
if (animPathSegList->IsAnimating()) {
|
||||
SMILOverrideStyle()->SetSMILValue(nsCSSPropertyID::eCSSProperty_d,
|
||||
*animPathSegList);
|
||||
SMILOverrideStyle()->SetSMILValue(eCSSProperty_d, *animPathSegList);
|
||||
} else {
|
||||
SMILOverrideStyle()->ClearSMILValue(nsCSSPropertyID::eCSSProperty_d);
|
||||
SMILOverrideStyle()->ClearSMILValue(eCSSProperty_d);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1970,25 +2083,20 @@ void SVGElement::DidAnimateTransformList(int32_t aModType) {
|
|||
MOZ_ASSERT(GetTransformListAttrName(),
|
||||
"Animating non-existent transform data?");
|
||||
|
||||
if (auto* frame = GetPrimaryFrame()) {
|
||||
nsAtom* transformAttr = GetTransformListAttrName();
|
||||
frame->AttributeChanged(kNameSpaceID_None, transformAttr, aModType);
|
||||
// When script changes the 'transform' attribute, Element::SetAttrAndNotify
|
||||
// will call MutationObservers::NotifyAttributeChanged, under which
|
||||
// SVGTransformableElement::GetAttributeChangeHint will be called and an
|
||||
// appropriate change event posted to update our frame's overflow rects.
|
||||
// The SetAttrAndNotify doesn't happen for transform changes caused by
|
||||
// 'animateTransform' though (and sending out the mutation events that
|
||||
// MutationObservers::NotifyAttributeChanged dispatches would be
|
||||
// inappropriate anyway), so we need to post the change event ourself.
|
||||
nsChangeHint changeHint = GetAttributeChangeHint(transformAttr, aModType);
|
||||
if (changeHint) {
|
||||
nsLayoutUtils::PostRestyleEvent(this, RestyleHint{0}, changeHint);
|
||||
nsAtom* transformAttr = GetTransformListAttrName();
|
||||
if (transformAttr == nsGkAtoms::transform) {
|
||||
const auto* animTransformList = GetAnimatedTransformList();
|
||||
const auto* animateMotion = GetAnimateMotionTransform();
|
||||
if (animateMotion ||
|
||||
(animTransformList && animTransformList->IsAnimating())) {
|
||||
SMILOverrideStyle()->SetSMILValue(eCSSProperty_transform,
|
||||
animTransformList, animateMotion);
|
||||
} else {
|
||||
SMILOverrideStyle()->ClearSMILValue(eCSSProperty_transform);
|
||||
}
|
||||
SVGObserverUtils::InvalidateRenderingObservers(frame);
|
||||
return;
|
||||
}
|
||||
SVGObserverUtils::InvalidateDirectRenderingObservers(this);
|
||||
DidAnimateAttribute(kNameSpaceID_None, transformAttr);
|
||||
}
|
||||
|
||||
SVGElement::StringAttributesInfo SVGElement::GetStringInfo() {
|
||||
|
|
|
@ -181,12 +181,16 @@ class SVGElement : public SVGElementBase // nsIContent
|
|||
void SetLength(nsAtom* aName, const SVGAnimatedLength& aLength);
|
||||
|
||||
enum class ValToUse { Base, Anim };
|
||||
static bool UpdateDeclarationBlockFromLength(
|
||||
StyleLockedDeclarationBlock& aBlock, nsCSSPropertyID aPropId,
|
||||
const SVGAnimatedLength& aLength, ValToUse aValToUse);
|
||||
static bool UpdateDeclarationBlockFromPath(
|
||||
StyleLockedDeclarationBlock& aBlock, const SVGAnimatedPathSegList& aPath,
|
||||
ValToUse aValToUse);
|
||||
static bool UpdateDeclarationBlockFromLength(StyleLockedDeclarationBlock&,
|
||||
nsCSSPropertyID,
|
||||
const SVGAnimatedLength&,
|
||||
ValToUse);
|
||||
static bool UpdateDeclarationBlockFromPath(StyleLockedDeclarationBlock&,
|
||||
const SVGAnimatedPathSegList&,
|
||||
ValToUse);
|
||||
static bool UpdateDeclarationBlockFromTransform(
|
||||
StyleLockedDeclarationBlock&, const SVGAnimatedTransformList*,
|
||||
const gfx::Matrix* aAnimateMotionTransform, ValToUse);
|
||||
|
||||
nsAttrValue WillChangeLength(uint8_t aAttrEnum,
|
||||
const mozAutoDocUpdate& aProofOfUpdate);
|
||||
|
|
|
@ -43,11 +43,9 @@ class MOZ_RAII AutoSVGViewHandler {
|
|||
if (mValid) {
|
||||
mRoot->mSVGView = std::move(mSVGView);
|
||||
}
|
||||
mRoot->InvalidateTransformNotifyFrame();
|
||||
if (nsIFrame* f = mRoot->GetPrimaryFrame()) {
|
||||
if (SVGOuterSVGFrame* osf = do_QueryFrame(f)) {
|
||||
osf->MaybeSendIntrinsicSizeAndRatioToEmbedder();
|
||||
}
|
||||
mRoot->DidChangeSVGView();
|
||||
if (SVGOuterSVGFrame* osf = do_QueryFrame(mRoot->GetPrimaryFrame())) {
|
||||
osf->MaybeSendIntrinsicSizeAndRatioToEmbedder();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
#include "mozilla/dom/SVGLengthBinding.h"
|
||||
#include "mozilla/gfx/2D.h"
|
||||
#include "mozilla/RefPtr.h"
|
||||
#include "mozilla/StaticPrefs_layout.h"
|
||||
#include "nsLayoutUtils.h"
|
||||
#include "mozilla/SVGContentUtils.h"
|
||||
|
||||
using namespace mozilla::gfx;
|
||||
|
@ -242,6 +242,21 @@ already_AddRefed<DOMSVGPoint> SVGGeometryElement::GetPointAtLength(
|
|||
clamped(distance, 0.f, path->ComputeLength()))));
|
||||
}
|
||||
|
||||
gfx::Matrix SVGGeometryElement::LocalTransform() const {
|
||||
gfx::Matrix result;
|
||||
nsIFrame* f = GetPrimaryFrame();
|
||||
if (!f || !f->IsTransformed()) {
|
||||
return result;
|
||||
}
|
||||
Matrix4x4Flagged matrix = nsDisplayTransform::GetResultingTransformMatrix(
|
||||
f, nsPoint(), f->PresContext()->AppUnitsPerDevPixel(),
|
||||
nsDisplayTransform::INCLUDE_PERSPECTIVE);
|
||||
if (!matrix.IsIdentity()) {
|
||||
std::ignore = matrix.CanDraw2D(&result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
float SVGGeometryElement::GetPathLengthScale(PathLengthScaleForType aFor) {
|
||||
MOZ_ASSERT(aFor == eForTextPath || aFor == eForStroking, "Unknown enum");
|
||||
if (mPathLength.IsExplicitlySet()) {
|
||||
|
@ -257,10 +272,9 @@ float SVGGeometryElement::GetPathLengthScale(PathLengthScaleForType aFor) {
|
|||
// For textPath, a transform on the referenced path affects the
|
||||
// textPath layout, so when calculating the actual path length
|
||||
// we need to take that into account.
|
||||
gfxMatrix matrix = PrependLocalTransformsTo(gfxMatrix());
|
||||
auto matrix = LocalTransform();
|
||||
if (!matrix.IsIdentity()) {
|
||||
RefPtr<PathBuilder> builder =
|
||||
path->TransformedCopyToBuilder(ToMatrix(matrix));
|
||||
RefPtr<PathBuilder> builder = path->TransformedCopyToBuilder(matrix);
|
||||
path = builder->Finish();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -247,6 +247,8 @@ class SVGGeometryElement : public SVGGeometryElementBase {
|
|||
MOZ_CAN_RUN_SCRIPT already_AddRefed<DOMSVGPoint> GetPointAtLength(
|
||||
float distance, ErrorResult& rv);
|
||||
|
||||
gfx::Matrix LocalTransform() const;
|
||||
|
||||
protected:
|
||||
// SVGElement method
|
||||
NumberAttributesInfo GetNumberInfo() override;
|
||||
|
|
|
@ -441,10 +441,19 @@ bool SVGSVGElement::WillBeOutermostSVG(nsINode& aParent) const {
|
|||
return true;
|
||||
}
|
||||
|
||||
void SVGSVGElement::DidChangeSVGView() {
|
||||
InvalidateTransformNotifyFrame();
|
||||
// We map the SVGView transform as the transform css property, so need to
|
||||
// schedule attribute mapping.
|
||||
if (!IsPendingMappedAttributeEvaluation() &&
|
||||
mAttrs.MarkAsPendingPresAttributeEvaluation()) {
|
||||
OwnerDoc()->ScheduleForPresAttrEvaluation(this);
|
||||
}
|
||||
}
|
||||
|
||||
void SVGSVGElement::InvalidateTransformNotifyFrame() {
|
||||
ISVGSVGFrame* svgframe = do_QueryFrame(GetPrimaryFrame());
|
||||
// might fail this check if we've failed conditional processing
|
||||
if (svgframe) {
|
||||
if (ISVGSVGFrame* svgframe = do_QueryFrame(GetPrimaryFrame())) {
|
||||
svgframe->NotifyViewportOrTransformChanged(
|
||||
ISVGDisplayableFrame::TRANSFORM_CHANGED);
|
||||
}
|
||||
|
@ -584,9 +593,4 @@ const SVGAnimatedViewBox& SVGSVGElement::GetViewBoxInternal() const {
|
|||
return mViewBox;
|
||||
}
|
||||
|
||||
SVGAnimatedTransformList* SVGSVGElement::GetTransformInternal() const {
|
||||
return (mSVGView && mSVGView->mTransforms) ? mSVGView->mTransforms.get()
|
||||
: mTransforms.get();
|
||||
}
|
||||
|
||||
} // namespace mozilla::dom
|
||||
|
|
|
@ -183,6 +183,7 @@ class SVGSVGElement final : public SVGSVGElementBase {
|
|||
|
||||
// invalidate viewbox -> viewport xform & inform frames
|
||||
void InvalidateTransformNotifyFrame();
|
||||
void DidChangeSVGView();
|
||||
|
||||
// Methods for <image> elements to override my "PreserveAspectRatio" value.
|
||||
// These are private so that only our friends
|
||||
|
@ -197,7 +198,6 @@ class SVGSVGElement final : public SVGSVGElementBase {
|
|||
bool ClearPreserveAspectRatioProperty();
|
||||
|
||||
const SVGAnimatedViewBox& GetViewBoxInternal() const override;
|
||||
SVGAnimatedTransformList* GetTransformInternal() const override;
|
||||
|
||||
EnumAttributesInfo GetEnumInfo() override;
|
||||
|
||||
|
|
|
@ -26,6 +26,12 @@ SVGTransformableElement::Transform() {
|
|||
//----------------------------------------------------------------------
|
||||
// nsIContent methods
|
||||
|
||||
bool SVGTransformableElement::IsAttributeMapped(
|
||||
const nsAtom* aAttribute) const {
|
||||
return aAttribute == nsGkAtoms::transform ||
|
||||
SVGElement::IsAttributeMapped(aAttribute);
|
||||
}
|
||||
|
||||
nsChangeHint SVGTransformableElement::GetAttributeChangeHint(
|
||||
const nsAtom* aAttribute, int32_t aModType) const {
|
||||
nsChangeHint retval =
|
||||
|
@ -75,14 +81,9 @@ bool SVGTransformableElement::IsEventAttributeNameInternal(nsAtom* aName) {
|
|||
|
||||
gfxMatrix SVGTransformableElement::PrependLocalTransformsTo(
|
||||
const gfxMatrix& aMatrix, SVGTransformTypes aWhich) const {
|
||||
if (aWhich == eChildToUserSpace) {
|
||||
// We don't have any eUserSpaceToParent transforms. (Sub-classes that do
|
||||
// must override this function and handle that themselves.)
|
||||
return aMatrix;
|
||||
}
|
||||
return GetUserToParentTransform(mAnimateMotionTransform.get(),
|
||||
mTransforms.get()) *
|
||||
aMatrix;
|
||||
// We don't have any eUserSpaceToParent transforms. (Sub-classes that do
|
||||
// must override this function and handle that themselves.)
|
||||
return aMatrix;
|
||||
}
|
||||
|
||||
const gfx::Matrix* SVGTransformableElement::GetAnimateMotionTransform() const {
|
||||
|
@ -130,21 +131,4 @@ SVGAnimatedTransformList* SVGTransformableElement::GetAnimatedTransformList(
|
|||
return mTransforms.get();
|
||||
}
|
||||
|
||||
/* static */
|
||||
gfxMatrix SVGTransformableElement::GetUserToParentTransform(
|
||||
const gfx::Matrix* aAnimateMotionTransform,
|
||||
const SVGAnimatedTransformList* aTransforms) {
|
||||
gfxMatrix result;
|
||||
|
||||
if (aAnimateMotionTransform) {
|
||||
result.PreMultiply(ThebesMatrix(*aAnimateMotionTransform));
|
||||
}
|
||||
|
||||
if (aTransforms) {
|
||||
result.PreMultiply(aTransforms->GetAnimValue().GetConsolidationMatrix());
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace mozilla::dom
|
||||
|
|
|
@ -45,6 +45,7 @@ class SVGTransformableElement : public SVGElement {
|
|||
SVGTransformTypes aWhich = eAllTransforms) const override;
|
||||
const gfx::Matrix* GetAnimateMotionTransform() const override;
|
||||
void SetAnimateMotionTransform(const gfx::Matrix* aMatrix) override;
|
||||
NS_IMETHOD_(bool) IsAttributeMapped(const nsAtom* aAttribute) const override;
|
||||
|
||||
SVGAnimatedTransformList* GetAnimatedTransformList(
|
||||
uint32_t aFlags = 0) override;
|
||||
|
@ -55,17 +56,6 @@ class SVGTransformableElement : public SVGElement {
|
|||
bool IsTransformable() override { return true; }
|
||||
|
||||
protected:
|
||||
/**
|
||||
* Helper for overrides of PrependLocalTransformsTo. If both arguments are
|
||||
* provided they are multiplied in the order in which the arguments appear,
|
||||
* and the result is returned. If neither argument is provided, the identity
|
||||
* matrix is returned. If only one argument is provided its transform is
|
||||
* returned.
|
||||
*/
|
||||
static gfxMatrix GetUserToParentTransform(
|
||||
const gfx::Matrix* aAnimateMotionTransform,
|
||||
const SVGAnimatedTransformList* aTransforms);
|
||||
|
||||
UniquePtr<SVGAnimatedTransformList> mTransforms;
|
||||
|
||||
// XXX maybe move this to property table, to save space on un-animated elems?
|
||||
|
|
|
@ -606,15 +606,8 @@ void SVGUseElement::UnlinkSource() {
|
|||
/* virtual */
|
||||
gfxMatrix SVGUseElement::PrependLocalTransformsTo(
|
||||
const gfxMatrix& aMatrix, SVGTransformTypes aWhich) const {
|
||||
// 'transform' attribute:
|
||||
gfxMatrix userToParent;
|
||||
|
||||
if (aWhich == eUserSpaceToParent || aWhich == eAllTransforms) {
|
||||
userToParent = GetUserToParentTransform(mAnimateMotionTransform.get(),
|
||||
mTransforms.get());
|
||||
if (aWhich == eUserSpaceToParent) {
|
||||
return userToParent * aMatrix;
|
||||
}
|
||||
if (aWhich == eUserSpaceToParent) {
|
||||
return aMatrix;
|
||||
}
|
||||
|
||||
// our 'x' and 'y' attributes:
|
||||
|
@ -624,13 +617,8 @@ gfxMatrix SVGUseElement::PrependLocalTransformsTo(
|
|||
}
|
||||
|
||||
gfxMatrix childToUser = gfxMatrix::Translation(x, y);
|
||||
|
||||
if (aWhich == eAllTransforms) {
|
||||
return childToUser * userToParent * aMatrix;
|
||||
}
|
||||
|
||||
MOZ_ASSERT(aWhich == eChildToUserSpace, "Unknown TransformTypes");
|
||||
|
||||
MOZ_ASSERT(aWhich == eChildToUserSpace || aWhich == eAllTransforms,
|
||||
"Unknown TransformTypes");
|
||||
// The following may look broken because pre-multiplying our eChildToUserSpace
|
||||
// transform with another matrix without including our eUserSpaceToParent
|
||||
// transform between the two wouldn't make sense. We don't expect that to
|
||||
|
|
|
@ -241,14 +241,8 @@ float SVGViewportElement::GetLength(uint8_t aCtxType) const {
|
|||
gfxMatrix SVGViewportElement::PrependLocalTransformsTo(
|
||||
const gfxMatrix& aMatrix, SVGTransformTypes aWhich) const {
|
||||
// 'transform' attribute (or an override from a fragment identifier):
|
||||
gfxMatrix userToParent;
|
||||
|
||||
if (aWhich == eUserSpaceToParent || aWhich == eAllTransforms) {
|
||||
userToParent = GetUserToParentTransform(mAnimateMotionTransform.get(),
|
||||
GetTransformInternal());
|
||||
if (aWhich == eUserSpaceToParent) {
|
||||
return userToParent * aMatrix;
|
||||
}
|
||||
if (aWhich == eUserSpaceToParent) {
|
||||
return aMatrix;
|
||||
}
|
||||
|
||||
gfxMatrix childToUser;
|
||||
|
@ -271,12 +265,8 @@ gfxMatrix SVGViewportElement::PrependLocalTransformsTo(
|
|||
childToUser = ThebesMatrix(GetViewBoxTransform());
|
||||
}
|
||||
|
||||
if (aWhich == eAllTransforms) {
|
||||
return childToUser * userToParent * aMatrix;
|
||||
}
|
||||
|
||||
MOZ_ASSERT(aWhich == eChildToUserSpace, "Unknown TransformTypes");
|
||||
|
||||
MOZ_ASSERT(aWhich == eAllTransforms || aWhich == eChildToUserSpace,
|
||||
"Unknown TransformTypes");
|
||||
// The following may look broken because pre-multiplying our eChildToUserSpace
|
||||
// transform with another matrix without including our eUserSpaceToParent
|
||||
// transform between the two wouldn't make sense. We don't expect that to
|
||||
|
|
|
@ -160,9 +160,6 @@ class SVGViewportElement : public SVGGraphicsElement {
|
|||
virtual const SVGAnimatedViewBox& GetViewBoxInternal() const {
|
||||
return mViewBox;
|
||||
}
|
||||
virtual SVGAnimatedTransformList* GetTransformInternal() const {
|
||||
return mTransforms.get();
|
||||
}
|
||||
SVGAnimatedViewBox mViewBox;
|
||||
SVGAnimatedPreserveAspectRatio mPreserveAspectRatio;
|
||||
|
||||
|
|
|
@ -6247,12 +6247,13 @@ Matrix4x4 nsDisplayTransform::GetResultingTransformMatrixInternal(
|
|||
aProperties.mTranslate, aProperties.mRotate, aProperties.mScale,
|
||||
aProperties.mMotion.ptrOr(nullptr), aProperties.mTransform, aRefBox,
|
||||
aAppUnitsPerPixel);
|
||||
} else if (hasSVGTransforms) {
|
||||
}
|
||||
if (hasSVGTransforms) {
|
||||
// Correct the translation components for zoom:
|
||||
float pixelsPerCSSPx = AppUnitsPerCSSPixel() / aAppUnitsPerPixel;
|
||||
svgTransform._31 *= pixelsPerCSSPx;
|
||||
svgTransform._32 *= pixelsPerCSSPx;
|
||||
result = Matrix4x4::From2D(svgTransform);
|
||||
result *= Matrix4x4::From2D(svgTransform);
|
||||
}
|
||||
|
||||
// Apply any translation due to 'transform-origin' and/or 'transform-box':
|
||||
|
|
|
@ -182,7 +182,8 @@ nsresult nsDOMCSSAttributeDeclaration::SetSMILValue(
|
|||
}
|
||||
|
||||
nsresult nsDOMCSSAttributeDeclaration::SetSMILValue(
|
||||
const nsCSSPropertyID /*aPropID*/, const SVGAnimatedPathSegList& aPath) {
|
||||
const nsCSSPropertyID aPropID, const SVGAnimatedPathSegList& aPath) {
|
||||
MOZ_ASSERT(aPropID == eCSSProperty_d);
|
||||
return SetSMILValueHelper([&aPath](DeclarationBlock& aDecl) {
|
||||
MOZ_ASSERT(aDecl.IsMutable());
|
||||
return SVGElement::UpdateDeclarationBlockFromPath(
|
||||
|
@ -190,6 +191,19 @@ nsresult nsDOMCSSAttributeDeclaration::SetSMILValue(
|
|||
});
|
||||
}
|
||||
|
||||
nsresult nsDOMCSSAttributeDeclaration::SetSMILValue(
|
||||
const nsCSSPropertyID aPropID, const SVGAnimatedTransformList* aTransform,
|
||||
const gfx::Matrix* aAnimateMotionTransform) {
|
||||
MOZ_ASSERT(aPropID == eCSSProperty_transform);
|
||||
return SetSMILValueHelper(
|
||||
[aTransform, aAnimateMotionTransform](DeclarationBlock& aDecl) {
|
||||
MOZ_ASSERT(aDecl.IsMutable());
|
||||
return SVGElement::UpdateDeclarationBlockFromTransform(
|
||||
*aDecl.Raw(), aTransform, aAnimateMotionTransform,
|
||||
SVGElement::ValToUse::Anim);
|
||||
});
|
||||
}
|
||||
|
||||
// Scripted modifications to style.opacity or style.transform (or other
|
||||
// transform-like properties, e.g. style.translate, style.rotate, style.scale)
|
||||
// could immediately force us into the animated state if heuristics suggest
|
||||
|
|
|
@ -21,6 +21,7 @@ namespace mozilla {
|
|||
class SMILValue;
|
||||
class SVGAnimatedLength;
|
||||
class SVGAnimatedPathSegList;
|
||||
class SVGAnimatedTransformList;
|
||||
|
||||
namespace dom {
|
||||
class DomGroup;
|
||||
|
@ -55,6 +56,9 @@ class nsDOMCSSAttributeDeclaration final : public nsDOMCSSDeclaration {
|
|||
const SVGAnimatedLength& aLength);
|
||||
nsresult SetSMILValue(const nsCSSPropertyID,
|
||||
const mozilla::SVGAnimatedPathSegList& aPath);
|
||||
nsresult SetSMILValue(const nsCSSPropertyID,
|
||||
const mozilla::SVGAnimatedTransformList*,
|
||||
const mozilla::gfx::Matrix* aAnimateMotion = nullptr);
|
||||
void ClearSMILValue(const nsCSSPropertyID aPropID) {
|
||||
// Put empty string in override style for our property
|
||||
SetPropertyValue(aPropID, ""_ns, nullptr, mozilla::IgnoreErrors());
|
||||
|
|
|
@ -104,14 +104,17 @@ void SVGGeometryFrame::DidSetComputedStyle(ComputedStyle* aOldComputedStyle) {
|
|||
// For clipPath we use clip-rule as the path's fill-rule.
|
||||
element->ClearAnyCachedPath();
|
||||
}
|
||||
} else {
|
||||
if (StyleSVG()->mFillRule != oldStyleSVG->mFillRule) {
|
||||
// Moz2D Path objects are fill-rule specific.
|
||||
element->ClearAnyCachedPath();
|
||||
}
|
||||
} else if (StyleSVG()->mFillRule != oldStyleSVG->mFillRule) {
|
||||
// Moz2D Path objects are fill-rule specific.
|
||||
element->ClearAnyCachedPath();
|
||||
}
|
||||
}
|
||||
|
||||
if (StyleDisplay()->CalcTransformPropertyDifference(
|
||||
*aOldComputedStyle->StyleDisplay())) {
|
||||
SVGUtils::NotifyChildrenOfSVGChange(this, TRANSFORM_CHANGED);
|
||||
}
|
||||
|
||||
if (element->IsGeometryChangedViaCSS(*Style(), *aOldComputedStyle)) {
|
||||
element->ClearAnyCachedPath();
|
||||
SVGObserverUtils::InvalidateRenderingObservers(this);
|
||||
|
|
|
@ -44,7 +44,7 @@ SVGOuterSVGFrame::SVGOuterSVGFrame(ComputedStyle* aStyle,
|
|||
// Outer-<svg> has CSS layout, so remove this bit:
|
||||
RemoveStateBits(NS_FRAME_SVG_LAYOUT);
|
||||
AddStateBits(NS_FRAME_REFLOW_ROOT | NS_FRAME_FONT_INFLATION_CONTAINER |
|
||||
NS_FRAME_FONT_INFLATION_FLOW_ROOT | NS_FRAME_MAY_BE_TRANSFORMED);
|
||||
NS_FRAME_FONT_INFLATION_FLOW_ROOT);
|
||||
}
|
||||
|
||||
// The CSS Containment spec says that size-contained replaced elements must be
|
||||
|
@ -531,21 +531,7 @@ bool SVGOuterSVGFrame::IsSVGTransformed(Matrix* aOwnTransform,
|
|||
// Our anonymous child's HasChildrenOnlyTransform() implementation makes sure
|
||||
// our children-only transforms are applied to our children. We only care
|
||||
// about transforms that transform our own frame here.
|
||||
|
||||
bool foundTransform = false;
|
||||
|
||||
SVGSVGElement* content = static_cast<SVGSVGElement*>(GetContent());
|
||||
SVGAnimatedTransformList* transformList = content->GetAnimatedTransformList();
|
||||
if ((transformList && transformList->HasTransform()) ||
|
||||
content->GetAnimateMotionTransform()) {
|
||||
if (aOwnTransform) {
|
||||
*aOwnTransform = gfx::ToMatrix(
|
||||
content->PrependLocalTransformsTo(gfxMatrix(), eUserSpaceToParent));
|
||||
}
|
||||
foundTransform = true;
|
||||
}
|
||||
|
||||
return foundTransform;
|
||||
return false;
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------
|
||||
|
@ -667,8 +653,7 @@ SVGBBox SVGOuterSVGFrame::GetBBoxContribution(
|
|||
|
||||
gfxMatrix SVGOuterSVGFrame::GetCanvasTM() {
|
||||
if (!mCanvasTM) {
|
||||
SVGSVGElement* content = static_cast<SVGSVGElement*>(GetContent());
|
||||
|
||||
auto* content = static_cast<SVGSVGElement*>(GetContent());
|
||||
float devPxPerCSSPx = 1.0f / nsPresContext::AppUnitsToFloatCSSPixels(
|
||||
PresContext()->AppUnitsPerDevPixel());
|
||||
|
||||
|
|
|
@ -351,13 +351,26 @@ static SVGTextFrame* FrameIfAnonymousChildReflowed(SVGTextFrame* aFrame) {
|
|||
return aFrame;
|
||||
}
|
||||
|
||||
static double GetContextScale(const gfxMatrix& aMatrix) {
|
||||
// The context scale is the ratio of the length of the transformed
|
||||
// diagonal vector (1,1) to the length of the untransformed diagonal
|
||||
// (which is sqrt(2)).
|
||||
gfxPoint p = aMatrix.TransformPoint(gfxPoint(1, 1)) -
|
||||
aMatrix.TransformPoint(gfxPoint(0, 0));
|
||||
return SVGContentUtils::ComputeNormalizedHypotenuse(p.x, p.y);
|
||||
// FIXME(emilio): SVG is a special-case where transforms affect layout. We don't
|
||||
// want that to go outside the SVG stuff (and really we should aim to remove
|
||||
// that).
|
||||
static float GetContextScale(SVGTextFrame* aFrame) {
|
||||
if (aFrame->HasAnyStateBits(NS_FRAME_IS_NONDISPLAY)) {
|
||||
// When we are non-display, we could be painted in different coordinate
|
||||
// spaces, and we don't want to have to reflow for each of these. We just
|
||||
// assume that the context scale is 1.0 for them all, so we don't get stuck
|
||||
// with a font size scale factor based on whichever referencing frame
|
||||
// happens to reflow first.
|
||||
return 1.0f;
|
||||
}
|
||||
auto matrix = nsLayoutUtils::GetTransformToAncestor(
|
||||
RelativeTo{aFrame}, RelativeTo{SVGUtils::GetOuterSVGFrame(aFrame)});
|
||||
Matrix transform2D;
|
||||
if (!matrix.CanDraw2D(&transform2D)) {
|
||||
return {};
|
||||
}
|
||||
auto scales = transform2D.ScaleFactors();
|
||||
return std::max(1.0f, std::max(scales.xScale, scales.yScale));
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
@ -2993,16 +3006,16 @@ void SVGTextFrame::NotifySVGChanged(uint32_t aFlags) {
|
|||
// do not get too far out of sync with the final font size on the screen.
|
||||
if (needNewCanvasTM && mLastContextScale != 0.0f) {
|
||||
mCanvasTM = nullptr;
|
||||
// If we are a non-display frame, then we don't want to call
|
||||
// GetCanvasTM(), since the context scale does not use it.
|
||||
gfxMatrix newTM =
|
||||
HasAnyStateBits(NS_FRAME_IS_NONDISPLAY) ? gfxMatrix() : GetCanvasTM();
|
||||
// Compare the old and new context scales.
|
||||
float scale = GetContextScale(newTM);
|
||||
float change = scale / mLastContextScale;
|
||||
if (change >= 2.0f || change <= 0.5f) {
|
||||
needNewBounds = true;
|
||||
needGlyphMetricsUpdate = true;
|
||||
// If we are a non-display frame, then we don't want to get the transform
|
||||
// since the context scale does not use it.
|
||||
if (!HasAnyStateBits(NS_FRAME_IS_NONDISPLAY)) {
|
||||
// Compare the old and new context scales.
|
||||
float scale = GetContextScale(this);
|
||||
float change = scale / mLastContextScale;
|
||||
if (change >= 2.0f || change <= 0.5f) {
|
||||
needNewBounds = true;
|
||||
needGlyphMetricsUpdate = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4521,11 +4534,10 @@ already_AddRefed<Path> SVGTextFrame::GetTextPath(nsIFrame* aTextPathFrame) {
|
|||
return nullptr;
|
||||
}
|
||||
|
||||
gfxMatrix matrix = geomElement->PrependLocalTransformsTo(gfxMatrix());
|
||||
// Apply the geometry element's transform if appropriate.
|
||||
auto matrix = geomElement->LocalTransform();
|
||||
if (!matrix.IsIdentity()) {
|
||||
// Apply the geometry element's transform
|
||||
RefPtr<PathBuilder> builder =
|
||||
path->TransformedCopyToBuilder(ToMatrix(matrix));
|
||||
RefPtr<PathBuilder> builder = path->TransformedCopyToBuilder(matrix);
|
||||
path = builder->Finish();
|
||||
}
|
||||
|
||||
|
@ -5119,8 +5131,6 @@ void SVGTextFrame::DoReflow() {
|
|||
bool SVGTextFrame::UpdateFontSizeScaleFactor() {
|
||||
double oldFontSizeScaleFactor = mFontSizeScaleFactor;
|
||||
|
||||
nsPresContext* presContext = PresContext();
|
||||
|
||||
bool geometricPrecision = false;
|
||||
CSSCoord min = std::numeric_limits<float>::max();
|
||||
CSSCoord max = std::numeric_limits<float>::min();
|
||||
|
@ -5158,31 +5168,9 @@ bool SVGTextFrame::UpdateFontSizeScaleFactor() {
|
|||
return mFontSizeScaleFactor != oldFontSizeScaleFactor;
|
||||
}
|
||||
|
||||
// When we are non-display, we could be painted in different coordinate
|
||||
// spaces, and we don't want to have to reflow for each of these. We
|
||||
// just assume that the context scale is 1.0 for them all, so we don't
|
||||
// get stuck with a font size scale factor based on whichever referencing
|
||||
// frame happens to reflow first.
|
||||
double contextScale = 1.0;
|
||||
if (!HasAnyStateBits(NS_FRAME_IS_NONDISPLAY)) {
|
||||
gfxMatrix m(GetCanvasTM());
|
||||
if (!m.IsSingular()) {
|
||||
contextScale = GetContextScale(m);
|
||||
if (!std::isfinite(contextScale)) {
|
||||
contextScale = 1.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
float contextScale = GetContextScale(this);
|
||||
mLastContextScale = contextScale;
|
||||
|
||||
// But we want to ignore any scaling required due to HiDPI displays, since
|
||||
// regular CSS text frames will still create text runs using the font size
|
||||
// in CSS pixels, and we want SVG text to have the same rendering as HTML
|
||||
// text for regular font sizes.
|
||||
float cssPxPerDevPx = nsPresContext::AppUnitsToFloatCSSPixels(
|
||||
presContext->AppUnitsPerDevPixel());
|
||||
contextScale *= cssPxPerDevPx;
|
||||
|
||||
double minTextRunSize = min * contextScale;
|
||||
double maxTextRunSize = max * contextScale;
|
||||
|
||||
|
|
|
@ -357,23 +357,10 @@ bool SVGUtils::IsSVGTransformed(const nsIFrame* aFrame,
|
|||
NS_FRAME_MAY_BE_TRANSFORMED),
|
||||
"Expecting an SVG frame that can be transformed");
|
||||
bool foundTransform = false;
|
||||
|
||||
// Check if our parent has children-only transforms:
|
||||
if (SVGContainerFrame* parent = do_QueryFrame(aFrame->GetParent())) {
|
||||
foundTransform = parent->HasChildrenOnlyTransform(aFromParentTransform);
|
||||
}
|
||||
|
||||
if (auto* content = SVGElement::FromNode(aFrame->GetContent())) {
|
||||
auto* transformList = content->GetAnimatedTransformList();
|
||||
if ((transformList && transformList->HasTransform()) ||
|
||||
content->GetAnimateMotionTransform()) {
|
||||
if (aOwnTransform) {
|
||||
*aOwnTransform = gfx::ToMatrix(
|
||||
content->PrependLocalTransformsTo(gfxMatrix(), eUserSpaceToParent));
|
||||
}
|
||||
foundTransform = true;
|
||||
}
|
||||
}
|
||||
return foundTransform;
|
||||
}
|
||||
|
||||
|
@ -1532,15 +1519,14 @@ gfxMatrix SVGUtils::GetTransformMatrixInUserSpace(const nsIFrame* aFrame) {
|
|||
|
||||
Matrix svgTransform;
|
||||
Matrix4x4 trans;
|
||||
(void)aFrame->IsSVGTransformed(&svgTransform);
|
||||
|
||||
if (properties.HasTransform()) {
|
||||
trans = nsStyleTransformMatrix::ReadTransforms(
|
||||
properties.mTranslate, properties.mRotate, properties.mScale,
|
||||
properties.mMotion.ptrOr(nullptr), properties.mTransform, refBox,
|
||||
AppUnitsPerCSSPixel());
|
||||
} else {
|
||||
trans = Matrix4x4::From2D(svgTransform);
|
||||
}
|
||||
if (aFrame->IsSVGTransformed(&svgTransform)) {
|
||||
trans *= Matrix4x4::From2D(svgTransform);
|
||||
}
|
||||
|
||||
trans.ChangeBasis(svgTransformOrigin);
|
||||
|
|
|
@ -5690,6 +5690,24 @@ pub extern "C" fn Servo_DeclarationBlock_SetLengthValue(
|
|||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn Servo_DeclarationBlock_SetTransform(
|
||||
declarations: &LockedDeclarationBlock,
|
||||
property: nsCSSPropertyID,
|
||||
ops: &nsTArray<computed::TransformOperation>,
|
||||
) {
|
||||
use style::values::generics::transform::GenericTransform;
|
||||
use style::properties::PropertyDeclaration;
|
||||
let long = get_longhand_from_id!(property);
|
||||
let v = GenericTransform(ops.iter().map(ToComputedValue::from_computed_value).collect());
|
||||
let prop = match_wrap_declared! { long,
|
||||
Transform => v,
|
||||
};
|
||||
write_locked_arc(declarations, |decls: &mut PropertyDeclarationBlock| {
|
||||
decls.push(prop, Importance::Normal);
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn Servo_DeclarationBlock_SetPathValue(
|
||||
declarations: &LockedDeclarationBlock,
|
||||
|
|
|
@ -1,2 +0,0 @@
|
|||
[translate-and-transform-css-property-in-svg.html]
|
||||
expected: FAIL
|
|
@ -1,7 +1,3 @@
|
|||
[presentation-attributes-relevant.html]
|
||||
[text-overflow presentation attribute supported on a relevant element]
|
||||
expected: FAIL
|
||||
|
||||
[transform presentation attribute supported on a relevant element]
|
||||
expected: FAIL
|
||||
|
||||
|
|
|
@ -11,9 +11,6 @@
|
|||
[fill presentation attribute not supported on discard]
|
||||
expected: FAIL
|
||||
|
||||
[transform presentation attribute supported on g]
|
||||
expected: FAIL
|
||||
|
||||
[patternTransform presentation attribute supported on pattern]
|
||||
expected: FAIL
|
||||
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
<!doctype html>
|
||||
<svg width="200" height="200" viewBox="0 0 34 34">
|
||||
<rect x="8" y="18" width="2" height="18" rx="1" transform="rotate(-90 8 18)" fill="#333333"/>
|
||||
</svg>
|
|
@ -0,0 +1,7 @@
|
|||
<!doctype html>
|
||||
<link rel="help" href="https://drafts.csswg.org/css-viewport/#zoom-property">
|
||||
<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1906261">
|
||||
<link rel="match" href="svg-transform-ref.html">
|
||||
<svg style="zoom: 2" width="100" height="100" viewBox="0 0 17 17">
|
||||
<rect x="4" y="9" width="1" height="9" rx=".5" transform="rotate(-90 4 9)" fill="#333333"/>
|
||||
</svg>
|
Загрузка…
Ссылка в новой задаче