Skip to content

Commit 24c857d

Browse files
authored
Make the Text tool control bar font family, style, and size sync with the selected layer (#4118)
* Fix contenteditable preview alignment * Make the Text tool control bar font family, style, and size sync with the selected layer * Tidying up
1 parent 2ae35a6 commit 24c857d

6 files changed

Lines changed: 118 additions & 62 deletions

File tree

editor/src/messages/frontend/frontend_message.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ use crate::messages::tool::tool_messages::eyedropper_tool::PrimarySecondary;
1515
use graph_craft::document::NodeId;
1616
use graphene_std::raster::Image;
1717
use graphene_std::raster::color::Color;
18-
use graphene_std::text::{Font, TextAlign};
18+
use graphene_std::text::Font;
1919
use graphene_std::vector::style::FillChoice;
2020
use std::path::PathBuf;
2121

@@ -51,7 +51,9 @@ pub enum FrontendMessage {
5151
max_width: Option<f64>,
5252
#[serde(rename = "maxHeight")]
5353
max_height: Option<f64>,
54-
align: TextAlign,
54+
align: String,
55+
#[serde(rename = "alignLast")]
56+
align_last: String,
5557
},
5658
DisplayEditableTextboxUpdateFontData {
5759
#[serde(rename = "fontData")]

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1728,7 +1728,9 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
17281728
}
17291729
NodeGraphMessage::SetInputValue { node_id, input_index, value } => {
17301730
let is_fill = matches!(value, TaggedValue::Fill(_));
1731-
let is_text_align = matches!(value, TaggedValue::TextAlign(_));
1731+
let is_text_node = network_interface
1732+
.reference(&node_id, selection_network_path)
1733+
.is_some_and(|reference| reference == DefinitionIdentifier::ProtoNode(graphene_std::text::text::IDENTIFIER));
17321734
let input = NodeInput::value(value, false);
17331735
responses.add(NodeGraphMessage::SetInput {
17341736
input_connector: InputConnector::node(node_id, input_index),
@@ -1738,7 +1740,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
17381740
if is_fill {
17391741
responses.add(OverlaysMessage::Draw);
17401742
}
1741-
if is_text_align {
1743+
if is_text_node {
17421744
responses.add(TextToolMessage::SelectionChanged);
17431745
}
17441746
if network_interface.connected_to_output(&node_id, selection_network_path) {

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -860,6 +860,8 @@ pub fn font_inputs(parameter_widgets_info: ParameterWidgetsInfo) -> (Vec<WidgetI
860860
let font_style = family.closest_style(weight, italic).to_named_style();
861861

862862
move |_| {
863+
// Intentionally drop `font_style_to_restore` on commit so the committed style becomes the new basis
864+
// for subsequent family switches. Preserving the original style intent is hover-only behavior.
863865
let new_font = Font::new(font_family.clone(), font_style.clone());
864866

865867
DeferMessage::AfterGraphRun {

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

Lines changed: 94 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ pub struct TextTool {
3232

3333
pub struct TextOptions {
3434
font_size: f64,
35-
line_height_ratio: f64,
3635
character_spacing: f64,
3736
font: Font,
3837
fill: ToolColorOptions,
@@ -44,7 +43,6 @@ impl Default for TextOptions {
4443
fn default() -> Self {
4544
Self {
4645
font_size: 24.,
47-
line_height_ratio: 1.2,
4846
character_spacing: 0.,
4947
font: Font::new(graphene_std::consts::DEFAULT_FONT_FAMILY.into(), graphene_std::consts::DEFAULT_FONT_STYLE.into()),
5048
fill: ToolColorOptions::new_primary(),
@@ -84,7 +82,6 @@ pub enum TextOptionsUpdate {
8482
FillColorType(ToolColorType),
8583
Font { font: Font },
8684
FontSize(f64),
87-
LineHeightRatio(f64),
8885
Align(TextAlign),
8986
WorkingColors(Option<Color>, Option<Color>),
9087
}
@@ -101,36 +98,63 @@ impl ToolMetadata for TextTool {
10198
}
10299
}
103100

104-
fn create_text_widgets(tool: &TextTool, font_catalog: &FontCatalog) -> Vec<WidgetInstance> {
105-
fn update_options(font: Font, commit_style: Option<String>) -> impl Fn(&()) -> Message + Clone {
106-
let mut font = font;
107-
if let Some(style) = commit_style {
108-
font.font_style = style;
101+
fn create_text_widgets(tool: &TextTool, font_catalog: &FontCatalog, document: &DocumentMessageHandler) -> Vec<WidgetInstance> {
102+
// If a single text layer is selected, the toolbar's font/style menus drive that layer's text node directly, going through the
103+
// same code path as the Properties panel (LoadFontData + SetInputValue, with closest_style and font_style_to_restore bookkeeping).
104+
// Otherwise the menus only update the toolbar option for the next created text.
105+
let text_node_id = can_edit_selected(document).and_then(|layer| graph_modification_utils::get_text_id(layer, &document.network_interface));
106+
107+
let font_input_index = graphene_std::text::text::FontInput::INDEX;
108+
let apply_font = move |new_font: Font| -> Message {
109+
match text_node_id {
110+
Some(node_id) => NodeGraphMessage::SetInputValue {
111+
node_id,
112+
input_index: font_input_index,
113+
value: TaggedValue::Font(new_font),
114+
}
115+
.into(),
116+
None => TextToolMessage::UpdateOptions {
117+
options: TextOptionsUpdate::Font { font: new_font },
118+
}
119+
.into(),
109120
}
110-
111-
move |_| {
112-
TextToolMessage::UpdateOptions {
113-
options: TextOptionsUpdate::Font { font: font.clone() },
121+
};
122+
let preview_font = move |new_font: Font| -> Message {
123+
Message::Batched {
124+
messages: Box::new([PortfolioMessage::LoadFontData { font: new_font.clone() }.into(), apply_font(new_font)]),
125+
}
126+
};
127+
let commit_font = move |new_font: Font| -> Message {
128+
match text_node_id {
129+
Some(_) => DeferMessage::AfterGraphRun {
130+
messages: vec![apply_font(new_font), DocumentMessage::AddTransaction.into()],
114131
}
115-
.into()
132+
.into(),
133+
None => apply_font(new_font),
116134
}
117-
}
135+
};
118136

119137
let font = DropdownInput::new(vec![
120138
font_catalog
121139
.0
122140
.iter()
123141
.map(|family| {
124-
let font = Font::new(family.name.clone(), tool.options.font.font_style.clone());
125-
let commit_style = font_catalog.find_font_style_in_catalog(&tool.options.font).map(|style| style.to_named_style());
126-
let update = update_options(font.clone(), None);
127-
let commit = update_options(font, commit_style);
142+
let current_font = &tool.options.font;
143+
let mut new_font = Font::new(family.name.clone(), current_font.font_style_to_restore.clone().unwrap_or_else(|| current_font.font_style.clone()));
144+
new_font.font_style_to_restore = current_font.font_style_to_restore.clone().or_else(|| Some(new_font.font_style.clone()));
145+
let FontCatalogStyle { weight, italic, .. } = FontCatalogStyle::from_named_style(&new_font.font_style, "");
146+
new_font.font_style = family.closest_style(weight, italic).to_named_style();
147+
148+
// Intentionally drop `font_style_to_restore` on commit so the committed style becomes the new basis for
149+
// subsequent family switches. Preserving the original style intent is hover-only behavior (handled by `new_font`).
150+
let FontCatalogStyle { weight, italic, .. } = FontCatalogStyle::from_named_style(&current_font.font_style, "");
151+
let commit_only_font = Font::new(family.name.clone(), family.closest_style(weight, italic).to_named_style());
128152

129153
MenuListEntry::new(family.name.clone())
130154
.label(family.name.clone())
131155
.font(family.closest_style(400, false).preview_url(&family.name))
132-
.on_update(update)
133-
.on_commit(commit)
156+
.on_update(move |_| preview_font(new_font.clone()))
157+
.on_commit(move |_| commit_font(commit_only_font.clone()))
134158
})
135159
.collect::<Vec<_>>(),
136160
])
@@ -146,13 +170,14 @@ fn create_text_widgets(tool: &TextTool, font_catalog: &FontCatalog) -> Vec<Widge
146170
.map(|family| {
147171
let build_entry = |style: &FontCatalogStyle| {
148172
let font_style = style.to_named_style();
173+
let new_font = Font::new(tool.options.font.font_family.clone(), font_style.clone());
149174

150-
let font = Font::new(tool.options.font.font_family.clone(), font_style.clone());
151-
let commit_style = font_catalog.find_font_style_in_catalog(&tool.options.font).map(|style| style.to_named_style());
152-
let update = update_options(font.clone(), None);
153-
let commit = update_options(font, commit_style);
175+
let new_font_for_commit = new_font.clone();
154176

155-
MenuListEntry::new(font_style.clone()).on_update(update).on_commit(commit).label(font_style)
177+
MenuListEntry::new(font_style.clone())
178+
.label(font_style)
179+
.on_update(move |_| preview_font(new_font.clone()))
180+
.on_commit(move |_| commit_font(new_font_for_commit.clone()))
156181
};
157182

158183
vec![
@@ -192,19 +217,6 @@ fn create_text_widgets(tool: &TextTool, font_catalog: &FontCatalog) -> Vec<Widge
192217
.into()
193218
})
194219
.widget_instance();
195-
let line_height_ratio = NumberInput::new(Some(tool.options.line_height_ratio))
196-
.label("Line Height")
197-
.int()
198-
.min(0.)
199-
.max((1_u64 << f64::MANTISSA_DIGITS) as f64)
200-
.step(0.1)
201-
.on_update(|number_input: &NumberInput| {
202-
TextToolMessage::UpdateOptions {
203-
options: TextOptionsUpdate::LineHeightRatio(number_input.value.unwrap()),
204-
}
205-
.into()
206-
})
207-
.widget_instance();
208220
let align_entries: Vec<_> = TextAlign::list()
209221
.iter()
210222
.flat_map(|section| section.iter())
@@ -229,29 +241,29 @@ fn create_text_widgets(tool: &TextTool, font_catalog: &FontCatalog) -> Vec<Widge
229241
style,
230242
Separator::new(SeparatorStyle::Related).widget_instance(),
231243
size,
232-
Separator::new(SeparatorStyle::Related).widget_instance(),
233-
line_height_ratio,
234244
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
235245
align,
236246
]
237247
}
238248

239249
impl ToolRefreshOptions for TextTool {
240-
fn refresh_options(&self, responses: &mut VecDeque<Message>, cached_data: &CachedData) {
241-
self.send_layout(responses, LayoutTarget::ToolOptions, &cached_data.font_catalog);
250+
fn refresh_options(&self, responses: &mut VecDeque<Message>, _cached_data: &CachedData) {
251+
// Defer to the SelectionChanged handler which has document context, required for the font/style
252+
// dropdowns to bind to the selected text layer's node graph inputs
253+
responses.add(TextToolMessage::SelectionChanged);
242254
}
243255
}
244256

245257
impl TextTool {
246-
fn send_layout(&self, responses: &mut VecDeque<Message>, layout_target: LayoutTarget, font_catalog: &FontCatalog) {
258+
fn send_layout(&self, responses: &mut VecDeque<Message>, layout_target: LayoutTarget, font_catalog: &FontCatalog, document: &DocumentMessageHandler) {
247259
responses.add(LayoutMessage::SendLayout {
248-
layout: self.layout(font_catalog),
260+
layout: self.layout(font_catalog, document),
249261
layout_target,
250262
});
251263
}
252264

253-
fn layout(&self, font_catalog: &FontCatalog) -> Layout {
254-
let mut widgets = create_text_widgets(self, font_catalog);
265+
fn layout(&self, font_catalog: &FontCatalog, document: &DocumentMessageHandler) -> Layout {
266+
let mut widgets = create_text_widgets(self, font_catalog, document);
255267

256268
widgets.push(Separator::new(SeparatorStyle::Unrelated).widget_instance());
257269

@@ -291,14 +303,18 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Text
291303
ToolMessage::Text(TextToolMessage::UpdateOptions { options }) => options,
292304
ToolMessage::Text(TextToolMessage::SelectionChanged) => {
293305
if let Some(layer) = can_edit_selected(context.document)
294-
&& let Some((_, _, typesetting, _)) = graph_modification_utils::get_text(layer, &context.document.network_interface)
306+
&& let Some((_, font, typesetting, _)) = graph_modification_utils::get_text(layer, &context.document.network_interface)
295307
{
296308
self.options.align = typesetting.align;
309+
self.options.font_size = typesetting.font_size;
310+
self.options.font = font.clone();
297311
if let Some(editing_text) = self.tool_data.editing_text.as_mut() {
298312
editing_text.typesetting.align = typesetting.align;
313+
editing_text.typesetting.font_size = typesetting.font_size;
314+
editing_text.font = font.clone();
299315
}
300316
}
301-
self.send_layout(responses, LayoutTarget::ToolOptions, &context.cached_data.font_catalog);
317+
self.send_layout(responses, LayoutTarget::ToolOptions, &context.cached_data.font_catalog, context.document);
302318
return;
303319
}
304320
_ => {
@@ -308,10 +324,28 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Text
308324
};
309325
match options {
310326
TextOptionsUpdate::Font { font } => {
311-
self.options.font = font;
327+
// The toolbar font/style menus go through `SetInputValue` directly when a text layer is selected, so this
328+
// arm only fires when no layer is selected (toolbar font is just the default for the next-created text).
329+
self.options.font = font.clone();
330+
if let Some(editing_text) = self.tool_data.editing_text.as_mut() {
331+
editing_text.font = font;
332+
}
333+
}
334+
TextOptionsUpdate::FontSize(font_size) => {
335+
self.options.font_size = font_size;
336+
if let Some(editing_text) = self.tool_data.editing_text.as_mut() {
337+
editing_text.typesetting.font_size = font_size;
338+
}
339+
if let Some(layer) = can_edit_selected(context.document)
340+
&& let Some(node_id) = graph_modification_utils::get_text_id(layer, &context.document.network_interface)
341+
{
342+
responses.add(NodeGraphMessage::SetInputValue {
343+
node_id,
344+
input_index: graphene_std::text::text::SizeInput::INDEX,
345+
value: TaggedValue::F64(font_size),
346+
});
347+
}
312348
}
313-
TextOptionsUpdate::FontSize(font_size) => self.options.font_size = font_size,
314-
TextOptionsUpdate::LineHeightRatio(line_height_ratio) => self.options.line_height_ratio = line_height_ratio,
315349
TextOptionsUpdate::Align(align) => {
316350
self.options.align = align;
317351
if let Some(editing_text) = self.tool_data.editing_text.as_mut() {
@@ -320,11 +354,11 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Text
320354
if let Some(layer) = can_edit_selected(context.document)
321355
&& let Some(node_id) = graph_modification_utils::get_text_id(layer, &context.document.network_interface)
322356
{
323-
responses.add(NodeGraphMessage::SetInput {
324-
input_connector: InputConnector::node(node_id, graphene_std::text::text::AlignInput::INDEX),
325-
input: NodeInput::value(TaggedValue::TextAlign(align), false),
357+
responses.add(NodeGraphMessage::SetInputValue {
358+
node_id,
359+
input_index: graphene_std::text::text::AlignInput::INDEX,
360+
value: TaggedValue::TextAlign(align),
326361
});
327-
responses.add(NodeGraphMessage::RunDocumentGraph);
328362
}
329363
}
330364
TextOptionsUpdate::FillColor(color) => {
@@ -338,7 +372,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Text
338372
}
339373
}
340374

341-
self.send_layout(responses, LayoutTarget::ToolOptions, &context.cached_data.font_catalog);
375+
self.send_layout(responses, LayoutTarget::ToolOptions, &context.cached_data.font_catalog, context.document);
342376
}
343377

344378
fn actions(&self) -> ActionList {
@@ -446,6 +480,7 @@ impl TextToolData {
446480
/// Set the editing state of the currently modifying layer
447481
fn set_editing(&self, editable: bool, font_cache: &FontCache, responses: &mut VecDeque<Message>) {
448482
if let Some(editing_text) = self.editing_text.as_ref().filter(|_| editable) {
483+
let (align, align_last) = editing_text.typesetting.align.css();
449484
responses.add(FrontendMessage::DisplayEditableTextbox {
450485
text: editing_text.text.clone(),
451486
line_height_ratio: editing_text.typesetting.line_height_ratio,
@@ -455,7 +490,8 @@ impl TextToolData {
455490
transform: editing_text.transform.to_cols_array(),
456491
max_width: editing_text.typesetting.max_width,
457492
max_height: editing_text.typesetting.max_height,
458-
align: editing_text.typesetting.align,
493+
align: align.to_string(),
494+
align_last: align_last.to_string(),
459495
});
460496
} else {
461497
// Check if DisplayRemoveEditableTextbox is already in the responses queue
@@ -930,12 +966,12 @@ impl Fsm for TextToolFsmState {
930966
transform: DAffine2::from_translation(start),
931967
typesetting: TypesettingConfig {
932968
font_size: tool_options.font_size,
933-
line_height_ratio: tool_options.line_height_ratio,
934969
max_width: constraint_size.map(|size| size.x),
935970
character_spacing: tool_options.character_spacing,
936971
max_height: constraint_size.map(|size| size.y),
937972
tilt: tool_options.tilt,
938973
align: tool_options.align,
974+
..TypesettingConfig::default()
939975
},
940976
font: Font::new(tool_options.font.font_family.clone(), tool_options.font.font_style.clone()),
941977
color: tool_options.fill.active_color(),

frontend/src/components/panels/Document.svelte

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,7 @@
380380
textInput.style.fontSize = `${data.fontSize}px`;
381381
textInput.style.color = data.color;
382382
textInput.style.textAlign = data.align;
383+
textInput.style.textAlignLast = data.alignLast;
383384
384385
textInput.oninput = () => {
385386
if (!textInput) return;

node-graph/nodes/text/src/lib.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,19 @@ impl TextAlign {
7171
_ => None,
7272
}
7373
}
74+
75+
/// CSS `(text-align, text-align-last)` values approximating this alignment for the `contenteditable` text overlay.
76+
pub fn css(self) -> (&'static str, &'static str) {
77+
match self {
78+
Self::AlignLeft => ("left", "auto"),
79+
Self::AlignCenter => ("center", "auto"),
80+
Self::AlignRight => ("right", "auto"),
81+
Self::JustifyLeft => ("justify", "auto"),
82+
Self::JustifyCenter => ("justify", "center"),
83+
Self::JustifyRight => ("justify", "right"),
84+
Self::JustifyAll => ("justify", "justify"),
85+
}
86+
}
7487
}
7588

7689
#[derive(PartialEq, Clone, Copy, Debug)]

0 commit comments

Comments
 (0)