diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index dcffdac794..99026ddcc0 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -286,6 +286,7 @@ impl MessageHandler> for DocumentMes network_interface: &mut self.network_interface, collapsed: &mut self.collapsed, node_graph: &mut self.node_graph_handler, + fonts, }; let mut graph_operation_message_handler = GraphOperationMessageHandler {}; graph_operation_message_handler.process_message(message, responses, context); diff --git a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs index 9c35da3ec7..f172af93a4 100644 --- a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs +++ b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs @@ -21,6 +21,7 @@ pub struct GraphOperationMessageContext<'a> { pub network_interface: &'a mut NodeNetworkInterface, pub collapsed: &'a mut CollapsedLayers, pub node_graph: &'a mut NodeGraphMessageHandler, + pub fonts: &'a FontsMessageHandler, } #[derive(Debug, Clone, PartialEq, Default, serde::Serialize, serde::Deserialize, ExtractField)] @@ -423,7 +424,27 @@ impl MessageHandler> for insert_index, center, } => { - let tree = match usvg::Tree::from_str(&svg, &usvg::Options::default()) { + let mut options = usvg::Options::default(); + options.font_family = graphene_std::consts::DEFAULT_FONT_FAMILY.to_string(); + let mut fontdb = usvg::fontdb::Database::new(); + fontdb.load_system_fonts(); + fontdb.load_font_data(graphene_std::text::FALLBACK_FONT_RESOURCE.to_vec()); + for data in context.fonts.font_data().values() { + fontdb.load_font_data(data.to_vec()); + } + let fallback_family = fontdb + .faces() + .next() + .and_then(|face| face.families.first().map(|(name, _)| name.clone())) + .unwrap_or_else(|| "Source Sans Pro".to_string()); + fontdb.set_sans_serif_family(&fallback_family); + fontdb.set_serif_family(&fallback_family); + fontdb.set_monospace_family(&fallback_family); + fontdb.set_cursive_family(&fallback_family); + fontdb.set_fantasy_family(&fallback_family); + options.fontdb = std::sync::Arc::new(fontdb); + + let tree = match usvg::Tree::from_str(&svg, &options) { Ok(t) => t, Err(e) => { responses.add(DialogMessage::DisplayDialogError { @@ -621,8 +642,9 @@ fn import_usvg_node( } usvg::Node::Text(text) => { let font = Font::new(graphene_std::consts::DEFAULT_FONT_FAMILY.to_string(), graphene_std::consts::DEFAULT_FONT_STYLE.to_string()); - modify_inputs.insert_text(text.chunks().iter().map(|chunk| chunk.text()).collect(), font, TypesettingConfig::default(), layer); + modify_inputs.insert_text(text.chunks().iter().map(|chunk| chunk.text()).collect(), font, usvg_text_typesetting(text), layer); modify_inputs.fill_set(Fill::Solid(Color::BLACK)); + apply_usvg_text_transform(modify_inputs, text); } } } @@ -673,13 +695,45 @@ fn import_usvg_node_inner( } usvg::Node::Text(text) => { let font = Font::new(graphene_std::consts::DEFAULT_FONT_FAMILY.to_string(), graphene_std::consts::DEFAULT_FONT_STYLE.to_string()); - modify_inputs.insert_text(text.chunks().iter().map(|chunk| chunk.text()).collect(), font, TypesettingConfig::default(), layer); + modify_inputs.insert_text(text.chunks().iter().map(|chunk| chunk.text()).collect(), font, usvg_text_typesetting(text), layer); modify_inputs.fill_set(Fill::Solid(Color::BLACK)); + apply_usvg_text_transform(modify_inputs, text); 0 } } } +fn usvg_text_typesetting(text: &usvg::Text) -> TypesettingConfig { + let mut typesetting = TypesettingConfig::default(); + + for span in text.chunks().iter().flat_map(|chunk| chunk.spans()) { + let decoration = span.decoration(); + typesetting.underline |= decoration.underline().is_some(); + typesetting.overline |= decoration.overline().is_some(); + typesetting.strikethrough |= decoration.line_through().is_some(); + } + + if let Some(first_span) = text.chunks().first().and_then(|chunk| chunk.spans().first()) { + typesetting.font_size = first_span.font_size().get() as f64; + } + + typesetting +} + +fn apply_usvg_text_transform(modify_inputs: &mut ModifyInputsContext, text: &usvg::Text) { + let elem_transform = usvg_transform(text.abs_transform()); + let chunk_offset = text.chunks().first().map(|c| DVec2::new(c.x().unwrap_or(0.) as f64, c.y().unwrap_or(0.) as f64)).unwrap_or_default(); + let text_transform = elem_transform * DAffine2::from_translation(chunk_offset); + + if text_transform.abs_diff_eq(DAffine2::IDENTITY, 1e-6) { + return; + } + // `insert_text` always creates a Transform node; update it in-place. + if let Some(transform_node_id) = modify_inputs.existing_proto_node_id(graphene_std::transform_nodes::transform::IDENTIFIER, false) { + transform_utils::update_transform(modify_inputs.network_interface, &transform_node_id, text_transform); + } +} + /// Helper to apply path data (vector geometry, fill, stroke, transform) to a layer. fn import_usvg_path(modify_inputs: &mut ModifyInputsContext, node: &usvg::Node, path: &usvg::Path, layer: LayerNodeIdentifier, graphite_gradient_stops: &HashMap) { let subpaths = convert_usvg_path(path); 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 fe9ac97284..3f44dbf576 100644 --- a/editor/src/messages/portfolio/document/graph_operation/utility_types.rs +++ b/editor/src/messages/portfolio/document/graph_operation/utility_types.rs @@ -266,6 +266,9 @@ impl<'a> ModifyInputsContext<'a> { Some(NodeInput::value(TaggedValue::Bool(typesetting.max_height.is_some()), false)), Some(NodeInput::value(TaggedValue::F64(typesetting.max_height.unwrap_or(100.)), false)), Some(NodeInput::value(TaggedValue::TextAlign(typesetting.align), false)), + Some(NodeInput::value(TaggedValue::Bool(typesetting.underline), false)), + Some(NodeInput::value(TaggedValue::Bool(typesetting.overline), false)), + Some(NodeInput::value(TaggedValue::Bool(typesetting.strikethrough), false)), ]); let text_to_vector = resolve_proto_node_type(graphene_std::text::text_to_vector::IDENTIFIER) .expect("Text to Vector node does not exist") diff --git a/editor/src/messages/portfolio/document/overlays/utility_functions.rs b/editor/src/messages/portfolio/document/overlays/utility_functions.rs index 57315f58d3..6e813e90a8 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_functions.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_functions.rs @@ -233,6 +233,9 @@ pub fn text_width(text: &str, font_size: f64) -> f64 { max_width: None, max_height: None, align: TextAlign::AlignLeft, + underline: false, + overline: false, + strikethrough: false, }; let mut text_context = GLOBAL_TEXT_CONTEXT.lock().expect("Failed to lock global text context"); diff --git a/editor/src/messages/portfolio/document/overlays/utility_types_native.rs b/editor/src/messages/portfolio/document/overlays/utility_types_native.rs index 743953d439..59b9d57562 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types_native.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types_native.rs @@ -1114,6 +1114,9 @@ impl OverlayContextInternal { max_width: None, max_height: None, align: TextAlign::AlignLeft, + underline: false, + overline: false, + strikethrough: false, }; // Get text dimensions directly from layout diff --git a/editor/src/messages/portfolio/document_migration.rs b/editor/src/messages/portfolio/document_migration.rs index 3580fc3ea7..c78e757dec 100644 --- a/editor/src/messages/portfolio/document_migration.rs +++ b/editor/src/messages/portfolio/document_migration.rs @@ -1715,6 +1715,35 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId], inputs_count = 13; } + // Insert text decoration parameters: underline, overline, and strikethrough. + if reference == DefinitionIdentifier::ProtoNode(graphene_std::text::text::IDENTIFIER) && inputs_count == 13 { + let mut template: NodeTemplate = resolve_document_node_type(&reference)?.default_node_template(); + document.network_interface.replace_implementation(node_id, network_path, &mut template); + let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut template)?; + + #[allow(clippy::needless_range_loop)] + for i in 0..=11 { + document.network_interface.set_input(&InputConnector::node(*node_id, i), old_inputs[i].clone(), network_path); + } + + document.network_interface.set_input( + &InputConnector::node(*node_id, 12), + NodeInput::value(TaggedValue::Bool(TypesettingConfig::default().underline), false), + network_path, + ); + document.network_interface.set_input( + &InputConnector::node(*node_id, 13), + NodeInput::value(TaggedValue::Bool(TypesettingConfig::default().overline), false), + network_path, + ); + document.network_interface.set_input( + &InputConnector::node(*node_id, 14), + NodeInput::value(TaggedValue::Bool(TypesettingConfig::default().strikethrough), false), + network_path, + ); + document.network_interface.set_input(&InputConnector::node(*node_id, 15), old_inputs[12].clone(), network_path); + } + // Upgrade Sine, Cosine, and Tangent nodes to include a boolean input for whether the output should be in radians, which was previously the only option but is now not the default if inputs_count == 1 && (reference == DefinitionIdentifier::ProtoNode(graphene_std::math_nodes::sine::IDENTIFIER) diff --git a/editor/src/messages/portfolio/fonts/fonts_message_handler.rs b/editor/src/messages/portfolio/fonts/fonts_message_handler.rs index fe16ab4775..000f2d039a 100644 --- a/editor/src/messages/portfolio/fonts/fonts_message_handler.rs +++ b/editor/src/messages/portfolio/fonts/fonts_message_handler.rs @@ -110,6 +110,10 @@ impl FontsMessageHandler { self.font_hashes.values().copied().chain(self.font_data.keys().copied()) } + pub fn font_data(&self) -> &HashMap { + &self.font_data + } + fn normalize(&self, font: Font) -> Font { self.font_catalog.normalize(font) } 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 cac806fa1a..a5e9dae66a 100644 --- a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs +++ b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs @@ -571,6 +571,15 @@ pub fn get_text<'a>( let Some(&TaggedValue::TextAlign(align)) = inputs.get(graphene_std::text::text::AlignInput::INDEX)?.as_value() else { return None; }; + let Some(&TaggedValue::Bool(underline)) = inputs.get(graphene_std::text::text::UnderlineInput::INDEX)?.as_value() else { + return None; + }; + let Some(&TaggedValue::Bool(overline)) = inputs.get(graphene_std::text::text::OverlineInput::INDEX)?.as_value() else { + return None; + }; + let Some(&TaggedValue::Bool(strikethrough)) = inputs.get(graphene_std::text::text::StrikethroughInput::INDEX)?.as_value() else { + return None; + }; let typesetting = TypesettingConfig { font_size, @@ -580,6 +589,9 @@ pub fn get_text<'a>( max_width: has_max_width.then_some(max_width), max_height: has_max_height.then_some(max_height), align, + underline, + overline, + strikethrough, }; Some((text, font, typesetting)) } diff --git a/node-graph/libraries/core-types/src/lib.rs b/node-graph/libraries/core-types/src/lib.rs index 878e7f5360..04b9a25e78 100644 --- a/node-graph/libraries/core-types/src/lib.rs +++ b/node-graph/libraries/core-types/src/lib.rs @@ -26,7 +26,7 @@ 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, ATTR_FONT_SIZE, ATTR_GRADIENT_TYPE, ATTR_LETTER_SPACING, ATTR_LETTER_TILT, ATTR_LINE_HEIGHT, ATTR_LOCATION, ATTR_MAX_HEIGHT, ATTR_MAX_WIDTH, ATTR_NAME, ATTR_OPACITY, ATTR_OPACITY_FILL, - ATTR_SPREAD_METHOD, ATTR_START, ATTR_TEXT_ALIGN, ATTR_TRANSFORM, ATTR_TYPE, + ATTR_OVERLINE, ATTR_SPREAD_METHOD, ATTR_START, ATTR_STRIKETHROUGH, ATTR_TEXT_ALIGN, ATTR_TRANSFORM, ATTR_TYPE, ATTR_UNDERLINE, }; 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 bd4780e3a7..a6630f5142 100644 --- a/node-graph/libraries/core-types/src/list.rs +++ b/node-graph/libraries/core-types/src/list.rs @@ -78,6 +78,12 @@ pub const ATTR_MAX_HEIGHT: &str = "max_height"; pub const ATTR_LETTER_TILT: &str = "letter_tilt"; /// Text item's `TextAlign` horizontal alignment of lines within the block. pub const ATTR_TEXT_ALIGN: &str = "text_align"; +/// Text item's underline enabled status (`bool`, implicit default `false`). +pub const ATTR_UNDERLINE: &str = "underline"; +/// Text item's overline enabled status (`bool`, implicit default `false`). +pub const ATTR_OVERLINE: &str = "overline"; +/// Text item's strikethrough enabled status (`bool`, implicit default `false`). +pub const ATTR_STRIKETHROUGH: &str = "strikethrough"; // =========================== // Implicit attribute defaults diff --git a/node-graph/libraries/rendering/src/renderer.rs b/node-graph/libraries/rendering/src/renderer.rs index 58fde12096..9ffbeea525 100644 --- a/node-graph/libraries/rendering/src/renderer.rs +++ b/node-graph/libraries/rendering/src/renderer.rs @@ -14,8 +14,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, - ATTR_FONT_SIZE, ATTR_GRADIENT_TYPE, ATTR_LETTER_SPACING, ATTR_LETTER_TILT, ATTR_LINE_HEIGHT, ATTR_LOCATION, ATTR_MAX_HEIGHT, ATTR_MAX_WIDTH, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_SPREAD_METHOD, - ATTR_TEXT_ALIGN, ATTR_TRANSFORM, + ATTR_FONT_SIZE, ATTR_GRADIENT_TYPE, ATTR_LETTER_SPACING, ATTR_LETTER_TILT, ATTR_LINE_HEIGHT, ATTR_LOCATION, ATTR_MAX_HEIGHT, ATTR_MAX_WIDTH, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_OVERLINE, + ATTR_SPREAD_METHOD, ATTR_STRIKETHROUGH, ATTR_TEXT_ALIGN, ATTR_TRANSFORM, ATTR_UNDERLINE, }; use dyn_any::DynAny; use glam::{DAffine2, DMat2, DVec2}; @@ -2316,6 +2316,9 @@ fn text_item_size_and_transform(list: &List, index: usize) -> Option<(DV let max_height: Option = list.attribute_cloned_or(ATTR_MAX_HEIGHT, index, None); let align: text_nodes::TextAlign = list.attribute_cloned_or_default(ATTR_TEXT_ALIGN, index); let transform: DAffine2 = list.attribute_cloned_or_default(ATTR_TRANSFORM, index); + let underline: bool = list.attribute_cloned_or(ATTR_UNDERLINE, index, false); + let overline: bool = list.attribute_cloned_or(ATTR_OVERLINE, index, false); + let strikethrough: bool = list.attribute_cloned_or(ATTR_STRIKETHROUGH, index, false); let typesetting = text_nodes::TypesettingConfig { font_size, @@ -2325,6 +2328,9 @@ fn text_item_size_and_transform(list: &List, index: usize) -> Option<(DV max_width, max_height, align, + underline, + overline, + strikethrough, }; let (width, height) = text_nodes::TextContext::with_thread_local(|ctx| { @@ -2416,6 +2422,9 @@ impl Render for List { let max_height: Option = self.attribute_cloned_or(ATTR_MAX_HEIGHT, index, None); let letter_tilt: f64 = self.attribute_cloned_or(ATTR_LETTER_TILT, index, 0.); let align: text_nodes::TextAlign = self.attribute_cloned_or_default(ATTR_TEXT_ALIGN, index); + let underline: bool = self.attribute_cloned_or(ATTR_UNDERLINE, index, false); + let overline: bool = self.attribute_cloned_or(ATTR_OVERLINE, index, false); + let strikethrough: bool = self.attribute_cloned_or(ATTR_STRIKETHROUGH, index, false); let opacity = (opacity_attr * if render_params.for_mask { 1. } else { opacity_fill_attr }) as f32; let typesetting = text_nodes::TypesettingConfig { @@ -2426,6 +2435,9 @@ impl Render for List { max_width, max_height, align, + underline, + overline, + strikethrough, }; let mut glyph_paths: Vec = Vec::new(); @@ -2498,6 +2510,9 @@ impl Render for List { let max_height: Option = self.attribute_cloned_or(ATTR_MAX_HEIGHT, index, None); let letter_tilt: f64 = self.attribute_cloned_or(ATTR_LETTER_TILT, index, 0.); let align: text_nodes::TextAlign = self.attribute_cloned_or_default(ATTR_TEXT_ALIGN, index); + let underline: bool = self.attribute_cloned_or(ATTR_UNDERLINE, index, false); + let overline: bool = self.attribute_cloned_or(ATTR_OVERLINE, index, false); + let strikethrough: bool = self.attribute_cloned_or(ATTR_STRIKETHROUGH, index, false); 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.); @@ -2511,6 +2526,9 @@ impl Render for List { max_width, max_height, align, + underline, + overline, + strikethrough, }; let affine = Affine::new((transform * item_transform).to_cols_array()); diff --git a/node-graph/nodes/gstd/src/text.rs b/node-graph/nodes/gstd/src/text.rs index 8f229def0b..c271dc4ee5 100644 --- a/node-graph/nodes/gstd/src/text.rs +++ b/node-graph/nodes/gstd/src/text.rs @@ -1,6 +1,8 @@ use core_types::consts::{DEFAULT_FONT_SIZE, DEFAULT_LINE_HEIGHT}; use core_types::list::List; -use core_types::{ATTR_FONT, ATTR_FONT_SIZE, ATTR_LETTER_SPACING, ATTR_LETTER_TILT, ATTR_LINE_HEIGHT, ATTR_MAX_HEIGHT, ATTR_MAX_WIDTH, ATTR_TEXT_ALIGN, Ctx}; +use core_types::{ + ATTR_FONT, ATTR_FONT_SIZE, ATTR_LETTER_SPACING, ATTR_LETTER_TILT, ATTR_LINE_HEIGHT, ATTR_MAX_HEIGHT, ATTR_MAX_WIDTH, ATTR_OVERLINE, ATTR_STRIKETHROUGH, ATTR_TEXT_ALIGN, ATTR_UNDERLINE, Ctx, +}; use graph_craft::application_io::resource::Resource; use graphic_types::Vector; pub use text_nodes::*; @@ -60,6 +62,9 @@ fn text( /// The horizontal alignment of each line of text within its surrounding box. To have an effect on a single line of text, *Max Width* must be set. #[widget(ParsedWidgetOverride::Custom = "text_align")] align: TextAlign, + underline: bool, + overline: bool, + strikethrough: bool, ) -> List { let mut list = List::new_from_element(text); @@ -87,6 +92,15 @@ fn text( if align != TextAlign::default() { list.set_attribute(ATTR_TEXT_ALIGN, 0, align); } + if underline { + list.set_attribute(ATTR_UNDERLINE, 0, underline); + } + if overline { + list.set_attribute(ATTR_OVERLINE, 0, overline); + } + if strikethrough { + list.set_attribute(ATTR_STRIKETHROUGH, 0, strikethrough); + } list } diff --git a/node-graph/nodes/text/src/lib.rs b/node-graph/nodes/text/src/lib.rs index 742e12cb04..8c49e8997c 100644 --- a/node-graph/nodes/text/src/lib.rs +++ b/node-graph/nodes/text/src/lib.rs @@ -99,6 +99,9 @@ pub struct TypesettingConfig { pub max_width: Option, pub max_height: Option, pub align: TextAlign, + pub underline: bool, + pub overline: bool, + pub strikethrough: bool, } impl Default for TypesettingConfig { @@ -111,6 +114,9 @@ impl Default for TypesettingConfig { max_width: None, max_height: None, align: TextAlign::default(), + underline: false, + overline: false, + strikethrough: false, } } } diff --git a/node-graph/nodes/text/src/path_builder.rs b/node-graph/nodes/text/src/path_builder.rs index 740d120a0e..599694f64a 100644 --- a/node-graph/nodes/text/src/path_builder.rs +++ b/node-graph/nodes/text/src/path_builder.rs @@ -163,6 +163,41 @@ impl PathBuilder { } } + pub fn render_decoration_run(&mut self, glyph_run: &GlyphRun<'_, ()>, underline: bool, overline: bool, strikethrough: bool, per_glyph_items: bool) { + if !underline && !overline && !strikethrough { + return; + } + + let run = glyph_run.run(); + let baseline = glyph_run.baseline() as f64; + let metrics = run.metrics(); + let start = glyph_run.offset() as f64; + let end = start + glyph_run.advance() as f64; + + let decorations = [ + (underline, baseline - metrics.underline_offset as f64, metrics.underline_size as f64), + (overline, baseline - metrics.ascent as f64, metrics.underline_size as f64), + (strikethrough, baseline - metrics.strikethrough_offset as f64, metrics.strikethrough_size as f64), + ]; + + for (_, y, thickness) in decorations.into_iter().filter(|(enabled, _, _)| *enabled) { + let thickness = thickness.max(1.); + if per_glyph_items { + let translation = DVec2::new(start, y); + let frame = DAffine2::from_scale_angle_translation(self.text_frame_size, 0., -translation); + let rect = Subpath::new_rectangle(DVec2::ZERO, DVec2::new(end - start, thickness) * self.scale); + let item = Item::new_from_element(Vector::from_subpaths([rect], false)) + .with_attribute(ATTR_TRANSFORM, DAffine2::from_translation(translation)) + .with_attribute(ATTR_EDITOR_TEXT_FRAME, frame); + self.vector_list.push(item); + self.per_glyph_bboxes.push(None); + } else { + let rect = Subpath::new_rectangle(DVec2::new(start, y) * self.scale, DVec2::new(end, y + thickness) * self.scale); + self.vector_list.element_mut(0).unwrap().append_subpath(rect, false); + } + } + } + pub fn finalize(mut self) -> List { // Empty list = all glyphs clipped by height. Create a placeholder with the same item-0 // transform a populated list would have so `local_transforms` stays stable mid-drag. diff --git a/node-graph/nodes/text/src/text_context.rs b/node-graph/nodes/text/src/text_context.rs index c582035357..f2250da4c1 100644 --- a/node-graph/nodes/text/src/text_context.rs +++ b/node-graph/nodes/text/src/text_context.rs @@ -155,7 +155,9 @@ impl TextContext { let mut path_builder = PathBuilder::new(per_glyph_items, layout.scale() as f64, text_frame_size, first_glyph_offset); for_each_styled_glyph_run(&layout, text, typesetting, |glyph_run, x_offset, space_extra| { + path_builder.render_decoration_run(glyph_run, typesetting.underline, typesetting.overline, false, per_glyph_items); path_builder.render_glyph_run(glyph_run, typesetting.letter_tilt, per_glyph_items, x_offset, space_extra); + path_builder.render_decoration_run(glyph_run, false, false, typesetting.strikethrough, per_glyph_items); }); path_builder.finalize() diff --git a/node-graph/nodes/text/src/to_path.rs b/node-graph/nodes/text/src/to_path.rs index 1ffc20df62..f654a64036 100644 --- a/node-graph/nodes/text/src/to_path.rs +++ b/node-graph/nodes/text/src/to_path.rs @@ -5,7 +5,7 @@ use core_types::list::List; use core_types::uuid::NodeId; use core_types::{ ATTR_BLEND_MODE, ATTR_EDITOR_LAYER_PATH, ATTR_FONT, ATTR_FONT_SIZE, ATTR_LETTER_SPACING, ATTR_LETTER_TILT, ATTR_LINE_HEIGHT, ATTR_MAX_HEIGHT, ATTR_MAX_WIDTH, ATTR_OPACITY, ATTR_OPACITY_FILL, - ATTR_TEXT_ALIGN, ATTR_TRANSFORM, + ATTR_OVERLINE, ATTR_STRIKETHROUGH, ATTR_TEXT_ALIGN, ATTR_TRANSFORM, ATTR_UNDERLINE, }; use glam::{DAffine2, DVec2}; use graphene_resource::Resource; @@ -50,6 +50,9 @@ pub fn shape_text_list(strings: &List, separate_glyphs: bool) -> List>(ATTR_MAX_WIDTH, index, defaults.max_width), max_height: strings.attribute_cloned_or::>(ATTR_MAX_HEIGHT, index, defaults.max_height), align: strings.attribute_cloned_or(ATTR_TEXT_ALIGN, index, defaults.align), + underline: strings.attribute_cloned_or(ATTR_UNDERLINE, index, defaults.underline), + overline: strings.attribute_cloned_or(ATTR_OVERLINE, index, defaults.overline), + strikethrough: strings.attribute_cloned_or(ATTR_STRIKETHROUGH, index, defaults.strikethrough), }; let vectors = to_path(text, &font, typesetting, separate_glyphs);