Bug 1947746 - View transition dynamic style rules. r=view-transitions-reviewers,firefox-style-system-reviewers,boris

This should be rather uncontroversial. I filed bug 1947747 for inspector
support for this (which should work similarly than for presentational
hints).

Differential Revision: https://phabricator.services.mozilla.com/D237886
This commit is contained in:
Emilio Cobos Álvarez
2025-02-13 09:57:35 +00:00
parent 5f353ee586
commit edf65b2923
8 changed files with 162 additions and 27 deletions

View File

@@ -86,6 +86,9 @@ static RefPtr<gfx::DataSourceSurface> CaptureFallbackSnapshot(
struct CapturedElementOldState {
RefPtr<gfx::DataSourceSurface> mImage;
// Whether we tried to capture an image. Note we might fail to get a
// snapshot, so this might not be the same as !!mImage.
bool mTriedImage = false;
// Encompasses width and height.
nsSize mSize;
@@ -99,6 +102,7 @@ struct CapturedElementOldState {
CapturedElementOldState(nsIFrame* aFrame,
const nsSize& aSnapshotContainingBlockSize)
: mImage(CaptureFallbackSnapshot(aFrame)),
mTriedImage(true),
mSize(aFrame->Style()->IsRootElementStyle()
? aSnapshotContainingBlockSize
: aFrame->GetRect().Size()),
@@ -121,8 +125,15 @@ struct ViewTransition::CapturedElement {
CapturedElement(nsIFrame* aFrame, const nsSize& aSnapshotContainingBlockSize)
: mOldState(aFrame, aSnapshotContainingBlockSize) {}
// TODO: Style definitions as per:
// https://drafts.csswg.org/css-view-transitions/#captured-element-style-definitions
// https://drafts.csswg.org/css-view-transitions-1/#captured-element-style-definitions
// The group animation-name rule and group styles rule, merged into one.
RefPtr<StyleLockedDeclarationBlock> mGroupRule;
// The image pair isolation rule.
RefPtr<StyleLockedDeclarationBlock> mImagePairRule;
// The rules for ::view-transition-old(<name>).
RefPtr<StyleLockedDeclarationBlock> mOldRule;
// The rules for ::view-transition-new(<name>).
RefPtr<StyleLockedDeclarationBlock> mNewRule;
};
static inline void ImplCycleCollectionTraverse(
@@ -153,7 +164,7 @@ ViewTransition::ViewTransition(Document& aDoc,
ViewTransition::~ViewTransition() { ClearTimeoutTimer(); }
gfx::DataSourceSurface* ViewTransition::GetOldSurface(nsAtom* aName) const {
auto el = mNamedElements.Get(aName);
auto* el = mNamedElements.Get(aName);
if (NS_WARN_IF(!el)) {
return nullptr;
}
@@ -339,6 +350,23 @@ 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,
/* is_important = */ false, aDoc->DefaultStyleAttrURLData(),
StyleParsingMode::DEFAULT, eCompatibility_FullStandards,
aDoc->CSSLoader(), StyleCssRuleType::Style, {});
}
static StyleLockedDeclarationBlock* EnsureRule(
RefPtr<StyleLockedDeclarationBlock>& aRule) {
if (!aRule) {
aRule = Servo_DeclarationBlock_CreateEmpty().Consume();
}
return aRule.get();
}
// https://drafts.csswg.org/css-view-transitions-1/#setup-transition-pseudo-elements
void ViewTransition::SetupTransitionPseudoElements() {
MOZ_ASSERT(!mViewTransitionRoot);
@@ -370,7 +398,7 @@ void ViewTransition::SetupTransitionPseudoElements() {
constexpr bool kNotify = false;
nsAtom* transitionName = entry.GetKey();
const CapturedElement& capturedElement = *entry.GetData();
CapturedElement& capturedElement = *entry.GetData();
// Let group be a new ::view-transition-group(), with its view transition
// name set to transitionName.
RefPtr<Element> group = MakePseudo(
@@ -384,7 +412,7 @@ void ViewTransition::SetupTransitionPseudoElements() {
// Append imagePair to group.
group->AppendChildTo(imagePair, kNotify, IgnoreErrors());
// If capturedElement's old image is not null, then:
if (capturedElement.mOldState.mImage) {
if (capturedElement.mOldState.mTriedImage) {
// Let old be a new ::view-transition-old(), with its view transition
// name set to transitionName, displaying capturedElement's old image as
// its replaced content.
@@ -392,6 +420,14 @@ void ViewTransition::SetupTransitionPseudoElements() {
*mDocument, PseudoStyleType::viewTransitionOld, transitionName);
// Append old to imagePair.
imagePair->AppendChildTo(old, kNotify, IgnoreErrors());
} else {
// Moved around for simplicity. If capturedElement's old image is null,
// then: Assert: capturedElement's new element is not null.
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,
"-ua-view-transition-fade-in"_ns);
}
// If capturedElement's new element is not null, then:
if (capturedElement.mNewElement) {
@@ -401,9 +437,41 @@ void ViewTransition::SetupTransitionPseudoElements() {
*mDocument, PseudoStyleType::viewTransitionNew, transitionName);
// Append new to imagePair.
imagePair->AppendChildTo(new_, kNotify, IgnoreErrors());
} else {
// Moved around from the next step for simplicity.
// Assert: capturedElement's old image is not null.
// Set capturedElement's image animation name rule to a new CSSStyleRule
// representing the following CSS, and append it to documents 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);
}
// If both of capturedElement's old image and new element are not null,
// then:
if (capturedElement.mOldState.mTriedImage && capturedElement.mNewElement) {
nsAutoCString dynamicAnimationName;
transitionName->ToUTF8String(dynamicAnimationName);
dynamicAnimationName.InsertLiteral("-ua-view-transition-group-anim-", 0);
// TODO(emilio): Group keyframes.
// Set capturedElement's group animation name rule to ...
SetProp(EnsureRule(capturedElement.mGroupRule), mDocument,
"animation-name"_ns, dynamicAnimationName);
// Set capturedElement's image pair isolation rule to ...
SetProp(EnsureRule(capturedElement.mImagePairRule), mDocument,
"isolation"_ns, "isolate"_ns);
// Set capturedElement's image animation name rule to ...
SetProp(
EnsureRule(capturedElement.mOldRule), mDocument, "animation-name"_ns,
"-ua-view-transition-fade-out, -ua-mix-blend-mode-plus-lighter"_ns);
SetProp(
EnsureRule(capturedElement.mNewRule), mDocument, "animation-name"_ns,
"-ua-view-transition-fade-in, -ua-mix-blend-mode-plus-lighter"_ns);
}
// TODO(emilio): Dynamic UA sheet shenanigans. Seems we could have a custom
// element class with a transition-specific
}
BindContext context(*docElement, BindContext::ForNativeAnonymous);
if (NS_FAILED(mViewTransitionRoot->BindToTree(context, *docElement))) {
@@ -483,6 +551,31 @@ nsRect ViewTransition::SnapshotContainingBlockRect() const {
return pc ? pc->GetVisibleArea() : nsRect();
}
const StyleLockedDeclarationBlock* ViewTransition::GetDynamicRuleFor(
const Element& aElement) const {
if (!aElement.HasName()) {
return nullptr;
}
nsAtom* name = aElement.GetParsedAttr(nsGkAtoms::name)->GetAtomValue();
auto* capture = mNamedElements.Get(name);
if (!capture) {
return nullptr;
}
switch (aElement.GetPseudoElementType()) {
case PseudoStyleType::viewTransitionNew:
return capture->mNewRule.get();
case PseudoStyleType::viewTransitionOld:
return capture->mOldRule.get();
case PseudoStyleType::viewTransitionImagePair:
return capture->mImagePairRule.get();
case PseudoStyleType::viewTransitionGroup:
return capture->mGroupRule.get();
default:
return nullptr;
}
}
// FIXME(emilio): This should actually iterate in paint order.
template <typename Callback>
static bool ForEachChildFrame(nsIFrame* aFrame, const Callback& aCb) {
@@ -663,13 +756,11 @@ void ViewTransition::HandleFrame() {
finished->MaybeResolveWithUndefined();
}
return;
} else {
// 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.
mDocument->EnsureViewTransitionOperationsHappen();
}
// 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.
mDocument->EnsureViewTransitionOperationsHappen();
// TODO(emilio): Steps 5-6 (check CB size, update pseudo styles).
}

View File

@@ -9,6 +9,7 @@
#include "nsWrapperCache.h"
#include "nsAtomHashKeys.h"
#include "nsClassHashtable.h"
#include "nsRefPtrHashtable.h"
class nsIGlobalObject;
class nsITimer;
@@ -16,6 +17,7 @@ class nsITimer;
namespace mozilla {
class ErrorResult;
struct StyleLockedDeclarationBlock;
namespace gfx {
class DataSourceSurface;
@@ -66,6 +68,8 @@ class ViewTransition final : public nsISupports, public nsWrapperCache {
Element* GetRoot() const { return mViewTransitionRoot; }
gfx::DataSourceSurface* GetOldSurface(nsAtom* aName) const;
const StyleLockedDeclarationBlock* GetDynamicRuleFor(const Element&) const;
nsIGlobalObject* GetParentObject() const;
JSObject* WrapObject(JSContext*, JS::Handle<JSObject*> aGivenProto) override;

View File

@@ -25,6 +25,7 @@
#include "nsLayoutUtils.h"
#include "nsIContentInlines.h"
#include "mozilla/dom/DocumentInlines.h"
#include "mozilla/dom/ViewTransition.h"
#include "nsILoadContext.h"
#include "nsIFrame.h"
#include "nsINode.h"
@@ -407,6 +408,15 @@ Gecko_GetHTMLPresentationAttrDeclarationBlock(const Element* aElement) {
return aElement->GetMappedAttributeStyle();
}
const StyleLockedDeclarationBlock* Gecko_GetViewTransitionDynamicRule(
const Element* aElement) {
const auto* vt = aElement->OwnerDoc()->GetActiveViewTransition();
if (!vt) {
return nullptr;
}
return vt->GetDynamicRuleFor(*aElement);
}
const StyleLockedDeclarationBlock* Gecko_GetExtraContentStyleDeclarations(
const Element* aElement) {
if (const auto* cell = HTMLTableCellElement::FromNode(aElement)) {

View File

@@ -180,6 +180,9 @@ const mozilla::StyleLockedDeclarationBlock* Gecko_GetStyleAttrDeclarationBlock(
void Gecko_UnsetDirtyStyleAttr(const mozilla::dom::Element* element);
const mozilla::StyleLockedDeclarationBlock* Gecko_GetViewTransitionDynamicRule(
const mozilla::dom::Element* element);
const mozilla::StyleLockedDeclarationBlock*
Gecko_GetHTMLPresentationAttrDeclarationBlock(
const mozilla::dom::Element* element);

View File

@@ -893,6 +893,14 @@ pub trait TElement:
) where
V: Push<ApplicableDeclarationBlock>;
/// Generate the proper applicable declarations due to view transition dynamic rules, and
/// insert them into `rules`.
/// https://drafts.csswg.org/css-view-transitions-1/#document-dynamic-view-transition-style-sheet
fn synthesize_view_transition_dynamic_rules<V>(&self, _rules: &mut V)
where
V: Push<ApplicableDeclarationBlock>
{}
/// Returns element's local name.
fn local_name(&self) -> &<SelectorImpl as selectors::parser::SelectorImpl>::BorrowedLocalName;

View File

@@ -1683,6 +1683,22 @@ impl<'le> TElement for GeckoElement<'le> {
unsafe { bindings::Gecko_IsDocumentBody(self.0) }
}
fn synthesize_view_transition_dynamic_rules<V>(&self, rules: &mut V)
where
V: Push<ApplicableDeclarationBlock>,
{
use crate::stylesheets::layer_rule::LayerOrder;
let declarations =
unsafe { bindings::Gecko_GetViewTransitionDynamicRule(self.0).as_ref() };
if let Some(decl) = declarations {
rules.push(ApplicableDeclarationBlock::from_declarations(
unsafe { Arc::from_raw_addrefed(decl) },
ServoCascadeLevel::UANormal,
LayerOrder::root(),
));
}
}
fn synthesize_presentational_hints_for_legacy_attributes<V>(
&self,
visited_handling: VisitedHandlingMode,

View File

@@ -174,6 +174,20 @@ where
fn collect_user_agent_rules(&mut self) {
self.collect_stylist_rules(Origin::UserAgent);
self.collect_view_transition_dynamic_rules();
}
fn collect_view_transition_dynamic_rules(&mut self) {
if !self.pseudo_element.is_some_and(|p| p.is_named_view_transition()) {
return;
}
let len_before_vt_rules = self.rules.len();
self.element.synthesize_view_transition_dynamic_rules(self.rules);
if cfg!(debug_assertions) && self.rules.len() != len_before_vt_rules {
for declaration in &self.rules[len_before_vt_rules..] {
assert_eq!(declaration.level(), CascadeLevel::UANormal);
}
}
}
fn collect_user_rules(&mut self) {
@@ -199,11 +213,9 @@ where
self.context.visited_handling(),
self.rules,
);
if cfg!(debug_assertions) {
if self.rules.len() != length_before_preshints {
for declaration in &self.rules[length_before_preshints..] {
assert_eq!(declaration.level(), CascadeLevel::PresHints);
}
if cfg!(debug_assertions) && self.rules.len() != length_before_preshints {
for declaration in &self.rules[length_before_preshints..] {
assert_eq!(declaration.level(), CascadeLevel::PresHints);
}
}
}

View File

@@ -1,9 +0,0 @@
[dynamic-stylesheet-animations.html]
[Both old and new snapshots]
expected: FAIL
[Only old snapshot]
expected: FAIL
[Only new snapshot]
expected: FAIL