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