LibWeb: Separate font computation logic from StyleComputer

Font computation and loading is distinct enough from style computation
that it makes more sense to have this in it's own class.

This will be useful later when we move the font loading process to
`ComputedProperties` in order to respect animated values.
This commit is contained in:
Callum Law 2025-11-06 18:27:22 +13:00 committed by Sam Atkins
parent dfa47d9ed6
commit 6c236d04d8
Notes: github-actions[bot] 2025-12-05 10:04:34 +00:00
13 changed files with 722 additions and 646 deletions

View file

@ -162,6 +162,7 @@ set(SOURCES
CSS/EdgeRect.cpp
CSS/Fetch.cpp
CSS/Flex.cpp
CSS/FontComputer.cpp
CSS/FontFace.cpp
CSS/FontFaceSet.cpp
CSS/Frequency.cpp

View file

@ -11,6 +11,7 @@
#include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/CSS/CSSImportRule.h>
#include <LibWeb/CSS/CSSStyleSheet.h>
#include <LibWeb/CSS/FontComputer.h>
#include <LibWeb/CSS/Parser/Parser.h>
#include <LibWeb/CSS/StyleComputer.h>
#include <LibWeb/CSS/StyleSheetList.h>
@ -325,7 +326,7 @@ void CSSStyleSheet::add_owning_document_or_shadow_root(DOM::Node& document_or_sh
// All owning documents or shadow roots must be part of the same document so we only need to load this style
// sheet's fonts against the document of the first
if (this->owning_documents_or_shadow_roots().size() == 1)
document_or_shadow_root.document().style_computer().load_fonts_from_sheet(*this);
document_or_shadow_root.document().font_computer().load_fonts_from_sheet(*this);
for (auto const& import_rule : m_import_rules) {
if (import_rule->loaded_style_sheet())
@ -340,7 +341,7 @@ void CSSStyleSheet::remove_owning_document_or_shadow_root(DOM::Node& document_or
// All owning documents or shadow roots must be part of the same document so we only need to unload this style
// sheet's fonts once we have none remaining.
if (this->owning_documents_or_shadow_roots().size() == 0)
document_or_shadow_root.document().style_computer().unload_fonts_from_sheet(*this);
document_or_shadow_root.document().font_computer().unload_fonts_from_sheet(*this);
for (auto const& import_rule : m_import_rules) {
if (import_rule->loaded_style_sheet())

View file

@ -0,0 +1,573 @@
/*
* Copyright (c) 2018-2025, Andreas Kling <andreas@ladybird.org>
* Copyright (c) 2021, the SerenityOS developers.
* Copyright (c) 2021-2025, Sam Atkins <sam@ladybird.org>
* Copyright (c) 2024, Matthew Olsson <mattco@serenityos.org>
* Copyright (c) 2025, Callum Law <callumlaw1709@outlook.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include "FontComputer.h"
#include <AK/NonnullRawPtr.h>
#include <LibGfx/Font/FontDatabase.h>
#include <LibGfx/Font/WOFF/Loader.h>
#include <LibGfx/Font/WOFF2/Loader.h>
#include <LibWeb/CSS/CSSFontFaceRule.h>
#include <LibWeb/CSS/ComputedProperties.h>
#include <LibWeb/CSS/Fetch.h>
#include <LibWeb/CSS/StyleValues/CustomIdentStyleValue.h>
#include <LibWeb/CSS/StyleValues/KeywordStyleValue.h>
#include <LibWeb/CSS/StyleValues/StringStyleValue.h>
#include <LibWeb/CSS/StyleValues/StyleValueList.h>
#include <LibWeb/DOM/Document.h>
#include <LibWeb/Fetch/Infrastructure/HTTP/MIME.h>
#include <LibWeb/Fetch/Response.h>
#include <LibWeb/MimeSniff/Resource.h>
#include <LibWeb/Platform/FontPlugin.h>
namespace Web::CSS {
GC_DEFINE_ALLOCATOR(FontComputer);
GC_DEFINE_ALLOCATOR(FontLoader);
struct FontFaceKey {
NonnullRawPtr<FlyString const> family_name;
int weight { 0 };
int slope { 0 };
};
}
namespace AK {
namespace Detail {
template<>
inline constexpr bool IsHashCompatible<Web::CSS::FontFaceKey, Web::CSS::OwnFontFaceKey> = true;
template<>
inline constexpr bool IsHashCompatible<Web::CSS::OwnFontFaceKey, Web::CSS::FontFaceKey> = true;
}
template<>
struct Traits<Web::CSS::FontFaceKey> : public DefaultTraits<Web::CSS::FontFaceKey> {
static unsigned hash(Web::CSS::FontFaceKey const& key) { return pair_int_hash(key.family_name->hash(), pair_int_hash(key.weight, key.slope)); }
};
template<>
struct Traits<Web::CSS::OwnFontFaceKey> : public DefaultTraits<Web::CSS::OwnFontFaceKey> {
static unsigned hash(Web::CSS::OwnFontFaceKey const& key) { return pair_int_hash(key.family_name.hash(), pair_int_hash(key.weight, key.slope)); }
};
template<>
struct Traits<Web::CSS::FontMatchingAlgorithmCacheKey> : public DefaultTraits<Web::CSS::FontMatchingAlgorithmCacheKey> {
static unsigned hash(Web::CSS::FontMatchingAlgorithmCacheKey const& key)
{
auto hash = key.family_name.hash();
hash = pair_int_hash(hash, key.weight);
hash = pair_int_hash(hash, key.slope);
hash = pair_int_hash(hash, Traits<float>::hash(key.font_size_in_pt));
return hash;
}
};
}
namespace Web::CSS {
OwnFontFaceKey::OwnFontFaceKey(FontFaceKey const& other)
: family_name(other.family_name)
, weight(other.weight)
, slope(other.slope)
{
}
OwnFontFaceKey::operator FontFaceKey() const
{
return FontFaceKey {
family_name,
weight,
slope
};
}
[[nodiscard]] bool OwnFontFaceKey::operator==(FontFaceKey const& other) const
{
return family_name == other.family_name
&& weight == other.weight
&& slope == other.slope;
}
FontLoader::FontLoader(FontComputer& font_computer, GC::Ptr<CSSStyleSheet> parent_style_sheet, FlyString family_name, Vector<Gfx::UnicodeRange> unicode_ranges, Vector<URL> urls, Function<void(RefPtr<Gfx::Typeface const>)> on_load)
: m_font_computer(font_computer)
, m_parent_style_sheet(parent_style_sheet)
, m_family_name(move(family_name))
, m_unicode_ranges(move(unicode_ranges))
, m_urls(move(urls))
, m_on_load(move(on_load))
{
}
FontLoader::~FontLoader() = default;
void FontLoader::visit_edges(Visitor& visitor)
{
Base::visit_edges(visitor);
visitor.visit(m_font_computer);
visitor.visit(m_parent_style_sheet);
visitor.visit(m_fetch_controller);
}
bool FontLoader::is_loading() const
{
return m_fetch_controller && !m_vector_font;
}
RefPtr<Gfx::Font const> FontLoader::font_with_point_size(float point_size, Gfx::FontVariationSettings const& variations)
{
if (!m_vector_font) {
if (!m_fetch_controller)
start_loading_next_url();
return nullptr;
}
return m_vector_font->font(point_size, variations);
}
void FontLoader::start_loading_next_url()
{
// FIXME: Load local() fonts somehow.
if (m_fetch_controller && m_fetch_controller->state() == Fetch::Infrastructure::FetchController::State::Ongoing)
return;
if (m_urls.is_empty())
return;
// https://drafts.csswg.org/css-fonts-4/#fetch-a-font
// To fetch a font given a selected <url> url for @font-face rule, fetch url, with stylesheet being rules parent
// CSS style sheet, destination "font", CORS mode "cors", and processResponse being the following steps given
// response res and null, failure or a byte stream stream:
auto style_sheet_or_document = m_parent_style_sheet ? StyleSheetOrDocument { *m_parent_style_sheet } : StyleSheetOrDocument { m_font_computer->document() };
m_fetch_controller = fetch_a_style_resource(m_urls.take_first(), style_sheet_or_document, Fetch::Infrastructure::Request::Destination::Font, CorsMode::Cors,
[loader = this](auto response, auto stream) {
// 1. If stream is null, return.
// 2. Load a font from stream according to its type.
// NB: We need to fetch the next source if this one fails to fetch OR decode. So, first try to decode it.
RefPtr<Gfx::Typeface const> typeface;
if (auto* bytes = stream.template get_pointer<ByteBuffer>()) {
if (auto maybe_typeface = loader->try_load_font(response, *bytes); !maybe_typeface.is_error())
typeface = maybe_typeface.release_value();
}
if (!typeface) {
// NB: If we have other sources available, try the next one.
if (loader->m_urls.is_empty()) {
loader->font_did_load_or_fail(nullptr);
} else {
loader->m_fetch_controller = nullptr;
loader->start_loading_next_url();
}
} else {
loader->font_did_load_or_fail(move(typeface));
}
});
if (!m_fetch_controller)
font_did_load_or_fail(nullptr);
}
void FontLoader::font_did_load_or_fail(RefPtr<Gfx::Typeface const> typeface)
{
if (typeface) {
m_vector_font = typeface.release_nonnull();
m_font_computer->did_load_font(m_family_name);
if (m_on_load)
m_on_load(m_vector_font);
} else {
if (m_on_load)
m_on_load(nullptr);
}
m_fetch_controller = nullptr;
}
ErrorOr<NonnullRefPtr<Gfx::Typeface const>> FontLoader::try_load_font(Fetch::Infrastructure::Response const& response, ByteBuffer const& bytes)
{
// FIXME: This could maybe use the format() provided in @font-face as well, since often the mime type is just application/octet-stream and we have to try every format
auto mime_type = Fetch::Infrastructure::extract_mime_type(response.header_list());
if (!mime_type.has_value() || !mime_type->is_font()) {
mime_type = MimeSniff::Resource::sniff(bytes, MimeSniff::SniffingConfiguration { .sniffing_context = MimeSniff::SniffingContext::Font });
}
if (mime_type.has_value()) {
if (mime_type->essence() == "font/ttf"sv || mime_type->essence() == "application/x-font-ttf"sv || mime_type->essence() == "font/otf"sv) {
if (auto result = Gfx::Typeface::try_load_from_temporary_memory(bytes); !result.is_error()) {
return result;
}
}
if (mime_type->essence() == "font/woff"sv || mime_type->essence() == "application/font-woff"sv) {
if (auto result = WOFF::try_load_from_bytes(bytes); !result.is_error()) {
return result;
}
}
if (mime_type->essence() == "font/woff2"sv || mime_type->essence() == "application/font-woff2"sv) {
if (auto result = WOFF2::try_load_from_bytes(bytes); !result.is_error()) {
return result;
}
}
}
return Error::from_string_literal("Automatic format detection failed");
}
struct FontComputer::MatchingFontCandidate {
FontFaceKey key;
Variant<FontLoaderList*, Gfx::Typeface const*> loader_or_typeface;
[[nodiscard]] RefPtr<Gfx::FontCascadeList const> font_with_point_size(float point_size, Gfx::FontVariationSettings const& variations) const
{
auto font_list = Gfx::FontCascadeList::create();
if (auto const* loader_list = loader_or_typeface.get_pointer<FontLoaderList*>(); loader_list) {
for (auto const& loader : **loader_list) {
if (auto font = loader->font_with_point_size(point_size, variations); font)
font_list->add(*font, loader->unicode_ranges());
}
return font_list;
}
font_list->add(loader_or_typeface.get<Gfx::Typeface const*>()->font(point_size, variations));
return font_list;
}
};
void FontComputer::visit_edges(Visitor& visitor)
{
Base::visit_edges(visitor);
visitor.visit(m_document);
visitor.visit(m_loaded_fonts);
}
RefPtr<Gfx::FontCascadeList const> FontComputer::find_matching_font_weight_ascending(Vector<MatchingFontCandidate> const& candidates, int target_weight, float font_size_in_pt, Gfx::FontVariationSettings const& variations, bool inclusive)
{
using Fn = AK::Function<bool(MatchingFontCandidate const&)>;
auto pred = inclusive ? Fn([&](auto const& matching_font_candidate) { return matching_font_candidate.key.weight >= target_weight; })
: Fn([&](auto const& matching_font_candidate) { return matching_font_candidate.key.weight > target_weight; });
auto it = find_if(candidates.begin(), candidates.end(), pred);
for (; it != candidates.end(); ++it) {
if (auto found_font = it->font_with_point_size(font_size_in_pt, variations))
return found_font;
}
return {};
}
RefPtr<Gfx::FontCascadeList const> FontComputer::find_matching_font_weight_descending(Vector<MatchingFontCandidate> const& candidates, int target_weight, float font_size_in_pt, Gfx::FontVariationSettings const& variations, bool inclusive)
{
using Fn = AK::Function<bool(MatchingFontCandidate const&)>;
auto pred = inclusive ? Fn([&](auto const& matching_font_candidate) { return matching_font_candidate.key.weight <= target_weight; })
: Fn([&](auto const& matching_font_candidate) { return matching_font_candidate.key.weight < target_weight; });
auto it = find_if(candidates.rbegin(), candidates.rend(), pred);
for (; it != candidates.rend(); ++it) {
if (auto found_font = it->font_with_point_size(font_size_in_pt, variations))
return found_font;
}
return {};
}
RefPtr<Gfx::FontCascadeList const> FontComputer::font_matching_algorithm(FlyString const& family_name, int weight, int slope, float font_size_in_pt) const
{
FontMatchingAlgorithmCacheKey key { family_name, weight, slope, font_size_in_pt };
return m_font_matching_algorithm_cache.ensure(key, [&] {
return font_matching_algorithm_impl(family_name, weight, slope, font_size_in_pt);
});
}
// Partial implementation of the font-matching algorithm: https://www.w3.org/TR/css-fonts-4/#font-matching-algorithm
// FIXME: This should be replaced by the full CSS font selection algorithm.
RefPtr<Gfx::FontCascadeList const> FontComputer::font_matching_algorithm_impl(FlyString const& family_name, int weight, int slope, float font_size_in_pt) const
{
// If a font family match occurs, the user agent assembles the set of font faces in that family and then
// narrows the set to a single face using other font properties in the order given below.
Vector<MatchingFontCandidate> matching_family_fonts;
for (auto const& font_key_and_loader : m_loaded_fonts) {
if (font_key_and_loader.key.family_name.equals_ignoring_ascii_case(family_name))
matching_family_fonts.empend(font_key_and_loader.key, const_cast<FontLoaderList*>(&font_key_and_loader.value));
}
Gfx::FontDatabase::the().for_each_typeface_with_family_name(family_name, [&](Gfx::Typeface const& typeface) {
matching_family_fonts.empend(
FontFaceKey {
.family_name = typeface.family(),
.weight = static_cast<int>(typeface.weight()),
.slope = typeface.slope(),
},
&typeface);
});
quick_sort(matching_family_fonts, [](auto const& a, auto const& b) {
return a.key.weight < b.key.weight;
});
// FIXME: 1. font-stretch is tried first.
// FIXME: 2. font-style is tried next.
// We don't have complete support of italic and oblique fonts, so matching on font-style can be simplified to:
// If a matching slope is found, all faces which don't have that matching slope are excluded from the matching set.
auto style_it = find_if(matching_family_fonts.begin(), matching_family_fonts.end(),
[&](auto const& matching_font_candidate) { return matching_font_candidate.key.slope == slope; });
if (style_it != matching_family_fonts.end()) {
matching_family_fonts.remove_all_matching([&](auto const& matching_font_candidate) {
return matching_font_candidate.key.slope != slope;
});
}
// 3. font-weight is matched next.
// If the desired weight is inclusively between 400 and 500, weights greater than or equal to the target weight
// are checked in ascending order until 500 is hit and checked, followed by weights less than the target weight
// in descending order, followed by weights greater than 500, until a match is found.
Gfx::FontVariationSettings variations;
variations.set_weight(weight);
if (weight >= 400 && weight <= 500) {
auto it = find_if(matching_family_fonts.begin(), matching_family_fonts.end(),
[&](auto const& matching_font_candidate) { return matching_font_candidate.key.weight >= weight; });
for (; it != matching_family_fonts.end() && it->key.weight <= 500; ++it) {
if (auto found_font = it->font_with_point_size(font_size_in_pt, variations))
return found_font;
}
if (auto found_font = find_matching_font_weight_descending(matching_family_fonts, weight, font_size_in_pt, variations, false))
return found_font;
for (; it != matching_family_fonts.end(); ++it) {
if (auto found_font = it->font_with_point_size(font_size_in_pt, variations))
return found_font;
}
}
// If the desired weight is less than 400, weights less than or equal to the desired weight are checked in descending order
// followed by weights above the desired weight in ascending order until a match is found.
if (weight < 400) {
if (auto found_font = find_matching_font_weight_descending(matching_family_fonts, weight, font_size_in_pt, variations, true))
return found_font;
if (auto found_font = find_matching_font_weight_ascending(matching_family_fonts, weight, font_size_in_pt, variations, false))
return found_font;
}
// If the desired weight is greater than 500, weights greater than or equal to the desired weight are checked in ascending order
// followed by weights below the desired weight in descending order until a match is found.
if (weight > 500) {
if (auto found_font = find_matching_font_weight_ascending(matching_family_fonts, weight, font_size_in_pt, variations, true))
return found_font;
if (auto found_font = find_matching_font_weight_descending(matching_family_fonts, weight, font_size_in_pt, variations, false))
return found_font;
}
return {};
}
RefPtr<Gfx::FontCascadeList const> FontComputer::compute_font_for_style_values(StyleValue const& font_family, CSSPixels const& font_size, int slope, double font_weight, Percentage const& font_width, HashMap<FlyString, double> const& font_variation_settings) const
{
// FIXME: We round to int here as that is what is expected by our font infrastructure below
auto width = round_to<int>(font_width.value());
// FIXME: We round to int here as that is what is expected by our font infrastructure below
auto weight = round_to<int>(font_weight);
// FIXME: Implement the full font-matching algorithm: https://www.w3.org/TR/css-fonts-4/#font-matching-algorithm
float const font_size_in_pt = font_size * 0.75f;
auto find_font = [&](FlyString const& family) -> RefPtr<Gfx::FontCascadeList const> {
FontFaceKey key {
.family_name = family,
.weight = weight,
.slope = slope,
};
auto result = Gfx::FontCascadeList::create();
if (auto it = m_loaded_fonts.find(key); it != m_loaded_fonts.end()) {
auto const& loaders = it->value;
Gfx::FontVariationSettings variation;
variation.set_weight(font_weight);
for (auto const& [tag_string, value] : font_variation_settings) {
auto string_view = tag_string.bytes_as_string_view();
if (string_view.length() != 4)
continue;
auto tag = Gfx::FourCC(string_view.characters_without_null_termination());
variation.axes.set(tag, value);
}
for (auto const& loader : loaders) {
if (auto found_font = loader->font_with_point_size(font_size_in_pt, variation))
result->add(*found_font, loader->unicode_ranges());
}
return result;
}
if (auto found_font = font_matching_algorithm(family, weight, slope, font_size_in_pt); found_font && !found_font->is_empty()) {
return found_font;
}
if (auto found_font = Gfx::FontDatabase::the().get(family, font_size_in_pt, weight, width, slope)) {
result->add(*found_font);
return result;
}
return {};
};
auto find_generic_font = [&](Keyword font_id) -> RefPtr<Gfx::FontCascadeList const> {
Platform::GenericFont generic_font {};
switch (font_id) {
case Keyword::Monospace:
case Keyword::UiMonospace:
generic_font = Platform::GenericFont::Monospace;
break;
case Keyword::Serif:
generic_font = Platform::GenericFont::Serif;
break;
case Keyword::Fantasy:
generic_font = Platform::GenericFont::Fantasy;
break;
case Keyword::SansSerif:
generic_font = Platform::GenericFont::SansSerif;
break;
case Keyword::Cursive:
generic_font = Platform::GenericFont::Cursive;
break;
case Keyword::UiSerif:
generic_font = Platform::GenericFont::UiSerif;
break;
case Keyword::UiSansSerif:
generic_font = Platform::GenericFont::UiSansSerif;
break;
case Keyword::UiRounded:
generic_font = Platform::GenericFont::UiRounded;
break;
default:
return {};
}
return find_font(Platform::FontPlugin::the().generic_font_name(generic_font));
};
auto font_list = Gfx::FontCascadeList::create();
for (auto const& family : font_family.as_value_list().values()) {
RefPtr<Gfx::FontCascadeList const> other_font_list;
if (family->is_keyword()) {
other_font_list = find_generic_font(family->to_keyword());
} else if (family->is_string()) {
other_font_list = find_font(family->as_string().string_value());
} else if (family->is_custom_ident()) {
other_font_list = find_font(family->as_custom_ident().custom_ident());
}
if (other_font_list)
font_list->extend(*other_font_list);
}
auto default_font = Platform::FontPlugin::the().default_font(font_size_in_pt);
if (font_list->is_empty()) {
// This is needed to make sure we check default font before reaching to emojis.
font_list->add(*default_font);
}
// Add emoji and symbol fonts
for (auto font_name : Platform::FontPlugin::the().symbol_font_names()) {
if (auto other_font_list = find_font(font_name)) {
font_list->extend(*other_font_list);
}
}
// The default font is already included in the font list, but we explicitly set it
// as the last-resort font. This ensures that if none of the specified fonts contain
// the requested code point, there is still a font available to provide a fallback glyph.
font_list->set_last_resort_font(*default_font);
return font_list;
}
Gfx::Font const& FontComputer::initial_font() const
{
// FIXME: This is not correct.
static auto font = ComputedProperties::font_fallback(false, false, 12);
return font;
}
void FontComputer::did_load_font(FlyString const&)
{
m_font_matching_algorithm_cache = {};
document().invalidate_style(DOM::StyleInvalidationReason::CSSFontLoaded);
}
GC::Ptr<FontLoader> FontComputer::load_font_face(ParsedFontFace const& font_face, Function<void(RefPtr<Gfx::Typeface const>)> on_load)
{
if (font_face.sources().is_empty()) {
if (on_load)
on_load({});
return {};
}
FontFaceKey key {
.family_name = font_face.font_family(),
.weight = font_face.weight().value_or(0),
.slope = font_face.slope().value_or(0),
};
// FIXME: Pass the sources directly, so the font loader can make use of the format information, or load local fonts.
Vector<URL> urls;
for (auto const& source : font_face.sources()) {
if (source.local_or_url.has<URL>())
urls.append(source.local_or_url.get<URL>());
// FIXME: Handle local()
}
if (urls.is_empty()) {
if (on_load)
on_load({});
return {};
}
auto loader = heap().allocate<FontLoader>(*this, font_face.parent_style_sheet(), font_face.font_family(), font_face.unicode_ranges(), move(urls), move(on_load));
auto& loader_ref = *loader;
auto maybe_font_loaders_list = m_loaded_fonts.get(key);
if (maybe_font_loaders_list.has_value()) {
maybe_font_loaders_list->append(move(loader));
} else {
FontLoaderList loaders;
loaders.append(loader);
m_loaded_fonts.set(OwnFontFaceKey(key), move(loaders));
}
// Actual object owned by font loader list inside m_loaded_fonts, this isn't use-after-move/free
return loader_ref;
}
void FontComputer::load_fonts_from_sheet(CSSStyleSheet& sheet)
{
for (auto const& rule : sheet.rules()) {
if (!is<CSSFontFaceRule>(*rule))
continue;
auto const& font_face_rule = static_cast<CSSFontFaceRule const&>(*rule);
if (!font_face_rule.is_valid())
continue;
if (auto font_loader = load_font_face(font_face_rule.font_face())) {
sheet.add_associated_font_loader(*font_loader);
}
}
}
void FontComputer::unload_fonts_from_sheet(CSSStyleSheet& sheet)
{
for (auto& [_, font_loader_list] : m_loaded_fonts) {
font_loader_list.remove_all_matching([&](auto& font_loader) {
return sheet.has_associated_font_loader(*font_loader);
});
}
}
size_t FontComputer::number_of_css_font_faces_with_loading_in_progress() const
{
size_t count = 0;
for (auto const& [_, loaders] : m_loaded_fonts) {
for (auto const& loader : loaders) {
if (loader->is_loading())
++count;
}
}
return count;
}
}

View file

@ -0,0 +1,124 @@
/*
* Copyright (c) 2018-2025, Andreas Kling <andreas@ladybird.org>
* Copyright (c) 2021, the SerenityOS developers.
* Copyright (c) 2021-2025, Sam Atkins <sam@ladybird.org>
* Copyright (c) 2024, Matthew Olsson <mattco@serenityos.org>
* Copyright (c) 2025, Callum Law <callumlaw1709@outlook.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibGC/CellAllocator.h>
#include <LibGfx/FontCascadeList.h>
#include <LibWeb/Export.h>
#include <LibWeb/Forward.h>
#pragma once
namespace Web::CSS {
struct FontFaceKey;
struct OwnFontFaceKey {
explicit OwnFontFaceKey(FontFaceKey const& other);
operator FontFaceKey() const;
[[nodiscard]] u32 hash() const { return pair_int_hash(family_name.hash(), pair_int_hash(weight, slope)); }
[[nodiscard]] bool operator==(OwnFontFaceKey const& other) const = default;
[[nodiscard]] bool operator==(FontFaceKey const& other) const;
FlyString family_name;
int weight { 0 };
int slope { 0 };
};
struct FontMatchingAlgorithmCacheKey {
FlyString family_name;
int weight;
int slope;
float font_size_in_pt;
[[nodiscard]] bool operator==(FontMatchingAlgorithmCacheKey const& other) const = default;
};
class FontLoader final : public GC::Cell {
GC_CELL(FontLoader, GC::Cell);
GC_DECLARE_ALLOCATOR(FontLoader);
public:
FontLoader(FontComputer& font_computer, GC::Ptr<CSSStyleSheet> parent_style_sheet, FlyString family_name, Vector<Gfx::UnicodeRange> unicode_ranges, Vector<URL> urls, ESCAPING Function<void(RefPtr<Gfx::Typeface const>)> on_load = {});
virtual ~FontLoader();
Vector<Gfx::UnicodeRange> const& unicode_ranges() const { return m_unicode_ranges; }
RefPtr<Gfx::Typeface const> vector_font() const { return m_vector_font; }
RefPtr<Gfx::Font const> font_with_point_size(float point_size, Gfx::FontVariationSettings const& variations = {});
void start_loading_next_url();
bool is_loading() const;
private:
virtual void visit_edges(Visitor&) override;
ErrorOr<NonnullRefPtr<Gfx::Typeface const>> try_load_font(Fetch::Infrastructure::Response const&, ByteBuffer const&);
void font_did_load_or_fail(RefPtr<Gfx::Typeface const>);
GC::Ref<FontComputer> m_font_computer;
GC::Ptr<CSSStyleSheet> m_parent_style_sheet;
FlyString m_family_name;
Vector<Gfx::UnicodeRange> m_unicode_ranges;
RefPtr<Gfx::Typeface const> m_vector_font;
Vector<URL> m_urls;
GC::Ptr<Fetch::Infrastructure::FetchController> m_fetch_controller;
Function<void(RefPtr<Gfx::Typeface const>)> m_on_load;
};
class WEB_API FontComputer final : public GC::Cell {
GC_CELL(FontComputer, GC::Cell);
GC_DECLARE_ALLOCATOR(FontComputer);
public:
explicit FontComputer(DOM::Document& document)
: m_document(document)
{
}
~FontComputer() = default;
DOM::Document& document() { return m_document; }
DOM::Document const& document() const { return m_document; }
Gfx::Font const& initial_font() const;
void did_load_font(FlyString const& family_name);
GC::Ptr<FontLoader> load_font_face(ParsedFontFace const&, ESCAPING Function<void(RefPtr<Gfx::Typeface const>)> on_load = {});
void load_fonts_from_sheet(CSSStyleSheet&);
void unload_fonts_from_sheet(CSSStyleSheet&);
RefPtr<Gfx::FontCascadeList const> compute_font_for_style_values(StyleValue const& font_family, CSSPixels const& font_size, int font_slope, double font_weight, Percentage const& font_width, HashMap<FlyString, double> const& font_variation_settings) const;
size_t number_of_css_font_faces_with_loading_in_progress() const;
private:
virtual void visit_edges(Visitor&) override;
struct MatchingFontCandidate;
static RefPtr<Gfx::FontCascadeList const> find_matching_font_weight_ascending(Vector<MatchingFontCandidate> const& candidates, int target_weight, float font_size_in_pt, Gfx::FontVariationSettings const& variations, bool inclusive);
static RefPtr<Gfx::FontCascadeList const> find_matching_font_weight_descending(Vector<MatchingFontCandidate> const& candidates, int target_weight, float font_size_in_pt, Gfx::FontVariationSettings const& variations, bool inclusive);
RefPtr<Gfx::FontCascadeList const> font_matching_algorithm(FlyString const& family_name, int weight, int slope, float font_size_in_pt) const;
RefPtr<Gfx::FontCascadeList const> font_matching_algorithm_impl(FlyString const& family_name, int weight, int slope, float font_size_in_pt) const;
GC::Ref<DOM::Document> m_document;
using FontLoaderList = Vector<GC::Ref<FontLoader>>;
HashMap<OwnFontFaceKey, FontLoaderList> m_loaded_fonts;
mutable HashMap<FontMatchingAlgorithmCacheKey, RefPtr<Gfx::FontCascadeList const>> m_font_matching_algorithm_cache;
};
}

View file

@ -16,9 +16,9 @@
#include <LibJS/Runtime/Realm.h>
#include <LibWeb/Bindings/FontFacePrototype.h>
#include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/CSS/FontComputer.h>
#include <LibWeb/CSS/FontFace.h>
#include <LibWeb/CSS/Parser/Parser.h>
#include <LibWeb/CSS/StyleComputer.h>
#include <LibWeb/CSS/StyleValues/CustomIdentStyleValue.h>
#include <LibWeb/HTML/Scripting/TemporaryExecutionContext.h>
#include <LibWeb/HTML/Window.h>
@ -483,7 +483,7 @@ GC::Ref<WebIDL::Promise> FontFace::load()
// FIXME: We should probably put the 'font cache' on the WindowOrWorkerGlobalScope instead of tying it to the document's style computer
auto& global = HTML::relevant_global_object(*font);
if (auto* window = as_if<HTML::Window>(global)) {
auto& style_computer = const_cast<StyleComputer&>(window->document()->style_computer());
auto& font_computer = const_cast<FontComputer&>(window->document()->font_computer());
// FIXME: The ParsedFontFace is kind of expensive to create. We should be using a shared sub-object for the data
ParsedFontFace parsed_font_face {
@ -503,7 +503,7 @@ GC::Ref<WebIDL::Promise> FontFace::load()
{}, // FIXME: feature_settings
{}, // FIXME: variation_settings
};
if (auto loader = style_computer.load_font_face(parsed_font_face, move(on_load)))
if (auto loader = font_computer.load_font_face(parsed_font_face, move(on_load)))
loader->start_loading_next_url();
} else {
// FIXME: Don't know how to load fonts in workers! They don't have a StyleComputer

View file

@ -11,9 +11,9 @@
#include <LibGfx/Font/Font.h>
#include <LibGfx/Rect.h>
#include <LibWeb/CSS/ComputedProperties.h>
#include <LibWeb/CSS/FontComputer.h>
#include <LibWeb/CSS/Length.h>
#include <LibWeb/CSS/Percentage.h>
#include <LibWeb/CSS/StyleComputer.h>
#include <LibWeb/DOM/Document.h>
#include <LibWeb/HTML/BrowsingContext.h>
#include <LibWeb/HTML/Navigable.h>
@ -139,7 +139,7 @@ Length::ResolutionContext Length::ResolutionContext::for_element(DOM::AbstractEl
Length::ResolutionContext Length::ResolutionContext::for_window(HTML::Window const& window)
{
auto const& initial_font = window.associated_document().style_computer().initial_font();
auto const& initial_font = window.associated_document().font_computer().initial_font();
Gfx::FontPixelMetrics const& initial_font_metrics = initial_font.pixel_metrics();
Length::FontMetrics font_metrics { CSSPixels { initial_font.pixel_size() }, initial_font_metrics, InitialValues::line_height() };
return Length::ResolutionContext {
@ -151,7 +151,7 @@ Length::ResolutionContext Length::ResolutionContext::for_window(HTML::Window con
Length::ResolutionContext Length::ResolutionContext::for_document(DOM::Document const& document)
{
auto const& initial_font = document.style_computer().initial_font();
auto const& initial_font = document.font_computer().initial_font();
Gfx::FontPixelMetrics const& initial_font_metrics = initial_font.pixel_metrics();
Length::FontMetrics font_metrics { CSSPixels { initial_font.pixel_size() }, initial_font_metrics, InitialValues::line_height() };
CSSPixelRect viewport_rect;

View file

@ -17,19 +17,12 @@
#include <AK/Math.h>
#include <AK/NonnullRawPtr.h>
#include <AK/QuickSort.h>
#include <LibGfx/Font/Font.h>
#include <LibGfx/Font/FontDatabase.h>
#include <LibGfx/Font/FontStyleMapping.h>
#include <LibGfx/Font/Typeface.h>
#include <LibGfx/Font/WOFF/Loader.h>
#include <LibGfx/Font/WOFF2/Loader.h>
#include <LibWeb/Animations/Animatable.h>
#include <LibWeb/Animations/AnimationEffect.h>
#include <LibWeb/Animations/DocumentTimeline.h>
#include <LibWeb/Bindings/PrincipalHostDefined.h>
#include <LibWeb/CSS/AnimationEvent.h>
#include <LibWeb/CSS/CSSAnimation.h>
#include <LibWeb/CSS/CSSFontFaceRule.h>
#include <LibWeb/CSS/CSSImportRule.h>
#include <LibWeb/CSS/CSSLayerBlockRule.h>
#include <LibWeb/CSS/CSSLayerStatementRule.h>
@ -38,7 +31,7 @@
#include <LibWeb/CSS/CSSStyleRule.h>
#include <LibWeb/CSS/CSSTransition.h>
#include <LibWeb/CSS/ComputedProperties.h>
#include <LibWeb/CSS/Fetch.h>
#include <LibWeb/CSS/FontComputer.h>
#include <LibWeb/CSS/Interpolation.h>
#include <LibWeb/CSS/InvalidationSet.h>
#include <LibWeb/CSS/Parser/ArbitrarySubstitutionFunctions.h>
@ -82,17 +75,12 @@
#include <LibWeb/DOM/Document.h>
#include <LibWeb/DOM/Element.h>
#include <LibWeb/DOM/ShadowRoot.h>
#include <LibWeb/Fetch/Infrastructure/FetchController.h>
#include <LibWeb/Fetch/Infrastructure/HTTP/MIME.h>
#include <LibWeb/Fetch/Response.h>
#include <LibWeb/HTML/HTMLBRElement.h>
#include <LibWeb/HTML/HTMLHtmlElement.h>
#include <LibWeb/HTML/HTMLSlotElement.h>
#include <LibWeb/HTML/Parser/HTMLParser.h>
#include <LibWeb/HTML/Scripting/TemporaryExecutionContext.h>
#include <LibWeb/Layout/Node.h>
#include <LibWeb/MimeSniff/MimeType.h>
#include <LibWeb/MimeSniff/Resource.h>
#include <LibWeb/Namespace.h>
#include <LibWeb/Page/Page.h>
#include <LibWeb/Painting/PaintableBox.h>
@ -103,52 +91,6 @@
namespace Web::CSS {
GC_DEFINE_ALLOCATOR(StyleComputer);
GC_DEFINE_ALLOCATOR(FontLoader);
struct FontFaceKey {
NonnullRawPtr<FlyString const> family_name;
int weight { 0 };
int slope { 0 };
};
}
namespace AK {
namespace Detail {
template<>
inline constexpr bool IsHashCompatible<Web::CSS::FontFaceKey, Web::CSS::OwnFontFaceKey> = true;
template<>
inline constexpr bool IsHashCompatible<Web::CSS::OwnFontFaceKey, Web::CSS::FontFaceKey> = true;
}
template<>
struct Traits<Web::CSS::FontFaceKey> : public DefaultTraits<Web::CSS::FontFaceKey> {
static unsigned hash(Web::CSS::FontFaceKey const& key) { return pair_int_hash(key.family_name->hash(), pair_int_hash(key.weight, key.slope)); }
};
template<>
struct Traits<Web::CSS::OwnFontFaceKey> : public DefaultTraits<Web::CSS::OwnFontFaceKey> {
static unsigned hash(Web::CSS::OwnFontFaceKey const& key) { return pair_int_hash(key.family_name.hash(), pair_int_hash(key.weight, key.slope)); }
};
template<>
struct Traits<Web::CSS::FontMatchingAlgorithmCacheKey> : public DefaultTraits<Web::CSS::FontMatchingAlgorithmCacheKey> {
static unsigned hash(Web::CSS::FontMatchingAlgorithmCacheKey const& key)
{
auto hash = key.family_name.hash();
hash = pair_int_hash(hash, key.weight);
hash = pair_int_hash(hash, key.slope);
hash = pair_int_hash(hash, Traits<float>::hash(key.font_size_in_pt));
return hash;
}
};
}
namespace Web::CSS {
CSSStyleProperties const& MatchingRule::declaration() const
{
@ -177,29 +119,6 @@ FlyString const& MatchingRule::qualified_layer_name() const
VERIFY_NOT_REACHED();
}
OwnFontFaceKey::OwnFontFaceKey(FontFaceKey const& other)
: family_name(other.family_name)
, weight(other.weight)
, slope(other.slope)
{
}
OwnFontFaceKey::operator FontFaceKey() const
{
return FontFaceKey {
family_name,
weight,
slope
};
}
[[nodiscard]] bool OwnFontFaceKey::operator==(FontFaceKey const& other) const
{
return family_name == other.family_name
&& weight == other.weight
&& slope == other.slope;
}
StyleComputer::StyleComputer(DOM::Document& document)
: m_document(document)
, m_default_font_metrics(16, Platform::FontPlugin::the().default_font(16)->pixel_metrics(), InitialValues::line_height())
@ -214,148 +133,8 @@ void StyleComputer::visit_edges(Visitor& visitor)
{
Base::visit_edges(visitor);
visitor.visit(m_document);
visitor.visit(m_loaded_fonts);
}
FontLoader::FontLoader(StyleComputer& style_computer, GC::Ptr<CSSStyleSheet> parent_style_sheet, FlyString family_name, Vector<Gfx::UnicodeRange> unicode_ranges, Vector<URL> urls, Function<void(RefPtr<Gfx::Typeface const>)> on_load)
: m_style_computer(style_computer)
, m_parent_style_sheet(parent_style_sheet)
, m_family_name(move(family_name))
, m_unicode_ranges(move(unicode_ranges))
, m_urls(move(urls))
, m_on_load(move(on_load))
{
}
FontLoader::~FontLoader() = default;
void FontLoader::visit_edges(Visitor& visitor)
{
Base::visit_edges(visitor);
visitor.visit(m_style_computer);
visitor.visit(m_parent_style_sheet);
visitor.visit(m_fetch_controller);
}
bool FontLoader::is_loading() const
{
return m_fetch_controller && !m_vector_font;
}
RefPtr<Gfx::Font const> FontLoader::font_with_point_size(float point_size, Gfx::FontVariationSettings const& variations)
{
if (!m_vector_font) {
if (!m_fetch_controller)
start_loading_next_url();
return nullptr;
}
return m_vector_font->font(point_size, variations);
}
void FontLoader::start_loading_next_url()
{
// FIXME: Load local() fonts somehow.
if (m_fetch_controller && m_fetch_controller->state() == Fetch::Infrastructure::FetchController::State::Ongoing)
return;
if (m_urls.is_empty())
return;
// https://drafts.csswg.org/css-fonts-4/#fetch-a-font
// To fetch a font given a selected <url> url for @font-face rule, fetch url, with stylesheet being rules parent
// CSS style sheet, destination "font", CORS mode "cors", and processResponse being the following steps given
// response res and null, failure or a byte stream stream:
auto style_sheet_or_document = m_parent_style_sheet ? StyleSheetOrDocument { *m_parent_style_sheet } : StyleSheetOrDocument { m_style_computer->document() };
m_fetch_controller = fetch_a_style_resource(m_urls.take_first(), style_sheet_or_document, Fetch::Infrastructure::Request::Destination::Font, CorsMode::Cors,
[loader = this](auto response, auto stream) {
// 1. If stream is null, return.
// 2. Load a font from stream according to its type.
// NB: We need to fetch the next source if this one fails to fetch OR decode. So, first try to decode it.
RefPtr<Gfx::Typeface const> typeface;
if (auto* bytes = stream.template get_pointer<ByteBuffer>()) {
if (auto maybe_typeface = loader->try_load_font(response, *bytes); !maybe_typeface.is_error())
typeface = maybe_typeface.release_value();
}
if (!typeface) {
// NB: If we have other sources available, try the next one.
if (loader->m_urls.is_empty()) {
loader->font_did_load_or_fail(nullptr);
} else {
loader->m_fetch_controller = nullptr;
loader->start_loading_next_url();
}
} else {
loader->font_did_load_or_fail(move(typeface));
}
});
if (!m_fetch_controller)
font_did_load_or_fail(nullptr);
}
void FontLoader::font_did_load_or_fail(RefPtr<Gfx::Typeface const> typeface)
{
if (typeface) {
m_vector_font = typeface.release_nonnull();
m_style_computer->did_load_font(m_family_name);
if (m_on_load)
m_on_load(m_vector_font);
} else {
if (m_on_load)
m_on_load(nullptr);
}
m_fetch_controller = nullptr;
}
ErrorOr<NonnullRefPtr<Gfx::Typeface const>> FontLoader::try_load_font(Fetch::Infrastructure::Response const& response, ByteBuffer const& bytes)
{
// FIXME: This could maybe use the format() provided in @font-face as well, since often the mime type is just application/octet-stream and we have to try every format
auto mime_type = Fetch::Infrastructure::extract_mime_type(response.header_list());
if (!mime_type.has_value() || !mime_type->is_font()) {
mime_type = MimeSniff::Resource::sniff(bytes, MimeSniff::SniffingConfiguration { .sniffing_context = MimeSniff::SniffingContext::Font });
}
if (mime_type.has_value()) {
if (mime_type->essence() == "font/ttf"sv || mime_type->essence() == "application/x-font-ttf"sv || mime_type->essence() == "font/otf"sv) {
if (auto result = Gfx::Typeface::try_load_from_temporary_memory(bytes); !result.is_error()) {
return result;
}
}
if (mime_type->essence() == "font/woff"sv || mime_type->essence() == "application/font-woff"sv) {
if (auto result = WOFF::try_load_from_bytes(bytes); !result.is_error()) {
return result;
}
}
if (mime_type->essence() == "font/woff2"sv || mime_type->essence() == "application/font-woff2"sv) {
if (auto result = WOFF2::try_load_from_bytes(bytes); !result.is_error()) {
return result;
}
}
}
return Error::from_string_literal("Automatic format detection failed");
}
struct StyleComputer::MatchingFontCandidate {
FontFaceKey key;
Variant<FontLoaderList*, Gfx::Typeface const*> loader_or_typeface;
[[nodiscard]] RefPtr<Gfx::FontCascadeList const> font_with_point_size(float point_size, Gfx::FontVariationSettings const& variations) const
{
auto font_list = Gfx::FontCascadeList::create();
if (auto* loader_list = loader_or_typeface.get_pointer<FontLoaderList*>(); loader_list) {
for (auto const& loader : **loader_list) {
if (auto font = loader->font_with_point_size(point_size, variations); font)
font_list->add(*font, loader->unicode_ranges());
}
return font_list;
}
font_list->add(loader_or_typeface.get<Gfx::Typeface const*>()->font(point_size, variations));
return font_list;
}
};
Optional<String> StyleComputer::user_agent_style_sheet_source(StringView name)
{
extern String default_stylesheet_source;
@ -1646,115 +1425,6 @@ Length::FontMetrics StyleComputer::calculate_root_element_font_metrics(ComputedP
return font_metrics;
}
RefPtr<Gfx::FontCascadeList const> StyleComputer::find_matching_font_weight_ascending(Vector<MatchingFontCandidate> const& candidates, int target_weight, float font_size_in_pt, Gfx::FontVariationSettings const& variations, bool inclusive)
{
using Fn = AK::Function<bool(MatchingFontCandidate const&)>;
auto pred = inclusive ? Fn([&](auto const& matching_font_candidate) { return matching_font_candidate.key.weight >= target_weight; })
: Fn([&](auto const& matching_font_candidate) { return matching_font_candidate.key.weight > target_weight; });
auto it = find_if(candidates.begin(), candidates.end(), pred);
for (; it != candidates.end(); ++it) {
if (auto found_font = it->font_with_point_size(font_size_in_pt, variations))
return found_font;
}
return {};
}
RefPtr<Gfx::FontCascadeList const> StyleComputer::find_matching_font_weight_descending(Vector<MatchingFontCandidate> const& candidates, int target_weight, float font_size_in_pt, Gfx::FontVariationSettings const& variations, bool inclusive)
{
using Fn = AK::Function<bool(MatchingFontCandidate const&)>;
auto pred = inclusive ? Fn([&](auto const& matching_font_candidate) { return matching_font_candidate.key.weight <= target_weight; })
: Fn([&](auto const& matching_font_candidate) { return matching_font_candidate.key.weight < target_weight; });
auto it = find_if(candidates.rbegin(), candidates.rend(), pred);
for (; it != candidates.rend(); ++it) {
if (auto found_font = it->font_with_point_size(font_size_in_pt, variations))
return found_font;
}
return {};
}
RefPtr<Gfx::FontCascadeList const> StyleComputer::font_matching_algorithm(FlyString const& family_name, int weight, int slope, float font_size_in_pt) const
{
FontMatchingAlgorithmCacheKey key { family_name, weight, slope, font_size_in_pt };
return m_font_matching_algorithm_cache.ensure(key, [&] {
return font_matching_algorithm_impl(family_name, weight, slope, font_size_in_pt);
});
}
// Partial implementation of the font-matching algorithm: https://www.w3.org/TR/css-fonts-4/#font-matching-algorithm
// FIXME: This should be replaced by the full CSS font selection algorithm.
RefPtr<Gfx::FontCascadeList const> StyleComputer::font_matching_algorithm_impl(FlyString const& family_name, int weight, int slope, float font_size_in_pt) const
{
// If a font family match occurs, the user agent assembles the set of font faces in that family and then
// narrows the set to a single face using other font properties in the order given below.
Vector<MatchingFontCandidate> matching_family_fonts;
for (auto const& font_key_and_loader : m_loaded_fonts) {
if (font_key_and_loader.key.family_name.equals_ignoring_ascii_case(family_name))
matching_family_fonts.empend(font_key_and_loader.key, const_cast<FontLoaderList*>(&font_key_and_loader.value));
}
Gfx::FontDatabase::the().for_each_typeface_with_family_name(family_name, [&](Gfx::Typeface const& typeface) {
matching_family_fonts.empend(
FontFaceKey {
.family_name = typeface.family(),
.weight = static_cast<int>(typeface.weight()),
.slope = typeface.slope(),
},
&typeface);
});
quick_sort(matching_family_fonts, [](auto const& a, auto const& b) {
return a.key.weight < b.key.weight;
});
// FIXME: 1. font-stretch is tried first.
// FIXME: 2. font-style is tried next.
// We don't have complete support of italic and oblique fonts, so matching on font-style can be simplified to:
// If a matching slope is found, all faces which don't have that matching slope are excluded from the matching set.
auto style_it = find_if(matching_family_fonts.begin(), matching_family_fonts.end(),
[&](auto const& matching_font_candidate) { return matching_font_candidate.key.slope == slope; });
if (style_it != matching_family_fonts.end()) {
matching_family_fonts.remove_all_matching([&](auto const& matching_font_candidate) {
return matching_font_candidate.key.slope != slope;
});
}
// 3. font-weight is matched next.
// If the desired weight is inclusively between 400 and 500, weights greater than or equal to the target weight
// are checked in ascending order until 500 is hit and checked, followed by weights less than the target weight
// in descending order, followed by weights greater than 500, until a match is found.
Gfx::FontVariationSettings variations;
variations.set_weight(weight);
if (weight >= 400 && weight <= 500) {
auto it = find_if(matching_family_fonts.begin(), matching_family_fonts.end(),
[&](auto const& matching_font_candidate) { return matching_font_candidate.key.weight >= weight; });
for (; it != matching_family_fonts.end() && it->key.weight <= 500; ++it) {
if (auto found_font = it->font_with_point_size(font_size_in_pt, variations))
return found_font;
}
if (auto found_font = find_matching_font_weight_descending(matching_family_fonts, weight, font_size_in_pt, variations, false))
return found_font;
for (; it != matching_family_fonts.end(); ++it) {
if (auto found_font = it->font_with_point_size(font_size_in_pt, variations))
return found_font;
}
}
// If the desired weight is less than 400, weights less than or equal to the desired weight are checked in descending order
// followed by weights above the desired weight in ascending order until a match is found.
if (weight < 400) {
if (auto found_font = find_matching_font_weight_descending(matching_family_fonts, weight, font_size_in_pt, variations, true))
return found_font;
if (auto found_font = find_matching_font_weight_ascending(matching_family_fonts, weight, font_size_in_pt, variations, false))
return found_font;
}
// If the desired weight is greater than 500, weights greater than or equal to the desired weight are checked in ascending order
// followed by weights below the desired weight in descending order until a match is found.
if (weight > 500) {
if (auto found_font = find_matching_font_weight_ascending(matching_family_fonts, weight, font_size_in_pt, variations, true))
return found_font;
if (auto found_font = find_matching_font_weight_descending(matching_family_fonts, weight, font_size_in_pt, variations, false))
return found_font;
}
return {};
}
CSSPixels StyleComputer::default_user_font_size()
{
// FIXME: This value should be configurable by the user.
@ -1811,132 +1481,6 @@ CSSPixels StyleComputer::relative_size_mapping(RelativeSize relative_size, CSSPi
VERIFY_NOT_REACHED();
}
RefPtr<Gfx::FontCascadeList const> StyleComputer::compute_font_for_style_values(StyleValue const& font_family, CSSPixels const& font_size, int slope, double font_weight, Percentage const& font_width, HashMap<FlyString, double> const& font_variation_settings) const
{
// FIXME: We round to int here as that is what is expected by our font infrastructure below
auto width = round_to<int>(font_width.value());
// FIXME: We round to int here as that is what is expected by our font infrastructure below
auto weight = round_to<int>(font_weight);
// FIXME: Implement the full font-matching algorithm: https://www.w3.org/TR/css-fonts-4/#font-matching-algorithm
float const font_size_in_pt = font_size * 0.75f;
auto find_font = [&](FlyString const& family) -> RefPtr<Gfx::FontCascadeList const> {
FontFaceKey key {
.family_name = family,
.weight = weight,
.slope = slope,
};
auto result = Gfx::FontCascadeList::create();
if (auto it = m_loaded_fonts.find(key); it != m_loaded_fonts.end()) {
auto const& loaders = it->value;
Gfx::FontVariationSettings variation;
variation.set_weight(font_weight);
for (auto const& [tag_string, value] : font_variation_settings) {
auto string_view = tag_string.bytes_as_string_view();
if (string_view.length() != 4)
continue;
auto tag = Gfx::FourCC(string_view.characters_without_null_termination());
variation.axes.set(tag, value);
}
for (auto const& loader : loaders) {
if (auto found_font = loader->font_with_point_size(font_size_in_pt, variation))
result->add(*found_font, loader->unicode_ranges());
}
return result;
}
if (auto found_font = font_matching_algorithm(family, weight, slope, font_size_in_pt); found_font && !found_font->is_empty()) {
return found_font;
}
if (auto found_font = Gfx::FontDatabase::the().get(family, font_size_in_pt, weight, width, slope)) {
result->add(*found_font);
return result;
}
return {};
};
auto find_generic_font = [&](Keyword font_id) -> RefPtr<Gfx::FontCascadeList const> {
Platform::GenericFont generic_font {};
switch (font_id) {
case Keyword::Monospace:
case Keyword::UiMonospace:
generic_font = Platform::GenericFont::Monospace;
break;
case Keyword::Serif:
generic_font = Platform::GenericFont::Serif;
break;
case Keyword::Fantasy:
generic_font = Platform::GenericFont::Fantasy;
break;
case Keyword::SansSerif:
generic_font = Platform::GenericFont::SansSerif;
break;
case Keyword::Cursive:
generic_font = Platform::GenericFont::Cursive;
break;
case Keyword::UiSerif:
generic_font = Platform::GenericFont::UiSerif;
break;
case Keyword::UiSansSerif:
generic_font = Platform::GenericFont::UiSansSerif;
break;
case Keyword::UiRounded:
generic_font = Platform::GenericFont::UiRounded;
break;
default:
return {};
}
return find_font(Platform::FontPlugin::the().generic_font_name(generic_font));
};
auto font_list = Gfx::FontCascadeList::create();
for (auto const& family : font_family.as_value_list().values()) {
RefPtr<Gfx::FontCascadeList const> other_font_list;
if (family->is_keyword()) {
other_font_list = find_generic_font(family->to_keyword());
} else if (family->is_string()) {
other_font_list = find_font(family->as_string().string_value());
} else if (family->is_custom_ident()) {
other_font_list = find_font(family->as_custom_ident().custom_ident());
}
if (other_font_list)
font_list->extend(*other_font_list);
}
auto default_font = Platform::FontPlugin::the().default_font(font_size_in_pt);
if (font_list->is_empty()) {
// This is needed to make sure we check default font before reaching to emojis.
font_list->add(*default_font);
}
// Add emoji and symbol fonts
for (auto font_name : Platform::FontPlugin::the().symbol_font_names()) {
if (auto other_font_list = find_font(font_name)) {
font_list->extend(*other_font_list);
}
}
// The default font is already included in the font list, but we explicitly set it
// as the last-resort font. This ensures that if none of the specified fonts contain
// the requested code point, there is still a font available to provide a fallback glyph.
font_list->set_last_resort_font(*default_font);
return font_list;
}
void StyleComputer::compute_font(ComputedProperties& style, Optional<DOM::AbstractElement> abstract_element) const
{
auto const& inheritance_parent = abstract_element.has_value() ? abstract_element->element_to_inherit_style_from() : OptionalNone {};
@ -1985,7 +1529,7 @@ void StyleComputer::compute_font(ComputedProperties& style, Optional<DOM::Abstra
auto const& font_family = style.property(CSS::PropertyID::FontFamily);
auto font_list = compute_font_for_style_values(font_family, style.font_size(), style.font_slope(), style.font_weight(), style.font_width(), style.font_variation_settings());
auto font_list = document().font_computer().compute_font_for_style_values(font_family, style.font_size(), style.font_slope(), style.font_weight(), style.font_width(), style.font_variation_settings());
VERIFY(font_list);
VERIFY(!font_list->is_empty());
@ -2058,13 +1602,6 @@ LogicalAliasMappingContext StyleComputer::compute_logical_alias_mapping_context(
};
}
Gfx::Font const& StyleComputer::initial_font() const
{
// FIXME: This is not correct.
static auto font = ComputedProperties::font_fallback(false, false, 12);
return font;
}
void StyleComputer::compute_property_values(ComputedProperties& style, Optional<DOM::AbstractElement> abstract_element) const
{
Length::FontMetrics font_metrics {
@ -2640,77 +2177,6 @@ static Optional<SimplifiedSelectorForBucketing> is_roundabout_selector_bucketabl
return {};
}
void StyleComputer::did_load_font(FlyString const&)
{
m_font_matching_algorithm_cache = {};
document().invalidate_style(DOM::StyleInvalidationReason::CSSFontLoaded);
}
GC::Ptr<FontLoader> StyleComputer::load_font_face(ParsedFontFace const& font_face, Function<void(RefPtr<Gfx::Typeface const>)> on_load)
{
if (font_face.sources().is_empty()) {
if (on_load)
on_load({});
return {};
}
FontFaceKey key {
.family_name = font_face.font_family(),
.weight = font_face.weight().value_or(0),
.slope = font_face.slope().value_or(0),
};
// FIXME: Pass the sources directly, so the font loader can make use of the format information, or load local fonts.
Vector<URL> urls;
for (auto const& source : font_face.sources()) {
if (source.local_or_url.has<URL>())
urls.append(source.local_or_url.get<URL>());
// FIXME: Handle local()
}
if (urls.is_empty()) {
if (on_load)
on_load({});
return {};
}
auto loader = heap().allocate<FontLoader>(*this, font_face.parent_style_sheet(), font_face.font_family(), font_face.unicode_ranges(), move(urls), move(on_load));
auto& loader_ref = *loader;
auto maybe_font_loaders_list = m_loaded_fonts.get(key);
if (maybe_font_loaders_list.has_value()) {
maybe_font_loaders_list->append(move(loader));
} else {
FontLoaderList loaders;
loaders.append(loader);
m_loaded_fonts.set(OwnFontFaceKey(key), move(loaders));
}
// Actual object owned by font loader list inside m_loaded_fonts, this isn't use-after-move/free
return loader_ref;
}
void StyleComputer::load_fonts_from_sheet(CSSStyleSheet& sheet)
{
for (auto const& rule : sheet.rules()) {
if (!is<CSSFontFaceRule>(*rule))
continue;
auto const& font_face_rule = static_cast<CSSFontFaceRule const&>(*rule);
if (!font_face_rule.is_valid())
continue;
if (auto font_loader = load_font_face(font_face_rule.font_face())) {
sheet.add_associated_font_loader(*font_loader);
}
}
}
void StyleComputer::unload_fonts_from_sheet(CSSStyleSheet& sheet)
{
for (auto& [_, font_loader_list] : m_loaded_fonts) {
font_loader_list.remove_all_matching([&](auto& font_loader) {
return sheet.has_associated_font_loader(*font_loader);
});
}
}
NonnullRefPtr<StyleValue const> StyleComputer::compute_value_of_custom_property(DOM::AbstractElement abstract_element, FlyString const& name, Optional<Parser::GuardedSubstitutionContexts&> guarded_contexts)
{
// https://drafts.csswg.org/css-variables/#propdef-
@ -3432,18 +2898,6 @@ void StyleComputer::pop_ancestor(DOM::Element const& element)
});
}
size_t StyleComputer::number_of_css_font_faces_with_loading_in_progress() const
{
size_t count = 0;
for (auto const& [_, loaders] : m_loaded_fonts) {
for (auto const& loader : loaders) {
if (loader->is_loading())
++count;
}
}
return count;
}
void RuleCache::add_rule(MatchingRule const& matching_rule, Optional<PseudoElement> pseudo_element, bool contains_root_pseudo_class)
{
if (matching_rule.slotted) {

View file

@ -10,8 +10,6 @@
#include <AK/HashMap.h>
#include <AK/Optional.h>
#include <AK/OwnPtr.h>
#include <LibGfx/Font/Typeface.h>
#include <LibGfx/FontCascadeList.h>
#include <LibWeb/Animations/KeyframeEffect.h>
#include <LibWeb/CSS/CSSFontFaceRule.h>
#include <LibWeb/CSS/CSSKeyframesRule.h>
@ -23,7 +21,6 @@
#include <LibWeb/CSS/StyleScope.h>
#include <LibWeb/Export.h>
#include <LibWeb/Forward.h>
#include <LibWeb/Loader/ResourceLoader.h>
namespace Web::CSS {
@ -76,33 +73,6 @@ private:
CounterType m_buckets[bucket_count];
};
struct FontFaceKey;
struct OwnFontFaceKey {
explicit OwnFontFaceKey(FontFaceKey const& other);
operator FontFaceKey() const;
[[nodiscard]] u32 hash() const { return pair_int_hash(family_name.hash(), pair_int_hash(weight, slope)); }
[[nodiscard]] bool operator==(OwnFontFaceKey const& other) const = default;
[[nodiscard]] bool operator==(FontFaceKey const& other) const;
FlyString family_name;
int weight { 0 };
int slope { 0 };
};
struct FontMatchingAlgorithmCacheKey {
FlyString family_name;
int weight;
int slope;
float font_size_in_pt;
[[nodiscard]] bool operator==(FontMatchingAlgorithmCacheKey const& other) const = default;
};
class FontLoader;
class WEB_API StyleComputer final : public GC::Cell {
GC_CELL(StyleComputer, GC::Cell);
GC_DECLARE_ALLOCATOR(StyleComputer);
@ -138,27 +108,15 @@ public:
InvalidationSet invalidation_set_for_properties(Vector<InvalidationSet::Property> const&, StyleScope const&) const;
bool invalidation_property_used_in_has_selector(InvalidationSet::Property const&, StyleScope const&) const;
Gfx::Font const& initial_font() const;
void did_load_font(FlyString const& family_name);
GC::Ptr<FontLoader> load_font_face(ParsedFontFace const&, ESCAPING Function<void(RefPtr<Gfx::Typeface const>)> on_load = {});
void load_fonts_from_sheet(CSSStyleSheet&);
void unload_fonts_from_sheet(CSSStyleSheet&);
static CSSPixels default_user_font_size();
static CSSPixels absolute_size_mapping(AbsoluteSize, CSSPixels default_font_size);
static CSSPixels relative_size_mapping(RelativeSize, CSSPixels inherited_font_size);
RefPtr<Gfx::FontCascadeList const> compute_font_for_style_values(StyleValue const& font_family, CSSPixels const& font_size, int font_slope, double font_weight, Percentage const& font_width, HashMap<FlyString, double> const& font_variation_settings) const;
[[nodiscard]] RefPtr<StyleValue const> recascade_font_size_if_needed(DOM::AbstractElement, CascadedProperties&) const;
void set_viewport_rect(Badge<DOM::Document>, CSSPixelRect const& viewport_rect) { m_viewport_rect = viewport_rect; }
void collect_animation_into(DOM::AbstractElement, GC::Ref<Animations::KeyframeEffect> animation, ComputedProperties&) const;
size_t number_of_css_font_faces_with_loading_in_progress() const;
[[nodiscard]] GC::Ref<ComputedProperties> compute_properties(DOM::AbstractElement, CascadedProperties&) const;
void compute_property_values(ComputedProperties&, Optional<DOM::AbstractElement>) const;
@ -190,8 +148,6 @@ private:
CreatePseudoElementStyleIfNeeded,
};
struct MatchingFontCandidate;
struct LayerMatchingRules {
FlyString qualified_layer_name;
Vector<MatchingRule const*> rules;
@ -208,10 +164,6 @@ private:
LogicalAliasMappingContext compute_logical_alias_mapping_context(DOM::AbstractElement, ComputeStyleMode, MatchingRuleSet const&) const;
[[nodiscard]] GC::Ptr<ComputedProperties> compute_style_impl(DOM::AbstractElement, ComputeStyleMode, Optional<bool&> did_change_custom_properties, StyleScope const&) const;
[[nodiscard]] GC::Ref<CascadedProperties> compute_cascaded_values(DOM::AbstractElement, bool did_match_any_pseudo_element_rules, ComputeStyleMode, MatchingRuleSet const&, Optional<LogicalAliasMappingContext>, ReadonlySpan<PropertyID> properties_to_cascade) const;
static RefPtr<Gfx::FontCascadeList const> find_matching_font_weight_ascending(Vector<MatchingFontCandidate> const& candidates, int target_weight, float font_size_in_pt, Gfx::FontVariationSettings const& variations, bool inclusive);
static RefPtr<Gfx::FontCascadeList const> find_matching_font_weight_descending(Vector<MatchingFontCandidate> const& candidates, int target_weight, float font_size_in_pt, Gfx::FontVariationSettings const& variations, bool inclusive);
RefPtr<Gfx::FontCascadeList const> font_matching_algorithm(FlyString const& family_name, int weight, int slope, float font_size_in_pt) const;
RefPtr<Gfx::FontCascadeList const> font_matching_algorithm_impl(FlyString const& family_name, int weight, int slope, float font_size_in_pt) const;
void compute_custom_properties(ComputedProperties&, DOM::AbstractElement) const;
void compute_math_depth(ComputedProperties&, Optional<DOM::AbstractElement>) const;
void start_needed_transitions(ComputedProperties const& old_style, ComputedProperties& new_style, DOM::AbstractElement) const;
@ -236,51 +188,12 @@ private:
[[nodiscard]] RuleCache const* rule_cache_for_cascade_origin(CascadeOrigin, Optional<FlyString const> qualified_layer_name, GC::Ptr<DOM::ShadowRoot const>) const;
using FontLoaderList = Vector<GC::Ref<FontLoader>>;
HashMap<OwnFontFaceKey, FontLoaderList> m_loaded_fonts;
Length::FontMetrics m_default_font_metrics;
Length::FontMetrics m_root_element_font_metrics;
CSSPixelRect m_viewport_rect;
OwnPtr<CountingBloomFilter<u8, 14>> m_ancestor_filter;
mutable HashMap<FontMatchingAlgorithmCacheKey, RefPtr<Gfx::FontCascadeList const>> m_font_matching_algorithm_cache;
};
class FontLoader final : public GC::Cell {
GC_CELL(FontLoader, GC::Cell);
GC_DECLARE_ALLOCATOR(FontLoader);
public:
FontLoader(StyleComputer& style_computer, GC::Ptr<CSSStyleSheet> parent_style_sheet, FlyString family_name, Vector<Gfx::UnicodeRange> unicode_ranges, Vector<URL> urls, ESCAPING Function<void(RefPtr<Gfx::Typeface const>)> on_load = {});
virtual ~FontLoader();
Vector<Gfx::UnicodeRange> const& unicode_ranges() const { return m_unicode_ranges; }
RefPtr<Gfx::Typeface const> vector_font() const { return m_vector_font; }
RefPtr<Gfx::Font const> font_with_point_size(float point_size, Gfx::FontVariationSettings const& variations = {});
void start_loading_next_url();
bool is_loading() const;
private:
virtual void visit_edges(Visitor&) override;
ErrorOr<NonnullRefPtr<Gfx::Typeface const>> try_load_font(Fetch::Infrastructure::Response const&, ByteBuffer const&);
void font_did_load_or_fail(RefPtr<Gfx::Typeface const>);
GC::Ref<StyleComputer> m_style_computer;
GC::Ptr<CSSStyleSheet> m_parent_style_sheet;
FlyString m_family_name;
Vector<Gfx::UnicodeRange> m_unicode_ranges;
RefPtr<Gfx::Typeface const> m_vector_font;
Vector<URL> m_urls;
GC::Ptr<Fetch::Infrastructure::FetchController> m_fetch_controller;
Function<void(RefPtr<Gfx::Typeface const>)> m_on_load;
};
inline bool StyleComputer::should_reject_with_ancestor_filter(Selector const& selector) const

View file

@ -40,6 +40,7 @@
#include <LibWeb/CSS/CSSImportRule.h>
#include <LibWeb/CSS/CSSTransition.h>
#include <LibWeb/CSS/ComputedProperties.h>
#include <LibWeb/CSS/FontComputer.h>
#include <LibWeb/CSS/FontFaceSet.h>
#include <LibWeb/CSS/MediaQueryList.h>
#include <LibWeb/CSS/MediaQueryListEvent.h>
@ -467,6 +468,7 @@ Document::Document(JS::Realm& realm, URL::URL const& url, TemporaryDocumentForFr
: ParentNode(realm, *this, NodeType::DOCUMENT_NODE)
, m_page(Bindings::principal_host_defined_page(realm))
, m_style_computer(realm.heap().allocate<CSS::StyleComputer>(*this))
, m_font_computer(realm.heap().allocate<CSS::FontComputer>(*this))
, m_url(url)
, m_temporary_document_for_fragment_parsing(temporary_document_for_fragment_parsing)
, m_editing_host_manager(EditingHostManager::create(realm, *this))
@ -569,6 +571,7 @@ void Document::visit_edges(Cell::Visitor& visitor)
visitor.visit(m_pending_parsing_blocking_script);
visitor.visit(m_history);
visitor.visit(m_style_computer);
visitor.visit(m_font_computer);
visitor.visit(m_browsing_context);
visitor.visit(m_applets);

View file

@ -260,6 +260,9 @@ public:
CSS::StyleComputer& style_computer() { return *m_style_computer; }
CSS::StyleComputer const& style_computer() const { return *m_style_computer; }
CSS::FontComputer& font_computer() { return *m_font_computer; }
CSS::FontComputer const& font_computer() const { return *m_font_computer; }
CSS::StyleSheetList& style_sheets();
CSS::StyleSheetList const& style_sheets() const;
@ -1010,6 +1013,7 @@ private:
GC::Ref<Page> m_page;
GC::Ptr<CSS::StyleComputer> m_style_computer;
GC::Ptr<CSS::FontComputer> m_font_computer;
GC::Ptr<CSS::StyleSheetList> m_style_sheets;
GC::Ptr<Node> m_active_favicon;
GC::Ptr<HTML::BrowsingContext> m_browsing_context;

View file

@ -16,6 +16,7 @@
namespace Web {
class CSSPixels;
class DisplayListRecordingContext;
class DragAndDropEventHandler;
class EventHandler;
@ -294,6 +295,7 @@ class FitContentStyleValue;
class Flex;
class FlexOrCalculated;
class FlexStyleValue;
class FontComputer;
class FontFace;
class FontFaceSet;
class FontSourceStyleValue;

View file

@ -7,6 +7,7 @@
#include "CanvasTextDrawingStyles.h"
#include <LibWeb/CSS/ComputedProperties.h>
#include <LibWeb/CSS/FontComputer.h>
#include <LibWeb/CSS/Parser/Parser.h>
#include <LibWeb/CSS/StyleComputer.h>
#include <LibWeb/CSS/StyleValues/FontStyleStyleValue.h>
@ -142,7 +143,7 @@ void CanvasTextDrawingStyles<IncludingClass, CanvasType>::set_font(StringView fo
auto const& computed_font_width = CSS::StyleComputer::compute_font_width(font_width, computation_context);
auto const& computed_font_style = CSS::StyleComputer::compute_font_style(font_style, computation_context);
return document->style_computer().compute_font_for_style_values(
return document->font_computer().compute_font_for_style_values(
font_family,
computed_font_size->as_length().length().absolute_length_to_px(),
computed_font_style->as_font_style().to_font_slope(),

View file

@ -8,8 +8,8 @@
#include <LibCore/EventLoop.h>
#include <LibJS/Runtime/VM.h>
#include <LibWeb/Bindings/MainThreadVM.h>
#include <LibWeb/CSS/FontComputer.h>
#include <LibWeb/CSS/FontFaceSet.h>
#include <LibWeb/CSS/StyleComputer.h>
#include <LibWeb/DOM/Document.h>
#include <LibWeb/DOM/Element.h>
#include <LibWeb/HTML/BrowsingContext.h>
@ -508,7 +508,7 @@ void EventLoop::update_the_rendering()
}
for (auto& document : docs) {
if (document->readiness() == HTML::DocumentReadyState::Complete && document->style_computer().number_of_css_font_faces_with_loading_in_progress() == 0) {
if (document->readiness() == HTML::DocumentReadyState::Complete && document->font_computer().number_of_css_font_faces_with_loading_in_progress() == 0) {
HTML::TemporaryExecutionContext context(document->realm(), HTML::TemporaryExecutionContext::CallbacksEnabled::Yes);
document->fonts()->resolve_ready_promise();
}