script: Implement the FontFaceSet DOM API (#32576)

Add a skeleton implementation of FontFaceSet interface with support
for resolving the `document.fonts.ready` Promise when the loading of
web fonts is completed.

This change exposes new failures in the web platform tests.

These were ERROR before the change because `document.fonts.ready` caused
a `ReferenceError` causing the tests to be aborted and they now FAIL:
- /css/CSS2/linebox/vertical-align-top-bottom-001.html
- /css/css-flexbox/flex-one-sets-flex-basis-to-zero-px.html
- /css/css-fonts/generic-family-keywords-001.html
- /css/css-fonts/math-script-level-and-math-style/math-script-level-004.tentative.html
- /css/css-fonts/math-script-level-and-math-style/math-script-level-002.tentative.html
- /css/css-text/text-autospace/text-autospace-ligature-001.html
- /css/css-values/calc-size/calc-size-width.tentative.html

These were TIMEOUT before the change because `document.fonts.ready` was
a ReferenceError and the tests were asynchronous (reftest-wait). These now
FAIL because the assertions are now executed after fonts are loaded:
- /css/css-fonts/matching/fixed-stretch-style-over-weight.html
- /css/css-fonts/matching/range-descriptor-reversed.html
- /css/css-fonts/matching/stretch-distance-over-weight-distance.html
- /css/css-fonts/matching/style-ranges-over-weight-direction.html
- /css/css-fonts/variations/variable-box-font.html
- /css/css-fonts/variations/variable-gpos-m2b.html
- /css/css-fonts/variations/variable-gsub.html
- /css/css-fonts/variations/variable-opsz-size-adjust.html
- /css/css-position/sticky/position-sticky-change-top.html
- /css/css-position/sticky/position-sticky-fixed-ancestor.html
- /css/css-position/sticky/position-sticky-flexbox.html
- /css/css-position/sticky/position-sticky-grid.html
- /css/css-position/sticky/position-sticky-inline.html
- /css/css-position/sticky/position-sticky-rendering.html
- /css/css-position/sticky/position-sticky-stacking-context.html
- /css/css-position/sticky/position-sticky-table-td-left.html
- /css/css-position/sticky/position-sticky-table-td-right.html
- /css/css-position/sticky/position-sticky-table-tfoot-bottom.html
- /css/css-position/sticky/position-sticky-table-th-right.html
- /css/css-position/sticky/position-sticky-table-thead-top.html
- /css/css-position/sticky/position-sticky-table-tr-bottom.html
- /css/css-position/sticky/position-sticky-table-tr-top.html
- /css/css-position/sticky/position-sticky-writing-modes.html
- /css/css-pseudo/marker-intrinsic-contribution-001.html
- /css/css-text/hyphens/hyphens-character.html

These tests now PASS due to this patch:
* FAIL -> PASS
  - /html/canvas/element/text/2d.text.draw.fill.maxWidth.fontface.html
  - /html/canvas/element/text/2d.text.measure.width.empty.html
* TIMEOUT -> PASS
  - /css/css-fonts/variations/font-descriptor-range-reversed.html
  - /css/css-fonts/variations/variable-opsz.html
  - /css/css-position/sticky/position-sticky-table-th-left.html
* ERROR -> PASS
  - /css/css-fonts/generic-family-keywords-002.html
  - /css/css-fonts/generic-family-keywords-003.html
* These two tests only PASS in Layout 2020:
  - /css/CSS2/positioning/inline-static-position-001.html
  - /css/cssom-view/getBoundingClientRect-empty-inline.html

These two tests have subtests that PASS intermittenttly:
- /fetch/metadata/generated/css-font-face.sub.tentative.html
- /css/css-fonts/generic-family-keywords-001.html

These tests are new TIMEOUTS that used to FAIL because
`documents.fonts.ready` was undefined:
- /resource-timing/TAO-match.html
- /resource-timing/content-type.html
- /resource-timing/nextHopProtocol-is-tao-protected.https.html

The failure in `/resize-observer/change-layout-in-error.html` could be
due to an issue in the ResizeObserver implementation that is now exposed
with this change, but this needs more investigation.

Signed-off-by: Mukilan Thiyagarajan <mukilan@igalia.com>
Co-authored-by: Martin Robinson <mrobinson@igalia.com>
This commit is contained in:
Mukilan Thiyagarajan 2024-06-26 15:14:47 +05:30 committed by GitHub
parent 7ea894774f
commit a730469b70
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
95 changed files with 642 additions and 212 deletions

View file

@ -121,6 +121,7 @@ use crate::dom::element::{
use crate::dom::event::{Event, EventBubbles, EventCancelable, EventDefault, EventStatus};
use crate::dom::eventtarget::EventTarget;
use crate::dom::focusevent::FocusEvent;
use crate::dom::fontfaceset::FontFaceSet;
use crate::dom::globalscope::GlobalScope;
use crate::dom::gpucanvascontext::{GPUCanvasContext, WebGPUContextId};
use crate::dom::hashchangeevent::HashChangeEvent;
@ -466,6 +467,9 @@ pub struct Document {
/// - <https://bugzilla.mozilla.org/show_bug.cgi?id=1596992>
/// - <https://github.com/w3c/csswg-drafts/issues/4518>
resize_observers: DomRefCell<Vec<Dom<ResizeObserver>>>,
/// The set of all fonts loaded by this document.
/// <https://drafts.csswg.org/css-font-loading/#font-face-source>
fonts: MutNullableDom<FontFaceSet>,
}
#[derive(JSTraceable, MallocSizeOf)]
@ -3291,6 +3295,7 @@ impl Document {
pending_compositor_events: Default::default(),
mouse_move_event_index: Default::default(),
resize_observers: Default::default(),
fonts: Default::default(),
}
}
@ -5364,6 +5369,12 @@ impl DocumentMethods for Document {
None
}
}
// https://drafts.csswg.org/css-font-loading/#font-face-source
fn Fonts(&self) -> DomRoot<FontFaceSet> {
self.fonts
.or_init(|| FontFaceSet::new(&*self.global(), None))
}
}
fn update_with_current_time_ms(marker: &Cell<u64>) {

View file

@ -0,0 +1,50 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use std::rc::Rc;
use dom_struct::dom_struct;
use js::rust::HandleObject;
use crate::dom::bindings::codegen::Bindings::FontFaceSetBinding::FontFaceSetMethods;
use crate::dom::bindings::reflector::reflect_dom_object_with_proto;
use crate::dom::bindings::root::DomRoot;
use crate::dom::eventtarget::EventTarget;
use crate::dom::globalscope::GlobalScope;
use crate::dom::promise::Promise;
use crate::realms::enter_realm;
#[dom_struct]
pub struct FontFaceSet {
target: EventTarget,
#[ignore_malloc_size_of = "Rc"]
promise: Rc<Promise>,
}
impl FontFaceSet {
pub fn new_inherited(global: &GlobalScope) -> Self {
FontFaceSet {
target: EventTarget::new_inherited(),
promise: Promise::new(global),
}
}
pub fn new(global: &GlobalScope, proto: Option<HandleObject>) -> DomRoot<Self> {
reflect_dom_object_with_proto(Box::new(FontFaceSet::new_inherited(global)), global, proto)
}
pub fn fulfill_ready_promise_if_needed(&self) {
if !self.promise.is_fulfilled() {
let _ac = enter_realm(&*self.promise);
self.promise.resolve_native(self);
}
}
}
impl FontFaceSetMethods for FontFaceSet {
/// https://drafts.csswg.org/css-font-loading/#dom-fontfaceset-ready
fn Ready(&self) -> Rc<Promise> {
self.promise.clone()
}
}

View file

@ -313,6 +313,7 @@ pub mod filelist;
pub mod filereader;
pub mod filereadersync;
pub mod focusevent;
pub mod fontfaceset;
pub mod formdata;
pub mod formdataevent;
pub mod gainnode;

View file

@ -0,0 +1,49 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
/*
dictionary FontFaceSetLoadEventInit : EventInit {
sequence<FontFace> fontfaces = [];
};
[Exposed=(Window,Worker)]
interface FontFaceSetLoadEvent : Event {
constructor(DOMString type, optional FontFaceSetLoadEventInit eventInitDict = {});
// WebIDL needs to support FrozenArray & SameObject
// [SameObject] readonly attribute FrozenArray<FontFace> fontfaces;
readonly attribute any fontfaces;
};
enum FontFaceSetLoadStatus { "loading" , "loaded" };
*/
// https://drafts.csswg.org/css-font-loading/#FontFaceSet-interface
[Exposed=(Window,Worker)]
interface FontFaceSet : EventTarget {
// constructor(sequence<FontFace> initialFaces);
// setlike<FontFace>;
// FontFaceSet add(FontFace font);
// boolean delete(FontFace font);
// undefined clear();
// events for when loading state changes
// attribute EventHandler onloading;
// attribute EventHandler onloadingdone;
// attribute EventHandler onloadingerror;
// check and start loads if appropriate
// and fulfill promise when all loads complete
// Promise<sequence<FontFace>> load(DOMString font, optional DOMString text = " ");
// return whether all fonts in the fontlist are loaded
// (does not initiate load if not available)
// boolean check(DOMString font, optional DOMString text = " ");
// async notification that font loading and layout operations are done
readonly attribute Promise<FontFaceSet> ready;
// loading state, "loading" while one or more fonts loading, "loaded" otherwise
// readonly attribute FontFaceSetLoadStatus status;
};

View file

@ -0,0 +1,11 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
// https://drafts.csswg.org/css-font-loading/#font-face-source
interface mixin FontFaceSource {
readonly attribute FontFaceSet fonts;
};
Document includes FontFaceSource;
// WorkerGlobalScope includes FontFaceSource;

View file

@ -1952,6 +1952,10 @@ impl Window {
/// may happen in the only case a query reflow may bail out, that is, if the
/// viewport size is not present). See #11223 for an example of that.
pub fn reflow(&self, reflow_goal: ReflowGoal, reason: ReflowReason) -> bool {
// Fetch the pending web fonts before layout, in case a font loads during
// the layout.
let pending_web_fonts = self.layout.borrow().waiting_for_web_fonts_to_load();
self.Document().ensure_safe_to_run_script_or_layout();
let for_display = reflow_goal == ReflowGoal::Full;
@ -1980,6 +1984,24 @@ impl Window {
);
}
let document = self.Document();
let font_face_set = document.Fonts();
let is_ready_state_complete = document.ReadyState() == DocumentReadyState::Complete;
// From https://drafts.csswg.org/css-font-loading/#font-face-set-ready:
// > A FontFaceSet is pending on the environment if any of the following are true:
// > - the document is still loading
// > - the document has pending stylesheet requests
// > - the document has pending layout operations which might cause the user agent to request
// > a font, or which depend on recently-loaded fonts
//
// Thus, we are queueing promise resolution here. This reflow should have been triggered by
// a "rendering opportunity" in `ScriptThread::handle_web_font_loaded, which should also
// make sure a microtask checkpoint happens, triggering the promise callback.
if !pending_web_fonts && is_ready_state_complete {
font_face_set.fulfill_ready_promise_if_needed();
}
// If writing a screenshot, check if the script has reached a state
// where it's safe to write the image. This means that:
// 1) The reflow is for display (otherwise it could be a query)
@ -1989,8 +2011,6 @@ impl Window {
// that this pipeline is ready to write the image (from the script thread
// perspective at least).
if self.prepare_for_screenshot && for_display {
let document = self.Document();
// Checks if the html element has reftest-wait attribute present.
// See http://testthewebforward.org/docs/reftests.html
let html_element = document.GetDocumentElement();
@ -1998,9 +2018,7 @@ impl Window {
elem.has_class(&atom!("reftest-wait"), CaseSensitivity::CaseSensitive)
});
let pending_web_fonts = self.layout.borrow().waiting_for_web_fonts_to_load();
let has_sent_idle_message = self.has_sent_idle_message.get();
let is_ready_state_complete = document.ReadyState() == DocumentReadyState::Complete;
let pending_images = !self.pending_layout_images.borrow().is_empty();
if !has_sent_idle_message &&

View file

@ -2125,7 +2125,7 @@ impl ScriptThread {
FocusIFrame(id, ..) => Some(id),
WebDriverScriptCommand(id, ..) => Some(id),
TickAllAnimations(id, ..) => Some(id),
WebFontLoaded(id) => Some(id),
WebFontLoaded(id, ..) => Some(id),
DispatchIFrameLoadEvent {
target: _,
parent: id,
@ -2325,8 +2325,8 @@ impl ScriptThread {
ConstellationControlMsg::WebDriverScriptCommand(pipeline_id, msg) => {
self.handle_webdriver_msg(pipeline_id, msg)
},
ConstellationControlMsg::WebFontLoaded(pipeline_id) => {
self.handle_web_font_loaded(pipeline_id)
ConstellationControlMsg::WebFontLoaded(pipeline_id, success) => {
self.handle_web_font_loaded(pipeline_id, success)
},
ConstellationControlMsg::DispatchIFrameLoadEvent {
target: browsing_context_id,
@ -3301,7 +3301,7 @@ impl ScriptThread {
}
/// Handles a Web font being loaded. Does nothing if the page no longer exists.
fn handle_web_font_loaded(&self, pipeline_id: PipelineId) {
fn handle_web_font_loaded(&self, pipeline_id: PipelineId, _success: bool) {
let Some(document) = self.documents.borrow().find_document(pipeline_id) else {
warn!("Web font loaded in closed pipeline {}.", pipeline_id);
return;
@ -3310,6 +3310,12 @@ impl ScriptThread {
// TODO: This should only dirty nodes that are waiting for a web font to finish loading!
document.dirty_all_nodes();
document.window().add_pending_reflow();
// This is required because the handlers added to the promise exposed at
// `document.fonts.ready` are run by the event loop only when it performs a microtask
// checkpoint. Without the call below, this never happens and the promise is 'stuck' waiting
// to be resolved until another event forces a microtask checkpoint.
self.rendering_opportunity(pipeline_id);
}
/// Handles a worklet being loaded. Does nothing if the page no longer exists.