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:
Emilio Cobos Álvarez
2025-02-14 07:50:18 +00:00
parent 88524ddb48
commit ecc0677523
4 changed files with 215 additions and 87 deletions

View File

@@ -4630,66 +4630,7 @@ static Element* SearchViewTransitionPseudo(const Element* aElement,
return nullptr; return nullptr;
} }
Element* root = vt->GetRoot(); return vt->FindPseudo(aRequest);
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;
} }
Element* Element::GetPseudoElement(const PseudoStyleRequest& aRequest) const { Element* Element::GetPseudoElement(const PseudoStyleRequest& aRequest) const {

View File

@@ -350,15 +350,31 @@ static already_AddRefed<Element> MakePseudo(Document& aDoc,
return el.forget(); return el.forget();
} }
static void SetProp(StyleLockedDeclarationBlock* aDecls, Document* aDoc, static bool SetProp(StyleLockedDeclarationBlock* aDecls, Document* aDoc,
const nsACString& aProp, const nsACString& aValue) { nsCSSPropertyID aProp, const nsACString& aValue) {
Servo_DeclarationBlock_SetProperty( return Servo_DeclarationBlock_SetPropertyById(
aDecls, &aProp, &aValue, aDecls, aProp, &aValue,
/* is_important = */ false, aDoc->DefaultStyleAttrURLData(), /* is_important = */ false, aDoc->DefaultStyleAttrURLData(),
StyleParsingMode::DEFAULT, eCompatibility_FullStandards, StyleParsingMode::DEFAULT, eCompatibility_FullStandards,
aDoc->CSSLoader(), StyleCssRuleType::Style, {}); 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( static StyleLockedDeclarationBlock* EnsureRule(
RefPtr<StyleLockedDeclarationBlock>& aRule) { RefPtr<StyleLockedDeclarationBlock>& aRule) {
if (!aRule) { if (!aRule) {
@@ -426,7 +442,7 @@ void ViewTransition::SetupTransitionPseudoElements() {
MOZ_ASSERT(capturedElement.mNewElement); MOZ_ASSERT(capturedElement.mNewElement);
// Set capturedElement's image animation name rule to a new ... // Set capturedElement's image animation name rule to a new ...
auto* rule = EnsureRule(capturedElement.mNewRule); auto* rule = EnsureRule(capturedElement.mNewRule);
SetProp(rule, mDocument, "animation-name"_ns, SetProp(rule, mDocument, eCSSProperty_animation_name,
"-ua-view-transition-fade-in"_ns); "-ua-view-transition-fade-in"_ns);
} }
// If capturedElement's new element is not null, then: // If capturedElement's new element is not null, then:
@@ -444,9 +460,21 @@ void ViewTransition::SetupTransitionPseudoElements() {
// representing the following CSS, and append it to documents dynamic // representing the following CSS, and append it to documents dynamic
// view transition style sheet: // view transition style sheet:
MOZ_ASSERT(capturedElement.mOldState.mTriedImage); MOZ_ASSERT(capturedElement.mOldState.mTriedImage);
auto* rule = EnsureRule(capturedElement.mOldRule); SetProp(EnsureRule(capturedElement.mOldRule), mDocument,
SetProp(rule, mDocument, "animation-name"_ns, eCSSProperty_animation_name, "-ua-view-transition-fade-out"_ns);
"-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, // If both of capturedElement's old image and new element are not null,
// then: // then:
@@ -458,18 +486,20 @@ void ViewTransition::SetupTransitionPseudoElements() {
// TODO(emilio): Group keyframes. // TODO(emilio): Group keyframes.
// Set capturedElement's group animation name rule to ... // Set capturedElement's group animation name rule to ...
SetProp(EnsureRule(capturedElement.mGroupRule), mDocument, SetProp(EnsureRule(capturedElement.mGroupRule), mDocument,
"animation-name"_ns, dynamicAnimationName); eCSSProperty_animation_name, dynamicAnimationName);
// Set capturedElement's image pair isolation rule to ... // Set capturedElement's image pair isolation rule to ...
SetProp(EnsureRule(capturedElement.mImagePairRule), mDocument, SetProp(EnsureRule(capturedElement.mImagePairRule), mDocument,
"isolation"_ns, "isolate"_ns); eCSSProperty_isolation, "isolate"_ns);
// Set capturedElement's image animation name rule to ... // Set capturedElement's image animation name rule to ...
SetProp( 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); "-ua-view-transition-fade-out, -ua-mix-blend-mode-plus-lighter"_ns);
SetProp( 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); "-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, capturedElements 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 // https://drafts.csswg.org/css-view-transitions-1/#activate-view-transition
void ViewTransition::Activate() { void ViewTransition::Activate() {
// Step 1: If transition's phase is "done", then return. // 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. // Step 6: Setup transition pseudo-elements for transition.
SetupTransitionPseudoElements(); 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". // Step 8: Set transition's phase to "animating".
mPhase = Phase::Animating; mPhase = Phase::Animating;
@@ -551,6 +643,69 @@ nsRect ViewTransition::SnapshotContainingBlockRect() const {
return pc ? pc->GetVisibleArea() : nsRect(); 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 StyleLockedDeclarationBlock* ViewTransition::GetDynamicRuleFor(
const Element& aElement) const { const Element& aElement) const {
if (!aElement.HasName()) { if (!aElement.HasName()) {
@@ -757,11 +912,20 @@ void ViewTransition::HandleFrame() {
} }
return; return;
} }
// If the view tranimation is still animating after HandleFrame(), // TODO(emilio): Step 5 (check CB resize)
// we have to periodically perform operations to check if it is still
// animating in the following ticks. // 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(); mDocument->EnsureViewTransitionOperationsHappen();
// TODO(emilio): Steps 5-6 (check CB size, update pseudo styles).
} }
static bool CheckForActiveAnimationsForEachPseudo( static bool CheckForActiveAnimationsForEachPseudo(
@@ -978,6 +1142,10 @@ void ViewTransition::SkipTransition(
readyPromise->MaybeRejectWithInvalidStateError( readyPromise->MaybeRejectWithInvalidStateError(
"Skipped view transition due to viewport resize"); "Skipped view transition due to viewport resize");
break; break;
case SkipTransitionReason::PseudoUpdateFailure:
readyPromise->MaybeRejectWithInvalidStateError(
"Skipped view transition due to hidden new element");
break;
case SkipTransitionReason::UpdateCallbackRejected: case SkipTransitionReason::UpdateCallbackRejected:
readyPromise->MaybeReject(aUpdateCallbackRejectReason); readyPromise->MaybeReject(aUpdateCallbackRejectReason);
break; break;

View File

@@ -17,6 +17,7 @@ class nsITimer;
namespace mozilla { namespace mozilla {
class ErrorResult; class ErrorResult;
struct PseudoStyleRequest;
struct StyleLockedDeclarationBlock; struct StyleLockedDeclarationBlock;
namespace gfx { namespace gfx {
@@ -38,6 +39,7 @@ enum class SkipTransitionReason : uint8_t {
UpdateCallbackRejected, UpdateCallbackRejected,
DuplicateTransitionNameCapturingOldState, DuplicateTransitionNameCapturingOldState,
DuplicateTransitionNameCapturingNewState, DuplicateTransitionNameCapturingNewState,
PseudoUpdateFailure,
Resize, Resize,
}; };
@@ -68,6 +70,8 @@ class ViewTransition final : public nsISupports, public nsWrapperCache {
Element* GetRoot() const { return mViewTransitionRoot; } Element* GetRoot() const { return mViewTransitionRoot; }
gfx::DataSourceSurface* GetOldSurface(nsAtom* aName) const; gfx::DataSourceSurface* GetOldSurface(nsAtom* aName) const;
Element* FindPseudo(const PseudoStyleRequest&) const;
const StyleLockedDeclarationBlock* GetDynamicRuleFor(const Element&) const; const StyleLockedDeclarationBlock* GetDynamicRuleFor(const Element&) const;
nsIGlobalObject* GetParentObject() const; nsIGlobalObject* GetParentObject() const;
@@ -87,6 +91,7 @@ class ViewTransition final : public nsISupports, public nsWrapperCache {
[[nodiscard]] Maybe<SkipTransitionReason> CaptureOldState(); [[nodiscard]] Maybe<SkipTransitionReason> CaptureOldState();
[[nodiscard]] Maybe<SkipTransitionReason> CaptureNewState(); [[nodiscard]] Maybe<SkipTransitionReason> CaptureNewState();
void SetupTransitionPseudoElements(); void SetupTransitionPseudoElements();
[[nodiscard]] bool UpdatePseudoElementStyles(bool aNeedsInvalidation);
void ClearNamedElements(); void ClearNamedElements();
void HandleFrame(); void HandleFrame();
bool CheckForActiveAnimations() const; bool CheckForActiveAnimations() const;

View File

@@ -172,6 +172,11 @@ trait ClosureHelper {
fn invoke(&self, property_id: Option<NonCustomPropertyId>); fn invoke(&self, property_id: Option<NonCustomPropertyId>);
} }
const NO_MUTATION_CLOSURE : DeclarationBlockMutationClosure = DeclarationBlockMutationClosure {
data: std::ptr::null_mut(),
function: None,
};
impl ClosureHelper for DeclarationBlockMutationClosure { impl ClosureHelper for DeclarationBlockMutationClosure {
#[inline] #[inline]
fn invoke(&self, property_id: Option<NonCustomPropertyId>) { fn invoke(&self, property_id: Option<NonCustomPropertyId>) {
@@ -5599,7 +5604,7 @@ pub extern "C" fn Servo_DeclarationBlock_SetLengthValue(
property: nsCSSPropertyID, property: nsCSSPropertyID,
value: f32, value: f32,
unit: structs::nsCSSUnit, unit: structs::nsCSSUnit,
) { ) -> bool {
use style::properties::PropertyDeclaration; use style::properties::PropertyDeclaration;
use style::values::generics::length::{LengthPercentageOrAuto, Size}; use style::values::generics::length::{LengthPercentageOrAuto, Size};
use style::values::generics::NonNegative; use style::values::generics::NonNegative;
@@ -5660,7 +5665,7 @@ pub extern "C" fn Servo_DeclarationBlock_SetLengthValue(
_ => unreachable!("Unknown unit passed to 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))), Width => Size::LengthPercentage(NonNegative(LengthPercentage::Length(nocalc))),
Height => Size::LengthPercentage(NonNegative(LengthPercentage::Length(nocalc))), Height => Size::LengthPercentage(NonNegative(LengthPercentage::Length(nocalc))),
X => 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))), Rx => LengthPercentageOrAuto::LengthPercentage(NonNegative(LengthPercentage::Length(nocalc))),
Ry => LengthPercentageOrAuto::LengthPercentage(NonNegative(LengthPercentage::Length(nocalc))), Ry => LengthPercentageOrAuto::LengthPercentage(NonNegative(LengthPercentage::Length(nocalc))),
FontSize => FontSize::Length(LengthPercentage::Length(nocalc)), FontSize => FontSize::Length(LengthPercentage::Length(nocalc)),
}; });
write_locked_arc(declarations, |decls: &mut PropertyDeclarationBlock| { set_property_to_declarations(
decls.push(prop, Importance::Normal); Some(long.into()),
}) declarations,
&mut source_declarations,
NO_MUTATION_CLOSURE,
Importance::Normal,
)
} }
#[no_mangle] #[no_mangle]
@@ -5682,7 +5691,7 @@ pub extern "C" fn Servo_DeclarationBlock_SetTransform(
declarations: &LockedDeclarationBlock, declarations: &LockedDeclarationBlock,
property: nsCSSPropertyID, property: nsCSSPropertyID,
ops: &nsTArray<computed::TransformOperation>, ops: &nsTArray<computed::TransformOperation>,
) { ) -> bool {
use style::values::generics::transform::GenericTransform; use style::values::generics::transform::GenericTransform;
use style::properties::PropertyDeclaration; use style::properties::PropertyDeclaration;
let long = get_longhand_from_id!(property); let long = get_longhand_from_id!(property);
@@ -5690,9 +5699,14 @@ pub extern "C" fn Servo_DeclarationBlock_SetTransform(
let prop = match_wrap_declared! { long, let prop = match_wrap_declared! { long,
Transform => v, Transform => v,
}; };
write_locked_arc(declarations, |decls: &mut PropertyDeclarationBlock| { let mut source_declarations = SourcePropertyDeclaration::with_one(prop);
decls.push(prop, Importance::Normal); set_property_to_declarations(
}) Some(long.into()),
declarations,
&mut source_declarations,
NO_MUTATION_CLOSURE,
Importance::Normal,
)
} }
#[no_mangle] #[no_mangle]