diff --git a/moli-layout/src/text.rs b/moli-layout/src/text.rs index 4c94c6cde2..1ee2d30a8e 100644 --- a/moli-layout/src/text.rs +++ b/moli-layout/src/text.rs @@ -631,6 +631,7 @@ pub struct WebFontRegistration { slot: String, face: WebFontFace, bytes: Vec, + stylesheet: bool, } impl WebFontRegistration { @@ -639,9 +640,17 @@ impl WebFontRegistration { slot: slot.into(), face, bytes, + stylesheet: false, } } + /// Restricts this registration to passes where its stylesheet declaration + /// is active. Other native/programmatic registrations remain available. + pub fn with_stylesheet_scope(mut self) -> Self { + self.stylesheet = true; + self + } + pub fn slot(&self) -> &str { &self.slot } @@ -678,6 +687,7 @@ pub enum WebFontRegistrationError { struct RegisteredWebFont { face: WebFontFace, sfnt_bytes: Arc<[u8]>, + stylesheet: bool, } /// Lazily initialized text resources reused by successive layout demands for @@ -758,6 +768,7 @@ impl DocumentLayoutServices { let font = RegisteredWebFont { face: registration.face, sfnt_bytes, + stylesheet: registration.stylesheet, }; let outcome = match self.web_fonts.get(®istration.slot) { Some(current) if current == &font => WebFontRegistrationOutcome::Unchanged, @@ -791,6 +802,53 @@ impl DocumentLayoutServices { true } + pub fn has_stylesheet_web_fonts(&self) -> bool { + self.web_fonts.values().any(|font| font.stylesheet) + } + + /// Selects loaded CSS fonts for one pass without changing the document's + /// resource registrations. Temporary media/viewports and synchronous rule + /// changes must not discard bytes that another view can still use. + /// + /// When every registered CSS face is active, reuse the existing contexts. + /// Otherwise the temporary service shares font bytes and drops its contexts + /// on return (including an error result); only the pass counter is retained. + pub fn with_active_stylesheet_fonts( + &mut self, + is_active: impl Fn(&str) -> bool, + consume: impl FnOnce(&mut Self) -> R, + ) -> R { + let excluded = self + .web_fonts + .iter() + .filter_map(|(slot, font)| { + (font.stylesheet && !is_active(slot)).then_some(slot.as_str()) + }) + .collect::>(); + if excluded.is_empty() { + return consume(self); + } + // BTreeMap iteration keeps the excluded slots sorted. Font bytes are + // shared Arcs; a temporary selection never copies their payloads. + let web_fonts = self + .web_fonts + .iter() + .filter(|(slot, _)| excluded.binary_search(&slot.as_str()).is_err()) + .map(|(slot, font)| (slot.clone(), font.clone())) + .collect(); + let mut temporary = Self { + parley: None, + system_font_policy: self.system_font_policy, + web_fonts, + text_layout_passes: 0, + }; + let result = consume(&mut temporary); + self.text_layout_passes = self + .text_layout_passes + .saturating_add(temporary.text_layout_passes); + result + } + pub(crate) fn parley_mut(&mut self) -> &mut ParleyDocumentServices { if self.parley.is_none() { self.parley = Some(Box::new(build_parley_services( @@ -948,6 +1006,7 @@ fn validate_registered_font( let font = RegisteredWebFont { face: face.clone(), sfnt_bytes, + stylesheet: false, }; register_font(&mut font_context, &font) .then_some(()) @@ -1082,6 +1141,7 @@ mod tests { &RegisteredWebFont { face: WebFontFace::new("Moli Ahem"), sfnt_bytes: Arc::from(TEST_TTF), + stylesheet: false, }, )); let families = FontFamily::List(Cow::Owned(vec![ @@ -1210,6 +1270,74 @@ mod tests { .to_vec() } + #[test] + fn stylesheet_font_selection_preserves_native_faces_and_restores_after_error() { + let mut services = + DocumentLayoutServices::with_system_font_policy(SystemFontPolicy::Disabled); + services + .register_web_font(WebFontRegistration::new( + "native", + WebFontFace::new("Native CJK"), + TEST_CJK_TTF.to_vec(), + )) + .unwrap(); + services + .register_web_font( + WebFontRegistration::new("css", WebFontFace::new("CSS Latin"), TEST_TTF.to_vec()) + .with_stylesheet_scope(), + ) + .unwrap(); + assert_eq!( + shape_one_character( + services.parley_mut(), + web_font_style("CSS Latin", 400.0), + 'R' + ), + TEST_TTF + ); + let original = services.parley.as_deref().unwrap() as *const _; + let error: Result<(), &str> = services.with_active_stylesheet_fonts( + |_| false, + |selected| { + assert_eq!( + shape_one_character( + selected.parley_mut(), + web_font_style("Native CJK", 400.0), + 'δΈ­' + ), + TEST_CJK_TTF + ); + assert!( + !selected + .parley_mut() + .web_font_families + .contains_key("css latin") + ); + Err("pass failed") + }, + ); + assert_eq!(error, Err("pass failed")); + services.with_active_stylesheet_fonts( + |_| true, + |selected| { + assert_eq!( + selected.parley.as_deref().unwrap() as *const _, + original, + "matching passes reuse their text services" + ); + assert_eq!( + shape_one_character( + selected.parley_mut(), + web_font_style("CSS Latin", 400.0), + 'R' + ), + TEST_TTF + ); + }, + ); + assert_eq!(services.web_font_count(), 2); + } + #[test] fn segmented_web_font_faces_select_the_range_for_each_character() { let mut services = diff --git a/moli-renderer-v8/src/css_resource_urls.rs b/moli-renderer-v8/src/css_resource_urls.rs index 2190492b3d..1629048b64 100644 --- a/moli-renderer-v8/src/css_resource_urls.rs +++ b/moli-renderer-v8/src/css_resource_urls.rs @@ -89,6 +89,7 @@ impl StylesheetWebFont { pub(crate) fn registration(&self, bytes: Vec) -> WebFontRegistration { WebFontRegistration::new(self.slot.clone(), self.face.clone(), bytes) + .with_stylesheet_scope() } pub(crate) fn slot(&self) -> &str { diff --git a/moli-renderer-v8/src/layout_renderer/mod.rs b/moli-renderer-v8/src/layout_renderer/mod.rs index b6463a50a4..6d6dddf618 100644 --- a/moli-renderer-v8/src/layout_renderer/mod.rs +++ b/moli-renderer-v8/src/layout_renderer/mod.rs @@ -72,7 +72,40 @@ fn build_native_layout_pass_recursive( document_stack, embedded_document_services, }; - build_layout_pass_with_embedded_frames(&source, &mut styles, services, request, &mut frames) + if services.has_stylesheet_web_fonts() { + if let Some(resources) = styles.stylesheet_resources(root) { + let active = resources + .web_fonts() + .iter() + .filter_map(|resource| resource.web_font().map(|font| font.slot())) + .collect::>(); + services.with_active_stylesheet_fonts( + |slot| active.contains(slot), + |services| { + build_layout_pass_with_embedded_frames( + &source, + &mut styles, + services, + request, + &mut frames, + ) + }, + ) + } else { + Err(moli_layout::LayoutError::source_contract( + "layout fonts", + "missing current stylesheet resource manifest", + )) + } + } else { + build_layout_pass_with_embedded_frames( + &source, + &mut styles, + services, + request, + &mut frames, + ) + } }; if let (Some(started), Some(source_ready), Some(styles_ready)) = (profile_started, source_ready, styles_ready) diff --git a/moli-renderer-v8/src/layout_renderer/style_resolver.rs b/moli-renderer-v8/src/layout_renderer/style_resolver.rs index 729d50776a..75c764e8f3 100644 --- a/moli-renderer-v8/src/layout_renderer/style_resolver.rs +++ b/moli-renderer-v8/src/layout_renderer/style_resolver.rs @@ -58,6 +58,13 @@ impl<'a> NativeLayoutStyleResolver<'a> { } } + pub(super) fn stylesheet_resources( + &mut self, + root: DomHandle, + ) -> Option { + self.reads.read(root).stylesheet_resource_snapshot() + } + pub(super) fn trace_profile(&self, document: crate::document_runtime::DomHandle) { let Some(profile) = self.profile.as_ref() else { return; diff --git a/moli-renderer-v8/src/script_vm/tests/current_pass_css_fonts.rs b/moli-renderer-v8/src/script_vm/tests/current_pass_css_fonts.rs new file mode 100644 index 0000000000..b6e68f3ef9 --- /dev/null +++ b/moli-renderer-v8/src/script_vm/tests/current_pass_css_fonts.rs @@ -0,0 +1,74 @@ +use super::*; +use crate::protocol_types::OptionalResourceFetchMask; +use moli_layout::{LayoutFlushReason, LayoutPassRequest}; +use moli_page_types::LayoutPolicy; + +fn explicit_font_width(vm: &StandaloneScriptVmHarness) -> f32 { + let target = vm.document_runtime.get_element_by_id("target").unwrap(); + let host = vm._context_host.borrow(); + let document = host.document_handle(); + let pass = host + .build_layout_pass_for_document( + document, + LayoutPassRequest::new( + host.layout_viewport_for_document(document), + LayoutFlushReason::Test, + ), + ) + .unwrap() + .unwrap(); + assert_eq!(pass.metrics.paint_operation_count, 0); + pass.tree.client_rects_for_source(target)[0] + .bounding_rect() + .width +} + +#[tokio::test(flavor = "current_thread")] +async fn explicit_layout_selects_current_css_fonts_without_retiring_loaded_bytes() { + use base64::Engine; + + let font = base64::engine::general_purpose::STANDARD.encode(include_bytes!( + "../../../../moli-layout/tests/fixtures/moli-ahem.woff2" + )); + let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).unwrap(); + loader.set_optional_resource_fetch_mask(OptionalResourceFetchMask::FONT); + let markup = format!( + "iiii" + ); + let (mut vm, _completions) = new_parsed_test_vm_with_loader_and_resource_completion_queue( + "https://current-pass-font.test/", + &markup, + &loader, + ); + vm.set_layout_policy(LayoutPolicy::OnDemand); + vm.reconcile_document_web_fonts_for_layout(); + assert_eq!(explicit_font_width(&vm), 48.0); + let loaded = vm + ._context_host + .borrow() + .document_web_font_counts_for_test(); + assert_eq!(loaded, (1, 1, 1)); + + // Resource reconciliation and font bytes are deliberately unchanged. + // Each explicit native pass must still use the current stylesheet input. + vm.eval("globalThis.fontRule=document.styleSheets[0].cssRules[0].cssText;document.styleSheets[0].deleteRule(0)") + .unwrap(); + let removed = explicit_font_width(&vm); + assert_ne!(removed, 48.0); + assert_eq!( + vm._context_host + .borrow() + .document_web_font_counts_for_test(), + loaded + ); + + vm.eval("document.styleSheets[0].insertRule(fontRule,0)") + .unwrap(); + assert_eq!(explicit_font_width(&vm), 48.0); + assert_eq!( + vm._context_host + .borrow() + .document_web_font_counts_for_test(), + loaded + ); +} diff --git a/moli-renderer-v8/src/script_vm/tests/mod.rs b/moli-renderer-v8/src/script_vm/tests/mod.rs index ccfc74af4c..b699a8e902 100644 --- a/moli-renderer-v8/src/script_vm/tests/mod.rs +++ b/moli-renderer-v8/src/script_vm/tests/mod.rs @@ -1,6 +1,7 @@ mod beforeinput_target_validation; mod credential_container; mod credential_json_options; +mod current_pass_css_fonts; mod picture_in_picture_interfaces; mod visual_viewport_receiver; mod writable_stream_native_receivers;