From e2da802d76d8af4d722b020603b5b3988009e281 Mon Sep 17 00:00:00 2001 From: Pierre Le Fevre Date: Fri, 17 Jul 2026 13:47:40 +0800 Subject: [PATCH] Resolve CSS font families per text run References isu issue 281 and isu issue 396. --- crates/browser/src/iframe_loader.rs | 11 +- crates/browser/src/main.rs | 26 +- crates/e2e/src/render.rs | 39 ++- crates/layout/src/lib.rs | 418 ++++++++++++++++++++++++---- crates/render/src/gpu.rs | 75 ++++- crates/render/src/lib.rs | 8 +- crates/style/src/computed.rs | 66 ++++- crates/text/src/font/registry.rs | 110 ++++++++ 8 files changed, 671 insertions(+), 82 deletions(-) diff --git a/crates/browser/src/iframe_loader.rs b/crates/browser/src/iframe_loader.rs index 12ee605..ac52b42 100644 --- a/crates/browser/src/iframe_loader.rs +++ b/crates/browser/src/iframe_loader.rs @@ -244,7 +244,7 @@ pub fn render_iframe_to_image( use we_css::parser::Stylesheet; use we_layout::layout; use we_platform::metal::{ClearColor, Device}; - use we_render::{build_display_list, GpuRenderer}; + use we_render::{build_display_list, GpuRenderer, RenderFonts}; let white = || Image { width, @@ -290,7 +290,14 @@ pub fn render_iframe_to_image( let rendered = Device::system_default() .and_then(GpuRenderer::new) .and_then(|mut backend| { - backend.render_to_pixels(&display_list, font, &image_refs, width, height, clear) + backend.render_to_pixels( + &display_list, + RenderFonts::new(font, None), + &image_refs, + width, + height, + clear, + ) }); match rendered { diff --git a/crates/browser/src/main.rs b/crates/browser/src/main.rs index a81cd22..f680b6d 100644 --- a/crates/browser/src/main.rs +++ b/crates/browser/src/main.rs @@ -17,11 +17,13 @@ use we_css::parser::Stylesheet; use we_dom::{Document, NodeData, NodeId}; use we_html::parse_html; use we_image::pixel::Image; -use we_layout::{layout, ReplacedContent}; +use we_layout::{layout, layout_with_font_registry, ReplacedContent}; use we_net::referrer::ReferrerPolicy; use we_platform::appkit; use we_platform::metal::ClearColor; -use we_render::{build_display_list_with_page_scroll, GpuRenderer, PaintCommand, ScrollState}; +use we_render::{ + build_display_list_with_page_scroll, GpuRenderer, PaintCommand, RenderFonts, ScrollState, +}; use we_style::computed::resolve_styles; use we_svg::{render_svg_scaled, svg_intrinsic_size, SvgIntrinsicSize}; use we_text::font::{self, Font, FontRegistry}; @@ -785,12 +787,13 @@ fn render_page( } // Layout with the reduced viewport height (below chrome). - let tree = layout( + let tree = layout_with_font_registry( &styled, &page.doc, viewport_width, content_viewport_height, font, + &page.font_registry, &sizes, ); @@ -864,7 +867,7 @@ fn render_page( // Submit to the Metal GPU compositor. backend.render( &display_list, - font, + RenderFonts::new(font, Some(&page.font_registry)), &refs, view.layer(), view.command_queue(), @@ -1372,12 +1375,13 @@ fn hit_test_dropdown( img_sizes.extend(svg_sizes); we_browser::iframe_loader::collect_iframe_sizes(&state.page.doc, &mut img_sizes); - let tree = we_layout::layout( + let tree = we_layout::layout_with_font_registry( &styled, &state.page.doc, viewport_width, content_viewport_height, &state.font, + &state.page.font_registry, &img_sizes, ); @@ -2298,12 +2302,13 @@ fn handle_mouse_down(x: f64, y: f64, click_count: u32, mods: appkit::KeyModifier img_sizes.extend(svg_sizes); we_browser::iframe_loader::collect_iframe_sizes(&state.page.doc, &mut img_sizes); - let tree = we_layout::layout( + let tree = we_layout::layout_with_font_registry( &styled, &state.page.doc, viewport_width, content_viewport_height, &state.font, + &state.page.font_registry, &img_sizes, ); @@ -2431,12 +2436,13 @@ fn handle_multi_select_click( img_sizes.extend(svg_sizes); we_browser::iframe_loader::collect_iframe_sizes(&state.page.doc, &mut img_sizes); - let tree = we_layout::layout( + let tree = we_layout::layout_with_font_registry( &styled, &state.page.doc, viewport_width, content_viewport_height, &state.font, + &state.page.font_registry, &img_sizes, ); @@ -2518,12 +2524,13 @@ fn handle_mouse_dragged(x: f64, y: f64) { img_sizes.extend(svg_sizes); we_browser::iframe_loader::collect_iframe_sizes(&state.page.doc, &mut img_sizes); - let tree = we_layout::layout( + let tree = we_layout::layout_with_font_registry( &styled, &state.page.doc, viewport_width, content_viewport_height, &state.font, + &state.page.font_registry, &img_sizes, ); @@ -2757,12 +2764,13 @@ fn compute_element_scroll_y(state: &BrowserState, target: NodeId) -> Option sizes.extend(svg_sizes); we_browser::iframe_loader::collect_iframe_sizes(&state.page.doc, &mut sizes); - let tree = layout( + let tree = layout_with_font_registry( &styled, &state.page.doc, viewport_width, content_viewport_height, &state.font, + &state.page.font_registry, &sizes, ); diff --git a/crates/e2e/src/render.rs b/crates/e2e/src/render.rs index 96e9eab..d1bb372 100644 --- a/crates/e2e/src/render.rs +++ b/crates/e2e/src/render.rs @@ -29,9 +29,9 @@ use we_dom::{Document, NodeData, NodeId}; use we_html::parse_html; use we_image::pixel::Image; use we_js::vm::{Value as JsValue, Vm}; -use we_layout::{layout, ReplacedContent}; +use we_layout::{layout_with_font_registry, ReplacedContent}; use we_platform::metal::{ClearColor, Device}; -use we_render::{build_display_list_with_page_scroll, GpuRenderer}; +use we_render::{build_display_list_with_page_scroll, GpuRenderer, RenderFonts}; use we_style::computed::resolve_styles; use we_svg::{render_svg, svg_intrinsic_size, SvgIntrinsicSize}; use we_text::font::{self, Font, FontRegistry}; @@ -93,6 +93,8 @@ pub struct Session { stylesheet: Stylesheet, /// Font picked for rendering. font: Font, + /// Font registry with system fonts plus fonts registered by @font-face. + font_registry: FontRegistry, /// Metal GPU renderer (offscreen). The harness renders through the exact /// production pipeline so tests can't pass while the real renderer is broken. backend: GpuRenderer, @@ -160,6 +162,7 @@ impl Session { console, stylesheet, font, + font_registry, backend, base_url, bgra: Vec::new(), @@ -215,7 +218,15 @@ impl Session { refs.insert(*id, img); } - let tree = layout(&styled, &doc, viewport_w, viewport_h, &self.font, &sizes); + let tree = layout_with_font_registry( + &styled, + &doc, + viewport_w, + viewport_h, + &self.font, + &self.font_registry, + &sizes, + ); let display_list = build_display_list_with_page_scroll(&tree, 0.0, &HashMap::new()); // Render offscreen through the production Metal pipeline (1:1 pixels — @@ -233,7 +244,7 @@ impl Session { }; match self.backend.render_to_pixels( &display_list, - &self.font, + RenderFonts::new(&self.font, Some(&self.font_registry)), &refs, self.width, self.height, @@ -284,7 +295,15 @@ impl Session { let svg_sizes = collect_svg_sizes(&doc); sizes.extend(svg_sizes); we_browser::iframe_loader::collect_iframe_sizes(&doc, &mut sizes); - let tree = layout(&styled, &doc, viewport_w, viewport_h, &self.font, &sizes); + let tree = layout_with_font_registry( + &styled, + &doc, + viewport_w, + viewport_h, + &self.font, + &self.font_registry, + &sizes, + ); let (cx, cy) = we_browser::hit_test::element_center(&tree.root, node) .ok_or_else(|| format!("no layout box for selector {selector}"))?; @@ -326,7 +345,15 @@ impl Session { let svg_sizes = collect_svg_sizes(&doc); sizes.extend(svg_sizes); we_browser::iframe_loader::collect_iframe_sizes(&doc, &mut sizes); - let tree = layout(&styled, &doc, viewport_w, viewport_h, &self.font, &sizes); + let tree = layout_with_font_registry( + &styled, + &doc, + viewport_w, + viewport_h, + &self.font, + &self.font_registry, + &sizes, + ); let (cx, cy) = we_browser::hit_test::element_center(&tree.root, node) .ok_or_else(|| format!("no layout box for selector {selector}"))?; diff --git a/crates/layout/src/lib.rs b/crates/layout/src/lib.rs index 688442d..5b2b8ee 100644 --- a/crates/layout/src/lib.rs +++ b/crates/layout/src/lib.rs @@ -16,7 +16,7 @@ use we_style::computed::{ StyledNode, TextAlign, TextDecoration, TextOverflow, TextShadow, TextTransform, Transform, Visibility, WhiteSpace, WillChange, }; -use we_text::font::Font; +use we_text::font::{Font, FontRegistry}; /// Width of scroll bars in pixels. pub const SCROLLBAR_WIDTH: f32 = 15.0; @@ -64,6 +64,8 @@ pub struct TextLine { pub y: f32, pub width: f32, pub font_size: f32, + /// Computed CSS `font-family` list for this text fragment. + pub font_family: String, pub color: Color, pub text_shadows: Vec, pub text_decoration: TextDecoration, @@ -167,6 +169,8 @@ pub struct LayoutBox { pub border: EdgeSizes, pub children: Vec, pub font_size: f32, + /// Computed CSS `font-family` list inherited by this box. + pub font_family: String, /// True if the text in this box should be rendered bold (font-weight >= 600). pub bold: bool, /// True if the text in this box should be rendered italic. @@ -328,6 +332,7 @@ impl LayoutBox { border: EdgeSizes::default(), children: Vec::new(), font_size: style.font_size, + font_family: style.font_family.as_str().to_string(), bold: style.font_weight.0 >= 600.0, italic: !matches!(style.font_style, FontStyle::Normal), white_space: style.white_space, @@ -1987,6 +1992,7 @@ fn compute_layout( viewport_height: f32, position_cb_height: f32, font: &Font, + font_registry: Option<&FontRegistry>, doc: &Document, abs_cb: Rect, float_ctx: Option<&FloatContext>, @@ -2194,7 +2200,15 @@ fn compute_layout( vertical_extra, ); set_sticky_constraints(b); - layout_abspos_children(b, abs_cb, viewport_width, viewport_height, font, doc); + layout_abspos_children( + b, + abs_cb, + viewport_width, + viewport_height, + font, + font_registry, + doc, + ); apply_relative_offset(b, available_width, position_cb_height); return; } @@ -2215,21 +2229,56 @@ fn compute_layout( BoxType::Block(node_id) => { let is_fieldset = doc.tag_name(*node_id) == Some("fieldset"); if matches!(b.display, Display::Table | Display::InlineTable) { - layout_table_children(b, viewport_width, viewport_height, font, doc, abs_cb); + layout_table_children( + b, + viewport_width, + viewport_height, + font, + font_registry, + doc, + abs_cb, + ); } else if matches!(b.display, Display::Grid | Display::InlineGrid) { - layout_grid_children(b, viewport_width, viewport_height, font, doc, abs_cb); + layout_grid_children( + b, + viewport_width, + viewport_height, + font, + font_registry, + doc, + abs_cb, + ); } else if matches!(b.display, Display::Flex | Display::InlineFlex) { - layout_flex_children(b, viewport_width, viewport_height, font, doc, abs_cb); + layout_flex_children( + b, + viewport_width, + viewport_height, + font, + font_registry, + doc, + abs_cb, + ); } else if has_block_children(b) || has_float_children(b) { - layout_block_children(b, viewport_width, viewport_height, font, doc, abs_cb); - } else { - layout_inline_children( + layout_block_children( b, viewport_width, viewport_height, font, + font_registry, doc, abs_cb, + ); + } else { + layout_inline_children( + b, + &InlineLayoutContext { + doc, + font, + font_registry, + viewport_width, + viewport_height, + abs_cb, + }, float_ctx, ); } @@ -2239,30 +2288,84 @@ fn compute_layout( } BoxType::Anonymous => { if matches!(b.display, Display::Table | Display::InlineTable) { - layout_table_children(b, viewport_width, viewport_height, font, doc, abs_cb); + layout_table_children( + b, + viewport_width, + viewport_height, + font, + font_registry, + doc, + abs_cb, + ); } else if matches!(b.display, Display::Grid | Display::InlineGrid) { - layout_grid_children(b, viewport_width, viewport_height, font, doc, abs_cb); + layout_grid_children( + b, + viewport_width, + viewport_height, + font, + font_registry, + doc, + abs_cb, + ); } else if matches!(b.display, Display::Flex | Display::InlineFlex) { - layout_flex_children(b, viewport_width, viewport_height, font, doc, abs_cb); + layout_flex_children( + b, + viewport_width, + viewport_height, + font, + font_registry, + doc, + abs_cb, + ); } else if has_block_children(b) || has_float_children(b) { - layout_block_children(b, viewport_width, viewport_height, font, doc, abs_cb); - } else { - layout_inline_children( + layout_block_children( b, viewport_width, viewport_height, font, + font_registry, doc, abs_cb, + ); + } else { + layout_inline_children( + b, + &InlineLayoutContext { + doc, + font, + font_registry, + viewport_width, + viewport_height, + abs_cb, + }, float_ctx, ); } } BoxType::Inline(_) if b.display == Display::InlineBlock => { if has_block_children(b) || has_float_children(b) { - layout_block_children(b, viewport_width, viewport_height, font, doc, abs_cb); + layout_block_children( + b, + viewport_width, + viewport_height, + font, + font_registry, + doc, + abs_cb, + ); } else { - layout_inline_children(b, viewport_width, viewport_height, font, doc, abs_cb, None); + layout_inline_children( + b, + &InlineLayoutContext { + doc, + font, + font_registry, + viewport_width, + viewport_height, + abs_cb, + }, + None, + ); } } BoxType::TextRun { .. } | BoxType::Inline(_) => { @@ -2319,7 +2422,15 @@ fn compute_layout( // Layout absolutely and fixed positioned children after this box's // dimensions are fully resolved. - layout_abspos_children(b, abs_cb, viewport_width, viewport_height, font, doc); + layout_abspos_children( + b, + abs_cb, + viewport_width, + viewport_height, + font, + font_registry, + doc, + ); apply_relative_offset(b, available_width, position_cb_height); @@ -2411,6 +2522,7 @@ fn layout_abspos_children( viewport_width: f32, viewport_height: f32, font: &Font, + font_registry: Option<&FontRegistry>, doc: &Document, ) { let viewport_cb = Rect { @@ -2436,6 +2548,7 @@ fn layout_abspos_children( viewport_width, viewport_height, font, + font_registry, doc, ); } else if parent.children[i].position == Position::Fixed { @@ -2445,6 +2558,7 @@ fn layout_abspos_children( viewport_width, viewport_height, font, + font_registry, doc, ); } @@ -2461,6 +2575,7 @@ fn layout_absolute_child( viewport_width: f32, viewport_height: f32, font: &Font, + font_registry: Option<&FontRegistry>, doc: &Document, ) { let [css_top, css_right, css_bottom, css_left] = child.css_offsets; @@ -2659,6 +2774,7 @@ fn layout_absolute_child( viewport_width, viewport_height, font, + font_registry, doc, child_abs_cb, ); @@ -2668,6 +2784,7 @@ fn layout_absolute_child( viewport_width, viewport_height, font, + font_registry, doc, child_abs_cb, ); @@ -2677,6 +2794,7 @@ fn layout_absolute_child( viewport_width, viewport_height, font, + font_registry, doc, child_abs_cb, ); @@ -2686,17 +2804,21 @@ fn layout_absolute_child( viewport_width, viewport_height, font, + font_registry, doc, child_abs_cb, ); } else { layout_inline_children( child, - viewport_width, - viewport_height, - font, - doc, - child_abs_cb, + &InlineLayoutContext { + doc, + font, + font_registry, + viewport_width, + viewport_height, + abs_cb: child_abs_cb, + }, None, ); } @@ -2759,6 +2881,7 @@ fn layout_absolute_child( viewport_width, viewport_height, font, + font_registry, doc, ); } @@ -2917,6 +3040,7 @@ fn layout_block_children( viewport_width: f32, viewport_height: f32, font: &Font, + font_registry: Option<&FontRegistry>, doc: &Document, abs_cb: Rect, ) { @@ -2959,6 +3083,7 @@ fn layout_block_children( viewport_height, child_position_cb_height, font, + font_registry, doc, abs_cb, ); @@ -3024,6 +3149,7 @@ fn layout_block_children( viewport_height, child_position_cb_height, font, + font_registry, doc, abs_cb, Some(&float_ctx), @@ -3085,6 +3211,7 @@ fn layout_float_child( viewport_height: f32, position_cb_height: f32, font: &Font, + font_registry: Option<&FontRegistry>, doc: &Document, abs_cb: Rect, ) { @@ -3166,30 +3293,51 @@ fn layout_float_child( viewport_width, viewport_height, font, + font_registry, doc, abs_cb, ); } else if matches!(child.display, Display::Grid | Display::InlineGrid) { - layout_grid_children(child, viewport_width, viewport_height, font, doc, abs_cb); + layout_grid_children( + child, + viewport_width, + viewport_height, + font, + font_registry, + doc, + abs_cb, + ); } else if matches!(child.display, Display::Flex | Display::InlineFlex) { - layout_flex_children(child, viewport_width, viewport_height, font, doc, abs_cb); - } else if has_block_children(child) || has_float_children(child) { - layout_block_children( + layout_flex_children( child, viewport_width, viewport_height, font, + font_registry, doc, abs_cb, ); - } else { - layout_inline_children( + } else if has_block_children(child) || has_float_children(child) { + layout_block_children( child, viewport_width, viewport_height, font, + font_registry, doc, abs_cb, + ); + } else { + layout_inline_children( + child, + &InlineLayoutContext { + doc, + font, + font_registry, + viewport_width, + viewport_height, + abs_cb, + }, None, ); } @@ -3269,7 +3417,15 @@ fn layout_float_child( // Set sticky constraints and handle abspos children. set_sticky_constraints(child); - layout_abspos_children(child, abs_cb, viewport_width, viewport_height, font, doc); + layout_abspos_children( + child, + abs_cb, + viewport_width, + viewport_height, + font, + font_registry, + doc, + ); apply_relative_offset(child, container_width, position_cb_height); } @@ -3559,6 +3715,7 @@ fn layout_table_children( viewport_width: f32, viewport_height: f32, font: &Font, + font_registry: Option<&FontRegistry>, doc: &Document, abs_cb: Rect, ) { @@ -3727,6 +3884,7 @@ fn layout_table_children( viewport_height, viewport_height, font, + font_registry, doc, abs_cb, None, @@ -3908,6 +4066,7 @@ fn layout_grid_children( viewport_width: f32, viewport_height: f32, font: &Font, + font_registry: Option<&FontRegistry>, doc: &Document, abs_cb: Rect, ) { @@ -4041,6 +4200,7 @@ fn layout_grid_children( viewport_height, viewport_height, font, + font_registry, doc, abs_cb, None, @@ -4785,6 +4945,7 @@ fn layout_flex_children( viewport_width: f32, viewport_height: f32, font: &Font, + font_registry: Option<&FontRegistry>, doc: &Document, abs_cb: Rect, ) { @@ -4995,6 +5156,7 @@ fn layout_flex_children( viewport_height, viewport_height, font, + font_registry, doc, abs_cb, None, @@ -5168,6 +5330,7 @@ fn layout_flex_children( viewport_height, viewport_height, font, + font_registry, doc, abs_cb, None, @@ -5187,6 +5350,7 @@ fn layout_flex_children( viewport_height, viewport_height, font, + font_registry, doc, abs_cb, None, @@ -5224,6 +5388,7 @@ fn layout_flex_children( viewport_height, viewport_height, font, + font_registry, doc, abs_cb, None, @@ -5307,6 +5472,7 @@ fn layout_flex_children( viewport_height, viewport_height, font, + font_registry, doc, abs_cb, None, @@ -5330,6 +5496,7 @@ fn layout_flex_children( viewport_height, viewport_height, font, + font_registry, doc, abs_cb, None, @@ -5594,6 +5761,7 @@ enum InlineItemKind { node: NodeId, text: String, font_size: f32, + font_family: String, color: Color, text_shadows: Vec, text_decoration: TextDecoration, @@ -5608,6 +5776,7 @@ enum InlineItemKind { /// The source DOM node (text node) this space came from. node: NodeId, font_size: f32, + font_family: String, color: Color, text_shadows: Vec, text_decoration: TextDecoration, @@ -5656,6 +5825,7 @@ struct PendingFragment { width: f32, height: f32, font_size: f32, + font_family: String, color: Color, text_shadows: Vec, text_decoration: TextDecoration, @@ -5668,6 +5838,7 @@ struct PendingFragment { struct InlineLayoutContext<'a> { doc: &'a Document, font: &'a Font, + font_registry: Option<&'a FontRegistry>, viewport_width: f32, viewport_height: f32, abs_cb: Rect, @@ -5704,6 +5875,7 @@ fn flatten_inline_tree( node, text: w, font_size: child.font_size, + font_family: child.font_family.clone(), color: child.color, text_shadows: child.text_shadows.clone(), text_decoration: child.text_decoration, @@ -5716,6 +5888,7 @@ fn flatten_inline_tree( items.push(InlineItemKind::Space { node, font_size: child.font_size, + font_family: child.font_family.clone(), color: child.color, text_shadows: child.text_shadows.clone(), text_decoration: child.text_decoration, @@ -5852,6 +6025,7 @@ fn layout_inline_block_child( ctx.viewport_height, ctx.viewport_height, ctx.font, + ctx.font_registry, ctx.doc, ctx.abs_cb, None, @@ -5928,11 +6102,7 @@ fn split_into_words_preserve(text: &str) -> Vec { /// with active floats from the parent block formatting context. fn layout_inline_children( parent: &mut LayoutBox, - viewport_width: f32, - viewport_height: f32, - font: &Font, - doc: &Document, - abs_cb: Rect, + ctx: &InlineLayoutContext<'_>, float_ctx: Option<&FloatContext>, ) { let available_width = parent.rect.width; @@ -5945,14 +6115,7 @@ fn layout_inline_children( let parent_font_size = parent.font_size; let mut items = Vec::new(); - let ctx = InlineLayoutContext { - doc, - font, - viewport_width, - viewport_height, - abs_cb, - }; - flatten_inline_tree(&mut parent.children, &ctx, &mut items, available_width); + flatten_inline_tree(&mut parent.children, ctx, &mut items, available_width); if items.is_empty() { parent.rect.height = 0.0; @@ -5972,6 +6135,7 @@ fn layout_inline_children( let mut current_line_max_atomic_height: f32 = 0.0; // The largest font-size of any text fragment on the current line. let mut current_line_max_font_size: f32 = 0.0; + let mut font_resolver = FontResolver::new(ctx.font_registry, ctx.font); // Compute the available width for the current line, narrowed by floats. let line_avail = |y: f32| -> (f32, f32) { @@ -6034,6 +6198,7 @@ fn layout_inline_children( node, text, font_size, + font_family, color, text_shadows, text_decoration, @@ -6041,7 +6206,8 @@ fn layout_inline_children( bold, italic, } => { - let word_width = measure_text_width(font, text, *font_size); + let resolved_font = font_resolver.resolve(font_family, *bold, *italic); + let word_width = measure_text_width(resolved_font, text, *font_size); // If this word doesn't fit and the line isn't empty, break // (unless wrapping is suppressed by white-space: nowrap/pre). @@ -6078,6 +6244,7 @@ fn layout_inline_children( width: word_width, height: 0.0, font_size: *font_size, + font_family: font_family.clone(), color: *color, text_shadows: text_shadows.clone(), text_decoration: *text_decoration, @@ -6091,6 +6258,7 @@ fn layout_inline_children( InlineItemKind::Space { node, font_size, + font_family, color, text_shadows, text_decoration, @@ -6099,7 +6267,8 @@ fn layout_inline_children( italic, preserved, } => { - let space_width = measure_text_width(font, " ", *font_size); + let resolved_font = font_resolver.resolve(font_family, *bold, *italic); + let space_width = measure_text_width(resolved_font, " ", *font_size); // In white-space:pre, preserve leading spaces too. Otherwise, // only add a space if we have content on the line so leading // whitespace doesn't create indentation. @@ -6117,6 +6286,7 @@ fn layout_inline_children( width: space_width, height: 0.0, font_size: *font_size, + font_family: font_family.clone(), color: *color, text_shadows: text_shadows.clone(), text_decoration: *text_decoration, @@ -6142,6 +6312,9 @@ fn layout_inline_children( && prev.text_shadows == *text_shadows && prev.background_color == *background_color && prev.font_size == *font_size + && prev.font_family == *font_family + && prev.bold == *bold + && prev.italic == *italic { prev.width += space_width; } @@ -6225,6 +6398,7 @@ fn layout_inline_children( width: *width, height: *height, font_size: *height, + font_family: parent.font_family.clone(), color: if *transparent_placeholder { Color::new(0, 0, 0, 0) } else { @@ -6286,7 +6460,13 @@ fn layout_inline_children( if all_lines.len() > max { all_lines.truncate(max); if let Some((line_fragments, _, line_avail_w, _)) = all_lines.last_mut() { - apply_ellipsis_to_line(line_fragments, *line_avail_w, font, parent_font_size, true); + apply_ellipsis_to_line( + line_fragments, + *line_avail_w, + &mut font_resolver, + parent_font_size, + true, + ); } } } @@ -6301,7 +6481,13 @@ fn layout_inline_children( } if ellipsize { - apply_ellipsis_to_line(line_fragments, *line_avail_w, font, parent_font_size, false); + apply_ellipsis_to_line( + line_fragments, + *line_avail_w, + &mut font_resolver, + parent_font_size, + false, + ); } // Compute line width from last fragment. @@ -6339,6 +6525,7 @@ fn layout_inline_children( y: frag_y, width: frag.width, font_size: frag.font_size, + font_family: frag.font_family.clone(), color: frag.color, text_shadows: frag.text_shadows.clone(), text_decoration: frag.text_decoration, @@ -6541,6 +6728,51 @@ fn compute_align_offset( // Text measurement // --------------------------------------------------------------------------- +#[derive(Clone, Hash, PartialEq, Eq)] +struct FontKey { + family_list: String, + bold: bool, + italic: bool, +} + +struct FontResolver<'a> { + registry: Option<&'a FontRegistry>, + fallback: &'a Font, + cache: HashMap, +} + +impl<'a> FontResolver<'a> { + fn new(registry: Option<&'a FontRegistry>, fallback: &'a Font) -> Self { + FontResolver { + registry, + fallback, + cache: HashMap::new(), + } + } + + fn resolve(&mut self, family_list: &str, bold: bool, italic: bool) -> &Font { + let Some(registry) = self.registry else { + return self.fallback; + }; + if family_list.trim().is_empty() { + return self.fallback; + } + + let key = FontKey { + family_list: family_list.to_string(), + bold, + italic, + }; + if !self.cache.contains_key(&key) { + if let Some(font) = registry.find_font_for_family_list(family_list, bold, italic) { + self.cache.insert(key.clone(), font); + } + } + + self.cache.get(&key).unwrap_or(self.fallback) + } +} + /// Measure the total advance width of a text string at the given font size. fn measure_text_width(font: &Font, text: &str, font_size: f32) -> f32 { let shaped = font.shape_text(text, font_size); @@ -6596,7 +6828,7 @@ fn truncate_text_to_width( fn apply_ellipsis_to_line( fragments: &mut [PendingFragment], avail_width: f32, - font: &Font, + font_resolver: &mut FontResolver<'_>, fallback_font_size: f32, force: bool, ) { @@ -6613,14 +6845,19 @@ fn apply_ellipsis_to_line( // Font size used to size the ellipsis glyph: the last text fragment's size // (the ellipsis is appended to a text fragment), falling back to the strut. - let ell_font_size = fragments - .iter() - .rev() - .find(|f| !f.atomic) - .map(|f| f.font_size) - .unwrap_or(fallback_font_size); + let ell_host = fragments.iter().rposition(|f| !f.atomic); + let (ell_font_size, ell_font) = match ell_host { + Some(idx) => { + let f = &fragments[idx]; + ( + f.font_size, + font_resolver.resolve(&f.font_family, f.bold, f.italic), + ) + } + None => (fallback_font_size, font_resolver.fallback), + }; let ellipsis = "\u{2026}"; - let ell_width = measure_text_width(font, ellipsis, ell_font_size); + let ell_width = measure_text_width(ell_font, ellipsis, ell_font_size); // Forced ellipsis where the line plus the ellipsis already fits: append the // glyph to the last text fragment without truncating any text. @@ -6643,8 +6880,13 @@ fn apply_ellipsis_to_line( Some(h) => { let avail_for_text = (target - fragments[h].x).max(0.0); let fs = fragments[h].font_size; + let host_font = font_resolver.resolve( + &fragments[h].font_family, + fragments[h].bold, + fragments[h].italic, + ); let (truncated, tw) = - truncate_text_to_width(font, &fragments[h].text, fs, avail_for_text); + truncate_text_to_width(host_font, &fragments[h].text, fs, avail_for_text); fragments[h].text = format!("{truncated}{ellipsis}"); fragments[h].width = tw + ell_width; for f in fragments.iter_mut().skip(h + 1) { @@ -6679,6 +6921,48 @@ pub fn layout( viewport_height: f32, font: &Font, image_sizes: &HashMap, +) -> LayoutTree { + layout_internal( + styled_root, + doc, + viewport_width, + viewport_height, + font, + None, + image_sizes, + ) +} + +/// Build and lay out using CSS family/weight/style resolution from a font +/// registry, falling back to `font` when no requested family is available. +pub fn layout_with_font_registry( + styled_root: &StyledNode, + doc: &Document, + viewport_width: f32, + viewport_height: f32, + font: &Font, + font_registry: &FontRegistry, + image_sizes: &HashMap, +) -> LayoutTree { + layout_internal( + styled_root, + doc, + viewport_width, + viewport_height, + font, + Some(font_registry), + image_sizes, + ) +} + +fn layout_internal( + styled_root: &StyledNode, + doc: &Document, + viewport_width: f32, + viewport_height: f32, + font: &Font, + font_registry: Option<&FontRegistry>, + image_sizes: &HashMap, ) -> LayoutTree { let mut root = match build_box(styled_root, doc, image_sizes) { Some(b) => b, @@ -6711,6 +6995,7 @@ pub fn layout( viewport_height, viewport_height, font, + font_registry, doc, viewport_cb, None, @@ -7009,6 +7294,7 @@ pub fn layout_incremental( viewport_height, viewport_height, font, + None, doc, viewport_cb, None, @@ -7852,6 +8138,28 @@ p { margin-top: 50px; margin-bottom: 50px; } ); } + #[test] + fn text_lines_keep_computed_font_family_list() { + let html_str = r#" + + +

Hello

+"#; + let doc = we_html::parse_html(html_str); + let font = test_font(); + let sheets = extract_stylesheets(&doc); + let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); + let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); + + let body_box = &tree.root.children[0]; + let p_box = &body_box.children[0]; + + assert_eq!( + p_box.lines[0].font_family, + "Be Vietnam Pro, arial, sans-serif" + ); + } + /// Recursively find the first layout box whose box type is `Inline(node)` /// for the element with the given tag name. fn find_inline_box<'a>(b: &'a LayoutBox, doc: &Document, tag: &str) -> Option<&'a LayoutBox> { diff --git a/crates/render/src/gpu.rs b/crates/render/src/gpu.rs index 2f6b809..f8578c1 100644 --- a/crates/render/src/gpu.rs +++ b/crates/render/src/gpu.rs @@ -21,7 +21,7 @@ use we_platform::metal::{ TEXTURE_MODE_MASK, TEXTURE_MODE_SDF, TEXTURE_MODE_SOLID, }; use we_style::computed::ObjectFit; -use we_text::font::Font; +use we_text::font::{Font, FontRegistry}; use crate::atlas::{GlyphAtlas, TexturedQuad}; use crate::{DisplayList, PaintCommand}; @@ -86,6 +86,64 @@ enum TextureKey { Image(usize), } +/// Font inputs used by the GPU text renderer. +#[derive(Clone, Copy)] +pub struct RenderFonts<'a> { + pub base: &'a Font, + pub registry: Option<&'a FontRegistry>, +} + +impl<'a> RenderFonts<'a> { + pub fn new(base: &'a Font, registry: Option<&'a FontRegistry>) -> Self { + RenderFonts { base, registry } + } +} + +#[derive(Clone, Hash, PartialEq, Eq)] +struct FontKey { + family_list: String, + bold: bool, + italic: bool, +} + +struct FontResolver<'a> { + registry: Option<&'a FontRegistry>, + fallback: &'a Font, + cache: HashMap, +} + +impl<'a> FontResolver<'a> { + fn new(registry: Option<&'a FontRegistry>, fallback: &'a Font) -> Self { + FontResolver { + registry, + fallback, + cache: HashMap::new(), + } + } + + fn resolve(&mut self, family_list: &str, bold: bool, italic: bool) -> &Font { + let Some(registry) = self.registry else { + return self.fallback; + }; + if family_list.trim().is_empty() { + return self.fallback; + } + + let key = FontKey { + family_list: family_list.to_string(), + bold, + italic, + }; + if !self.cache.contains_key(&key) { + if let Some(font) = registry.find_font_for_family_list(family_list, bold, italic) { + self.cache.insert(key.clone(), font); + } + } + + self.cache.get(&key).unwrap_or(self.fallback) + } +} + // --------------------------------------------------------------------------- // Compositing layer state // --------------------------------------------------------------------------- @@ -198,7 +256,7 @@ impl GpuRenderer { pub fn render( &mut self, display_list: &DisplayList, - font: &Font, + fonts: RenderFonts<'_>, images: &HashMap, layer: &MetalLayer, queue: &CommandQueue, @@ -241,7 +299,7 @@ impl GpuRenderer { // Process the display list, creating/ending encoders as needed for layers. self.encode_display_list_with_layers( display_list, - font, + fonts, images, &cmd_buf, texture_id, @@ -267,7 +325,7 @@ impl GpuRenderer { pub fn render_to_pixels( &mut self, display_list: &DisplayList, - font: &Font, + fonts: RenderFonts<'_>, images: &HashMap, width: u32, height: u32, @@ -286,7 +344,7 @@ impl GpuRenderer { self.encode_display_list_with_layers( display_list, - font, + fonts, images, &cmd_buf, target.id(), @@ -307,7 +365,7 @@ impl GpuRenderer { fn encode_display_list_with_layers( &mut self, display_list: &DisplayList, - font: &Font, + fonts: RenderFonts<'_>, images: &HashMap, cmd_buf: &CommandBuffer, initial_target_id: we_platform::objc::Id, @@ -373,6 +431,7 @@ impl GpuRenderer { // Current batch of vertices and their texture key. let mut batch_vertices: Vec = Vec::with_capacity(4096); let mut batch_texture: TextureKey = TextureKey::Solid; + let mut font_resolver = FontResolver::new(fonts.registry, fonts.base); for cmd in display_list { match cmd { @@ -665,7 +724,9 @@ impl GpuRenderer { color, } => { // Build glyph quads from the atlas. - let quads = self.atlas.build_text_quads(line, font, scale); + let text_font = + font_resolver.resolve(&line.font_family, line.bold, line.italic); + let quads = self.atlas.build_text_quads(line, text_font, scale); self.ensure_atlas_textures(); for quad in &quads { diff --git a/crates/render/src/lib.rs b/crates/render/src/lib.rs index ce5f1e4..982d504 100644 --- a/crates/render/src/lib.rs +++ b/crates/render/src/lib.rs @@ -11,7 +11,7 @@ pub mod atlas; pub mod gpu; pub mod layer; -pub use gpu::GpuRenderer; +pub use gpu::{GpuRenderer, RenderFonts}; use std::collections::HashMap; @@ -1239,6 +1239,7 @@ fn paint_list_marker(layout_box: &LayoutBox, list: &mut DisplayList, tx: f32, ty y: marker_y, width: marker_width, font_size, + font_family: String::new(), color: layout_box.color, text_shadows: Vec::new(), text_decoration: TextDecoration::None, @@ -1695,6 +1696,7 @@ fn paint_text_input( y: text_y, width: layout_box.rect.width, font_size, + font_family: String::new(), color: text_color, text_shadows: Vec::new(), text_decoration: TextDecoration::None, @@ -2040,6 +2042,7 @@ fn paint_button( y: text_y, width: text_width, font_size, + font_family: String::new(), color: text_color, text_shadows: Vec::new(), text_decoration: TextDecoration::None, @@ -2117,6 +2120,7 @@ fn paint_select_button( y: text_y, width: layout_box.rect.width - 20.0, // leave room for arrow font_size, + font_family: String::new(), color: text_color, text_shadows: Vec::new(), text_decoration: TextDecoration::None, @@ -2251,6 +2255,7 @@ fn paint_select_listbox( } else { font_size }, + font_family: String::new(), color: opt_color, text_shadows: Vec::new(), text_decoration: TextDecoration::None, @@ -2405,6 +2410,7 @@ fn paint_dropdown_overlay(dd: &PendingDropdown, list: &mut DisplayList) { y: oy, width: menu_width - indent - padding, font_size: opt_font_size, + font_family: String::new(), color: text_color, text_shadows: Vec::new(), text_decoration: TextDecoration::None, diff --git a/crates/style/src/computed.rs b/crates/style/src/computed.rs index d0c0360..5478ce9 100644 --- a/crates/style/src/computed.rs +++ b/crates/style/src/computed.rs @@ -2086,6 +2086,50 @@ fn css_transform_length(value: CssTransformLength) -> TransformLength { // Apply a single property value to a ComputedStyle // --------------------------------------------------------------------------- +fn font_family_to_css_text(value: &CssValue) -> Option { + match value { + CssValue::String(s) | CssValue::Keyword(s) => { + if s.is_empty() { + None + } else { + Some(s.clone()) + } + } + CssValue::List(items) => { + let mut out = String::new(); + for item in items { + match item { + CssValue::String(s) | CssValue::Keyword(s) => { + if s == "," { + while out.ends_with(' ') { + out.pop(); + } + if !out.ends_with(',') { + out.push(','); + } + out.push(' '); + } else if !s.is_empty() { + if !out.is_empty() && !out.ends_with(' ') && !out.ends_with(',') { + out.push(' '); + } + out.push_str(s); + } + } + _ => {} + } + } + + let text = out.trim(); + if text.is_empty() { + None + } else { + Some(text.to_string()) + } + } + _ => None, + } +} + fn apply_property( style: &mut ComputedStyle, property: &str, @@ -2432,8 +2476,8 @@ fn apply_property( // Font-family (inherited) "font-family" => { - if let CssValue::String(s) | CssValue::Keyword(s) = value { - style.font_family = Atom::new(s); + if let Some(s) = font_family_to_css_text(value) { + style.font_family = Atom::new(&s); } } @@ -5416,6 +5460,24 @@ mod tests { assert_eq!(text_node.style.font_size, 20.0); } + #[test] + fn font_family_list_computes_to_css_text() { + let (mut doc, _, _, body) = make_doc_with_body(); + let p = doc.create_element("p"); + let text = doc.create_text("hello"); + doc.append_child(body, p); + doc.append_child(p, text); + + let ss = Parser::parse(r#"p { font-family: "Be Vietnam Pro", Arial, sans-serif; }"#); + let styled = resolve_styles(&doc, &[ss], (800.0, 600.0)).unwrap(); + let text_node = &styled.children[0].children[0].children[0]; + + assert_eq!( + text_node.style.font_family.as_str(), + "Be Vietnam Pro, arial, sans-serif" + ); + } + // ----------------------------------------------------------------------- // Multiple stylesheets // ----------------------------------------------------------------------- diff --git a/crates/text/src/font/registry.rs b/crates/text/src/font/registry.rs index 71eb3c8..07c8b16 100644 --- a/crates/text/src/font/registry.rs +++ b/crates/text/src/font/registry.rs @@ -305,6 +305,46 @@ impl FontRegistry { self.find_font(family).or_else(|| self.find_fallback()) } + /// Find the first available face from a CSS `font-family` list. + /// + /// The input is the computed CSS text, e.g. + /// `"Inter", "Helvetica Neue", sans-serif`. Quoted family names, unquoted + /// names with spaces, and generic families are supported. Returns `None` + /// when no listed family can be resolved. + pub fn find_font_for_family_list( + &self, + family_list: &str, + bold: bool, + italic: bool, + ) -> Option { + for family in parse_css_font_family_list(family_list) { + if let Some(font) = self.find_generic_font(&family, bold, italic) { + return Some(font); + } + if let Some(font) = self.find_font_with_style(&family, bold, italic) { + return Some(font); + } + } + None + } + + fn find_generic_font(&self, family: &str, bold: bool, italic: bool) -> Option { + let candidates: &[&str] = match family.to_ascii_lowercase().as_str() { + "sans-serif" => &["Helvetica", "Arial", "Geneva", "Lucida Grande"], + "serif" => &["Times", "Times New Roman", "Georgia"], + "monospace" => &["Menlo", "Monaco", "Courier New", "Courier"], + "system-ui" => &["SF Pro", "Helvetica", "Arial"], + _ => return None, + }; + + for candidate in candidates { + if let Some(font) = self.find_font_with_style(candidate, bold, italic) { + return Some(font); + } + } + None + } + fn find_local_entry(&self, local_name: &str, bold: bool, italic: bool) -> Option<&FontEntry> { let key = local_name.to_ascii_lowercase(); @@ -378,6 +418,47 @@ impl FontRegistry { } } +fn parse_css_font_family_list(input: &str) -> Vec { + let mut families = Vec::new(); + let mut current = String::new(); + let mut quote = None; + let mut escaped = false; + + for ch in input.chars() { + if escaped { + current.push(ch); + escaped = false; + continue; + } + + if ch == '\\' { + escaped = true; + continue; + } + + match quote { + Some(q) if ch == q => quote = None, + Some(_) => current.push(ch), + None if ch == '"' || ch == '\'' => quote = Some(ch), + None if ch == ',' => { + push_css_font_family(&mut families, &mut current); + } + None => current.push(ch), + } + } + + push_css_font_family(&mut families, &mut current); + families +} + +fn push_css_font_family(out: &mut Vec, current: &mut String) { + let family = current.trim(); + if !family.is_empty() { + out.push(family.to_string()); + } + current.clear(); +} + fn scan_font_dir(dir: &Path, families: &mut HashMap>) { let entries = match std::fs::read_dir(dir) { Ok(e) => e, @@ -571,6 +652,22 @@ mod tests { Path::new("/System/Library/Fonts").exists() } + #[test] + fn parse_css_font_family_list_handles_quotes_and_generics() { + assert_eq!( + parse_css_font_family_list(r#""Be Vietnam Pro", "Roboto Fallback", sans-serif"#), + vec!["Be Vietnam Pro", "Roboto Fallback", "sans-serif"] + ); + assert_eq!( + parse_css_font_family_list("Arial Bold,serif"), + vec!["Arial Bold", "serif"] + ); + assert_eq!( + parse_css_font_family_list(r#""Escaped \" Name", monospace"#), + vec!["Escaped \" Name", "monospace"] + ); + } + #[test] fn registry_discovers_fonts() { if !has_system_fonts() { @@ -856,6 +953,19 @@ mod tests { assert!(font.is_some(), "should find web font by family name"); } + #[test] + fn registry_resolves_css_family_list() { + if !has_system_fonts() { + return; + } + let reg = FontRegistry::new(); + let font = reg.find_font_for_family_list("Definitely Missing, sans-serif", false, false); + assert!( + font.is_some(), + "generic fallback should resolve from a CSS family list" + ); + } + #[test] fn register_local_font_alias() { if !has_system_fonts() { -- 2.51.2