From 915f8ff9071865415af54a9a5bb8d24042dd7b33 Mon Sep 17 00:00:00 2001 From: Kulratan Date: Wed, 12 Aug 2026 11:36:37 +0000 Subject: [PATCH] rebase --- .../document/document_message_handler.rs | 1 + .../graph_operation_message_handler.rs | 60 ++++++++++++++++++- .../document/graph_operation/utility_types.rs | 3 + .../document/overlays/utility_types_native.rs | 3 + .../document/utility_types/text_metrics.rs | 3 + .../messages/portfolio/document_migration.rs | 31 ++++++++++ .../portfolio/fonts/fonts_message_handler.rs | 4 ++ .../graph_modification_utils.rs | 10 ++++ node-graph/libraries/core-types/src/lib.rs | 3 +- node-graph/libraries/core-types/src/list.rs | 6 ++ .../libraries/rendering/src/renderer.rs | 22 ++++++- node-graph/nodes/gstd/src/text.rs | 22 ++++++- node-graph/nodes/text/src/lib.rs | 6 ++ node-graph/nodes/text/src/path_builder.rs | 43 +++++++++++++ node-graph/nodes/text/src/text_context.rs | 2 + node-graph/nodes/text/src/to_path.rs | 5 +- 16 files changed, 215 insertions(+), 9 deletions(-) diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index 5908d70c64..7d65e4bfe8 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -293,6 +293,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 aa2dc553b1..a51703d433 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)] @@ -472,7 +473,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(|| graphene_std::consts::DEFAULT_FONT_FAMILY.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 { @@ -784,8 +805,9 @@ fn import_usvg_node(modify_inputs: &mut ModifyInputsContext, node: &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_color_set(Some(Color::BLACK)); + apply_usvg_text_transform(modify_inputs, text); } } } @@ -836,13 +858,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_color_set(Some(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, gradient_info: &SvgGradientInfo) { 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 d792346fe0..27871073f8 100644 --- a/editor/src/messages/portfolio/document/graph_operation/utility_types.rs +++ b/editor/src/messages/portfolio/document/graph_operation/utility_types.rs @@ -203,6 +203,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_types_native.rs b/editor/src/messages/portfolio/document/overlays/utility_types_native.rs index 1781aa9105..51a723e010 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types_native.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types_native.rs @@ -1115,6 +1115,9 @@ impl OverlayContextInternal { max_width: None, max_height: None, align: TextAlign::AlignLeft, + underline: false, + overline: false, + strikethrough: false, }; // Lay out the text once, taking its dimensions and vector paths from the same thread-local context pass diff --git a/editor/src/messages/portfolio/document/utility_types/text_metrics.rs b/editor/src/messages/portfolio/document/utility_types/text_metrics.rs index 8449988edb..311495a8d9 100644 --- a/editor/src/messages/portfolio/document/utility_types/text_metrics.rs +++ b/editor/src/messages/portfolio/document/utility_types/text_metrics.rs @@ -10,6 +10,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, }; TextContext::with_thread_local(|text_context| text_context.bounding_box(text, &FALLBACK_FONT_RESOURCE, typesetting, false).x) diff --git a/editor/src/messages/portfolio/document_migration.rs b/editor/src/messages/portfolio/document_migration.rs index 7aed7ebf49..4a3a45ee73 100644 --- a/editor/src/messages/portfolio/document_migration.rs +++ b/editor/src/messages/portfolio/document_migration.rs @@ -2048,6 +2048,37 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId], inputs_count = 13; } + // Insert text decoration parameters: underline, overline, and strikethrough. + // Currently text node has 15 inputs (0–14): the three decoration booleans are appended at 12/13/14. + if reference == DefinitionIdentifier::ProtoNode(graphene_std::text::text::IDENTIFIER) && inputs_count == 12 { + 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)?; + + // Copy all original inputs (including `align` at index 11) into the new node unchanged. + #[allow(clippy::needless_range_loop)] + for i in 0..=11 { + document.network_interface.set_input(&InputConnector::node_at_index(*node_id, i), old_inputs[i].clone(), network_path); + } + + // Append the three new decoration inputs at their correct indices (12, 13, 14) with defaults. + document.network_interface.set_input( + &InputConnector::node_at_index(*node_id, 12), + NodeInput::value(TaggedValue::Bool(TypesettingConfig::default().underline), false), + network_path, + ); + document.network_interface.set_input( + &InputConnector::node_at_index(*node_id, 13), + NodeInput::value(TaggedValue::Bool(TypesettingConfig::default().overline), false), + network_path, + ); + document.network_interface.set_input( + &InputConnector::node_at_index(*node_id, 14), + NodeInput::value(TaggedValue::Bool(TypesettingConfig::default().strikethrough), false), + 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 dfc0bbe702..b9c124e485 100644 --- a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs +++ b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs @@ -637,6 +637,13 @@ pub fn get_text<'a>( return None; }; let Some(&TaggedValue::TextAlign(align)) = parameters.value(text::AlignInput) else { return None }; + let Some(&TaggedValue::Bool(underline)) = parameters.value(text::UnderlineInput) else { + return None; + }; + let Some(&TaggedValue::Bool(overline)) = parameters.value(text::OverlineInput) else { return None }; + let Some(&TaggedValue::Bool(strikethrough)) = parameters.value(text::StrikethroughInput) else { + return None; + }; let typesetting = TypesettingConfig { font_size, @@ -646,6 +653,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 5cf3ffaf66..06af2a3c52 100644 --- a/node-graph/libraries/core-types/src/lib.rs +++ b/node-graph/libraries/core-types/src/lib.rs @@ -26,7 +26,8 @@ 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_CYCLIC, ATTR_GRADIENT_FORM, ATTR_GRADIENT_HUE_DIRECTION, ATTR_GRADIENT_INTERPOLATION, ATTR_GRADIENT_SPACE, ATTR_GRADIENT_SPREAD, 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_START, ATTR_TEXT_ALIGN, ATTR_TRANSFORM, ATTR_TYPE, + ATTR_LETTER_TILT, ATTR_LINE_HEIGHT, ATTR_LOCATION, ATTR_MAX_HEIGHT, ATTR_MAX_WIDTH, ATTR_NAME, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_OVERLINE, 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 6cdfda3920..a757fb587d 100644 --- a/node-graph/libraries/core-types/src/list.rs +++ b/node-graph/libraries/core-types/src/list.rs @@ -98,6 +98,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"; // ===================== // TYPE: NodeIdPath diff --git a/node-graph/libraries/rendering/src/renderer.rs b/node-graph/libraries/rendering/src/renderer.rs index 274b8cb648..69b2a0f6ec 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_FORM, ATTR_LETTER_SPACING, ATTR_LETTER_TILT, ATTR_LINE_HEIGHT, ATTR_LOCATION, ATTR_MAX_HEIGHT, ATTR_MAX_WIDTH, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_TEXT_ALIGN, - ATTR_TRANSFORM, + ATTR_FONT_SIZE, ATTR_GRADIENT_FORM, 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_STRIKETHROUGH, ATTR_TEXT_ALIGN, ATTR_TRANSFORM, ATTR_UNDERLINE, }; use dyn_any::DynAny; use glam::{DAffine2, DMat2, DVec2}; @@ -2581,6 +2581,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, @@ -2590,6 +2593,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| { @@ -2681,6 +2687,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 { @@ -2691,6 +2700,9 @@ impl Render for List { max_width, max_height, align, + underline, + overline, + strikethrough, }; let mut glyph_paths: Vec = Vec::new(); @@ -2763,6 +2775,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.); @@ -2776,6 +2791,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 e58f16f590..9956669d06 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::{Item, 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::*; @@ -59,12 +61,19 @@ 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: Item, + /// Draws a line below each line of text at the font's underline position. + underline: Item, + /// Draws a line above each line of text at the font's ascent position. + overline: Item, + /// Draws a line through the middle of each line of text at the font's strikethrough position. + strikethrough: Item, ) -> Item { let text = text.into_element(); let font = font.into_element(); let (size, line_height, letter_spacing, letter_tilt) = (*size.element(), *line_height.element(), *letter_spacing.element(), *letter_tilt.element()); - let (has_max_width, max_width, has_max_height, max_height) = (*has_max_width.element(), *max_width.element(), *has_max_height.element(), *max_height.element()); + let (has_max_width, max_width, has_max_height, max_height) = (*has_max_width.element(), *max_width.element(), *has_max_height.element(), *has_max_height.element()); let align = align.into_element(); + let (underline, overline, strikethrough) = (*underline.element(), *overline.element(), *strikethrough.element()); let mut item = Item::new_from_element(text); @@ -92,6 +101,15 @@ fn text( if align != TextAlign::default() { item.set_attribute(ATTR_TEXT_ALIGN, align); } + if underline { + item.set_attribute(ATTR_UNDERLINE, underline); + } + if overline { + item.set_attribute(ATTR_OVERLINE, overline); + } + if strikethrough { + item.set_attribute(ATTR_STRIKETHROUGH, strikethrough); + } item } diff --git a/node-graph/nodes/text/src/lib.rs b/node-graph/nodes/text/src/lib.rs index 6dd4bd6e57..ef7a3c4971 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 facf154c86..7a13f0195c 100644 --- a/node-graph/nodes/text/src/path_builder.rs +++ b/node-graph/nodes/text/src/path_builder.rs @@ -163,6 +163,49 @@ impl PathBuilder { } } + pub fn render_decoration_run(&mut self, glyph_run: &GlyphRun<'_, ()>, underline: bool, overline: bool, strikethrough: bool, per_glyph_items: bool, x_offset: f32, space_extra: f32) { + if !underline && !overline && !strikethrough { + return; + } + + let run = glyph_run.run(); + let baseline = glyph_run.baseline() as f64; + let metrics = run.metrics(); + + // Apply the same alignment shift and justification stretch that render_glyph_run applies to + // glyph placement, so decoration lines stay aligned under non-left-aligned text. + let start = (glyph_run.offset() + x_offset) as f64; + let extra_advance: f32 = if space_extra != 0. { + glyph_run.glyphs().filter(|g| g.advance > 0.).count().saturating_sub(1) as f32 * space_extra + } else { + 0. + }; + let end = start + (glyph_run.advance() + extra_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..c2f3c9b911 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, x_offset, space_extra); 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, x_offset, space_extra); }); path_builder.finalize() diff --git a/node-graph/nodes/text/src/to_path.rs b/node-graph/nodes/text/src/to_path.rs index 0aca43caa4..08c585a2c7 100644 --- a/node-graph/nodes/text/src/to_path.rs +++ b/node-graph/nodes/text/src/to_path.rs @@ -4,7 +4,7 @@ use core_types::blending::BlendMode; use core_types::list::{Item, List, NodeIdPath}; 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; @@ -46,6 +46,9 @@ pub fn shape_text_item(item: &Item, separate_glyphs: bool) -> List>(ATTR_MAX_WIDTH, defaults.max_width), max_height: item.attribute_cloned_or::>(ATTR_MAX_HEIGHT, defaults.max_height), align: item.attribute_cloned_or(ATTR_TEXT_ALIGN, defaults.align), + underline: item.attribute_cloned_or(ATTR_UNDERLINE, defaults.underline), + overline: item.attribute_cloned_or(ATTR_OVERLINE, defaults.overline), + strikethrough: item.attribute_cloned_or(ATTR_STRIKETHROUGH, defaults.strikethrough), }; let vectors = to_path(text, &font, typesetting, separate_glyphs);