Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,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)]
Expand Down Expand Up @@ -502,7 +503,27 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageContext<'_>> 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 {
Expand Down Expand Up @@ -814,8 +835,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);
}
}
}
Expand Down Expand Up @@ -866,8 +888,9 @@ 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
}
}
Expand All @@ -889,6 +912,37 @@ fn insert_brush_strokes_chain(network_interface: &mut NodeNetworkInterface, laye
network_interface.set_input(&InputConnector::node_at_index(layer.to_node(), 1), NodeInput::node(strokes_node_id, 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 bezpath = convert_usvg_path(path);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1089,6 +1089,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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
31 changes: 31 additions & 0 deletions editor/src/messages/portfolio/document_migration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2016,6 +2016,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)
Expand Down
4 changes: 4 additions & 0 deletions editor/src/messages/portfolio/fonts/fonts_message_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,10 @@ impl FontsMessageHandler {
self.font_hashes.values().copied().chain(self.font_data.keys().copied())
}

pub fn font_data(&self) -> &HashMap<ResourceHash, Resource> {
&self.font_data
}

fn normalize(&self, font: Font) -> Font {
self.font_catalog.normalize(font)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -627,6 +627,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,
Expand All @@ -636,6 +643,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))
}
Expand Down
3 changes: 2 additions & 1 deletion node-graph/libraries/core-types/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,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;
Expand Down
6 changes: 6 additions & 0 deletions node-graph/libraries/core-types/src/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,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
Expand Down
10 changes: 8 additions & 2 deletions node-graph/libraries/rendering/src/renderer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,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};
Expand Down Expand Up @@ -3040,6 +3040,9 @@ fn text_item_size_and_transform(item: ItemRef<'_, String>) -> Option<(DVec2, DAf
let max_height: Option<f64> = item.attribute_cloned_or(ATTR_MAX_HEIGHT, None);
let align: text_nodes::TextAlign = item.attribute_cloned_or_default(ATTR_TEXT_ALIGN);
let transform: DAffine2 = item.attribute_cloned_or_default(ATTR_TRANSFORM);
let underline: bool = item.attribute_cloned_or(ATTR_UNDERLINE, false);
let overline: bool = item.attribute_cloned_or(ATTR_OVERLINE, false);
let strikethrough: bool = item.attribute_cloned_or(ATTR_STRIKETHROUGH, false);

let typesetting = text_nodes::TypesettingConfig {
font_size,
Expand All @@ -3049,6 +3052,9 @@ fn text_item_size_and_transform(item: ItemRef<'_, String>) -> Option<(DVec2, DAf
max_width,
max_height,
align,
underline,
overline,
strikethrough,
};

let (width, height) = text_nodes::TextContext::with_thread_local(|ctx| {
Expand Down
22 changes: 20 additions & 2 deletions node-graph/nodes/gstd/src/text.rs
Original file line number Diff line number Diff line change
@@ -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::*;
Expand Down Expand Up @@ -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<TextAlign>,
/// Draws a line below each line of text at the font's underline position.
underline: Item<bool>,
/// Draws a line above each line of text at the font's ascent position.
overline: Item<bool>,
/// Draws a line through the middle of each line of text at the font's strikethrough position.
strikethrough: Item<bool>,
) -> Item<String> {
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);

Expand Down Expand Up @@ -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
}
Expand Down
6 changes: 6 additions & 0 deletions node-graph/nodes/text/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,9 @@ pub struct TypesettingConfig {
pub max_width: Option<f64>,
pub max_height: Option<f64>,
pub align: TextAlign,
pub underline: bool,
pub overline: bool,
pub strikethrough: bool,
}

impl Default for TypesettingConfig {
Expand All @@ -112,6 +115,9 @@ impl Default for TypesettingConfig {
max_width: None,
max_height: None,
align: TextAlign::default(),
underline: false,
overline: false,
strikethrough: false,
}
}
}
Expand Down
43 changes: 43 additions & 0 deletions node-graph/nodes/text/src/path_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,49 @@
}
}

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);

Check failure on line 189 in node-graph/nodes/text/src/path_builder.rs

View workflow job for this annotation

GitHub Actions / test

cannot find type `Subpath` in this scope

Check failure on line 189 in node-graph/nodes/text/src/path_builder.rs

View workflow job for this annotation

GitHub Actions / build / web

cannot find type `Subpath` in this scope
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);

Check failure on line 196 in node-graph/nodes/text/src/path_builder.rs

View workflow job for this annotation

GitHub Actions / test

cannot find type `Subpath` in this scope

Check failure on line 196 in node-graph/nodes/text/src/path_builder.rs

View workflow job for this annotation

GitHub Actions / build / web

cannot find type `Subpath` in this scope
self.vector_list.element_mut(0).unwrap().append_subpath(rect, false);
}
}
}

pub fn finalize(mut self) -> List<Vector> {
// 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.
Expand Down
2 changes: 2 additions & 0 deletions node-graph/nodes/text/src/text_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Loading
Loading