Skip to content

Add underline, overline, and strikethrough text decorations to the Text node - #4193

Open
jsjgdh wants to merge 4 commits into
GraphiteEditor:masterfrom
jsjgdh:text-decoration
Open

jsjgdh wants to merge 4 commits into
GraphiteEditor:masterfrom
jsjgdh:text-decoration

Conversation

@jsjgdh

@jsjgdh jsjgdh commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

Text-decoration

@jsjgdh

jsjgdh commented Jun 3, 2026

Copy link
Copy Markdown
Contributor Author

@cubic-dev

@cubic-dev-ai

cubic-dev-ai Bot commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

@cubic-dev

@jsjgdh I have started the AI code review. It will take a few minutes to complete.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces support for text decorations (underline, overline, and strikethrough) in the text rendering pipeline. It updates TypesettingConfig and the text node to support these properties, implements rendering logic in PathBuilder, and updates the USVG importer to extract decorations and text transforms. Additionally, document migration is added to upgrade existing text nodes. The reviewer suggested a performance optimization in render_decoration_run to return early if no decorations are enabled, avoiding unnecessary font metric lookups.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread node-graph/nodes/text/src/path_builder.rs Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 13 files

Confidence score: 4/5

  • This PR looks safe to merge from a functionality standpoint; the reported issue is process-related (PR title wording) rather than a code behavior defect.
  • The most severe finding targets title style compliance, not runtime logic in editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs, so user-facing regression risk appears low.
  • Because the issue has high confidence but non-functional impact, it introduces mild merge uncertainty for workflow/policy compliance rather than product stability.
  • Pay close attention to editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs - ensure no actual graph operation behavior changes were missed while addressing the title-enforcement note.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs">

<violation number="1" location="editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs:17">
P2: Custom agent: **PR title enforcement**

PR title is not written in imperative mood with a leading action verb. "Text-decoration" is a noun phrase and does not begin with an action verb as required by the PR title enforcement rule. Suggested: "Add text decoration support for SVG import" or similar imperative form.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

@@ -14,14 +14,15 @@ use graphene_std::Color;
use graphene_std::renderer::Quad;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Custom agent: PR title enforcement

PR title is not written in imperative mood with a leading action verb. "Text-decoration" is a noun phrase and does not begin with an action verb as required by the PR title enforcement rule. Suggested: "Add text decoration support for SVG import" or similar imperative form.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs, line 17:

<comment>PR title is not written in imperative mood with a leading action verb. "Text-decoration" is a noun phrase and does not begin with an action verb as required by the PR title enforcement rule. Suggested: "Add text decoration support for SVG import" or similar imperative form.</comment>

<file context>
@@ -14,14 +14,15 @@ use graphene_std::Color;
 use graphene_std::renderer::convert_usvg_path::convert_usvg_path;
 use graphene_std::table::Table;
-use graphene_std::text::{Font, TypesettingConfig};
+use graphene_std::text::{Font, FontCache, TypesettingConfig};
 use graphene_std::vector::style::{Fill, Gradient, GradientSpreadMethod, GradientStop, GradientStops, GradientType, PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin};
 
</file context>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Advice title and description.

@jsjgdh

jsjgdh commented Jun 30, 2026

Copy link
Copy Markdown
Contributor Author

@cubic-dev

@cubic-dev-ai

cubic-dev-ai Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

@cubic-dev

@jsjgdh I have started the AI code review. It will take a few minutes to complete.

@jsjgdh
jsjgdh force-pushed the text-decoration branch 2 times, most recently from 4e9c832 to 34c57c1 Compare June 30, 2026 00:21

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found and verified against the latest diff

Confidence score: 4/5

  • In editor/src/messages/portfolio/document/overlays/utility_types_native.rs, the only reported risk is process-level: the PR title does not meet the imperative-format rule, which can cause CI/policy checks to fail and delay or block merge despite no concrete code defect being identified — rename the PR to the required imperative format before merging.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="editor/src/messages/portfolio/document/overlays/utility_types_native.rs">

<violation number="1" location="editor/src/messages/portfolio/document/overlays/utility_types_native.rs:1117">
P1: Custom agent: **PR title enforcement**

PR title 'Text-decoration' violates the PR title enforcement rule by not using an imperative verb as the first word and not following the required format.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread node-graph/nodes/text/src/path_builder.rs Outdated
Comment thread editor/src/messages/portfolio/document_migration.rs Outdated
max_width: None,
max_height: None,
align: TextAlign::AlignLeft,
underline: false,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Custom agent: PR title enforcement

PR title 'Text-decoration' violates the PR title enforcement rule by not using an imperative verb as the first word and not following the required format.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At editor/src/messages/portfolio/document/overlays/utility_types_native.rs, line 1117:

<comment>PR title 'Text-decoration' violates the PR title enforcement rule by not using an imperative verb as the first word and not following the required format.</comment>

<file context>
@@ -1114,6 +1114,9 @@ impl OverlayContextInternal {
 			max_width: None,
 			max_height: None,
 			align: TextAlign::AlignLeft,
+			underline: false,
+			overline: false,
+			strikethrough: false,
</file context>

@jsjgdh
jsjgdh marked this pull request as ready for review July 8, 2026 10:14

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 16 files

Confidence score: 3/5

  • In editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs, SVG text import appears to use only the first usvg::Text chunk’s position, so multi-chunk text (such as <tspan>-based content) can be placed incorrectly after import; merging as-is risks visibly broken layouts in user SVGs — update transform handling to account for all text chunks and add a regression test with multi-chunk text before merging.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs">

<violation number="1" location="editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs:734">
P2: SVG text transform import only accounts for the first text chunk's position, so multi-chunk SVG text can be mispositioned. `usvg::Text` can contain multiple chunks (for example from `<tspan>` elements with different `x`/`y` attributes), but all chunks are merged into a single Graphite text node and only the first chunk's coordinates are used for the transform offset. Consider splitting chunks into separate text layers or encoding their individual offsets so that imported multi-chunk SVG text preserves its original positioning.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

typesetting
}

fn apply_usvg_text_transform(modify_inputs: &mut ModifyInputsContext, text: &usvg::Text) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: SVG text transform import only accounts for the first text chunk's position, so multi-chunk SVG text can be mispositioned. usvg::Text can contain multiple chunks (for example from <tspan> elements with different x/y attributes), but all chunks are merged into a single Graphite text node and only the first chunk's coordinates are used for the transform offset. Consider splitting chunks into separate text layers or encoding their individual offsets so that imported multi-chunk SVG text preserves its original positioning.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs, line 734:

<comment>SVG text transform import only accounts for the first text chunk's position, so multi-chunk SVG text can be mispositioned. `usvg::Text` can contain multiple chunks (for example from `<tspan>` elements with different `x`/`y` attributes), but all chunks are merged into a single Graphite text node and only the first chunk's coordinates are used for the transform offset. Consider splitting chunks into separate text layers or encoding their individual offsets so that imported multi-chunk SVG text preserves its original positioning.</comment>

<file context>
@@ -684,13 +706,45 @@ fn import_usvg_node_inner(
+	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();
</file context>

@jsjgdh jsjgdh changed the title Text-decoration Add underline, overline, and strikethrough text decorations to the Text node Jul 15, 2026
@jsjgdh
jsjgdh force-pushed the text-decoration branch from 13536a6 to 1f551d7 Compare July 15, 2026 14:57
@jsjgdh
jsjgdh force-pushed the text-decoration branch 3 times, most recently from 7abdd28 to 915f8ff Compare August 12, 2026 11:36

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 existing issue remains and 4 new issues found across 16 files (changes from recent commits).

Confidence score: 2/5

  • In document_migration.rs, legacy TextNodes with fewer than 12 inputs can keep the old geometry-producing form. Handle these legacy shapes in the migration before relying on the later conversion.
  • With Max Height enabled, text.rs passes the enable flag to ATTR_MAX_HEIGHT instead of the configured height. Pass the configured Option<f64> so the renderer receives the height.
  • Flattening <tspan> content in graph_operation_message_handler.rs loses span-specific behavior: decorations can bleed onto undecorated text, and later chunks' x/y positions are discarded. Preserve the spans and their positions separately.
  • With per_glyph_items enabled, text_context.rs adds decoration rectangles to vector_list, breaking the one-item-per-glyph contract. Keep decoration items out of the per-glyph output.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="node-graph/nodes/gstd/src/text.rs">

<violation number="1" location="node-graph/nodes/gstd/src/text.rs:74">
P2: When *Max Height* is enabled, this expression stores the enable flag as `max_height`, so `ATTR_MAX_HEIGHT` receives an `Option<bool>` instead of the configured `Option<f64>`. The renderer then treats the attribute as absent and does not limit the text block; use `max_height.element()` for the fourth tuple value.</violation>
</file>

<file name="editor/src/messages/portfolio/document_migration.rs">

<violation number="1" location="editor/src/messages/portfolio/document_migration.rs:2021">
P1: When opening a legacy `TextNode` with fewer than 12 inputs, this migration still leaves the node in the old geometry-producing form. `legacy_text_node_template()` now creates 16 inputs, so the later 13-input conversion pass skips it; normalize the legacy template to exactly 13 inputs before the conversion, then apply the 15-input template with decoration defaults.</violation>
</file>

<file name="node-graph/nodes/text/src/text_context.rs">

<violation number="1" location="node-graph/nodes/text/src/text_context.rs:158">
P2: With per_glyph_items true, each enabled decoration emits an extra non-glyph vector item (a rectangle) into vector_list, so the output no longer contains one item per glyph. This breaks the documented contract of the 'Text to Vector Glyphs' node (one item per glyph) and desynchronizes per-glyph item indexing for editor text editing, where decoration items also get no click target. Gate the decoration items to the merged path (per_glyph_items == false) or attach them to the shared text frame instead of pushing separate selectable items.</violation>
</file>

<file name="editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs">

<violation number="1" location="editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs:920">
P2: When SVG text contains mixed `<tspan>` decorations, this ORs the styles into one text node, decorating otherwise undecorated spans too. Preserve spans as separate text nodes or add per-span decoration data.</violation>
</file>

Requires human review: Auto-approval blocked because this review re-detected 1 unresolved issue already reported by Cubic.

Re-trigger cubic


// 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 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: When opening a legacy TextNode with fewer than 12 inputs, this migration still leaves the node in the old geometry-producing form. legacy_text_node_template() now creates 16 inputs, so the later 13-input conversion pass skips it; normalize the legacy template to exactly 13 inputs before the conversion, then apply the 15-input template with decoration defaults.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At editor/src/messages/portfolio/document_migration.rs, line 2021:

<comment>When opening a legacy `TextNode` with fewer than 12 inputs, this migration still leaves the node in the old geometry-producing form. `legacy_text_node_template()` now creates 16 inputs, so the later 13-input conversion pass skips it; normalize the legacy template to exactly 13 inputs before the conversion, then apply the 15-input template with decoration defaults.</comment>

<file context>
@@ -2016,6 +2016,37 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId],
 
+	// 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);
</file context>


for span in text.chunks().iter().flat_map(|chunk| chunk.spans()) {
let decoration = span.decoration();
typesetting.underline |= decoration.underline().is_some();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When SVG text contains mixed <tspan> decorations, this ORs the styles into one text node, decorating otherwise undecorated spans too. Preserve spans as separate text nodes or add per-span decoration data.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs, line 920:

<comment>When SVG text contains mixed `<tspan>` decorations, this ORs the styles into one text node, decorating otherwise undecorated spans too. Preserve spans as separate text nodes or add per-span decoration data.</comment>

<file context>
@@ -889,6 +912,37 @@ fn insert_brush_strokes_chain(network_interface: &mut NodeNetworkInterface, laye
+
+	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();
</file context>

Comment thread node-graph/nodes/gstd/src/text.rs Outdated
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());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When Max Height is enabled, this expression stores the enable flag as max_height, so ATTR_MAX_HEIGHT receives an Option<bool> instead of the configured Option<f64>. The renderer then treats the attribute as absent and does not limit the text block; use max_height.element() for the fourth tuple value.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At node-graph/nodes/gstd/src/text.rs, line 74:

<comment>When *Max Height* is enabled, this expression stores the enable flag as `max_height`, so `ATTR_MAX_HEIGHT` receives an `Option<bool>` instead of the configured `Option<f64>`. The renderer then treats the attribute as absent and does not limit the text block; use `max_height.element()` for the fourth tuple value.</comment>

<file context>
@@ -59,12 +61,19 @@ fn text(
 	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());
</file context>

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: With per_glyph_items true, each enabled decoration emits an extra non-glyph vector item (a rectangle) into vector_list, so the output no longer contains one item per glyph. This breaks the documented contract of the 'Text to Vector Glyphs' node (one item per glyph) and desynchronizes per-glyph item indexing for editor text editing, where decoration items also get no click target. Gate the decoration items to the merged path (per_glyph_items == false) or attach them to the shared text frame instead of pushing separate selectable items.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At node-graph/nodes/text/src/text_context.rs, line 158:

<comment>With per_glyph_items true, each enabled decoration emits an extra non-glyph vector item (a rectangle) into vector_list, so the output no longer contains one item per glyph. This breaks the documented contract of the 'Text to Vector Glyphs' node (one item per glyph) and desynchronizes per-glyph item indexing for editor text editing, where decoration items also get no click target. Gate the decoration items to the merged path (per_glyph_items == false) or attach them to the shared text frame instead of pushing separate selectable items.</comment>

<file context>
@@ -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);
</file context>

Comment thread node-graph/nodes/text/src/path_builder.rs Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

4 existing issues remain and 2 new issues found across 16 files

Confidence score: 3/5

  • In path_builder.rs, per_glyph_items puts the decoration rectangle at item 0, so Text to Vector Glyphs can use the decoration instead of the first glyph. Keep the decoration out of the glyph reference list.
  • In graph_operation_message_handler.rs, SVG text import flattens styled spans: mixed font sizes are lost and a decorated <tspan> can style the other spans too. Preserve the spans as styled runs before importing them.
  • In graph_operation_message_handler.rs, SVG generic font families are all mapped to the first font database face, which can change how imported text looks. Keep the generic-family mappings or choose deliberate per-family fallbacks.
  • In graph_operation_message_handler.rs, SVG text import applies only the first chunk’s x/y, so later <tspan> positions are lost. Preserve chunk boundaries and positioning.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs">

<violation number="1" location="editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs:519">
P2: These assignments collapse every SVG generic font family into the arbitrary first database face. Preserve the font database's generic mappings, or configure deliberate per-family fallbacks instead of assigning one face to all five families.</violation>

<violation number="2" location="editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs:926">
P2: This helper keeps only the first span's font size while the importer concatenates every span into one node. Split styled spans into separate text nodes or preserve per-span sizing before importing mixed-size SVG text.</violation>
</file>

Requires human review: Auto-approval blocked because this review re-detected 5 unresolved issues already reported by Cubic.

Re-trigger cubic

Comment thread editor/src/messages/portfolio/document_migration.rs Outdated
Comment thread node-graph/libraries/rendering/src/renderer.rs
}

if let Some(first_span) = text.chunks().first().and_then(|chunk| chunk.spans().first()) {
typesetting.font_size = first_span.font_size().get() as f64;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: This helper keeps only the first span's font size while the importer concatenates every span into one node. Split styled spans into separate text nodes or preserve per-span sizing before importing mixed-size SVG text.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs, line 926:

<comment>This helper keeps only the first span's font size while the importer concatenates every span into one node. Split styled spans into separate text nodes or preserve per-span sizing before importing mixed-size SVG text.</comment>

<file context>
@@ -889,6 +912,37 @@ fn insert_brush_strokes_chain(network_interface: &mut NodeNetworkInterface, laye
+	}
+
+	if let Some(first_span) = text.chunks().first().and_then(|chunk| chunk.spans().first()) {
+		typesetting.font_size = first_span.font_size().get() as f64;
+	}
+
</file context>

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: These assignments collapse every SVG generic font family into the arbitrary first database face. Preserve the font database's generic mappings, or configure deliberate per-family fallbacks instead of assigning one face to all five families.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs, line 519:

<comment>These assignments collapse every SVG generic font family into the arbitrary first database face. Preserve the font database's generic mappings, or configure deliberate per-family fallbacks instead of assigning one face to all five families.</comment>

<file context>
@@ -502,7 +503,27 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageContext<'_>> for
+					.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);
</file context>

jsjgdh added 4 commits October 6, 2026 16:28
usvg drops a `<text>` element outright when it has no font to shape it with, so
the database needs a face, but loading the system's fonts and every font the
editor has cached on each paste does a lot of work to import text that Graphite
then reshapes itself. One fallback font is enough, and holding it in a
`OnceLock` means only the first paste pays for it.

Pointing the generic families at that font also keeps text whose own family is
one usvg doesn't have, which is the common case for an imported document.
Dropping the editor's font list from the context leaves the import with no
reason to reach the fonts handler.
Decoration rectangles were merged with whatever winding the helper happened to
build, so on fonts winding the other way they cancelled through the glyphs
instead of painting over them. They now wait for every glyph to land, then join
wound to match. Strikethrough ran in both the behind-glyphs pass and its own
after-glyphs pass; the first pass no longer includes it. Justified runs counted
glyphs for spaces, so decorations overshot; the run's own share travels with it
now. Per-glyph decorations join behind the glyphs so item i stays glyph i.

Imported SVG text also sat a line low, because SVG marks the baseline while a
layer starts at its layout's top-left, and middle and end anchors were ignored.
The text is laid out once, the first baseline comes off, and the width shifts
the anchor. The editing overlay carries the decoration string so it mirrors the
canvas while typing.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant