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 @@ -293,6 +293,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 @@ -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)]
Expand Down Expand Up @@ -434,7 +435,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 @@ -632,8 +653,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_color_set(Some(Color::BLACK));
apply_usvg_text_transform(modify_inputs, text);
}
}
}
Expand Down Expand Up @@ -684,13 +706,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, graphite_gradient_stops: &HashMap<String, GradientStops>) {
let subpaths = convert_usvg_path(path);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
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 @@ -1807,6 +1807,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(*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(*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,
);
}

// 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 @@ -508,6 +508,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,
Expand All @@ -517,6 +526,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
2 changes: 1 addition & 1 deletion node-graph/libraries/core-types/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
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 @@ -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
Expand Down
22 changes: 20 additions & 2 deletions node-graph/libraries/rendering/src/renderer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -2314,6 +2314,9 @@ fn text_item_size_and_transform(list: &List<String>, index: usize) -> Option<(DV
let max_height: Option<f64> = 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,
Expand All @@ -2323,6 +2326,9 @@ fn text_item_size_and_transform(list: &List<String>, index: usize) -> Option<(DV
max_width,
max_height,
align,
underline,
overline,
strikethrough,
};

let (width, height) = text_nodes::TextContext::with_thread_local(|ctx| {
Expand Down Expand Up @@ -2414,6 +2420,9 @@ impl Render for List<String> {
let max_height: Option<f64> = 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 {
Expand All @@ -2424,6 +2433,9 @@ impl Render for List<String> {
max_width,
max_height,
align,
underline,
overline,
strikethrough,
};

let mut glyph_paths: Vec<String> = Vec::new();
Expand Down Expand Up @@ -2496,6 +2508,9 @@ impl Render for List<String> {
let max_height: Option<f64> = 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.);
Expand All @@ -2509,6 +2524,9 @@ impl Render for List<String> {
max_width,
max_height,
align,
underline,
overline,
strikethrough,
};

let affine = Affine::new((transform * item_transform).to_cols_array());
Expand Down
19 changes: 18 additions & 1 deletion 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::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,6 +61,12 @@ 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,
/// Draws a line below each line of text at the font's underline position.
underline: bool,
/// Draws a line above each line of text at the font's ascent position.
overline: bool,
/// Draws a line through the middle of each line of text at the font's strikethrough position.
strikethrough: bool,
) -> List<String> {
let mut list = List::new_from_element(text);

Expand Down Expand Up @@ -86,6 +94,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
}
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 @@ -99,6 +99,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 @@ -111,6 +114,9 @@ impl Default for TypesettingConfig {
max_width: None,
max_height: None,
align: TextAlign::default(),
underline: false,
overline: false,
strikethrough: false,
}
}
}
Expand Down
Loading
Loading