Skip to content

Commit 2d86fb2

Browse files
authored
Add line height and character spacing to the Text node (#2016)
1 parent 904cf09 commit 2d86fb2

File tree

10 files changed

+127
-34
lines changed

10 files changed

+127
-34
lines changed

editor/src/messages/portfolio/document/graph_operation/graph_operation_message.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,8 @@ pub enum GraphOperationMessage {
9494
text: String,
9595
font: Font,
9696
size: f64,
97+
line_height_ratio: f64,
98+
character_spacing: f64,
9799
parent: LayerNodeIdentifier,
98100
insert_index: usize,
99101
},

editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -182,12 +182,14 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageData<'_>> for Gr
182182
text,
183183
font,
184184
size,
185+
line_height_ratio,
186+
character_spacing,
185187
parent,
186188
insert_index,
187189
} => {
188190
let mut modify_inputs = ModifyInputsContext::new(network_interface, responses);
189191
let layer = modify_inputs.create_layer(id);
190-
modify_inputs.insert_text(text, font, size, layer);
192+
modify_inputs.insert_text(text, font, size, line_height_ratio, character_spacing, layer);
191193
network_interface.move_layer_to_stack(layer, parent, insert_index, &[]);
192194
responses.add(GraphOperationMessage::StrokeSet { layer, stroke: Stroke::default() });
193195
responses.add(NodeGraphMessage::RunDocumentGraph);
@@ -284,7 +286,7 @@ fn import_usvg_node(modify_inputs: &mut ModifyInputsContext, node: &usvg::Node,
284286
}
285287
usvg::Node::Text(text) => {
286288
let font = Font::new(graphene_core::consts::DEFAULT_FONT_FAMILY.to_string(), graphene_core::consts::DEFAULT_FONT_STYLE.to_string());
287-
modify_inputs.insert_text(text.chunks().iter().map(|chunk| chunk.text()).collect(), font, 24., layer);
289+
modify_inputs.insert_text(text.chunks().iter().map(|chunk| chunk.text()).collect(), font, 24., 1.2, 1., layer);
288290
modify_inputs.fill_set(Fill::Solid(Color::BLACK));
289291
}
290292
}

editor/src/messages/portfolio/document/graph_operation/utility_types.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ impl<'a> ModifyInputsContext<'a> {
177177
}
178178
}
179179

180-
pub fn insert_text(&mut self, text: String, font: Font, size: f64, layer: LayerNodeIdentifier) {
180+
pub fn insert_text(&mut self, text: String, font: Font, size: f64, line_height_ratio: f64, character_spacing: f64, layer: LayerNodeIdentifier) {
181181
let stroke = resolve_document_node_type("Stroke").expect("Stroke node does not exist").default_node_template();
182182
let fill = resolve_document_node_type("Fill").expect("Fill node does not exist").default_node_template();
183183
let transform = resolve_document_node_type("Transform").expect("Transform node does not exist").default_node_template();
@@ -186,6 +186,8 @@ impl<'a> ModifyInputsContext<'a> {
186186
Some(NodeInput::value(TaggedValue::String(text), false)),
187187
Some(NodeInput::value(TaggedValue::Font(font), false)),
188188
Some(NodeInput::value(TaggedValue::F64(size), false)),
189+
Some(NodeInput::value(TaggedValue::F64(line_height_ratio), false)),
190+
Some(NodeInput::value(TaggedValue::F64(character_spacing), false)),
189191
]);
190192

191193
let text_id = NodeId(generate_uuid());

editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2056,11 +2056,20 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
20562056
false,
20572057
),
20582058
NodeInput::value(TaggedValue::F64(24.), false),
2059+
NodeInput::value(TaggedValue::F64(1.2), false),
2060+
NodeInput::value(TaggedValue::F64(1.), false),
20592061
],
20602062
..Default::default()
20612063
},
20622064
persistent_node_metadata: DocumentNodePersistentMetadata {
2063-
input_names: vec!["Editor API".to_string(), "Text".to_string(), "Font".to_string(), "Size".to_string()],
2065+
input_names: vec![
2066+
"Editor API".to_string(),
2067+
"Text".to_string(),
2068+
"Font".to_string(),
2069+
"Size".to_string(),
2070+
"Line Height".to_string(),
2071+
"Character Spacing".to_string(),
2072+
],
20642073
output_names: vec!["Vector".to_string()],
20652074
..Default::default()
20662075
},

editor/src/messages/portfolio/document/node_graph/node_properties.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1731,12 +1731,16 @@ pub(crate) fn text_properties(document_node: &DocumentNode, node_id: NodeId, _co
17311731
let text = text_area_widget(document_node, node_id, 1, "Text", true);
17321732
let (font, style) = font_inputs(document_node, node_id, 2, "Font", true);
17331733
let size = number_widget(document_node, node_id, 3, "Size", NumberInput::default().unit(" px").min(1.), true);
1734+
let line_height_ratio = number_widget(document_node, node_id, 4, "Line Height", NumberInput::default().min(0.).step(0.1), true);
1735+
let character_spacing = number_widget(document_node, node_id, 5, "Character Spacing", NumberInput::default().min(0.).step(0.1), true);
17341736

17351737
let mut result = vec![LayoutGroup::Row { widgets: text }, LayoutGroup::Row { widgets: font }];
17361738
if let Some(style) = style {
17371739
result.push(LayoutGroup::Row { widgets: style });
17381740
}
17391741
result.push(LayoutGroup::Row { widgets: size });
1742+
result.push(LayoutGroup::Row { widgets: line_height_ratio });
1743+
result.push(LayoutGroup::Row { widgets: character_spacing });
17401744
result
17411745
}
17421746

editor/src/messages/portfolio/portfolio_message_handler.rs

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -471,8 +471,9 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageData<'_>> for PortfolioMes
471471
log::error!("could not get node in deserialize_document");
472472
continue;
473473
};
474+
let inputs_count = node.inputs.len();
474475

475-
if reference == "Fill" && node.inputs.len() == 8 {
476+
if reference == "Fill" && inputs_count == 8 {
476477
let node_definition = resolve_document_node_type(reference).unwrap();
477478
let document_node = node_definition.default_node_template().document_node;
478479
document.network_interface.replace_implementation(node_id, &[], document_node.implementation.clone());
@@ -529,6 +530,26 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageData<'_>> for PortfolioMes
529530
}
530531
}
531532

533+
// Upgrade Text node to include line height and character spacing, which were previously hardcoded to 1, from https://github.com/GraphiteEditor/Graphite/pull/2016
534+
if reference == "Text" && inputs_count == 4 {
535+
let node_definition = resolve_document_node_type(reference).unwrap();
536+
let document_node = node_definition.default_node_template().document_node;
537+
document.network_interface.replace_implementation(node_id, &[], document_node.implementation.clone());
538+
539+
let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), &[]);
540+
541+
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), &[]);
542+
document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[1].clone(), &[]);
543+
document.network_interface.set_input(&InputConnector::node(*node_id, 2), old_inputs[2].clone(), &[]);
544+
document.network_interface.set_input(&InputConnector::node(*node_id, 3), old_inputs[3].clone(), &[]);
545+
document
546+
.network_interface
547+
.set_input(&InputConnector::node(*node_id, 4), NodeInput::value(TaggedValue::F64(1.), false), &[]);
548+
document
549+
.network_interface
550+
.set_input(&InputConnector::node(*node_id, 5), NodeInput::value(TaggedValue::F64(1.), false), &[]);
551+
}
552+
532553
// Upgrade layer implementation from https://github.com/GraphiteEditor/Graphite/pull/1946
533554
if reference == "Merge" || reference == "Artboard" {
534555
let node_definition = crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type(reference).unwrap();

editor/src/messages/tool/common_functionality/graph_modification_utils.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,14 +127,16 @@ pub fn get_text_id(layer: LayerNodeIdentifier, network_interface: &NodeNetworkIn
127127
}
128128

129129
/// Gets properties from the Text node
130-
pub fn get_text(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<(&String, &Font, f64)> {
130+
pub fn get_text(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<(&String, &Font, f64, f64, f64)> {
131131
let inputs = NodeGraphLayer::new(layer, network_interface).find_node_inputs("Text")?;
132132

133133
let Some(TaggedValue::String(text)) = &inputs[1].as_value() else { return None };
134134
let Some(TaggedValue::Font(font)) = &inputs[2].as_value() else { return None };
135135
let Some(&TaggedValue::F64(font_size)) = inputs[3].as_value() else { return None };
136+
let Some(&TaggedValue::F64(line_height_ratio)) = inputs[4].as_value() else { return None };
137+
let Some(&TaggedValue::F64(character_spacing)) = inputs[5].as_value() else { return None };
136138

137-
Some((text, font, font_size))
139+
Some((text, font, font_size, line_height_ratio, character_spacing))
138140
}
139141

140142
pub fn get_stroke_width(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<f64> {

editor/src/messages/tool/tool_messages/text_tool.rs

Lines changed: 53 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@ pub struct TextTool {
2424
}
2525

2626
pub struct TextOptions {
27-
font_size: u32,
27+
font_size: f64,
28+
line_height_ratio: f64,
29+
character_spacing: f64,
2830
font_name: String,
2931
font_style: String,
3032
fill: ToolColorOptions,
@@ -33,7 +35,9 @@ pub struct TextOptions {
3335
impl Default for TextOptions {
3436
fn default() -> Self {
3537
Self {
36-
font_size: 24,
38+
font_size: 24.,
39+
line_height_ratio: 1.2,
40+
character_spacing: 1.,
3741
font_name: graphene_core::consts::DEFAULT_FONT_FAMILY.into(),
3842
font_style: graphene_core::consts::DEFAULT_FONT_STYLE.into(),
3943
fill: ToolColorOptions::new_primary(),
@@ -63,7 +67,9 @@ pub enum TextOptionsUpdate {
6367
FillColor(Option<Color>),
6468
FillColorType(ToolColorType),
6569
Font { family: String, style: String },
66-
FontSize(u32),
70+
FontSize(f64),
71+
LineHeightRatio(f64),
72+
CharacterSpacing(f64),
6773
WorkingColors(Option<Color>, Option<Color>),
6874
}
6975

@@ -100,20 +106,40 @@ fn create_text_widgets(tool: &TextTool) -> Vec<WidgetHolder> {
100106
.into()
101107
})
102108
.widget_holder();
103-
let size = NumberInput::new(Some(tool.options.font_size as f64))
109+
let size = NumberInput::new(Some(tool.options.font_size))
104110
.unit(" px")
105111
.label("Size")
106112
.int()
107113
.min(1.)
108114
.max((1_u64 << f64::MANTISSA_DIGITS) as f64)
109-
.on_update(|number_input: &NumberInput| TextToolMessage::UpdateOptions(TextOptionsUpdate::FontSize(number_input.value.unwrap() as u32)).into())
115+
.on_update(|number_input: &NumberInput| TextToolMessage::UpdateOptions(TextOptionsUpdate::FontSize(number_input.value.unwrap())).into())
116+
.widget_holder();
117+
let line_height_ratio = NumberInput::new(Some(tool.options.line_height_ratio))
118+
.label("Line Height")
119+
.int()
120+
.min(0.)
121+
.max((1_u64 << f64::MANTISSA_DIGITS) as f64)
122+
.step(0.1)
123+
.on_update(|number_input: &NumberInput| TextToolMessage::UpdateOptions(TextOptionsUpdate::LineHeightRatio(number_input.value.unwrap())).into())
124+
.widget_holder();
125+
let character_spacing = NumberInput::new(Some(tool.options.character_spacing))
126+
.label("Character Spacing")
127+
.int()
128+
.min(0.)
129+
.max((1_u64 << f64::MANTISSA_DIGITS) as f64)
130+
.step(0.1)
131+
.on_update(|number_input: &NumberInput| TextToolMessage::UpdateOptions(TextOptionsUpdate::CharacterSpacing(number_input.value.unwrap())).into())
110132
.widget_holder();
111133
vec![
112134
font,
113135
Separator::new(SeparatorType::Related).widget_holder(),
114136
style,
115137
Separator::new(SeparatorType::Related).widget_holder(),
116138
size,
139+
Separator::new(SeparatorType::Related).widget_holder(),
140+
line_height_ratio,
141+
Separator::new(SeparatorType::Related).widget_holder(),
142+
character_spacing,
117143
]
118144
}
119145

@@ -149,6 +175,8 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for TextToo
149175
self.send_layout(responses, LayoutTarget::ToolOptions);
150176
}
151177
TextOptionsUpdate::FontSize(font_size) => self.options.font_size = font_size,
178+
TextOptionsUpdate::LineHeightRatio(line_height_ratio) => self.options.line_height_ratio = line_height_ratio,
179+
TextOptionsUpdate::CharacterSpacing(character_spacing) => self.options.character_spacing = character_spacing,
152180
TextOptionsUpdate::FillColor(color) => {
153181
self.options.fill.custom_color = color;
154182
self.options.fill.color_type = ToolColorType::Custom;
@@ -200,6 +228,8 @@ pub struct EditingText {
200228
text: String,
201229
font: Font,
202230
font_size: f64,
231+
line_height_ratio: f64,
232+
character_spacing: f64,
203233
color: Option<Color>,
204234
transform: DAffine2,
205235
}
@@ -233,11 +263,13 @@ impl TextToolData {
233263
fn load_layer_text_node(&mut self, document: &DocumentMessageHandler) -> Option<()> {
234264
let transform = document.metadata().transform_to_viewport(self.layer);
235265
let color = graph_modification_utils::get_fill_color(self.layer, &document.network_interface).unwrap_or(Color::BLACK);
236-
let (text, font, font_size) = graph_modification_utils::get_text(self.layer, &document.network_interface)?;
266+
let (text, font, font_size, line_height_ratio, character_spacing) = graph_modification_utils::get_text(self.layer, &document.network_interface)?;
237267
self.editing_text = Some(EditingText {
238268
text: text.clone(),
239269
font: font.clone(),
240270
font_size,
271+
line_height_ratio,
272+
character_spacing,
241273
color: Some(color),
242274
transform,
243275
});
@@ -295,6 +327,8 @@ impl TextToolData {
295327
text: String::new(),
296328
font: editing_text.font.clone(),
297329
size: editing_text.font_size,
330+
line_height_ratio: editing_text.line_height_ratio,
331+
character_spacing: editing_text.character_spacing,
298332
parent: document.new_layer_parent(true),
299333
insert_index: 0,
300334
});
@@ -364,7 +398,14 @@ impl Fsm for TextToolFsmState {
364398
});
365399
if let Some(editing_text) = tool_data.editing_text.as_ref() {
366400
let buzz_face = font_cache.get(&editing_text.font).map(|data| load_face(data));
367-
let far = graphene_core::text::bounding_box(&tool_data.new_text, buzz_face, editing_text.font_size, None);
401+
let far = graphene_core::text::bounding_box(
402+
&tool_data.new_text,
403+
buzz_face,
404+
editing_text.font_size,
405+
editing_text.line_height_ratio,
406+
editing_text.character_spacing,
407+
None,
408+
);
368409
if far.x != 0. && far.y != 0. {
369410
let quad = Quad::from_box([DVec2::ZERO, far]);
370411
let transformed_quad = document.metadata().transform_to_viewport(tool_data.layer) * quad;
@@ -376,11 +417,11 @@ impl Fsm for TextToolFsmState {
376417
}
377418
(_, TextToolMessage::Overlays(mut overlay_context)) => {
378419
for layer in document.network_interface.selected_nodes(&[]).unwrap().selected_layers(document.metadata()) {
379-
let Some((text, font, font_size)) = graph_modification_utils::get_text(layer, &document.network_interface) else {
420+
let Some((text, font, font_size, line_height_ratio, character_spacing)) = graph_modification_utils::get_text(layer, &document.network_interface) else {
380421
continue;
381422
};
382423
let buzz_face = font_cache.get(font).map(|data| load_face(data));
383-
let far = graphene_core::text::bounding_box(text, buzz_face, font_size, None);
424+
let far = graphene_core::text::bounding_box(text, buzz_face, font_size, line_height_ratio, character_spacing, None);
384425
let quad = Quad::from_box([DVec2::ZERO, far]);
385426
let multiplied = document.metadata().transform_to_viewport(layer) * quad;
386427
overlay_context.quad(multiplied, None);
@@ -392,7 +433,9 @@ impl Fsm for TextToolFsmState {
392433
tool_data.editing_text = Some(EditingText {
393434
text: String::new(),
394435
transform: DAffine2::from_translation(input.mouse.position),
395-
font_size: tool_options.font_size as f64,
436+
font_size: tool_options.font_size,
437+
line_height_ratio: tool_options.line_height_ratio,
438+
character_spacing: tool_options.character_spacing,
396439
font: Font::new(tool_options.font_name.clone(), tool_options.font_style.clone()),
397440
color: tool_options.fill.active_color(),
398441
});

0 commit comments

Comments
 (0)