fix(layout): select CSS fonts from current pass styles

This commit is contained in:
ldm0
2026-10-18 03:28:37 +08:00
committed by Donough Liu
parent 59cca282f9
commit de08ab789b
6 changed files with 245 additions and 1 deletions
+128
View File
@@ -631,6 +631,7 @@ pub struct WebFontRegistration {
slot: String,
face: WebFontFace,
bytes: Vec<u8>,
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(&registration.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<R>(
&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::<Vec<_>>();
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 =
@@ -89,6 +89,7 @@ impl StylesheetWebFont {
pub(crate) fn registration(&self, bytes: Vec<u8>) -> WebFontRegistration {
WebFontRegistration::new(self.slot.clone(), self.face.clone(), bytes)
.with_stylesheet_scope()
}
pub(crate) fn slot(&self) -> &str {
+34 -1
View File
@@ -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::<std::collections::HashSet<_>>();
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)
@@ -58,6 +58,13 @@ impl<'a> NativeLayoutStyleResolver<'a> {
}
}
pub(super) fn stylesheet_resources(
&mut self,
root: DomHandle,
) -> Option<crate::style_engine::StylesheetResourceSnapshot> {
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;
@@ -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!(
"<!doctype html><style>@font-face{{font-family:CurrentPassFont;src:url(data:font/woff2;base64,{font})}}</style><span id=target style='display:inline-block;font:20px CurrentPassFont,serif'>iiii</span>"
);
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
);
}
@@ -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;