Connect document font faces to rendering

This commit is contained in:
SGavrl committed 2026-07-31 03:31:21 +00:00
1 parent 0acf4e1b06
commit 5c34e74f10
5 files changed
+698 -24

No files matched your search

+9 -3
View File
@@ -28,9 +28,9 @@ pub use dom::{
mod paint;
#[cfg(feature = "paint")]
pub use paint::{
paint_dom, paint_dom_scrolled, paint_prepared, prepare_dom, screenshot_png,
screenshot_png_scrolled, screenshot_prepared, PreparedRender, RenderResourceCache,
RenderResourceLoader, SelectedImage,
paint_dom, paint_dom_scrolled, paint_prepared, prepare_dom, prepare_dom_with_dynamic_fonts,
screenshot_png, screenshot_png_scrolled, screenshot_prepared, DynamicFontFace,
PreparedRender, RenderResourceCache, RenderResourceLoader, SelectedImage,
};
// Real inline text layout (cosmic-text) lives behind the paint feature; the
@@ -467,6 +467,12 @@ pub struct LayoutStyle {
/// alignment keywords. Kept separate from flex/grid `align-items`: using
/// one field for both made `text-align:left` shrink-wrap flex children.
pub text_align: Option<taffy::AlignItems>,
/// Computed inherited `text-indent`. Font/viewport-relative lengths are
/// resolved to pixels during the top-down inheritance pass; percentages
/// remain typed until the final inline-formatting-context width is known.
/// `None` during cascade means inherit, while the root resolves to the
/// initial zero length.
pub text_indent: Option<Dimension>,
pub align_items: Option<taffy::AlignItems>,
pub justify_items: Option<taffy::JustifyItems>,
pub align_self: Option<taffy::AlignSelf>,
+132 -1
View File
@@ -31,6 +31,19 @@ pub trait RenderResourceLoader {
fn load(&mut self, url: &str) -> Option<Vec<u8>>;
}
/// One script-created `FontFace` registered with the document's
/// `FontFaceSet`. The JS runtime owns the registry and supplies this snapshot
/// alongside the DOM when preparing a render; keeping it out of the DOM avoids
/// exposing an implementation-only `<style>` element to page selectors.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DynamicFontFace {
pub family: String,
pub source: String,
pub style: String,
pub weight: String,
pub unicode_range: String,
}
impl<F> RenderResourceLoader for F
where
F: FnMut(&str) -> Option<Vec<u8>>,
@@ -415,6 +428,20 @@ pub fn prepare_dom(
viewport: (f32, f32),
base_url: Option<&str>,
resources: &mut RenderResourceCache,
) -> Option<PreparedRender> {
prepare_dom_with_dynamic_fonts(tree, viewport, base_url, resources, &[])
}
/// Prepare a DOM with the document's script-created `FontFace` registrations.
///
/// Authored rules and dynamic faces deliberately share the same bounded
/// resource cache, decoder, descriptor matching, and shaping database.
pub fn prepare_dom_with_dynamic_fonts(
tree: &DomTree,
viewport: (f32, f32),
base_url: Option<&str>,
resources: &mut RenderResourceCache,
dynamic_fonts: &[DynamicFontFace],
) -> Option<PreparedRender> {
if !viewport.0.is_finite()
|| !viewport.1.is_finite()
@@ -429,7 +456,7 @@ pub fn prepare_dom(
// each URL is still fetched at most once.
let (mut intrinsic, mut selected_images) =
collect_image_intrinsics(tree, viewport, base_url, resources);
let fonts = collect_web_fonts(tree, base_url, resources);
let fonts = collect_web_fonts(tree, base_url, resources, dynamic_fonts);
// Most framework pages use web fonts and many decorative SVG icons, but
// only SVG text needs the page font faces. Avoid cloning/loading the page
// font database for ordinary icons and HTML-only text.
@@ -2064,6 +2091,7 @@ fn collect_web_fonts(
tree: &DomTree,
base_url: Option<&str>,
cache: &mut RenderResourceCache,
dynamic_fonts: &[DynamicFontFace],
) -> Vec<crate::inline::WebFont> {
let mut seen = std::collections::HashSet::new();
let mut fonts = Vec::new();
@@ -2095,6 +2123,25 @@ fn collect_web_fonts(
));
}
}
for face in dynamic_fonts {
let descriptor_block = format!(
"src:{};font-weight:{};font-style:{};unicode-range:{}",
face.source, face.weight, face.style, face.unicode_range
);
if !font_face_covers_ascii(&descriptor_block) {
continue;
}
let Some(src) = font_face_urls(&descriptor_block).into_iter().next() else {
continue;
};
rules.push((
font_resource_key(&src, base_url),
src,
(!face.family.is_empty()).then(|| face.family.clone()),
font_face_weight(&descriptor_block),
font_face_italic(&descriptor_block),
));
}
// Critical web fonts are normally preloaded from the document with a URL
// already resolved relative to the HTML. Fetch those first, while retaining
@@ -5220,6 +5267,38 @@ mod tests {
);
}
#[test]
fn negative_text_indent_clips_label_without_hiding_background_icon() {
let tree = parse_html(
r#"<html><body style="margin:0">
<a style="display:block;width:24px;height:24px;overflow:hidden;
white-space:nowrap;text-indent:-9999px;color:black;background-color:blue;
background-image:url(&quot;data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='16'%20height='16'%3E%3Crect%20width='16'%20height='16'%20fill='red'/%3E%3C/svg%3E&quot;);
background-position:center;background-size:16px 16px;background-repeat:no-repeat">Bluesky (@mozilla.org)</a>
</body></html>"#,
);
let pixmap = paint_dom(&tree, (32.0, 32.0), None).expect("pixmap");
let corner = pixmap.pixel(1, 1).expect("anchor background");
assert!(
corner.blue() > 200 && corner.red() < 40 && corner.green() < 40,
"the anchor background must remain visible: {corner:?}"
);
let icon = pixmap.pixel(12, 12).expect("centered SVG icon");
assert!(
icon.red() > 200 && icon.green() < 40 && icon.blue() < 40,
"the background SVG must paint independently of indented text: {icon:?}"
);
for y in 0..24 {
for x in 0..24 {
let pixel = pixmap.pixel(x, y).expect("anchor pixel");
assert!(
pixel.red() > 40 || pixel.green() > 40 || pixel.blue() > 40,
"black label glyph leaked through the 24px overflow clip at ({x},{y})"
);
}
}
}
#[test]
fn contextual_background_size_preserves_auto_axis_ratio() {
let source = "data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='200'%20height='50'%3E%3C/svg%3E";
@@ -6345,4 +6424,56 @@ mod tests {
assert!(font_face_covers_ascii(face));
assert_eq!(font_face_urls(face), vec!["example.otf"]);
}
#[test]
fn dynamic_font_face_uses_shared_fetch_decode_and_layout_path() {
let tree = parse_html(
r#"<html><body><span id="sample" style="display:inline-block;width:max-content;
font:40px DynamicFixture;white-space:nowrap">WWWWiiii</span></body></html>"#,
);
let sample = tree
.query_selector("#sample")
.expect("valid selector")
.expect("sample");
let fallback = prepare_dom(
&tree,
(400.0, 100.0),
Some("https://example.test/page/index.html"),
&mut RenderResourceCache::default(),
)
.expect("fallback render")
.document_rect(sample)
.expect("fallback geometry")
.width;
let loads = Arc::new(std::sync::Mutex::new(Vec::new()));
let loader_loads = Arc::clone(&loads);
let mut resources = RenderResourceCache::with_loader(move |url: &str| {
loader_loads.lock().expect("font loads").push(url.to_string());
(url == "https://example.test/fonts/fixture.ttf")
.then(|| SERIF_FONT_BYTES.to_vec())
});
let prepared = prepare_dom_with_dynamic_fonts(
&tree,
(400.0, 100.0),
Some("https://example.test/page/index.html"),
&mut resources,
&[DynamicFontFace {
family: "DynamicFixture".to_string(),
source: "url('../../fonts/fixture.ttf') format('truetype')".to_string(),
style: "normal".to_string(),
weight: "400".to_string(),
unicode_range: "U+20-7E".to_string(),
}],
)
.expect("dynamic font render");
let dynamic = prepared
.document_rect(sample)
.expect("dynamic geometry")
.width;
assert_ne!(fallback, dynamic, "dynamic face must participate in shaping");
assert_eq!(
*loads.lock().expect("dynamic font loads"),
vec!["https://example.test/fonts/fixture.ttf".to_string()]
);
}
}