Bug 1947961 - Implement basic "update pseudo-element styles" steps. r=view-transitions-reviewers,boris
Pretty bare bones (some properties missing), but this allows the transition pseudo-elements to be in the right place. We need to implement live capturing / integrate nical's work to display the "new" image properly, but we'll get to that. Meanwhile this should be uncontroversial. Differential Revision: https://phabricator.services.mozilla.com/D238005
This commit is contained in:
@@ -4630,66 +4630,7 @@ static Element* SearchViewTransitionPseudo(const Element* aElement,
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Element* root = vt->GetRoot();
|
||||
if (!root) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (aRequest.mType == PseudoStyleType::viewTransition) {
|
||||
return root;
|
||||
}
|
||||
|
||||
// Linear search ::view-transition-group by |aRequest.mIdentifier|.
|
||||
// Note: perhaps we can add a hashtable to improve the performance if it's
|
||||
// common that there are a lot of view-transition-names.
|
||||
Element* group = root->GetFirstElementChild();
|
||||
for (; group; group = group->GetNextElementSibling()) {
|
||||
MOZ_ASSERT(group->HasName(),
|
||||
"The generated ::view-transition-group() should have a name");
|
||||
nsAtom* name = group->GetParsedAttr(nsGkAtoms::name)->GetAtomValue();
|
||||
if (name == aRequest.mIdentifier) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// No one specifies view-transition-name or we mismatch all names.
|
||||
if (!group) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (aRequest.mType == PseudoStyleType::viewTransitionGroup) {
|
||||
return group;
|
||||
}
|
||||
|
||||
Element* imagePair = group->GetFirstElementChild();
|
||||
MOZ_ASSERT(imagePair, "::view-transition-image-pair() should exist always");
|
||||
if (aRequest.mType == PseudoStyleType::viewTransitionImagePair) {
|
||||
return imagePair;
|
||||
}
|
||||
|
||||
Element* child = imagePair->GetFirstElementChild();
|
||||
// Neither ::view-transition-old() nor ::view-transition-new() doesn't exist.
|
||||
if (!child) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Check if the first element matches our request.
|
||||
const PseudoStyleType type = child->GetPseudoElementType();
|
||||
if (type == aRequest.mType) {
|
||||
return child;
|
||||
}
|
||||
|
||||
// Since the second child is either ::view-transition-new() or nullptr, so we
|
||||
// can reject viewTransitionOld request here.
|
||||
if (aRequest.mType == PseudoStyleType::viewTransitionOld) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
child = child->GetNextElementSibling();
|
||||
MOZ_ASSERT(aRequest.mType == PseudoStyleType::viewTransitionNew);
|
||||
MOZ_ASSERT(!child || !child->GetNextElementSibling(),
|
||||
"No more psuedo elements in this subtree");
|
||||
return child;
|
||||
return vt->FindPseudo(aRequest);
|
||||
}
|
||||
|
||||
Element* Element::GetPseudoElement(const PseudoStyleRequest& aRequest) const {
|
||||
|
||||
@@ -350,15 +350,31 @@ static already_AddRefed<Element> MakePseudo(Document& aDoc,
|
||||
return el.forget();
|
||||
}
|
||||
|
||||
static void SetProp(StyleLockedDeclarationBlock* aDecls, Document* aDoc,
|
||||
const nsACString& aProp, const nsACString& aValue) {
|
||||
Servo_DeclarationBlock_SetProperty(
|
||||
aDecls, &aProp, &aValue,
|
||||
static bool SetProp(StyleLockedDeclarationBlock* aDecls, Document* aDoc,
|
||||
nsCSSPropertyID aProp, const nsACString& aValue) {
|
||||
return Servo_DeclarationBlock_SetPropertyById(
|
||||
aDecls, aProp, &aValue,
|
||||
/* is_important = */ false, aDoc->DefaultStyleAttrURLData(),
|
||||
StyleParsingMode::DEFAULT, eCompatibility_FullStandards,
|
||||
aDoc->CSSLoader(), StyleCssRuleType::Style, {});
|
||||
}
|
||||
|
||||
static bool SetProp(StyleLockedDeclarationBlock* aDecls, Document*,
|
||||
nsCSSPropertyID aProp, float aLength, nsCSSUnit aUnit) {
|
||||
return Servo_DeclarationBlock_SetLengthValue(aDecls, aProp, aLength, aUnit);
|
||||
}
|
||||
|
||||
static bool SetProp(StyleLockedDeclarationBlock* aDecls, Document*,
|
||||
nsCSSPropertyID aProp, const CSSToCSSMatrix4x4Flagged& aM) {
|
||||
MOZ_ASSERT(aProp == eCSSProperty_transform);
|
||||
AutoTArray<StyleTransformOperation, 1> ops;
|
||||
ops.AppendElement(
|
||||
StyleTransformOperation::Matrix3D(StyleGenericMatrix3D<StyleNumber>{
|
||||
aM._11, aM._12, aM._13, aM._14, aM._21, aM._22, aM._23, aM._24,
|
||||
aM._31, aM._32, aM._33, aM._34, aM._41, aM._42, aM._43, aM._44}));
|
||||
return Servo_DeclarationBlock_SetTransform(aDecls, aProp, &ops);
|
||||
}
|
||||
|
||||
static StyleLockedDeclarationBlock* EnsureRule(
|
||||
RefPtr<StyleLockedDeclarationBlock>& aRule) {
|
||||
if (!aRule) {
|
||||
@@ -426,7 +442,7 @@ void ViewTransition::SetupTransitionPseudoElements() {
|
||||
MOZ_ASSERT(capturedElement.mNewElement);
|
||||
// Set capturedElement's image animation name rule to a new ...
|
||||
auto* rule = EnsureRule(capturedElement.mNewRule);
|
||||
SetProp(rule, mDocument, "animation-name"_ns,
|
||||
SetProp(rule, mDocument, eCSSProperty_animation_name,
|
||||
"-ua-view-transition-fade-in"_ns);
|
||||
}
|
||||
// If capturedElement's new element is not null, then:
|
||||
@@ -444,9 +460,21 @@ void ViewTransition::SetupTransitionPseudoElements() {
|
||||
// representing the following CSS, and append it to document’s dynamic
|
||||
// view transition style sheet:
|
||||
MOZ_ASSERT(capturedElement.mOldState.mTriedImage);
|
||||
auto* rule = EnsureRule(capturedElement.mOldRule);
|
||||
SetProp(rule, mDocument, "animation-name"_ns,
|
||||
"-ua-view-transition-fade-out"_ns);
|
||||
SetProp(EnsureRule(capturedElement.mOldRule), mDocument,
|
||||
eCSSProperty_animation_name, "-ua-view-transition-fade-out"_ns);
|
||||
|
||||
// Moved around from "update pseudo-element styles" because it's a one
|
||||
// time operation.
|
||||
auto* rule = EnsureRule(capturedElement.mGroupRule);
|
||||
auto oldRect = CSSPixel::FromAppUnits(capturedElement.mOldState.mSize);
|
||||
SetProp(rule, mDocument, eCSSProperty_width, oldRect.width,
|
||||
eCSSUnit_Pixel);
|
||||
SetProp(rule, mDocument, eCSSProperty_height, oldRect.height,
|
||||
eCSSUnit_Pixel);
|
||||
SetProp(rule, mDocument, eCSSProperty_transform,
|
||||
capturedElement.mOldState.mTransform);
|
||||
// TODO: writing-mode, direction, text-orientation, mix-blend-mode,
|
||||
// backdrop-filter, color-scheme.
|
||||
}
|
||||
// If both of capturedElement's old image and new element are not null,
|
||||
// then:
|
||||
@@ -458,18 +486,20 @@ void ViewTransition::SetupTransitionPseudoElements() {
|
||||
// TODO(emilio): Group keyframes.
|
||||
// Set capturedElement's group animation name rule to ...
|
||||
SetProp(EnsureRule(capturedElement.mGroupRule), mDocument,
|
||||
"animation-name"_ns, dynamicAnimationName);
|
||||
eCSSProperty_animation_name, dynamicAnimationName);
|
||||
|
||||
// Set capturedElement's image pair isolation rule to ...
|
||||
SetProp(EnsureRule(capturedElement.mImagePairRule), mDocument,
|
||||
"isolation"_ns, "isolate"_ns);
|
||||
eCSSProperty_isolation, "isolate"_ns);
|
||||
|
||||
// Set capturedElement's image animation name rule to ...
|
||||
SetProp(
|
||||
EnsureRule(capturedElement.mOldRule), mDocument, "animation-name"_ns,
|
||||
EnsureRule(capturedElement.mOldRule), mDocument,
|
||||
eCSSProperty_animation_name,
|
||||
"-ua-view-transition-fade-out, -ua-mix-blend-mode-plus-lighter"_ns);
|
||||
SetProp(
|
||||
EnsureRule(capturedElement.mNewRule), mDocument, "animation-name"_ns,
|
||||
EnsureRule(capturedElement.mNewRule), mDocument,
|
||||
eCSSProperty_animation_name,
|
||||
"-ua-view-transition-fade-in, -ua-mix-blend-mode-plus-lighter"_ns);
|
||||
}
|
||||
}
|
||||
@@ -484,6 +514,60 @@ void ViewTransition::SetupTransitionPseudoElements() {
|
||||
}
|
||||
}
|
||||
|
||||
// https://drafts.csswg.org/css-view-transitions-1/#style-transition-pseudo-elements-algorithm
|
||||
bool ViewTransition::UpdatePseudoElementStyles(bool aNeedsInvalidation) {
|
||||
// 1. For each transitionName -> capturedElement of transition's "named
|
||||
// elements".
|
||||
for (auto& entry : mNamedElements) {
|
||||
nsAtom* transitionName = entry.GetKey();
|
||||
CapturedElement& capturedElement = *entry.GetData();
|
||||
// If capturedElement's new element is null, then:
|
||||
// We already did this in SetupTransitionPseudoElements().
|
||||
if (!capturedElement.mNewElement) {
|
||||
continue;
|
||||
}
|
||||
// Otherwise.
|
||||
// Return failure if any of the following conditions is true:
|
||||
// * capturedElement's new element has a flat tree ancestor that skips its
|
||||
// contents.
|
||||
// * capturedElement's new element is not rendered.
|
||||
// * capturedElement has more than one box fragment.
|
||||
nsIFrame* frame = capturedElement.mNewElement->GetPrimaryFrame();
|
||||
if (!frame || frame->IsHiddenByContentVisibilityOnAnyAncestor() ||
|
||||
frame->GetPrevContinuation() || frame->GetNextContinuation()) {
|
||||
return false;
|
||||
}
|
||||
auto* rule = EnsureRule(capturedElement.mGroupRule);
|
||||
// Let newRect be snapshot containing block if capturedElement is the
|
||||
// document element, otherwise, capturedElement’s border box.
|
||||
auto newRect = frame->Style()->IsRootElementStyle()
|
||||
? SnapshotContainingBlockRect()
|
||||
: frame->GetRect();
|
||||
auto size = CSSPixel::FromAppUnits(newRect);
|
||||
// NOTE(emilio): Intentionally not short-circuiting. Int cast is needed to
|
||||
// silence warning.
|
||||
bool changed = int(SetProp(rule, mDocument, eCSSProperty_width, size.width,
|
||||
eCSSUnit_Pixel)) |
|
||||
SetProp(rule, mDocument, eCSSProperty_height, size.height,
|
||||
eCSSUnit_Pixel) |
|
||||
SetProp(rule, mDocument, eCSSProperty_transform,
|
||||
EffectiveTransform(frame));
|
||||
// TODO: writing-mode, direction, text-orientation, mix-blend-mode,
|
||||
// backdrop-filter, color-scheme.
|
||||
if (changed && aNeedsInvalidation) {
|
||||
auto* pseudo = FindPseudo(PseudoStyleRequest(
|
||||
PseudoStyleType::viewTransitionGroup, transitionName));
|
||||
MOZ_ASSERT(pseudo);
|
||||
// TODO(emilio): Maybe we need something more than recascade? But I don't
|
||||
// see how off-hand.
|
||||
nsLayoutUtils::PostRestyleEvent(pseudo, RestyleHint::RECASCADE_SELF,
|
||||
nsChangeHint(0));
|
||||
}
|
||||
// 5. TODO(emilio): Live capturing (probably nothing to do here)
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// https://drafts.csswg.org/css-view-transitions-1/#activate-view-transition
|
||||
void ViewTransition::Activate() {
|
||||
// Step 1: If transition's phase is "done", then return.
|
||||
@@ -513,7 +597,15 @@ void ViewTransition::Activate() {
|
||||
// Step 6: Setup transition pseudo-elements for transition.
|
||||
SetupTransitionPseudoElements();
|
||||
|
||||
// TODO(emilio): Step 7.
|
||||
// Step 7: Update pseudo-element styles for transition.
|
||||
// We don't need to invalidate the pseudo-element styles since we just
|
||||
// generated them.
|
||||
if (!UpdatePseudoElementStyles(/* aNeedsInvalidation = */ false)) {
|
||||
// If failure is returned, then skip the view transition for transition
|
||||
// with an "InvalidStateError" DOMException in transition's relevant Realm,
|
||||
// and return.
|
||||
return SkipTransition(SkipTransitionReason::PseudoUpdateFailure);
|
||||
}
|
||||
|
||||
// Step 8: Set transition's phase to "animating".
|
||||
mPhase = Phase::Animating;
|
||||
@@ -551,6 +643,69 @@ nsRect ViewTransition::SnapshotContainingBlockRect() const {
|
||||
return pc ? pc->GetVisibleArea() : nsRect();
|
||||
}
|
||||
|
||||
Element* ViewTransition::FindPseudo(const PseudoStyleRequest& aRequest) const {
|
||||
Element* root = GetRoot();
|
||||
if (!root) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (aRequest.mType == PseudoStyleType::viewTransition) {
|
||||
return root;
|
||||
}
|
||||
|
||||
// Linear search ::view-transition-group by |aRequest.mIdentifier|.
|
||||
// Note: perhaps we can add a hashtable to improve the performance if it's
|
||||
// common that there are a lot of view-transition-names.
|
||||
Element* group = root->GetFirstElementChild();
|
||||
for (; group; group = group->GetNextElementSibling()) {
|
||||
MOZ_ASSERT(group->HasName(),
|
||||
"The generated ::view-transition-group() should have a name");
|
||||
nsAtom* name = group->GetParsedAttr(nsGkAtoms::name)->GetAtomValue();
|
||||
if (name == aRequest.mIdentifier) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// No one specifies view-transition-name or we mismatch all names.
|
||||
if (!group) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (aRequest.mType == PseudoStyleType::viewTransitionGroup) {
|
||||
return group;
|
||||
}
|
||||
|
||||
Element* imagePair = group->GetFirstElementChild();
|
||||
MOZ_ASSERT(imagePair, "::view-transition-image-pair() should exist always");
|
||||
if (aRequest.mType == PseudoStyleType::viewTransitionImagePair) {
|
||||
return imagePair;
|
||||
}
|
||||
|
||||
Element* child = imagePair->GetFirstElementChild();
|
||||
// Neither ::view-transition-old() nor ::view-transition-new() doesn't exist.
|
||||
if (!child) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Check if the first element matches our request.
|
||||
const PseudoStyleType type = child->GetPseudoElementType();
|
||||
if (type == aRequest.mType) {
|
||||
return child;
|
||||
}
|
||||
|
||||
// Since the second child is either ::view-transition-new() or nullptr, so we
|
||||
// can reject viewTransitionOld request here.
|
||||
if (aRequest.mType == PseudoStyleType::viewTransitionOld) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
child = child->GetNextElementSibling();
|
||||
MOZ_ASSERT(aRequest.mType == PseudoStyleType::viewTransitionNew);
|
||||
MOZ_ASSERT(!child || !child->GetNextElementSibling(),
|
||||
"No more psuedo elements in this subtree");
|
||||
return child;
|
||||
}
|
||||
|
||||
const StyleLockedDeclarationBlock* ViewTransition::GetDynamicRuleFor(
|
||||
const Element& aElement) const {
|
||||
if (!aElement.HasName()) {
|
||||
@@ -757,11 +912,20 @@ void ViewTransition::HandleFrame() {
|
||||
}
|
||||
return;
|
||||
}
|
||||
// If the view tranimation is still animating after HandleFrame(),
|
||||
// we have to periodically perform operations to check if it is still
|
||||
// animating in the following ticks.
|
||||
// TODO(emilio): Step 5 (check CB resize)
|
||||
|
||||
// Step 6: Update pseudo-element styles for transition.
|
||||
if (!UpdatePseudoElementStyles(/* aNeedsInvalidation= */ true)) {
|
||||
// If failure is returned, then skip the view transition for transition
|
||||
// with an "InvalidStateError" DOMException in transition's relevant Realm,
|
||||
// and return.
|
||||
return SkipTransition(SkipTransitionReason::PseudoUpdateFailure);
|
||||
}
|
||||
|
||||
// If the view transition is still animating after HandleFrame(), we have to
|
||||
// periodically perform operations to check if it is still animating in the
|
||||
// following ticks.
|
||||
mDocument->EnsureViewTransitionOperationsHappen();
|
||||
// TODO(emilio): Steps 5-6 (check CB size, update pseudo styles).
|
||||
}
|
||||
|
||||
static bool CheckForActiveAnimationsForEachPseudo(
|
||||
@@ -978,6 +1142,10 @@ void ViewTransition::SkipTransition(
|
||||
readyPromise->MaybeRejectWithInvalidStateError(
|
||||
"Skipped view transition due to viewport resize");
|
||||
break;
|
||||
case SkipTransitionReason::PseudoUpdateFailure:
|
||||
readyPromise->MaybeRejectWithInvalidStateError(
|
||||
"Skipped view transition due to hidden new element");
|
||||
break;
|
||||
case SkipTransitionReason::UpdateCallbackRejected:
|
||||
readyPromise->MaybeReject(aUpdateCallbackRejectReason);
|
||||
break;
|
||||
|
||||
@@ -17,6 +17,7 @@ class nsITimer;
|
||||
namespace mozilla {
|
||||
|
||||
class ErrorResult;
|
||||
struct PseudoStyleRequest;
|
||||
struct StyleLockedDeclarationBlock;
|
||||
|
||||
namespace gfx {
|
||||
@@ -38,6 +39,7 @@ enum class SkipTransitionReason : uint8_t {
|
||||
UpdateCallbackRejected,
|
||||
DuplicateTransitionNameCapturingOldState,
|
||||
DuplicateTransitionNameCapturingNewState,
|
||||
PseudoUpdateFailure,
|
||||
Resize,
|
||||
};
|
||||
|
||||
@@ -68,6 +70,8 @@ class ViewTransition final : public nsISupports, public nsWrapperCache {
|
||||
Element* GetRoot() const { return mViewTransitionRoot; }
|
||||
gfx::DataSourceSurface* GetOldSurface(nsAtom* aName) const;
|
||||
|
||||
Element* FindPseudo(const PseudoStyleRequest&) const;
|
||||
|
||||
const StyleLockedDeclarationBlock* GetDynamicRuleFor(const Element&) const;
|
||||
|
||||
nsIGlobalObject* GetParentObject() const;
|
||||
@@ -87,6 +91,7 @@ class ViewTransition final : public nsISupports, public nsWrapperCache {
|
||||
[[nodiscard]] Maybe<SkipTransitionReason> CaptureOldState();
|
||||
[[nodiscard]] Maybe<SkipTransitionReason> CaptureNewState();
|
||||
void SetupTransitionPseudoElements();
|
||||
[[nodiscard]] bool UpdatePseudoElementStyles(bool aNeedsInvalidation);
|
||||
void ClearNamedElements();
|
||||
void HandleFrame();
|
||||
bool CheckForActiveAnimations() const;
|
||||
|
||||
@@ -172,6 +172,11 @@ trait ClosureHelper {
|
||||
fn invoke(&self, property_id: Option<NonCustomPropertyId>);
|
||||
}
|
||||
|
||||
const NO_MUTATION_CLOSURE : DeclarationBlockMutationClosure = DeclarationBlockMutationClosure {
|
||||
data: std::ptr::null_mut(),
|
||||
function: None,
|
||||
};
|
||||
|
||||
impl ClosureHelper for DeclarationBlockMutationClosure {
|
||||
#[inline]
|
||||
fn invoke(&self, property_id: Option<NonCustomPropertyId>) {
|
||||
@@ -5599,7 +5604,7 @@ pub extern "C" fn Servo_DeclarationBlock_SetLengthValue(
|
||||
property: nsCSSPropertyID,
|
||||
value: f32,
|
||||
unit: structs::nsCSSUnit,
|
||||
) {
|
||||
) -> bool {
|
||||
use style::properties::PropertyDeclaration;
|
||||
use style::values::generics::length::{LengthPercentageOrAuto, Size};
|
||||
use style::values::generics::NonNegative;
|
||||
@@ -5660,7 +5665,7 @@ pub extern "C" fn Servo_DeclarationBlock_SetLengthValue(
|
||||
_ => unreachable!("Unknown unit passed to SetLengthValue"),
|
||||
};
|
||||
|
||||
let prop = match_wrap_declared! { long,
|
||||
let mut source_declarations = SourcePropertyDeclaration::with_one(match_wrap_declared! { long,
|
||||
Width => Size::LengthPercentage(NonNegative(LengthPercentage::Length(nocalc))),
|
||||
Height => Size::LengthPercentage(NonNegative(LengthPercentage::Length(nocalc))),
|
||||
X => LengthPercentage::Length(nocalc),
|
||||
@@ -5671,10 +5676,14 @@ pub extern "C" fn Servo_DeclarationBlock_SetLengthValue(
|
||||
Rx => LengthPercentageOrAuto::LengthPercentage(NonNegative(LengthPercentage::Length(nocalc))),
|
||||
Ry => LengthPercentageOrAuto::LengthPercentage(NonNegative(LengthPercentage::Length(nocalc))),
|
||||
FontSize => FontSize::Length(LengthPercentage::Length(nocalc)),
|
||||
};
|
||||
write_locked_arc(declarations, |decls: &mut PropertyDeclarationBlock| {
|
||||
decls.push(prop, Importance::Normal);
|
||||
})
|
||||
});
|
||||
set_property_to_declarations(
|
||||
Some(long.into()),
|
||||
declarations,
|
||||
&mut source_declarations,
|
||||
NO_MUTATION_CLOSURE,
|
||||
Importance::Normal,
|
||||
)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -5682,7 +5691,7 @@ pub extern "C" fn Servo_DeclarationBlock_SetTransform(
|
||||
declarations: &LockedDeclarationBlock,
|
||||
property: nsCSSPropertyID,
|
||||
ops: &nsTArray<computed::TransformOperation>,
|
||||
) {
|
||||
) -> bool {
|
||||
use style::values::generics::transform::GenericTransform;
|
||||
use style::properties::PropertyDeclaration;
|
||||
let long = get_longhand_from_id!(property);
|
||||
@@ -5690,9 +5699,14 @@ pub extern "C" fn Servo_DeclarationBlock_SetTransform(
|
||||
let prop = match_wrap_declared! { long,
|
||||
Transform => v,
|
||||
};
|
||||
write_locked_arc(declarations, |decls: &mut PropertyDeclarationBlock| {
|
||||
decls.push(prop, Importance::Normal);
|
||||
})
|
||||
let mut source_declarations = SourcePropertyDeclaration::with_one(prop);
|
||||
set_property_to_declarations(
|
||||
Some(long.into()),
|
||||
declarations,
|
||||
&mut source_declarations,
|
||||
NO_MUTATION_CLOSURE,
|
||||
Importance::Normal,
|
||||
)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
|
||||
Reference in New Issue
Block a user