LibWeb: Implement the transition-behavior CSS property

This specifies whether transitions should be started for transitions
whose animation behavior is discrete.
This commit is contained in:
Tim Ledbetter 2025-04-17 19:11:14 +01:00 committed by Sam Atkins
commit 542c3cbe51
Notes: github-actions[bot] 2025-05-02 10:08:20 +00:00
20 changed files with 289 additions and 47 deletions

View file

@ -591,6 +591,10 @@
"stroke-box",
"view-box "
],
"transition-behavior": [
"normal",
"allow-discrete"
],
"unicode-bidi": [
"bidi-override",
"embed",

View file

@ -177,13 +177,13 @@ ValueComparingRefPtr<CSSStyleValue const> interpolate_property(DOM::Element& ele
}
// https://drafts.csswg.org/css-transitions/#transitionable
bool property_values_are_transitionable(PropertyID property_id, CSSStyleValue const& old_value, CSSStyleValue const& new_value)
bool property_values_are_transitionable(PropertyID property_id, CSSStyleValue const& old_value, CSSStyleValue const& new_value, TransitionBehavior transition_behavior)
{
// When comparing the before-change style and after-change style for a given property,
// the property values are transitionable if they have an animation type that is neither not animatable nor discrete.
auto animation_type = animation_type_from_longhand_property(property_id);
if (animation_type == AnimationType::None || animation_type == AnimationType::Discrete)
if (animation_type == AnimationType::None || (transition_behavior != TransitionBehavior::AllowDiscrete && animation_type == AnimationType::Discrete))
return false;
// FIXME: Even when a property is transitionable, the two values may not be. The spec uses the example of inset/non-inset shadows.

View file

@ -7,6 +7,7 @@
#pragma once
#include <LibWeb/CSS/CSSStyleValue.h>
#include <LibWeb/CSS/Enums.h>
#include <LibWeb/Forward.h>
namespace Web::CSS {
@ -16,7 +17,7 @@ struct CalculationContext;
ValueComparingRefPtr<CSSStyleValue const> interpolate_property(DOM::Element&, PropertyID, CSSStyleValue const& from, CSSStyleValue const& to, float delta);
// https://drafts.csswg.org/css-transitions/#transitionable
bool property_values_are_transitionable(PropertyID, CSSStyleValue const& old_value, CSSStyleValue const& new_value);
bool property_values_are_transitionable(PropertyID, CSSStyleValue const& old_value, CSSStyleValue const& new_value, TransitionBehavior transition_behavior);
NonnullRefPtr<CSSStyleValue const> interpolate_value(DOM::Element&, CalculationContext const&, CSSStyleValue const& from, CSSStyleValue const& to, float delta);
NonnullRefPtr<CSSStyleValue const> interpolate_repeatable_list(DOM::Element&, CalculationContext const&, CSSStyleValue const& from, CSSStyleValue const& to, float delta);

View file

@ -73,6 +73,7 @@
"all-petite-caps",
"all-scroll",
"all-small-caps",
"allow-discrete",
"alpha",
"alternate",
"alternate-reverse",

View file

@ -3724,7 +3724,7 @@ RefPtr<CSSStyleValue const> Parser::parse_transition_value(TokenStream<Component
while (tokens.has_next_token()) {
TransitionStyleValue::Transition transition;
auto time_value_count = 0;
bool transition_behavior_found = false;
while (tokens.has_next_token() && !tokens.next_token().is(Token::Type::Comma)) {
if (auto maybe_time = parse_time(tokens); maybe_time.has_value()) {
auto time = maybe_time.release_value();
@ -3757,6 +3757,14 @@ RefPtr<CSSStyleValue const> Parser::parse_transition_value(TokenStream<Component
continue;
}
if (!transition_behavior_found && (tokens.peek_token().is_ident("normal"sv) || tokens.peek_token().is_ident("allow-discrete"sv))) {
transition_behavior_found = true;
auto ident = tokens.consume_a_token().token().ident();
if (ident == "allow-discrete"sv)
transition.transition_behavior = TransitionBehavior::AllowDiscrete;
continue;
}
if (auto token = tokens.peek_token(); token.is_ident("all"sv)) {
auto transition_keyword = parse_keyword_value(tokens);
VERIFY(transition_keyword->to_keyword() == Keyword::All);
@ -3775,10 +3783,10 @@ RefPtr<CSSStyleValue const> Parser::parse_transition_value(TokenStream<Component
}
auto custom_ident = transition_property->custom_ident();
if (auto property = property_id_from_string(custom_ident); property.has_value())
if (auto property = property_id_from_string(custom_ident); property.has_value()) {
transition.property_name = CustomIdentStyleValue::create(custom_ident);
continue;
continue;
}
}
dbgln_if(CSS_PARSER_DEBUG, "Transition property has unexpected token \"{}\"", tokens.next_token().to_string());

View file

@ -2986,7 +2986,17 @@
"transition-property",
"transition-duration",
"transition-timing-function",
"transition-delay"
"transition-delay",
"transition-behavior"
]
},
"transition-behavior": {
"affects-layout": false,
"animation-type": "none",
"inherited": false,
"initial": "normal",
"valid-types": [
"transition-behavior"
]
},
"transition-delay": {

View file

@ -913,22 +913,25 @@ void StyleComputer::for_each_property_expanding_shorthands(PropertyID property_i
set_longhand_property(CSS::PropertyID::TransitionDuration, TimeStyleValue::create(CSS::Time::make_seconds(0)));
set_longhand_property(CSS::PropertyID::TransitionDelay, TimeStyleValue::create(CSS::Time::make_seconds(0)));
set_longhand_property(CSS::PropertyID::TransitionTimingFunction, EasingStyleValue::create(EasingStyleValue::CubicBezier::ease()));
set_longhand_property(CSS::PropertyID::TransitionBehavior, CSSKeywordValue::create(Keyword::Normal));
return;
}
auto const& transitions = value.as_transition().transitions();
Array<Vector<ValueComparingNonnullRefPtr<CSSStyleValue const>>, 4> transition_values;
Array<Vector<ValueComparingNonnullRefPtr<CSSStyleValue const>>, 5> transition_values;
for (auto const& transition : transitions) {
transition_values[0].append(*transition.property_name);
transition_values[1].append(transition.duration.as_style_value());
transition_values[2].append(transition.delay.as_style_value());
if (transition.easing)
transition_values[3].append(*transition.easing);
transition_values[4].append(CSSKeywordValue::create(to_keyword(transition.transition_behavior)));
}
set_longhand_property(CSS::PropertyID::TransitionProperty, StyleValueList::create(move(transition_values[0]), StyleValueList::Separator::Comma));
set_longhand_property(CSS::PropertyID::TransitionDuration, StyleValueList::create(move(transition_values[1]), StyleValueList::Separator::Comma));
set_longhand_property(CSS::PropertyID::TransitionDelay, StyleValueList::create(move(transition_values[2]), StyleValueList::Separator::Comma));
set_longhand_property(CSS::PropertyID::TransitionTimingFunction, StyleValueList::create(move(transition_values[3]), StyleValueList::Separator::Comma));
set_longhand_property(CSS::PropertyID::TransitionBehavior, StyleValueList::create(move(transition_values[4]), StyleValueList::Separator::Comma));
return;
}
@ -1371,8 +1374,11 @@ static void compute_transitioned_properties(ComputedProperties const& style, DOM
auto timing_functions = normalize_transition_length_list(
PropertyID::TransitionTimingFunction,
[] { return EasingStyleValue::create(EasingStyleValue::CubicBezier::ease()); });
auto transition_behaviors = normalize_transition_length_list(
PropertyID::TransitionBehavior,
[] { return CSSKeywordValue::create(Keyword::None); });
element.add_transitioned_properties(move(properties), move(delays), move(durations), move(timing_functions));
element.add_transitioned_properties(move(properties), move(delays), move(durations), move(timing_functions), move(transition_behaviors));
}
// https://drafts.csswg.org/css-transitions/#starting
@ -1414,13 +1420,13 @@ void StyleComputer::start_needed_transitions(ComputedProperties const& previous_
if (
// - the element does not have a running transition for the property,
(!has_running_transition) &&
// - there is a matching transition-property value, and
(matching_transition_properties.has_value()) &&
// - the before-change style is different from the after-change style for that property, and the values for the property are transitionable,
(!before_change_value.equals(after_change_value) && property_values_are_transitionable(property_id, before_change_value, after_change_value)) &&
(!before_change_value.equals(after_change_value) && property_values_are_transitionable(property_id, before_change_value, after_change_value, matching_transition_properties->transition_behavior)) &&
// - the element does not have a completed transition for the property
// or the end value of the completed transition is different from the after-change style for the property,
(!has_completed_transition || !existing_transition->transition_end_value()->equals(after_change_value)) &&
// - there is a matching transition-property value, and
(matching_transition_properties.has_value()) &&
// - the combined duration is greater than 0s,
(combined_duration(matching_transition_properties.value()) > 0)) {
@ -1480,7 +1486,7 @@ void StyleComputer::start_needed_transitions(ComputedProperties const& previous_
// or if these two values are not transitionable,
// then implementations must cancel the running transition.
auto current_value = existing_transition->value_at_time(style_change_event_time);
if (current_value->equals(after_change_value) || !property_values_are_transitionable(property_id, current_value, after_change_value)) {
if (current_value->equals(after_change_value) || !property_values_are_transitionable(property_id, current_value, after_change_value, matching_transition_properties->transition_behavior)) {
dbgln_if(CSS_TRANSITIONS_DEBUG, "Transition step 4.1");
existing_transition->cancel();
}
@ -1489,7 +1495,7 @@ void StyleComputer::start_needed_transitions(ComputedProperties const& previous_
// or if the current value of the property in the running transition is not transitionable with the value of the property in the after-change style,
// then implementations must cancel the running transition.
else if ((combined_duration(matching_transition_properties.value()) <= 0)
|| !property_values_are_transitionable(property_id, current_value, after_change_value)) {
|| !property_values_are_transitionable(property_id, current_value, after_change_value, matching_transition_properties->transition_behavior)) {
dbgln_if(CSS_TRANSITIONS_DEBUG, "Transition step 4.2");
existing_transition->cancel();
}

View file

@ -18,6 +18,8 @@ String TransitionStyleValue::to_string(SerializationMode mode) const
builder.append(", "sv);
first = false;
builder.appendff("{} {} {} {}", transition.property_name->to_string(mode), transition.duration, transition.easing->to_string(mode), transition.delay);
if (transition.transition_behavior != TransitionBehavior::Normal)
builder.appendff(" {}", CSS::to_string(transition.transition_behavior));
}
return MUST(builder.to_string());

View file

@ -21,6 +21,7 @@ public:
TimeOrCalculated duration { CSS::Time::make_seconds(0.0) };
TimeOrCalculated delay { CSS::Time::make_seconds(0.0) };
ValueComparingRefPtr<EasingStyleValue const> easing;
TransitionBehavior transition_behavior { TransitionBehavior::Normal };
bool operator==(Transition const&) const = default;
};