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..52397da8ad 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,19 @@ impl TableRowLayout for GradientStops { } } +impl TableRowLayout for Typography { + fn type_name() -> &'static str { + "Typography" + } + fn identifier(&self) -> String { + format!("Typography: {self:?}") + } + fn element_page(&self, _data: &mut LayoutData) -> Vec { + let widgets = vec![TextLabel::new("TODO").widget_holder()]; + vec![LayoutGroup::Row { widgets }] + } +} + impl TableRowLayout for f64 { fn type_name() -> &'static str { "Number (f64)" 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/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..abba3fbe5d 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 { + self.layout.lines().map(|line| line.items().count()).sum() + } +} diff --git a/node-graph/gcore/src/text.rs b/node-graph/gcore/src/text.rs index 9321d7f729..155f021b22 100644 --- a/node-graph/gcore/src/text.rs +++ b/node-graph/gcore/src/text.rs @@ -3,8 +3,13 @@ mod path_builder; mod text_context; mod to_path; +use std::fmt; + use dyn_any::DynAny; pub use font_cache::*; +use graphene_core_shaders::color::Color; +use parley::Layout; +use std::hash::{Hash, Hasher}; pub use text_context::TextContext; pub use to_path::*; @@ -57,3 +62,33 @@ impl Default for TypesettingConfig { } } } + +#[derive(Clone, DynAny)] +pub struct Typography { + pub layout: Layout<()>, + pub family_name: 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.family_name) + .field("color", &self.color) + .field("stroke", &self.stroke) + .finish() + } +} + +impl PartialEq for Typography { + fn eq(&self, _other: &Self) -> bool { + unimplemented!("Typography cannot be compared") + } +} + +impl Hash for Typography { + fn hash(&self, _: &mut H) { + unimplemented!("Typography cannot be hashed") + } +} diff --git a/node-graph/gcore/src/text/text_context.rs b/node-graph/gcore/src/text/text_context.rs index da7952493b..44a765a63f 100644 --- a/node-graph/gcore/src/text/text_context.rs +++ b/node-graph/gcore/src/text/text_context.rs @@ -38,34 +38,35 @@ impl TextContext { } /// Get or cache font information for a given font - fn get_font_info(&mut self, font: &Font, font_data: &Blob) -> Option<(String, FontInfo)> { + pub fn get_font_info(&mut self, font: &Font, font_cache: &FontCache) -> Option<(String, FontInfo)> { + // Note that the actual_font may not be the desired font if that font is not yet loaded. + // It is important not to cache the default font under the name of another font. + let (font_data, actual_font) = self.resolve_font_data(font, font_cache)?; + // Check if we already have the font info cached - if let Some((family_id, font_info)) = self.font_info_cache.get(font) { + if let Some((family_id, font_info)) = self.font_info_cache.get(actual_font) { if let Some(family_name) = self.font_context.collection.family_name(*family_id) { return Some((family_name.to_string(), font_info.clone())); } } // Register the font and cache the info - let families = self.font_context.collection.register_fonts(font_data.clone(), None); + let families = self.font_context.collection.register_fonts(font_data, None); - families.first().and_then(|(family_id, fonts_info)| { - fonts_info.first().and_then(|font_info| { - self.font_context.collection.family_name(*family_id).map(|family_name| { + families.into_iter().next().and_then(|(family_id, fonts_info)| { + fonts_info.into_iter().next().and_then(|font_info| { + self.font_context.collection.family_name(family_id).map(|family_name| { // Cache the font info for future use - self.font_info_cache.insert(font.clone(), (*family_id, font_info.clone())); - (family_name.to_string(), font_info.clone()) + self.font_info_cache.insert(actual_font.clone(), (family_id, font_info.clone())); + (family_name.to_string(), font_info) }) }) }) } /// Create a text layout using the specified font and typesetting configuration - fn layout_text(&mut self, text: &str, font: &Font, font_cache: &FontCache, typesetting: TypesettingConfig) -> Option> { - // Note that the actual_font may not be the desired font if that font is not yet loaded. - // It is important not to cache the default font under the name of another font. - let (font_data, actual_font) = self.resolve_font_data(font, font_cache)?; - let (font_family, font_info) = self.get_font_info(actual_font, &font_data)?; + pub fn layout_text(&mut self, text: &str, font: &Font, font_cache: &FontCache, typesetting: TypesettingConfig) -> Option> { + let (font_family, font_info) = self.get_font_info(font, font_cache)?; const DISPLAY_SCALE: f32 = 1.; let mut builder = self.layout_context.ranged_builder(&mut self.font_context, text, DISPLAY_SCALE, false); @@ -87,17 +88,21 @@ impl TextContext { } /// Convert text to vector paths using the specified font and typesetting configuration - pub fn to_path(&mut self, text: &str, font: &Font, font_cache: &FontCache, typesetting: TypesettingConfig, per_glyph_instances: bool) -> Table { + pub fn text_to_path(&mut self, text: &str, font: &Font, font_cache: &FontCache, typesetting: TypesettingConfig, per_glyph_instances: bool) -> Table { let Some(layout) = self.layout_text(text, font, font_cache, typesetting) else { return Table::new_from_element(Vector::default()); }; + self.layout_to_path(layout, 0., per_glyph_instances) + } + + pub fn layout_to_path(&mut self, layout: Layout<()>, tilt: f64, per_glyph_instances: bool) -> Table { let mut path_builder = PathBuilder::new(per_glyph_instances, layout.scale() as f64); for line in layout.lines() { for item in line.items() { if let PositionedLayoutItem::GlyphRun(glyph_run) = item { - path_builder.render_glyph_run(&glyph_run, typesetting.tilt, per_glyph_instances); + path_builder.render_glyph_run(&glyph_run, tilt, per_glyph_instances); } } } diff --git a/node-graph/gcore/src/text/to_path.rs b/node-graph/gcore/src/text/to_path.rs index 64fc4b3b5a..4f0457d179 100644 --- a/node-graph/gcore/src/text/to_path.rs +++ b/node-graph/gcore/src/text/to_path.rs @@ -1,13 +1,28 @@ use super::text_context::TextContext; use super::{Font, FontCache, TypesettingConfig}; use crate::table::Table; +use crate::text::Typography; use crate::vector::Vector; use glam::DVec2; +use graphene_core_shaders::color::Color; use parley::fontique::Blob; use std::sync::Arc; +pub fn to_typography(text: &str, font: &Font, font_cache: &FontCache, typesetting: TypesettingConfig) -> Option { + TextContext::with_thread_local(|ctx| { + let layout = ctx.layout_text(text, font, font_cache, typesetting)?; + let (family_name, _) = ctx.get_font_info(font, font_cache)?; + Some(Typography { + layout, + family_name, + color: Color::BLACK, + stroke: None, + }) + }) +} + pub fn to_path(text: &str, font: &Font, font_cache: &FontCache, typesetting: TypesettingConfig, per_glyph_instances: bool) -> Table { - TextContext::with_thread_local(|ctx| ctx.to_path(text, font, font_cache, typesetting, per_glyph_instances)) + TextContext::with_thread_local(|ctx| ctx.text_to_path(text, font, font_cache, typesetting, per_glyph_instances)) } pub fn bounding_box(text: &str, font: &Font, font_cache: &FontCache, typesetting: TypesettingConfig, for_clipping_test: bool) -> DVec2 { diff --git a/node-graph/gpath-bool/src/lib.rs b/node-graph/gpath-bool/src/lib.rs index df3a089414..5abb5def2a 100644 --- a/node-graph/gpath-bool/src/lib.rs +++ b/node-graph/gpath-bool/src/lib.rs @@ -2,6 +2,7 @@ use dyn_any::DynAny; use glam::{DAffine2, DVec2}; use graphene_core::subpath::{ManipulatorGroup, PathSegPoints, Subpath, pathseg_points}; use graphene_core::table::{Table, TableRow, TableRowRef}; +use graphene_core::text::TextContext; use graphene_core::vector::algorithms::merge_by_distance::MergeByDistanceExt; use graphene_core::vector::style::Fill; use graphene_core::vector::{PointId, Vector}; @@ -318,6 +319,10 @@ fn flatten_vector(graphic_table: &Table) -> Table { } }) .collect::>(), + Graphic::Typography(typography) => typography + .into_iter() + .flat_map(|row| TextContext::with_thread_local(|ctx| ctx.layout_to_path(row.element.layout, 0., false))) + .collect::>(), } }) .collect() 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..0ecac289c7 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.family_name.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),