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
4 changes: 3 additions & 1 deletion editor/src/messages/frontend/frontend_message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use crate::messages::tool::tool_messages::eyedropper_tool::PrimarySecondary;
use graph_craft::document::NodeId;
use graphene_std::raster::Image;
use graphene_std::raster::color::Color;
use graphene_std::text::{Font, TextAlign};
use graphene_std::text::{Font, TextAlign, VerticalAlign};
use std::path::PathBuf;

#[cfg(not(target_family = "wasm"))]
Expand Down Expand Up @@ -50,6 +50,8 @@ pub enum FrontendMessage {
#[serde(rename = "maxHeight")]
max_height: Option<f64>,
align: TextAlign,
#[serde(rename = "verticalAlign")]
vertical_align: VerticalAlign,
},
DisplayEditableTextboxUpdateFontData {
#[serde(rename = "fontData")]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ impl<'a> ModifyInputsContext<'a> {
Some(NodeInput::value(TaggedValue::F64(typesetting.max_height.unwrap_or(100.)), false)),
Some(NodeInput::value(TaggedValue::F64(typesetting.tilt), false)),
Some(NodeInput::value(TaggedValue::TextAlign(typesetting.align), false)),
Some(NodeInput::value(TaggedValue::VerticalAlign(typesetting.vertical_align), false)),
]);
let transform = resolve_network_node_type("Transform").expect("Transform node does not exist").default_node_template();
let stroke = resolve_proto_node_type(graphene_std::vector_nodes::stroke::IDENTIFIER)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2586,6 +2586,13 @@ fn static_input_properties() -> InputProperties {
Ok(vec![choices])
}),
);
map.insert(
"text_vertical_align".to_string(),
Box::new(|node_id, index, context| {
let choices = enum_choice::<text::VerticalAlign>().for_socket(ParameterWidgetsInfo::new(node_id, index, true, context)).property_row();
Ok(vec![choices])
}),
);
map
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,7 @@ pub fn text_width(text: &str, font_size: f64) -> f64 {
max_height: None,
tilt: 0.0,
align: TextAlign::Left,
..Default::default()
};

// Load Source Sans Pro font data
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1111,6 +1111,7 @@ impl OverlayContextInternal {
max_height: None,
tilt: 0.,
align: TextAlign::Left, // We'll handle alignment manually via pivot
..Default::default()
};

// Load Source Sans Pro font data
Expand Down
27 changes: 25 additions & 2 deletions editor/src/messages/portfolio/document_migration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1281,11 +1281,34 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId],
network_path,
);

// Copy over old inputs
// Copy over old inputs (tilt, align)
#[allow(clippy::needless_range_loop)]
for i in 10..=12 {
for i in 10..=11 {
document.network_interface.set_input(&InputConnector::node(*node_id, i), old_inputs[i - 2].clone(), network_path);
}

// vertical_align at index 12 gets its default from the template

// Copy over separate_glyph_elements from old index 10 to new index 13
document.network_interface.set_input(&InputConnector::node(*node_id, 13), old_inputs[10].clone(), network_path);
}

// Insert vertical_align parameter between align and separate_glyph_elements:
// https://github.com/GraphiteEditor/Graphite/issues/3883
if reference == DefinitionIdentifier::ProtoNode(graphene_std::text::text::IDENTIFIER) && inputs_count == 13 {
let mut template: NodeTemplate = resolve_document_node_type(&reference)?.default_node_template();
document.network_interface.replace_implementation(node_id, network_path, &mut template);
let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut template)?;

#[allow(clippy::needless_range_loop)]
for i in 0..=11 {
document.network_interface.set_input(&InputConnector::node(*node_id, i), old_inputs[i].clone(), network_path);
}

// vertical_align at index 12 gets its default from the template

// Shift separate_glyph_elements from old index 12 to new index 13
document.network_interface.set_input(&InputConnector::node(*node_id, 13), old_inputs[12].clone(), network_path);
}

// Upgrade Sine, Cosine, and Tangent nodes to include a boolean input for whether the output should be in radians, which was previously the only option but is now not the default
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,9 @@ pub fn get_text(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInter
let Some(&TaggedValue::TextAlign(align)) = inputs[graphene_std::text::text::AlignInput::INDEX].as_value() else {
return None;
};
let Some(&TaggedValue::VerticalAlign(vertical_align)) = inputs[graphene_std::text::text::VerticalAlignInput::INDEX].as_value() else {
return None;
};
let Some(&TaggedValue::Bool(per_glyph_instances)) = inputs[graphene_std::text::text::SeparateGlyphElementsInput::INDEX].as_value() else {
return None;
};
Expand All @@ -433,6 +436,7 @@ pub fn get_text(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInter
character_spacing,
tilt,
align,
vertical_align,
};
Some((text, font, typesetting, per_glyph_instances))
}
Expand Down
24 changes: 23 additions & 1 deletion editor/src/messages/tool/tool_messages/text_tool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ use crate::messages::tool::utility_types::ToolRefreshOptions;
use graph_craft::document::value::TaggedValue;
use graph_craft::document::{NodeId, NodeInput};
use graphene_std::renderer::Quad;
use graphene_std::text::{Font, FontCache, TextAlign, TypesettingConfig, lines_clipping};
use graphene_std::text::{Font, FontCache, TextAlign, TypesettingConfig, VerticalAlign, lines_clipping};
use graphene_std::vector::style::Fill;
use graphene_std::{Color, NodeInputDecleration};

Expand All @@ -38,6 +38,7 @@ pub struct TextOptions {
fill: ToolColorOptions,
tilt: f64,
align: TextAlign,
vertical_align: VerticalAlign,
}

impl Default for TextOptions {
Expand All @@ -50,6 +51,7 @@ impl Default for TextOptions {
fill: ToolColorOptions::new_primary(),
tilt: 0.,
align: TextAlign::default(),
vertical_align: VerticalAlign::default(),
}
}
}
Expand Down Expand Up @@ -85,6 +87,7 @@ pub enum TextOptionsUpdate {
FontSize(f64),
LineHeightRatio(f64),
Align(TextAlign),
VerticalAlign(VerticalAlign),
WorkingColors(Option<Color>, Option<Color>),
}

Expand Down Expand Up @@ -216,6 +219,20 @@ fn create_text_widgets(tool: &TextTool, font_catalog: &FontCatalog) -> Vec<Widge
})
.collect();
let align = RadioInput::new(align_entries).selected_index(Some(tool.options.align as u32)).widget_instance();

let vertical_align_entries: Vec<_> = [VerticalAlign::Top, VerticalAlign::Center, VerticalAlign::Bottom]
.into_iter()
.map(|va| {
RadioEntryData::new(format!("{va:?}")).label(va.to_string()).on_update(move |_| {
TextToolMessage::UpdateOptions {
options: TextOptionsUpdate::VerticalAlign(va),
}
.into()
})
})
.collect();
let vertical_align = RadioInput::new(vertical_align_entries).selected_index(Some(tool.options.vertical_align as u32)).widget_instance();

vec![
font,
Separator::new(SeparatorStyle::Related).widget_instance(),
Expand All @@ -226,6 +243,8 @@ fn create_text_widgets(tool: &TextTool, font_catalog: &FontCatalog) -> Vec<Widge
line_height_ratio,
Separator::new(SeparatorStyle::Related).widget_instance(),
align,
Separator::new(SeparatorStyle::Related).widget_instance(),
vertical_align,
]
}

Expand Down Expand Up @@ -291,6 +310,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Text
TextOptionsUpdate::FontSize(font_size) => self.options.font_size = font_size,
TextOptionsUpdate::LineHeightRatio(line_height_ratio) => self.options.line_height_ratio = line_height_ratio,
TextOptionsUpdate::Align(align) => self.options.align = align,
TextOptionsUpdate::VerticalAlign(vertical_align) => self.options.vertical_align = vertical_align,
TextOptionsUpdate::FillColor(color) => {
self.options.fill.custom_color = color;
self.options.fill.color_type = ToolColorType::Custom;
Expand Down Expand Up @@ -420,6 +440,7 @@ impl TextToolData {
max_width: editing_text.typesetting.max_width,
max_height: editing_text.typesetting.max_height,
align: editing_text.typesetting.align,
vertical_align: editing_text.typesetting.vertical_align,
});
} else {
// Check if DisplayRemoveEditableTextbox is already in the responses queue
Expand Down Expand Up @@ -904,6 +925,7 @@ impl Fsm for TextToolFsmState {
max_height: constraint_size.map(|size| size.y),
tilt: tool_options.tilt,
align: tool_options.align,
vertical_align: tool_options.vertical_align,
},
font: Font::new(tool_options.font.font_family.clone(), tool_options.font.font_style.clone()),
color: tool_options.fill.active_color(),
Expand Down
25 changes: 25 additions & 0 deletions frontend/src/components/panels/Document.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,31 @@
textInput.style.color = data.color;
textInput.style.textAlign = data.align;

// Apply vertical alignment using padding-top to offset text within the container
if (data.maxHeight !== undefined) {
if (data.verticalAlign === "Center" || data.verticalAlign === "Bottom") {
// Reset any existing padding first
textInput.style.paddingTop = "0px";

// Wait for layout to settle so we can measure the content height
await tick();

const contentHeight = textInput.scrollHeight;
Copy link
Contributor

Choose a reason for hiding this comment

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

P2: Vertical alignment padding is measured once and not recomputed after text edits or font updates, causing center/bottom-aligned text overlay drift.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At frontend/src/components/panels/Document.svelte, line 378:

<comment>Vertical alignment padding is measured once and not recomputed after text edits or font updates, causing center/bottom-aligned text overlay drift.</comment>

<file context>
@@ -366,6 +366,31 @@
+				// Wait for layout to settle so we can measure the content height
+				await tick();
+
+				const contentHeight = textInput.scrollHeight;
+				const containerHeight = Math.floor(data.maxHeight / (data.lineHeightRatio * data.fontSize)) * (data.lineHeightRatio * data.fontSize);
+				const freeSpace = Math.max(containerHeight - contentHeight, 0);
</file context>

const containerHeight = data.maxHeight;
const freeSpace = Math.max(containerHeight - contentHeight, 0);

if (data.verticalAlign === "Center") {
textInput.style.paddingTop = `${freeSpace / 2}px`;
} else {
textInput.style.paddingTop = `${freeSpace}px`;
}
} else {
textInput.style.paddingTop = "0px";
}
} else {
textInput.style.paddingTop = "0px";
}

textInput.oninput = () => {
if (!textInput) return;
editor.handle.updateBounds(textInputCleanup(textInput.innerText));
Expand Down
1 change: 1 addition & 0 deletions node-graph/graph-craft/src/document/value.rs
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,7 @@ tagged_value! {
CentroidType(vector::misc::CentroidType),
BooleanOperation(vector::misc::BooleanOperation),
TextAlign(text_nodes::TextAlign),
VerticalAlign(text_nodes::VerticalAlign),
}

impl TaggedValue {
Expand Down
15 changes: 15 additions & 0 deletions node-graph/libraries/core-types/src/text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,18 @@ impl From<TextAlign> for parley::Alignment {
}
}

/// Vertical alignment of text within its bounding box.
#[repr(C)]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, node_macro::ChoiceType)]
#[widget(Radio)]
pub enum VerticalAlign {
#[default]
Top,
Center,
Bottom,
}

#[derive(PartialEq, Clone, Copy, Debug, serde::Serialize, serde::Deserialize)]
pub struct TypesettingConfig {
pub font_size: f64,
Expand All @@ -43,6 +55,8 @@ pub struct TypesettingConfig {
pub max_height: Option<f64>,
pub tilt: f64,
pub align: TextAlign,
#[serde(default)]
pub vertical_align: VerticalAlign,
}

impl Default for TypesettingConfig {
Expand All @@ -55,6 +69,7 @@ impl Default for TypesettingConfig {
max_height: None,
tilt: 0.,
align: TextAlign::default(),
vertical_align: VerticalAlign::default(),
}
}
}
4 changes: 4 additions & 0 deletions node-graph/nodes/gstd/src/text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ fn text<'i: 'n>(
/// To have an effect on a single line of text, *Max Width* must be set.
#[widget(ParsedWidgetOverride::Custom = "text_align")]
align: TextAlign,
/// The vertical alignment of text within the text box. Requires *Max Height* to be set.
#[widget(ParsedWidgetOverride::Custom = "text_vertical_align")]
vertical_align: VerticalAlign,
/// Whether to split every letterform into its own vector path element. Otherwise, a single compound path is produced.
separate_glyph_elements: bool,
) -> Table<Vector> {
Expand All @@ -70,6 +73,7 @@ fn text<'i: 'n>(
max_height: has_max_height.then_some(max_height),
tilt,
align,
vertical_align,
};

to_path(&text, &font, &editor_resources.font_cache, typesetting, separate_glyph_elements)
Expand Down
15 changes: 15 additions & 0 deletions node-graph/nodes/text/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,18 @@ impl From<TextAlign> for parley::Alignment {
}
}

/// Vertical alignment of text within its bounding box.
#[repr(C)]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, node_macro::ChoiceType)]
#[widget(Radio)]
pub enum VerticalAlign {
#[default]
Top,
Center,
Bottom,
}
Comment on lines +41 to +51
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

This file appears to be a duplicate of node-graph/libraries/core-types/src/text.rs. To improve maintainability and avoid having to update two files for any change to these types, consider removing this file's contents and instead re-exporting the types from core-types. For example, you could replace the contents of this file with pub use core_types::text::*; if the module structure allows, or whatever is appropriate.


#[derive(PartialEq, Clone, Copy, Debug, serde::Serialize, serde::Deserialize)]
pub struct TypesettingConfig {
pub font_size: f64,
Expand All @@ -47,6 +59,8 @@ pub struct TypesettingConfig {
pub max_height: Option<f64>,
pub tilt: f64,
pub align: TextAlign,
#[serde(default)]
pub vertical_align: VerticalAlign,
}

impl Default for TypesettingConfig {
Expand All @@ -59,6 +73,7 @@ impl Default for TypesettingConfig {
max_height: None,
tilt: 0.,
align: TextAlign::default(),
vertical_align: VerticalAlign::default(),
}
}
}
4 changes: 2 additions & 2 deletions node-graph/nodes/text/src/path_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ impl<Upstream: Default + 'static> PathBuilder<Upstream> {
}
}

pub fn render_glyph_run(&mut self, glyph_run: &GlyphRun<'_, ()>, tilt: f64, per_glyph_instances: bool) {
pub fn render_glyph_run(&mut self, glyph_run: &GlyphRun<'_, ()>, tilt: f64, per_glyph_instances: bool, vertical_offset: f64) {
let mut run_x = glyph_run.offset();
let run_y = glyph_run.baseline();

Expand Down Expand Up @@ -105,7 +105,7 @@ impl<Upstream: Default + 'static> PathBuilder<Upstream> {
let outlines = font_ref.outline_glyphs();

for glyph in glyph_run.glyphs() {
let glyph_offset = DVec2::new((run_x + glyph.x) as f64, (run_y - glyph.y) as f64);
let glyph_offset = DVec2::new((run_x + glyph.x) as f64, (run_y - glyph.y) as f64 + vertical_offset);
Copy link
Contributor

Choose a reason for hiding this comment

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

P2: Vertical offset is applied before skew in merged-path mode, but skew pivot remains at run_y, causing unintended horizontal drift for tilted/synthetic-italic text.

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/path_builder.rs, line 108:

<comment>Vertical offset is applied before skew in merged-path mode, but skew pivot remains at `run_y`, causing unintended horizontal drift for tilted/synthetic-italic text.</comment>

<file context>
@@ -105,7 +105,7 @@ impl<Upstream: Default + 'static> PathBuilder<Upstream> {
 
 		for glyph in glyph_run.glyphs() {
-			let glyph_offset = DVec2::new((run_x + glyph.x) as f64, (run_y - glyph.y) as f64);
+			let glyph_offset = DVec2::new((run_x + glyph.x) as f64, (run_y - glyph.y) as f64 + vertical_offset);
 			run_x += glyph.advance;
 
</file context>

run_x += glyph.advance;

let glyph_id = GlyphId::from(glyph.id);
Expand Down
17 changes: 15 additions & 2 deletions node-graph/nodes/text/src/text_context.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use super::{Font, FontCache, TypesettingConfig};
use super::{Font, FontCache, TypesettingConfig, VerticalAlign};
use core::cell::RefCell;
use core_types::table::Table;
use glam::DVec2;
Expand Down Expand Up @@ -94,12 +94,25 @@ impl TextContext {

let mut path_builder = PathBuilder::new(per_glyph_instances, layout.scale() as f64);

let vertical_offset = if let Some(container_height) = typesetting.max_height {
let layout_height = layout.height() as f64;
let free_space = (container_height - layout_height).max(0.);

match typesetting.vertical_align {
VerticalAlign::Top => 0.,
VerticalAlign::Center => free_space / 2.,
VerticalAlign::Bottom => free_space,
}
} else {
0.
};

for line in layout.lines() {
for item in line.items() {
if let PositionedLayoutItem::GlyphRun(glyph_run) = item
&& typesetting.max_height.filter(|&max_height| glyph_run.baseline() > max_height as f32).is_none()
{
path_builder.render_glyph_run(&glyph_run, typesetting.tilt, per_glyph_instances);
path_builder.render_glyph_run(&glyph_run, typesetting.tilt, per_glyph_instances, vertical_offset);
}
}
}
Expand Down