Make font selection show a live preview on hover; move its code to the backend (#3487)

* Remove FontInput.svelte

* Move font picking to the backend

* Fix Text tool font choice style turning to "-" on font that doesn't support previous style
This commit is contained in:
Keavon Chambers
2025-12-19 22:17:28 -08:00
committed by GitHub
parent 6f087eb981
commit 2d6d054359
31 changed files with 766 additions and 702 deletions

View File

@@ -51,7 +51,7 @@ const SIDE_EFFECT_FREE_MESSAGES: &[MessageDiscriminant] = &[
NodeGraphMessageDiscriminant::RunDocumentGraph,
))),
MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::SubmitActiveGraphRender),
MessageDiscriminant::Frontend(FrontendMessageDiscriminant::TriggerFontLoad),
MessageDiscriminant::Frontend(FrontendMessageDiscriminant::TriggerFontDataLoad),
MessageDiscriminant::Frontend(FrontendMessageDiscriminant::UpdateUIScale),
];
/// Since we don't need to update the frontend multiple times per frame,
@@ -69,6 +69,7 @@ const FRONTEND_UPDATE_MESSAGES: &[MessageDiscriminant] = &[
const DEBUG_MESSAGE_BLOCK_LIST: &[MessageDiscriminant] = &[
MessageDiscriminant::Broadcast(BroadcastMessageDiscriminant::TriggerEvent(EventMessageDiscriminant::AnimationFrame)),
MessageDiscriminant::Animation(AnimationMessageDiscriminant::IncrementFrameCounter),
MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::AutoSaveAllDocuments),
];
// TODO: Find a way to combine these with the list above. We use strings for now since these are the standard variant names used by multiple messages. But having these also type-checked would be best.
const DEBUG_MESSAGE_ENDING_BLOCK_LIST: &[&str] = &["PointerMove", "PointerOutsideViewport", "Overlays", "Draw", "CurrentTime", "Time"];
@@ -179,7 +180,7 @@ impl Dispatcher {
}
Message::Frontend(message) => {
// Handle these messages immediately by returning early
if let FrontendMessage::TriggerFontLoad { .. } = message {
if let FrontendMessage::TriggerFontDataLoad { .. } | FrontendMessage::TriggerFontCatalogLoad = message {
self.responses.push(message);
self.cleanup_queues(false);
@@ -359,8 +360,9 @@ impl Dispatcher {
fn log_message(&self, message: &Message, queues: &[VecDeque<Message>], message_logging_verbosity: MessageLoggingVerbosity) {
let discriminant = MessageDiscriminant::from(message);
let is_blocked = DEBUG_MESSAGE_BLOCK_LIST.contains(&discriminant) || DEBUG_MESSAGE_ENDING_BLOCK_LIST.iter().any(|blocked_name| discriminant.local_name().ends_with(blocked_name));
let is_empty_batched = if let Message::Batched { messages } = message { messages.is_empty() } else { false };
if !is_blocked {
if !is_blocked && !is_empty_batched {
match message_logging_verbosity {
MessageLoggingVerbosity::Off => {}
MessageLoggingVerbosity::Names => {

View File

@@ -39,7 +39,8 @@ pub enum FrontendMessage {
#[serde(rename = "fontSize")]
font_size: f64,
color: Color,
url: String,
#[serde(rename = "fontData")]
font_data: Vec<u8>,
transform: [f64; 6],
#[serde(rename = "maxWidth")]
max_width: Option<f64>,
@@ -47,6 +48,10 @@ pub enum FrontendMessage {
max_height: Option<f64>,
align: TextAlign,
},
DisplayEditableTextboxUpdateFontData {
#[serde(rename = "fontData")]
font_data: Vec<u8>,
},
DisplayEditableTextboxTransform {
transform: [f64; 6],
},
@@ -92,8 +97,10 @@ pub enum FrontendMessage {
name: String,
filename: String,
},
TriggerFontLoad {
TriggerFontCatalogLoad,
TriggerFontDataLoad {
font: Font,
url: String,
},
TriggerImport,
TriggerPersistenceRemoveDocument {

View File

@@ -2,7 +2,6 @@ use crate::messages::input_mapper::utility_types::input_keyboard::KeysGroup;
use crate::messages::layout::utility_types::widget_prelude::*;
use crate::messages::prelude::*;
use graphene_std::raster::color::Color;
use graphene_std::text::Font;
use graphene_std::vector::style::{FillChoice, GradientStops};
use serde_json::Value;
use std::collections::HashMap;
@@ -48,7 +47,6 @@ impl MessageHandler<LayoutMessage, LayoutMessageContext<'_>> for LayoutMessageHa
}
LayoutMessage::WidgetValueUpdate { layout_target, widget_id, value } => {
self.handle_widget_callback(layout_target, widget_id, value, WidgetValueAction::Update, responses);
responses.add(LayoutMessage::ResendActiveWidget { layout_target, widget_id });
}
}
}
@@ -264,44 +262,6 @@ impl LayoutMessageHandler {
responses.add(callback_message);
}
Widget::FontInput(font_input) => {
let callback_message = match action {
WidgetValueAction::Commit => (font_input.on_commit.callback)(&()),
WidgetValueAction::Update => {
let Some(update_value) = value.as_object() else {
error!("FontInput update was not of type: object");
return;
};
let Some(font_family_value) = update_value.get("fontFamily") else {
error!("FontInput update does not have a fontFamily");
return;
};
let Some(font_style_value) = update_value.get("fontStyle") else {
error!("FontInput update does not have a fontStyle");
return;
};
let Some(font_family) = font_family_value.as_str() else {
error!("FontInput update fontFamily was not of type: string");
return;
};
let Some(font_style) = font_style_value.as_str() else {
error!("FontInput update fontStyle was not of type: string");
return;
};
font_input.font_family = font_family.into();
font_input.font_style = font_style.into();
responses.add(PortfolioMessage::LoadFont {
font: Font::new(font_family.into(), font_style.into()),
});
(font_input.on_update.callback)(font_input)
}
};
responses.add(callback_message);
}
Widget::IconButton(icon_button) => {
let callback_message = match action {
WidgetValueAction::Commit => (icon_button.on_commit.callback)(&()),

View File

@@ -353,43 +353,7 @@ impl From<Vec<WidgetInstance>> for LayoutGroup {
}
impl LayoutGroup {
/// Applies a tooltip label to all widgets in this row or column without a tooltip.
pub fn with_tooltip_label(self, label: impl Into<String>) -> Self {
let (is_col, mut widgets) = match self {
LayoutGroup::Column { widgets } => (true, widgets),
LayoutGroup::Row { widgets } => (false, widgets),
_ => unimplemented!(),
};
let label = label.into();
for widget in &mut widgets {
let val = match &mut widget.widget {
Widget::CheckboxInput(x) => &mut x.tooltip_label,
Widget::ColorInput(x) => &mut x.tooltip_label,
Widget::CurveInput(x) => &mut x.tooltip_label,
Widget::DropdownInput(x) => &mut x.tooltip_label,
Widget::FontInput(x) => &mut x.tooltip_label,
Widget::IconButton(x) => &mut x.tooltip_label,
Widget::IconLabel(x) => &mut x.tooltip_label,
Widget::ImageButton(x) => &mut x.tooltip_label,
Widget::ImageLabel(x) => &mut x.tooltip_label,
Widget::NumberInput(x) => &mut x.tooltip_label,
Widget::ParameterExposeButton(x) => &mut x.tooltip_label,
Widget::PopoverButton(x) => &mut x.tooltip_label,
Widget::TextAreaInput(x) => &mut x.tooltip_label,
Widget::TextButton(x) => &mut x.tooltip_label,
Widget::TextInput(x) => &mut x.tooltip_label,
Widget::TextLabel(x) => &mut x.tooltip_label,
Widget::BreadcrumbTrailButtons(x) => &mut x.tooltip_label,
Widget::ReferencePointInput(_) | Widget::RadioInput(_) | Widget::Separator(_) | Widget::ShortcutLabel(_) | Widget::WorkingColorsInput(_) | Widget::NodeCatalog(_) => continue,
};
if val.is_empty() {
val.clone_from(&label);
}
}
if is_col { Self::Column { widgets } } else { Self::Row { widgets } }
}
/// Applies a tooltip description to all widgets in this row or column without a tooltip.
/// Applies a tooltip description to all widgets without a tooltip in this row or column.
pub fn with_tooltip_description(self, description: impl Into<String>) -> Self {
let (is_col, mut widgets) = match self {
LayoutGroup::Column { widgets } => (true, widgets),
@@ -403,20 +367,24 @@ impl LayoutGroup {
Widget::ColorInput(x) => &mut x.tooltip_description,
Widget::CurveInput(x) => &mut x.tooltip_description,
Widget::DropdownInput(x) => &mut x.tooltip_description,
Widget::FontInput(x) => &mut x.tooltip_description,
Widget::IconButton(x) => &mut x.tooltip_description,
Widget::IconLabel(x) => &mut x.tooltip_description,
Widget::ImageButton(x) => &mut x.tooltip_description,
Widget::ImageLabel(x) => &mut x.tooltip_description,
Widget::NumberInput(x) => &mut x.tooltip_description,
Widget::ParameterExposeButton(x) => &mut x.tooltip_description,
Widget::PopoverButton(x) => &mut x.tooltip_description,
Widget::TextAreaInput(x) => &mut x.tooltip_description,
Widget::TextButton(x) => &mut x.tooltip_description,
Widget::TextInput(x) => &mut x.tooltip_description,
Widget::TextLabel(x) => &mut x.tooltip_description,
Widget::BreadcrumbTrailButtons(x) => &mut x.tooltip_description,
Widget::ReferencePointInput(_) | Widget::RadioInput(_) | Widget::Separator(_) | Widget::ShortcutLabel(_) | Widget::WorkingColorsInput(_) | Widget::NodeCatalog(_) => continue,
Widget::ReferencePointInput(_)
| Widget::RadioInput(_)
| Widget::Separator(_)
| Widget::ShortcutLabel(_)
| Widget::WorkingColorsInput(_)
| Widget::NodeCatalog(_)
| Widget::ParameterExposeButton(_) => continue,
};
if val.is_empty() {
val.clone_from(&description);
@@ -727,7 +695,6 @@ pub enum Widget {
ColorInput(ColorInput),
CurveInput(CurveInput),
DropdownInput(DropdownInput),
FontInput(FontInput),
IconButton(IconButton),
IconLabel(IconLabel),
ImageButton(ImageButton),
@@ -782,7 +749,6 @@ impl DiffUpdate {
Widget::CheckboxInput(widget) => widget.tooltip_shortcut.as_mut(),
Widget::ColorInput(widget) => widget.tooltip_shortcut.as_mut(),
Widget::DropdownInput(widget) => widget.tooltip_shortcut.as_mut(),
Widget::FontInput(widget) => widget.tooltip_shortcut.as_mut(),
Widget::IconButton(widget) => widget.tooltip_shortcut.as_mut(),
Widget::NumberInput(widget) => widget.tooltip_shortcut.as_mut(),
Widget::ParameterExposeButton(widget) => widget.tooltip_shortcut.as_mut(),
@@ -838,10 +804,38 @@ impl DiffUpdate {
(recursive_wrapper.0)(entry_sections, &recursive_wrapper)
};
// Hash the menu list entry sections for caching purposes
let hash_menu_list_entry_sections = |entry_sections: &MenuListEntrySections| {
struct RecursiveHasher<'a> {
hasher: DefaultHasher,
hash_fn: &'a dyn Fn(&mut RecursiveHasher, &MenuListEntrySections),
}
let mut recursive_hasher = RecursiveHasher {
hasher: DefaultHasher::new(),
hash_fn: &|recursive_hasher, entry_sections| {
for (index, entries) in entry_sections.iter().enumerate() {
index.hash(&mut recursive_hasher.hasher);
for entry in entries {
entry.hash(&mut recursive_hasher.hasher);
(recursive_hasher.hash_fn)(recursive_hasher, &entry.children);
}
}
},
};
(recursive_hasher.hash_fn)(&mut recursive_hasher, entry_sections);
recursive_hasher.hasher.finish()
};
// Apply shortcut conversions to all widgets that have menu lists
let convert_menu_lists = |widget_instance: &mut WidgetInstance| match &mut widget_instance.widget {
Widget::DropdownInput(dropdown_input) => apply_action_shortcut_to_menu_lists(&mut dropdown_input.entries),
Widget::TextButton(text_button) => apply_action_shortcut_to_menu_lists(&mut text_button.menu_list_children),
Widget::DropdownInput(dropdown_input) => {
apply_action_shortcut_to_menu_lists(&mut dropdown_input.entries);
dropdown_input.entries_hash = hash_menu_list_entry_sections(&dropdown_input.entries);
}
Widget::TextButton(text_button) => {
apply_action_shortcut_to_menu_lists(&mut text_button.menu_list_children);
text_button.menu_list_children_hash = hash_menu_list_entry_sections(&text_button.menu_list_children);
}
_ => {}
};

View File

@@ -144,6 +144,10 @@ pub struct TextButton {
#[serde(rename = "menuListChildren")]
pub menu_list_children: MenuListEntrySections,
#[serde(rename = "menuListChildrenHash")]
#[widget_builder(skip)]
pub menu_list_children_hash: u64,
// Callbacks
#[serde(skip)]
#[derivative(Debug = "ignore", PartialEq = "ignore")]

View File

@@ -80,6 +80,10 @@ pub struct DropdownInput {
#[widget_builder(constructor)]
pub entries: MenuListEntrySections,
#[serde(rename = "entriesHash")]
#[widget_builder(skip)]
pub entries_hash: u64,
// This uses `u32` instead of `usize` since it will be serialized as a normal JS number (replace this with `usize` after switching to a Rust-based GUI)
#[serde(rename = "selectedIndex")]
pub selected_index: Option<u32>,
@@ -94,6 +98,9 @@ pub struct DropdownInput {
pub narrow: bool,
#[serde(rename = "virtualScrolling")]
pub virtual_scrolling: bool,
#[serde(rename = "tooltipLabel")]
pub tooltip_label: String,
@@ -142,6 +149,10 @@ pub struct MenuListEntry {
pub children: MenuListEntrySections,
#[serde(rename = "childrenHash")]
#[widget_builder(skip)]
pub children_hash: u64,
// Callbacks
#[serde(skip)]
#[derivative(Debug = "ignore", PartialEq = "ignore")]
@@ -152,39 +163,13 @@ pub struct MenuListEntry {
pub on_commit: WidgetCallback<()>,
}
#[derive(Clone, serde::Serialize, serde::Deserialize, Derivative, WidgetBuilder, specta::Type)]
#[derivative(Debug, PartialEq, Default)]
pub struct FontInput {
#[serde(rename = "fontFamily")]
#[widget_builder(constructor)]
pub font_family: String,
#[serde(rename = "fontStyle")]
#[widget_builder(constructor)]
pub font_style: String,
#[serde(rename = "isStyle")]
pub is_style_picker: bool,
pub disabled: bool,
#[serde(rename = "tooltipLabel")]
pub tooltip_label: String,
#[serde(rename = "tooltipDescription")]
pub tooltip_description: String,
#[serde(rename = "tooltipShortcut")]
pub tooltip_shortcut: Option<ActionShortcut>,
// Callbacks
#[serde(skip)]
#[derivative(Debug = "ignore", PartialEq = "ignore")]
pub on_update: WidgetCallback<FontInput>,
#[serde(skip)]
#[derivative(Debug = "ignore", PartialEq = "ignore")]
pub on_commit: WidgetCallback<()>,
impl std::hash::Hash for MenuListEntry {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.value.hash(state);
self.label.hash(state);
self.icon.hash(state);
self.disabled.hash(state);
}
}
#[derive(Clone, serde::Serialize, serde::Deserialize, Derivative, WidgetBuilder, specta::Type)]

View File

@@ -19,8 +19,7 @@ use crate::messages::portfolio::document::utility_types::document_metadata::{Doc
use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate, AlignAxis, FlipAxis, PTZ};
use crate::messages::portfolio::document::utility_types::network_interface::{FlowType, InputConnector, NodeTemplate};
use crate::messages::portfolio::document::utility_types::nodes::RawBuffer;
use crate::messages::portfolio::utility_types::PanelType;
use crate::messages::portfolio::utility_types::PersistentData;
use crate::messages::portfolio::utility_types::{FontCatalog, PanelType, PersistentData};
use crate::messages::prelude::*;
use crate::messages::tool::common_functionality::graph_modification_utils::{self, get_blend_mode, get_fill, get_opacity};
use crate::messages::tool::tool_messages::select_tool::SelectToolPointerKeys;
@@ -36,6 +35,7 @@ use graphene_std::raster::BlendMode;
use graphene_std::raster_types::Raster;
use graphene_std::subpath::Subpath;
use graphene_std::table::Table;
use graphene_std::text::Font;
use graphene_std::vector::PointId;
use graphene_std::vector::click_target::{ClickTarget, ClickTargetType};
use graphene_std::vector::misc::{dvec2_to_point, point_to_dvec2};
@@ -2171,54 +2171,28 @@ impl DocumentMessageHandler {
}
/// Loads all of the fonts in the document.
pub fn load_layer_resources(&self, responses: &mut VecDeque<Message>) {
let mut fonts = HashSet::new();
for (_node_id, node, _) in self.document_network().recursive_nodes() {
pub fn load_layer_resources(&self, responses: &mut VecDeque<Message>, font_catalog: &FontCatalog) {
let mut fonts_to_load = HashSet::new();
for (_, node, _) in self.document_network().recursive_nodes() {
for input in &node.inputs {
if let Some(TaggedValue::Font(font)) = input.as_value() {
fonts.insert(font.clone());
fonts_to_load.insert(font.clone());
}
}
}
for font in fonts {
responses.add_front(FrontendMessage::TriggerFontLoad { font });
for font in fonts_to_load {
if let Some(style) = font_catalog.find_font_style_in_catalog(&font) {
responses.add_front(FrontendMessage::TriggerFontDataLoad {
font: Font::new(font.font_family, style.to_named_style()),
url: style.url,
});
}
}
}
pub fn update_document_widgets(&self, responses: &mut VecDeque<Message>, animation_is_playing: bool, time: Duration) {
// // Document mode (dropdown menu at the left of the bar above the viewport, before the tool options)
// let layout = Layout(vec![LayoutGroup::Row {
// widgets: vec![
// DropdownInput::new(
// vec![vec![
// MenuListEntry::new(format!("{:?}", DocumentMode::DesignMode))
// .label(DocumentMode::DesignMode.to_string())
// .icon(DocumentMode::DesignMode.icon_name()),
// // TODO: See issue #330
// MenuListEntry::new(format!("{:?}", DocumentMode::SelectMode))
// .label(DocumentMode::SelectMode.to_string())
// .icon(DocumentMode::SelectMode.icon_name())
// .on_commit(|_| todo!()),
// // TODO: See issue #331
// MenuListEntry::new(format!("{:?}", DocumentMode::GuideMode))
// .label(DocumentMode::GuideMode.to_string())
// .icon(DocumentMode::GuideMode.icon_name())
// .on_commit(|_| todo!()),
// ]])
// .selected_index(Some(self.document_mode as u32))
// .draw_icon(true)
// .interactive(false)
// .widget_instance(),
// Separator::new(SeparatorType::Section).widget_instance(),
// ],
// }]);
// responses.add(LayoutMessage::SendLayout {
// layout,
// layout_target: LayoutTarget::DocumentMode,
// });
// Document bar (right portion of the bar above the viewport)
let mut snapping_state = self.snapping_state.clone();
let mut snapping_state2 = self.snapping_state.clone();

View File

@@ -4,6 +4,7 @@ use super::document_node_definitions::{NODE_OVERRIDES, NodePropertiesContext};
use super::utility_types::FrontendGraphDataType;
use crate::messages::layout::utility_types::widget_prelude::*;
use crate::messages::portfolio::document::utility_types::network_interface::InputConnector;
use crate::messages::portfolio::utility_types::{FontCatalogStyle, PersistentData};
use crate::messages::prelude::*;
use choice::enum_choice;
use dyn_any::DynAny;
@@ -34,7 +35,7 @@ pub(crate) fn string_properties(text: &str) -> Vec<LayoutGroup> {
fn optionally_update_value<T>(value: impl Fn(&T) -> Option<TaggedValue> + 'static + Send + Sync, node_id: NodeId, input_index: usize) -> impl Fn(&T) -> Message + 'static + Send + Sync {
move |input_value: &T| match value(input_value) {
Some(value) => NodeGraphMessage::SetInputValue { node_id, input_index, value }.into(),
_ => Message::NoOp,
None => Message::NoOp,
}
}
@@ -86,6 +87,7 @@ pub fn start_widgets(parameter_widgets_info: ParameterWidgetsInfo) -> Vec<Widget
input_type,
blank_assist,
exposable,
..
} = parameter_widgets_info;
let Some(document_node) = document_node else {
@@ -759,36 +761,141 @@ pub fn array_of_vec2_widget(parameter_widgets_info: ParameterWidgetsInfo, text_p
}
pub fn font_inputs(parameter_widgets_info: ParameterWidgetsInfo) -> (Vec<WidgetInstance>, Option<Vec<WidgetInstance>>) {
let ParameterWidgetsInfo { document_node, node_id, index, .. } = parameter_widgets_info;
let ParameterWidgetsInfo {
persistent_data,
document_node,
node_id,
index,
..
} = parameter_widgets_info;
let mut first_widgets = start_widgets(parameter_widgets_info);
let mut second_widgets = None;
let from_font_input = |font: &FontInput| TaggedValue::Font(Font::new(font.font_family.clone(), font.font_style.clone()));
let Some(document_node) = document_node else { return (Vec::new(), None) };
let Some(input) = document_node.inputs.get(index) else {
log::warn!("A widget failed to be built because its node's input index is invalid.");
return (vec![], None);
};
if let Some(TaggedValue::Font(font)) = &input.as_non_exposed_value() {
first_widgets.extend_from_slice(&[
Separator::new(SeparatorType::Unrelated).widget_instance(),
FontInput::new(font.font_family.clone(), font.font_style.clone())
.on_update(update_value(from_font_input, node_id, index))
.on_commit(commit_value)
.widget_instance(),
DropdownInput::new(vec![
persistent_data
.font_catalog
.0
.iter()
.map(|family| {
MenuListEntry::new(family.name.clone())
.label(family.name.clone())
.font(family.closest_style(400, false).preview_url(&family.name))
.on_update({
// Construct the new font using the new family and the initial or previous style, although this style might not exist in the catalog
let mut new_font = Font::new(family.name.clone(), font.font_style_to_restore.clone().unwrap_or_else(|| font.font_style.clone()));
new_font.font_style_to_restore = font.font_style_to_restore.clone();
// If not already, store the initial style so it can be restored if the user switches to another family
if new_font.font_style_to_restore.is_none() {
new_font.font_style_to_restore = Some(new_font.font_style.clone());
}
// Use the closest style available in the family for the new font to ensure the style exists
let FontCatalogStyle { weight, italic, .. } = FontCatalogStyle::from_named_style(&new_font.font_style, "");
new_font.font_style = family.closest_style(weight, italic).to_named_style();
move |_| {
let new_font = new_font.clone();
Message::Batched {
messages: Box::new([
PortfolioMessage::LoadFontData { font: new_font.clone() }.into(),
update_value(move |_| TaggedValue::Font(new_font.clone()), node_id, index)(&()),
]),
}
}
})
.on_commit({
// Use the new value from the user selection
let font_family = family.name.clone();
// Use the previous style selection and extract its weight and italic properties, then find the closest style in the new family
let FontCatalogStyle { weight, italic, .. } = FontCatalogStyle::from_named_style(&font.font_style, "");
let font_style = family.closest_style(weight, italic).to_named_style();
move |_| {
let new_font = Font::new(font_family.clone(), font_style.clone());
DeferMessage::AfterGraphRun {
messages: vec![update_value(move |_| TaggedValue::Font(new_font.clone()), node_id, index)(&()), commit_value(&())],
}
.into()
}
})
})
.collect::<Vec<_>>(),
])
.selected_index(persistent_data.font_catalog.0.iter().position(|family| family.name == font.font_family).map(|i| i as u32))
.virtual_scrolling(true)
.widget_instance(),
]);
let mut second_row = vec![TextLabel::new("").widget_instance()];
add_blank_assist(&mut second_row);
second_row.extend_from_slice(&[
Separator::new(SeparatorType::Unrelated).widget_instance(),
FontInput::new(font.font_family.clone(), font.font_style.clone())
.is_style_picker(true)
.on_update(update_value(from_font_input, node_id, index))
.on_commit(commit_value)
.widget_instance(),
DropdownInput::new({
persistent_data
.font_catalog
.0
.iter()
.find(|family| family.name == font.font_family)
.map(|family| {
let build_entry = |style: &FontCatalogStyle| {
let font_style = style.to_named_style();
MenuListEntry::new(font_style.clone())
.label(font_style.clone())
.on_update({
let font_family = font.font_family.clone();
let font_style = font_style.clone();
move |_| {
// Keep the existing family
let new_font = Font::new(font_family.clone(), font_style.clone());
Message::Batched {
messages: Box::new([
PortfolioMessage::LoadFontData { font: new_font.clone() }.into(),
update_value(move |_| TaggedValue::Font(new_font.clone()), node_id, index)(&()),
]),
}
}
})
.on_commit(commit_value)
};
vec![
family.styles.iter().filter(|style| !style.italic).map(build_entry).collect::<Vec<_>>(),
family.styles.iter().filter(|style| style.italic).map(build_entry).collect::<Vec<_>>(),
]
})
.filter(|styles| !styles.is_empty())
.unwrap_or_default()
})
.selected_index(
persistent_data
.font_catalog
.0
.iter()
.find(|family| family.name == font.font_family)
.and_then(|family| {
let not_italic = family.styles.iter().filter(|style| !style.italic);
let italic = family.styles.iter().filter(|style| style.italic);
not_italic.chain(italic).position(|style| style.to_named_style() == font.font_style)
})
.map(|i| i as u32),
)
.widget_instance(),
]);
second_widgets = Some(second_row);
}
@@ -2033,6 +2140,7 @@ pub fn math_properties(node_id: NodeId, context: &mut NodePropertiesContext) ->
}
pub struct ParameterWidgetsInfo<'a> {
persistent_data: &'a PersistentData,
document_node: Option<&'a DocumentNode>,
node_id: NodeId,
index: usize,
@@ -2053,6 +2161,7 @@ impl<'a> ParameterWidgetsInfo<'a> {
let document_node = context.network_interface.document_node(&node_id, context.selection_network_path);
ParameterWidgetsInfo {
persistent_data: context.persistent_data,
document_node,
node_id,
index,
@@ -2227,7 +2336,7 @@ pub mod choice {
let mut row = LayoutGroup::Row { widgets };
if let Some(desc) = self.widget_factory.description() {
row = row.with_tooltip_label(desc);
row = row.with_tooltip_description(desc);
}
row
}

View File

@@ -227,7 +227,7 @@ pub static GLOBAL_FONT_CACHE: LazyLock<FontCache> = LazyLock::new(|| {
// Initialize with the hardcoded font used by overlay text
const FONT_DATA: &[u8] = include_bytes!("source-sans-pro-regular.ttf");
let font = Font::new("Source Sans Pro".to_string(), "Regular".to_string());
font_cache.insert(font, String::new(), FONT_DATA.to_vec());
font_cache.insert(font, FONT_DATA.to_vec());
font_cache
});

View File

@@ -2,6 +2,7 @@ use super::document::utility_types::document_metadata::LayerNodeIdentifier;
use super::utility_types::PanelType;
use crate::messages::frontend::utility_types::{ExportBounds, FileType};
use crate::messages::portfolio::document::utility_types::clipboards::Clipboard;
use crate::messages::portfolio::utility_types::FontCatalog;
use crate::messages::prelude::*;
use graphene_std::Color;
use graphene_std::raster::Image;
@@ -46,19 +47,21 @@ pub enum PortfolioMessage {
},
DestroyAllDocuments,
EditorPreferences,
FontCatalogLoaded {
catalog: FontCatalog,
},
LoadFontData {
font: Font,
},
FontLoaded {
font_family: String,
font_style: String,
preview_url: String,
data: Vec<u8>,
},
Import,
LoadDocumentResources {
document_id: DocumentId,
},
LoadFont {
font: Font,
},
NewDocumentWithName {
name: String,
},

View File

@@ -349,16 +349,31 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
self.active_document_id = None;
responses.add(MenuBarMessage::SendLayout);
}
PortfolioMessage::FontLoaded {
font_family,
font_style,
preview_url,
data,
} => {
let font = Font::new(font_family, font_style);
PortfolioMessage::FontCatalogLoaded { catalog } => {
self.persistent_data.font_catalog = catalog;
self.persistent_data.font_cache.insert(font, preview_url, data);
if let Some(document_id) = self.active_document_id {
responses.add(PortfolioMessage::LoadDocumentResources { document_id });
}
// Load the default font
let font = Font::new(graphene_std::consts::DEFAULT_FONT_FAMILY.into(), graphene_std::consts::DEFAULT_FONT_STYLE.into());
responses.add(PortfolioMessage::LoadFontData { font });
}
PortfolioMessage::LoadFontData { font } => {
if let Some(style) = self.persistent_data.font_catalog.find_font_style_in_catalog(&font) {
let font = Font::new(font.font_family, style.to_named_style());
if !self.persistent_data.font_cache.loaded_font(&font) {
responses.add(FrontendMessage::TriggerFontDataLoad { font, url: style.url });
}
}
}
PortfolioMessage::FontLoaded { font_family, font_style, data } => {
let font = Font::new(font_family, font_style);
self.persistent_data.font_cache.insert(font, data);
self.executor.update_font_cache(self.persistent_data.font_cache.clone());
for document_id in self.document_ids.iter() {
let node_to_inspect = self.node_to_inspect();
@@ -382,6 +397,10 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
if self.active_document_mut().is_some() {
responses.add(NodeGraphMessage::RunDocumentGraph);
}
if current_tool == &ToolType::Text {
responses.add(TextToolMessage::RefreshEditingFontData);
}
}
PortfolioMessage::EditorPreferences => self.executor.update_editor_preferences(preferences.editor_preferences()),
PortfolioMessage::Import => {
@@ -389,13 +408,14 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
responses.add(FrontendMessage::TriggerImport);
}
PortfolioMessage::LoadDocumentResources { document_id } => {
if let Some(document) = self.document_mut(document_id) {
document.load_layer_resources(responses);
let catalog = &self.persistent_data.font_catalog;
if catalog.0.is_empty() {
log::error!("Tried to load document resources before font catalog was loaded");
}
}
PortfolioMessage::LoadFont { font } => {
if !self.persistent_data.font_cache.loaded_font(&font) {
responses.add_front(FrontendMessage::TriggerFontLoad { font });
if let Some(document) = self.documents.get_mut(&document_id) {
document.load_layer_resources(responses, catalog);
}
}
PortfolioMessage::NewDocumentWithName { name } => {
@@ -592,7 +612,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
added_nodes = true;
}
document.load_layer_resources(responses);
document.load_layer_resources(responses, &self.persistent_data.font_catalog);
let new_ids: HashMap<_, _> = entry.nodes.iter().map(|(id, _)| (*id, NodeId::new())).collect();
let layer = LayerNodeIdentifier::new_unchecked(new_ids[&NodeId(0)]);
all_new_ids.extend(new_ids.values().cloned());
@@ -973,7 +993,9 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
};
if !document.is_loaded {
document.is_loaded = true;
responses.add(PortfolioMessage::LoadDocumentResources { document_id });
if self.persistent_data.font_catalog.0.is_empty() {
responses.add_front(FrontendMessage::TriggerFontCatalogLoad);
}
responses.add(PortfolioMessage::UpdateDocumentWidgets);
responses.add(PropertiesPanelMessage::Clear);
}
@@ -1229,10 +1251,6 @@ impl PortfolioMessageHandler {
if self.active_document().is_some() {
responses.add(EventMessage::ToolAbort);
responses.add(ToolMessage::DeactivateTools);
} else {
// Load the default font upon creating the first document
let font = Font::new(graphene_std::consts::DEFAULT_FONT_FAMILY.into(), graphene_std::consts::DEFAULT_FONT_STYLE.into());
responses.add(FrontendMessage::TriggerFontLoad { font });
}
// TODO: Remove this and find a way to fix the issue where creating a new document when the node graph is open causes the transform in the new document to be incorrect

View File

@@ -1,11 +1,87 @@
use graphene_std::text::FontCache;
use graphene_std::text::{Font, FontCache};
#[derive(Debug, Default)]
pub struct PersistentData {
pub font_cache: FontCache,
pub font_catalog: FontCatalog,
pub use_vello: bool,
}
// TODO: Should this be a BTreeMap instead?
#[derive(Clone, Debug, Default, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct FontCatalog(pub Vec<FontCatalogFamily>);
impl FontCatalog {
pub fn find_font_style_in_catalog(&self, font: &Font) -> Option<FontCatalogStyle> {
let family = self.0.iter().find(|family| family.name == font.font_family);
let found_style = family.map(|family| {
let FontCatalogStyle { weight, italic, .. } = FontCatalogStyle::from_named_style(&font.font_style, "");
family.closest_style(weight, italic).clone()
});
if found_style.is_none() {
log::warn!("Font not found in catalog: {:?}", font);
}
found_style
}
}
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct FontCatalogFamily {
/// The font family name.
pub name: String,
/// The font styles (variants) available for the font family.
pub styles: Vec<FontCatalogStyle>,
}
impl FontCatalogFamily {
/// Finds the closest style to the given weight and italic setting.
/// Aims to find the nearest weight while maintaining the italic setting if possible, but italic may change if no other option is available.
pub fn closest_style(&self, weight: u32, italic: bool) -> &FontCatalogStyle {
self.styles
.iter()
.map(|style| ((style.weight as i32 - weight as i32).unsigned_abs() + 10000 * (style.italic != italic) as u32, style))
.min_by_key(|(distance, _)| *distance)
.map(|(_, style)| style)
.unwrap_or(&self.styles[0])
}
}
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct FontCatalogStyle {
pub weight: u32,
pub italic: bool,
pub url: String,
}
impl FontCatalogStyle {
pub fn to_named_style(&self) -> String {
let weight = self.weight;
let italic = self.italic;
let named_weight = Font::named_weight(weight);
let maybe_italic = if italic { " Italic" } else { "" };
format!("{named_weight}{maybe_italic} ({weight})")
}
pub fn from_named_style(named_style: &str, url: impl Into<String>) -> FontCatalogStyle {
let weight = named_style.split_terminator(['(', ')']).next_back().and_then(|x| x.parse::<u32>().ok()).unwrap_or(400);
let italic = named_style.contains("Italic (");
FontCatalogStyle { weight, italic, url: url.into() }
}
/// Get the URL for the stylesheet for loading a font preview for this style of the given family name, subsetted to only the letters in the family name.
pub fn preview_url(&self, family: impl Into<String>) -> String {
let name = family.into().replace(' ', "+");
let italic = if self.italic { "ital," } else { "" };
let weight = self.weight;
format!("https://fonts.googleapis.com/css2?display=swap&family={name}:{italic}wght@{weight}&text={name}")
}
}
#[derive(PartialEq, Eq, Clone, Copy, Default, Debug, serde::Serialize, serde::Deserialize)]
pub enum Platform {
#[default]

View File

@@ -44,7 +44,6 @@ impl MessageHandler<ToolMessage, ToolMessageContext<'_>> for ToolMessageHandler
preferences,
viewport,
} = context;
let font_cache = &persistent_data.font_cache;
match message {
// Messages
@@ -122,11 +121,11 @@ impl MessageHandler<ToolMessage, ToolMessageContext<'_>> for ToolMessageHandler
document_id,
global_tool_data: &self.tool_state.document_tool_data,
input,
font_cache,
shape_editor: &mut self.shape_editor,
node_graph,
preferences,
viewport,
persistent_data,
};
if let Some(tool_abort_message) = tool.event_to_message_map().tool_abort {
@@ -217,7 +216,7 @@ impl MessageHandler<ToolMessage, ToolMessageContext<'_>> for ToolMessageHandler
tool_data.tools.get(active_tool).unwrap().activate(responses);
// Register initial properties
tool_data.tools.get(active_tool).unwrap().send_layout(responses, LayoutTarget::ToolOptions);
tool_data.tools.get(active_tool).unwrap().refresh_options(responses, persistent_data);
// Notify the frontend about the initial active tool
tool_data.send_layout(responses, LayoutTarget::ToolShelf, preferences.brush_tool);
@@ -230,11 +229,11 @@ impl MessageHandler<ToolMessage, ToolMessageContext<'_>> for ToolMessageHandler
document_id,
global_tool_data: &self.tool_state.document_tool_data,
input,
font_cache,
shape_editor: &mut self.shape_editor,
node_graph,
preferences,
viewport,
persistent_data,
};
// Set initial hints and cursor
@@ -257,7 +256,8 @@ impl MessageHandler<ToolMessage, ToolMessageContext<'_>> for ToolMessageHandler
}
ToolMessage::RefreshToolOptions => {
let tool_data = &mut self.tool_state.tool_data;
tool_data.tools.get(&tool_data.active_tool_type).unwrap().send_layout(responses, LayoutTarget::ToolOptions);
tool_data.tools.get(&tool_data.active_tool_type).unwrap().refresh_options(responses, persistent_data);
}
ToolMessage::RefreshToolShelf => {
let tool_data = &mut self.tool_state.tool_data;
@@ -341,11 +341,11 @@ impl MessageHandler<ToolMessage, ToolMessageContext<'_>> for ToolMessageHandler
document_id,
global_tool_data: &self.tool_state.document_tool_data,
input,
font_cache,
shape_editor: &mut self.shape_editor,
node_graph,
preferences,
viewport,
persistent_data,
};
if matches!(tool_message, ToolMessage::UpdateHints) {
if graph_view_overlay_open {

View File

@@ -600,7 +600,7 @@ impl Fsm for SelectToolFsmState {
document,
input,
viewport,
font_cache,
persistent_data,
..
} = tool_action_data;
@@ -625,7 +625,7 @@ impl Fsm for SelectToolFsmState {
overlay_context.outline(document.metadata().layer_with_free_points_outline(layer), layer_to_viewport, None);
if is_layer_fed_by_node_of_name(layer, &document.network_interface, "Text") {
let transformed_quad = layer_to_viewport * text_bounding_box(layer, document, font_cache);
let transformed_quad = layer_to_viewport * text_bounding_box(layer, document, &persistent_data.font_cache);
overlay_context.dashed_quad(transformed_quad, None, None, Some(7.), Some(5.), None);
}
}

View File

@@ -6,6 +6,7 @@ use crate::messages::portfolio::document::graph_operation::utility_types::Transf
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::network_interface::InputConnector;
use crate::messages::portfolio::utility_types::{FontCatalog, FontCatalogStyle, PersistentData};
use crate::messages::tool::common_functionality::auto_panning::AutoPanning;
use crate::messages::tool::common_functionality::color_selector::{ToolColorOptions, ToolColorType};
use crate::messages::tool::common_functionality::graph_modification_utils::{self, is_layer_fed_by_node_of_name};
@@ -13,6 +14,7 @@ use crate::messages::tool::common_functionality::resize::Resize;
use crate::messages::tool::common_functionality::snapping::{self, SnapCandidatePoint, SnapData};
use crate::messages::tool::common_functionality::transformation_cage::*;
use crate::messages::tool::common_functionality::utility_functions::text_bounding_box;
use crate::messages::tool::utility_types::ToolRefreshOptions;
use graph_craft::document::value::TaggedValue;
use graph_craft::document::{NodeId, NodeInput};
use graphene_std::Color;
@@ -31,8 +33,7 @@ pub struct TextOptions {
font_size: f64,
line_height_ratio: f64,
character_spacing: f64,
font_name: String,
font_style: String,
font: Font,
fill: ToolColorOptions,
tilt: f64,
align: TextAlign,
@@ -44,8 +45,7 @@ impl Default for TextOptions {
font_size: 24.,
line_height_ratio: 1.2,
character_spacing: 0.,
font_name: graphene_std::consts::DEFAULT_FONT_FAMILY.into(),
font_style: graphene_std::consts::DEFAULT_FONT_STYLE.into(),
font: Font::new(graphene_std::consts::DEFAULT_FONT_FAMILY.into(), graphene_std::consts::DEFAULT_FONT_STYLE.into()),
fill: ToolColorOptions::new_primary(),
tilt: 0.,
align: TextAlign::default(),
@@ -71,13 +71,14 @@ pub enum TextToolMessage {
TextChange { new_text: String, is_left_or_right_click: bool },
UpdateBounds { new_text: String },
UpdateOptions { options: TextOptionsUpdate },
RefreshEditingFontData,
}
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)]
pub enum TextOptionsUpdate {
FillColor(Option<Color>),
FillColorType(ToolColorType),
Font { family: String, style: String },
Font { font: Font },
FontSize(f64),
LineHeightRatio(f64),
Align(TextAlign),
@@ -96,31 +97,84 @@ impl ToolMetadata for TextTool {
}
}
fn create_text_widgets(tool: &TextTool) -> Vec<WidgetInstance> {
let font = FontInput::new(&tool.options.font_name, &tool.options.font_style)
.is_style_picker(false)
.on_update(|font_input: &FontInput| {
fn create_text_widgets(tool: &TextTool, font_catalog: &FontCatalog) -> Vec<WidgetInstance> {
fn update_options(font: Font, commit_style: Option<String>) -> impl Fn(&()) -> Message + Clone {
let mut font = font;
if let Some(style) = commit_style {
font.font_style = style;
}
move |_| {
TextToolMessage::UpdateOptions {
options: TextOptionsUpdate::Font {
family: font_input.font_family.clone(),
style: font_input.font_style.clone(),
},
options: TextOptionsUpdate::Font { font: font.clone() },
}
.into()
})
.widget_instance();
let style = FontInput::new(&tool.options.font_name, &tool.options.font_style)
.is_style_picker(true)
.on_update(|font_input: &FontInput| {
TextToolMessage::UpdateOptions {
options: TextOptionsUpdate::Font {
family: font_input.font_family.clone(),
style: font_input.font_style.clone(),
},
}
.into()
})
.widget_instance();
}
}
let font = DropdownInput::new(vec![
font_catalog
.0
.iter()
.map(|family| {
let font = Font::new(family.name.clone(), tool.options.font.font_style.clone());
let commit_style = font_catalog.find_font_style_in_catalog(&tool.options.font).map(|style| style.to_named_style());
let update = update_options(font.clone(), None);
let commit = update_options(font, commit_style);
MenuListEntry::new(family.name.clone())
.label(family.name.clone())
.font(family.closest_style(400, false).preview_url(&family.name))
.on_update(update)
.on_commit(commit)
})
.collect::<Vec<_>>(),
])
.selected_index(font_catalog.0.iter().position(|family| family.name == tool.options.font.font_family).map(|i| i as u32))
.virtual_scrolling(true)
.widget_instance();
let style = DropdownInput::new({
font_catalog
.0
.iter()
.find(|family| family.name == tool.options.font.font_family)
.map(|family| {
let build_entry = |style: &FontCatalogStyle| {
let font_style = style.to_named_style();
let font = Font::new(tool.options.font.font_family.clone(), font_style.clone());
let commit_style = font_catalog.find_font_style_in_catalog(&tool.options.font).map(|style| style.to_named_style());
let update = update_options(font.clone(), None);
let commit = update_options(font, commit_style);
MenuListEntry::new(font_style.clone()).on_update(update).on_commit(commit).label(font_style)
};
vec![
family.styles.iter().filter(|style| !style.italic).map(build_entry).collect::<Vec<_>>(),
family.styles.iter().filter(|style| style.italic).map(build_entry).collect::<Vec<_>>(),
]
})
.filter(|styles| !styles.is_empty())
.unwrap_or_default()
})
.selected_index(
font_catalog
.0
.iter()
.find(|family| family.name == tool.options.font.font_family)
.and_then(|family| {
let not_italic = family.styles.iter().filter(|style| !style.italic);
let italic = family.styles.iter().filter(|style| style.italic);
not_italic
.chain(italic)
.position(|style| Some(style) == font_catalog.find_font_style_in_catalog(&tool.options.font).as_ref())
})
.map(|i| i as u32),
)
.widget_instance();
let size = NumberInput::new(Some(tool.options.font_size))
.unit(" px")
.label("Size")
@@ -172,9 +226,22 @@ fn create_text_widgets(tool: &TextTool) -> Vec<WidgetInstance> {
]
}
impl LayoutHolder for TextTool {
fn layout(&self) -> Layout {
let mut widgets = create_text_widgets(self);
impl ToolRefreshOptions for TextTool {
fn refresh_options(&self, responses: &mut VecDeque<Message>, persistent_data: &PersistentData) {
self.send_layout(responses, LayoutTarget::ToolOptions, &persistent_data.font_catalog);
}
}
impl TextTool {
fn send_layout(&self, responses: &mut VecDeque<Message>, layout_target: LayoutTarget, font_catalog: &FontCatalog) {
responses.add(LayoutMessage::SendLayout {
layout: self.layout(font_catalog),
layout_target,
});
}
fn layout(&self, font_catalog: &FontCatalog) -> Layout {
let mut widgets = create_text_widgets(self, font_catalog);
widgets.push(Separator::new(SeparatorType::Unrelated).widget_instance());
@@ -215,11 +282,8 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Text
return;
};
match options {
TextOptionsUpdate::Font { family, style } => {
self.options.font_name = family;
self.options.font_style = style;
self.send_layout(responses, LayoutTarget::ToolOptions);
TextOptionsUpdate::Font { font } => {
self.options.font = font;
}
TextOptionsUpdate::FontSize(font_size) => self.options.font_size = font_size,
TextOptionsUpdate::LineHeightRatio(line_height_ratio) => self.options.line_height_ratio = line_height_ratio,
@@ -235,7 +299,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Text
}
}
self.send_layout(responses, LayoutTarget::ToolOptions);
self.send_layout(responses, LayoutTarget::ToolOptions, &context.persistent_data.font_catalog);
}
fn actions(&self) -> ActionList {
@@ -339,6 +403,7 @@ impl TextToolData {
TextToolFsmState::Ready
}
/// Set the editing state of the currently modifying layer
fn set_editing(&self, editable: bool, font_cache: &FontCache, responses: &mut VecDeque<Message>) {
if let Some(editing_text) = self.editing_text.as_ref().filter(|_| editable) {
@@ -347,7 +412,7 @@ impl TextToolData {
line_height_ratio: editing_text.typesetting.line_height_ratio,
font_size: editing_text.typesetting.font_size,
color: editing_text.color.unwrap_or(Color::BLACK),
url: font_cache.get_preview_url(&editing_text.font).cloned().unwrap_or_default(),
font_data: font_cache.get(&editing_text.font).map(|(data, _)| data.clone()).unwrap_or_default(),
transform: editing_text.transform.to_cols_array(),
max_width: editing_text.typesetting.max_width,
max_height: editing_text.typesetting.max_height,
@@ -411,6 +476,7 @@ impl TextToolData {
self.layer = LayerNodeIdentifier::new_unchecked(NodeId::new());
responses.add(PortfolioMessage::LoadFontData { font: editing_text.font.clone() });
responses.add(GraphOperationMessage::NewTextLayer {
id: self.layer.to_node(),
text: String::new(),
@@ -498,10 +564,11 @@ impl Fsm for TextToolFsmState {
document,
global_tool_data,
input,
font_cache,
persistent_data,
viewport,
..
} = transition_data;
let font_cache = &persistent_data.font_cache;
let fill_color = graphene_std::Color::from_rgb_str(COLOR_OVERLAY_BLUE.strip_prefix('#').unwrap())
.unwrap()
.with_alpha(0.05)
@@ -827,7 +894,7 @@ impl Fsm for TextToolFsmState {
tilt: tool_options.tilt,
align: tool_options.align,
},
font: Font::new(tool_options.font_name.clone(), tool_options.font_style.clone()),
font: Font::new(tool_options.font.font_family.clone(), tool_options.font.font_style.clone()),
color: tool_options.fill.active_color(),
};
tool_data.new_text(document, editing_text, font_cache, responses);
@@ -852,6 +919,14 @@ impl Fsm for TextToolFsmState {
TextToolFsmState::Ready
}
(TextToolFsmState::Editing, TextToolMessage::RefreshEditingFontData) => {
let font = Font::new(tool_options.font.font_family.clone(), tool_options.font.font_style.clone());
responses.add(FrontendMessage::DisplayEditableTextboxUpdateFontData {
font_data: font_cache.get(&font).map(|(data, _)| data.clone()).unwrap_or_default(),
});
TextToolFsmState::Editing
}
(TextToolFsmState::Editing, TextToolMessage::TextChange { new_text, is_left_or_right_click }) => {
tool_data.new_text = new_text;
@@ -871,6 +946,7 @@ impl Fsm for TextToolFsmState {
}
responses.add(FrontendMessage::TriggerTextCommit);
TextToolFsmState::Editing
}
}

View File

@@ -9,12 +9,12 @@ use crate::messages::input_mapper::utility_types::macros::action_shortcut;
use crate::messages::input_mapper::utility_types::misc::ActionShortcut;
use crate::messages::layout::utility_types::widget_prelude::*;
use crate::messages::portfolio::document::overlays::utility_types::OverlayProvider;
use crate::messages::portfolio::utility_types::PersistentData;
use crate::messages::preferences::PreferencesMessageHandler;
use crate::messages::prelude::*;
use crate::messages::tool::common_functionality::shapes::shape_utility::ShapeType;
use crate::node_graph_executor::NodeGraphExecutor;
use graphene_std::raster::color::Color;
use graphene_std::text::FontCache;
use std::borrow::Cow;
use std::fmt::{self, Debug};
@@ -24,18 +24,28 @@ pub struct ToolActionMessageContext<'a> {
pub document_id: DocumentId,
pub global_tool_data: &'a DocumentToolData,
pub input: &'a InputPreprocessorMessageHandler,
pub font_cache: &'a FontCache,
pub persistent_data: &'a PersistentData,
pub shape_editor: &'a mut ShapeState,
pub node_graph: &'a NodeGraphExecutor,
pub preferences: &'a PreferencesMessageHandler,
pub viewport: &'a ViewportMessageHandler,
}
pub trait ToolCommon: for<'a, 'b> MessageHandler<ToolMessage, &'b mut ToolActionMessageContext<'a>> + LayoutHolder + ToolTransition + ToolMetadata {}
impl<T> ToolCommon for T where T: for<'a, 'b> MessageHandler<ToolMessage, &'b mut ToolActionMessageContext<'a>> + LayoutHolder + ToolTransition + ToolMetadata {}
pub trait ToolCommon: for<'a, 'b> MessageHandler<ToolMessage, &'b mut ToolActionMessageContext<'a>> + ToolRefreshOptions + ToolTransition + ToolMetadata {}
impl<T> ToolCommon for T where T: for<'a, 'b> MessageHandler<ToolMessage, &'b mut ToolActionMessageContext<'a>> + ToolRefreshOptions + ToolTransition + ToolMetadata {}
type Tool = dyn ToolCommon + Send + Sync;
pub trait ToolRefreshOptions {
fn refresh_options(&self, responses: &mut VecDeque<Message>, _persistent_data: &PersistentData);
}
impl<T: LayoutHolder> ToolRefreshOptions for T {
fn refresh_options(&self, responses: &mut VecDeque<Message>, _persistent_data: &PersistentData) {
self.send_layout(responses, LayoutTarget::ToolOptions);
}
}
/// The FSM (finite state machine) is a flowchart between different operating states that a specific tool might be in.
/// It is the central "core" logic area of each tool which is in charge of maintaining the state of the tool and responding to events coming from outside (like user input).
/// For example, a tool might be `Ready` or `Drawing` depending on if the user is idle or actively drawing with the mouse held down.