From d965a9552feb020de15091f4d3cc482bfe454c24 Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Tue, 21 Sep 2021 11:38:18 +0200 Subject: [PATCH] LibWeb: Start implementing the CSS cascade The 'C' in "CSS" is for Cascade, so let's actually implement the cascade in LibWeb. :^) StyleResolver::resolve_style() now begins by collecting all the matching CSS rules for the given DOM::Element. Rules are then processed in the spec's cascade order (instead of in the order we encounter them.) With this, "!important" is now honored on CSS properties. After performing the cascade, we do another pass of what the spec calls "defaulting" where we resolve "inherit" and "initial" values. I've left a FIXME about supporting correct "initial" values for every property, since we're currently lacking some coverage there. Note that this mechanism now resolves every known CSS property. This is *not* space-efficient and we'll eventually need to come up with some strategies to reduce memory usage around this. However, this will do fine until we have more of the engine working correctly. :^) --- .../Libraries/LibWeb/CSS/StyleProperties.cpp | 40 +---- .../Libraries/LibWeb/CSS/StyleProperties.h | 4 +- .../Libraries/LibWeb/CSS/StyleResolver.cpp | 159 +++++++++++++----- Userland/Libraries/LibWeb/CSS/StyleResolver.h | 24 ++- 4 files changed, 150 insertions(+), 77 deletions(-) diff --git a/Userland/Libraries/LibWeb/CSS/StyleProperties.cpp b/Userland/Libraries/LibWeb/CSS/StyleProperties.cpp index 6fc9bc461b..a06f40af8d 100644 --- a/Userland/Libraries/LibWeb/CSS/StyleProperties.cpp +++ b/Userland/Libraries/LibWeb/CSS/StyleProperties.cpp @@ -36,48 +36,20 @@ NonnullRefPtr StyleProperties::clone() const void StyleProperties::set_property(CSS::PropertyID id, NonnullRefPtr value) { - m_property_values.set((unsigned)id, move(value)); + m_property_values.set(id, move(value)); } void StyleProperties::set_property(CSS::PropertyID id, const StringView& value) { - m_property_values.set((unsigned)id, StringStyleValue::create(value)); + m_property_values.set(id, StringStyleValue::create(value)); } -Optional> StyleProperties::property(CSS::PropertyID id) const +Optional> StyleProperties::property(CSS::PropertyID property_id) const { - auto fetch_initial = [](CSS::PropertyID id) -> Optional> { - auto initial_value = property_initial_value(id); - if (initial_value) - return initial_value.release_nonnull(); + auto it = m_property_values.find(property_id); + if (it == m_property_values.end()) return {}; - }; - auto fetch_inherited = [](CSS::PropertyID) -> Optional> { - // FIXME: Implement inheritance - return {}; - }; - - auto it = m_property_values.find((unsigned)id); - if (it == m_property_values.end()) { - if (is_inherited_property(id)) - return fetch_inherited(id); - return fetch_initial(id); - } - - auto& value = it->value; - if (value->is_initial()) - return fetch_initial(id); - if (value->is_inherit()) - return fetch_inherited(id); - if (value->is_unset()) { - if (is_inherited_property(id)) { - return fetch_inherited(id); - } else { - return fetch_initial(id); - } - } - - return value; + return it->value; } Length StyleProperties::length_or_fallback(CSS::PropertyID id, const Length& fallback) const diff --git a/Userland/Libraries/LibWeb/CSS/StyleProperties.h b/Userland/Libraries/LibWeb/CSS/StyleProperties.h index 36284f8549..05fcc8cefb 100644 --- a/Userland/Libraries/LibWeb/CSS/StyleProperties.h +++ b/Userland/Libraries/LibWeb/CSS/StyleProperties.h @@ -82,7 +82,9 @@ public: Optional z_index() const; private: - HashMap> m_property_values; + friend class StyleResolver; + + HashMap> m_property_values; Optional overflow(CSS::PropertyID) const; void load_font(Layout::Node const&) const; diff --git a/Userland/Libraries/LibWeb/CSS/StyleResolver.cpp b/Userland/Libraries/LibWeb/CSS/StyleResolver.cpp index be94138111..0e6f283263 100644 --- a/Userland/Libraries/LibWeb/CSS/StyleResolver.cpp +++ b/Userland/Libraries/LibWeb/CSS/StyleResolver.cpp @@ -52,22 +52,26 @@ static StyleSheet& quirks_mode_stylesheet() } template -void StyleResolver::for_each_stylesheet(Callback callback) const +void StyleResolver::for_each_stylesheet(CascadeOrigin cascade_origin, Callback callback) const { - callback(default_stylesheet()); - if (document().in_quirks_mode()) - callback(quirks_mode_stylesheet()); - for (auto& sheet : document().style_sheets().sheets()) { - callback(sheet); + if (cascade_origin == CascadeOrigin::Any || cascade_origin == CascadeOrigin::UserAgent) { + callback(default_stylesheet()); + if (document().in_quirks_mode()) + callback(quirks_mode_stylesheet()); + } + if (cascade_origin == CascadeOrigin::Any || cascade_origin == CascadeOrigin::Author) { + for (auto& sheet : document().style_sheets().sheets()) { + callback(sheet); + } } } -Vector StyleResolver::collect_matching_rules(DOM::Element const& element) const +Vector StyleResolver::collect_matching_rules(DOM::Element const& element, CascadeOrigin declaration_type) const { Vector matching_rules; size_t style_sheet_index = 0; - for_each_stylesheet([&](auto& sheet) { + for_each_stylesheet(declaration_type, [&](auto& sheet) { size_t rule_index = 0; static_cast(sheet).for_each_effective_style_rule([&](auto& rule) { size_t selector_index = 0; @@ -531,54 +535,129 @@ Optional StyleResolver::resolve_custom_property(DOM::Element& ele return resolved_with_specificity.style; } -NonnullRefPtr StyleResolver::resolve_style(DOM::Element& element) const +struct MatchingDeclarations { + Vector user_agent_rules; + Vector author_rules; +}; + +void StyleResolver::cascade_declarations(StyleProperties& style, DOM::Element& element, Vector const& matching_rules, CascadeOrigin cascade_origin, bool important) const { - auto style = StyleProperties::create(); - auto* parent_style = element.parent_element() ? element.parent_element()->specified_css_values() : nullptr; - if (parent_style) { - parent_style->for_each_property([&](auto property_id, auto& value) { - if (is_inherited_property(property_id)) - set_property_expanding_shorthands(style, property_id, value, m_document); - }); - } - - element.apply_presentational_hints(*style); - - auto matching_rules = collect_matching_rules(element); - sort_matching_rules(matching_rules); - for (auto& match : matching_rules) { for (auto& property : verify_cast(match.rule->declaration()).properties()) { + if (important != property.important) + continue; auto property_value = property.value; if (property.value->is_custom_property()) { - auto prop = reinterpret_cast(property.value.ptr()); - auto custom_prop_name = prop->custom_property_name(); - auto resolved = resolve_custom_property(element, custom_prop_name); + auto custom_property_name = static_cast(*property.value).custom_property_name(); + auto resolved = resolve_custom_property(element, custom_property_name); if (resolved.has_value()) { property_value = resolved.value().value; } } - // FIXME: This also captures shorthands of which we ideally want to resolve the long names separately. - if (property_value->is_inherit()) { - // HACK: Trying to resolve the font property here lead to copious amounts of debug-spam - if (property.property_id == CSS::PropertyID::Font) - continue; - if (parent_style) { - auto maybe_parent_property_value = parent_style->property(property.property_id); - if (maybe_parent_property_value.has_value()) - property_value = maybe_parent_property_value.release_value(); - } - } set_property_expanding_shorthands(style, property.property_id, property_value, m_document); } } - if (auto* inline_style = verify_cast(element.inline_style())) { - for (auto& property : inline_style->properties()) { - set_property_expanding_shorthands(style, property.property_id, property.value, m_document); + if (cascade_origin == CascadeOrigin::Author) { + if (auto* inline_style = verify_cast(element.inline_style())) { + for (auto& property : inline_style->properties()) { + if (important != property.important) + continue; + set_property_expanding_shorthands(style, property.property_id, property.value, m_document); + } } } +} +// https://drafts.csswg.org/css-cascade/#cascading +void StyleResolver::compute_cascaded_values(StyleProperties& style, DOM::Element& element) const +{ + // First, we collect all the CSS rules whose selectors match `element`: + MatchingRuleSet matching_rule_set; + matching_rule_set.user_agent_rules = collect_matching_rules(element, CascadeOrigin::UserAgent); + sort_matching_rules(matching_rule_set.user_agent_rules); + matching_rule_set.author_rules = collect_matching_rules(element, CascadeOrigin::Author); + sort_matching_rules(matching_rule_set.author_rules); + + // Then we apply the declarations from the matched rules in cascade order: + + // Normal user agent declarations + cascade_declarations(style, element, matching_rule_set.user_agent_rules, CascadeOrigin::UserAgent, false); + + // FIXME: Normal user declarations + + // Normal author declarations + cascade_declarations(style, element, matching_rule_set.author_rules, CascadeOrigin::Author, false); + + // Author presentational hints (NOTE: The spec doesn't say exactly how to prioritize these.) + element.apply_presentational_hints(style); + + // FIXME: Animation declarations [css-animations-1] + + // Important author declarations + cascade_declarations(style, element, matching_rule_set.author_rules, CascadeOrigin::Author, true); + + // FIXME: Important user declarations + + // Important user agent declarations + cascade_declarations(style, element, matching_rule_set.user_agent_rules, CascadeOrigin::UserAgent, true); + + // FIXME: Transition declarations [css-transitions-1] +} + +// https://drafts.csswg.org/css-cascade/#defaulting +void StyleResolver::compute_defaulted_values(StyleProperties& style, DOM::Element const& element) const +{ + // FIXME: If we don't know the correct initial value for a property, we fall back to InitialStyleValue. + + auto get_initial_value = [&](PropertyID property_id) -> NonnullRefPtr { + auto value = property_initial_value(property_id); + if (!value) + return InitialStyleValue::the(); + return value.release_nonnull(); + }; + + auto get_inherit_value = [&](PropertyID property_id) -> NonnullRefPtr { + if (!element.parent_element() || !element.parent_element()->specified_css_values()) + return get_initial_value(property_id); + auto& map = element.parent_element()->specified_css_values()->m_property_values; + auto it = map.find(property_id); + VERIFY(it != map.end()); + return const_cast(*it->value); + }; + + // Walk the list of all known CSS properties and: + // - Add them to `style` if they are missing. + // - Resolve `inherit` and `initial` as needed. + for (auto i = to_underlying(CSS::first_property_id); i <= to_underlying(CSS::last_property_id); ++i) { + auto property_id = (CSS::PropertyID)i; + auto it = style.m_property_values.find(property_id); + if (it == style.m_property_values.end()) { + + if (is_inherited_property(property_id)) + style.m_property_values.set(property_id, get_inherit_value(property_id)); + else + style.m_property_values.set(property_id, get_initial_value(property_id)); + continue; + } + + if (it->value->is_initial()) { + it->value = get_initial_value(property_id); + continue; + } + + if (it->value->is_inherit()) { + it->value = get_inherit_value(property_id); + continue; + } + } +} + +NonnullRefPtr StyleResolver::resolve_style(DOM::Element& element) const +{ + auto style = StyleProperties::create(); + compute_cascaded_values(style, element); + compute_defaulted_values(style, element); return style; } diff --git a/Userland/Libraries/LibWeb/CSS/StyleResolver.h b/Userland/Libraries/LibWeb/CSS/StyleResolver.h index f21415706f..04dd65cf0a 100644 --- a/Userland/Libraries/LibWeb/CSS/StyleResolver.h +++ b/Userland/Libraries/LibWeb/CSS/StyleResolver.h @@ -32,7 +32,17 @@ public: NonnullRefPtr resolve_style(DOM::Element&) const; - Vector collect_matching_rules(DOM::Element const&) const; + // https://www.w3.org/TR/css-cascade/#origin + enum class CascadeOrigin { + Any, // FIXME: This is not part of the spec. Get rid of it. + Author, + User, + UserAgent, + Animation, + Transition, + }; + + Vector collect_matching_rules(DOM::Element const&, CascadeOrigin = CascadeOrigin::Any) const; void sort_matching_rules(Vector&) const; struct CustomPropertyResolutionTuple { Optional style {}; @@ -42,8 +52,18 @@ public: Optional resolve_custom_property(DOM::Element&, String const&) const; private: + void compute_cascaded_values(StyleProperties&, DOM::Element&) const; + void compute_defaulted_values(StyleProperties&, DOM::Element const&) const; + template - void for_each_stylesheet(Callback) const; + void for_each_stylesheet(CascadeOrigin, Callback) const; + + struct MatchingRuleSet { + Vector user_agent_rules; + Vector author_rules; + }; + + void cascade_declarations(StyleProperties&, DOM::Element&, Vector const&, CascadeOrigin, bool important) const; DOM::Document& m_document; };