diff --git a/editor/src/messages/frontend/frontend_message.rs b/editor/src/messages/frontend/frontend_message.rs index 55fa2c71f1..e84507dd68 100644 --- a/editor/src/messages/frontend/frontend_message.rs +++ b/editor/src/messages/frontend/frontend_message.rs @@ -10,7 +10,7 @@ use crate::messages::tool::utility_types::HintData; use graph_craft::document::NodeId; use graphene_std::raster::Image; use graphene_std::raster::color::Color; -use graphene_std::text::Font; +use graphene_std::text::{Font, TextAlign}; #[impl_message(Message, Frontend)] #[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)] @@ -38,6 +38,7 @@ pub enum FrontendMessage { max_width: Option, #[serde(rename = "maxHeight")] max_height: Option, + align: TextAlign, }, DisplayEditableTextboxTransform { transform: [f64; 6], 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 85cce0c5c8..5bcf0dabbc 100644 --- a/editor/src/messages/portfolio/document/graph_operation/utility_types.rs +++ b/editor/src/messages/portfolio/document/graph_operation/utility_types.rs @@ -198,6 +198,7 @@ impl<'a> ModifyInputsContext<'a> { Some(NodeInput::value(TaggedValue::OptionalF64(typesetting.max_width), false)), Some(NodeInput::value(TaggedValue::OptionalF64(typesetting.max_height), false)), Some(NodeInput::value(TaggedValue::F64(typesetting.tilt), false)), + Some(NodeInput::value(TaggedValue::TextAlign(typesetting.align), false)), ]); let text_id = NodeId::new(); 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 240a89f739..d24ebaa23b 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 @@ -1304,6 +1304,7 @@ fn static_nodes() -> Vec { NodeInput::value(TaggedValue::OptionalF64(TypesettingConfig::default().max_width), false), NodeInput::value(TaggedValue::OptionalF64(TypesettingConfig::default().max_height), false), NodeInput::value(TaggedValue::F64(TypesettingConfig::default().tilt), false), + NodeInput::value(TaggedValue::TextAlign(text::TextAlign::default()), false), NodeInput::value(TaggedValue::Bool(false), false), ], ..Default::default() @@ -1337,7 +1338,6 @@ fn static_nodes() -> Vec { "TODO", WidgetOverride::Number(NumberInputSettings { unit: Some(" px".to_string()), - min: Some(0.), step: Some(0.1), ..Default::default() }), @@ -1372,6 +1372,7 @@ fn static_nodes() -> Vec { ..Default::default() }), ), + InputMetadata::with_name_description_override("Align", "TODO", WidgetOverride::Custom("text_align".to_string())), ("Per-Glyph Instances", "Splits each text glyph into its own instance, i.e. row in the table of vector data.").into(), ], output_names: vec!["Vector".to_string()], @@ -2404,6 +2405,13 @@ fn static_input_properties() -> InputProperties { )]) }), ); + map.insert( + "text_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/node_graph/node_properties.rs b/editor/src/messages/portfolio/document/node_graph/node_properties.rs index e9b6562216..64fb50ada1 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_properties.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_properties.rs @@ -20,7 +20,7 @@ use graphene_std::raster::{ SelectiveColorChoice, }; use graphene_std::raster_types::{CPU, GPU, RasterDataTable}; -use graphene_std::text::Font; +use graphene_std::text::{Font, TextAlign}; use graphene_std::transform::{Footprint, ReferencePoint, Transform}; use graphene_std::vector::VectorDataTable; use graphene_std::vector::misc::GridType; @@ -223,6 +223,7 @@ pub(crate) fn property_from_type( Some(x) if x == TypeId::of::() => enum_choice::().for_socket(default_info).property_row(), Some(x) if x == TypeId::of::() => enum_choice::().for_socket(default_info).property_row(), Some(x) if x == TypeId::of::() => enum_choice::().for_socket(default_info).property_row(), + Some(x) if x == TypeId::of::() => enum_choice::().for_socket(default_info).property_row(), Some(x) if x == TypeId::of::() => enum_choice::().for_socket(default_info).property_row(), Some(x) if x == TypeId::of::() => enum_choice::().for_socket(default_info).property_row(), Some(x) if x == TypeId::of::() => enum_choice::().for_socket(default_info).property_row(), @@ -2018,7 +2019,7 @@ pub mod choice { let updater = updater_factory(); let committer = committer_factory(); let entry = RadioEntryData::new(var_meta.name).on_update(move |_| updater(item)).on_commit(committer); - match (var_meta.icon.as_deref(), var_meta.docstring.as_deref()) { + match (var_meta.icon, var_meta.docstring) { (None, None) => entry.label(var_meta.label), (None, Some(doc)) => entry.label(var_meta.label).tooltip(doc), (Some(icon), None) => entry.icon(icon).tooltip(var_meta.label), diff --git a/editor/src/messages/portfolio/document_migration.rs b/editor/src/messages/portfolio/document_migration.rs index b56d764fba..a55d63ff53 100644 --- a/editor/src/messages/portfolio/document_migration.rs +++ b/editor/src/messages/portfolio/document_migration.rs @@ -10,7 +10,7 @@ use glam::IVec2; use graph_craft::document::DocumentNode; use graph_craft::document::{DocumentNodeImplementation, NodeInput, value::TaggedValue}; use graphene_std::ProtoNodeIdentifier; -use graphene_std::text::TypesettingConfig; +use graphene_std::text::{TextAlign, TypesettingConfig}; use graphene_std::uuid::NodeId; use graphene_std::vector::style::{PaintOrder, StrokeAlign}; use graphene_std::vector::{VectorData, VectorDataTable}; @@ -646,7 +646,7 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId], } // Upgrade Text node to include line height and character spacing, which were previously hardcoded to 1, from https://github.com/GraphiteEditor/Graphite/pull/2016 - if reference == "Text" && inputs_count != 10 { + if reference == "Text" && inputs_count != 11 { let mut template = 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)?; @@ -702,8 +702,17 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId], ); document.network_interface.set_input( &InputConnector::node(*node_id, 9), - if inputs_count >= 10 { + if inputs_count >= 11 { old_inputs[9].clone() + } else { + NodeInput::value(TaggedValue::TextAlign(TextAlign::default()), false) + }, + network_path, + ); + document.network_interface.set_input( + &InputConnector::node(*node_id, 10), + if inputs_count >= 11 { + old_inputs[10].clone() } else { NodeInput::value(TaggedValue::Bool(false), false) }, 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 126e5b160c..2f5e10ce88 100644 --- a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs +++ b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs @@ -368,9 +368,8 @@ pub fn get_text(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInter let Some(&TaggedValue::OptionalF64(max_width)) = inputs[6].as_value() else { return None }; let Some(&TaggedValue::OptionalF64(max_height)) = inputs[7].as_value() else { return None }; let Some(&TaggedValue::F64(tilt)) = inputs[8].as_value() else { return None }; - let Some(TaggedValue::Bool(per_glyph_instances)) = &inputs[9].as_value() else { - return None; - }; + let Some(&TaggedValue::TextAlign(align)) = inputs[9].as_value() else { return None }; + let Some(&TaggedValue::Bool(per_glyph_instances)) = inputs[10].as_value() else { return None }; let typesetting = TypesettingConfig { font_size, @@ -379,8 +378,9 @@ pub fn get_text(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInter character_spacing, max_height, tilt, + align, }; - Some((text, font, typesetting, *per_glyph_instances)) + Some((text, font, typesetting, per_glyph_instances)) } pub fn get_stroke_width(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option { diff --git a/editor/src/messages/tool/tool_messages/brush_tool.rs b/editor/src/messages/tool/tool_messages/brush_tool.rs index 202d026e0b..76061c12b4 100644 --- a/editor/src/messages/tool/tool_messages/brush_tool.rs +++ b/editor/src/messages/tool/tool_messages/brush_tool.rs @@ -144,7 +144,11 @@ impl LayoutHolder for BrushTool { let draw_mode_entries: Vec<_> = [DrawMode::Draw, DrawMode::Erase, DrawMode::Restore] .into_iter() - .map(|draw_mode| RadioEntryData::new(format!("{draw_mode:?}")).on_update(move |_| BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::DrawMode(draw_mode)).into())) + .map(|draw_mode| { + RadioEntryData::new(format!("{draw_mode:?}")) + .label(format!("{draw_mode:?}")) + .on_update(move |_| BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::DrawMode(draw_mode)).into()) + }) .collect(); widgets.push(RadioInput::new(draw_mode_entries).selected_index(Some(self.options.draw_mode as u32)).widget_holder()); diff --git a/editor/src/messages/tool/tool_messages/text_tool.rs b/editor/src/messages/tool/tool_messages/text_tool.rs index 0653bb83d8..cb86fecacb 100644 --- a/editor/src/messages/tool/tool_messages/text_tool.rs +++ b/editor/src/messages/tool/tool_messages/text_tool.rs @@ -17,7 +17,7 @@ use graph_craft::document::value::TaggedValue; use graph_craft::document::{NodeId, NodeInput}; use graphene_std::Color; use graphene_std::renderer::Quad; -use graphene_std::text::{Font, FontCache, TypesettingConfig, lines_clipping, load_font}; +use graphene_std::text::{Font, FontCache, TextAlign, TypesettingConfig, lines_clipping, load_font}; use graphene_std::vector::style::Fill; #[derive(Default, ExtractField)] @@ -35,6 +35,7 @@ pub struct TextOptions { font_style: String, fill: ToolColorOptions, tilt: f64, + align: TextAlign, } impl Default for TextOptions { @@ -47,6 +48,7 @@ impl Default for TextOptions { font_style: graphene_std::consts::DEFAULT_FONT_STYLE.into(), fill: ToolColorOptions::new_primary(), tilt: 0., + align: TextAlign::default(), } } } @@ -78,7 +80,7 @@ pub enum TextOptionsUpdate { Font { family: String, style: String }, FontSize(f64), LineHeightRatio(f64), - CharacterSpacing(f64), + Align(TextAlign), WorkingColors(Option, Option), } @@ -131,14 +133,15 @@ fn create_text_widgets(tool: &TextTool) -> Vec { .step(0.1) .on_update(|number_input: &NumberInput| TextToolMessage::UpdateOptions(TextOptionsUpdate::LineHeightRatio(number_input.value.unwrap())).into()) .widget_holder(); - let character_spacing = NumberInput::new(Some(tool.options.character_spacing)) - .label("Char. Spacing") - .int() - .min(0.) - .max((1_u64 << f64::MANTISSA_DIGITS) as f64) - .step(0.1) - .on_update(|number_input: &NumberInput| TextToolMessage::UpdateOptions(TextOptionsUpdate::CharacterSpacing(number_input.value.unwrap())).into()) - .widget_holder(); + let align_entries: 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(TextOptionsUpdate::Align(align)).into()) + }) + .collect(); + let align = RadioInput::new(align_entries).selected_index(Some(tool.options.align as u32)).widget_holder(); vec![ font, Separator::new(SeparatorType::Related).widget_holder(), @@ -148,7 +151,7 @@ fn create_text_widgets(tool: &TextTool) -> Vec { Separator::new(SeparatorType::Related).widget_holder(), line_height_ratio, Separator::new(SeparatorType::Related).widget_holder(), - character_spacing, + align, ] } @@ -186,7 +189,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::CharacterSpacing(character_spacing) => self.options.character_spacing = character_spacing, + TextOptionsUpdate::Align(align) => self.options.align = align, TextOptionsUpdate::FillColor(color) => { self.options.fill.custom_color = color; self.options.fill.color_type = ToolColorType::Custom; @@ -314,6 +317,7 @@ impl TextToolData { transform: editing_text.transform.to_cols_array(), max_width: editing_text.typesetting.max_width, max_height: editing_text.typesetting.max_height, + align: editing_text.typesetting.align, }); } else { // Check if DisplayRemoveEditableTextbox is already in the responses queue @@ -792,6 +796,7 @@ impl Fsm for TextToolFsmState { character_spacing: tool_options.character_spacing, max_height: constraint_size.map(|size| size.y), tilt: tool_options.tilt, + align: tool_options.align, }, font: Font::new(tool_options.font_name.clone(), tool_options.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 bd36152f04..dea13269e9 100644 --- a/frontend/src/components/panels/Document.svelte +++ b/frontend/src/components/panels/Document.svelte @@ -343,6 +343,7 @@ textInput.style.lineHeight = `${displayEditableTextbox.lineHeightRatio}`; textInput.style.fontSize = `${displayEditableTextbox.fontSize}px`; textInput.style.color = displayEditableTextbox.color.toHexOptionalAlpha() || "transparent"; + textInput.style.textAlign = displayEditableTextbox.align; textInput.oninput = () => { if (!textInput) return; @@ -774,7 +775,6 @@ .text-input { word-break: break-all; unicode-bidi: plaintext; - text-align: left; } .text-input div { @@ -789,7 +789,6 @@ white-space: pre-wrap; word-break: normal; unicode-bidi: plaintext; - text-align: left; display: inline-block; // Workaround to force Chrome to display the flashing text entry cursor when text is empty padding-left: 1px; diff --git a/frontend/src/messages.ts b/frontend/src/messages.ts index 28b777f806..48abf5af72 100644 --- a/frontend/src/messages.ts +++ b/frontend/src/messages.ts @@ -814,6 +814,8 @@ export class UpdateDocumentLayerStructureJs extends JsMessage { readonly dataBuffer!: DataBuffer; } +export type TextAlign = "Left" | "Center" | "Right" | "JustifyLeft"; + export class DisplayEditableTextbox extends JsMessage { readonly text!: string; @@ -831,6 +833,8 @@ export class DisplayEditableTextbox extends JsMessage { readonly maxWidth!: undefined | number; readonly maxHeight!: undefined | number; + + readonly align!: TextAlign; } export class DisplayEditableTextboxTransform extends JsMessage { diff --git a/node-graph/gcore/src/text.rs b/node-graph/gcore/src/text.rs index 485cb2a884..3337a1488c 100644 --- a/node-graph/gcore/src/text.rs +++ b/node-graph/gcore/src/text.rs @@ -1,5 +1,31 @@ mod font_cache; mod to_path; +use dyn_any::DynAny; pub use font_cache::*; pub use to_path::*; + +/// Alignment of lines of type within a text block. +#[repr(C)] +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)] +#[widget(Radio)] +pub enum TextAlign { + #[default] + Left, + Center, + Right, + #[label("Justify")] + JustifyLeft, + // TODO: JustifyCenter, JustifyRight, JustifyAll +} + +impl From for parley::Alignment { + fn from(val: TextAlign) -> Self { + match val { + TextAlign::Left => parley::Alignment::Left, + TextAlign::Center => parley::Alignment::Middle, + TextAlign::Right => parley::Alignment::Right, + TextAlign::JustifyLeft => parley::Alignment::Justified, + } + } +} diff --git a/node-graph/gcore/src/text/to_path.rs b/node-graph/gcore/src/text/to_path.rs index 880a05fe8e..a1aa6e2228 100644 --- a/node-graph/gcore/src/text/to_path.rs +++ b/node-graph/gcore/src/text/to_path.rs @@ -1,10 +1,11 @@ +use super::TextAlign; use crate::instances::Instance; use crate::vector::{PointId, VectorData, VectorDataTable}; use bezier_rs::{ManipulatorGroup, Subpath}; use core::cell::RefCell; use glam::{DAffine2, DVec2}; use parley::fontique::Blob; -use parley::{Alignment, AlignmentOptions, FontContext, GlyphRun, Layout, LayoutContext, LineHeight, PositionedLayoutItem, StyleProperty}; +use parley::{AlignmentOptions, FontContext, GlyphRun, Layout, LayoutContext, LineHeight, PositionedLayoutItem, StyleProperty}; use skrifa::GlyphId; use skrifa::instance::{LocationRef, NormalizedCoord, Size}; use skrifa::outline::{DrawSettings, OutlinePen}; @@ -103,6 +104,7 @@ pub struct TypesettingConfig { pub max_width: Option, pub max_height: Option, pub tilt: f64, + pub align: TextAlign, } impl Default for TypesettingConfig { @@ -114,6 +116,7 @@ impl Default for TypesettingConfig { max_width: None, max_height: None, tilt: 0., + align: TextAlign::default(), } } } @@ -197,7 +200,7 @@ fn layout_text(str: &str, font_data: Option>, typesetting: TypesettingC let mut layout: Layout<()> = builder.build(str); layout.break_all_lines(typesetting.max_width.map(|mw| mw as f32)); - layout.align(typesetting.max_width.map(|max_w| max_w as f32), Alignment::Left, AlignmentOptions::default()); + layout.align(typesetting.max_width.map(|max_w| max_w as f32), typesetting.align.into(), AlignmentOptions::default()); Some(layout) } diff --git a/node-graph/graph-craft/src/document/value.rs b/node-graph/graph-craft/src/document/value.rs index b6cdd179af..8e24d5e359 100644 --- a/node-graph/graph-craft/src/document/value.rs +++ b/node-graph/graph-craft/src/document/value.rs @@ -248,6 +248,7 @@ tagged_value! { ReferencePoint(graphene_core::transform::ReferencePoint), CentroidType(graphene_core::vector::misc::CentroidType), BooleanOperation(graphene_path_bool::BooleanOperation), + TextAlign(graphene_core::text::TextAlign), } impl TaggedValue { diff --git a/node-graph/gstd/src/text.rs b/node-graph/gstd/src/text.rs index ec5df2522b..7ea7e6badb 100644 --- a/node-graph/gstd/src/text.rs +++ b/node-graph/gstd/src/text.rs @@ -28,6 +28,7 @@ fn text<'i: 'n>( #[unit("°")] #[default(0.)] tilt: f64, + align: TextAlign, /// Splits each text glyph into its own instance, i.e. row in the table of vector data. #[default(false)] per_glyph_instances: bool, @@ -39,6 +40,7 @@ fn text<'i: 'n>( max_width, max_height, tilt, + align, }; let font_data = editor.font_cache.get(&font_name).map(|f| load_font(f));