Add text alignment to the Text node (#2920)
* Add text alignment to Text node * Lots of renames and improvements * Add text alignment to the Text tool --------- Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
parent
91156d295c
commit
85021fd9e0
|
|
@ -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<f64>,
|
||||
#[serde(rename = "maxHeight")]
|
||||
max_height: Option<f64>,
|
||||
align: TextAlign,
|
||||
},
|
||||
DisplayEditableTextboxTransform {
|
||||
transform: [f64; 6],
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -1304,6 +1304,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
|||
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<DocumentNodeDefinition> {
|
|||
"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<DocumentNodeDefinition> {
|
|||
..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::<text::TextAlign>().for_socket(ParameterWidgetsInfo::new(node_id, index, true, context)).property_row();
|
||||
Ok(vec![choices])
|
||||
}),
|
||||
);
|
||||
map
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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::<StrokeAlign>() => enum_choice::<StrokeAlign>().for_socket(default_info).property_row(),
|
||||
Some(x) if x == TypeId::of::<PaintOrder>() => enum_choice::<PaintOrder>().for_socket(default_info).property_row(),
|
||||
Some(x) if x == TypeId::of::<ArcType>() => enum_choice::<ArcType>().for_socket(default_info).property_row(),
|
||||
Some(x) if x == TypeId::of::<TextAlign>() => enum_choice::<TextAlign>().for_socket(default_info).property_row(),
|
||||
Some(x) if x == TypeId::of::<MergeByDistanceAlgorithm>() => enum_choice::<MergeByDistanceAlgorithm>().for_socket(default_info).property_row(),
|
||||
Some(x) if x == TypeId::of::<PointSpacingType>() => enum_choice::<PointSpacingType>().for_socket(default_info).property_row(),
|
||||
Some(x) if x == TypeId::of::<BooleanOperation>() => enum_choice::<BooleanOperation>().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),
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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<f64> {
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
||||
|
|
|
|||
|
|
@ -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<Color>, Option<Color>),
|
||||
}
|
||||
|
||||
|
|
@ -131,14 +133,15 @@ fn create_text_widgets(tool: &TextTool) -> Vec<WidgetHolder> {
|
|||
.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<WidgetHolder> {
|
|||
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<ToolMessage, &mut ToolActionMessageContext<'a>> 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(),
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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<TextAlign> 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<f64>,
|
||||
pub max_height: Option<f64>,
|
||||
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<Blob<u8>>, 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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
Loading…
Reference in New Issue