From 5bb755488c0bedcc6fa0a544683330b1299a6f3d Mon Sep 17 00:00:00 2001 From: Annonnymmousss Date: Sun, 12 Apr 2026 07:53:56 +0530 Subject: [PATCH 1/4] feat: Add justify proper alignments(right/left/centre/all) --- .../document/node_graph/node_properties.rs | 2 +- .../messages/tool/tool_messages/text_tool.rs | 42 ++++++++++++++----- node-graph/libraries/core-types/src/text.rs | 26 ++++++++++-- node-graph/nodes/text/src/lib.rs | 26 ++++++++++-- node-graph/nodes/text/src/path_builder.rs | 24 +++++++++-- node-graph/nodes/text/src/text_context.rs | 31 +++++++++++++- 6 files changed, 128 insertions(+), 23 deletions(-) diff --git a/editor/src/messages/portfolio/document/node_graph/node_properties.rs b/editor/src/messages/portfolio/document/node_graph/node_properties.rs index 69c5c127f4..11a59f8eca 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_properties.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_properties.rs @@ -2953,7 +2953,7 @@ pub mod choice { if let Some(icon) = var_meta.icon { entry.icon(icon) } else { entry.label(var_meta.label) } }) .collect(); - RadioInput::new(items).selected_index(Some(current.as_u32())).widget_instance() + RadioInput::new(items).selected_index(Some(current.as_u32())).disabled(self.disabled).widget_instance() } } diff --git a/editor/src/messages/tool/tool_messages/text_tool.rs b/editor/src/messages/tool/tool_messages/text_tool.rs index 3e28fc8eb9..f4ac04db77 100644 --- a/editor/src/messages/tool/tool_messages/text_tool.rs +++ b/editor/src/messages/tool/tool_messages/text_tool.rs @@ -203,17 +203,25 @@ fn create_text_widgets(tool: &TextTool, font_catalog: &FontCatalog) -> Vec = [TextAlign::Left, TextAlign::Center, TextAlign::Right, TextAlign::JustifyLeft] - .into_iter() - .map(|align| { - RadioEntryData::new(format!("{align:?}")).label(align.to_string()).on_update(move |_| { - TextToolMessage::UpdateOptions { - options: TextOptionsUpdate::Align(align), - } - .into() - }) + let align_entries: Vec<_> = [ + TextAlign::Left, + TextAlign::Center, + TextAlign::Right, + TextAlign::JustifyLeft, + TextAlign::JustifyCenter, + TextAlign::JustifyRight, + TextAlign::JustifyAll, + ] + .into_iter() + .map(|align| { + RadioEntryData::new(format!("{align:?}")).label(align.to_string()).on_update(move |_| { + TextToolMessage::UpdateOptions { + options: TextOptionsUpdate::Align(align), + } + .into() }) - .collect(); + }) + .collect(); let align = RadioInput::new(align_entries).selected_index(Some(tool.options.align as u32)).widget_instance(); vec![ font, @@ -289,7 +297,19 @@ impl<'a> 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::Align(align) => { + self.options.align = align; + if let Some(editing_text) = self.tool_data.editing_text.as_mut() { + editing_text.typesetting.align = align; + } + if let Some(node_id) = graph_modification_utils::get_text_id(self.tool_data.layer, &context.document.network_interface) { + responses.add(NodeGraphMessage::SetInput { + input_connector: InputConnector::node(node_id, graphene_std::text::text::AlignInput::INDEX), + input: NodeInput::value(TaggedValue::TextAlign(align), false), + }); + responses.add(NodeGraphMessage::RunDocumentGraph); + } + } TextOptionsUpdate::FillColor(color) => { self.options.fill.custom_color = color; self.options.fill.color_type = ToolColorType::Custom; diff --git a/node-graph/libraries/core-types/src/text.rs b/node-graph/libraries/core-types/src/text.rs index 81adab3526..443112a5d5 100644 --- a/node-graph/libraries/core-types/src/text.rs +++ b/node-graph/libraries/core-types/src/text.rs @@ -19,9 +19,14 @@ pub enum TextAlign { Left, Center, Right, - #[label("Justify")] + #[label("Justify Left")] JustifyLeft, - // TODO: JustifyCenter, JustifyRight, JustifyAll + #[label("Justify Center")] + JustifyCenter, + #[label("Justify Right")] + JustifyRight, + #[label("Justify All")] + JustifyAll, } impl From for parley::Alignment { @@ -30,11 +35,26 @@ impl From for parley::Alignment { TextAlign::Left => parley::Alignment::Left, TextAlign::Center => parley::Alignment::Center, TextAlign::Right => parley::Alignment::Right, - TextAlign::JustifyLeft => parley::Alignment::Justify, + TextAlign::JustifyLeft | TextAlign::JustifyCenter | TextAlign::JustifyRight | TextAlign::JustifyAll => parley::Alignment::Justify, } } } +impl TextAlign { + pub fn last_line_correction(self) -> Option { + match self { + Self::JustifyCenter => Some(parley::Alignment::Center), + Self::JustifyRight => Some(parley::Alignment::Right), + Self::JustifyAll => Some(parley::Alignment::Justify), + _ => None, + } + } + + pub fn is_justify(self) -> bool { + matches!(self, Self::JustifyLeft | Self::JustifyCenter | Self::JustifyRight | Self::JustifyAll) + } +} + #[derive(PartialEq, Clone, Copy, Debug)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct TypesettingConfig { diff --git a/node-graph/nodes/text/src/lib.rs b/node-graph/nodes/text/src/lib.rs index eb57c9e5fe..f3d0ef1721 100644 --- a/node-graph/nodes/text/src/lib.rs +++ b/node-graph/nodes/text/src/lib.rs @@ -34,9 +34,14 @@ pub enum TextAlign { Left, Center, Right, - #[label("Justify")] + #[label("Justify Left")] JustifyLeft, - // TODO: JustifyCenter, JustifyRight, JustifyAll + #[label("Justify Center")] + JustifyCenter, + #[label("Justify Right")] + JustifyRight, + #[label("Justify All")] + JustifyAll, } impl From for parley::Alignment { @@ -45,11 +50,26 @@ impl From for parley::Alignment { TextAlign::Left => parley::Alignment::Left, TextAlign::Center => parley::Alignment::Center, TextAlign::Right => parley::Alignment::Right, - TextAlign::JustifyLeft => parley::Alignment::Justify, + TextAlign::JustifyLeft | TextAlign::JustifyCenter | TextAlign::JustifyRight | TextAlign::JustifyAll => parley::Alignment::Justify, } } } +impl TextAlign { + pub fn last_line_correction(self) -> Option { + match self { + Self::JustifyCenter => Some(parley::Alignment::Center), + Self::JustifyRight => Some(parley::Alignment::Right), + Self::JustifyAll => Some(parley::Alignment::Justify), + _ => None, + } + } + + pub fn is_justify(self) -> bool { + matches!(self, Self::JustifyLeft | Self::JustifyCenter | Self::JustifyRight | Self::JustifyAll) + } +} + #[derive(PartialEq, Clone, Copy, Debug)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct TypesettingConfig { diff --git a/node-graph/nodes/text/src/path_builder.rs b/node-graph/nodes/text/src/path_builder.rs index 9e7684e7be..c985774773 100644 --- a/node-graph/nodes/text/src/path_builder.rs +++ b/node-graph/nodes/text/src/path_builder.rs @@ -52,10 +52,20 @@ impl PathBuilder { } #[allow(clippy::too_many_arguments)] - fn draw_glyph(&mut self, glyph: &OutlineGlyph<'_>, size: f32, normalized_coords: &[NormalizedCoord], glyph_offset: DVec2, style_skew: Option, skew: DAffine2, per_glyph_items: bool) { + fn draw_glyph( + &mut self, + glyph: &OutlineGlyph<'_>, + size: f32, + normalized_coords: &[NormalizedCoord], + glyph_offset: DVec2, + style_skew: Option, + skew: DAffine2, + per_glyph_items: bool, + ) -> bool { let location_ref = LocationRef::new(normalized_coords); let settings = DrawSettings::unhinted(Size::new(size), location_ref); glyph.draw(settings, self).unwrap(); + let has_geometry = !self.glyph_subpaths.is_empty(); // Apply transforms in correct order: style-based skew first, then user-requested skew // This ensures font synthesis (italic) is applied before user transformations @@ -91,10 +101,12 @@ impl PathBuilder { self.merged_click_target_baselines.push(glyph_offset.y); } } + + has_geometry } - pub fn render_glyph_run(&mut self, glyph_run: &GlyphRun<'_, ()>, tilt: f64, per_glyph_items: bool) { - let mut run_x = glyph_run.offset(); + pub fn render_glyph_run(&mut self, glyph_run: &GlyphRun<'_, ()>, tilt: f64, per_glyph_items: bool, x_offset: f64, space_extra: f32) { + let mut run_x = glyph_run.offset() + x_offset as f32; let run_y = glyph_run.baseline(); let run = glyph_run.run(); @@ -142,7 +154,11 @@ impl PathBuilder { if !per_glyph_items { self.origin = glyph_offset; } - self.draw_glyph(&glyph_outline, font_size, &normalized_coords, glyph_offset, style_skew, skew, per_glyph_items); + let drew_geometry = self.draw_glyph(&glyph_outline, font_size, &normalized_coords, glyph_offset, style_skew, skew, per_glyph_items); + + if !drew_geometry && space_extra != 0. && glyph.advance > 0. { + run_x += space_extra; + } } } } diff --git a/node-graph/nodes/text/src/text_context.rs b/node-graph/nodes/text/src/text_context.rs index cc16ea8fa7..36d30ed1bb 100644 --- a/node-graph/nodes/text/src/text_context.rs +++ b/node-graph/nodes/text/src/text_context.rs @@ -108,14 +108,43 @@ impl TextContext { }) .unwrap_or_default(); + let alignment_width = typesetting.max_width.map(|w| w as f32).unwrap_or_else(|| layout.full_width()); + let last_line_correction = typesetting.align.last_line_correction(); + let mut path_builder = PathBuilder::new(per_glyph_items, layout.scale() as f64, text_frame_size, first_glyph_offset); for line in layout.lines() { + let range = line.text_range(); + let is_last_para_line = range.end == text.len() || text.get(range.clone()).map(|s| s.ends_with('\n')).unwrap_or(false) || text.as_bytes().get(range.end) == Some(&b'\n'); + + let (x_offset, space_extra) = if is_last_para_line { + if let Some(correction) = last_line_correction { + let metrics = line.metrics(); + let content_advance = metrics.advance - metrics.trailing_whitespace; + let free_space = alignment_width - content_advance; + + match correction { + parley::Alignment::Center => (free_space as f64 * 0.5, 0_f32), + parley::Alignment::Right => (free_space as f64, 0_f32), + parley::Alignment::Justify => { + let space_count: usize = line.runs().map(|run| run.clusters().filter(|c| c.is_space_or_nbsp()).count()).sum(); + let extra = if space_count > 0 { free_space / space_count as f32 } else { 0. }; + (0_f64, extra) + } + _ => (0_f64, 0_f32), + } + } else { + (0_f64, 0_f32) + } + } else { + (0_f64, 0_f32) + }; + 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_items); + path_builder.render_glyph_run(&glyph_run, typesetting.tilt, per_glyph_items, x_offset, space_extra); } } } From a0bb72a65ab5cd6b58ff015c3689972df526a26b Mon Sep 17 00:00:00 2001 From: Annonnymmousss Date: Sun, 12 Apr 2026 07:56:35 +0530 Subject: [PATCH 2/4] chore: fmt --- editor/src/messages/tool/tool_messages/text_tool.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/editor/src/messages/tool/tool_messages/text_tool.rs b/editor/src/messages/tool/tool_messages/text_tool.rs index f4ac04db77..040e1a6065 100644 --- a/editor/src/messages/tool/tool_messages/text_tool.rs +++ b/editor/src/messages/tool/tool_messages/text_tool.rs @@ -298,7 +298,7 @@ impl<'a> 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; + self.options.align = align; if let Some(editing_text) = self.tool_data.editing_text.as_mut() { editing_text.typesetting.align = align; } From fea67e6dc1a896642a1adaffa452085484ae5457 Mon Sep 17 00:00:00 2001 From: Annonnymmousss Date: Sun, 12 Apr 2026 08:29:17 +0530 Subject: [PATCH 3/4] chore: refactor --- node-graph/nodes/text/src/text_context.rs | 37 ++++++++++++----------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/node-graph/nodes/text/src/text_context.rs b/node-graph/nodes/text/src/text_context.rs index 36d30ed1bb..92df5028c4 100644 --- a/node-graph/nodes/text/src/text_context.rs +++ b/node-graph/nodes/text/src/text_context.rs @@ -117,24 +117,27 @@ impl TextContext { let range = line.text_range(); let is_last_para_line = range.end == text.len() || text.get(range.clone()).map(|s| s.ends_with('\n')).unwrap_or(false) || text.as_bytes().get(range.end) == Some(&b'\n'); - let (x_offset, space_extra) = if is_last_para_line { - if let Some(correction) = last_line_correction { - let metrics = line.metrics(); - let content_advance = metrics.advance - metrics.trailing_whitespace; - let free_space = alignment_width - content_advance; - - match correction { - parley::Alignment::Center => (free_space as f64 * 0.5, 0_f32), - parley::Alignment::Right => (free_space as f64, 0_f32), - parley::Alignment::Justify => { - let space_count: usize = line.runs().map(|run| run.clusters().filter(|c| c.is_space_or_nbsp()).count()).sum(); - let extra = if space_count > 0 { free_space / space_count as f32 } else { 0. }; - (0_f64, extra) - } - _ => (0_f64, 0_f32), + let (x_offset, space_extra) = if let (true, Some(correction)) = (is_last_para_line, last_line_correction) { + let metrics = line.metrics(); + let content_advance = metrics.advance - metrics.trailing_whitespace; + let free_space = alignment_width - content_advance; + + match correction { + parley::Alignment::Center => (free_space as f64 * 0.5, 0_f32), + parley::Alignment::Right => (free_space as f64, 0_f32), + parley::Alignment::Justify => { + let line_text = text.get(range.clone()).unwrap_or(""); + let trailing_len = line_text.len() - line_text.trim_end().len(); + let visible_end_index = range.end - trailing_len; + + let space_count: usize = line + .runs() + .map(|run| run.clusters().filter(|c| c.is_space_or_nbsp() && c.text_range().start < visible_end_index).count()) + .sum(); + let extra = if space_count > 0 { free_space / space_count as f32 } else { 0. }; + (0_f64, extra) } - } else { - (0_f64, 0_f32) + _ => (0_f64, 0_f32), } } else { (0_f64, 0_f32) From b25204dddd40ce35667f5925b261cef25a37acc8 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Wed, 6 May 2026 18:13:58 -0700 Subject: [PATCH 4/4] Cleanup --- .../document/overlays/utility_functions.rs | 2 +- .../document/overlays/utility_types_native.rs | 2 +- .../messages/tool/tool_messages/text_tool.rs | 6 +- node-graph/libraries/core-types/src/text.rs | 82 ------------------- node-graph/nodes/text/src/lib.rs | 26 +++--- node-graph/nodes/text/src/path_builder.rs | 4 +- node-graph/nodes/text/src/text_context.rs | 16 ++-- 7 files changed, 28 insertions(+), 110 deletions(-) delete mode 100644 node-graph/libraries/core-types/src/text.rs diff --git a/editor/src/messages/portfolio/document/overlays/utility_functions.rs b/editor/src/messages/portfolio/document/overlays/utility_functions.rs index 03a34dcbda..cdc6968ee5 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_functions.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_functions.rs @@ -241,7 +241,7 @@ pub fn text_width(text: &str, font_size: f64) -> f64 { max_width: None, max_height: None, tilt: 0.0, - align: TextAlign::Left, + align: TextAlign::AlignLeft, }; // 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 f03e617772..d4580f834b 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types_native.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types_native.rs @@ -1112,7 +1112,7 @@ impl OverlayContextInternal { max_width: None, max_height: None, tilt: 0., - align: TextAlign::Left, // We'll handle alignment manually via pivot + align: TextAlign::AlignLeft, }; // Load Source Sans Pro font data diff --git a/editor/src/messages/tool/tool_messages/text_tool.rs b/editor/src/messages/tool/tool_messages/text_tool.rs index 040e1a6065..e86206d7b9 100644 --- a/editor/src/messages/tool/tool_messages/text_tool.rs +++ b/editor/src/messages/tool/tool_messages/text_tool.rs @@ -204,9 +204,9 @@ fn create_text_widgets(tool: &TextTool, font_catalog: &FontCatalog) -> Vec = [ - TextAlign::Left, - TextAlign::Center, - TextAlign::Right, + TextAlign::AlignLeft, + TextAlign::AlignCenter, + TextAlign::AlignRight, TextAlign::JustifyLeft, TextAlign::JustifyCenter, TextAlign::JustifyRight, diff --git a/node-graph/libraries/core-types/src/text.rs b/node-graph/libraries/core-types/src/text.rs deleted file mode 100644 index 443112a5d5..0000000000 --- a/node-graph/libraries/core-types/src/text.rs +++ /dev/null @@ -1,82 +0,0 @@ -mod font_cache; -mod path_builder; -mod text_context; -mod to_path; - -use dyn_any::DynAny; -pub use font_cache::*; -pub use text_context::TextContext; -pub use to_path::*; - -/// Alignment of lines of type within a text block. -#[repr(C)] -#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[widget(Radio)] -pub enum TextAlign { - #[default] - Left, - Center, - Right, - #[label("Justify Left")] - JustifyLeft, - #[label("Justify Center")] - JustifyCenter, - #[label("Justify Right")] - JustifyRight, - #[label("Justify All")] - JustifyAll, -} - -impl From for parley::Alignment { - fn from(val: TextAlign) -> Self { - match val { - TextAlign::Left => parley::Alignment::Left, - TextAlign::Center => parley::Alignment::Center, - TextAlign::Right => parley::Alignment::Right, - TextAlign::JustifyLeft | TextAlign::JustifyCenter | TextAlign::JustifyRight | TextAlign::JustifyAll => parley::Alignment::Justify, - } - } -} - -impl TextAlign { - pub fn last_line_correction(self) -> Option { - match self { - Self::JustifyCenter => Some(parley::Alignment::Center), - Self::JustifyRight => Some(parley::Alignment::Right), - Self::JustifyAll => Some(parley::Alignment::Justify), - _ => None, - } - } - - pub fn is_justify(self) -> bool { - matches!(self, Self::JustifyLeft | Self::JustifyCenter | Self::JustifyRight | Self::JustifyAll) - } -} - -#[derive(PartialEq, Clone, Copy, Debug)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub struct TypesettingConfig { - pub font_size: f64, - pub line_height_ratio: f64, - pub character_spacing: f64, - pub max_width: Option, - pub max_height: Option, - pub tilt: f64, - pub align: TextAlign, -} - -impl Default for TypesettingConfig { - fn default() -> Self { - Self { - font_size: 24., - line_height_ratio: 1.2, - character_spacing: 0., - max_width: None, - max_height: None, - tilt: 0., - align: TextAlign::default(), - } - } -} diff --git a/node-graph/nodes/text/src/lib.rs b/node-graph/nodes/text/src/lib.rs index f3d0ef1721..7d60038dc7 100644 --- a/node-graph/nodes/text/src/lib.rs +++ b/node-graph/nodes/text/src/lib.rs @@ -31,31 +31,31 @@ pub use vector_types; #[widget(Radio)] pub enum TextAlign { #[default] - Left, - Center, - Right, - #[label("Justify Left")] + AlignLeft, + AlignCenter, + AlignRight, JustifyLeft, - #[label("Justify Center")] JustifyCenter, - #[label("Justify Right")] JustifyRight, - #[label("Justify All")] JustifyAll, } impl From for parley::Alignment { fn from(val: TextAlign) -> Self { match val { - TextAlign::Left => parley::Alignment::Left, - TextAlign::Center => parley::Alignment::Center, - TextAlign::Right => parley::Alignment::Right, - TextAlign::JustifyLeft | TextAlign::JustifyCenter | TextAlign::JustifyRight | TextAlign::JustifyAll => parley::Alignment::Justify, + TextAlign::AlignLeft => parley::Alignment::Left, + TextAlign::AlignCenter => parley::Alignment::Center, + TextAlign::AlignRight => parley::Alignment::Right, + _ => parley::Alignment::Justify, } } } impl TextAlign { + /// What `parley::Alignment` to apply as a post-correction to the last line of a paragraph, or `None` if parley's default already handles it. + /// + /// `JustifyLeft` returns `None` because parley already left-aligns the last line of a `Justify` layout. The other justify modes need + /// the last line shifted (`Center`/`Right`) or its inter-word spaces redistributed (`Justify` / `JustifyAll`). pub fn last_line_correction(self) -> Option { match self { Self::JustifyCenter => Some(parley::Alignment::Center), @@ -64,10 +64,6 @@ impl TextAlign { _ => None, } } - - pub fn is_justify(self) -> bool { - matches!(self, Self::JustifyLeft | Self::JustifyCenter | Self::JustifyRight | Self::JustifyAll) - } } #[derive(PartialEq, Clone, Copy, Debug)] diff --git a/node-graph/nodes/text/src/path_builder.rs b/node-graph/nodes/text/src/path_builder.rs index c985774773..f1a6865610 100644 --- a/node-graph/nodes/text/src/path_builder.rs +++ b/node-graph/nodes/text/src/path_builder.rs @@ -105,8 +105,8 @@ impl PathBuilder { has_geometry } - pub fn render_glyph_run(&mut self, glyph_run: &GlyphRun<'_, ()>, tilt: f64, per_glyph_items: bool, x_offset: f64, space_extra: f32) { - let mut run_x = glyph_run.offset() + x_offset as f32; + pub fn render_glyph_run(&mut self, glyph_run: &GlyphRun<'_, ()>, tilt: f64, per_glyph_items: bool, x_offset: f32, space_extra: f32) { + let mut run_x = glyph_run.offset() + x_offset; let run_y = glyph_run.baseline(); let run = glyph_run.run(); diff --git a/node-graph/nodes/text/src/text_context.rs b/node-graph/nodes/text/src/text_context.rs index 92df5028c4..47b43cc708 100644 --- a/node-graph/nodes/text/src/text_context.rs +++ b/node-graph/nodes/text/src/text_context.rs @@ -115,7 +115,9 @@ impl TextContext { for line in layout.lines() { let range = line.text_range(); - let is_last_para_line = range.end == text.len() || text.get(range.clone()).map(|s| s.ends_with('\n')).unwrap_or(false) || text.as_bytes().get(range.end) == Some(&b'\n'); + // Parley always includes a hard-break `\n` as the last byte of the preceding line's range, so the line + // is at the end of a paragraph if it's the very last line of the buffer or its text ends with `\n`. + let is_last_para_line = range.end == text.len() || text.get(range.clone()).is_some_and(|s| s.ends_with('\n')); let (x_offset, space_extra) = if let (true, Some(correction)) = (is_last_para_line, last_line_correction) { let metrics = line.metrics(); @@ -123,9 +125,11 @@ impl TextContext { let free_space = alignment_width - content_advance; match correction { - parley::Alignment::Center => (free_space as f64 * 0.5, 0_f32), - parley::Alignment::Right => (free_space as f64, 0_f32), + parley::Alignment::Center => (free_space * 0.5, 0.), + parley::Alignment::Right => (free_space, 0.), parley::Alignment::Justify => { + // Exclude trailing-whitespace clusters from the divisor so the redistribution stretches only the internal spaces. + // Parley's `trailing_whitespace` is in advance units, not bytes, so we re-derive the byte boundary here to filter cluster ranges. let line_text = text.get(range.clone()).unwrap_or(""); let trailing_len = line_text.len() - line_text.trim_end().len(); let visible_end_index = range.end - trailing_len; @@ -135,12 +139,12 @@ impl TextContext { .map(|run| run.clusters().filter(|c| c.is_space_or_nbsp() && c.text_range().start < visible_end_index).count()) .sum(); let extra = if space_count > 0 { free_space / space_count as f32 } else { 0. }; - (0_f64, extra) + (0., extra) } - _ => (0_f64, 0_f32), + _ => (0., 0.), } } else { - (0_f64, 0_f32) + (0., 0.) }; for item in line.items() {