mirror of
https://github.com/lexmount/moli.git
synced 2026-10-30 16:07:24 +00:00
fix(layout): select CSS fonts from current pass styles
This commit is contained in:
@@ -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(®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<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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user