mirror of
https://github.com/lexmount/moli.git
synced 2026-10-30 16:07:24 +00:00
fix(fonts): settle document fonts ready after loading and layout invalidation
Validation on this branch: cargo fmt --all; cargo clippy --workspace --all-targets --all-features -- -D warnings. Nextest and WPT reruns omitted at the user's request. Includes the ablation-confirmed cleanup from 0e3ce99dcf.
This commit is contained in:
@@ -146,7 +146,9 @@ pub(crate) use crypto::{
|
||||
CryptoKeyAlgorithmClonePayload, CryptoKeyClonePayload, WebCryptoRejection, WebCryptoTaskResult,
|
||||
crypto_key_clone_payload_from_object, crypto_key_object_from_clone_payload,
|
||||
};
|
||||
pub(crate) use css_fontface_runtime::{new_font_face_set, rebuild_font_face_set_faces};
|
||||
pub(crate) use css_fontface_runtime::{
|
||||
new_font_face_set, rebuild_font_face_set_faces, settle_document_font_face_set_ready,
|
||||
};
|
||||
pub(crate) use location_navigation::{
|
||||
LocationNavigationKind, dispatch_top_level_form_navigation_event,
|
||||
dispatch_top_level_navigation_event_with_source_element, is_native_location,
|
||||
|
||||
@@ -56,3 +56,4 @@ pub(super) use font_face_set::{
|
||||
pub(super) use storage::install_font_face_set_template_accessors;
|
||||
pub(crate) use storage::new_font_face_set;
|
||||
pub(crate) use storage::rebuild_font_face_set_faces;
|
||||
pub(crate) use storage::settle_document_font_face_set_ready;
|
||||
|
||||
@@ -86,6 +86,12 @@ fn font_face_set_attribute_getter_callback<'s>(
|
||||
rv.set_undefined();
|
||||
return;
|
||||
};
|
||||
if matches!(
|
||||
attribute,
|
||||
FontFaceSetAttribute::Status | FontFaceSetAttribute::Ready
|
||||
) {
|
||||
reconcile_document_font_face_set_ready(scope, args.this());
|
||||
}
|
||||
match attribute {
|
||||
FontFaceSetAttribute::Status => rv.set(
|
||||
font_face_set_slot_value(scope, args.this(), FONT_FACE_SET_STATUS_SLOT)
|
||||
@@ -105,6 +111,86 @@ fn font_face_set_attribute_getter_callback<'s>(
|
||||
}
|
||||
}
|
||||
|
||||
const DOCUMENT_FONT_FACE_SET_OWNER: &str = "__moliFontFaceSetOwnerDocument";
|
||||
const DOCUMENT_FONT_FACE_SET_READY_RESOLVER: &str = "__moliDocumentFontFaceSetReadyResolver";
|
||||
|
||||
fn reconcile_document_font_face_set_ready<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
fonts: v8::Local<'s, v8::Object>,
|
||||
) {
|
||||
let Some(document) = get_private_value(scope, fonts, DOCUMENT_FONT_FACE_SET_OWNER)
|
||||
.and_then(|value| v8::Local::<v8::Object>::try_from(value).ok())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let Ok((host_ptr, handle)) =
|
||||
crate::native_bridge::node_runtime_and_handle_from_object(scope, document)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
if unsafe { &*host_ptr }.document_handle() != handle {
|
||||
return;
|
||||
}
|
||||
crate::document_runtime::DocumentRuntime::reconcile_document_web_fonts_in_current_scope(
|
||||
scope, host_ptr,
|
||||
);
|
||||
if unsafe { &*host_ptr }.document_web_fonts_have_pending_requests() {
|
||||
if get_private_value(scope, fonts, DOCUMENT_FONT_FACE_SET_READY_RESOLVER)
|
||||
.is_some_and(|value| value.is_object())
|
||||
{
|
||||
return;
|
||||
}
|
||||
if let Some(resolver) = v8::PromiseResolver::new(scope) {
|
||||
let promise = resolver.get_promise(scope);
|
||||
set_private_value(
|
||||
scope,
|
||||
fonts,
|
||||
DOCUMENT_FONT_FACE_SET_READY_RESOLVER,
|
||||
resolver.into(),
|
||||
);
|
||||
set_font_face_set_slot_value(scope, fonts, FONT_FACE_SET_READY_SLOT, promise.into());
|
||||
set_font_face_set_status(scope, fonts, "loading");
|
||||
}
|
||||
} else {
|
||||
settle_document_font_face_set_ready(scope, host_ptr);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn settle_document_font_face_set_ready(
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
host_ptr: *mut crate::native_bridge::JsContextHost,
|
||||
) {
|
||||
let host = unsafe { &*host_ptr };
|
||||
if host.document_web_fonts_have_pending_requests() {
|
||||
return;
|
||||
}
|
||||
let Some(document) = crate::util::node_wrapper_from_handle(scope, host.document_handle())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let Some(fonts) = document
|
||||
.get(scope, crate::util::v8str(scope, "__moliFonts").into())
|
||||
.and_then(|value| v8::Local::<v8::Object>::try_from(value).ok())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let Some(resolver) = get_private_value(scope, fonts, DOCUMENT_FONT_FACE_SET_READY_RESOLVER)
|
||||
.filter(|value| value.is_object())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
// Only a native PromiseResolver can occupy this private slot.
|
||||
let resolver = unsafe { v8::Local::<v8::PromiseResolver>::cast_unchecked(resolver) };
|
||||
set_font_face_set_status(scope, fonts, "loaded");
|
||||
set_private_value(
|
||||
scope,
|
||||
fonts,
|
||||
DOCUMENT_FONT_FACE_SET_READY_RESOLVER,
|
||||
v8::undefined(scope).into(),
|
||||
);
|
||||
let _ = resolver.resolve(scope, fonts.into());
|
||||
}
|
||||
|
||||
pub(crate) fn new_font_face_set<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
) -> Option<v8::Local<'s, v8::Object>> {
|
||||
|
||||
@@ -589,7 +589,21 @@ impl JsContextHost {
|
||||
&self,
|
||||
terminal: CompletedStylesheetWebFont,
|
||||
) -> DocumentWebFontCompletion {
|
||||
self.document_layout_state.borrow_mut().complete(terminal)
|
||||
let completion = self.document_layout_state.borrow_mut().complete(terminal);
|
||||
if matches!(&completion, DocumentWebFontCompletion::Registered(_)) {
|
||||
// Geometry sampled while the request was pending contains fallback
|
||||
// glyph advances. Retire that tree before fonts.ready observers run.
|
||||
self.document_layout_state
|
||||
.borrow_mut()
|
||||
.clear_latest_layout();
|
||||
}
|
||||
completion
|
||||
}
|
||||
|
||||
pub(crate) fn document_web_fonts_have_pending_requests(&self) -> bool {
|
||||
self.document_layout_state
|
||||
.borrow()
|
||||
.web_fonts_have_pending_requests()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -257,6 +257,10 @@ impl DocumentLayoutState {
|
||||
completion
|
||||
}
|
||||
|
||||
pub(super) fn web_fonts_have_pending_requests(&self) -> bool {
|
||||
self.web_fonts.has_pending_requests()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(super) fn web_font_counts(&self) -> (usize, usize, usize) {
|
||||
(
|
||||
|
||||
@@ -239,79 +239,12 @@ impl ScriptVm {
|
||||
}
|
||||
|
||||
pub(super) fn reconcile_document_web_fonts_for_layout(&mut self) {
|
||||
let font_fetch_enabled = self
|
||||
.document_runtime
|
||||
.current_document_resource_loader()
|
||||
.is_some_and(|loader| {
|
||||
loader
|
||||
.request_client()
|
||||
.optional_resource_fetch_enabled(crate::types::SubresourceResourceType::Font)
|
||||
});
|
||||
if !font_fetch_enabled {
|
||||
return;
|
||||
if let Err(error) = self.with_default_context_scope(|scope, host_ptr| {
|
||||
DocumentRuntime::reconcile_document_web_fonts_in_current_scope(scope, host_ptr);
|
||||
Ok(())
|
||||
}) {
|
||||
tracing::warn!(%error, "failed to reconcile current document web fonts");
|
||||
}
|
||||
let root = {
|
||||
let host = self._context_host.borrow();
|
||||
host.dom_host().document_element_handle()
|
||||
};
|
||||
let Some(root) = root else {
|
||||
return;
|
||||
};
|
||||
let skip_pristine_document = {
|
||||
let host = self._context_host.borrow();
|
||||
let document = host.document_handle();
|
||||
host.document_web_font_sidecar_is_pristine()
|
||||
&& !host.document_has_style_state(document)
|
||||
&& !host.document_has_active_author_stylesheet_sources(document)
|
||||
};
|
||||
if skip_pristine_document {
|
||||
return;
|
||||
}
|
||||
let Some(resources) = crate::layout_renderer::current_native_stylesheet_resources(
|
||||
&self._context_host.borrow(),
|
||||
root,
|
||||
) else {
|
||||
return;
|
||||
};
|
||||
tracing::trace!(
|
||||
stylesheet_import_dependency_count = resources.imports().len(),
|
||||
stylesheet_web_font_dependency_count = resources.web_fonts().len(),
|
||||
"observed typed stylesheet resource manifest"
|
||||
);
|
||||
let generation = resources.generation();
|
||||
if self
|
||||
._context_host
|
||||
.borrow()
|
||||
.document_web_font_resources_are_current(generation)
|
||||
{
|
||||
return;
|
||||
}
|
||||
let resources = resources.web_fonts().to_vec();
|
||||
self._context_host
|
||||
.borrow()
|
||||
.retain_document_web_font_slots(resources.iter());
|
||||
if resources.is_empty() {
|
||||
self._context_host
|
||||
.borrow()
|
||||
.publish_document_web_font_resource_generation(generation);
|
||||
return;
|
||||
}
|
||||
let resource_count = resources.len();
|
||||
let bound = {
|
||||
let mut host = self._context_host.borrow_mut();
|
||||
let bound = resources
|
||||
.into_iter()
|
||||
.filter_map(|resource| {
|
||||
host.accept_current_main_stylesheet_subresource_load_delay()
|
||||
.map(|binding| (binding, resource))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
if bound.len() == resource_count {
|
||||
host.publish_document_web_font_resource_generation(generation);
|
||||
}
|
||||
bound
|
||||
};
|
||||
self.start_stylesheet_subresource_fetches(bound);
|
||||
}
|
||||
|
||||
pub(super) fn start_css_images_discovered_by_layout(
|
||||
@@ -446,6 +379,12 @@ impl ScriptVm {
|
||||
tracing::debug!("discarded superseded document web font response")
|
||||
}
|
||||
}
|
||||
if let Err(error) = self.with_default_context_scope(|scope, host_ptr| {
|
||||
crate::context_bootstrap::settle_document_font_face_set_ready(scope, host_ptr);
|
||||
Ok(())
|
||||
}) {
|
||||
tracing::warn!(%error, "failed to settle current document font readiness");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -1,5 +1,63 @@
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn document_fonts_ready_waits_for_shadow_font_and_refreshes_cached_geometry() {
|
||||
let (font_url, request_rx, release_tx, server) = spawn_gated_font_resource_server().await;
|
||||
let document_url = font_url.replace("/print-only.woff2", "/page");
|
||||
let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).expect("loader");
|
||||
loader.set_optional_resource_fetch_mask(crate::protocol_types::OptionalResourceFetchMask::FONT);
|
||||
let (mut vm, mut completions) = new_parsed_test_vm_with_loader_and_resource_completion_queue(
|
||||
&document_url,
|
||||
"<!doctype html><html><head></head><body></body></html>",
|
||||
&loader,
|
||||
);
|
||||
vm.eval(&format!(r#"
|
||||
const host=document.createElement('span');host.id='font-host';document.body.append(host);
|
||||
const shadow=host.attachShadow({{mode:'open'}});
|
||||
shadow.innerHTML='<style>@font-face{{font-family:ShadowReady;src:url({font_url})}}span{{display:inline-block;font:20px ShadowReady}}</style><span id="font-probe">AAAA</span>';
|
||||
globalThis.fontProbe=shadow.getElementById('font-probe');
|
||||
globalThis.fontReadyResolved=false;
|
||||
globalThis.fontReady=document.fonts.ready;
|
||||
fontReady.then(fonts=>fontReadyResolved=fonts===document.fonts);
|
||||
'installed'
|
||||
"#)).expect("install shadow font without a layout request");
|
||||
tokio::time::timeout(std::time::Duration::from_secs(2), request_rx)
|
||||
.await
|
||||
.expect("fonts.ready starts the font request")
|
||||
.expect("request signal");
|
||||
assert_eq!(vm.eval("JSON.stringify([fontReadyResolved,document.fonts.status,fontReady===document.fonts.ready])").unwrap(),r#"[false,"loading",true]"#);
|
||||
let fallback_width = vm
|
||||
.eval("fontProbe.getBoundingClientRect().width")
|
||||
.expect("cache fallback font geometry")
|
||||
.parse::<f64>()
|
||||
.expect("numeric fallback width");
|
||||
// The project fixture has a 600-unit advance in a 1000-unit em:
|
||||
// four glyphs at 20px must measure 48px after loading.
|
||||
assert!((fallback_width - 48.0).abs() > 0.5);
|
||||
release_tx
|
||||
.send(())
|
||||
.expect("release the exact font response");
|
||||
server.await.expect("font server completes");
|
||||
assert!(
|
||||
tokio::time::timeout(
|
||||
std::time::Duration::from_secs(2),
|
||||
completions.wait_for_arrival_without_timeout()
|
||||
)
|
||||
.await
|
||||
.expect("font completion")
|
||||
);
|
||||
let completion = completions
|
||||
.pop_next_async_subresource_event()
|
||||
.expect("typed font completion");
|
||||
let _ = vm
|
||||
.complete_async_subresource_fetch_event_body(completion)
|
||||
.expect("register the current font");
|
||||
vm.perform_owner_lane_task_microtask_checkpoints()
|
||||
.expect("font completion checkpoint");
|
||||
assert_eq!(vm.document_web_font_counts_for_test(), (1, 1, 1));
|
||||
assert_eq!(vm.eval("JSON.stringify([fontReadyResolved,document.fonts.status,fontProbe.getBoundingClientRect().width])").unwrap(),r#"[true,"loaded",48]"#);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn linked_stylesheet_client_terminal_installs_source_before_its_load_event() {
|
||||
let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).expect("loader");
|
||||
|
||||
@@ -48,6 +48,12 @@ impl DocumentWebFontState {
|
||||
self.slots.is_empty()
|
||||
}
|
||||
|
||||
pub(crate) fn has_pending_requests(&self) -> bool {
|
||||
self.slots
|
||||
.values()
|
||||
.any(|slot| slot.status == WebFontRequestStatus::Pending)
|
||||
}
|
||||
|
||||
/// Removes registrations whose declarations are no longer in the current
|
||||
/// Stylo source set. Pending network work may still finish, but its exact
|
||||
/// request identity will no longer be accepted by [`Self::complete`].
|
||||
|
||||
@@ -7,6 +7,71 @@
|
||||
use super::*;
|
||||
|
||||
impl DocumentRuntime {
|
||||
pub(crate) fn reconcile_document_web_fonts_in_current_scope(
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
host_ptr: *mut JsContextHost,
|
||||
) {
|
||||
let host = unsafe { &mut *host_ptr };
|
||||
host.apply_pending_stylesheet_source_css_projections(scope, host_ptr);
|
||||
let font_fetch_enabled =
|
||||
host.current_main_document_resource_loader()
|
||||
.is_some_and(|loader| {
|
||||
loader.request_client().optional_resource_fetch_enabled(
|
||||
crate::types::SubresourceResourceType::Font,
|
||||
)
|
||||
});
|
||||
if !font_fetch_enabled {
|
||||
return;
|
||||
}
|
||||
let root = host.dom_host().document_element_handle();
|
||||
let Some(root) = root else {
|
||||
return;
|
||||
};
|
||||
let skip_pristine_document = {
|
||||
let document = host.document_handle();
|
||||
host.document_web_font_sidecar_is_pristine()
|
||||
&& !host.document_has_style_state(document)
|
||||
&& !host.document_has_active_author_stylesheet_sources(document)
|
||||
};
|
||||
if skip_pristine_document {
|
||||
return;
|
||||
}
|
||||
let Some(resources) =
|
||||
crate::layout_renderer::current_native_stylesheet_resources(host, root)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
tracing::trace!(
|
||||
stylesheet_import_dependency_count = resources.imports().len(),
|
||||
stylesheet_web_font_dependency_count = resources.web_fonts().len(),
|
||||
"observed typed stylesheet resource manifest"
|
||||
);
|
||||
let generation = resources.generation();
|
||||
if host.document_web_font_resources_are_current(generation) {
|
||||
return;
|
||||
}
|
||||
let resources = resources.web_fonts().to_vec();
|
||||
host.retain_document_web_font_slots(resources.iter());
|
||||
if resources.is_empty() {
|
||||
host.publish_document_web_font_resource_generation(generation);
|
||||
return;
|
||||
}
|
||||
let resource_count = resources.len();
|
||||
let bound = {
|
||||
let bound = resources
|
||||
.into_iter()
|
||||
.filter_map(|resource| {
|
||||
host.accept_current_main_stylesheet_subresource_load_delay()
|
||||
.map(|binding| (binding, resource))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
if bound.len() == resource_count {
|
||||
host.publish_document_web_font_resource_generation(generation);
|
||||
}
|
||||
bound
|
||||
};
|
||||
Self::start_stylesheet_subresource_fetches_in_current_scope(scope, host_ptr, bound);
|
||||
}
|
||||
pub(crate) fn settle_stylesheet_link_clients_in_current_scope(
|
||||
&mut self,
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
|
||||
Reference in New Issue
Block a user