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.
This commit is contained in:
Andreas Kling 2021-09-23 13:13:51 +02:00
commit 23a08fd35a
Notes: sideshowbarker 2024-07-18 03:29:02 +09:00
6 changed files with 305 additions and 51 deletions

View file

@ -7,6 +7,8 @@
*/
#include <AK/QuickSort.h>
#include <LibGfx/Font.h>
#include <LibGfx/FontDatabase.h>
#include <LibWeb/CSS/CSSStyleRule.h>
#include <LibWeb/CSS/Parser/Parser.h>
#include <LibWeb/CSS/SelectorEngine.h>
@ -15,6 +17,8 @@
#include <LibWeb/DOM/Document.h>
#include <LibWeb/DOM/Element.h>
#include <LibWeb/Dump.h>
#include <LibWeb/FontCache.h>
#include <LibWeb/Page/BrowsingContext.h>
#include <ctype.h>
#include <stdio.h>
@ -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<StyleValue> 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<StyleValue> 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<StyleValue> {
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<StyleValue> {
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<StyleValue&>(*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<IdentifierStyleValue const&>(*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<NumericStyleValue const&>(*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<const IdentifierStyleValue&>(*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<Length> 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<LengthStyleValue const&>(*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<CalculatedStyleValue>(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<Gfx::Font> {
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<Gfx::Font> {
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<Gfx::Font> found_font;
auto family_value = style.property(PropertyID::FontFamily).value();
if (family_value->is_value_list()) {
auto& family_list = static_cast<StyleValueList const&>(*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<StyleProperties> 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;
}
}