diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 58467c78ec..df3f4e8b53 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -4,6 +4,8 @@ on:
push:
branches:
- master
+ tags:
+ - latest-stable
workflow_dispatch:
inputs:
web:
@@ -59,8 +61,6 @@ jobs:
RUSTC_WRAPPER: /usr/bin/sccache
CARGO_INCREMENTAL: 0
SCCACHE_DIR: /var/lib/github-actions/.cache
- INDEX_HTML_HEAD_REPLACEMENT:
-
steps:
- name: 📥 Clone repository
uses: actions/checkout@v6
@@ -90,9 +90,22 @@ jobs:
rustflags: ""
target: wasm32-unknown-unknown
+ - name: 🔀 Choose production deployment environment
+ id: production-env
+ if: github.event_name == 'push'
+ run: |
+ if [[ "${{ github.ref }}" == "refs/tags/latest-stable" ]]; then
+ echo "cf_project=graphite-editor" >> $GITHUB_OUTPUT
+ DOMAIN="editor.graphite.art"
+ else
+ echo "cf_project=graphite-dev" >> $GITHUB_OUTPUT
+ DOMAIN="dev.graphite.art"
+ fi
+ echo "head_replacement=" >> $GITHUB_OUTPUT
+
- name: ✂ Replace template in
of index.html
if: github.event_name == 'push'
- run: sed -i "s||$INDEX_HTML_HEAD_REPLACEMENT|" frontend/index.html
+ run: sed -i "s||${{ steps.production-env.outputs.head_replacement }}|" frontend/index.html
- name: 🌐 Build Graphite web code
env:
@@ -105,27 +118,68 @@ jobs:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
run: |
- MAX_ATTEMPTS=5
- DELAY=30
+ if [ -z "$CLOUDFLARE_API_TOKEN" ]; then
+ echo "No Cloudflare API token available (fork PR), skipping deploy."
+ exit 0
+ fi
+ MAX_ATTEMPTS=8
+ DELAY=15
for ATTEMPT in $(seq 1 $MAX_ATTEMPTS); do
echo "Attempt $ATTEMPT of $MAX_ATTEMPTS..."
- if OUTPUT=$(npx wrangler@3 pages deploy "frontend/dist" --project-name="graphite-dev" --commit-dirty=true 2>&1); then
- URL=$(echo "$OUTPUT" | grep -oP 'https://[^\s]+\.pages\.dev' | head -1)
+ npx wrangler@3 pages deploy "frontend/dist" --project-name="${{ steps.production-env.outputs.cf_project || 'graphite-dev' }}" --commit-dirty=true 2>&1 | tee /tmp/wrangler_output
+ if [ ${PIPESTATUS[0]} -eq 0 ]; then
+ URL=$(grep -oP 'https://[^\s]+\.pages\.dev' /tmp/wrangler_output | head -1)
echo "url=$URL" >> "$GITHUB_OUTPUT"
echo "Published successfully: $URL"
exit 0
fi
- echo "Attempt $ATTEMPT failed:"
- echo "$OUTPUT"
+ echo "Attempt $ATTEMPT failed."
if [ "$ATTEMPT" -lt "$MAX_ATTEMPTS" ]; then
echo "Retrying in ${DELAY}s..."
sleep $DELAY
- DELAY=$((DELAY * 3))
+ DELAY=$((DELAY * 2))
fi
done
echo "All $MAX_ATTEMPTS Cloudflare Pages publish attempts failed."
exit 1
+ - name: 🚀 Create GitHub Deployment for "View deployment" button
+ if: inputs.checkout_repo == '' || inputs.checkout_repo == github.repository
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ CF_URL: ${{ steps.cloudflare.outputs.url }}
+ run: |
+ if [ -z "$CF_URL" ]; then
+ echo "No Cloudflare URL available, skipping deployment."
+ exit 0
+ fi
+ if [ "${{ github.ref }}" = "refs/tags/latest-stable" ]; then
+ REF="latest-stable"
+ ENVIRONMENT="graphite-editor (Production)"
+ elif [ "${{ github.event_name }}" = "push" ]; then
+ REF="master"
+ ENVIRONMENT="graphite-dev (Production)"
+ else
+ REF="${{ inputs.checkout_ref || github.head_ref || github.ref_name }}"
+ ENVIRONMENT="graphite-dev (Preview)"
+ fi
+ DEPLOY_ID=$(gh api \
+ -X POST \
+ -H "Accept: application/vnd.github+json" \
+ repos/${{ github.repository }}/deployments \
+ --input - \
+ --jq '.id' <
-
- steps:
- - name: 📥 Clone repository
- uses: actions/checkout@v6
-
- - name: 🗑 Clear wasm-bindgen cache
- run: rm -r ~/.cache/.wasm-pack
-
- - name: 🟢 Install Node.js
- uses: actions/setup-node@v6
- with:
- node-version-file: .nvmrc
-
- - name: 🚧 Install build dependencies
- run: |
- cd frontend
- npm run setup
-
- - name: 🦀 Install Rust
- uses: actions-rust-lang/setup-rust-toolchain@v1
- with:
- toolchain: stable
- override: true
- cache: false
- rustflags: ""
- target: wasm32-unknown-unknown
-
- - name: ✂ Replace template in of index.html
- run: sed -i "s||$INDEX_HTML_HEAD_REPLACEMENT|" frontend/index.html
-
- - name: 🌐 Build Graphite web code
- env:
- NODE_ENV: production
- run: mold -run cargo run build web
-
- - name: 📤 Publish to Cloudflare Pages
- env:
- CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
- CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
- run: npx wrangler@3 pages deploy "frontend/dist" --project-name="graphite-editor" --branch="master" --commit-dirty=true
-
- - name: 📦 Upload assets to GitHub release
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- run: |
- DATE=$(git log -1 --format=%cd --date=format:%Y-%m-%d)
- cd frontend
- sed -i "s|$INDEX_HTML_HEAD_REPLACEMENT||" dist/index.html
- mv dist "graphite-$DATE"
- zip -r "graphite-self-hosted-build.zip" "graphite-$DATE"
- gh release upload latest-stable "graphite-self-hosted-build.zip" --clobber
diff --git a/editor/src/messages/frontend/frontend_message.rs b/editor/src/messages/frontend/frontend_message.rs
index 743d6ed2c2..933cffd2ff 100644
--- a/editor/src/messages/frontend/frontend_message.rs
+++ b/editor/src/messages/frontend/frontend_message.rs
@@ -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"))]
@@ -50,6 +50,8 @@ pub enum FrontendMessage {
#[serde(rename = "maxHeight")]
max_height: Option,
align: TextAlign,
+ #[serde(rename = "verticalAlign")]
+ vertical_align: VerticalAlign,
},
DisplayEditableTextboxUpdateFontData {
#[serde(rename = "fontData")]
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 f74fb67c15..d84640c05c 100644
--- a/editor/src/messages/portfolio/document/graph_operation/utility_types.rs
+++ b/editor/src/messages/portfolio/document/graph_operation/utility_types.rs
@@ -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)
diff --git a/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs b/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs
index b7db96ec65..ada7be0c5a 100644
--- a/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs
+++ b/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs
@@ -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::().for_socket(ParameterWidgetsInfo::new(node_id, index, true, context)).property_row();
+ Ok(vec![choices])
+ }),
+ );
map
}
diff --git a/editor/src/messages/portfolio/document/overlays/utility_functions.rs b/editor/src/messages/portfolio/document/overlays/utility_functions.rs
index dec2fda58e..d59a797f1c 100644
--- a/editor/src/messages/portfolio/document/overlays/utility_functions.rs
+++ b/editor/src/messages/portfolio/document/overlays/utility_functions.rs
@@ -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
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 9991d39c57..f979f7e30f 100644
--- a/editor/src/messages/portfolio/document/overlays/utility_types_native.rs
+++ b/editor/src/messages/portfolio/document/overlays/utility_types_native.rs
@@ -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
diff --git a/editor/src/messages/portfolio/document_migration.rs b/editor/src/messages/portfolio/document_migration.rs
index ac5ec052c0..cc1e158701 100644
--- a/editor/src/messages/portfolio/document_migration.rs
+++ b/editor/src/messages/portfolio/document_migration.rs
@@ -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
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 c71f199078..e527480f48 100644
--- a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs
+++ b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs
@@ -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;
};
@@ -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))
}
diff --git a/editor/src/messages/tool/tool_messages/text_tool.rs b/editor/src/messages/tool/tool_messages/text_tool.rs
index c5471f0bff..72228add0d 100644
--- a/editor/src/messages/tool/tool_messages/text_tool.rs
+++ b/editor/src/messages/tool/tool_messages/text_tool.rs
@@ -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};
@@ -38,6 +38,7 @@ pub struct TextOptions {
fill: ToolColorOptions,
tilt: f64,
align: TextAlign,
+ vertical_align: VerticalAlign,
}
impl Default for TextOptions {
@@ -50,6 +51,7 @@ impl Default for TextOptions {
fill: ToolColorOptions::new_primary(),
tilt: 0.,
align: TextAlign::default(),
+ vertical_align: VerticalAlign::default(),
}
}
}
@@ -85,6 +87,7 @@ pub enum TextOptionsUpdate {
FontSize(f64),
LineHeightRatio(f64),
Align(TextAlign),
+ VerticalAlign(VerticalAlign),
WorkingColors(Option, Option),
}
@@ -216,6 +219,20 @@ fn create_text_widgets(tool: &TextTool, font_catalog: &FontCatalog) -> 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(),
@@ -226,6 +243,8 @@ fn create_text_widgets(tool: &TextTool, font_catalog: &FontCatalog) -> Vec MessageHandler> 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;
@@ -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
@@ -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(),
diff --git a/frontend/src/components/panels/Document.svelte b/frontend/src/components/panels/Document.svelte
index 1767c18e6c..2909fa5440 100644
--- a/frontend/src/components/panels/Document.svelte
+++ b/frontend/src/components/panels/Document.svelte
@@ -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;
+ 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));
diff --git a/node-graph/graph-craft/src/document/value.rs b/node-graph/graph-craft/src/document/value.rs
index 91b6831809..8cebc5b4c7 100644
--- a/node-graph/graph-craft/src/document/value.rs
+++ b/node-graph/graph-craft/src/document/value.rs
@@ -269,6 +269,7 @@ tagged_value! {
CentroidType(vector::misc::CentroidType),
BooleanOperation(vector::misc::BooleanOperation),
TextAlign(text_nodes::TextAlign),
+ VerticalAlign(text_nodes::VerticalAlign),
}
impl TaggedValue {
diff --git a/node-graph/libraries/core-types/src/text.rs b/node-graph/libraries/core-types/src/text.rs
index 29917694b6..f5de96c30d 100644
--- a/node-graph/libraries/core-types/src/text.rs
+++ b/node-graph/libraries/core-types/src/text.rs
@@ -34,6 +34,18 @@ impl From 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,
@@ -43,6 +55,8 @@ pub struct TypesettingConfig {
pub max_height: Option,
pub tilt: f64,
pub align: TextAlign,
+ #[serde(default)]
+ pub vertical_align: VerticalAlign,
}
impl Default for TypesettingConfig {
@@ -55,6 +69,7 @@ impl Default for TypesettingConfig {
max_height: None,
tilt: 0.,
align: TextAlign::default(),
+ vertical_align: VerticalAlign::default(),
}
}
}
diff --git a/node-graph/nodes/gstd/src/text.rs b/node-graph/nodes/gstd/src/text.rs
index 4cb280ba30..1b6f059972 100644
--- a/node-graph/nodes/gstd/src/text.rs
+++ b/node-graph/nodes/gstd/src/text.rs
@@ -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 {
@@ -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)
diff --git a/node-graph/nodes/text/src/lib.rs b/node-graph/nodes/text/src/lib.rs
index ca4738ffa1..68853d72d5 100644
--- a/node-graph/nodes/text/src/lib.rs
+++ b/node-graph/nodes/text/src/lib.rs
@@ -38,6 +38,18 @@ impl From 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,
@@ -47,6 +59,8 @@ pub struct TypesettingConfig {
pub max_height: Option,
pub tilt: f64,
pub align: TextAlign,
+ #[serde(default)]
+ pub vertical_align: VerticalAlign,
}
impl Default for TypesettingConfig {
@@ -59,6 +73,7 @@ impl Default for TypesettingConfig {
max_height: None,
tilt: 0.,
align: TextAlign::default(),
+ vertical_align: VerticalAlign::default(),
}
}
}
diff --git a/node-graph/nodes/text/src/path_builder.rs b/node-graph/nodes/text/src/path_builder.rs
index c5ba250409..a5eb5cd2ea 100644
--- a/node-graph/nodes/text/src/path_builder.rs
+++ b/node-graph/nodes/text/src/path_builder.rs
@@ -64,7 +64,7 @@ impl PathBuilder {
}
}
- 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();
@@ -105,7 +105,7 @@ impl PathBuilder {
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);
run_x += glyph.advance;
let glyph_id = GlyphId::from(glyph.id);
diff --git a/node-graph/nodes/text/src/text_context.rs b/node-graph/nodes/text/src/text_context.rs
index 7934feb885..5118c2da90 100644
--- a/node-graph/nodes/text/src/text_context.rs
+++ b/node-graph/nodes/text/src/text_context.rs
@@ -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;
@@ -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);
}
}
}