From 9dafacbff38b314c9685194c8ad2fbad43e7105e Mon Sep 17 00:00:00 2001 From: Annonnymmousss Date: Wed, 13 May 2026 03:08:36 +0530 Subject: [PATCH 01/12] feat: Render List as raw paths in SVG and Vell mode --- Cargo.lock | 2 + .../data_panel/data_panel_message_handler.rs | 2 + node-graph/libraries/core-types/src/lib.rs | 2 +- node-graph/libraries/core-types/src/list.rs | 6 + .../core-types/src/render_complexity.rs | 6 + .../libraries/graphic-types/src/graphic.rs | 29 ++ node-graph/libraries/rendering/Cargo.toml | 2 + .../libraries/rendering/src/renderer.rs | 343 +++++++++++++++++- node-graph/nodes/gstd/src/render_node.rs | 1 + node-graph/nodes/path-bool/src/lib.rs | 1 + 10 files changed, 389 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f3878680c1..93426ad3c2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4582,7 +4582,9 @@ dependencies = [ "kurbo", "log", "num-traits", + "parley", "serde", + "skrifa 0.40.0", "usvg", "vector-types", "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 36cde235a8..c321e2385f 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 @@ -336,6 +336,7 @@ impl TableItemLayout for Graphic { Self::RasterGPU(list) => list.identifier(), Self::Color(list) => list.identifier(), Self::Gradient(list) => list.identifier(), + Self::Text(list) => list.identifier(), } } // Don't put a breadcrumb for Graphic @@ -350,6 +351,7 @@ impl TableItemLayout for Graphic { Self::RasterGPU(list) => list.layout_with_breadcrumb(data), Self::Color(list) => list.layout_with_breadcrumb(data), Self::Gradient(list) => list.layout_with_breadcrumb(data), + Self::Text(list) => list.layout_with_breadcrumb(data), } } } diff --git a/node-graph/libraries/core-types/src/lib.rs b/node-graph/libraries/core-types/src/lib.rs index fcebee42b6..0ad5bc23f2 100644 --- a/node-graph/libraries/core-types/src/lib.rs +++ b/node-graph/libraries/core-types/src/lib.rs @@ -25,7 +25,7 @@ pub use graphene_hash; pub use graphene_hash::CacheHash; pub use list::{ ATTR_BACKGROUND, ATTR_BLEND_MODE, ATTR_CLIP, ATTR_CLIPPING_MASK, ATTR_DIMENSIONS, ATTR_EDITOR_CLICK_TARGET, ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_EDITOR_TEXT_FRAME, ATTR_END, - ATTR_GRADIENT_TYPE, ATTR_LOCATION, ATTR_NAME, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_SPREAD_METHOD, ATTR_START, ATTR_TRANSFORM, ATTR_TYPE, + ATTR_FONT_FAMILY, ATTR_FONT_SIZE, ATTR_GRADIENT_TYPE, ATTR_LOCATION, ATTR_NAME, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_SPREAD_METHOD, ATTR_START, ATTR_TRANSFORM, ATTR_TYPE, }; pub use memo::MemoHash; pub use no_std_types::AsU32; diff --git a/node-graph/libraries/core-types/src/list.rs b/node-graph/libraries/core-types/src/list.rs index 61f54597b4..d8722f3a55 100644 --- a/node-graph/libraries/core-types/src/list.rs +++ b/node-graph/libraries/core-types/src/list.rs @@ -77,6 +77,12 @@ pub const ATTR_SPREAD_METHOD: &str = "spread_method"; /// Gradient's `GradientType` (`Linear` or `Radial`). pub const ATTR_GRADIENT_TYPE: &str = "gradient_type"; +/// Text item's font family (`String`, implicit default `"sans-serif"`). +pub const ATTR_FONT_FAMILY: &str = "font_family"; + +/// Text item's font size in document-space units (`f64`, implicit default `16.`). +pub const ATTR_FONT_SIZE: &str = "font_size"; + // ======================== // TRAIT: AnyAttributeValue // ======================== diff --git a/node-graph/libraries/core-types/src/render_complexity.rs b/node-graph/libraries/core-types/src/render_complexity.rs index fc035c720a..15578d771c 100644 --- a/node-graph/libraries/core-types/src/render_complexity.rs +++ b/node-graph/libraries/core-types/src/render_complexity.rs @@ -19,3 +19,9 @@ impl RenderComplexity for Color { 1 } } + +impl RenderComplexity for String { + fn render_complexity(&self) -> usize { + 1 + } +} diff --git a/node-graph/libraries/graphic-types/src/graphic.rs b/node-graph/libraries/graphic-types/src/graphic.rs index 0efade8880..9d55eea034 100644 --- a/node-graph/libraries/graphic-types/src/graphic.rs +++ b/node-graph/libraries/graphic-types/src/graphic.rs @@ -22,6 +22,7 @@ pub enum Graphic { RasterGPU(List>), Color(List), Gradient(List), + Text(List), } impl Default for Graphic { @@ -103,6 +104,18 @@ impl From> for Graphic { } } +// String +impl From for Graphic { + fn from(text: String) -> Self { + Graphic::Text(List::new_from_element(text)) + } +} +impl From> for Graphic { + fn from(text: List) -> Self { + Graphic::Text(text) + } +} + /// Deeply flattens a `List`, collecting only elements matching a specific variant (extracted by `extract_variant`) /// and discarding all other non-matching content. Recursion through `Graphic::Graphic` sub-`List`s composes transforms and opacity. fn flatten_graphic_list(content: List, extract_variant: fn(Graphic) -> Option>) -> List { @@ -199,6 +212,12 @@ impl TryFromGraphic for GradientStops { } } +impl TryFromGraphic for String { + fn try_from_graphic(graphic: Graphic) -> Option> { + if let Graphic::Text(t) = graphic { Some(t) } else { None } + } +} + // Local trait to convert types to List (avoids orphan rule issues) pub trait IntoGraphicList { fn into_graphic_list(self) -> List; @@ -255,6 +274,12 @@ impl IntoGraphicList for List { } } +impl IntoGraphicList for List { + fn into_graphic_list(self) -> List { + List::new_from_element(Graphic::Text(self)) + } +} + impl IntoGraphicList for DAffine2 { fn into_graphic_list(self) -> List { List::new_from_element(Graphic::default()) @@ -324,6 +349,7 @@ impl Graphic { Graphic::RasterGPU(list) => all_clipped(list), Graphic::Color(list) => all_clipped(list), Graphic::Gradient(list) => all_clipped(list), + Graphic::Text(list) => all_clipped(list), } } @@ -348,6 +374,7 @@ impl BoundingBox for Graphic { Graphic::Graphic(list) => list.bounding_box(transform, include_stroke), Graphic::Color(list) => list.bounding_box(transform, include_stroke), Graphic::Gradient(list) => list.bounding_box(transform, include_stroke), + Graphic::Text(_) => RenderBoundingBox::None, } } @@ -359,6 +386,7 @@ impl BoundingBox for Graphic { Graphic::Graphic(graphic) => graphic.thumbnail_bounding_box(transform, include_stroke), Graphic::Color(color) => color.thumbnail_bounding_box(transform, include_stroke), Graphic::Gradient(gradient) => gradient.thumbnail_bounding_box(transform, include_stroke), + Graphic::Text(_) => RenderBoundingBox::None, } } } @@ -388,6 +416,7 @@ impl RenderComplexity for Graphic { Self::RasterGPU(list) => list.render_complexity(), Self::Color(list) => list.render_complexity(), Self::Gradient(list) => list.render_complexity(), + Self::Text(list) => list.len(), } } } diff --git a/node-graph/libraries/rendering/Cargo.toml b/node-graph/libraries/rendering/Cargo.toml index 1fdfb0c839..c8e4375e3c 100644 --- a/node-graph/libraries/rendering/Cargo.toml +++ b/node-graph/libraries/rendering/Cargo.toml @@ -27,6 +27,8 @@ vector-types = { workspace = true } graphic-types = { workspace = true } vello = { workspace = true } vello_encoding = { workspace = true } +parley = { workspace = true } +skrifa = { workspace = true } # Optional workspace dependencies serde = { workspace = true, optional = true } diff --git a/node-graph/libraries/rendering/src/renderer.rs b/node-graph/libraries/rendering/src/renderer.rs index 6a6ca7c82b..246cb18982 100644 --- a/node-graph/libraries/rendering/src/renderer.rs +++ b/node-graph/libraries/rendering/src/renderer.rs @@ -13,7 +13,7 @@ use core_types::transform::Footprint; use core_types::uuid::{NodeId, generate_uuid}; use core_types::{ ATTR_BACKGROUND, ATTR_BLEND_MODE, ATTR_CLIP, ATTR_CLIPPING_MASK, ATTR_DIMENSIONS, ATTR_EDITOR_CLICK_TARGET, ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_EDITOR_TEXT_FRAME, - ATTR_GRADIENT_TYPE, ATTR_LOCATION, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_SPREAD_METHOD, ATTR_TRANSFORM, + ATTR_FONT_FAMILY, ATTR_FONT_SIZE, ATTR_GRADIENT_TYPE, ATTR_LOCATION, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_SPREAD_METHOD, ATTR_TRANSFORM, }; use dyn_any::DynAny; use glam::{DAffine2, DVec2}; @@ -24,8 +24,16 @@ use graphic_types::vector_types::subpath::Subpath; use graphic_types::vector_types::vector::click_target::{ClickTarget, FreePoint}; use graphic_types::vector_types::vector::style::{Fill, PaintOrder, RenderMode, Stroke, StrokeAlign}; use graphic_types::{Artboard, Graphic, Vector}; -use kurbo::{Affine, Cap, Join, Shape}; +use kurbo::{Affine, BezPath, Cap, Join, Shape}; use num_traits::Zero; +use parley::{FontContext, FontFamily, FontStack, LayoutContext, PositionedLayoutItem, StyleProperty}; +use skrifa::GlyphId; +use skrifa::MetadataProvider; +use skrifa::instance::{LocationRef, NormalizedCoord, Size}; +use skrifa::outline::{DrawSettings, OutlinePen}; +use skrifa::raw::FontRef as SkrifaFontRef; +use std::borrow::Cow; +use std::cell::RefCell; use std::collections::{HashMap, HashSet}; use std::fmt::Write; use std::ops::Deref; @@ -33,6 +41,27 @@ use std::sync::{Arc, LazyLock}; use vector_types::gradient::GradientSpreadMethod; use vello::*; +// Thread local storage for font bytes +thread_local! { + static RENDER_FONTS: RefCell)]>> = RefCell::new(Arc::from([])); +} + +// Thread-local parley font shaping context +thread_local! { + static FONT_CTX: RefCell<(FontContext, LayoutContext<()>)> = RefCell::new((FontContext::default(), LayoutContext::default())); +} + +// Tracks which font bytes have already been registered into FONT_CTX +thread_local! { + static REGISTERED_FONTS: RefCell> = RefCell::new(HashSet::new()); +} + +// Set the font bytes available to the renderer for the current execution. +pub fn set_render_fonts(fonts: impl IntoIterator)>) { + let slice: Arc<[(String, Arc<[u8]>)]> = fonts.into_iter().collect::>().into(); + RENDER_FONTS.with(|f| *f.borrow_mut() = slice); +} + #[derive(Clone, Copy, Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] enum MaskType { @@ -221,16 +250,26 @@ pub struct RenderParams { pub artboard_background: Option, /// Viewport zoom level (document-space scale). Used to compute constant viewport-pixel stroke widths in Outline mode. pub viewport_zoom: f64, + // All loaded font bytes extracted from the `FontCache`, keyed by CSS family name. + pub available_fonts: Arc<[(String, Arc<[u8]>)]>, } impl RenderParams { pub fn for_clipper(&self) -> Self { - Self { for_mask: true, ..*self } + Self { + for_mask: true, + available_fonts: self.available_fonts.clone(), + ..*self + } } pub fn for_alignment(&self, transform: DAffine2) -> Self { let alignment_parent_transform = Some(transform); - Self { alignment_parent_transform, ..*self } + Self { + alignment_parent_transform, + available_fonts: self.available_fonts.clone(), + ..*self + } } pub fn to_canvas(&self) -> bool { @@ -431,6 +470,7 @@ impl Render for Graphic { Graphic::RasterGPU(_) => (), Graphic::Color(list) => list.render_svg(render, render_params), Graphic::Gradient(list) => list.render_svg(render, render_params), + Graphic::Text(list) => list.render_svg(render, render_params), } } @@ -442,6 +482,7 @@ impl Render for Graphic { Graphic::RasterGPU(list) => list.render_to_vello(scene, transform, context, render_params), Graphic::Color(list) => list.render_to_vello(scene, transform, context, render_params), Graphic::Gradient(list) => list.render_to_vello(scene, transform, context, render_params), + Graphic::Text(list) => list.render_to_vello(scene, transform, context, render_params), } } @@ -490,6 +531,14 @@ impl Render for Graphic { Graphic::Gradient(list) => { metadata.upstream_footprints.insert(element_id, footprint); + // TODO: Find a way to handle more than the first item + if !list.is_empty() { + metadata.local_transforms.insert(element_id, list.attribute_cloned_or_default(ATTR_TRANSFORM, 0)); + } + } + Graphic::Text(list) => { + metadata.upstream_footprints.insert(element_id, footprint); + // TODO: Find a way to handle more than the first item if !list.is_empty() { metadata.local_transforms.insert(element_id, list.attribute_cloned_or_default(ATTR_TRANSFORM, 0)); @@ -505,6 +554,7 @@ impl Render for Graphic { Graphic::RasterGPU(list) => list.collect_metadata(metadata, footprint, element_id), Graphic::Color(list) => list.collect_metadata(metadata, footprint, element_id), Graphic::Gradient(list) => list.collect_metadata(metadata, footprint, element_id), + Graphic::Text(list) => list.collect_metadata(metadata, footprint, element_id), } } @@ -516,6 +566,7 @@ impl Render for Graphic { Graphic::RasterGPU(list) => list.add_upstream_click_targets(click_targets), Graphic::Color(list) => list.add_upstream_click_targets(click_targets), Graphic::Gradient(list) => list.add_upstream_click_targets(click_targets), + Graphic::Text(list) => list.add_upstream_click_targets(click_targets), } } @@ -527,6 +578,7 @@ impl Render for Graphic { Graphic::RasterGPU(list) => list.add_upstream_outline_targets(outlines), Graphic::Color(list) => list.add_upstream_outline_targets(outlines), Graphic::Gradient(list) => list.add_upstream_outline_targets(outlines), + Graphic::Text(list) => list.add_upstream_outline_targets(outlines), } } @@ -538,6 +590,7 @@ impl Render for Graphic { Graphic::RasterGPU(list) => list.contains_artboard(), Graphic::Color(list) => list.contains_artboard(), Graphic::Gradient(list) => list.contains_artboard(), + Graphic::Text(_) => false, } } @@ -549,6 +602,7 @@ impl Render for Graphic { Graphic::RasterGPU(_) => (), Graphic::Color(_) => (), Graphic::Gradient(_) => (), + Graphic::Text(_) => (), } } } @@ -2040,6 +2094,287 @@ impl Render for List { } } +/// Helper struct to write path data to a string +struct SvgGlyphPen { + d: String, + ox: f64, + oy: f64, +} + +impl SvgGlyphPen { + #[inline] + fn px(&self, x: f32) -> f64 { + self.ox + x as f64 + } + + #[inline] + fn py(&self, y: f32) -> f64 { + self.oy - y as f64 + } +} + +impl OutlinePen for SvgGlyphPen { + fn move_to(&mut self, x: f32, y: f32) { + write!(self.d, "M {} {} ", self.px(x), self.py(y)).ok(); + } + fn line_to(&mut self, x: f32, y: f32) { + write!(self.d, "L {} {} ", self.px(x), self.py(y)).ok(); + } + fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) { + write!(self.d, "Q {} {} {} {} ", self.px(x1), self.py(y1), self.px(x), self.py(y)).ok(); + } + fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) { + write!(self.d, "C {} {} {} {} {} {} ", self.px(x1), self.py(y1), self.px(x2), self.py(y2), self.px(x), self.py(y)).ok(); + } + fn close(&mut self) { + self.d.push_str("Z "); + } +} + +/// Helper struct to build a `kurbo::BezPath` for Vello rendering. +struct VelloPen<'a> { + path: &'a mut BezPath, + ox: f64, + oy: f64, +} + +impl OutlinePen for VelloPen<'_> { + fn move_to(&mut self, x: f32, y: f32) { + self.path.move_to((self.ox + x as f64, self.oy - y as f64)); + } + fn line_to(&mut self, x: f32, y: f32) { + self.path.line_to((self.ox + x as f64, self.oy - y as f64)); + } + fn quad_to(&mut self, cx: f32, cy: f32, x: f32, y: f32) { + self.path.quad_to((self.ox + cx as f64, self.oy - cy as f64), (self.ox + x as f64, self.oy - y as f64)); + } + fn curve_to(&mut self, cx1: f32, cy1: f32, cx2: f32, cy2: f32, x: f32, y: f32) { + self.path.curve_to( + (self.ox + cx1 as f64, self.oy - cy1 as f64), + (self.ox + cx2 as f64, self.oy - cy2 as f64), + (self.ox + x as f64, self.oy - y as f64), + ); + } + fn close(&mut self) { + self.path.close_path(); + } +} + +/// Registers all fonts from `RENDER_FONTS` that aren't yet in `FONT_CTX`. +fn ensure_fonts_registered(font_ctx: &mut parley::FontContext) { + REGISTERED_FONTS.with(|reg| { + let mut reg = reg.borrow_mut(); + RENDER_FONTS.with(|rf| { + for (_, bytes) in rf.borrow().iter() { + let key = bytes.as_ptr() as usize; + if reg.insert(key) { + struct ArcBytes(std::sync::Arc<[u8]>); + impl AsRef<[u8]> for ArcBytes { + fn as_ref(&self) -> &[u8] { + &self.0 + } + } + let font_data: std::sync::Arc + Send + Sync> = std::sync::Arc::new(ArcBytes(bytes.clone())); + font_ctx.collection.register_fonts(parley::fontique::Blob::new(font_data), None); + } + } + }); + }); +} + +const DEFAULT_FONT_FAMILY: &str = "Lato"; +const DEFAULT_FONT_SIZE: f64 = 16.; + +impl Render for List { + fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) { + for index in 0..self.len() { + let Some(text) = self.element(index) else { continue }; + if text.is_empty() { + continue; + } + + let transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index); + let opacity_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY, index, 1.); + let opacity_fill_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY_FILL, index, 1.); + let blend_mode_attr: BlendMode = self.attribute_cloned_or_default(ATTR_BLEND_MODE, index); + let font_family: String = self.attribute_cloned_or(ATTR_FONT_FAMILY, index, DEFAULT_FONT_FAMILY.to_string()); + let font_size: f64 = self.attribute_cloned_or(ATTR_FONT_SIZE, index, DEFAULT_FONT_SIZE); + let opacity = (opacity_attr * if render_params.for_mask { 1. } else { opacity_fill_attr }) as f32; + + let mut glyph_paths: Vec = Vec::new(); + + FONT_CTX.with(|ctx| { + let Ok(mut ctx) = ctx.try_borrow_mut() else { return }; + let (font_ctx, layout_ctx) = &mut *ctx; + + ensure_fonts_registered(font_ctx); + + let mut builder = layout_ctx.ranged_builder(font_ctx, text, 1.0, false); + builder.push_default(StyleProperty::FontSize(font_size as f32)); + builder.push_default(StyleProperty::FontStack(FontStack::Single(FontFamily::Named(Cow::Borrowed(font_family.as_str()))))); + let mut layout = builder.build(text); + layout.break_all_lines(None); + + for line in layout.lines() { + for item in line.items() { + let PositionedLayoutItem::GlyphRun(glyph_run) = item else { continue }; + + let mut run_x = glyph_run.offset(); + let run_y = glyph_run.baseline(); + let run = glyph_run.run(); + let font = run.font(); + let font_size_pts = run.font_size(); + let normalized_coords: Vec = run.normalized_coords().iter().map(|c| NormalizedCoord::from_bits(*c)).collect(); + + let font_data = font.data.as_ref(); + let Ok(font_ref) = SkrifaFontRef::from_index(font_data, font.index) else { continue }; + let outlines = font_ref.outline_glyphs(); + + for glyph in glyph_run.glyphs() { + let ox = (run_x + glyph.x) as f64; + let oy = (run_y - glyph.y) as f64; + run_x += glyph.advance; + + let glyph_id = GlyphId::from(glyph.id); + let Some(outline) = outlines.get(glyph_id) else { continue }; + let settings = DrawSettings::unhinted(Size::new(font_size_pts), LocationRef::new(&normalized_coords)); + let mut pen = SvgGlyphPen { d: String::new(), ox, oy }; + if outline.draw(settings, &mut pen).is_ok() && !pen.d.is_empty() { + glyph_paths.push(pen.d); + } + } + } + } + }); + + if glyph_paths.is_empty() { + continue; + } + + // Wrap all glyph elements in a with the item's transform/opacity/blend-mode. + render.parent_tag( + "g", + |attributes| { + let matrix = format_transform_matrix(transform); + if !matrix.is_empty() { + attributes.push("transform", matrix); + } + if opacity < 1. { + attributes.push("opacity", opacity.to_string()); + } + if blend_mode_attr != BlendMode::default() { + attributes.push("style", blend_mode_attr.render()); + } + }, + |render| { + for path_d in glyph_paths { + render.leaf_tag("path", |attributes| { + attributes.push("d", path_d); + if let RenderMode::Outline = render_params.render_mode { + attributes.push("fill", "none"); + attributes.push("stroke", "black"); + attributes.push("stroke-width", "1"); + } else { + attributes.push("fill", "black"); + attributes.push("fill-rule", "nonzero"); + } + }); + } + }, + ); + } + } + + fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, _context: &mut RenderContext, render_params: &RenderParams) { + for index in 0..self.len() { + let Some(text) = self.element(index) else { continue }; + if text.is_empty() { + continue; + } + + let item_transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index); + let font_family: String = self.attribute_cloned_or(ATTR_FONT_FAMILY, index, DEFAULT_FONT_FAMILY.to_string()); + let font_size: f64 = self.attribute_cloned_or(ATTR_FONT_SIZE, index, DEFAULT_FONT_SIZE); + let opacity_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY, index, 1.); + let opacity_fill_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY_FILL, index, 1.); + let opacity = (opacity_attr * if render_params.for_mask { 1. } else { opacity_fill_attr }) as f32; + + let affine = Affine::new((transform * item_transform).to_cols_array()); + + FONT_CTX.with(|ctx| { + let Ok(mut ctx) = ctx.try_borrow_mut() else { return }; + let (font_ctx, layout_ctx) = &mut *ctx; + + ensure_fonts_registered(font_ctx); + + let mut builder = layout_ctx.ranged_builder(font_ctx, text, 1.0, false); + builder.push_default(StyleProperty::FontSize(font_size as f32)); + builder.push_default(StyleProperty::FontStack(FontStack::Single(FontFamily::Named(Cow::Borrowed(font_family.as_str()))))); + let mut layout = builder.build(text); + layout.break_all_lines(None); + + for line in layout.lines() { + for item in line.items() { + let PositionedLayoutItem::GlyphRun(glyph_run) = item else { continue }; + + let mut run_x = glyph_run.offset(); + let run_y = glyph_run.baseline(); + let run = glyph_run.run(); + let font = run.font(); + let font_size_pts = run.font_size(); + let normalized_coords: Vec = run.normalized_coords().iter().map(|c| NormalizedCoord::from_bits(*c)).collect(); + + let font_data = font.data.as_ref(); + let Ok(font_ref) = SkrifaFontRef::from_index(font_data, font.index) else { continue }; + let outlines = font_ref.outline_glyphs(); + + for glyph in glyph_run.glyphs() { + let ox = (run_x + glyph.x) as f64; + let oy = (run_y - glyph.y) as f64; + run_x += glyph.advance; + + let glyph_id = GlyphId::from(glyph.id); + let Some(outline) = outlines.get(glyph_id) else { continue }; + let settings = DrawSettings::unhinted(Size::new(font_size_pts), LocationRef::new(&normalized_coords)); + + let mut bez_path = BezPath::new(); + let mut pen = VelloPen { path: &mut bez_path, ox, oy }; + if outline.draw(settings, &mut pen).is_ok() && !bez_path.elements().is_empty() { + if let RenderMode::Outline = render_params.render_mode { + let (outline_stroke, outline_color) = get_outline_styles(render_params); + scene.stroke(&outline_stroke, affine, outline_color, None, &bez_path); + } else { + let color = peniko::Color::new([0_f32, 0., 0., opacity]); + scene.fill(peniko::Fill::NonZero, affine, color, None, &bez_path); + } + } + } + } + } + }); + } + } + fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, element_id: Option) { + let Some(element_id) = element_id else { return }; + metadata.upstream_footprints.insert(element_id, footprint); + if !self.is_empty() { + metadata.local_transforms.insert(element_id, self.attribute_cloned_or_default(ATTR_TRANSFORM, 0)); + } + } + + fn add_upstream_click_targets(&self, click_targets: &mut Vec) { + for index in 0..self.len() { + let font_size: f64 = self.attribute_cloned_or(ATTR_FONT_SIZE, index, DEFAULT_FONT_SIZE); + let transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index); + // TODO: temporary stepping stone until the Data Trees (Issue #3779) refactor is complete + let subpath = Subpath::new_rectangle(DVec2::ZERO, DVec2::new(font_size * 6., font_size)); + let mut target = ClickTarget::new_with_subpath(subpath, 0.); + target.apply_transform(transform); + click_targets.push(target); + } + } +} + #[derive(Debug, Clone, PartialEq, Eq)] pub enum SvgSegment { Slice(&'static str), diff --git a/node-graph/nodes/gstd/src/render_node.rs b/node-graph/nodes/gstd/src/render_node.rs index 121131a062..9afded17d9 100644 --- a/node-graph/nodes/gstd/src/render_node.rs +++ b/node-graph/nodes/gstd/src/render_node.rs @@ -39,6 +39,7 @@ async fn render_intermediate<'a: 'n, T: 'static + Render + WasmNotSend + Send + Context -> List>, Context -> List, Context -> List, + Context -> List, )] data: impl Node, Output = T>, ) -> RenderIntermediate { diff --git a/node-graph/nodes/path-bool/src/lib.rs b/node-graph/nodes/path-bool/src/lib.rs index 14c0025d52..9fffc0a704 100644 --- a/node-graph/nodes/path-bool/src/lib.rs +++ b/node-graph/nodes/path-bool/src/lib.rs @@ -278,6 +278,7 @@ fn flatten_vector(graphic_list: &List) -> List { Item::from_parts(element, attributes) }) .collect::>(), + Graphic::Text(_) => Vec::new(), } }) .collect() From e8755595413aa8fb51638913a7b9db42f1cd392c Mon Sep 17 00:00:00 2001 From: Annonnymmousss Date: Wed, 13 May 2026 03:56:52 +0530 Subject: [PATCH 02/12] chore: code review --- .../libraries/graphic-types/src/graphic.rs | 4 +-- .../libraries/rendering/src/renderer.rs | 27 ++++++++++--------- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/node-graph/libraries/graphic-types/src/graphic.rs b/node-graph/libraries/graphic-types/src/graphic.rs index 9d55eea034..cd14dc682b 100644 --- a/node-graph/libraries/graphic-types/src/graphic.rs +++ b/node-graph/libraries/graphic-types/src/graphic.rs @@ -374,7 +374,7 @@ impl BoundingBox for Graphic { Graphic::Graphic(list) => list.bounding_box(transform, include_stroke), Graphic::Color(list) => list.bounding_box(transform, include_stroke), Graphic::Gradient(list) => list.bounding_box(transform, include_stroke), - Graphic::Text(_) => RenderBoundingBox::None, + Graphic::Text(_) => RenderBoundingBox::Infinite, } } @@ -386,7 +386,7 @@ impl BoundingBox for Graphic { Graphic::Graphic(graphic) => graphic.thumbnail_bounding_box(transform, include_stroke), Graphic::Color(color) => color.thumbnail_bounding_box(transform, include_stroke), Graphic::Gradient(gradient) => gradient.thumbnail_bounding_box(transform, include_stroke), - Graphic::Text(_) => RenderBoundingBox::None, + Graphic::Text(_) => RenderBoundingBox::Infinite, } } } diff --git a/node-graph/libraries/rendering/src/renderer.rs b/node-graph/libraries/rendering/src/renderer.rs index 246cb18982..126184240e 100644 --- a/node-graph/libraries/rendering/src/renderer.rs +++ b/node-graph/libraries/rendering/src/renderer.rs @@ -250,24 +250,16 @@ pub struct RenderParams { pub artboard_background: Option, /// Viewport zoom level (document-space scale). Used to compute constant viewport-pixel stroke widths in Outline mode. pub viewport_zoom: f64, - // All loaded font bytes extracted from the `FontCache`, keyed by CSS family name. - pub available_fonts: Arc<[(String, Arc<[u8]>)]>, } impl RenderParams { pub fn for_clipper(&self) -> Self { - Self { - for_mask: true, - available_fonts: self.available_fonts.clone(), - ..*self - } + Self { for_mask: true, ..*self } } pub fn for_alignment(&self, transform: DAffine2) -> Self { - let alignment_parent_transform = Some(transform); Self { - alignment_parent_transform, - available_fonts: self.available_fonts.clone(), + alignment_parent_transform: Some(transform), ..*self } } @@ -2295,12 +2287,20 @@ impl Render for List { let item_transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index); let font_family: String = self.attribute_cloned_or(ATTR_FONT_FAMILY, index, DEFAULT_FONT_FAMILY.to_string()); let font_size: f64 = self.attribute_cloned_or(ATTR_FONT_SIZE, index, DEFAULT_FONT_SIZE); + let blend_mode_attr: BlendMode = self.attribute_cloned_or_default(ATTR_BLEND_MODE, index); let opacity_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY, index, 1.); let opacity_fill_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY_FILL, index, 1.); let opacity = (opacity_attr * if render_params.for_mask { 1. } else { opacity_fill_attr }) as f32; let affine = Affine::new((transform * item_transform).to_cols_array()); + let needs_layer = opacity < 1. || blend_mode_attr != BlendMode::default(); + if needs_layer { + let blending = peniko::BlendMode::new(blend_mode_attr.to_peniko(), peniko::Compose::SrcOver); + let inf_rect = kurbo::Rect::from_origin_size(kurbo::Point::ZERO, kurbo::Size::new(f64::INFINITY, f64::INFINITY)); + scene.push_layer(peniko::Fill::NonZero, blending, opacity, kurbo::Affine::IDENTITY, &inf_rect); + } + FONT_CTX.with(|ctx| { let Ok(mut ctx) = ctx.try_borrow_mut() else { return }; let (font_ctx, layout_ctx) = &mut *ctx; @@ -2344,14 +2344,17 @@ impl Render for List { let (outline_stroke, outline_color) = get_outline_styles(render_params); scene.stroke(&outline_stroke, affine, outline_color, None, &bez_path); } else { - let color = peniko::Color::new([0_f32, 0., 0., opacity]); - scene.fill(peniko::Fill::NonZero, affine, color, None, &bez_path); + scene.fill(peniko::Fill::NonZero, affine, peniko::Color::BLACK, None, &bez_path); } } } } } }); + + if needs_layer { + scene.pop_layer(); + } } } fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, element_id: Option) { From 933c38b097aeedcac676ce9960f064ed23be4655 Mon Sep 17 00:00:00 2001 From: Annonnymmousss Date: Fri, 15 May 2026 03:22:45 +0530 Subject: [PATCH 03/12] chore: change the hardcoded layout bounds to parley's --- .../libraries/rendering/src/renderer.rs | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/node-graph/libraries/rendering/src/renderer.rs b/node-graph/libraries/rendering/src/renderer.rs index 126184240e..30ecceb271 100644 --- a/node-graph/libraries/rendering/src/renderer.rs +++ b/node-graph/libraries/rendering/src/renderer.rs @@ -2367,10 +2367,27 @@ impl Render for List { fn add_upstream_click_targets(&self, click_targets: &mut Vec) { for index in 0..self.len() { + let Some(text) = self.element(index) else { continue }; let font_size: f64 = self.attribute_cloned_or(ATTR_FONT_SIZE, index, DEFAULT_FONT_SIZE); + let font_family: String = self.attribute_cloned_or(ATTR_FONT_FAMILY, index, DEFAULT_FONT_FAMILY.to_string()); let transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index); - // TODO: temporary stepping stone until the Data Trees (Issue #3779) refactor is complete - let subpath = Subpath::new_rectangle(DVec2::ZERO, DVec2::new(font_size * 6., font_size)); + + // Falls back to a single-em square if fonts are not yet registered. + let (width, height) = FONT_CTX + .with(|ctx| { + let Ok(mut ctx) = ctx.try_borrow_mut() else { return None }; + let (font_ctx, layout_ctx) = &mut *ctx; + ensure_fonts_registered(font_ctx); + let mut builder = layout_ctx.ranged_builder(font_ctx, text, 1.0, false); + builder.push_default(StyleProperty::FontSize(font_size as f32)); + builder.push_default(StyleProperty::FontStack(FontStack::Single(FontFamily::Named(Cow::Borrowed(font_family.as_str()))))); + let mut layout = builder.build(text); + layout.break_all_lines(None); + Some((layout.width() as f64, layout.height() as f64)) + }) + .unwrap_or((font_size, font_size)); + + let subpath = Subpath::new_rectangle(DVec2::ZERO, DVec2::new(width, height)); let mut target = ClickTarget::new_with_subpath(subpath, 0.); target.apply_transform(transform); click_targets.push(target); From 9302092ac9a7c329f9b7e054f1600ca4bc05f819 Mon Sep 17 00:00:00 2001 From: Annonnymmousss Date: Sat, 16 May 2026 13:12:55 +0530 Subject: [PATCH 04/12] chore: code review --- node-graph/libraries/core-types/src/list.rs | 2 +- .../libraries/graphic-types/src/graphic.rs | 7 ++- .../libraries/rendering/src/renderer.rs | 44 ++++++++++++------- 3 files changed, 34 insertions(+), 19 deletions(-) diff --git a/node-graph/libraries/core-types/src/list.rs b/node-graph/libraries/core-types/src/list.rs index d8722f3a55..4a3346a7ee 100644 --- a/node-graph/libraries/core-types/src/list.rs +++ b/node-graph/libraries/core-types/src/list.rs @@ -77,7 +77,7 @@ pub const ATTR_SPREAD_METHOD: &str = "spread_method"; /// Gradient's `GradientType` (`Linear` or `Radial`). pub const ATTR_GRADIENT_TYPE: &str = "gradient_type"; -/// Text item's font family (`String`, implicit default `"sans-serif"`). +/// Text item's font family (`String`, implicit default `"Lato"`). pub const ATTR_FONT_FAMILY: &str = "font_family"; /// Text item's font size in document-space units (`f64`, implicit default `16.`). diff --git a/node-graph/libraries/graphic-types/src/graphic.rs b/node-graph/libraries/graphic-types/src/graphic.rs index cd14dc682b..93869ba580 100644 --- a/node-graph/libraries/graphic-types/src/graphic.rs +++ b/node-graph/libraries/graphic-types/src/graphic.rs @@ -276,7 +276,12 @@ impl IntoGraphicList for List { impl IntoGraphicList for List { fn into_graphic_list(self) -> List { - List::new_from_element(Graphic::Text(self)) + let layer_path: List = self.attribute_cloned_or_default(ATTR_EDITOR_LAYER_PATH, 0); + let mut graphic_list = List::new_from_element(Graphic::Text(self)); + if !layer_path.is_empty() { + graphic_list.set_attribute(ATTR_EDITOR_LAYER_PATH, 0, layer_path); + } + graphic_list } } diff --git a/node-graph/libraries/rendering/src/renderer.rs b/node-graph/libraries/rendering/src/renderer.rs index 30ecceb271..e93da4785b 100644 --- a/node-graph/libraries/rendering/src/renderer.rs +++ b/node-graph/libraries/rendering/src/renderer.rs @@ -36,6 +36,7 @@ use std::borrow::Cow; use std::cell::RefCell; use std::collections::{HashMap, HashSet}; use std::fmt::Write; +use std::hash::{Hash, Hasher}; use std::ops::Deref; use std::sync::{Arc, LazyLock}; use vector_types::gradient::GradientSpreadMethod; @@ -43,7 +44,7 @@ use vello::*; // Thread local storage for font bytes thread_local! { - static RENDER_FONTS: RefCell)]>> = RefCell::new(Arc::from([])); + static RENDER_FONTS: RefCell)]>> = RefCell::new(Arc::from([])); } // Thread-local parley font shaping context @@ -53,12 +54,20 @@ thread_local! { // Tracks which font bytes have already been registered into FONT_CTX thread_local! { - static REGISTERED_FONTS: RefCell> = RefCell::new(HashSet::new()); + static REGISTERED_FONTS: RefCell> = RefCell::new(HashSet::new()); } // Set the font bytes available to the renderer for the current execution. pub fn set_render_fonts(fonts: impl IntoIterator)>) { - let slice: Arc<[(String, Arc<[u8]>)]> = fonts.into_iter().collect::>().into(); + let slice: Arc<[(String, u64, Arc<[u8]>)]> = fonts + .into_iter() + .map(|(name, bytes)| { + let mut hasher = std::collections::hash_map::DefaultHasher::new(); + bytes.hash(&mut hasher); + (name, hasher.finish(), bytes) + }) + .collect::>() + .into(); RENDER_FONTS.with(|f| *f.borrow_mut() = slice); } @@ -2157,9 +2166,8 @@ fn ensure_fonts_registered(font_ctx: &mut parley::FontContext) { REGISTERED_FONTS.with(|reg| { let mut reg = reg.borrow_mut(); RENDER_FONTS.with(|rf| { - for (_, bytes) in rf.borrow().iter() { - let key = bytes.as_ptr() as usize; - if reg.insert(key) { + for (_, hash, bytes) in rf.borrow().iter() { + if reg.insert(*hash) { struct ArcBytes(std::sync::Arc<[u8]>); impl AsRef<[u8]> for ArcBytes { fn as_ref(&self) -> &[u8] { @@ -2294,13 +2302,6 @@ impl Render for List { let affine = Affine::new((transform * item_transform).to_cols_array()); - let needs_layer = opacity < 1. || blend_mode_attr != BlendMode::default(); - if needs_layer { - let blending = peniko::BlendMode::new(blend_mode_attr.to_peniko(), peniko::Compose::SrcOver); - let inf_rect = kurbo::Rect::from_origin_size(kurbo::Point::ZERO, kurbo::Size::new(f64::INFINITY, f64::INFINITY)); - scene.push_layer(peniko::Fill::NonZero, blending, opacity, kurbo::Affine::IDENTITY, &inf_rect); - } - FONT_CTX.with(|ctx| { let Ok(mut ctx) = ctx.try_borrow_mut() else { return }; let (font_ctx, layout_ctx) = &mut *ctx; @@ -2313,6 +2314,15 @@ impl Render for List { let mut layout = builder.build(text); layout.break_all_lines(None); + let needs_layer = opacity < 1. || blend_mode_attr != BlendMode::default(); + if needs_layer { + let blending = peniko::BlendMode::new(blend_mode_attr.to_peniko(), peniko::Compose::SrcOver); + let padding = font_size; + let bounds = kurbo::Rect::new(-padding, -padding, layout.full_width() as f64 + padding, layout.height() as f64 + padding); + let transformed_bounds = affine.transform_rect_bbox(bounds); + scene.push_layer(peniko::Fill::NonZero, blending, opacity, kurbo::Affine::IDENTITY, &transformed_bounds); + } + for line in layout.lines() { for item in line.items() { let PositionedLayoutItem::GlyphRun(glyph_run) = item else { continue }; @@ -2350,11 +2360,11 @@ impl Render for List { } } } - }); - if needs_layer { - scene.pop_layer(); - } + if needs_layer { + scene.pop_layer(); + } + }); } } fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, element_id: Option) { From 2adea19d326599be1dfdbb34d91075dcbbbd8c9d Mon Sep 17 00:00:00 2001 From: Annonnymmousss Date: Mon, 25 May 2026 03:30:30 +0530 Subject: [PATCH 05/12] feat: Split text node to text_layer and text_to_vector node --- .../graph_modification_utils.rs | 54 ++++ node-graph/libraries/core-types/src/lib.rs | 3 +- node-graph/libraries/core-types/src/list.rs | 24 ++ .../libraries/rendering/src/renderer.rs | 259 +++++++++++++++--- node-graph/libraries/resources/src/lib.rs | 6 + node-graph/nodes/gstd/src/text.rs | 142 +++++++++- 6 files changed, 452 insertions(+), 36 deletions(-) diff --git a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs index 56ddf80297..1a38b87213 100644 --- a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs +++ b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs @@ -427,6 +427,10 @@ pub fn get_text_id(layer: LayerNodeIdentifier, network_interface: &NodeNetworkIn NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name(&DefinitionIdentifier::ProtoNode(graphene_std::text::text::IDENTIFIER)) } +pub fn get_text_layer_id(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option { + NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name(&DefinitionIdentifier::ProtoNode(graphene_std::text::text_layer::IDENTIFIER)) +} + pub fn get_grid_id(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option { NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name(&DefinitionIdentifier::ProtoNode(graphene_std::vector::generator_nodes::grid::IDENTIFIER)) } @@ -490,6 +494,56 @@ pub fn get_text<'a>( Some((text, font, typesetting, per_glyph_items)) } +/// Gets properties from the Text Layer node +pub fn get_text_layer(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<(&String, &Font, TypesettingConfig)> { + let inputs = NodeGraphLayer::new(layer, network_interface).find_node_inputs(&DefinitionIdentifier::ProtoNode(graphene_std::text::text_layer::IDENTIFIER))?; + + let Some(TaggedValue::String(text)) = &inputs[graphene_std::text::text_layer::TextInput::INDEX].as_value() else { + return None; + }; + let Some(TaggedValue::Font(font)) = &inputs[graphene_std::text::text_layer::FontInput::INDEX].as_value() else { + return None; + }; + let Some(&TaggedValue::F64(font_size)) = inputs[graphene_std::text::text_layer::SizeInput::INDEX].as_value() else { + return None; + }; + let Some(&TaggedValue::F64(line_height_ratio)) = inputs[graphene_std::text::text_layer::LineHeightInput::INDEX].as_value() else { + return None; + }; + let Some(&TaggedValue::F64(character_spacing)) = inputs[graphene_std::text::text_layer::CharacterSpacingInput::INDEX].as_value() else { + return None; + }; + let Some(&TaggedValue::Bool(has_max_width)) = inputs[graphene_std::text::text_layer::HasMaxWidthInput::INDEX].as_value() else { + return None; + }; + let Some(&TaggedValue::F64(max_width)) = inputs[graphene_std::text::text_layer::MaxWidthInput::INDEX].as_value() else { + return None; + }; + let Some(&TaggedValue::Bool(has_max_height)) = inputs[graphene_std::text::text_layer::HasMaxHeightInput::INDEX].as_value() else { + return None; + }; + let Some(&TaggedValue::F64(max_height)) = inputs[graphene_std::text::text_layer::MaxHeightInput::INDEX].as_value() else { + return None; + }; + let Some(&TaggedValue::F64(tilt)) = inputs[graphene_std::text::text_layer::TiltInput::INDEX].as_value() else { + return None; + }; + let Some(&TaggedValue::TextAlign(align)) = inputs[graphene_std::text::text_layer::AlignInput::INDEX].as_value() else { + return None; + }; + + let typesetting = TypesettingConfig { + font_size, + line_height_ratio, + max_width: has_max_width.then_some(max_width), + max_height: has_max_height.then_some(max_height), + character_spacing, + tilt, + align, + }; + Some((text, font, typesetting)) +} + pub fn get_stroke_width(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option { let weight_node_input_index = graphene_std::vector::stroke::WeightInput::INDEX; if let TaggedValue::F64(width) = NodeGraphLayer::new(layer, network_interface).find_input(&DefinitionIdentifier::ProtoNode(graphene_std::vector::stroke::IDENTIFIER), weight_node_input_index)? { diff --git a/node-graph/libraries/core-types/src/lib.rs b/node-graph/libraries/core-types/src/lib.rs index 0ad5bc23f2..f00e92839c 100644 --- a/node-graph/libraries/core-types/src/lib.rs +++ b/node-graph/libraries/core-types/src/lib.rs @@ -25,7 +25,8 @@ pub use graphene_hash; pub use graphene_hash::CacheHash; pub use list::{ ATTR_BACKGROUND, ATTR_BLEND_MODE, ATTR_CLIP, ATTR_CLIPPING_MASK, ATTR_DIMENSIONS, ATTR_EDITOR_CLICK_TARGET, ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_EDITOR_TEXT_FRAME, ATTR_END, - ATTR_FONT_FAMILY, ATTR_FONT_SIZE, ATTR_GRADIENT_TYPE, ATTR_LOCATION, ATTR_NAME, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_SPREAD_METHOD, ATTR_START, ATTR_TRANSFORM, ATTR_TYPE, + ATTR_FONT_FAMILY, ATTR_FONT_SIZE, ATTR_FONT_STYLE, ATTR_GRADIENT_TYPE, ATTR_LOCATION, ATTR_NAME, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_SPREAD_METHOD, ATTR_START, ATTR_TEXT_ALIGN, + ATTR_TEXT_CHARACTER_SPACING, ATTR_TEXT_FONT, ATTR_TEXT_LINE_HEIGHT, ATTR_TEXT_MAX_HEIGHT, ATTR_TEXT_MAX_WIDTH, ATTR_TEXT_TILT, ATTR_TRANSFORM, ATTR_TYPE, }; pub use memo::MemoHash; pub use no_std_types::AsU32; diff --git a/node-graph/libraries/core-types/src/list.rs b/node-graph/libraries/core-types/src/list.rs index 4a3346a7ee..112f394034 100644 --- a/node-graph/libraries/core-types/src/list.rs +++ b/node-graph/libraries/core-types/src/list.rs @@ -80,9 +80,33 @@ pub const ATTR_GRADIENT_TYPE: &str = "gradient_type"; /// Text item's font family (`String`, implicit default `"Lato"`). pub const ATTR_FONT_FAMILY: &str = "font_family"; +/// Text item's font style (`String`, implicit default `"Regular"`). +pub const ATTR_FONT_STYLE: &str = "font_style"; + /// Text item's font size in document-space units (`f64`, implicit default `16.`). pub const ATTR_FONT_SIZE: &str = "font_size"; +/// Text item's font `Resource`. Only set by `text_layer`; used by `text_to_vector` and the renderer to reconstruct exact glyph paths. +pub const ATTR_TEXT_FONT: &str = "text_font"; + +/// Text item's line height ratio relative to the font size (`f64`, implicit default `1.2`). Only stored when it deviates from the default. +pub const ATTR_TEXT_LINE_HEIGHT: &str = "text_line_height"; + +/// Text item's extra inter-character spacing in document-space units (`f64`, implicit default `0.0`). Only stored when non-zero. +pub const ATTR_TEXT_CHARACTER_SPACING: &str = "text_character_spacing"; + +/// Text item's optional max line-wrap width (`Option`). Absent = no limit; present = wrap at that width. +pub const ATTR_TEXT_MAX_WIDTH: &str = "text_max_width"; + +/// Text item's optional max height cutoff (`Option`). Absent = no limit; lines whose baseline exceeds this value are not drawn. +pub const ATTR_TEXT_MAX_HEIGHT: &str = "text_max_height"; + +/// Text item's faux-italic tilt angle in degrees (`f64`, implicit default `0.0`). Only stored when non-zero. +pub const ATTR_TEXT_TILT: &str = "text_tilt"; + +/// Text item's horizontal alignment. Only stored when it deviates from the default. +pub const ATTR_TEXT_ALIGN: &str = "text_align"; + // ======================== // TRAIT: AnyAttributeValue // ======================== diff --git a/node-graph/libraries/rendering/src/renderer.rs b/node-graph/libraries/rendering/src/renderer.rs index e93da4785b..dc073bef7a 100644 --- a/node-graph/libraries/rendering/src/renderer.rs +++ b/node-graph/libraries/rendering/src/renderer.rs @@ -13,7 +13,8 @@ use core_types::transform::Footprint; use core_types::uuid::{NodeId, generate_uuid}; use core_types::{ ATTR_BACKGROUND, ATTR_BLEND_MODE, ATTR_CLIP, ATTR_CLIPPING_MASK, ATTR_DIMENSIONS, ATTR_EDITOR_CLICK_TARGET, ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_EDITOR_TEXT_FRAME, - ATTR_FONT_FAMILY, ATTR_FONT_SIZE, ATTR_GRADIENT_TYPE, ATTR_LOCATION, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_SPREAD_METHOD, ATTR_TRANSFORM, + ATTR_FONT_FAMILY, ATTR_FONT_SIZE, ATTR_FONT_STYLE, ATTR_GRADIENT_TYPE, ATTR_LOCATION, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_SPREAD_METHOD, ATTR_TEXT_ALIGN, ATTR_TEXT_CHARACTER_SPACING, + ATTR_TEXT_LINE_HEIGHT, ATTR_TEXT_MAX_HEIGHT, ATTR_TEXT_MAX_WIDTH, ATTR_TEXT_TILT, ATTR_TRANSFORM, }; use dyn_any::DynAny; use glam::{DAffine2, DVec2}; @@ -26,7 +27,7 @@ use graphic_types::vector_types::vector::style::{Fill, PaintOrder, RenderMode, S use graphic_types::{Artboard, Graphic, Vector}; use kurbo::{Affine, BezPath, Cap, Join, Shape}; use num_traits::Zero; -use parley::{FontContext, FontFamily, FontStack, LayoutContext, PositionedLayoutItem, StyleProperty}; +use parley::{AlignmentOptions, FontContext, FontFamily, FontStack, LayoutContext, LineHeight, PositionedLayoutItem, StyleProperty}; use skrifa::GlyphId; use skrifa::MetadataProvider; use skrifa::instance::{LocationRef, NormalizedCoord, Size}; @@ -44,7 +45,7 @@ use vello::*; // Thread local storage for font bytes thread_local! { - static RENDER_FONTS: RefCell)]>> = RefCell::new(Arc::from([])); + static RENDER_FONTS: RefCell)]>> = RefCell::new(Arc::from([])); } // Thread-local parley font shaping context @@ -57,14 +58,19 @@ thread_local! { static REGISTERED_FONTS: RefCell> = RefCell::new(HashSet::new()); } +// Caches the first FontInfo (weight/style/width) for each (family, style) pair after registration +thread_local! { + static FONT_INFO_CACHE: RefCell> = RefCell::new(HashMap::new()); +} + // Set the font bytes available to the renderer for the current execution. -pub fn set_render_fonts(fonts: impl IntoIterator)>) { - let slice: Arc<[(String, u64, Arc<[u8]>)]> = fonts +pub fn set_render_fonts(fonts: impl IntoIterator)>) { + let slice: Arc<[(String, String, u64, Arc<[u8]>)]> = fonts .into_iter() - .map(|(name, bytes)| { + .map(|(family, style, bytes)| { let mut hasher = std::collections::hash_map::DefaultHasher::new(); bytes.hash(&mut hasher); - (name, hasher.finish(), bytes) + (family, style, hasher.finish(), bytes) }) .collect::>() .into(); @@ -2100,12 +2106,13 @@ struct SvgGlyphPen { d: String, ox: f64, oy: f64, + tilt_tan: f64, } impl SvgGlyphPen { #[inline] - fn px(&self, x: f32) -> f64 { - self.ox + x as f64 + fn px(&self, x: f32, y: f32) -> f64 { + self.ox + x as f64 + (y as f64 * self.tilt_tan) } #[inline] @@ -2116,16 +2123,16 @@ impl SvgGlyphPen { impl OutlinePen for SvgGlyphPen { fn move_to(&mut self, x: f32, y: f32) { - write!(self.d, "M {} {} ", self.px(x), self.py(y)).ok(); + write!(self.d, "M {} {} ", self.px(x, y), self.py(y)).ok(); } fn line_to(&mut self, x: f32, y: f32) { - write!(self.d, "L {} {} ", self.px(x), self.py(y)).ok(); + write!(self.d, "L {} {} ", self.px(x, y), self.py(y)).ok(); } fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) { - write!(self.d, "Q {} {} {} {} ", self.px(x1), self.py(y1), self.px(x), self.py(y)).ok(); + write!(self.d, "Q {} {} {} {} ", self.px(x1, y1), self.py(y1), self.px(x, y), self.py(y)).ok(); } fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) { - write!(self.d, "C {} {} {} {} {} {} ", self.px(x1), self.py(y1), self.px(x2), self.py(y2), self.px(x), self.py(y)).ok(); + write!(self.d, "C {} {} {} {} {} {} ", self.px(x1, y1), self.py(y1), self.px(x2, y2), self.py(y2), self.px(x, y), self.py(y)).ok(); } fn close(&mut self) { self.d.push_str("Z "); @@ -2137,24 +2144,33 @@ struct VelloPen<'a> { path: &'a mut BezPath, ox: f64, oy: f64, + tilt_tan: f64, +} + +impl VelloPen<'_> { + #[inline] + fn px(&self, x: f32, y: f32) -> f64 { + self.ox + x as f64 + (y as f64 * self.tilt_tan) + } + + #[inline] + fn py(&self, y: f32) -> f64 { + self.oy - y as f64 + } } impl OutlinePen for VelloPen<'_> { fn move_to(&mut self, x: f32, y: f32) { - self.path.move_to((self.ox + x as f64, self.oy - y as f64)); + self.path.move_to((self.px(x, y), self.py(y))); } fn line_to(&mut self, x: f32, y: f32) { - self.path.line_to((self.ox + x as f64, self.oy - y as f64)); + self.path.line_to((self.px(x, y), self.py(y))); } fn quad_to(&mut self, cx: f32, cy: f32, x: f32, y: f32) { - self.path.quad_to((self.ox + cx as f64, self.oy - cy as f64), (self.ox + x as f64, self.oy - y as f64)); + self.path.quad_to((self.px(cx, cy), self.py(cy)), (self.px(x, y), self.py(y))); } fn curve_to(&mut self, cx1: f32, cy1: f32, cx2: f32, cy2: f32, x: f32, y: f32) { - self.path.curve_to( - (self.ox + cx1 as f64, self.oy - cy1 as f64), - (self.ox + cx2 as f64, self.oy - cy2 as f64), - (self.ox + x as f64, self.oy - y as f64), - ); + self.path.curve_to((self.px(cx1, cy1), self.py(cy1)), (self.px(cx2, cy2), self.py(cy2)), (self.px(x, y), self.py(y))); } fn close(&mut self) { self.path.close_path(); @@ -2166,7 +2182,7 @@ fn ensure_fonts_registered(font_ctx: &mut parley::FontContext) { REGISTERED_FONTS.with(|reg| { let mut reg = reg.borrow_mut(); RENDER_FONTS.with(|rf| { - for (_, hash, bytes) in rf.borrow().iter() { + for (family, style, hash, bytes) in rf.borrow().iter() { if reg.insert(*hash) { struct ArcBytes(std::sync::Arc<[u8]>); impl AsRef<[u8]> for ArcBytes { @@ -2175,7 +2191,15 @@ fn ensure_fonts_registered(font_ctx: &mut parley::FontContext) { } } let font_data: std::sync::Arc + Send + Sync> = std::sync::Arc::new(ArcBytes(bytes.clone())); - font_ctx.collection.register_fonts(parley::fontique::Blob::new(font_data), None); + let families = font_ctx.collection.register_fonts(parley::fontique::Blob::new(font_data), None); + + if let Some((_, fonts_info)) = families.first() { + if let Some(font_info) = fonts_info.first() { + FONT_INFO_CACHE.with(|cache| { + cache.borrow_mut().insert((family.clone(), style.clone()), font_info.clone()); + }); + } + } } } }); @@ -2183,7 +2207,7 @@ fn ensure_fonts_registered(font_ctx: &mut parley::FontContext) { } const DEFAULT_FONT_FAMILY: &str = "Lato"; -const DEFAULT_FONT_SIZE: f64 = 16.; +const DEFAULT_FONT_SIZE: f64 = 24.; impl Render for List { fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) { @@ -2198,9 +2222,26 @@ impl Render for List { let opacity_fill_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY_FILL, index, 1.); let blend_mode_attr: BlendMode = self.attribute_cloned_or_default(ATTR_BLEND_MODE, index); let font_family: String = self.attribute_cloned_or(ATTR_FONT_FAMILY, index, DEFAULT_FONT_FAMILY.to_string()); + let font_style: String = self.attribute_cloned_or(ATTR_FONT_STYLE, index, "Regular".to_string()); let font_size: f64 = self.attribute_cloned_or(ATTR_FONT_SIZE, index, DEFAULT_FONT_SIZE); + let line_height: f64 = self.attribute_cloned_or(ATTR_TEXT_LINE_HEIGHT, index, 1.2); + let char_spacing: f64 = self.attribute_cloned_or(ATTR_TEXT_CHARACTER_SPACING, index, 0.); + let max_width: Option = self.attribute_cloned_or(ATTR_TEXT_MAX_WIDTH, index, None); + let max_height: Option = self.attribute_cloned_or(ATTR_TEXT_MAX_HEIGHT, index, None); + let tilt: f64 = self.attribute_cloned_or(ATTR_TEXT_TILT, index, 0.); + let align_u8: u8 = self.attribute_cloned_or(ATTR_TEXT_ALIGN, index, 0_u8); let opacity = (opacity_attr * if render_params.for_mask { 1. } else { opacity_fill_attr }) as f32; + let (parley_align, last_line_correction) = match align_u8 { + 1 => (parley::Alignment::Center, None), + 2 => (parley::Alignment::Right, None), + 3 => (parley::Alignment::Justify, Some(parley::Alignment::Left)), + 4 => (parley::Alignment::Justify, Some(parley::Alignment::Center)), + 5 => (parley::Alignment::Justify, Some(parley::Alignment::Right)), + 6 => (parley::Alignment::Justify, Some(parley::Alignment::Justify)), + _ => (parley::Alignment::Left, None), + }; + let mut glyph_paths: Vec = Vec::new(); FONT_CTX.with(|ctx| { @@ -2212,14 +2253,63 @@ impl Render for List { let mut builder = layout_ctx.ranged_builder(font_ctx, text, 1.0, false); builder.push_default(StyleProperty::FontSize(font_size as f32)); builder.push_default(StyleProperty::FontStack(FontStack::Single(FontFamily::Named(Cow::Borrowed(font_family.as_str()))))); + builder.push_default(StyleProperty::LetterSpacing(char_spacing as f32)); + builder.push_default(LineHeight::FontSizeRelative(line_height as f32)); + + FONT_INFO_CACHE.with(|cache| { + let cache = cache.borrow(); + if let Some(font_info) = cache.get(&(font_family.clone(), font_style.clone())) { + 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 = builder.build(text); - layout.break_all_lines(None); + let max_width_f32 = max_width.map(|w| w as f32); + let alignment_width = max_width_f32.unwrap_or_else(|| layout.full_width()); + layout.break_all_lines(max_width_f32); + layout.align(max_width_f32, parley_align, AlignmentOptions::default()); + + let tilt_tan = tilt.to_radians().tan(); for line in layout.lines() { + let range = line.text_range(); + let is_last_para_line = range.end == text.len() || text.get(range.clone()).is_some_and(|s| s.ends_with('\n')); + + let (x_offset, space_extra) = if let (true, Some(correction)) = (is_last_para_line, last_line_correction) { + let metrics = line.metrics(); + let content_advance = metrics.advance - metrics.trailing_whitespace; + let free_space = alignment_width - content_advance; + // Correction is needed because Parley doesn't remove trailing whitespaces + match correction { + parley::Alignment::Center => (free_space * 0.5, 0.), + parley::Alignment::Right => (free_space, 0.), + parley::Alignment::Justify => { + let line_text = text.get(range.clone()).unwrap_or(""); + let trailing_len = line_text.len() - line_text.trim_end().len(); + let visible_end_index = range.end - trailing_len; + + let space_count: usize = line + .runs() + .map(|run| run.clusters().filter(|c| c.is_space_or_nbsp() && c.text_range().start < visible_end_index).count()) + .sum(); + let extra = if space_count > 0 { free_space / space_count as f32 } else { 0. }; + (0., extra) + } + _ => (0., 0.), + } + } else { + (0., 0.) + }; + for item in line.items() { let PositionedLayoutItem::GlyphRun(glyph_run) = item else { continue }; + if max_height.is_some_and(|mh| glyph_run.baseline() > mh as f32) { + continue; + } - let mut run_x = glyph_run.offset(); + let mut run_x = glyph_run.offset() + x_offset; let run_y = glyph_run.baseline(); let run = glyph_run.run(); let font = run.font(); @@ -2230,6 +2320,12 @@ impl Render for List { let Ok(font_ref) = SkrifaFontRef::from_index(font_data, font.index) else { continue }; let outlines = font_ref.outline_glyphs(); + let mut pen = SvgGlyphPen { + d: String::new(), + ox: 0., + oy: 0., + tilt_tan, + }; for glyph in glyph_run.glyphs() { let ox = (run_x + glyph.x) as f64; let oy = (run_y - glyph.y) as f64; @@ -2238,9 +2334,14 @@ impl Render for List { let glyph_id = GlyphId::from(glyph.id); let Some(outline) = outlines.get(glyph_id) else { continue }; let settings = DrawSettings::unhinted(Size::new(font_size_pts), LocationRef::new(&normalized_coords)); - let mut pen = SvgGlyphPen { d: String::new(), ox, oy }; + + pen.d.clear(); + pen.ox = ox; + pen.oy = oy; if outline.draw(settings, &mut pen).is_ok() && !pen.d.is_empty() { - glyph_paths.push(pen.d); + glyph_paths.push(pen.d.clone()); + } else if space_extra != 0. && glyph.advance > 0. { + run_x += space_extra; } } } @@ -2294,12 +2395,29 @@ impl Render for List { let item_transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index); let font_family: String = self.attribute_cloned_or(ATTR_FONT_FAMILY, index, DEFAULT_FONT_FAMILY.to_string()); + let font_style: String = self.attribute_cloned_or(ATTR_FONT_STYLE, index, "Regular".to_string()); let font_size: f64 = self.attribute_cloned_or(ATTR_FONT_SIZE, index, DEFAULT_FONT_SIZE); + let line_height: f64 = self.attribute_cloned_or(ATTR_TEXT_LINE_HEIGHT, index, 1.2); + let char_spacing: f64 = self.attribute_cloned_or(ATTR_TEXT_CHARACTER_SPACING, index, 0.); + let max_width: Option = self.attribute_cloned_or(ATTR_TEXT_MAX_WIDTH, index, None); + let max_height: Option = self.attribute_cloned_or(ATTR_TEXT_MAX_HEIGHT, index, None); + let tilt: f64 = self.attribute_cloned_or(ATTR_TEXT_TILT, index, 0.); + let align_u8: u8 = self.attribute_cloned_or(ATTR_TEXT_ALIGN, index, 0_u8); let blend_mode_attr: BlendMode = self.attribute_cloned_or_default(ATTR_BLEND_MODE, index); let opacity_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY, index, 1.); let opacity_fill_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY_FILL, index, 1.); let opacity = (opacity_attr * if render_params.for_mask { 1. } else { opacity_fill_attr }) as f32; + let (parley_align, last_line_correction) = match align_u8 { + 1 => (parley::Alignment::Center, None), + 2 => (parley::Alignment::Right, None), + 3 => (parley::Alignment::Justify, Some(parley::Alignment::Left)), + 4 => (parley::Alignment::Justify, Some(parley::Alignment::Center)), + 5 => (parley::Alignment::Justify, Some(parley::Alignment::Right)), + 6 => (parley::Alignment::Justify, Some(parley::Alignment::Justify)), + _ => (parley::Alignment::Left, None), + }; + let affine = Affine::new((transform * item_transform).to_cols_array()); FONT_CTX.with(|ctx| { @@ -2311,8 +2429,23 @@ impl Render for List { let mut builder = layout_ctx.ranged_builder(font_ctx, text, 1.0, false); builder.push_default(StyleProperty::FontSize(font_size as f32)); builder.push_default(StyleProperty::FontStack(FontStack::Single(FontFamily::Named(Cow::Borrowed(font_family.as_str()))))); + builder.push_default(StyleProperty::LetterSpacing(char_spacing as f32)); + builder.push_default(LineHeight::FontSizeRelative(line_height as f32)); + + FONT_INFO_CACHE.with(|cache| { + let cache = cache.borrow(); + if let Some(font_info) = cache.get(&(font_family.clone(), font_style.clone())) { + 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 = builder.build(text); - layout.break_all_lines(None); + let max_width_f32 = max_width.map(|w| w as f32); + let alignment_width = max_width_f32.unwrap_or_else(|| layout.full_width()); + layout.break_all_lines(max_width_f32); + layout.align(max_width_f32, parley_align, AlignmentOptions::default()); let needs_layer = opacity < 1. || blend_mode_attr != BlendMode::default(); if needs_layer { @@ -2323,11 +2456,45 @@ impl Render for List { scene.push_layer(peniko::Fill::NonZero, blending, opacity, kurbo::Affine::IDENTITY, &transformed_bounds); } + let tilt_tan = tilt.to_radians().tan(); + for line in layout.lines() { + let range = line.text_range(); + let is_last_para_line = range.end == text.len() || text.get(range.clone()).is_some_and(|s| s.ends_with('\n')); + + let (x_offset, space_extra) = if let (true, Some(correction)) = (is_last_para_line, last_line_correction) { + let metrics = line.metrics(); + let content_advance = metrics.advance - metrics.trailing_whitespace; + let free_space = alignment_width - content_advance; + + match correction { + parley::Alignment::Center => (free_space * 0.5, 0.), + parley::Alignment::Right => (free_space, 0.), + parley::Alignment::Justify => { + let line_text = text.get(range.clone()).unwrap_or(""); + let trailing_len = line_text.len() - line_text.trim_end().len(); + let visible_end_index = range.end - trailing_len; + + let space_count: usize = line + .runs() + .map(|run| run.clusters().filter(|c| c.is_space_or_nbsp() && c.text_range().start < visible_end_index).count()) + .sum(); + let extra = if space_count > 0 { free_space / space_count as f32 } else { 0. }; + (0., extra) + } + _ => (0., 0.), + } + } else { + (0., 0.) + }; + for item in line.items() { let PositionedLayoutItem::GlyphRun(glyph_run) = item else { continue }; + if max_height.is_some_and(|mh| glyph_run.baseline() > mh as f32) { + continue; + } - let mut run_x = glyph_run.offset(); + let mut run_x = glyph_run.offset() + x_offset; let run_y = glyph_run.baseline(); let run = glyph_run.run(); let font = run.font(); @@ -2338,6 +2505,7 @@ impl Render for List { let Ok(font_ref) = SkrifaFontRef::from_index(font_data, font.index) else { continue }; let outlines = font_ref.outline_glyphs(); + let mut bez_path = BezPath::new(); for glyph in glyph_run.glyphs() { let ox = (run_x + glyph.x) as f64; let oy = (run_y - glyph.y) as f64; @@ -2347,8 +2515,13 @@ impl Render for List { let Some(outline) = outlines.get(glyph_id) else { continue }; let settings = DrawSettings::unhinted(Size::new(font_size_pts), LocationRef::new(&normalized_coords)); - let mut bez_path = BezPath::new(); - let mut pen = VelloPen { path: &mut bez_path, ox, oy }; + bez_path.truncate(0); + let mut pen = VelloPen { + path: &mut bez_path, + ox, + oy, + tilt_tan, + }; if outline.draw(settings, &mut pen).is_ok() && !bez_path.elements().is_empty() { if let RenderMode::Outline = render_params.render_mode { let (outline_stroke, outline_color) = get_outline_styles(render_params); @@ -2356,6 +2529,8 @@ impl Render for List { } else { scene.fill(peniko::Fill::NonZero, affine, peniko::Color::BLACK, None, &bez_path); } + } else if space_extra != 0. && glyph.advance > 0. { + run_x += space_extra; } } } @@ -2380,6 +2555,17 @@ impl Render for List { let Some(text) = self.element(index) else { continue }; let font_size: f64 = self.attribute_cloned_or(ATTR_FONT_SIZE, index, DEFAULT_FONT_SIZE); let font_family: String = self.attribute_cloned_or(ATTR_FONT_FAMILY, index, DEFAULT_FONT_FAMILY.to_string()); + let line_height: f64 = self.attribute_cloned_or(ATTR_TEXT_LINE_HEIGHT, index, 1.2); + let char_spacing: f64 = self.attribute_cloned_or(ATTR_TEXT_CHARACTER_SPACING, index, 0.); + let max_width: Option = self.attribute_cloned_or(ATTR_TEXT_MAX_WIDTH, index, None); + let max_height: Option = self.attribute_cloned_or(ATTR_TEXT_MAX_HEIGHT, index, None); + let align_u8: u8 = self.attribute_cloned_or(ATTR_TEXT_ALIGN, index, 0_u8); + let parley_align = match align_u8 { + 1 => parley::Alignment::Center, + 2 => parley::Alignment::Right, + 3..=6 => parley::Alignment::Justify, + _ => parley::Alignment::Left, + }; let transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index); // Falls back to a single-em square if fonts are not yet registered. @@ -2391,9 +2577,14 @@ impl Render for List { let mut builder = layout_ctx.ranged_builder(font_ctx, text, 1.0, false); builder.push_default(StyleProperty::FontSize(font_size as f32)); builder.push_default(StyleProperty::FontStack(FontStack::Single(FontFamily::Named(Cow::Borrowed(font_family.as_str()))))); + builder.push_default(StyleProperty::LetterSpacing(char_spacing as f32)); + builder.push_default(LineHeight::FontSizeRelative(line_height as f32)); let mut layout = builder.build(text); - layout.break_all_lines(None); - Some((layout.width() as f64, layout.height() as f64)) + layout.break_all_lines(max_width.map(|w| w as f32)); + layout.align(max_width.map(|w| w as f32), parley_align, AlignmentOptions::default()); + let w = max_width.unwrap_or_else(|| layout.width() as f64); + let h = max_height.unwrap_or_else(|| layout.height() as f64); + Some((w, h)) }) .unwrap_or((font_size, font_size)); diff --git a/node-graph/libraries/resources/src/lib.rs b/node-graph/libraries/resources/src/lib.rs index 0fffabb324..5a987073e3 100644 --- a/node-graph/libraries/resources/src/lib.rs +++ b/node-graph/libraries/resources/src/lib.rs @@ -32,6 +32,12 @@ impl Resource { } } +impl Default for Resource { + fn default() -> Self { + Self::empty() + } +} + impl From<&Resource> for Arc + Send + Sync> { fn from(val: &Resource) -> Self { val.inner.clone() diff --git a/node-graph/nodes/gstd/src/text.rs b/node-graph/nodes/gstd/src/text.rs index a7a24962c3..1fd87f87c5 100644 --- a/node-graph/nodes/gstd/src/text.rs +++ b/node-graph/nodes/gstd/src/text.rs @@ -1,5 +1,8 @@ -use core_types::Ctx; use core_types::list::List; +use core_types::{ + ATTR_EDITOR_LAYER_PATH, ATTR_FONT_SIZE, ATTR_TEXT_ALIGN, ATTR_TEXT_CHARACTER_SPACING, ATTR_TEXT_FONT, ATTR_TEXT_LINE_HEIGHT, ATTR_TEXT_MAX_HEIGHT, ATTR_TEXT_MAX_WIDTH, ATTR_TEXT_TILT, + ATTR_TRANSFORM, Ctx, +}; use graph_craft::application_io::resource::Resource; use graphic_types::Vector; pub use text_nodes::*; @@ -73,3 +76,140 @@ fn text( to_path(&text, &font, typesetting, separate_glyphs) } + +/// Produces a styled `List` carrying all typographic attributes. +#[node_macro::node(category("Text"))] +fn text_layer( + _: impl Ctx, + _primary: (), + /// The text content to display. + #[widget(ParsedWidgetOverride::Custom = "text_area")] + #[default("Lorem ipsum")] + text: String, + /// The loaded font file used to render the text. The editor resolves the chosen typeface to these bytes via the resource system. + #[widget(ParsedWidgetOverride::Custom = "text_font")] + font: Resource, + /// Font size in document-space pixels. + #[unit(" px")] + #[default(24.)] + #[hard_min(1.)] + size: f64, + /// Line height ratio relative to the font size. 1.2 is the typical default for body copy. + #[unit("x")] + #[hard_min(0.)] + #[step(0.1)] + #[default(1.2)] + line_height: f64, + /// Additional spacing in document-space pixels added between every character pair. + #[unit(" px")] + #[step(0.1)] + character_spacing: f64, + /// Enables the maximum width constraint so lines can wrap. + #[widget(ParsedWidgetOverride::Hidden)] + has_max_width: bool, + /// Maximum line-wrap width in document-space pixels. + #[unit(" px")] + #[hard_min(1.)] + #[widget(ParsedWidgetOverride::Custom = "optional_f64")] + max_width: f64, + /// Enables the maximum height constraint so excess lines are clipped. + #[widget(ParsedWidgetOverride::Hidden)] + has_max_height: bool, + /// Maximum block height in document-space pixels; lines whose baseline exceeds this are not drawn. + #[unit(" px")] + #[hard_min(1.)] + #[widget(ParsedWidgetOverride::Custom = "optional_f64")] + max_height: f64, + /// Faux-italic slant angle in degrees. + #[unit("°")] + #[hard_min(-85.)] + #[hard_max(85.)] + tilt: f64, + /// Horizontal alignment of each line within the text block. + #[widget(ParsedWidgetOverride::Custom = "text_align")] + align: TextAlign, +) -> List { + const DEFAULT_FONT_SIZE: f64 = 24.; + const DEFAULT_LINE_HEIGHT: f64 = 1.2; + + let mut list = List::new_from_element(text); + + // Insert only when value deviates from its default as each stored attribute has runtime cost. + + if font != Resource::default() { + list.set_attribute(ATTR_TEXT_FONT, 0, font); + } + if (size - DEFAULT_FONT_SIZE).abs() > f64::EPSILON { + list.set_attribute(ATTR_FONT_SIZE, 0, size); + } + if (line_height - DEFAULT_LINE_HEIGHT).abs() > f64::EPSILON { + list.set_attribute(ATTR_TEXT_LINE_HEIGHT, 0, line_height); + } + if character_spacing != 0. { + list.set_attribute(ATTR_TEXT_CHARACTER_SPACING, 0, character_spacing); + } + if has_max_width { + list.set_attribute(ATTR_TEXT_MAX_WIDTH, 0, Some(max_width)); + } + if has_max_height { + list.set_attribute(ATTR_TEXT_MAX_HEIGHT, 0, Some(max_height)); + } + if tilt != 0. { + list.set_attribute(ATTR_TEXT_TILT, 0, tilt); + } + if align != TextAlign::default() { + list.set_attribute(ATTR_TEXT_ALIGN, 0, align); + } + + list +} + +/// Converts a styled `List` into vector geometry. +/// Each string item is independently shaped by Parley and vectorised via skrifa. +#[node_macro::node(category("Text"))] +fn text_to_vector( + _: impl Ctx, + /// A styled list of text strings produced by the **Text Layer** node (or any other `List` source). + #[implementations(List)] + strings: List, + /// When enabled, each glyph is emitted as its own vector item instead of a single compound path per string. + separate_glyphs: bool, +) -> List { + let mut result = List::new(); + + for index in 0..strings.len() { + let Some(text) = strings.element(index) else { continue }; + if text.is_empty() { + continue; + } + + let font: Resource = strings.attribute_cloned_or_default(ATTR_TEXT_FONT, index); + + let typesetting = TypesettingConfig { + font_size: strings.attribute_cloned_or(ATTR_FONT_SIZE, index, 24.), + line_height_ratio: strings.attribute_cloned_or(ATTR_TEXT_LINE_HEIGHT, index, 1.2), + character_spacing: strings.attribute_cloned_or(ATTR_TEXT_CHARACTER_SPACING, index, 0.), + max_width: strings.attribute_cloned_or::>(ATTR_TEXT_MAX_WIDTH, index, None), + max_height: strings.attribute_cloned_or::>(ATTR_TEXT_MAX_HEIGHT, index, None), + tilt: strings.attribute_cloned_or(ATTR_TEXT_TILT, index, 0.), + align: strings.attribute_cloned_or(ATTR_TEXT_ALIGN, index, TextAlign::default()), + }; + + let vectors = to_path(text, &font, typesetting, separate_glyphs); + let transform = strings.attribute_cloned_or_default::(ATTR_TRANSFORM, index); + let layer_path = strings.attribute_cloned_or_default::>(ATTR_EDITOR_LAYER_PATH, index); + + for mut item in vectors.into_iter() { + if transform != glam::DAffine2::IDENTITY { + let local = item.attribute_cloned_or_default::(ATTR_TRANSFORM); + item.set_attribute(ATTR_TRANSFORM, transform * local); + } + if !layer_path.is_empty() { + item.set_attribute(ATTR_EDITOR_LAYER_PATH, layer_path.clone()); + } + result.push(item); + } + } + + result +} From 8cf6b8bde3012357233c5e3ad45df452a14cb023 Mon Sep 17 00:00:00 2001 From: Annonnymmousss Date: Mon, 25 May 2026 03:50:54 +0530 Subject: [PATCH 06/12] fix: CI fail because of difference in nature of Mac and github action --- node-graph/libraries/rendering/src/renderer.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/node-graph/libraries/rendering/src/renderer.rs b/node-graph/libraries/rendering/src/renderer.rs index dc073bef7a..6fbe02d0c5 100644 --- a/node-graph/libraries/rendering/src/renderer.rs +++ b/node-graph/libraries/rendering/src/renderer.rs @@ -27,7 +27,7 @@ use graphic_types::vector_types::vector::style::{Fill, PaintOrder, RenderMode, S use graphic_types::{Artboard, Graphic, Vector}; use kurbo::{Affine, BezPath, Cap, Join, Shape}; use num_traits::Zero; -use parley::{AlignmentOptions, FontContext, FontFamily, FontStack, LayoutContext, LineHeight, PositionedLayoutItem, StyleProperty}; +use parley::{AlignmentOptions, FontContext, LayoutContext, LineHeight, PositionedLayoutItem, StyleProperty}; use skrifa::GlyphId; use skrifa::MetadataProvider; use skrifa::instance::{LocationRef, NormalizedCoord, Size}; @@ -2252,7 +2252,7 @@ impl Render for List { let mut builder = layout_ctx.ranged_builder(font_ctx, text, 1.0, false); builder.push_default(StyleProperty::FontSize(font_size as f32)); - builder.push_default(StyleProperty::FontStack(FontStack::Single(FontFamily::Named(Cow::Borrowed(font_family.as_str()))))); + builder.push_default(StyleProperty::FontStack(parley::style::FontStack::Single(parley::style::FontFamily::Named(Cow::Borrowed(font_family.as_str()))))); builder.push_default(StyleProperty::LetterSpacing(char_spacing as f32)); builder.push_default(LineHeight::FontSizeRelative(line_height as f32)); @@ -2428,7 +2428,7 @@ impl Render for List { let mut builder = layout_ctx.ranged_builder(font_ctx, text, 1.0, false); builder.push_default(StyleProperty::FontSize(font_size as f32)); - builder.push_default(StyleProperty::FontStack(FontStack::Single(FontFamily::Named(Cow::Borrowed(font_family.as_str()))))); + builder.push_default(StyleProperty::FontStack(parley::style::FontStack::Single(parley::style::FontFamily::Named(Cow::Borrowed(font_family.as_str()))))); builder.push_default(StyleProperty::LetterSpacing(char_spacing as f32)); builder.push_default(LineHeight::FontSizeRelative(line_height as f32)); @@ -2576,7 +2576,7 @@ impl Render for List { ensure_fonts_registered(font_ctx); let mut builder = layout_ctx.ranged_builder(font_ctx, text, 1.0, false); builder.push_default(StyleProperty::FontSize(font_size as f32)); - builder.push_default(StyleProperty::FontStack(FontStack::Single(FontFamily::Named(Cow::Borrowed(font_family.as_str()))))); + builder.push_default(StyleProperty::FontStack(parley::style::FontStack::Single(parley::style::FontFamily::Named(Cow::Borrowed(font_family.as_str()))))); builder.push_default(StyleProperty::LetterSpacing(char_spacing as f32)); builder.push_default(LineHeight::FontSizeRelative(line_height as f32)); let mut layout = builder.build(text); From 8d7dcb78d40d29b754e3a4593e42a0b8eafa0c35 Mon Sep 17 00:00:00 2001 From: Annonnymmousss Date: Mon, 25 May 2026 04:02:36 +0530 Subject: [PATCH 07/12] chore: fix --- node-graph/libraries/rendering/src/renderer.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/node-graph/libraries/rendering/src/renderer.rs b/node-graph/libraries/rendering/src/renderer.rs index 6fbe02d0c5..f55257ec42 100644 --- a/node-graph/libraries/rendering/src/renderer.rs +++ b/node-graph/libraries/rendering/src/renderer.rs @@ -2252,7 +2252,7 @@ impl Render for List { let mut builder = layout_ctx.ranged_builder(font_ctx, text, 1.0, false); builder.push_default(StyleProperty::FontSize(font_size as f32)); - builder.push_default(StyleProperty::FontStack(parley::style::FontStack::Single(parley::style::FontFamily::Named(Cow::Borrowed(font_family.as_str()))))); + builder.push_default(StyleProperty::FontStack(parley::FontStack::Single(parley::FontFamily::Named(Cow::Borrowed(font_family.as_str()))))); builder.push_default(StyleProperty::LetterSpacing(char_spacing as f32)); builder.push_default(LineHeight::FontSizeRelative(line_height as f32)); @@ -2428,7 +2428,7 @@ impl Render for List { let mut builder = layout_ctx.ranged_builder(font_ctx, text, 1.0, false); builder.push_default(StyleProperty::FontSize(font_size as f32)); - builder.push_default(StyleProperty::FontStack(parley::style::FontStack::Single(parley::style::FontFamily::Named(Cow::Borrowed(font_family.as_str()))))); + builder.push_default(StyleProperty::FontStack(parley::FontStack::Single(parley::FontFamily::Named(Cow::Borrowed(font_family.as_str()))))); builder.push_default(StyleProperty::LetterSpacing(char_spacing as f32)); builder.push_default(LineHeight::FontSizeRelative(line_height as f32)); @@ -2576,7 +2576,7 @@ impl Render for List { ensure_fonts_registered(font_ctx); let mut builder = layout_ctx.ranged_builder(font_ctx, text, 1.0, false); builder.push_default(StyleProperty::FontSize(font_size as f32)); - builder.push_default(StyleProperty::FontStack(parley::style::FontStack::Single(parley::style::FontFamily::Named(Cow::Borrowed(font_family.as_str()))))); + builder.push_default(StyleProperty::FontStack(parley::FontStack::Single(parley::FontFamily::Named(Cow::Borrowed(font_family.as_str()))))); builder.push_default(StyleProperty::LetterSpacing(char_spacing as f32)); builder.push_default(LineHeight::FontSizeRelative(line_height as f32)); let mut layout = builder.build(text); From adfbd1d187e5dd606c09f7217e647ae3102eca11 Mon Sep 17 00:00:00 2001 From: Annonnymmousss Date: Mon, 25 May 2026 04:30:28 +0530 Subject: [PATCH 08/12] chore: replace FontStack as it got removed in parley 0.9 --- Cargo.lock | 2 +- node-graph/libraries/rendering/src/renderer.rs | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 93426ad3c2..8517bf6c42 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4584,7 +4584,7 @@ dependencies = [ "num-traits", "parley", "serde", - "skrifa 0.40.0", + "skrifa", "usvg", "vector-types", "vello", diff --git a/node-graph/libraries/rendering/src/renderer.rs b/node-graph/libraries/rendering/src/renderer.rs index f55257ec42..f504af72e4 100644 --- a/node-graph/libraries/rendering/src/renderer.rs +++ b/node-graph/libraries/rendering/src/renderer.rs @@ -2252,7 +2252,7 @@ impl Render for List { let mut builder = layout_ctx.ranged_builder(font_ctx, text, 1.0, false); builder.push_default(StyleProperty::FontSize(font_size as f32)); - builder.push_default(StyleProperty::FontStack(parley::FontStack::Single(parley::FontFamily::Named(Cow::Borrowed(font_family.as_str()))))); + builder.push_default(StyleProperty::FontFamily(parley::FontFamily::Single(parley::FontFamilyName::Named(Cow::Borrowed(font_family.as_str()))))); builder.push_default(StyleProperty::LetterSpacing(char_spacing as f32)); builder.push_default(LineHeight::FontSizeRelative(line_height as f32)); @@ -2269,7 +2269,7 @@ impl Render for List { let max_width_f32 = max_width.map(|w| w as f32); let alignment_width = max_width_f32.unwrap_or_else(|| layout.full_width()); layout.break_all_lines(max_width_f32); - layout.align(max_width_f32, parley_align, AlignmentOptions::default()); + layout.align(parley_align, AlignmentOptions::default()); let tilt_tan = tilt.to_radians().tan(); @@ -2428,7 +2428,7 @@ impl Render for List { let mut builder = layout_ctx.ranged_builder(font_ctx, text, 1.0, false); builder.push_default(StyleProperty::FontSize(font_size as f32)); - builder.push_default(StyleProperty::FontStack(parley::FontStack::Single(parley::FontFamily::Named(Cow::Borrowed(font_family.as_str()))))); + builder.push_default(StyleProperty::FontFamily(parley::FontFamily::Single(parley::FontFamilyName::Named(Cow::Borrowed(font_family.as_str()))))); builder.push_default(StyleProperty::LetterSpacing(char_spacing as f32)); builder.push_default(LineHeight::FontSizeRelative(line_height as f32)); @@ -2445,7 +2445,7 @@ impl Render for List { let max_width_f32 = max_width.map(|w| w as f32); let alignment_width = max_width_f32.unwrap_or_else(|| layout.full_width()); layout.break_all_lines(max_width_f32); - layout.align(max_width_f32, parley_align, AlignmentOptions::default()); + layout.align(parley_align, AlignmentOptions::default()); let needs_layer = opacity < 1. || blend_mode_attr != BlendMode::default(); if needs_layer { @@ -2576,12 +2576,12 @@ impl Render for List { ensure_fonts_registered(font_ctx); let mut builder = layout_ctx.ranged_builder(font_ctx, text, 1.0, false); builder.push_default(StyleProperty::FontSize(font_size as f32)); - builder.push_default(StyleProperty::FontStack(parley::FontStack::Single(parley::FontFamily::Named(Cow::Borrowed(font_family.as_str()))))); + builder.push_default(StyleProperty::FontFamily(parley::FontFamily::Single(parley::FontFamilyName::Named(Cow::Borrowed(font_family.as_str()))))); builder.push_default(StyleProperty::LetterSpacing(char_spacing as f32)); builder.push_default(LineHeight::FontSizeRelative(line_height as f32)); let mut layout = builder.build(text); layout.break_all_lines(max_width.map(|w| w as f32)); - layout.align(max_width.map(|w| w as f32), parley_align, AlignmentOptions::default()); + layout.align(parley_align, AlignmentOptions::default()); let w = max_width.unwrap_or_else(|| layout.width() as f64); let h = max_height.unwrap_or_else(|| layout.height() as f64); Some((w, h)) From b8610796849f9cfd6a8ba0ae7e96e44dcd6f2ed9 Mon Sep 17 00:00:00 2001 From: Annonnymmousss Date: Mon, 25 May 2026 04:31:23 +0530 Subject: [PATCH 09/12] chore: fmt --- node-graph/libraries/rendering/src/renderer.rs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/node-graph/libraries/rendering/src/renderer.rs b/node-graph/libraries/rendering/src/renderer.rs index f504af72e4..9c6324c72c 100644 --- a/node-graph/libraries/rendering/src/renderer.rs +++ b/node-graph/libraries/rendering/src/renderer.rs @@ -2252,7 +2252,9 @@ impl Render for List { let mut builder = layout_ctx.ranged_builder(font_ctx, text, 1.0, false); builder.push_default(StyleProperty::FontSize(font_size as f32)); - builder.push_default(StyleProperty::FontFamily(parley::FontFamily::Single(parley::FontFamilyName::Named(Cow::Borrowed(font_family.as_str()))))); + builder.push_default(StyleProperty::FontFamily(parley::FontFamily::Single(parley::FontFamilyName::Named(Cow::Borrowed( + font_family.as_str(), + ))))); builder.push_default(StyleProperty::LetterSpacing(char_spacing as f32)); builder.push_default(LineHeight::FontSizeRelative(line_height as f32)); @@ -2428,7 +2430,9 @@ impl Render for List { let mut builder = layout_ctx.ranged_builder(font_ctx, text, 1.0, false); builder.push_default(StyleProperty::FontSize(font_size as f32)); - builder.push_default(StyleProperty::FontFamily(parley::FontFamily::Single(parley::FontFamilyName::Named(Cow::Borrowed(font_family.as_str()))))); + builder.push_default(StyleProperty::FontFamily(parley::FontFamily::Single(parley::FontFamilyName::Named(Cow::Borrowed( + font_family.as_str(), + ))))); builder.push_default(StyleProperty::LetterSpacing(char_spacing as f32)); builder.push_default(LineHeight::FontSizeRelative(line_height as f32)); @@ -2576,7 +2580,9 @@ impl Render for List { ensure_fonts_registered(font_ctx); let mut builder = layout_ctx.ranged_builder(font_ctx, text, 1.0, false); builder.push_default(StyleProperty::FontSize(font_size as f32)); - builder.push_default(StyleProperty::FontFamily(parley::FontFamily::Single(parley::FontFamilyName::Named(Cow::Borrowed(font_family.as_str()))))); + builder.push_default(StyleProperty::FontFamily(parley::FontFamily::Single(parley::FontFamilyName::Named(Cow::Borrowed( + font_family.as_str(), + ))))); builder.push_default(StyleProperty::LetterSpacing(char_spacing as f32)); builder.push_default(LineHeight::FontSizeRelative(line_height as f32)); let mut layout = builder.build(text); From a7d99aac8c29a6e8c7c0db6163514961c5529670 Mon Sep 17 00:00:00 2001 From: Annonnymmousss Date: Sat, 6 Jun 2026 03:11:49 +0530 Subject: [PATCH 10/12] chore: migrate the rendering as of new resource architechture --- Cargo.lock | 2 + .../graph_modification_utils.rs | 14 +- editor/src/node_graph_executor/runtime.rs | 7 + node-graph/libraries/core-types/src/lib.rs | 4 +- node-graph/libraries/core-types/src/list.rs | 8 +- .../core-types/src/render_complexity.rs | 2 +- .../libraries/graphic-types/src/graphic.rs | 4 +- node-graph/libraries/rendering/Cargo.toml | 2 + .../libraries/rendering/src/renderer.rs | 218 ++++-------------- node-graph/nodes/blending/src/lib.rs | 23 ++ node-graph/nodes/graphic/src/artboard.rs | 1 + node-graph/nodes/graphic/src/graphic.rs | 13 +- node-graph/nodes/gstd/src/text.rs | 27 ++- node-graph/nodes/text/src/text_context.rs | 2 +- .../nodes/transform/src/transform_nodes.rs | 1 + 15 files changed, 130 insertions(+), 198 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8517bf6c42..ac9cd5352d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4578,6 +4578,7 @@ dependencies = [ "dyn-any", "glam", "graphene-hash", + "graphene-resource", "graphic-types", "kurbo", "log", @@ -4585,6 +4586,7 @@ dependencies = [ "parley", "serde", "skrifa", + "text-nodes", "usvg", "vector-types", "vello", diff --git a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs index 1a38b87213..a8f2547d99 100644 --- a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs +++ b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs @@ -494,15 +494,21 @@ pub fn get_text<'a>( Some((text, font, typesetting, per_glyph_items)) } -/// Gets properties from the Text Layer node -pub fn get_text_layer(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<(&String, &Font, TypesettingConfig)> { +/// Gets properties from the Text Layer node. Resolves the font selection by reading the resource id and lookup via the fonts message handler. +pub fn get_text_layer<'a>( + layer: LayerNodeIdentifier, + network_interface: &'a NodeNetworkInterface, + fonts: &FontsMessageHandler, + resources: &ResourceMessageHandler, +) -> Option<(&'a String, Font, TypesettingConfig)> { let inputs = NodeGraphLayer::new(layer, network_interface).find_node_inputs(&DefinitionIdentifier::ProtoNode(graphene_std::text::text_layer::IDENTIFIER))?; let Some(TaggedValue::String(text)) = &inputs[graphene_std::text::text_layer::TextInput::INDEX].as_value() else { return None; }; - let Some(TaggedValue::Font(font)) = &inputs[graphene_std::text::text_layer::FontInput::INDEX].as_value() else { - return None; + let font = match &inputs[graphene_std::text::text_layer::FontInput::INDEX].as_value() { + Some(TaggedValue::Resource(resource_id)) => fonts.id_font(resources, *resource_id).unwrap_or_default(), + _ => Font::default(), }; let Some(&TaggedValue::F64(font_size)) = inputs[graphene_std::text::text_layer::SizeInput::INDEX].as_value() else { return None; diff --git a/editor/src/node_graph_executor/runtime.rs b/editor/src/node_graph_executor/runtime.rs index df671f1809..a649924d17 100644 --- a/editor/src/node_graph_executor/runtime.rs +++ b/editor/src/node_graph_executor/runtime.rs @@ -435,6 +435,13 @@ impl NodeRuntime { // Insert the vector modify self.vector_modify.insert(parent_network_node_id, io.output.element(0).cloned().unwrap_or_default()); } + // String list: thumbnail + else if let Some(io) = introspected_data.downcast_ref::>>() { + if update_thumbnails { + let bounds = io.output.thumbnail_bounding_box(DAffine2::IDENTITY, true); + Self::render_thumbnail(&mut self.thumbnail_renders, parent_network_node_id, &io.output, bounds, responses) + } + } // Other else { log::warn!("Failed to downcast monitor node output {parent_network_node_id:?}"); diff --git a/node-graph/libraries/core-types/src/lib.rs b/node-graph/libraries/core-types/src/lib.rs index f00e92839c..981300a321 100644 --- a/node-graph/libraries/core-types/src/lib.rs +++ b/node-graph/libraries/core-types/src/lib.rs @@ -25,8 +25,8 @@ pub use graphene_hash; pub use graphene_hash::CacheHash; pub use list::{ ATTR_BACKGROUND, ATTR_BLEND_MODE, ATTR_CLIP, ATTR_CLIPPING_MASK, ATTR_DIMENSIONS, ATTR_EDITOR_CLICK_TARGET, ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_EDITOR_TEXT_FRAME, ATTR_END, - ATTR_FONT_FAMILY, ATTR_FONT_SIZE, ATTR_FONT_STYLE, ATTR_GRADIENT_TYPE, ATTR_LOCATION, ATTR_NAME, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_SPREAD_METHOD, ATTR_START, ATTR_TEXT_ALIGN, - ATTR_TEXT_CHARACTER_SPACING, ATTR_TEXT_FONT, ATTR_TEXT_LINE_HEIGHT, ATTR_TEXT_MAX_HEIGHT, ATTR_TEXT_MAX_WIDTH, ATTR_TEXT_TILT, ATTR_TRANSFORM, ATTR_TYPE, + ATTR_FONT_SIZE, ATTR_GRADIENT_TYPE, ATTR_LOCATION, ATTR_NAME, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_SPREAD_METHOD, ATTR_START, ATTR_TEXT_ALIGN, ATTR_TEXT_CHARACTER_SPACING, ATTR_TEXT_FONT, + ATTR_TEXT_LINE_HEIGHT, ATTR_TEXT_MAX_HEIGHT, ATTR_TEXT_MAX_WIDTH, ATTR_TEXT_TILT, ATTR_TRANSFORM, ATTR_TYPE, }; pub use memo::MemoHash; pub use no_std_types::AsU32; diff --git a/node-graph/libraries/core-types/src/list.rs b/node-graph/libraries/core-types/src/list.rs index 112f394034..fd677f9bb8 100644 --- a/node-graph/libraries/core-types/src/list.rs +++ b/node-graph/libraries/core-types/src/list.rs @@ -77,13 +77,7 @@ pub const ATTR_SPREAD_METHOD: &str = "spread_method"; /// Gradient's `GradientType` (`Linear` or `Radial`). pub const ATTR_GRADIENT_TYPE: &str = "gradient_type"; -/// Text item's font family (`String`, implicit default `"Lato"`). -pub const ATTR_FONT_FAMILY: &str = "font_family"; - -/// Text item's font style (`String`, implicit default `"Regular"`). -pub const ATTR_FONT_STYLE: &str = "font_style"; - -/// Text item's font size in document-space units (`f64`, implicit default `16.`). +/// Text item's font size in document-space units (`f64`, implicit default `24.`). pub const ATTR_FONT_SIZE: &str = "font_size"; /// Text item's font `Resource`. Only set by `text_layer`; used by `text_to_vector` and the renderer to reconstruct exact glyph paths. diff --git a/node-graph/libraries/core-types/src/render_complexity.rs b/node-graph/libraries/core-types/src/render_complexity.rs index 15578d771c..691c644aa4 100644 --- a/node-graph/libraries/core-types/src/render_complexity.rs +++ b/node-graph/libraries/core-types/src/render_complexity.rs @@ -22,6 +22,6 @@ impl RenderComplexity for Color { impl RenderComplexity for String { fn render_complexity(&self) -> usize { - 1 + self.chars().count() } } diff --git a/node-graph/libraries/graphic-types/src/graphic.rs b/node-graph/libraries/graphic-types/src/graphic.rs index 93869ba580..a70ddb381c 100644 --- a/node-graph/libraries/graphic-types/src/graphic.rs +++ b/node-graph/libraries/graphic-types/src/graphic.rs @@ -379,7 +379,7 @@ impl BoundingBox for Graphic { Graphic::Graphic(list) => list.bounding_box(transform, include_stroke), Graphic::Color(list) => list.bounding_box(transform, include_stroke), Graphic::Gradient(list) => list.bounding_box(transform, include_stroke), - Graphic::Text(_) => RenderBoundingBox::Infinite, + Graphic::Text(list) => list.bounding_box(transform, include_stroke), } } @@ -391,7 +391,7 @@ impl BoundingBox for Graphic { Graphic::Graphic(graphic) => graphic.thumbnail_bounding_box(transform, include_stroke), Graphic::Color(color) => color.thumbnail_bounding_box(transform, include_stroke), Graphic::Gradient(gradient) => gradient.thumbnail_bounding_box(transform, include_stroke), - Graphic::Text(_) => RenderBoundingBox::Infinite, + Graphic::Text(list) => list.thumbnail_bounding_box(transform, include_stroke), } } } diff --git a/node-graph/libraries/rendering/Cargo.toml b/node-graph/libraries/rendering/Cargo.toml index c8e4375e3c..13facc359c 100644 --- a/node-graph/libraries/rendering/Cargo.toml +++ b/node-graph/libraries/rendering/Cargo.toml @@ -15,6 +15,8 @@ serde = ["dep:serde", "core-types/serde", "vector-types/serde", "graphic-types/s dyn-any = { workspace = true } core-types = { workspace = true } graphene-hash = { workspace = true } +graphene-resource = { workspace = true } +text-nodes = { workspace = true } # Workspace dependencies glam = { workspace = true } diff --git a/node-graph/libraries/rendering/src/renderer.rs b/node-graph/libraries/rendering/src/renderer.rs index 9c6324c72c..74b903c48f 100644 --- a/node-graph/libraries/rendering/src/renderer.rs +++ b/node-graph/libraries/rendering/src/renderer.rs @@ -13,12 +13,13 @@ use core_types::transform::Footprint; use core_types::uuid::{NodeId, generate_uuid}; use core_types::{ ATTR_BACKGROUND, ATTR_BLEND_MODE, ATTR_CLIP, ATTR_CLIPPING_MASK, ATTR_DIMENSIONS, ATTR_EDITOR_CLICK_TARGET, ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_EDITOR_TEXT_FRAME, - ATTR_FONT_FAMILY, ATTR_FONT_SIZE, ATTR_FONT_STYLE, ATTR_GRADIENT_TYPE, ATTR_LOCATION, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_SPREAD_METHOD, ATTR_TEXT_ALIGN, ATTR_TEXT_CHARACTER_SPACING, - ATTR_TEXT_LINE_HEIGHT, ATTR_TEXT_MAX_HEIGHT, ATTR_TEXT_MAX_WIDTH, ATTR_TEXT_TILT, ATTR_TRANSFORM, + ATTR_FONT_SIZE, ATTR_GRADIENT_TYPE, ATTR_LOCATION, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_SPREAD_METHOD, ATTR_TEXT_ALIGN, ATTR_TEXT_CHARACTER_SPACING, ATTR_TEXT_FONT, ATTR_TEXT_LINE_HEIGHT, + ATTR_TEXT_MAX_HEIGHT, ATTR_TEXT_MAX_WIDTH, ATTR_TEXT_TILT, ATTR_TRANSFORM, }; use dyn_any::DynAny; use glam::{DAffine2, DVec2}; use graphene_hash::CacheHashWrapper; +use graphene_resource::Resource; use graphic_types::raster_types::{BitmapMut, CPU, GPU, Image, Raster}; use graphic_types::vector_types::gradient::{GradientStops, GradientType}; use graphic_types::vector_types::subpath::Subpath; @@ -27,56 +28,20 @@ use graphic_types::vector_types::vector::style::{Fill, PaintOrder, RenderMode, S use graphic_types::{Artboard, Graphic, Vector}; use kurbo::{Affine, BezPath, Cap, Join, Shape}; use num_traits::Zero; -use parley::{AlignmentOptions, FontContext, LayoutContext, LineHeight, PositionedLayoutItem, StyleProperty}; +use parley::PositionedLayoutItem; use skrifa::GlyphId; use skrifa::MetadataProvider; use skrifa::instance::{LocationRef, NormalizedCoord, Size}; use skrifa::outline::{DrawSettings, OutlinePen}; use skrifa::raw::FontRef as SkrifaFontRef; -use std::borrow::Cow; -use std::cell::RefCell; use std::collections::{HashMap, HashSet}; use std::fmt::Write; -use std::hash::{Hash, Hasher}; +use std::hash::Hash; use std::ops::Deref; use std::sync::{Arc, LazyLock}; use vector_types::gradient::GradientSpreadMethod; use vello::*; -// Thread local storage for font bytes -thread_local! { - static RENDER_FONTS: RefCell)]>> = RefCell::new(Arc::from([])); -} - -// Thread-local parley font shaping context -thread_local! { - static FONT_CTX: RefCell<(FontContext, LayoutContext<()>)> = RefCell::new((FontContext::default(), LayoutContext::default())); -} - -// Tracks which font bytes have already been registered into FONT_CTX -thread_local! { - static REGISTERED_FONTS: RefCell> = RefCell::new(HashSet::new()); -} - -// Caches the first FontInfo (weight/style/width) for each (family, style) pair after registration -thread_local! { - static FONT_INFO_CACHE: RefCell> = RefCell::new(HashMap::new()); -} - -// Set the font bytes available to the renderer for the current execution. -pub fn set_render_fonts(fonts: impl IntoIterator)>) { - let slice: Arc<[(String, String, u64, Arc<[u8]>)]> = fonts - .into_iter() - .map(|(family, style, bytes)| { - let mut hasher = std::collections::hash_map::DefaultHasher::new(); - bytes.hash(&mut hasher); - (family, style, hasher.finish(), bytes) - }) - .collect::>() - .into(); - RENDER_FONTS.with(|f| *f.borrow_mut() = slice); -} - #[derive(Clone, Copy, Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] enum MaskType { @@ -2177,36 +2142,6 @@ impl OutlinePen for VelloPen<'_> { } } -/// Registers all fonts from `RENDER_FONTS` that aren't yet in `FONT_CTX`. -fn ensure_fonts_registered(font_ctx: &mut parley::FontContext) { - REGISTERED_FONTS.with(|reg| { - let mut reg = reg.borrow_mut(); - RENDER_FONTS.with(|rf| { - for (family, style, hash, bytes) in rf.borrow().iter() { - if reg.insert(*hash) { - struct ArcBytes(std::sync::Arc<[u8]>); - impl AsRef<[u8]> for ArcBytes { - fn as_ref(&self) -> &[u8] { - &self.0 - } - } - let font_data: std::sync::Arc + Send + Sync> = std::sync::Arc::new(ArcBytes(bytes.clone())); - let families = font_ctx.collection.register_fonts(parley::fontique::Blob::new(font_data), None); - - if let Some((_, fonts_info)) = families.first() { - if let Some(font_info) = fonts_info.first() { - FONT_INFO_CACHE.with(|cache| { - cache.borrow_mut().insert((family.clone(), style.clone()), font_info.clone()); - }); - } - } - } - } - }); - }); -} - -const DEFAULT_FONT_FAMILY: &str = "Lato"; const DEFAULT_FONT_SIZE: f64 = 24.; impl Render for List { @@ -2221,58 +2156,33 @@ impl Render for List { let opacity_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY, index, 1.); let opacity_fill_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY_FILL, index, 1.); let blend_mode_attr: BlendMode = self.attribute_cloned_or_default(ATTR_BLEND_MODE, index); - let font_family: String = self.attribute_cloned_or(ATTR_FONT_FAMILY, index, DEFAULT_FONT_FAMILY.to_string()); - let font_style: String = self.attribute_cloned_or(ATTR_FONT_STYLE, index, "Regular".to_string()); + let font: Resource = self.attribute_cloned_or_default(ATTR_TEXT_FONT, index); let font_size: f64 = self.attribute_cloned_or(ATTR_FONT_SIZE, index, DEFAULT_FONT_SIZE); let line_height: f64 = self.attribute_cloned_or(ATTR_TEXT_LINE_HEIGHT, index, 1.2); let char_spacing: f64 = self.attribute_cloned_or(ATTR_TEXT_CHARACTER_SPACING, index, 0.); let max_width: Option = self.attribute_cloned_or(ATTR_TEXT_MAX_WIDTH, index, None); let max_height: Option = self.attribute_cloned_or(ATTR_TEXT_MAX_HEIGHT, index, None); let tilt: f64 = self.attribute_cloned_or(ATTR_TEXT_TILT, index, 0.); - let align_u8: u8 = self.attribute_cloned_or(ATTR_TEXT_ALIGN, index, 0_u8); + let align: text_nodes::TextAlign = self.attribute_cloned_or_default(ATTR_TEXT_ALIGN, index); let opacity = (opacity_attr * if render_params.for_mask { 1. } else { opacity_fill_attr }) as f32; - let (parley_align, last_line_correction) = match align_u8 { - 1 => (parley::Alignment::Center, None), - 2 => (parley::Alignment::Right, None), - 3 => (parley::Alignment::Justify, Some(parley::Alignment::Left)), - 4 => (parley::Alignment::Justify, Some(parley::Alignment::Center)), - 5 => (parley::Alignment::Justify, Some(parley::Alignment::Right)), - 6 => (parley::Alignment::Justify, Some(parley::Alignment::Justify)), - _ => (parley::Alignment::Left, None), + let typesetting = text_nodes::TypesettingConfig { + font_size, + line_height_ratio: line_height, + character_spacing: char_spacing, + max_width, + max_height, + align, + tilt, }; let mut glyph_paths: Vec = Vec::new(); - FONT_CTX.with(|ctx| { - let Ok(mut ctx) = ctx.try_borrow_mut() else { return }; - let (font_ctx, layout_ctx) = &mut *ctx; - - ensure_fonts_registered(font_ctx); - - let mut builder = layout_ctx.ranged_builder(font_ctx, text, 1.0, false); - builder.push_default(StyleProperty::FontSize(font_size as f32)); - builder.push_default(StyleProperty::FontFamily(parley::FontFamily::Single(parley::FontFamilyName::Named(Cow::Borrowed( - font_family.as_str(), - ))))); - builder.push_default(StyleProperty::LetterSpacing(char_spacing as f32)); - builder.push_default(LineHeight::FontSizeRelative(line_height as f32)); - - FONT_INFO_CACHE.with(|cache| { - let cache = cache.borrow(); - if let Some(font_info) = cache.get(&(font_family.clone(), font_style.clone())) { - 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 = builder.build(text); + text_nodes::TextContext::with_thread_local(|ctx| { + let Some(layout) = ctx.layout_text(text, &font, typesetting) else { return }; let max_width_f32 = max_width.map(|w| w as f32); let alignment_width = max_width_f32.unwrap_or_else(|| layout.full_width()); - layout.break_all_lines(max_width_f32); - layout.align(parley_align, AlignmentOptions::default()); - + let last_line_correction = align.last_line_correction(); let tilt_tan = tilt.to_radians().tan(); for line in layout.lines() { @@ -2396,60 +2306,36 @@ impl Render for List { } let item_transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index); - let font_family: String = self.attribute_cloned_or(ATTR_FONT_FAMILY, index, DEFAULT_FONT_FAMILY.to_string()); - let font_style: String = self.attribute_cloned_or(ATTR_FONT_STYLE, index, "Regular".to_string()); + let font: Resource = self.attribute_cloned_or_default(ATTR_TEXT_FONT, index); let font_size: f64 = self.attribute_cloned_or(ATTR_FONT_SIZE, index, DEFAULT_FONT_SIZE); let line_height: f64 = self.attribute_cloned_or(ATTR_TEXT_LINE_HEIGHT, index, 1.2); let char_spacing: f64 = self.attribute_cloned_or(ATTR_TEXT_CHARACTER_SPACING, index, 0.); let max_width: Option = self.attribute_cloned_or(ATTR_TEXT_MAX_WIDTH, index, None); let max_height: Option = self.attribute_cloned_or(ATTR_TEXT_MAX_HEIGHT, index, None); let tilt: f64 = self.attribute_cloned_or(ATTR_TEXT_TILT, index, 0.); - let align_u8: u8 = self.attribute_cloned_or(ATTR_TEXT_ALIGN, index, 0_u8); + let align: text_nodes::TextAlign = self.attribute_cloned_or_default(ATTR_TEXT_ALIGN, index); let blend_mode_attr: BlendMode = self.attribute_cloned_or_default(ATTR_BLEND_MODE, index); let opacity_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY, index, 1.); let opacity_fill_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY_FILL, index, 1.); let opacity = (opacity_attr * if render_params.for_mask { 1. } else { opacity_fill_attr }) as f32; - let (parley_align, last_line_correction) = match align_u8 { - 1 => (parley::Alignment::Center, None), - 2 => (parley::Alignment::Right, None), - 3 => (parley::Alignment::Justify, Some(parley::Alignment::Left)), - 4 => (parley::Alignment::Justify, Some(parley::Alignment::Center)), - 5 => (parley::Alignment::Justify, Some(parley::Alignment::Right)), - 6 => (parley::Alignment::Justify, Some(parley::Alignment::Justify)), - _ => (parley::Alignment::Left, None), + let typesetting = text_nodes::TypesettingConfig { + font_size, + line_height_ratio: line_height, + character_spacing: char_spacing, + max_width, + max_height, + align, + tilt, }; let affine = Affine::new((transform * item_transform).to_cols_array()); - FONT_CTX.with(|ctx| { - let Ok(mut ctx) = ctx.try_borrow_mut() else { return }; - let (font_ctx, layout_ctx) = &mut *ctx; - - ensure_fonts_registered(font_ctx); - - let mut builder = layout_ctx.ranged_builder(font_ctx, text, 1.0, false); - builder.push_default(StyleProperty::FontSize(font_size as f32)); - builder.push_default(StyleProperty::FontFamily(parley::FontFamily::Single(parley::FontFamilyName::Named(Cow::Borrowed( - font_family.as_str(), - ))))); - builder.push_default(StyleProperty::LetterSpacing(char_spacing as f32)); - builder.push_default(LineHeight::FontSizeRelative(line_height as f32)); - - FONT_INFO_CACHE.with(|cache| { - let cache = cache.borrow(); - if let Some(font_info) = cache.get(&(font_family.clone(), font_style.clone())) { - 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 = builder.build(text); + text_nodes::TextContext::with_thread_local(|ctx| { + let Some(layout) = ctx.layout_text(text, &font, typesetting) else { return }; let max_width_f32 = max_width.map(|w| w as f32); let alignment_width = max_width_f32.unwrap_or_else(|| layout.full_width()); - layout.break_all_lines(max_width_f32); - layout.align(parley_align, AlignmentOptions::default()); + let last_line_correction = align.last_line_correction(); let needs_layer = opacity < 1. || blend_mode_attr != BlendMode::default(); if needs_layer { @@ -2557,42 +2443,34 @@ impl Render for List { fn add_upstream_click_targets(&self, click_targets: &mut Vec) { for index in 0..self.len() { let Some(text) = self.element(index) else { continue }; + let font: Resource = self.attribute_cloned_or_default(ATTR_TEXT_FONT, index); let font_size: f64 = self.attribute_cloned_or(ATTR_FONT_SIZE, index, DEFAULT_FONT_SIZE); - let font_family: String = self.attribute_cloned_or(ATTR_FONT_FAMILY, index, DEFAULT_FONT_FAMILY.to_string()); let line_height: f64 = self.attribute_cloned_or(ATTR_TEXT_LINE_HEIGHT, index, 1.2); let char_spacing: f64 = self.attribute_cloned_or(ATTR_TEXT_CHARACTER_SPACING, index, 0.); let max_width: Option = self.attribute_cloned_or(ATTR_TEXT_MAX_WIDTH, index, None); let max_height: Option = self.attribute_cloned_or(ATTR_TEXT_MAX_HEIGHT, index, None); - let align_u8: u8 = self.attribute_cloned_or(ATTR_TEXT_ALIGN, index, 0_u8); - let parley_align = match align_u8 { - 1 => parley::Alignment::Center, - 2 => parley::Alignment::Right, - 3..=6 => parley::Alignment::Justify, - _ => parley::Alignment::Left, - }; + let align: text_nodes::TextAlign = self.attribute_cloned_or_default(ATTR_TEXT_ALIGN, index); let transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index); + let typesetting = text_nodes::TypesettingConfig { + font_size, + line_height_ratio: line_height, + character_spacing: char_spacing, + max_width, + max_height, + align, + tilt: 0., + }; + // Falls back to a single-em square if fonts are not yet registered. - let (width, height) = FONT_CTX - .with(|ctx| { - let Ok(mut ctx) = ctx.try_borrow_mut() else { return None }; - let (font_ctx, layout_ctx) = &mut *ctx; - ensure_fonts_registered(font_ctx); - let mut builder = layout_ctx.ranged_builder(font_ctx, text, 1.0, false); - builder.push_default(StyleProperty::FontSize(font_size as f32)); - builder.push_default(StyleProperty::FontFamily(parley::FontFamily::Single(parley::FontFamilyName::Named(Cow::Borrowed( - font_family.as_str(), - ))))); - builder.push_default(StyleProperty::LetterSpacing(char_spacing as f32)); - builder.push_default(LineHeight::FontSizeRelative(line_height as f32)); - let mut layout = builder.build(text); - layout.break_all_lines(max_width.map(|w| w as f32)); - layout.align(parley_align, AlignmentOptions::default()); + let (width, height) = text_nodes::TextContext::with_thread_local(|ctx| { + ctx.layout_text(text, &font, typesetting).map(|layout| { let w = max_width.unwrap_or_else(|| layout.width() as f64); let h = max_height.unwrap_or_else(|| layout.height() as f64); - Some((w, h)) + (w, h) }) - .unwrap_or((font_size, font_size)); + }) + .unwrap_or((font_size, font_size)); let subpath = Subpath::new_rectangle(DVec2::ZERO, DVec2::new(width, height)); let mut target = ClickTarget::new_with_subpath(subpath, 0.); diff --git a/node-graph/nodes/blending/src/lib.rs b/node-graph/nodes/blending/src/lib.rs index b81d32ec94..fea3aa5ad9 100644 --- a/node-graph/nodes/blending/src/lib.rs +++ b/node-graph/nodes/blending/src/lib.rs @@ -53,6 +53,11 @@ impl MultiplyAlpha for List { multiply_list_attribute(self, ATTR_OPACITY, factor); } } +impl MultiplyAlpha for List { + fn multiply_alpha(&mut self, factor: f64) { + multiply_list_attribute(self, ATTR_OPACITY, factor); + } +} pub(crate) trait MultiplyFill { fn multiply_fill(&mut self, factor: f64); @@ -87,6 +92,11 @@ impl MultiplyFill for List { multiply_list_attribute(self, ATTR_OPACITY_FILL, factor); } } +impl MultiplyFill for List { + fn multiply_fill(&mut self, factor: f64) { + multiply_list_attribute(self, ATTR_OPACITY_FILL, factor); + } +} trait SetBlendMode { fn set_blend_mode(&mut self, blend_mode: BlendMode); @@ -123,6 +133,11 @@ impl SetBlendMode for List { set_list_blend_mode(self, blend_mode); } } +impl SetBlendMode for List { + fn set_blend_mode(&mut self, blend_mode: BlendMode) { + set_list_blend_mode(self, blend_mode); + } +} trait SetClip { fn set_clip(&mut self, clip: bool); @@ -159,6 +174,11 @@ impl SetClip for List { set_list_clip(self, clip); } } +impl SetClip for List { + fn set_clip(&mut self, clip: bool) { + set_list_clip(self, clip); + } +} /// Applies the blend mode to the input graphics. Setting this allows for customizing how overlapping content is composited together. #[node_macro::node(category("Blending"))] @@ -171,6 +191,7 @@ fn blend_mode( List>, List, List, + List, )] mut content: T, /// The choice of equation that controls how brightness and color blends between overlapping pixels. @@ -194,6 +215,7 @@ fn opacity( List>, List, List, + List, )] mut content: T, /// Whether the *Opacity* property is enabled, multiplying the existing opacity by the chosen percentage. @@ -235,6 +257,7 @@ fn clipping_mask( List>, List, List, + List, )] mut content: T, /// Whether the content inherits the alpha of the content beneath it. diff --git a/node-graph/nodes/graphic/src/artboard.rs b/node-graph/nodes/graphic/src/artboard.rs index 1271372ecb..1a992c6656 100644 --- a/node-graph/nodes/graphic/src/artboard.rs +++ b/node-graph/nodes/graphic/src/artboard.rs @@ -15,6 +15,7 @@ pub async fn create_artboard( #[implementations( Context -> List, Context -> List, + Context -> List, Context -> List>, Context -> List>, Context -> List, diff --git a/node-graph/nodes/graphic/src/graphic.rs b/node-graph/nodes/graphic/src/graphic.rs index 17c64f8266..2e942d7471 100644 --- a/node-graph/nodes/graphic/src/graphic.rs +++ b/node-graph/nodes/graphic/src/graphic.rs @@ -116,6 +116,7 @@ async fn map( List>, List, List, + List, )] content: List, #[implementations( @@ -124,6 +125,7 @@ async fn map( Context -> List>, Context -> List, Context -> List, + Context -> List, )] mapped: impl Node, Output = List>, ) -> List { @@ -146,6 +148,7 @@ async fn mirror( #[implementations( List, List, + List, List>, List, List, @@ -495,11 +498,11 @@ fn read_attribute_raster( pub async fn extend( _: impl Ctx, /// The `List` whose items will appear at the start of the extended `List`. - #[implementations(List, List, List, List>, List>, List, List)] + #[implementations(List, List, List, List, List>, List>, List, List)] base: List, /// The `List` whose items will appear at the end of the extended `List`. #[expose] - #[implementations(List, List, List, List>, List>, List, List)] + #[implementations(List, List, List, List, List>, List>, List, List)] new: List, ) -> List { let mut base = base; @@ -514,9 +517,9 @@ pub async fn extend( #[node_macro::node(category(""))] pub async fn legacy_layer_extend( _: impl Ctx, - #[implementations(List, List, List, List>, List>, List, List)] base: List, + #[implementations(List, List, List, List, List>, List>, List, List)] base: List, #[expose] - #[implementations(List, List, List, List>, List>, List, List)] + #[implementations(List, List, List, List, List>, List>, List, List)] new: List, nested_node_path: List, ) -> List { @@ -548,6 +551,7 @@ pub async fn wrap_graphic + 'n>( List>, List, List, + List, DAffine2, )] content: T, @@ -567,6 +571,7 @@ pub async fn to_graphic( List>, List, List, + List, )] content: T, ) -> List { diff --git a/node-graph/nodes/gstd/src/text.rs b/node-graph/nodes/gstd/src/text.rs index 1fd87f87c5..35fc2eea3b 100644 --- a/node-graph/nodes/gstd/src/text.rs +++ b/node-graph/nodes/gstd/src/text.rs @@ -1,12 +1,16 @@ +use core_types::blending::BlendMode; use core_types::list::List; use core_types::{ - ATTR_EDITOR_LAYER_PATH, ATTR_FONT_SIZE, ATTR_TEXT_ALIGN, ATTR_TEXT_CHARACTER_SPACING, ATTR_TEXT_FONT, ATTR_TEXT_LINE_HEIGHT, ATTR_TEXT_MAX_HEIGHT, ATTR_TEXT_MAX_WIDTH, ATTR_TEXT_TILT, - ATTR_TRANSFORM, Ctx, + ATTR_BLEND_MODE, ATTR_EDITOR_LAYER_PATH, ATTR_FONT_SIZE, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_TEXT_ALIGN, ATTR_TEXT_CHARACTER_SPACING, ATTR_TEXT_FONT, ATTR_TEXT_LINE_HEIGHT, + ATTR_TEXT_MAX_HEIGHT, ATTR_TEXT_MAX_WIDTH, ATTR_TEXT_TILT, ATTR_TRANSFORM, Ctx, }; use graph_craft::application_io::resource::Resource; use graphic_types::Vector; pub use text_nodes::*; +const DEFAULT_FONT_SIZE: f64 = 24.; +const DEFAULT_LINE_HEIGHT: f64 = 1.2; + /// Draws a text string as vector geometry with a choice of font and styling. #[node_macro::node(category("Text"))] fn text( @@ -129,9 +133,6 @@ fn text_layer( #[widget(ParsedWidgetOverride::Custom = "text_align")] align: TextAlign, ) -> List { - const DEFAULT_FONT_SIZE: f64 = 24.; - const DEFAULT_LINE_HEIGHT: f64 = 1.2; - let mut list = List::new_from_element(text); // Insert only when value deviates from its default as each stored attribute has runtime cost. @@ -186,8 +187,8 @@ fn text_to_vector( let font: Resource = strings.attribute_cloned_or_default(ATTR_TEXT_FONT, index); let typesetting = TypesettingConfig { - font_size: strings.attribute_cloned_or(ATTR_FONT_SIZE, index, 24.), - line_height_ratio: strings.attribute_cloned_or(ATTR_TEXT_LINE_HEIGHT, index, 1.2), + font_size: strings.attribute_cloned_or(ATTR_FONT_SIZE, index, DEFAULT_FONT_SIZE), + line_height_ratio: strings.attribute_cloned_or(ATTR_TEXT_LINE_HEIGHT, index, DEFAULT_LINE_HEIGHT), character_spacing: strings.attribute_cloned_or(ATTR_TEXT_CHARACTER_SPACING, index, 0.), max_width: strings.attribute_cloned_or::>(ATTR_TEXT_MAX_WIDTH, index, None), max_height: strings.attribute_cloned_or::>(ATTR_TEXT_MAX_HEIGHT, index, None), @@ -198,6 +199,9 @@ fn text_to_vector( let vectors = to_path(text, &font, typesetting, separate_glyphs); let transform = strings.attribute_cloned_or_default::(ATTR_TRANSFORM, index); let layer_path = strings.attribute_cloned_or_default::>(ATTR_EDITOR_LAYER_PATH, index); + let blend_mode = strings.attribute::(ATTR_BLEND_MODE, index).copied(); + let opacity = strings.attribute::(ATTR_OPACITY, index).copied(); + let opacity_fill = strings.attribute::(ATTR_OPACITY_FILL, index).copied(); for mut item in vectors.into_iter() { if transform != glam::DAffine2::IDENTITY { @@ -207,6 +211,15 @@ fn text_to_vector( if !layer_path.is_empty() { item.set_attribute(ATTR_EDITOR_LAYER_PATH, layer_path.clone()); } + if let Some(blend_mode) = blend_mode { + item.set_attribute(ATTR_BLEND_MODE, blend_mode); + } + if let Some(opacity) = opacity { + item.set_attribute(ATTR_OPACITY, opacity); + } + if let Some(opacity_fill) = opacity_fill { + item.set_attribute(ATTR_OPACITY_FILL, opacity_fill); + } result.push(item); } } diff --git a/node-graph/nodes/text/src/text_context.rs b/node-graph/nodes/text/src/text_context.rs index e5944245d4..0a133a1754 100644 --- a/node-graph/nodes/text/src/text_context.rs +++ b/node-graph/nodes/text/src/text_context.rs @@ -54,7 +54,7 @@ impl TextContext { } /// Create a text layout from the given font resource and typesetting configuration. - fn layout_text(&mut self, text: &str, font: &Resource, typesetting: TypesettingConfig) -> Option> { + pub fn layout_text(&mut self, text: &str, font: &Resource, typesetting: TypesettingConfig) -> Option> { let (font_family, font_info) = self.get_font_info(font)?; const DISPLAY_SCALE: f32 = 1.; diff --git a/node-graph/nodes/transform/src/transform_nodes.rs b/node-graph/nodes/transform/src/transform_nodes.rs index bf5b2f6488..8ed04f2cfe 100644 --- a/node-graph/nodes/transform/src/transform_nodes.rs +++ b/node-graph/nodes/transform/src/transform_nodes.rs @@ -17,6 +17,7 @@ async fn transform( Context -> DAffine2, Context -> DVec2, Context -> List, + Context -> List, Context -> List, Context -> List>, Context -> List>, From d06c825521cd169ebf43a638addb4c4aede90431 Mon Sep 17 00:00:00 2001 From: Annonnymmousss Date: Sat, 6 Jun 2026 03:12:41 +0530 Subject: [PATCH 11/12] chore: add text_layer node to text tool for testing --- .../document/graph_operation/utility_types.rs | 12 ++------ .../common_functionality/utility_functions.rs | 4 +-- .../messages/tool/tool_messages/text_tool.rs | 30 +++++++++---------- 3 files changed, 19 insertions(+), 27 deletions(-) diff --git a/editor/src/messages/portfolio/document/graph_operation/utility_types.rs b/editor/src/messages/portfolio/document/graph_operation/utility_types.rs index 202bb7ba9e..9005ce234b 100644 --- a/editor/src/messages/portfolio/document/graph_operation/utility_types.rs +++ b/editor/src/messages/portfolio/document/graph_operation/utility_types.rs @@ -250,8 +250,8 @@ impl<'a> ModifyInputsContext<'a> { pub fn insert_text(&mut self, text: String, font: Font, typesetting: TypesettingConfig, layer: LayerNodeIdentifier) { let font_resource_id = ResourceId::new(); - let text = resolve_proto_node_type(graphene_std::text::text::IDENTIFIER) - .expect("Text node does not exist") + let text = resolve_proto_node_type(graphene_std::text::text_layer::IDENTIFIER) + .expect("Text Layer node does not exist") .node_template_input_override([ Some(NodeInput::value(TaggedValue::None, false)), Some(NodeInput::value(TaggedValue::String(text), false)), @@ -265,14 +265,10 @@ impl<'a> ModifyInputsContext<'a> { Some(NodeInput::value(TaggedValue::F64(typesetting.max_height.unwrap_or(100.)), false)), Some(NodeInput::value(TaggedValue::F64(typesetting.tilt), false)), Some(NodeInput::value(TaggedValue::TextAlign(typesetting.align), false)), - Some(NodeInput::value(TaggedValue::Bool(false), false)), ]); let transform = resolve_proto_node_type(graphene_std::transform_nodes::transform::IDENTIFIER) .expect("Transform node does not exist") .default_node_template(); - let fill = resolve_proto_node_type(graphene_std::vector_nodes::fill::IDENTIFIER) - .expect("Fill node does not exist") - .default_node_template(); let text_id = NodeId::new(); self.network_interface.insert_node(text_id, text, &[]); @@ -283,10 +279,6 @@ impl<'a> ModifyInputsContext<'a> { let transform_id = NodeId::new(); self.network_interface.insert_node(transform_id, transform, &[]); self.network_interface.move_node_to_chain_start(&transform_id, layer, &[], self.import); - - let fill_id = NodeId::new(); - self.network_interface.insert_node(fill_id, fill, &[]); - self.network_interface.move_node_to_chain_start(&fill_id, layer, &[], self.import); } pub fn insert_color_value(&mut self, color: Color, layer: LayerNodeIdentifier) { diff --git a/editor/src/messages/tool/common_functionality/utility_functions.rs b/editor/src/messages/tool/common_functionality/utility_functions.rs index fb19f4d2b9..e98e25ea01 100644 --- a/editor/src/messages/tool/common_functionality/utility_functions.rs +++ b/editor/src/messages/tool/common_functionality/utility_functions.rs @@ -6,7 +6,7 @@ use crate::messages::portfolio::document::utility_types::document_metadata::Laye use crate::messages::portfolio::document::utility_types::network_interface::{NodeNetworkInterface, OutputConnector}; use crate::messages::portfolio::document::utility_types::transformation::Selected; use crate::messages::prelude::*; -use crate::messages::tool::common_functionality::graph_modification_utils::{NodeGraphLayer, get_text}; +use crate::messages::tool::common_functionality::graph_modification_utils::{NodeGraphLayer, get_text_layer}; use crate::messages::tool::common_functionality::transformation_cage::SelectedEdges; use crate::messages::tool::tool_messages::path_tool::PathOverlayMode; use crate::messages::tool::utility_types::ToolType; @@ -69,7 +69,7 @@ pub fn text_bounding_box(layer: LayerNodeIdentifier, document: &DocumentMessageH } // Fallback: recompute from text content (e.g. layer hasn't rendered yet) - let Some((text, font, typesetting, _)) = get_text(layer, &document.network_interface, fonts, &document.resources) else { + let Some((text, font, typesetting)) = get_text_layer(layer, &document.network_interface, fonts, &document.resources) else { return Quad::from_box([DVec2::ZERO, DVec2::ZERO]); }; let font = fonts.get_resource_or_queue_load(&font, responses); diff --git a/editor/src/messages/tool/tool_messages/text_tool.rs b/editor/src/messages/tool/tool_messages/text_tool.rs index 9e4c4700c2..c0fc865463 100644 --- a/editor/src/messages/tool/tool_messages/text_tool.rs +++ b/editor/src/messages/tool/tool_messages/text_tool.rs @@ -24,7 +24,7 @@ use graphene_std::choice_type::ChoiceTypeStatic; use graphene_std::color::SRGBA8; use graphene_std::renderer::Quad; use graphene_std::text::{Font, TextAlign, TypesettingConfig, lines_clipping}; -use graphene_std::vector::style::{Fill, FillChoice, FillChoiceUI}; +use graphene_std::vector::style::{FillChoice, FillChoiceUI}; use graphene_std::{Color, NodeInputDecleration}; #[derive(Default, ExtractField)] @@ -106,7 +106,7 @@ impl ToolMetadata for TextTool { } fn create_text_widgets(tool: &TextTool, font_catalog: &FontCatalog, document: &DocumentMessageHandler) -> Vec { - let text_node_id = can_edit_selected(document).and_then(|layer| graph_modification_utils::get_text_id(layer, &document.network_interface)); + let text_node_id = can_edit_selected(document).and_then(|layer| graph_modification_utils::get_text_layer_id(layer, &document.network_interface)); let apply_font = move |font: Font| -> Message { match text_node_id { @@ -298,7 +298,7 @@ impl<'a> MessageHandler> for Text ToolMessage::Text(TextToolMessage::UpdateOptions { options }) => options, ToolMessage::Text(TextToolMessage::SelectionChanged) => { if let Some(layer) = can_edit_selected(context.document) - && let Some((_, font, typesetting, _)) = graph_modification_utils::get_text(layer, &context.document.network_interface, context.fonts, &context.document.resources) + && let Some((_, font, typesetting)) = graph_modification_utils::get_text_layer(layer, &context.document.network_interface, context.fonts, &context.document.resources) { self.options.align = typesetting.align; self.options.font_size = typesetting.font_size; @@ -344,7 +344,7 @@ impl<'a> MessageHandler> for Text editing_text.typesetting.font_size = font_size; } if let Some(layer) = can_edit_selected(context.document) - && let Some(node_id) = graph_modification_utils::get_text_id(layer, &context.document.network_interface) + && let Some(node_id) = graph_modification_utils::get_text_layer_id(layer, &context.document.network_interface) { responses.add(NodeGraphMessage::SetInputValue { node_id, @@ -359,7 +359,7 @@ impl<'a> MessageHandler> for Text editing_text.typesetting.align = align; } if let Some(layer) = can_edit_selected(context.document) - && let Some(node_id) = graph_modification_utils::get_text_id(layer, &context.document.network_interface) + && let Some(node_id) = graph_modification_utils::get_text_layer_id(layer, &context.document.network_interface) { responses.add(NodeGraphMessage::SetInputValue { node_id, @@ -516,7 +516,7 @@ impl TextToolData { fn load_layer_text_node(&mut self, document: &DocumentMessageHandler, fonts: &FontsMessageHandler) -> Option<()> { let transform = document.metadata().transform_to_viewport(self.layer); let color = graph_modification_utils::get_fill_color(self.layer, &document.network_interface).unwrap_or(Color::BLACK); - let (text, font, typesetting, _) = graph_modification_utils::get_text(self.layer, &document.network_interface, fonts, &document.resources)?; + let (text, font, typesetting) = graph_modification_utils::get_text_layer(self.layer, &document.network_interface, fonts, &document.resources)?; self.editing_text = Some(EditingText { text: text.clone(), font, @@ -546,7 +546,7 @@ impl TextToolData { responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![self.layer.to_node()] }); // Make the rendered text invisible while editing responses.add(NodeGraphMessage::SetInput { - input_connector: InputConnector::node(graph_modification_utils::get_text_id(self.layer, &document.network_interface).unwrap(), 1), + input_connector: InputConnector::node(graph_modification_utils::get_text_layer_id(self.layer, &document.network_interface).unwrap(), 1), input: NodeInput::value(TaggedValue::String("".to_string()), false), }); responses.add(NodeGraphMessage::RunDocumentGraph); @@ -571,10 +571,10 @@ impl TextToolData { parent: document.new_layer_parent(true), insert_index: 0, }); - responses.add(GraphOperationMessage::FillSet { - layer: self.layer, - fill: if let Some(color) = editing_text.color { Fill::Solid(color) } else { Fill::None }, - }); + // responses.add(GraphOperationMessage::FillSet { + // layer: self.layer, + // fill: if let Some(color) = editing_text.color { Fill::Solid(color) } else { Fill::None }, + // }); let transform = editing_text.transform; self.editing_text = Some(editing_text); @@ -631,7 +631,7 @@ fn can_edit_selected(document: &DocumentMessageHandler) -> Option Date: Sat, 6 Jun 2026 05:45:00 +0530 Subject: [PATCH 12/12] code review --- .../graph_modification_utils.rs | 22 +++++++++---------- .../libraries/graphic-types/src/graphic.rs | 2 +- .../libraries/rendering/src/renderer.rs | 2 +- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs index a8f2547d99..68355ecf07 100644 --- a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs +++ b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs @@ -503,38 +503,38 @@ pub fn get_text_layer<'a>( ) -> Option<(&'a String, Font, TypesettingConfig)> { let inputs = NodeGraphLayer::new(layer, network_interface).find_node_inputs(&DefinitionIdentifier::ProtoNode(graphene_std::text::text_layer::IDENTIFIER))?; - let Some(TaggedValue::String(text)) = &inputs[graphene_std::text::text_layer::TextInput::INDEX].as_value() else { + let Some(TaggedValue::String(text)) = inputs.get(graphene_std::text::text_layer::TextInput::INDEX)?.as_value() else { return None; }; - let font = match &inputs[graphene_std::text::text_layer::FontInput::INDEX].as_value() { + let font = match inputs.get(graphene_std::text::text_layer::FontInput::INDEX)?.as_value() { Some(TaggedValue::Resource(resource_id)) => fonts.id_font(resources, *resource_id).unwrap_or_default(), _ => Font::default(), }; - let Some(&TaggedValue::F64(font_size)) = inputs[graphene_std::text::text_layer::SizeInput::INDEX].as_value() else { + let Some(&TaggedValue::F64(font_size)) = inputs.get(graphene_std::text::text_layer::SizeInput::INDEX)?.as_value() else { return None; }; - let Some(&TaggedValue::F64(line_height_ratio)) = inputs[graphene_std::text::text_layer::LineHeightInput::INDEX].as_value() else { + let Some(&TaggedValue::F64(line_height_ratio)) = inputs.get(graphene_std::text::text_layer::LineHeightInput::INDEX)?.as_value() else { return None; }; - let Some(&TaggedValue::F64(character_spacing)) = inputs[graphene_std::text::text_layer::CharacterSpacingInput::INDEX].as_value() else { + let Some(&TaggedValue::F64(character_spacing)) = inputs.get(graphene_std::text::text_layer::CharacterSpacingInput::INDEX)?.as_value() else { return None; }; - let Some(&TaggedValue::Bool(has_max_width)) = inputs[graphene_std::text::text_layer::HasMaxWidthInput::INDEX].as_value() else { + let Some(&TaggedValue::Bool(has_max_width)) = inputs.get(graphene_std::text::text_layer::HasMaxWidthInput::INDEX)?.as_value() else { return None; }; - let Some(&TaggedValue::F64(max_width)) = inputs[graphene_std::text::text_layer::MaxWidthInput::INDEX].as_value() else { + let Some(&TaggedValue::F64(max_width)) = inputs.get(graphene_std::text::text_layer::MaxWidthInput::INDEX)?.as_value() else { return None; }; - let Some(&TaggedValue::Bool(has_max_height)) = inputs[graphene_std::text::text_layer::HasMaxHeightInput::INDEX].as_value() else { + let Some(&TaggedValue::Bool(has_max_height)) = inputs.get(graphene_std::text::text_layer::HasMaxHeightInput::INDEX)?.as_value() else { return None; }; - let Some(&TaggedValue::F64(max_height)) = inputs[graphene_std::text::text_layer::MaxHeightInput::INDEX].as_value() else { + let Some(&TaggedValue::F64(max_height)) = inputs.get(graphene_std::text::text_layer::MaxHeightInput::INDEX)?.as_value() else { return None; }; - let Some(&TaggedValue::F64(tilt)) = inputs[graphene_std::text::text_layer::TiltInput::INDEX].as_value() else { + let Some(&TaggedValue::F64(tilt)) = inputs.get(graphene_std::text::text_layer::TiltInput::INDEX)?.as_value() else { return None; }; - let Some(&TaggedValue::TextAlign(align)) = inputs[graphene_std::text::text_layer::AlignInput::INDEX].as_value() else { + let Some(&TaggedValue::TextAlign(align)) = inputs.get(graphene_std::text::text_layer::AlignInput::INDEX)?.as_value() else { return None; }; diff --git a/node-graph/libraries/graphic-types/src/graphic.rs b/node-graph/libraries/graphic-types/src/graphic.rs index a70ddb381c..ab1ef11e82 100644 --- a/node-graph/libraries/graphic-types/src/graphic.rs +++ b/node-graph/libraries/graphic-types/src/graphic.rs @@ -421,7 +421,7 @@ impl RenderComplexity for Graphic { Self::RasterGPU(list) => list.render_complexity(), Self::Color(list) => list.render_complexity(), Self::Gradient(list) => list.render_complexity(), - Self::Text(list) => list.len(), + Self::Text(list) => list.render_complexity(), } } } diff --git a/node-graph/libraries/rendering/src/renderer.rs b/node-graph/libraries/rendering/src/renderer.rs index 74b903c48f..2c48b1d58a 100644 --- a/node-graph/libraries/rendering/src/renderer.rs +++ b/node-graph/libraries/rendering/src/renderer.rs @@ -2341,7 +2341,7 @@ impl Render for List { if needs_layer { let blending = peniko::BlendMode::new(blend_mode_attr.to_peniko(), peniko::Compose::SrcOver); let padding = font_size; - let bounds = kurbo::Rect::new(-padding, -padding, layout.full_width() as f64 + padding, layout.height() as f64 + padding); + let bounds = kurbo::Rect::new(-padding, -padding, alignment_width as f64 + padding, layout.height() as f64 + padding); let transformed_bounds = affine.transform_rect_bbox(bounds); scene.push_layer(peniko::Fill::NonZero, blending, opacity, kurbo::Affine::IDENTITY, &transformed_bounds); }