diff --git a/Cargo.lock b/Cargo.lock index ae28a7bccb..4c260835ab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2232,7 +2232,9 @@ dependencies = [ "kurbo", "log", "num-traits", + "parley", "serde", + "skrifa 0.36.0", "usvg 0.45.1", "vello", ] diff --git a/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs b/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs index b478caac64..d3bdb75a05 100644 --- a/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs +++ b/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs @@ -12,6 +12,7 @@ use graphene_std::gradient::GradientStops; use graphene_std::memo::IORecord; use graphene_std::raster_types::{CPU, GPU, Raster}; use graphene_std::table::Table; +use graphene_std::text::Typography; use graphene_std::vector::Vector; use graphene_std::vector::style::{Fill, FillChoice}; use graphene_std::{Artboard, Graphic}; @@ -269,6 +270,7 @@ impl TableRowLayout for Graphic { Self::RasterGPU(table) => table.identifier(), Self::Color(table) => table.identifier(), Self::Gradient(table) => table.identifier(), + Self::Typography(table) => table.identifier(), } } // Don't put a breadcrumb for Graphic @@ -283,6 +285,7 @@ impl TableRowLayout for Graphic { Self::RasterGPU(table) => table.layout_with_breadcrumb(data), Self::Color(table) => table.layout_with_breadcrumb(data), Self::Gradient(table) => table.layout_with_breadcrumb(data), + Self::Typography(table) => table.layout_with_breadcrumb(data), } } } @@ -538,6 +541,21 @@ impl TableRowLayout for GradientStops { } } +impl TableRowLayout for Typography { + fn type_name() -> &'static str { + "Typography" + } + fn identifier(&self) -> String { + "Typography".to_string() + } + fn element_widget(&self, _index: usize) -> WidgetHolder { + TextLabel::new("Not supported").widget_holder() + } + fn element_page(&self, _data: &mut LayoutData) -> Vec { + vec![LayoutGroup::Row { widgets: Vec::new() }] + } +} + impl TableRowLayout for f64 { fn type_name() -> &'static str { "Number (f64)" diff --git a/editor/src/messages/portfolio/document/overlays/utility_types_vello.rs b/editor/src/messages/portfolio/document/overlays/utility_types_vello.rs index c188d3b574..6c7c91b385 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types_vello.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types_vello.rs @@ -9,6 +9,7 @@ use core::borrow::Borrow; use core::f64::consts::{FRAC_PI_2, PI, TAU}; use glam::{DAffine2, DVec2}; use graphene_std::Color; +use graphene_std::consts::SOURCE_SANS_FONT_DATA; use graphene_std::math::quad::Quad; use graphene_std::subpath::{self, Subpath}; use graphene_std::table::Table; diff --git a/node-graph/gcore/src/bounds.rs b/node-graph/gcore/src/bounds.rs index fb4b19cd42..852abc2788 100644 --- a/node-graph/gcore/src/bounds.rs +++ b/node-graph/gcore/src/bounds.rs @@ -1,4 +1,4 @@ -use crate::{Color, gradient::GradientStops}; +use crate::{Color, gradient::GradientStops, text::Typography}; use glam::{DAffine2, DVec2}; #[derive(Clone, Copy, Default, Debug, PartialEq)] @@ -38,3 +38,9 @@ impl BoundingBox for GradientStops { RenderBoundingBox::Infinite } } +impl BoundingBox for Typography { + fn bounding_box(&self, transform: DAffine2, _include_stroke: bool) -> RenderBoundingBox { + let bbox = DVec2::new(self.layout.full_width() as f64, self.layout.height() as f64); + RenderBoundingBox::Rectangle([transform.translation, transform.transform_point2(bbox)]) + } +} diff --git a/node-graph/gcore/src/consts.rs b/node-graph/gcore/src/consts.rs index 505dc81ccd..cec018c9e5 100644 --- a/node-graph/gcore/src/consts.rs +++ b/node-graph/gcore/src/consts.rs @@ -7,3 +7,10 @@ pub const LAYER_OUTLINE_STROKE_WEIGHT: f64 = 0.5; // Fonts pub const DEFAULT_FONT_FAMILY: &str = "Cabin"; pub const DEFAULT_FONT_STYLE: &str = "Regular (400)"; + +// Load Source Sans Pro font data +// TODO: Grab this from the node_modules folder (either with `include_bytes!` or ideally at runtime) instead of checking the font file into the repo. +// TODO: And maybe use the WOFF2 version (if it's supported) for its smaller, compressed file size. +pub const SOURCE_SANS_FONT_DATA: &[u8] = include_bytes!("text/source-sans-pro-regular.ttf"); +pub const SOURCE_SANS_FONT_FAMILY: &str = "Source Sans Pro"; +pub const SOURCE_SANS_FONT_STYLE: &str = "Regular (400)"; diff --git a/node-graph/gcore/src/graphic.rs b/node-graph/gcore/src/graphic.rs index bd7a4677a6..197c3ee585 100644 --- a/node-graph/gcore/src/graphic.rs +++ b/node-graph/gcore/src/graphic.rs @@ -3,6 +3,7 @@ use crate::bounds::{BoundingBox, RenderBoundingBox}; use crate::gradient::GradientStops; use crate::raster_types::{CPU, GPU, Raster}; use crate::table::{Table, TableRow}; +use crate::text::Typography; use crate::uuid::NodeId; use crate::vector::Vector; use crate::{Artboard, Color, Ctx}; @@ -11,7 +12,7 @@ use glam::{DAffine2, DVec2}; use std::hash::Hash; /// The possible forms of graphical content that can be rendered by the Render node into either an image or SVG syntax. -#[derive(Clone, Debug, Hash, PartialEq, DynAny, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, Hash, PartialEq, DynAny)] pub enum Graphic { Graphic(Table), Vector(Table), @@ -19,6 +20,26 @@ pub enum Graphic { RasterGPU(Table>), Color(Table), Gradient(Table), + Typography(Table), +} + +impl serde::Serialize for Graphic { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let default: Table = Table::new(); + default.serialize(serializer) + } +} + +impl<'de> serde::Deserialize<'de> for Graphic { + fn deserialize(_deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + Ok(Graphic::Graphic(Table::new())) + } } impl Default for Graphic { @@ -232,6 +253,7 @@ impl Graphic { Graphic::RasterGPU(raster) => raster.iter().all(|row| row.alpha_blending.clip), Graphic::Color(color) => color.iter().all(|row| row.alpha_blending.clip), Graphic::Gradient(gradient) => gradient.iter().all(|row| row.alpha_blending.clip), + Graphic::Typography(typography) => typography.iter().all(|row| row.alpha_blending.clip), } } @@ -256,6 +278,7 @@ impl BoundingBox for Graphic { Graphic::Graphic(graphic) => graphic.bounding_box(transform, include_stroke), Graphic::Color(color) => color.bounding_box(transform, include_stroke), Graphic::Gradient(gradient) => gradient.bounding_box(transform, include_stroke), + Graphic::Typography(typography) => typography.bounding_box(transform, include_stroke), } } } diff --git a/node-graph/gcore/src/render_complexity.rs b/node-graph/gcore/src/render_complexity.rs index 7920479377..3f8e1e8f5a 100644 --- a/node-graph/gcore/src/render_complexity.rs +++ b/node-graph/gcore/src/render_complexity.rs @@ -1,6 +1,7 @@ use crate::gradient::GradientStops; use crate::raster_types::{CPU, GPU, Raster}; use crate::table::Table; +use crate::text::Typography; use crate::vector::Vector; use crate::{Artboard, Color, Graphic}; @@ -31,6 +32,7 @@ impl RenderComplexity for Graphic { Self::RasterGPU(table) => table.render_complexity(), Self::Color(table) => table.render_complexity(), Self::Gradient(table) => table.render_complexity(), + Self::Typography(table) => table.render_complexity(), } } } @@ -65,3 +67,9 @@ impl RenderComplexity for GradientStops { 1 } } + +impl RenderComplexity for Typography { + fn render_complexity(&self) -> usize { + 1 + } +} diff --git a/node-graph/gcore/src/text.rs b/node-graph/gcore/src/text.rs index 9321d7f729..dce43239a5 100644 --- a/node-graph/gcore/src/text.rs +++ b/node-graph/gcore/src/text.rs @@ -3,11 +3,25 @@ mod path_builder; mod text_context; mod to_path; +use std::{ + borrow::Cow, + collections::{HashMap, hash_map::Entry}, + fmt, + sync::{Arc, Mutex}, +}; + use dyn_any::DynAny; pub use font_cache::*; +use graphene_core_shaders::color::Color; +use parley::{Layout, StyleProperty}; +use rustc_hash::FxBuildHasher; +use std::hash::BuildHasher; +use std::hash::{Hash, Hasher}; pub use text_context::TextContext; pub use to_path::*; +use crate::{consts::*, table::Table, vector::Vector}; + /// Alignment of lines of type within a text block. #[repr(C)] #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)] @@ -57,3 +71,139 @@ impl Default for TypesettingConfig { } } } + +#[derive(Clone, DynAny)] +pub struct Typography { + pub layout: Layout<()>, + pub font_family: String, + pub color: Color, + pub stroke: Option<(Color, f64)>, +} + +impl fmt::Debug for Typography { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Typography") + .field("font_family", &self.font_family) + .field("color", &self.color) + .field("stroke", &self.stroke) + .finish() + } +} + +impl PartialEq for Typography { + fn eq(&self, _other: &Self) -> bool { + true + } +} + +impl Hash for Typography { + fn hash(&self, state: &mut H) { + self.layout.len().hash(state); + } +} + +impl Typography { + pub fn to_vector(&self) -> Table { + // To implement this function, a clone of the `NewFontCacheWrapper` must be included in the typography data type + Table::new() + } +} + +#[derive(Clone)] +pub struct NewFontCacheWrapper(pub Arc>); + +impl fmt::Debug for NewFontCacheWrapper { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("font cache").finish() + } +} + +impl PartialEq for NewFontCacheWrapper { + fn eq(&self, _other: &Self) -> bool { + log::error!("Font cache should not be compared"); + false + } +} + +unsafe impl dyn_any::StaticType for NewFontCacheWrapper { + type Static = NewFontCacheWrapper; +} + +pub struct NewFontCache { + pub font_context: parley::FontContext, + pub layout_context: parley::LayoutContext<()>, + pub font_mapping: HashMap, + pub hash: u64, +} + +impl NewFontCache { + pub fn new() -> Self { + let mut new = NewFontCache { + font_context: parley::FontContext::new(), + layout_context: parley::LayoutContext::new(), + font_mapping: HashMap::new(), + hash: 0, + }; + + let source_sans_font = Font::new(SOURCE_SANS_FONT_FAMILY.to_string(), SOURCE_SANS_FONT_STYLE.to_string()); + new.register_font(source_sans_font, SOURCE_SANS_FONT_DATA.to_vec()); + new + } + + pub fn register_font(&mut self, font: Font, data: Vec) { + match self.font_mapping.entry(font) { + Entry::Occupied(occupied_entry) => { + log::error!("Trying to register font that already is added: {:?}", occupied_entry.key()); + } + Entry::Vacant(vacant_entry) => { + let registered_font = self.font_context.collection.register_fonts(parley::fontique::Blob::from(data), None); + if registered_font.len() > 1 { + log::error!("Registered multiple fonts for {:?}. Only the first is accessible", vacant_entry.key()); + }; + match registered_font.into_iter().next() { + Some((family_id, font_info)) => { + let Some(family_name) = self.font_context.collection.family_name(family_id) else { + log::error!("Could not get family name for font: {:?}", vacant_entry.key()); + return; + }; + let Some(font_info) = font_info.into_iter().next() else { + log::error!("Could not get font info for font: {:?}", vacant_entry.key()); + return; + }; + // Hash the Font for a unique id and add it to the cached hash + let hash_value = FxBuildHasher.hash_one(vacant_entry.key()); + self.hash = self.hash.wrapping_add(hash_value); + + vacant_entry.insert((family_name.to_string(), font_info)); + } + None => log::error!("Could not register font for {:?}", vacant_entry.key()), + } + } + } + } + + pub fn generate_typography(&mut self, font: &Font, font_size: f32, text: &str) -> Option { + let Some((font_family, font_info)) = self.font_mapping.get(font) else { + log::error!("Font not loaded: {:?}", font); + return None; + }; + let font_family = font_family.to_string(); + + let mut builder = self.layout_context.ranged_builder(&mut self.font_context, text, 1., false); + + builder.push_default(StyleProperty::FontStack(parley::FontStack::Single(parley::FontFamily::Named(Cow::Owned(font_family.clone()))))); + builder.push_default(StyleProperty::FontSize(font_size)); + builder.push_default(StyleProperty::FontWeight(font_info.weight())); + builder.push_default(StyleProperty::FontStyle(font_info.style())); + builder.push_default(StyleProperty::FontWidth(font_info.width())); + + let mut layout: Layout<()> = builder.build(text); + layout.break_all_lines(None); + Some(Typography { + layout, + font_family, + color: Color::BLACK, + stroke: None, + }) + } +} diff --git a/editor/src/messages/portfolio/document/overlays/source-sans-pro-regular.ttf b/node-graph/gcore/src/text/source-sans-pro-regular.ttf similarity index 100% rename from editor/src/messages/portfolio/document/overlays/source-sans-pro-regular.ttf rename to node-graph/gcore/src/text/source-sans-pro-regular.ttf diff --git a/node-graph/gpath-bool/src/lib.rs b/node-graph/gpath-bool/src/lib.rs index df3a089414..9489aab660 100644 --- a/node-graph/gpath-bool/src/lib.rs +++ b/node-graph/gpath-bool/src/lib.rs @@ -318,6 +318,7 @@ fn flatten_vector(graphic_table: &Table) -> Table { } }) .collect::>(), + Graphic::Typography(typography) => typography.into_iter().flat_map(|row| row.element.to_vector()).collect::>(), } }) .collect() diff --git a/node-graph/graph-craft/src/document/value.rs b/node-graph/graph-craft/src/document/value.rs index 18e9533f4a..0b2bc85991 100644 --- a/node-graph/graph-craft/src/document/value.rs +++ b/node-graph/graph-craft/src/document/value.rs @@ -11,6 +11,7 @@ use graphene_brush::brush_stroke::BrushStroke; use graphene_core::raster::Image; use graphene_core::raster_types::{CPU, Raster}; use graphene_core::table::Table; +use graphene_core::text::NewFontCacheWrapper; use graphene_core::transform::ReferencePoint; use graphene_core::uuid::NodeId; use graphene_core::vector::Vector; @@ -38,7 +39,9 @@ macro_rules! tagged_value { RenderOutput(RenderOutput), SurfaceFrame(SurfaceFrame), #[serde(skip)] - EditorApi(Arc) + EditorApi(Arc), + #[serde(skip)] + NewFontCache(NewFontCacheWrapper), } // We must manually implement hashing because some values are floats and so do not reproducibly hash (see FakeHash below) @@ -52,6 +55,7 @@ macro_rules! tagged_value { Self::RenderOutput(x) => x.hash(state), Self::SurfaceFrame(x) => x.hash(state), Self::EditorApi(x) => x.hash(state), + Self::NewFontCache(x) => x.hash(state), } } } @@ -64,6 +68,7 @@ macro_rules! tagged_value { Self::RenderOutput(x) => Box::new(x), Self::SurfaceFrame(x) => Box::new(x), Self::EditorApi(x) => Box::new(x), + Self::NewFontCache(x) => Box::new(x), } } /// Converts to a Arc @@ -74,6 +79,7 @@ macro_rules! tagged_value { Self::RenderOutput(x) => Arc::new(x), Self::SurfaceFrame(x) => Arc::new(x), Self::EditorApi(x) => Arc::new(x), + Self::NewFontCache(x) => Arc::new(x), } } /// Creates a graphene_core::Type::Concrete(TypeDescriptor { .. }) with the type of the value inside the tagged value @@ -83,7 +89,8 @@ macro_rules! tagged_value { $( Self::$identifier(_) => concrete!($ty), )* Self::RenderOutput(_) => concrete!(RenderOutput), Self::SurfaceFrame(_) => concrete!(SurfaceFrame), - Self::EditorApi(_) => concrete!(&WasmEditorApi) + Self::EditorApi(_) => concrete!(&WasmEditorApi), + Self::NewFontCache(_) => concrete!(NewFontCacheWrapper), } } /// Attempts to downcast the dynamic type to a tagged value @@ -522,6 +529,14 @@ mod fake_hash { self.1.hash(state) } } + impl FakeHash for NewFontCacheWrapper { + fn hash(&self, state: &mut H) { + match self.0.lock() { + Ok(inner) => inner.hash.hash(state), + Err(_) => log::error!("Could not lock font cache when hashing"), + } + } + } } #[test] diff --git a/node-graph/gsvg-renderer/Cargo.toml b/node-graph/gsvg-renderer/Cargo.toml index a6d0e395c5..d606dee915 100644 --- a/node-graph/gsvg-renderer/Cargo.toml +++ b/node-graph/gsvg-renderer/Cargo.toml @@ -19,6 +19,8 @@ log = { workspace = true } num-traits = { workspace = true } usvg = { workspace = true } kurbo = { workspace = true } +parley = { workspace = true } +skrifa = { workspace = true } # Optional workspace dependencies vello = { workspace = true, optional = true } diff --git a/node-graph/gsvg-renderer/src/renderer.rs b/node-graph/gsvg-renderer/src/renderer.rs index dda50dd321..75138a131c 100644 --- a/node-graph/gsvg-renderer/src/renderer.rs +++ b/node-graph/gsvg-renderer/src/renderer.rs @@ -15,6 +15,7 @@ use graphene_core::raster_types::{CPU, GPU, Raster}; use graphene_core::render_complexity::RenderComplexity; use graphene_core::subpath::Subpath; use graphene_core::table::{Table, TableRow}; +use graphene_core::text::Typography; use graphene_core::transform::{Footprint, Transform}; use graphene_core::uuid::{NodeId, generate_uuid}; use graphene_core::vector::Vector; @@ -23,11 +24,14 @@ use graphene_core::vector::style::{Fill, PaintOrder, RenderMode, Stroke, StrokeA use graphene_core::{Artboard, Graphic}; use kurbo::Affine; use num_traits::Zero; +use skrifa::MetadataProvider; +use skrifa::attribute::Style; use std::collections::{HashMap, HashSet}; use std::fmt::Write; use std::hash::{DefaultHasher, Hash, Hasher}; use std::ops::Deref; use std::sync::{Arc, LazyLock}; +use vello::peniko::StyleRef; #[cfg(feature = "vello")] use vello::*; @@ -115,6 +119,18 @@ impl SvgRender { self.svg.push("/>".into()); } + pub fn leaf_text(&mut self, text: impl Into, attributes: impl FnOnce(&mut SvgRenderAttrs)) { + self.indent(); + + self.svg.push("".into()); + self.svg.push(text.into()); + self.svg.push("".into()); + } + pub fn leaf_node(&mut self, content: impl Into) { self.indent(); self.svg.push(content.into()); @@ -289,6 +305,7 @@ impl Render for Graphic { Graphic::RasterGPU(_) => (), Graphic::Color(table) => table.render_svg(render, render_params), Graphic::Gradient(table) => table.render_svg(render, render_params), + Graphic::Typography(table) => table.render_svg(render, render_params), } } @@ -301,6 +318,7 @@ impl Render for Graphic { Graphic::RasterGPU(table) => table.render_to_vello(scene, transform, context, render_params), Graphic::Color(table) => table.render_to_vello(scene, transform, context, render_params), Graphic::Gradient(table) => table.render_to_vello(scene, transform, context, render_params), + Graphic::Typography(table) => table.render_to_vello(scene, transform, context, render_params), } } @@ -345,6 +363,14 @@ impl Render for Graphic { Graphic::Gradient(table) => { metadata.upstream_footprints.insert(element_id, footprint); + // TODO: Find a way to handle more than the first row + if let Some(row) = table.iter().next() { + metadata.local_transforms.insert(element_id, *row.transform); + } + } + Graphic::Typography(table) => { + metadata.upstream_footprints.insert(element_id, footprint); + // TODO: Find a way to handle more than the first row if let Some(row) = table.iter().next() { metadata.local_transforms.insert(element_id, *row.transform); @@ -360,6 +386,7 @@ impl Render for Graphic { Graphic::RasterGPU(table) => table.collect_metadata(metadata, footprint, element_id), Graphic::Color(table) => table.collect_metadata(metadata, footprint, element_id), Graphic::Gradient(table) => table.collect_metadata(metadata, footprint, element_id), + Graphic::Typography(table) => table.collect_metadata(metadata, footprint, element_id), } } @@ -371,6 +398,7 @@ impl Render for Graphic { Graphic::RasterGPU(table) => table.add_upstream_click_targets(click_targets), Graphic::Color(table) => table.add_upstream_click_targets(click_targets), Graphic::Gradient(table) => table.add_upstream_click_targets(click_targets), + Graphic::Typography(table) => table.add_upstream_click_targets(click_targets), } } @@ -382,6 +410,7 @@ impl Render for Graphic { Graphic::RasterGPU(table) => table.contains_artboard(), Graphic::Color(table) => table.contains_artboard(), Graphic::Gradient(table) => table.contains_artboard(), + Graphic::Typography(table) => table.contains_artboard(), } } @@ -393,6 +422,7 @@ impl Render for Graphic { Graphic::RasterGPU(_) => (), Graphic::Color(_) => (), Graphic::Gradient(_) => (), + Graphic::Typography(_) => (), } } } @@ -1542,6 +1572,87 @@ impl Render for Table { } } +impl Render for Table { + fn render_svg(&self, render: &mut SvgRender, _render_params: &RenderParams) { + for table_row in self.iter() { + for line in table_row.element.layout.lines() { + for item in line.items() { + match item { + parley::PositionedLayoutItem::GlyphRun(glyph_run) => { + let font = glyph_run.run().font(); + let font_ref = skrifa::FontRef::from_index(font.data.as_ref(), font.index).unwrap(); + let font_attributes = font_ref.attributes(); + let font_style = match font_attributes.style { + Style::Normal => "normal".to_string(), + Style::Italic => "italic".to_string(), + Style::Oblique(Some(angle)) => format!("oblique {}deg", angle), + Style::Oblique(None) => "oblique".to_string(), + }; + render.parent_tag( + "text", + |attributes| { + let matrix = format_transform_matrix(*table_row.transform); + if !matrix.is_empty() { + attributes.push("transform", matrix); + } + + attributes.push("font-family", table_row.element.font_family.clone()); + attributes.push("font-weight", font_attributes.weight.value().to_string()); + attributes.push("font-size", glyph_run.run().font_size().to_string()); + attributes.push("font-style", font_style); + attributes.push("fill", format!("#{}", table_row.element.color.to_rgb_hex_srgb_from_gamma())); + attributes.push("opacity", table_row.alpha_blending.opacity.to_string()); + if let Some((stroke_color, stroke_width)) = table_row.element.stroke.as_ref().cloned() { + attributes.push("stroke-color", format!("#{}", stroke_color.to_rgb_hex_srgb_from_gamma())); + attributes.push("stroke-width", format!("{stroke_width}")); + } + }, + |render| { + for glyph in glyph_run.positioned_glyphs() { + let character = font_ref.glyph_names().get(skrifa::GlyphId::new(glyph.id as u32)).unwrap().as_str().to_string(); + let character = character.replace("space", " "); + render.leaf_text(character, |attributes| { + attributes.push("x", glyph.x.to_string()); + attributes.push("y", glyph.y.to_string()); + }); + } + }, + ); + } + parley::PositionedLayoutItem::InlineBox(_positioned_inline_box) => { + log::error!("Inline box text rendering not supported"); + } + } + } + } + } + } + + fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, _context: &mut RenderContext, _render_params: &RenderParams) { + for table_row in self.iter() { + for line in table_row.element.layout.lines() { + for item in line.items() { + match item { + parley::PositionedLayoutItem::GlyphRun(glyph_run) => { + let color = table_row.element.color.clone(); + scene + .draw_glyphs(glyph_run.run().font()) + .transform(kurbo::Affine::new((transform * *table_row.transform).to_cols_array())) + .font_size(glyph_run.run().font_size()) + .brush(peniko::BrushRef::Solid(peniko::Color::new([color.r(), color.g(), color.b(), color.a()]))) + .brush_alpha(table_row.alpha_blending.opacity) + .draw(StyleRef::Fill(peniko::Fill::EvenOdd), glyph_run.glyphs().map(|g| Glyph { id: g.id as u32, x: g.x, y: g.y })); + } + parley::PositionedLayoutItem::InlineBox(_positioned_inline_box) => { + log::error!("Cannot render positioned inline box to vello"); + } + } + } + } + } + } +} + #[derive(Debug, Clone, PartialEq, Eq)] pub enum SvgSegment { Slice(&'static str),