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 a84e5cb127..effff06cb7 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_properties.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_properties.rs @@ -2292,7 +2292,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 c5471f0bff..22654bf2fa 100644 --- a/editor/src/messages/tool/tool_messages/text_tool.rs +++ b/editor/src/messages/tool/tool_messages/text_tool.rs @@ -204,17 +204,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, @@ -290,7 +298,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 29917694b6..612e9c5aaa 100644 --- a/node-graph/libraries/core-types/src/text.rs +++ b/node-graph/libraries/core-types/src/text.rs @@ -18,9 +18,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 { @@ -29,11 +34,27 @@ 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, serde::Serialize, serde::Deserialize)] pub struct TypesettingConfig { pub font_size: f64, diff --git a/node-graph/nodes/text/src/lib.rs b/node-graph/nodes/text/src/lib.rs index ca4738ffa1..25b7da281f 100644 --- a/node-graph/nodes/text/src/lib.rs +++ b/node-graph/nodes/text/src/lib.rs @@ -22,9 +22,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 { @@ -33,11 +38,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, serde::Serialize, serde::Deserialize)] pub struct TypesettingConfig { pub font_size: f64, diff --git a/node-graph/nodes/text/src/path_builder.rs b/node-graph/nodes/text/src/path_builder.rs index c5ba250409..230c3be732 100644 --- a/node-graph/nodes/text/src/path_builder.rs +++ b/node-graph/nodes/text/src/path_builder.rs @@ -35,10 +35,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_instances: bool) { + fn draw_glyph( + &mut self, + glyph: &OutlineGlyph<'_>, + size: f32, + normalized_coords: &[NormalizedCoord], + glyph_offset: DVec2, + style_skew: Option, + skew: DAffine2, + per_glyph_instances: 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 @@ -62,10 +72,12 @@ impl PathBuilder { self.vector_table.get_mut(0).unwrap().element.append_subpath(subpath, false); } } + + has_geometry } - pub fn render_glyph_run(&mut self, glyph_run: &GlyphRun<'_, ()>, tilt: f64, per_glyph_instances: bool) { - let mut run_x = glyph_run.offset(); + pub fn render_glyph_run(&mut self, glyph_run: &GlyphRun<'_, ()>, tilt: f64, per_glyph_instances: 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(); @@ -113,7 +125,11 @@ impl PathBuilder { if !per_glyph_instances { self.origin = glyph_offset; } - self.draw_glyph(&glyph_outline, font_size, &normalized_coords, glyph_offset, style_skew, skew, per_glyph_instances); + let drew_geometry = self.draw_glyph(&glyph_outline, font_size, &normalized_coords, glyph_offset, style_skew, skew, per_glyph_instances); + + 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 7934feb885..d458f998c6 100644 --- a/node-graph/nodes/text/src/text_context.rs +++ b/node-graph/nodes/text/src/text_context.rs @@ -92,14 +92,46 @@ impl TextContext { return Table::new_from_element(Vector::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_instances, layout.scale() as f64); 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 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) + } + _ => (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_instances); + path_builder.render_glyph_run(&glyph_run, typesetting.tilt, per_glyph_instances, x_offset, space_extra); } } }