From f96a44c4bc3646e2621fef6903495c2b2302375f Mon Sep 17 00:00:00 2001 From: Adam Date: Sun, 7 Sep 2025 21:16:05 -0700 Subject: [PATCH 1/3] New Typography type --- Cargo.lock | 2 + .../data_panel/data_panel_message_handler.rs | 18 +++ .../document/overlays/utility_types_vello.rs | 1 + node-graph/gcore/src/bounds.rs | 8 +- node-graph/gcore/src/consts.rs | 7 + node-graph/gcore/src/graphic.rs | 25 ++- node-graph/gcore/src/render_complexity.rs | 8 + node-graph/gcore/src/text.rs | 150 ++++++++++++++++++ .../src/text}/source-sans-pro-regular.ttf | Bin node-graph/gpath-bool/src/lib.rs | 1 + node-graph/graph-craft/src/document/value.rs | 19 ++- node-graph/gsvg-renderer/Cargo.toml | 2 + node-graph/gsvg-renderer/src/renderer.rs | 111 +++++++++++++ 13 files changed, 348 insertions(+), 4 deletions(-) rename {editor/src/messages/portfolio/document/overlays => node-graph/gcore/src/text}/source-sans-pro-regular.ttf (100%) 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), From e6d6aa6873ecbc541a2b1d4dec658d122596355e Mon Sep 17 00:00:00 2001 From: Adam Date: Sat, 27 Sep 2025 17:50:12 -0700 Subject: [PATCH 2/3] Integrate into existing font cache --- node-graph/gcore/src/text.rs | 129 +------------------ node-graph/gcore/src/text/text_context.rs | 35 ++--- node-graph/gcore/src/text/to_path.rs | 17 ++- node-graph/gpath-bool/src/lib.rs | 6 +- node-graph/graph-craft/src/document/value.rs | 19 +-- node-graph/gsvg-renderer/src/renderer.rs | 2 +- 6 files changed, 51 insertions(+), 157 deletions(-) diff --git a/node-graph/gcore/src/text.rs b/node-graph/gcore/src/text.rs index dce43239a5..7077da5a56 100644 --- a/node-graph/gcore/src/text.rs +++ b/node-graph/gcore/src/text.rs @@ -3,25 +3,16 @@ mod path_builder; mod text_context; mod to_path; -use std::{ - borrow::Cow, - collections::{HashMap, hash_map::Entry}, - fmt, - sync::{Arc, Mutex}, -}; +use std::fmt; 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 parley::Layout; 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)] @@ -75,7 +66,7 @@ impl Default for TypesettingConfig { #[derive(Clone, DynAny)] pub struct Typography { pub layout: Layout<()>, - pub font_family: String, + pub family_name: String, pub color: Color, pub stroke: Option<(Color, f64)>, } @@ -83,7 +74,7 @@ pub struct Typography { 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("font_family", &self.family_name) .field("color", &self.color) .field("stroke", &self.stroke) .finish() @@ -92,118 +83,12 @@ impl fmt::Debug for Typography { impl PartialEq for Typography { fn eq(&self, _other: &Self) -> bool { - true + unimplemented!("Typography data type cannot be compared") } } 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, - }) + 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 9489aab660..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,7 +319,10 @@ fn flatten_vector(graphic_table: &Table) -> Table { } }) .collect::>(), - Graphic::Typography(typography) => typography.into_iter().flat_map(|row| row.element.to_vector()).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/graph-craft/src/document/value.rs b/node-graph/graph-craft/src/document/value.rs index 0b2bc85991..18e9533f4a 100644 --- a/node-graph/graph-craft/src/document/value.rs +++ b/node-graph/graph-craft/src/document/value.rs @@ -11,7 +11,6 @@ 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; @@ -39,9 +38,7 @@ macro_rules! tagged_value { RenderOutput(RenderOutput), SurfaceFrame(SurfaceFrame), #[serde(skip)] - EditorApi(Arc), - #[serde(skip)] - NewFontCache(NewFontCacheWrapper), + EditorApi(Arc) } // We must manually implement hashing because some values are floats and so do not reproducibly hash (see FakeHash below) @@ -55,7 +52,6 @@ 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), } } } @@ -68,7 +64,6 @@ 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 @@ -79,7 +74,6 @@ 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 @@ -89,8 +83,7 @@ macro_rules! tagged_value { $( Self::$identifier(_) => concrete!($ty), )* Self::RenderOutput(_) => concrete!(RenderOutput), Self::SurfaceFrame(_) => concrete!(SurfaceFrame), - Self::EditorApi(_) => concrete!(&WasmEditorApi), - Self::NewFontCache(_) => concrete!(NewFontCacheWrapper), + Self::EditorApi(_) => concrete!(&WasmEditorApi) } } /// Attempts to downcast the dynamic type to a tagged value @@ -529,14 +522,6 @@ 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/src/renderer.rs b/node-graph/gsvg-renderer/src/renderer.rs index 75138a131c..0ecac289c7 100644 --- a/node-graph/gsvg-renderer/src/renderer.rs +++ b/node-graph/gsvg-renderer/src/renderer.rs @@ -1596,7 +1596,7 @@ impl Render for Table { attributes.push("transform", matrix); } - attributes.push("font-family", table_row.element.font_family.clone()); + 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); From fe7766b845cd921d99c89776363cc48e0884faba Mon Sep 17 00:00:00 2001 From: Adam Date: Sat, 4 Oct 2025 18:59:09 -0700 Subject: [PATCH 3/3] Code review fixes --- .../data_panel/data_panel_message_handler.rs | 8 +++----- .../document/overlays}/source-sans-pro-regular.ttf | Bin .../document/overlays/utility_types_vello.rs | 1 - node-graph/gcore/src/consts.rs | 7 ------- node-graph/gcore/src/render_complexity.rs | 2 +- node-graph/gcore/src/text.rs | 2 +- 6 files changed, 5 insertions(+), 15 deletions(-) rename {node-graph/gcore/src/text => editor/src/messages/portfolio/document/overlays}/source-sans-pro-regular.ttf (100%) 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 d3bdb75a05..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 @@ -546,13 +546,11 @@ impl TableRowLayout for Typography { "Typography" } fn identifier(&self) -> String { - "Typography".to_string() - } - fn element_widget(&self, _index: usize) -> WidgetHolder { - TextLabel::new("Not supported").widget_holder() + format!("Typography: {self:?}") } fn element_page(&self, _data: &mut LayoutData) -> Vec { - vec![LayoutGroup::Row { widgets: Vec::new() }] + let widgets = vec![TextLabel::new("TODO").widget_holder()]; + vec![LayoutGroup::Row { widgets }] } } diff --git a/node-graph/gcore/src/text/source-sans-pro-regular.ttf b/editor/src/messages/portfolio/document/overlays/source-sans-pro-regular.ttf similarity index 100% rename from node-graph/gcore/src/text/source-sans-pro-regular.ttf rename to editor/src/messages/portfolio/document/overlays/source-sans-pro-regular.ttf 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 6c7c91b385..c188d3b574 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types_vello.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types_vello.rs @@ -9,7 +9,6 @@ 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/consts.rs b/node-graph/gcore/src/consts.rs index cec018c9e5..505dc81ccd 100644 --- a/node-graph/gcore/src/consts.rs +++ b/node-graph/gcore/src/consts.rs @@ -7,10 +7,3 @@ 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/render_complexity.rs b/node-graph/gcore/src/render_complexity.rs index 3f8e1e8f5a..abba3fbe5d 100644 --- a/node-graph/gcore/src/render_complexity.rs +++ b/node-graph/gcore/src/render_complexity.rs @@ -70,6 +70,6 @@ impl RenderComplexity for GradientStops { impl RenderComplexity for Typography { fn render_complexity(&self) -> usize { - 1 + 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 7077da5a56..155f021b22 100644 --- a/node-graph/gcore/src/text.rs +++ b/node-graph/gcore/src/text.rs @@ -83,7 +83,7 @@ impl fmt::Debug for Typography { impl PartialEq for Typography { fn eq(&self, _other: &Self) -> bool { - unimplemented!("Typography data type cannot be compared") + unimplemented!("Typography cannot be compared") } }