From 23a08fd35a90c19b28997b81aecd83fb1d683902 Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Thu, 23 Sep 2021 13:13:51 +0200 Subject: [PATCH] LibWeb: Start absolutizing lengths after performing the CSS cascade Once we've performed the cascade on a set of values for an element, we should have enough information to resolve/absolutize some lengths. Basically, any CSS length that isn't "auto" or a percentage can be turned into an absolute length (in pixels) as long as we have the following information: - The viewport rect - The parent element's font - The document element's font - The element's own font To ensure that we can absolutize lengths relative to the element's own font, we now do a separate first pass where font-related properties are defaulted (in the cascade spec sense of the word) and become usable. There's a lot more work to do here, but this should open up a lot of simplification in layout code, since it will no longer need to care about relative lengths. Layout still needs to resolve percentages, since we can't do that for some properties until the containing block dimensions are known. --- Userland/Libraries/LibWeb/CSS/Length.cpp | 48 +-- Userland/Libraries/LibWeb/CSS/Length.h | 10 +- .../Libraries/LibWeb/CSS/StyleProperties.cpp | 3 +- .../Libraries/LibWeb/CSS/StyleProperties.h | 17 +- .../Libraries/LibWeb/CSS/StyleResolver.cpp | 274 ++++++++++++++++-- Userland/Libraries/LibWeb/CSS/StyleResolver.h | 4 + 6 files changed, 305 insertions(+), 51 deletions(-) diff --git a/Userland/Libraries/LibWeb/CSS/Length.cpp b/Userland/Libraries/LibWeb/CSS/Length.cpp index 3edf6e7b16..db59cdccce 100644 --- a/Userland/Libraries/LibWeb/CSS/Length.cpp +++ b/Userland/Libraries/LibWeb/CSS/Length.cpp @@ -7,6 +7,8 @@ #include #include +#include +#include #include #include #include @@ -14,37 +16,49 @@ namespace Web::CSS { -float Length::relative_length_to_px(const Layout::Node& layout_node) const +float Length::relative_length_to_px(Gfx::IntRect const& viewport_rect, Gfx::FontMetrics const& font_metrics, float root_font_size) const { switch (m_type) { case Type::Ex: - return m_value * layout_node.font().x_height(); + return m_value * font_metrics.x_height; case Type::Em: - return m_value * layout_node.font_size(); + return m_value * font_metrics.size; case Type::Ch: // FIXME: Use layout_node.font().glyph_height() when writing-mode is not horizontal-tb (it has to be implemented first) - return m_value * (layout_node.font().glyph_width('0') + layout_node.font().glyph_spacing()); + return m_value * (font_metrics.glyph_width + font_metrics.glyph_spacing); case Type::Rem: - return m_value * layout_node.document().document_element()->layout_node()->font_size(); + return m_value * root_font_size; case Type::Vw: - return layout_node.document().browsing_context()->viewport_rect().width() * (m_value / 100); + return viewport_rect.width() * (m_value / 100); case Type::Vh: - return layout_node.document().browsing_context()->viewport_rect().height() * (m_value / 100); - case Type::Vmin: { - auto viewport = layout_node.document().browsing_context()->viewport_rect(); - - return min(viewport.width(), viewport.height()) * (m_value / 100); - } - case Type::Vmax: { - auto viewport = layout_node.document().browsing_context()->viewport_rect(); - - return max(viewport.width(), viewport.height()) * (m_value / 100); - } + return viewport_rect.height() * (m_value / 100); + case Type::Vmin: + return min(viewport_rect.width(), viewport_rect.height()) * (m_value / 100); + case Type::Vmax: + return max(viewport_rect.width(), viewport_rect.height()) * (m_value / 100); default: VERIFY_NOT_REACHED(); } } +float Length::to_px(Layout::Node const& layout_node) const +{ + if (!layout_node.document().browsing_context()) + return 0; + auto viewport_rect = layout_node.document().browsing_context()->viewport_rect(); + auto* root_element = layout_node.document().document_element(); + if (!root_element || !root_element->layout_node()) + return 0; + return to_px(viewport_rect, layout_node.font().metrics('M'), root_element->layout_node()->font().presentation_size()); +} + +float Length::relative_length_to_px(Layout::Node const& layout_node) const +{ + auto viewport_rect = layout_node.document().browsing_context()->viewport_rect(); + auto root_element = layout_node.document().document_element(); + return relative_length_to_px(viewport_rect, layout_node.font().metrics('M'), root_element->layout_node()->font().presentation_size()); +} + static float resolve_calc_value(CalculatedStyleValue::CalcValue const&, const Layout::Node& layout_node, float reference_for_percent); static float resolve_calc_number_value(CalculatedStyleValue::CalcNumberValue const&); static float resolve_calc_product(NonnullOwnPtr const&, const Layout::Node& layout_node, float reference_for_percent); diff --git a/Userland/Libraries/LibWeb/CSS/Length.h b/Userland/Libraries/LibWeb/CSS/Length.h index b90324a419..615d8cb4a8 100644 --- a/Userland/Libraries/LibWeb/CSS/Length.h +++ b/Userland/Libraries/LibWeb/CSS/Length.h @@ -7,6 +7,7 @@ #pragma once #include +#include #include namespace Web::CSS { @@ -103,10 +104,13 @@ public: } float raw_value() const { return m_value; } - ALWAYS_INLINE float to_px(const Layout::Node& layout_node) const + + float to_px(Layout::Node const&) const; + + ALWAYS_INLINE float to_px(Gfx::IntRect const& viewport_rect, Gfx::FontMetrics const& font_metrics, float root_font_size) const { if (is_relative()) - return relative_length_to_px(layout_node); + return relative_length_to_px(viewport_rect, font_metrics, root_font_size); constexpr float inch_pixels = 96.0f; constexpr float centimeter_pixels = (inch_pixels / 2.54f); switch (m_type) { @@ -153,6 +157,8 @@ public: void set_calculated_style(CalculatedStyleValue* value) { m_calculated_style = value; } + float relative_length_to_px(Gfx::IntRect const& viewport_rect, Gfx::FontMetrics const& font_metrics, float root_font_size) const; + private: float relative_length_to_px(const Layout::Node&) const; float resolve_calculated_value(const Layout::Node& layout_node, float reference_for_percent) const; diff --git a/Userland/Libraries/LibWeb/CSS/StyleProperties.cpp b/Userland/Libraries/LibWeb/CSS/StyleProperties.cpp index 4d548aa293..e39facaef0 100644 --- a/Userland/Libraries/LibWeb/CSS/StyleProperties.cpp +++ b/Userland/Libraries/LibWeb/CSS/StyleProperties.cpp @@ -231,7 +231,6 @@ void StyleProperties::load_font(Layout::Node const& node) const } if (!found_font) { - dbgln("Font not found: '{}' {} {}", family_value->to_string(), size, weight); found_font = font_fallback(monospace, bold); } @@ -239,7 +238,7 @@ void StyleProperties::load_font(Layout::Node const& node) const FontCache::the().set(font_selector, *m_font); } -RefPtr StyleProperties::font_fallback(bool monospace, bool bold) const +NonnullRefPtr StyleProperties::font_fallback(bool monospace, bool bold) { if (monospace && bold) return Gfx::FontDatabase::default_fixed_width_font().bold_variant(); diff --git a/Userland/Libraries/LibWeb/CSS/StyleProperties.h b/Userland/Libraries/LibWeb/CSS/StyleProperties.h index 05fcc8cefb..f5681e206c 100644 --- a/Userland/Libraries/LibWeb/CSS/StyleProperties.h +++ b/Userland/Libraries/LibWeb/CSS/StyleProperties.h @@ -33,6 +33,9 @@ public: callback((CSS::PropertyID)it.key, *it.value); } + HashMap>& properties() { return m_property_values; } + HashMap> const& properties() const { return m_property_values; } + void set_property(CSS::PropertyID, NonnullRefPtr value); void set_property(CSS::PropertyID, const StringView&); Optional> property(CSS::PropertyID) const; @@ -73,6 +76,17 @@ public: return *m_font; } + Gfx::Font const& computed_font() const + { + VERIFY(m_font); + return *m_font; + } + + void set_computed_font(NonnullRefPtr font) + { + m_font = move(font); + } + float line_height(const Layout::Node&) const; bool operator==(const StyleProperties&) const; @@ -81,6 +95,8 @@ public: Optional position() const; Optional z_index() const; + static NonnullRefPtr font_fallback(bool monospace, bool bold); + private: friend class StyleResolver; @@ -88,7 +104,6 @@ private: Optional overflow(CSS::PropertyID) const; void load_font(Layout::Node const&) const; - RefPtr font_fallback(bool monospace, bool bold) const; mutable RefPtr m_font; }; diff --git a/Userland/Libraries/LibWeb/CSS/StyleResolver.cpp b/Userland/Libraries/LibWeb/CSS/StyleResolver.cpp index fbb1c2539e..122d41a86f 100644 --- a/Userland/Libraries/LibWeb/CSS/StyleResolver.cpp +++ b/Userland/Libraries/LibWeb/CSS/StyleResolver.cpp @@ -7,6 +7,8 @@ */ #include +#include +#include #include #include #include @@ -15,6 +17,8 @@ #include #include #include +#include +#include #include #include @@ -574,50 +578,253 @@ void StyleResolver::compute_cascaded_values(StyleProperties& style, DOM::Element // 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 +static NonnullRefPtr get_initial_value(CSS::PropertyID property_id) +{ + auto value = property_initial_value(property_id); + if (!value) + return InitialStyleValue::the(); + return value.release_nonnull(); +}; + +static NonnullRefPtr get_inherit_value(CSS::PropertyID property_id, DOM::Element const& element) +{ + if (!element.parent_element() || !element.parent_element()->specified_css_values()) + return get_initial_value(property_id); + auto& map = element.parent_element()->specified_css_values()->properties(); + auto it = map.find(property_id); + VERIFY(it != map.end()); + return *it->value; +}; + +void StyleResolver::compute_defaulted_property_value(StyleProperties& style, DOM::Element const& element, CSS::PropertyID property_id) 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 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, element)); + else + style.m_property_values.set(property_id, get_initial_value(property_id)); + return; + } - 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); - }; + if (it->value->is_initial()) { + it->value = get_initial_value(property_id); + return; + } + if (it->value->is_inherit()) { + it->value = get_inherit_value(property_id, element); + return; + } +} + +// https://drafts.csswg.org/css-cascade/#defaulting +void StyleResolver::compute_defaulted_values(StyleProperties& style, DOM::Element const& element) const +{ // 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_longhand_property_id); i <= to_underlying(CSS::last_longhand_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()) { + compute_defaulted_property_value(style, element, property_id); + } +} - 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; +void StyleResolver::compute_font(StyleProperties& style, DOM::Element const& element) const +{ + // To compute the font, first ensure that we've defaulted the relevant CSS font properties. + // FIXME: This should be more sophisticated. + compute_defaulted_property_value(style, element, CSS::PropertyID::FontFamily); + compute_defaulted_property_value(style, element, CSS::PropertyID::FontSize); + compute_defaulted_property_value(style, element, CSS::PropertyID::FontWeight); + + auto viewport_rect = document().browsing_context()->viewport_rect(); + + auto font_size = style.property(CSS::PropertyID::FontSize).value(); + auto font_weight = style.property(CSS::PropertyID::FontWeight).value(); + + int weight = Gfx::FontWeight::Regular; + if (font_weight->is_identifier()) { + switch (static_cast(*font_weight).id()) { + case CSS::ValueID::Normal: + weight = Gfx::FontWeight::Regular; + break; + case CSS::ValueID::Bold: + weight = Gfx::FontWeight::Bold; + break; + case CSS::ValueID::Lighter: + // FIXME: This should be relative to the parent. + weight = Gfx::FontWeight::Regular; + break; + case CSS::ValueID::Bolder: + // FIXME: This should be relative to the parent. + weight = Gfx::FontWeight::Bold; + break; + default: + break; } + } else if (font_weight->is_numeric()) { + int font_weight_integer = roundf(static_cast(*font_weight).value()); + if (font_weight_integer <= Gfx::FontWeight::Regular) + weight = Gfx::FontWeight::Regular; + else if (font_weight_integer <= Gfx::FontWeight::Bold) + weight = Gfx::FontWeight::Bold; + else + weight = Gfx::FontWeight::Black; + } + // FIXME: calc() for font-weight - if (it->value->is_initial()) { - it->value = get_initial_value(property_id); - continue; + bool bold = weight > Gfx::FontWeight::Regular; + + int size = 10; + + if (font_size->is_identifier()) { + switch (static_cast(*font_size).id()) { + case CSS::ValueID::XxSmall: + case CSS::ValueID::XSmall: + case CSS::ValueID::Small: + case CSS::ValueID::Medium: + // FIXME: Should be based on "user's default font size" + size = 10; + break; + case CSS::ValueID::Large: + case CSS::ValueID::XLarge: + case CSS::ValueID::XxLarge: + case CSS::ValueID::XxxLarge: + // FIXME: Should be based on "user's default font size" + size = 12; + break; + case CSS::ValueID::Smaller: + case CSS::ValueID::Larger: + // FIXME: Should be based on parent element + break; + default: + break; } + } else { + float root_font_size = 10; + if (element.document().document_element() != &element) + root_font_size = element.document().document_element()->layout_node()->font().presentation_size(); - if (it->value->is_inherit()) { - it->value = get_inherit_value(property_id); + Gfx::FontMetrics font_metrics; + if (element.parent_element()) + font_metrics = element.parent_element()->specified_css_values()->computed_font().metrics('M'); + else + font_metrics = Gfx::FontDatabase::default_font().metrics('M'); + + Optional maybe_length; + if (font_size->is_length()) { + maybe_length = font_size->to_length(); + if (maybe_length->is_percentage()) { + auto parent_font_size = size; + if (element.parent_element() && element.parent_element()->layout_node() && element.parent_element()->specified_css_values()) { + auto value = element.parent_element()->specified_css_values()->property(CSS::PropertyID::FontSize).value(); + if (value->is_length()) { + auto length = static_cast(*value).to_length(); + if (length.is_absolute() || length.is_relative()) + parent_font_size = length.to_px(viewport_rect, font_metrics, root_font_size); + } + } + + maybe_length = Length::make_px(maybe_length->raw_value() / 100.0f * (parent_font_size)); + } + } else if (font_size->is_calculated()) { + Length length = Length(0, Length::Type::Calculated); + length.set_calculated_style(verify_cast(font_size.ptr())); + maybe_length = length; + } + if (maybe_length.has_value()) { + auto calculated_size = maybe_length.value().to_px(viewport_rect, font_metrics, root_font_size); + if (calculated_size != 0) + size = calculated_size; + } + } + + // FIXME: Implement the full font-matching algorithm: https://www.w3.org/TR/css-fonts-4/#font-matching-algorithm + + // Note: This is modified by the find_font() lambda + FontSelector font_selector; + bool monospace = false; + + auto find_font = [&](String const& family) -> RefPtr { + font_selector = { family, size, weight }; + + if (auto found_font = FontCache::the().get(font_selector)) + return found_font; + + if (auto found_font = Gfx::FontDatabase::the().get(family, size, weight)) + return found_font; + + return {}; + }; + + // FIXME: Replace hard-coded font names with a relevant call to FontDatabase. + // Currently, we cannot request the default font's name, or request it at a specific size and weight. + // So, hard-coded font names it is. + auto find_generic_font = [&](ValueID font_id) -> RefPtr { + switch (font_id) { + case ValueID::Monospace: + case ValueID::UiMonospace: + monospace = true; + return find_font("Csilla"); + case ValueID::Serif: + case ValueID::SansSerif: + case ValueID::Cursive: + case ValueID::Fantasy: + case ValueID::UiSerif: + case ValueID::UiSansSerif: + case ValueID::UiRounded: + return find_font("Katica"); + default: + return {}; + } + }; + + RefPtr found_font; + + auto family_value = style.property(PropertyID::FontFamily).value(); + if (family_value->is_value_list()) { + auto& family_list = static_cast(*family_value).values(); + for (auto& family : family_list) { + if (family.is_identifier()) { + found_font = find_generic_font(family.to_identifier()); + } else if (family.is_string()) { + found_font = find_font(family.to_string()); + } + if (found_font) + break; + } + } else if (family_value->is_identifier()) { + found_font = find_generic_font(family_value->to_identifier()); + } else if (family_value->is_string()) { + found_font = find_font(family_value->to_string()); + } + + if (!found_font) { + found_font = StyleProperties::font_fallback(monospace, bold); + } + + FontCache::the().set(font_selector, *found_font); + + style.set_computed_font(found_font.release_nonnull()); +} + +void StyleResolver::absolutize_values(StyleProperties& style, DOM::Element const&) const +{ + auto viewport_rect = document().browsing_context()->viewport_rect(); + auto font_metrics = style.computed_font().metrics('M'); + // FIXME: Get the root element font. + float root_font_size = 10; + + for (auto& it : style.properties()) { + if (!it.value->is_length()) continue; + auto length = it.value->to_length(); + if (length.is_relative()) { + auto px = length.relative_length_to_px(viewport_rect, font_metrics, root_font_size); + it.value = LengthStyleValue::create(CSS::Length::make_px(px)); } } } @@ -625,9 +832,18 @@ void StyleResolver::compute_defaulted_values(StyleProperties& style, DOM::Elemen NonnullRefPtr StyleResolver::resolve_style(DOM::Element& element) const { auto style = StyleProperties::create(); + // 1. Perform the cascade. This produces the "specified style" compute_cascaded_values(style, element); + + // 2. Compute the font, since that may be needed for font-relative CSS units + compute_font(style, element); + + // 3. Absolutize values, turning font/viewport relative lengths into absolute lengths + absolutize_values(style, element); + + // 4. Default the values, applying inheritance and 'initial' as needed compute_defaulted_values(style, element); + return style; } - } diff --git a/Userland/Libraries/LibWeb/CSS/StyleResolver.h b/Userland/Libraries/LibWeb/CSS/StyleResolver.h index 04dd65cf0a..fc2f789467 100644 --- a/Userland/Libraries/LibWeb/CSS/StyleResolver.h +++ b/Userland/Libraries/LibWeb/CSS/StyleResolver.h @@ -53,7 +53,11 @@ public: private: void compute_cascaded_values(StyleProperties&, DOM::Element&) const; + void compute_font(StyleProperties&, DOM::Element const&) const; void compute_defaulted_values(StyleProperties&, DOM::Element const&) const; + void absolutize_values(StyleProperties&, DOM::Element const&) const; + + void compute_defaulted_property_value(StyleProperties&, DOM::Element const&, CSS::PropertyID) const; template void for_each_stylesheet(CascadeOrigin, Callback) const;