diff --git a/Libraries/LibGfx/FontCascadeList.cpp b/Libraries/LibGfx/FontCascadeList.cpp index 3b4991c6f74..8b812c32f48 100644 --- a/Libraries/LibGfx/FontCascadeList.cpp +++ b/Libraries/LibGfx/FontCascadeList.cpp @@ -34,13 +34,45 @@ void FontCascadeList::add(NonnullRefPtr font, Vector u } }); } +void FontCascadeList::add_pending_face(Vector unicode_ranges, Function start_load) +{ + if (unicode_ranges.is_empty()) + return; + + u32 lowest_code_point = 0xFFFFFFFF; + u32 highest_code_point = 0; + for (auto const& range : unicode_ranges) { + lowest_code_point = min(lowest_code_point, range.min_code_point()); + highest_code_point = max(highest_code_point, range.max_code_point()); + } + + m_pending_faces.append(adopt_ref(*new PendingFace( + UnicodeRange { lowest_code_point, highest_code_point }, + move(unicode_ranges), + move(start_load)))); +} + void FontCascadeList::extend(FontCascadeList const& other) { m_fonts.extend(other.m_fonts); + m_pending_faces.extend(other.m_pending_faces); } Gfx::Font const& FontCascadeList::font_for_code_point(u32 code_point) const { + // Walk pending entries first: if this codepoint falls in an unloaded face's + // unicode-range we kick off the fetch and drop the entry — a fallback font that + // happens to cover the codepoint shouldn't prevent the real face from loading. + // FontComputer::clear_computed_font_cache() rebuilds the cascade once the fetch + // completes, so later shapes pick up the loaded face. Run before the ASCII cache + // lookup so a previously-cached codepoint still triggers a newly-added face. + m_pending_faces.remove_all_matching([code_point](auto const& pending) { + if (!pending->covers(code_point)) + return false; + pending->start_load(); + return true; + }); + if (code_point < m_ascii_cache.size()) { if (auto const* cached = m_ascii_cache[code_point]) return *cached; diff --git a/Libraries/LibGfx/FontCascadeList.h b/Libraries/LibGfx/FontCascadeList.h index 1f053f9e4c2..82ae3597ddb 100644 --- a/Libraries/LibGfx/FontCascadeList.h +++ b/Libraries/LibGfx/FontCascadeList.h @@ -8,6 +8,7 @@ #include #include +#include #include #include @@ -23,7 +24,7 @@ public: } size_t size() const { return m_fonts.size(); } - bool is_empty() const { return m_fonts.is_empty() && !m_last_resort_font; } + bool is_empty() const { return m_fonts.is_empty() && m_pending_faces.is_empty() && !m_last_resort_font; } Font const& first() const { return !m_fonts.is_empty() ? *m_fonts.first().font : *m_last_resort_font; } template @@ -36,6 +37,10 @@ public: void add(NonnullRefPtr font); void add(NonnullRefPtr font, Vector unicode_ranges); + // Register an unloaded face covering `unicode_ranges`. The cascade invokes + // `start_load` the first time a rendered codepoint falls within one of the ranges. + void add_pending_face(Vector unicode_ranges, Function start_load); + void extend(FontCascadeList const& other); Gfx::Font const& font_for_code_point(u32 code_point) const; @@ -53,6 +58,34 @@ public: Optional range_data; }; + class PendingFace : public RefCounted { + public: + PendingFace(UnicodeRange enclosing, Vector ranges, Function start_load) + : m_enclosing_range(enclosing) + , m_unicode_ranges(move(ranges)) + , m_start_load(move(start_load)) + { + } + + bool covers(u32 code_point) const + { + if (!m_enclosing_range.contains(code_point)) + return false; + for (auto const& range : m_unicode_ranges) { + if (range.contains(code_point)) + return true; + } + return false; + } + + void start_load() { m_start_load(); } + + private: + UnicodeRange m_enclosing_range; + Vector m_unicode_ranges; + Function m_start_load; + }; + void set_last_resort_font(NonnullRefPtr font) { m_last_resort_font = move(font); } void set_system_font_fallback_callback(SystemFontFallbackCallback callback) { m_system_font_fallback_callback = move(callback); } @@ -67,6 +100,7 @@ public: private: RefPtr m_last_resort_font; mutable Vector m_fonts; + mutable Vector> m_pending_faces; SystemFontFallbackCallback m_system_font_fallback_callback; // OPTIMIZATION: Cache of resolved fonts for ASCII code points. Since m_fonts only grows and the cascade returns diff --git a/Libraries/LibWeb/CSS/FontComputer.cpp b/Libraries/LibWeb/CSS/FontComputer.cpp index 84354531c2b..d8820b45845 100644 --- a/Libraries/LibWeb/CSS/FontComputer.cpp +++ b/Libraries/LibWeb/CSS/FontComputer.cpp @@ -268,8 +268,18 @@ struct FontComputer::MatchingFontCandidate { auto font_list = Gfx::FontCascadeList::create(); for (auto const& face : it->value) { - if (auto face_fonts = face->font_with_point_size(point_size, variations, shape_features)) + if (auto face_fonts = face->font_with_point_size(point_size, variations, shape_features)) { font_list->extend(*face_fonts); + continue; + } + // Unloaded subset face: surface it as a pending entry so the fetch only + // fires once font_for_code_point() sees a codepoint in its unicode-range. + if (face->has_urls() && face->has_non_default_unicode_range()) { + GC::Root rooted_face(*face); + font_list->add_pending_face(face->unicode_ranges(), [rooted_face = move(rooted_face)] { + const_cast(*rooted_face).load(); + }); + } } if (font_list->is_empty()) return {}; @@ -771,12 +781,18 @@ void FontComputer::load_fonts_from_sheet(CSSStyleSheet& sheet) auto font_face = FontFace::create_css_connected(document().realm(), *font_face_rule); document().fonts()->add_css_connected_font(font_face); - // NB: Load via FontFace::load(), to satisfy this requirement: - // https://drafts.csswg.org/css-font-loading/#font-face-load - // User agents can initiate font loads on their own, whenever they determine that a given font face is - // necessary to render something on the page. When this happens, they must act as if they had called the - // corresponding FontFace’s load() method described here. - font_face->load(); + if (font_face->has_non_default_unicode_range()) { + // Register for matching, but defer loading until a rendered codepoint + // actually falls in this face's unicode-range. + register_font_face(font_face); + } else { + // NB: Load via FontFace::load(), to satisfy this requirement: + // https://drafts.csswg.org/css-font-loading/#font-face-load + // User agents can initiate font loads on their own, whenever they determine that a given font face is + // necessary to render something on the page. When this happens, they must act as if they had called the + // corresponding FontFace’s load() method described here. + font_face->load(); + } } if (auto* font_feature_values_rule = as_if(*rule)) diff --git a/Libraries/LibWeb/CSS/FontFace.h b/Libraries/LibWeb/CSS/FontFace.h index 6689f7834c9..982c79daf1c 100644 --- a/Libraries/LibWeb/CSS/FontFace.h +++ b/Libraries/LibWeb/CSS/FontFace.h @@ -100,6 +100,17 @@ public: RefPtr font_with_point_size(float point_size, Gfx::FontVariationSettings const&, Gfx::ShapeFeatures const&) const; + Vector const& unicode_ranges() const { return m_unicode_ranges; } + bool has_urls() const { return !m_urls.is_empty(); } + + bool has_non_default_unicode_range() const + { + if (m_unicode_ranges.size() != 1) + return true; + auto const& range = m_unicode_ranges.first(); + return range.min_code_point() != 0 || range.max_code_point() != 0x10FFFF; + } + Bindings::FontFaceLoadStatus status() const { return m_status; } GC::Ref load(); diff --git a/Libraries/LibWeb/CSS/FontFaceSet.cpp b/Libraries/LibWeb/CSS/FontFaceSet.cpp index 8eb1f2d0d4f..f35a4d4f8bd 100644 --- a/Libraries/LibWeb/CSS/FontFaceSet.cpp +++ b/Libraries/LibWeb/CSS/FontFaceSet.cpp @@ -379,6 +379,11 @@ void FontFaceSet::set_is_pending_on_the_environment(bool is_pending_on_the_envir // Spec issue: https://github.com/w3c/csswg-drafts/issues/13538#issuecomment-3933951987 if (m_set_entries->set_size() == 0 || (m_is_stuck_on_the_environment && m_loading_fonts.is_empty())) switch_to_loaded(); + // AD-HOC: Also switch when nothing has ever entered the LoadingFonts list — an empty set, or a set whose + // entries all have deferred unicode-ranges that no rendered codepoint matched. Without this the + // ready promise stays pending forever. + else if (m_loading_fonts.is_empty()) + switch_to_loaded(); // 2. If the FontFaceSet is stuck on the environment, unmark it as such. m_is_stuck_on_the_environment = false; diff --git a/Tests/LibWeb/Text/expected/css/font-face-unicode-range-load-gating.txt b/Tests/LibWeb/Text/expected/css/font-face-unicode-range-load-gating.txt index 696dc77bd62..529eac96a4f 100644 --- a/Tests/LibWeb/Text/expected/css/font-face-unicode-range-load-gating.txt +++ b/Tests/LibWeb/Text/expected/css/font-face-unicode-range-load-gating.txt @@ -1,4 +1,4 @@ MultiFont face count: 3 range=U+41 status=loaded -range=U+42 status=error -range=U+43 status=error +range=U+42 status=unloaded +range=U+43 status=unloaded