Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions docs/validation/css-starting-style-entry-transition-749.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Starting-style entry transition contract (#749)

WebScene retains document-level `@starting-style` as ordinary selector rules
marked with an internal, always-inactive media token. This reuses the existing
selector indexes, specificity, cascade layers, stylesheet ownership, mutation
invalidation, and teardown paths without maintaining a parallel rule engine.
Real media conditions surrounding a starting-style group remain attached and
are evaluated when an element enters the rendered state.

When an element is first rendered or returns from `display:none`, WebScene builds
one temporary initial cascade from the normal and matching starting rules. The
combined list is sorted by normal layer, specificity, source and important
precedence before declarations are replayed. Only
an admitted opacity, 2D transform, or background-color transition copies values
into the existing retained transition machine. The temporary style is released
before the normal computed style is exposed. Static and already-rendered nodes
pay two bits each and do not allocate snapshots or request frames.

`transition-behavior: allow-discrete` is retained per transition-list item. A
transition from a rendered display value to `none` keeps the used box until the
host-clock duration boundary while computed style reports the target `none`.
Cancellation, replacement, or a changed transition policy releases the deferred
state. Completion removes the box, dirties layout once, and stops frame demand.
A true DOM removal clears the subtree's before-change lifecycle and animation
runtime so reinsertion can enter again without detached frame demand. Atomic
connected-tree reparenting keeps the lifecycle generation and does not invent a
new entry.

Coverage:

- `webscene_css_parser_tests` proves the Rust syntax stream emits qualified
children inside `@starting-style`;
- `native_web_css_service` proves preparation, nested-media retention, initial
opacity/translate sampling, cascade precedence, midpoint/end values, discrete
display deferral, completion, re-entry, and zero animation-runtime allocations
across 256 unrelated nodes;
- `test_starting_style_entry_and_discrete_display_transitions` covers live
`CSSStartingStyleRule` identity and mutation, host-clock entry/display behavior,
removal/reinsertion, stylesheet teardown, and zero detached frame demand;
- `css-starting-style-entry-transition.html` is the browser/native candidate.

This slice does not add broad discrete-property interpolation, view transitions,
or new margin/border transition machines. Those declarations still participate
in the temporary cascade but only already-supported transition properties are
admitted.
Original file line number Diff line number Diff line change
Expand Up @@ -977,6 +977,7 @@ mod css_syntax {
|| prelude.name.eq_ignore_ascii_case("supports")
|| prelude.name.eq_ignore_ascii_case("layer")
|| prelude.name.eq_ignore_ascii_case("container")
|| prelude.name.eq_ignore_ascii_case("starting-style")
|| prelude.name.eq_ignore_ascii_case("keyframes")
|| prelude.name.eq_ignore_ascii_case("-webkit-keyframes")
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,10 @@ bool apply_decoration_value(dom_node& node,const std::string& name,const std::st
node.style.mutable_animations().transition_timing_function_value = value;
if (!defer_transition_configuration) configure_style_transitions(node.style);
return true;
} else if (name == "transition-behavior") {
node.style.mutable_animations().transition_behavior_value = value;
if (!defer_transition_configuration) configure_style_transitions(node.style);
return true;
} else if (name == "animation") {
apply_animation_shorthand(node.style, value);
decision.classification = "partially-supported";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,57 @@ bool apply_native_cascade(native_document& document,dom_node& node,
}
recompute_cascaded_line_height(node,cascade_order,variables);
recompute_inline_font_relative_metrics(node);
configure_style_transitions(node.style);
document.update_discrete_display_transition(node,previous.display);
std::optional<node_style> starting_style;
const auto rendered_after_change=node.style.display!=display_mode::none;
const auto entering_rendered_state=rendered_after_change
&& (!node.css_cascade_initialized || !node.css_was_rendered);
if(entering_rendered_state) {
std::vector<const css_rule*> starting_rules;
for(const auto index:indices) {
if(index>=sheets.state().rules.size()) continue;
const auto& rule=sheets.state().rules[index];
if(!sheets.starting_rule_media_matches(rule)
|| rule.payload->pseudo_kind!=0U
|| !query.matches_prepared(node,rule.compiled_selector())) continue;
starting_rules.push_back(&rule);
}
if(!starting_rules.empty()) {
auto after_change_style=node.style;
reset_cascaded_style(
node,variables,&sheets.state().registered_custom_properties);
auto initial_rules=matched.ordinary;
initial_rules.insert(
initial_rules.end(),starting_rules.begin(),starting_rules.end());
std::sort(
initial_rules.begin(),initial_rules.end(),
[](const css_rule* left,const css_rule* right) {
return cascade_rule_precedes(left,right,false);
});
const cascaded_rule_order initial_order(initial_rules);
apply_matched_declarations(
node,initial_order,[&](const css_declaration& declaration,bool inline_origin) {
property_result result;
if(declaration.name.starts_with("--")) {
apply_custom_property(
node,declaration,
&sheets.state().registered_custom_properties);
} else {
apply_declaration(
document,node,declaration,variables,inline_origin,
result,load_svg,[](bool) {},true);
}
});
configure_style_transitions(node.style);
recompute_cascaded_line_height(node,initial_order,variables);
recompute_inline_font_relative_metrics(node);
starting_style=node.style;
node.style=std::move(after_change_style);
}
}
node.css_cascade_initialized=true;
node.css_was_rendered=rendered_after_change;
bool backdrop_important=false;
for_each_cascaded_pseudo_declaration(
matched.pseudo,
Expand Down Expand Up @@ -139,6 +190,8 @@ bool apply_native_cascade(native_document& document,dom_node& node,
sheets.state().opacity_keyframes,
sheets.state().registered_custom_properties);
}
if(starting_style.has_value())
document.prime_starting_style(node,*starting_style);
document.update_style_animations(node);
}
const bool layout_changed=!computed_layout_style_equal(previous,node.style);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ inline uint64_t property_mask(std::string_view name)
if (name == "transition-duration") return inline_transition_duration;
if (name == "transition-delay") return inline_transition_delay;
if (name == "transition-timing-function") return inline_transition_timing;
if (name == "transition-behavior") return inline_transition_property;
if (name == "background") return inline_background | inline_background_image;
if (name == "background-color" || name == "backgroundColor") {
return inline_background;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -254,13 +254,15 @@ inline void apply_all_unset(
reset.outline_rgba = previous.outline_rgba;
}
if (has_inline({"transition", "transition-property", "transition-duration",
"transition-delay", "transition-timing-function"})) {
"transition-delay", "transition-timing-function", "transition-behavior"})) {
auto& reset_animations = reset.mutable_animations();
reset_animations.transition_property_value = previous.animations().transition_property_value;
reset_animations.transition_duration_value = previous.animations().transition_duration_value;
reset_animations.transition_delay_value = previous.animations().transition_delay_value;
reset_animations.transition_timing_function_value =
previous.animations().transition_timing_function_value;
reset_animations.transition_behavior_value =
previous.animations().transition_behavior_value;
configure_style_transitions(reset);
}
if (has_inline({"z-index"})) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
#include <vector>

namespace webscene_native::css {
inline constexpr std::string_view starting_style_media_marker =
"-webscene-starting-style";
struct compiled_css_selector_list;
// Native stylesheet/cascade storage shared by runtime adapters and build tools.
// No JavaScript handles, V8 headers or parser ownership belong in this model.
Expand Down Expand Up @@ -244,6 +246,12 @@ struct compiled_css_selector final {
{
return payload->declarations;
}
bool is_starting_style() const noexcept
{
return std::find(
payload->media_queries.begin(), payload->media_queries.end(),
starting_style_media_marker) != payload->media_queries.end();
}
const std::vector<std::string>& media_queries() const noexcept
{
return payload->media_queries;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@ class stylesheet_owner {
return std::all_of(rule.media_queries().begin(),rule.media_queries().end(),
[&](const auto& query) { return css::media_matches(query,environment_); });
}
bool starting_rule_media_matches_internal(const css_rule& rule) const {
return std::all_of(rule.media_queries().begin(),rule.media_queries().end(),
[&](const auto& query) {
return query == starting_style_media_marker
|| css::media_matches(query,environment_);
});
}
void rebuild() {
state_=css_cascade_state{};
for(const auto& entry:sheets_) {
Expand Down Expand Up @@ -58,6 +65,10 @@ class stylesheet_owner {
}
}
public:
bool starting_rule_media_matches(const css_rule& rule) const {
return rule.is_starting_style()
&& starting_rule_media_matches_internal(rule);
}
// Replacing preserves sheet source order; newly attached sheets append.
void replace(uint32_t id,prepared_stylesheet sheet) {
for(auto& entry:sheets_) if(entry.id==id) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,17 @@ class stylesheet_sink final : public css_syntax_sink {
current.prelude = trim_value(prelude);
current.declarations.reserve(3U);
current.children_active = false;
} else if (name == "starting-style" && has_block
&& trim_value(prelude).empty()
&& nearest_style_frame() == nullptr
&& std::none_of(stack_.begin(), stack_.end(), [](const frame& ancestor) {
return ancestor.starting_style;
})) {
current.starting_style = true;
owner_.record_feature(
"css", "at-rule:@starting-style", "supported",
"bounded entry-transition grouping rules",
"stylesheet-parser");
} else if (name == "font-face") {
current.children_active = false;
owner_.record_feature(
Expand Down Expand Up @@ -222,6 +233,9 @@ class stylesheet_sink final : public css_syntax_sink {
if (!ancestor.media_query.empty()) {
inherited_media.push_back(ancestor.media_query);
}
if (ancestor.starting_style) {
inherited_media.emplace_back(starting_style_media_marker);
}
}
std::vector<completed_style_rule> completed;
completed.reserve(1U + current.nested_rules.size());
Expand Down Expand Up @@ -342,6 +356,7 @@ class stylesheet_sink final : public css_syntax_sink {
bool children_active{false};
bool keyframes{false};
bool property_rule{false};
bool starting_style{false};
};

static std::optional<float> parse_typed_number(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -301,8 +301,10 @@ inline void configure_style_transitions(node_style& style)
const auto delays = split_css_component_list(animations.transition_delay_value, ',');
const auto timings = split_css_component_list(
animations.transition_timing_function_value, ',');
const auto behaviors = split_css_component_list(
animations.transition_behavior_value, ',');
const auto resolve = [&](std::string_view property) {
node_style::transition_timing result;
std::pair<node_style::transition_timing, bool> result;
for (size_t index = 0; index < properties.size(); ++index) {
auto candidate = ascii_lower(trim_value(properties[index]));
// Transition matching uses the canonical physical property.
Expand All @@ -313,26 +315,33 @@ inline void configure_style_transitions(node_style& style)
else if (candidate == "background") candidate = "background-color";
if (candidate != property && candidate != "all") continue;
if (!durations.empty()) {
result.duration_ms = std::max(
result.first.duration_ms = std::max(
0.0F, parse_css_time_ms(durations[index % durations.size()]));
}
if (!delays.empty()) {
result.delay_ms = parse_css_time_ms(delays[index % delays.size()]);
result.first.delay_ms = parse_css_time_ms(delays[index % delays.size()]);
}
if (!timings.empty()) {
parse_transition_timing(timings[index % timings.size()], result);
parse_transition_timing(timings[index % timings.size()], result.first);
}
if (!behaviors.empty()) {
result.second = ascii_lower(trim_value(
behaviors[index % behaviors.size()])) == "allow-discrete";
}
return result;
}
return result;
};
animations.transform_transition = resolve("transform");
animations.left_transition = resolve("left");
animations.top_transition = resolve("top");
animations.opacity_transition = resolve("opacity");
animations.color_transition = resolve("color");
animations.background_color_transition = resolve("background-color");
animations.filter_transition = resolve("filter");
animations.transform_transition = resolve("transform").first;
animations.left_transition = resolve("left").first;
animations.top_transition = resolve("top").first;
animations.opacity_transition = resolve("opacity").first;
animations.color_transition = resolve("color").first;
animations.background_color_transition = resolve("background-color").first;
animations.filter_transition = resolve("filter").first;
const auto display = resolve("display");
animations.display_transition = display.first;
animations.display_transition_allow_discrete = display.second;
}

inline void apply_transition_shorthand(
Expand All @@ -344,11 +353,13 @@ inline void apply_transition_shorthand(
std::vector<std::string> durations;
std::vector<std::string> delays;
std::vector<std::string> timings;
std::vector<std::string> behaviors;
for (const auto& item : split_css_component_list(value, ',')) {
auto property = std::string("all");
auto duration = std::string("0s");
auto delay = std::string("0s");
auto timing = std::string("ease");
auto behavior = std::string("normal");
auto saw_time = false;
for (const auto& token : split_value_tokens(item)) {
const auto lower = ascii_lower(token);
Expand All @@ -360,14 +371,17 @@ inline void apply_transition_shorthand(
|| lower == "ease-out" || lower == "ease-in-out"
|| lower.starts_with("cubic-bezier(")) {
timing = lower;
} else if (lower != "normal") {
} else if (lower == "normal" || lower == "allow-discrete") {
behavior = lower;
} else {
property = lower;
}
}
properties.push_back(std::move(property));
durations.push_back(std::move(duration));
delays.push_back(std::move(delay));
timings.push_back(std::move(timing));
behaviors.push_back(std::move(behavior));
}
const auto join = [](const std::vector<std::string>& values) {
std::string result;
Expand All @@ -382,6 +396,7 @@ inline void apply_transition_shorthand(
animations.transition_duration_value = join(durations);
animations.transition_delay_value = join(delays);
animations.transition_timing_function_value = join(timings);
animations.transition_behavior_value = join(behaviors);
if (configure) configure_style_transitions(style);
}

Expand All @@ -396,6 +411,7 @@ inline bool apply_compiled_single_transition_shorthand(
auto duration = std::string("0s");
auto delay = std::string("0s");
auto timing = std::string("ease");
auto behavior = std::string("normal");
auto saw_time = false;
for (const auto& component : specified.components) {
const auto lower = ascii_lower(component.name);
Expand All @@ -407,7 +423,9 @@ inline bool apply_compiled_single_transition_shorthand(
|| lower == "ease-out" || lower == "ease-in-out"
|| lower.starts_with("cubic-bezier(")) {
timing = lower;
} else if (lower != "normal") {
} else if (lower == "normal" || lower == "allow-discrete") {
behavior = lower;
} else {
property = lower;
}
}
Expand All @@ -416,6 +434,7 @@ inline bool apply_compiled_single_transition_shorthand(
animations.transition_duration_value = std::move(duration);
animations.transition_delay_value = std::move(delay);
animations.transition_timing_function_value = std::move(timing);
animations.transition_behavior_value = std::move(behavior);
if (configure) configure_style_transitions(style);
return true;
}
Expand Down
Loading
Loading