mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-12-23 10:11:54 +00:00
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:
parent
6f087eb981
commit
2d6d054359
31 changed files with 766 additions and 702 deletions
|
|
@ -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 => {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)(&()),
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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")]
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
import { createAppWindowState } from "@graphite/state-providers/app-window";
|
||||
import { createDialogState } from "@graphite/state-providers/dialog";
|
||||
import { createDocumentState } from "@graphite/state-providers/document";
|
||||
import { createFontsState } from "@graphite/state-providers/fonts";
|
||||
import { createFontsManager } from "/src/io-managers/fonts";
|
||||
import { createFullscreenState } from "@graphite/state-providers/fullscreen";
|
||||
import { createNodeGraphState } from "@graphite/state-providers/node-graph";
|
||||
import { createPortfolioState } from "@graphite/state-providers/portfolio";
|
||||
|
|
@ -31,8 +31,6 @@
|
|||
setContext("tooltip", tooltip);
|
||||
let document = createDocumentState(editor);
|
||||
setContext("document", document);
|
||||
let fonts = createFontsState(editor);
|
||||
setContext("fonts", fonts);
|
||||
let fullscreen = createFullscreenState(editor);
|
||||
setContext("fullscreen", fullscreen);
|
||||
let nodeGraph = createNodeGraphState(editor);
|
||||
|
|
@ -48,6 +46,7 @@
|
|||
createLocalizationManager(editor);
|
||||
createPanicManager(editor, dialog);
|
||||
createPersistenceManager(editor, portfolio);
|
||||
createFontsManager(editor);
|
||||
let inputManagerDestructor = createInputManager(editor, dialog, portfolio, document, fullscreen);
|
||||
|
||||
onMount(() => {
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@
|
|||
|
||||
export let parentsValuePath: string[] = [];
|
||||
export let entries: MenuListEntry[][];
|
||||
export let entriesHash: bigint;
|
||||
export let activeEntry: MenuListEntry | undefined = undefined;
|
||||
export let open: boolean;
|
||||
export let direction: MenuDirection = "Bottom";
|
||||
|
|
@ -37,26 +38,29 @@
|
|||
export let drawIcon = false;
|
||||
export let interactive = false;
|
||||
export let scrollableY = false;
|
||||
export let virtualScrollingEntryHeight = 0;
|
||||
export let virtualScrolling = false;
|
||||
|
||||
// Keep the child references outside of the entries array so as to avoid infinite recursion.
|
||||
let childReferences: MenuList[][] = [];
|
||||
let search = "";
|
||||
|
||||
let reactiveEntries = entries;
|
||||
let highlighted = activeEntry as MenuListEntry | undefined;
|
||||
let virtualScrollingEntriesStart = 0;
|
||||
|
||||
// Called only when `open` is changed from outside this component
|
||||
// `watchOpen` is called only when `open` is changed from outside this component
|
||||
$: watchOpen(open);
|
||||
$: watchEntries(entries);
|
||||
$: watchEntriesHash(entriesHash);
|
||||
$: watchRemeasureWidth(filteredEntries, drawIcon);
|
||||
$: watchHighlightedWithSearch(filteredEntries, open);
|
||||
|
||||
$: filteredEntries = entries.map((section) => section.filter((entry) => inSearch(search, entry)));
|
||||
$: virtualScrollingTotalHeight = filteredEntries.length === 0 ? 0 : filteredEntries[0].length * virtualScrollingEntryHeight;
|
||||
$: virtualScrollingStartIndex = Math.floor(virtualScrollingEntriesStart / virtualScrollingEntryHeight) || 0;
|
||||
$: virtualScrollingEndIndex = filteredEntries.length === 0 ? 0 : Math.min(filteredEntries[0].length, virtualScrollingStartIndex + 1 + 400 / virtualScrollingEntryHeight);
|
||||
$: virtualScrollingEntryHeight = virtualScrolling ? 20 : 0;
|
||||
$: filteredEntries = reactiveEntries.map((section) => section.filter((entry) => inSearch(search, entry)));
|
||||
$: startIndex = virtualScrollingEntryHeight ? virtualScrollingStartIndex : 0;
|
||||
// Virtual scrolling calculations
|
||||
$: virtualScrollingTotalHeight = filteredEntries.length === 0 ? 0 : filteredEntries[0].length * virtualScrollingEntryHeight;
|
||||
$: virtualScrollingStartIndex = filteredEntries.length === 0 ? 0 : Math.floor(virtualScrollingEntriesStart / virtualScrollingEntryHeight) || 0;
|
||||
$: virtualScrollingEndIndex = filteredEntries.length === 0 ? 0 : Math.min(filteredEntries[0].length, virtualScrollingStartIndex + 1 + 400 / virtualScrollingEntryHeight);
|
||||
|
||||
// TODO: Move keyboard input handling entirely to the unified system in `input.ts`.
|
||||
// TODO: The current approach is hacky and blocks the allowances for shortcuts like the key to open the browser's dev tools.
|
||||
|
|
@ -139,6 +143,10 @@
|
|||
});
|
||||
}
|
||||
|
||||
function watchEntriesHash(_entriesHash: bigint) {
|
||||
reactiveEntries = entries;
|
||||
}
|
||||
|
||||
function watchRemeasureWidth(_: MenuListEntry[][], __: boolean) {
|
||||
self?.measureAndEmitNaturalWidth();
|
||||
}
|
||||
|
|
@ -149,23 +157,31 @@
|
|||
}
|
||||
|
||||
function getChildReference(menuListEntry: MenuListEntry): MenuList | undefined {
|
||||
const index = filteredEntries.flat().indexOf(menuListEntry);
|
||||
return childReferences.flat().filter((x) => x)[index];
|
||||
const index = filteredEntries.flat().findIndex((entry) => entry.value === menuListEntry.value);
|
||||
|
||||
if (index !== -1) {
|
||||
return childReferences.flat().filter((x) => x)[index];
|
||||
} else {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("MenuListEntry not found in filteredEntries:", menuListEntry);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function onEntryClick(menuListEntry: MenuListEntry) {
|
||||
// Notify the parent about the clicked entry as the new active entry
|
||||
dispatch("activeEntry", menuListEntry);
|
||||
dispatch("selectedEntryValuePath", [...parentsValuePath, menuListEntry.value]);
|
||||
|
||||
// Close the containing menu
|
||||
let childReference = getChildReference(menuListEntry);
|
||||
if (childReference) {
|
||||
childReference.open = false;
|
||||
entries = entries;
|
||||
reactiveEntries = reactiveEntries;
|
||||
}
|
||||
dispatch("open", false);
|
||||
open = false;
|
||||
reactiveEntries = reactiveEntries;
|
||||
|
||||
// Notify the parent about the clicked entry as the new active entry
|
||||
dispatch("activeEntry", menuListEntry);
|
||||
dispatch("selectedEntryValuePath", [...parentsValuePath, menuListEntry.value]);
|
||||
}
|
||||
|
||||
function onEntryPointerEnter(menuListEntry: MenuListEntry) {
|
||||
|
|
@ -177,8 +193,10 @@
|
|||
let childReference = getChildReference(menuListEntry);
|
||||
if (childReference) {
|
||||
childReference.open = true;
|
||||
entries = entries;
|
||||
} else dispatch("open", true);
|
||||
reactiveEntries = reactiveEntries;
|
||||
} else {
|
||||
dispatch("open", true);
|
||||
}
|
||||
}
|
||||
|
||||
function onEntryPointerLeave(menuListEntry: MenuListEntry) {
|
||||
|
|
@ -190,8 +208,10 @@
|
|||
let childReference = getChildReference(menuListEntry);
|
||||
if (childReference) {
|
||||
childReference.open = false;
|
||||
entries = entries;
|
||||
} else dispatch("open", false);
|
||||
reactiveEntries = reactiveEntries;
|
||||
} else {
|
||||
dispatch("open", false);
|
||||
}
|
||||
}
|
||||
|
||||
function isEntryOpen(menuListEntry: MenuListEntry): boolean {
|
||||
|
|
@ -365,7 +385,7 @@
|
|||
let container = scroller?.div?.();
|
||||
if (!container || !highlighted) return;
|
||||
let containerBoundingRect = container.getBoundingClientRect();
|
||||
let highlightedIndex = filteredEntries.flat().findIndex((entry) => entry === highlighted);
|
||||
let highlightedIndex = filteredEntries.flat().findIndex((entry) => entry.value === highlighted?.value);
|
||||
|
||||
let selectedBoundingRect = new DOMRect();
|
||||
if (virtualScrollingEntryHeight) {
|
||||
|
|
@ -386,10 +406,6 @@
|
|||
container.scrollBy(0, selectedBoundingRect.y - (containerBoundingRect.y + containerBoundingRect.height) + selectedBoundingRect.height);
|
||||
}
|
||||
}
|
||||
|
||||
export function scrollViewTo(distanceDown: number) {
|
||||
scroller?.div?.()?.scrollTo(0, distanceDown);
|
||||
}
|
||||
</script>
|
||||
|
||||
<FloatingMenu
|
||||
|
|
@ -419,8 +435,8 @@
|
|||
{#if virtualScrollingEntryHeight}
|
||||
<LayoutRow class="scroll-spacer" styles={{ height: `${virtualScrollingStartIndex * virtualScrollingEntryHeight}px` }} />
|
||||
{/if}
|
||||
{#each entries as section, sectionIndex (sectionIndex)}
|
||||
{#if includeSeparator(entries, section, sectionIndex, search)}
|
||||
{#each reactiveEntries as section, sectionIndex (sectionIndex)}
|
||||
{#if includeSeparator(reactiveEntries, section, sectionIndex, search)}
|
||||
<Separator type="Section" direction="Vertical" />
|
||||
{/if}
|
||||
{#each currentEntries(section, virtualScrollingEntryHeight, virtualScrollingStartIndex, virtualScrollingEndIndex, search) as entry, entryIndex (entryIndex + startIndex)}
|
||||
|
|
@ -442,10 +458,10 @@
|
|||
{/if}
|
||||
|
||||
{#if entry.font}
|
||||
<link rel="stylesheet" href={entry.font?.toString()} />
|
||||
<link rel="stylesheet" href={entry.font} />
|
||||
{/if}
|
||||
|
||||
<TextLabel class="entry-label" styles={{ "font-family": `${!entry.font ? "inherit" : entry.value}` }}>{entry.label}</TextLabel>
|
||||
<TextLabel class="entry-label" styles={entry.font ? { "font-family": entry.value } : {}}>{entry.label}</TextLabel>
|
||||
|
||||
{#if entry.tooltipShortcut?.shortcut.length}
|
||||
<ShortcutLabel shortcut={entry.tooltipShortcut} />
|
||||
|
|
@ -470,6 +486,7 @@
|
|||
open={getChildReference(entry)?.open || false}
|
||||
direction="TopRight"
|
||||
entries={entry.children}
|
||||
entriesHash={entry.childrenHash || 0n}
|
||||
{minWidth}
|
||||
{drawIcon}
|
||||
{scrollableY}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
type MouseCursorIcon,
|
||||
type XY,
|
||||
DisplayEditableTextbox,
|
||||
DisplayEditableTextboxUpdateFontData,
|
||||
DisplayEditableTextboxTransform,
|
||||
DisplayRemoveEditableTextbox,
|
||||
TriggerTextCommit,
|
||||
|
|
@ -360,10 +361,14 @@
|
|||
if (!textInput) return;
|
||||
editor.handle.updateBounds(textInputCleanup(textInput.innerText));
|
||||
};
|
||||
|
||||
textInputMatrix = displayEditableTextbox.transform;
|
||||
const newFont = new FontFace("text-font", `url(${displayEditableTextbox.url})`);
|
||||
window.document.fonts.add(newFont);
|
||||
textInput.style.fontFamily = "text-font";
|
||||
|
||||
const data = new Uint8Array(displayEditableTextbox.fontData);
|
||||
if (data.length > 0) {
|
||||
window.document.fonts.add(new FontFace("text-font", data));
|
||||
textInput.style.fontFamily = "text-font";
|
||||
}
|
||||
|
||||
// Necessary to select contenteditable: https://stackoverflow.com/questions/6139107/programmatically-select-text-in-a-contenteditable-html-element/6150060#6150060
|
||||
|
||||
|
|
@ -471,6 +476,15 @@
|
|||
|
||||
displayEditableTextbox(data);
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(DisplayEditableTextboxUpdateFontData, async (data) => {
|
||||
await tick();
|
||||
|
||||
const fontData = new Uint8Array(data.fontData);
|
||||
if (fontData.length > 0 && textInput) {
|
||||
window.document.fonts.add(new FontFace("text-font", fontData));
|
||||
textInput.style.fontFamily = "text-font";
|
||||
}
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(DisplayEditableTextboxTransform, async (data) => {
|
||||
textInputMatrix = data.transform;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -627,7 +627,7 @@
|
|||
data-tooltip-description={(listing.entry.expanded
|
||||
? "Hide the layers nested within. (To affect all open descendants, perform the shortcut shown.)"
|
||||
: "Show the layers nested within. (To affect all closed descendants, perform the shortcut shown.)") +
|
||||
(listing.entry.ancestorOfSelected && !listing.entry.expanded ? "\n\nNote: a selected layer is currently contained within.\n" : "")}
|
||||
(listing.entry.ancestorOfSelected && !listing.entry.expanded ? "\n\nA selected layer is currently contained within.\n" : "")}
|
||||
data-tooltip-shortcut={altClickShortcut?.shortcut ? JSON.stringify(altClickShortcut.shortcut) : undefined}
|
||||
on:click={(e) => handleExpandArrowClickWithModifiers(e, listing.entry.id)}
|
||||
tabindex="0"
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@
|
|||
import ColorInput from "@graphite/components/widgets/inputs/ColorInput.svelte";
|
||||
import CurveInput from "@graphite/components/widgets/inputs/CurveInput.svelte";
|
||||
import DropdownInput from "@graphite/components/widgets/inputs/DropdownInput.svelte";
|
||||
import FontInput from "@graphite/components/widgets/inputs/FontInput.svelte";
|
||||
import NumberInput from "@graphite/components/widgets/inputs/NumberInput.svelte";
|
||||
import RadioInput from "@graphite/components/widgets/inputs/RadioInput.svelte";
|
||||
import ReferencePointInput from "@graphite/components/widgets/inputs/ReferencePointInput.svelte";
|
||||
|
|
@ -61,16 +60,16 @@
|
|||
return widgets;
|
||||
}
|
||||
|
||||
function widgetValueCommit(index: number, value: unknown) {
|
||||
editor.handle.widgetValueCommit(layoutTarget, widgets[index].widgetId, value);
|
||||
function widgetValueCommit(widgetIndex: number, value: unknown) {
|
||||
editor.handle.widgetValueCommit(layoutTarget, widgets[widgetIndex].widgetId, value);
|
||||
}
|
||||
|
||||
function widgetValueUpdate(index: number, value: unknown) {
|
||||
editor.handle.widgetValueUpdate(layoutTarget, widgets[index].widgetId, value);
|
||||
function widgetValueUpdate(widgetIndex: number, value: unknown, resendWidget: boolean) {
|
||||
editor.handle.widgetValueUpdate(layoutTarget, widgets[widgetIndex].widgetId, value, resendWidget);
|
||||
}
|
||||
|
||||
function widgetValueCommitAndUpdate(index: number, value: unknown) {
|
||||
editor.handle.widgetValueCommitAndUpdate(layoutTarget, widgets[index].widgetId, value);
|
||||
function widgetValueCommitAndUpdate(widgetIndex: number, value: unknown, resendWidget: boolean) {
|
||||
editor.handle.widgetValueCommitAndUpdate(layoutTarget, widgets[widgetIndex].widgetId, value, resendWidget);
|
||||
}
|
||||
|
||||
// TODO: This seems to work, but verify the correctness and terseness of this, it's adapted from https://stackoverflow.com/a/67434028/775283
|
||||
|
|
@ -85,43 +84,47 @@
|
|||
<!-- TODO: Refactor this component to use `<svelte:component this={attributesObject} />` to avoid all the separate conditional components -->
|
||||
|
||||
<div class={`widget-span ${className} ${extraClasses}`.trim()} class:narrow class:row={direction === "row"} class:column={direction === "column"}>
|
||||
{#each widgets as component, index}
|
||||
{#each widgets as component, widgetIndex}
|
||||
{@const checkboxInput = narrowWidgetProps(component.props, "CheckboxInput")}
|
||||
{#if checkboxInput}
|
||||
<CheckboxInput {...exclude(checkboxInput)} on:checked={({ detail }) => widgetValueCommitAndUpdate(index, detail)} />
|
||||
<CheckboxInput {...exclude(checkboxInput)} on:checked={({ detail }) => widgetValueCommitAndUpdate(widgetIndex, detail, true)} />
|
||||
{/if}
|
||||
{@const colorInput = narrowWidgetProps(component.props, "ColorInput")}
|
||||
{#if colorInput}
|
||||
<ColorInput {...exclude(colorInput)} on:value={({ detail }) => widgetValueUpdate(index, detail)} on:startHistoryTransaction={() => widgetValueCommit(index, colorInput.value)} />
|
||||
<ColorInput
|
||||
{...exclude(colorInput)}
|
||||
on:value={({ detail }) => widgetValueUpdate(widgetIndex, detail, false)}
|
||||
on:startHistoryTransaction={() => widgetValueCommit(widgetIndex, colorInput.value)}
|
||||
/>
|
||||
{/if}
|
||||
<!-- TODO: Curves Input is currently unused -->
|
||||
{@const curvesInput = narrowWidgetProps(component.props, "CurveInput")}
|
||||
{#if curvesInput}
|
||||
<CurveInput {...exclude(curvesInput)} on:value={({ detail }) => debouncer((value) => widgetValueCommitAndUpdate(index, value), { debounceTime: 120 }).debounceUpdateValue(detail)} />
|
||||
<CurveInput
|
||||
{...exclude(curvesInput)}
|
||||
on:value={({ detail }) => debouncer((value) => widgetValueCommitAndUpdate(widgetIndex, value, false), { debounceTime: 120 }).debounceUpdateValue(detail)}
|
||||
/>
|
||||
{/if}
|
||||
{@const dropdownInput = narrowWidgetProps(component.props, "DropdownInput")}
|
||||
{#if dropdownInput}
|
||||
<DropdownInput
|
||||
{...exclude(dropdownInput)}
|
||||
on:hoverInEntry={({ detail }) => {
|
||||
return widgetValueUpdate(index, detail);
|
||||
return widgetValueUpdate(widgetIndex, detail, false);
|
||||
}}
|
||||
on:hoverOutEntry={({ detail }) => {
|
||||
return widgetValueUpdate(index, detail);
|
||||
return widgetValueUpdate(widgetIndex, detail, false);
|
||||
}}
|
||||
on:selectedIndex={({ detail }) => widgetValueCommitAndUpdate(index, detail)}
|
||||
on:selectedIndex={({ detail }) => widgetValueCommitAndUpdate(widgetIndex, detail, true)}
|
||||
/>
|
||||
{/if}
|
||||
{@const fontInput = narrowWidgetProps(component.props, "FontInput")}
|
||||
{#if fontInput}
|
||||
<FontInput {...exclude(fontInput)} on:changeFont={({ detail }) => widgetValueCommitAndUpdate(index, detail)} />
|
||||
{/if}
|
||||
{@const parameterExposeButton = narrowWidgetProps(component.props, "ParameterExposeButton")}
|
||||
{#if parameterExposeButton}
|
||||
<ParameterExposeButton {...exclude(parameterExposeButton)} action={() => widgetValueCommitAndUpdate(index, undefined)} />
|
||||
<ParameterExposeButton {...exclude(parameterExposeButton)} action={() => widgetValueCommitAndUpdate(widgetIndex, undefined, true)} />
|
||||
{/if}
|
||||
{@const iconButton = narrowWidgetProps(component.props, "IconButton")}
|
||||
{#if iconButton}
|
||||
<IconButton {...exclude(iconButton)} action={() => widgetValueCommitAndUpdate(index, undefined)} />
|
||||
<IconButton {...exclude(iconButton)} action={() => widgetValueCommitAndUpdate(widgetIndex, undefined, true)} />
|
||||
{/if}
|
||||
{@const iconLabel = narrowWidgetProps(component.props, "IconLabel")}
|
||||
{#if iconLabel}
|
||||
|
|
@ -138,25 +141,25 @@
|
|||
{/if}
|
||||
{@const imageButton = narrowWidgetProps(component.props, "ImageButton")}
|
||||
{#if imageButton}
|
||||
<ImageButton {...exclude(imageButton)} action={() => widgetValueCommitAndUpdate(index, undefined)} />
|
||||
<ImageButton {...exclude(imageButton)} action={() => widgetValueCommitAndUpdate(widgetIndex, undefined, true)} />
|
||||
{/if}
|
||||
{@const nodeCatalog = narrowWidgetProps(component.props, "NodeCatalog")}
|
||||
{#if nodeCatalog}
|
||||
<NodeCatalog {...exclude(nodeCatalog)} on:selectNodeType={(e) => widgetValueCommitAndUpdate(index, e.detail)} />
|
||||
<NodeCatalog {...exclude(nodeCatalog)} on:selectNodeType={(e) => widgetValueCommitAndUpdate(widgetIndex, e.detail, false)} />
|
||||
{/if}
|
||||
{@const numberInput = narrowWidgetProps(component.props, "NumberInput")}
|
||||
{#if numberInput}
|
||||
<NumberInput
|
||||
{...exclude(numberInput)}
|
||||
on:value={({ detail }) => debouncer((value) => widgetValueUpdate(index, value)).debounceUpdateValue(detail)}
|
||||
on:startHistoryTransaction={() => widgetValueCommit(index, numberInput.value)}
|
||||
incrementCallbackIncrease={() => widgetValueCommitAndUpdate(index, "Increment")}
|
||||
incrementCallbackDecrease={() => widgetValueCommitAndUpdate(index, "Decrement")}
|
||||
on:value={({ detail }) => debouncer((value) => widgetValueUpdate(widgetIndex, value, true)).debounceUpdateValue(detail)}
|
||||
on:startHistoryTransaction={() => widgetValueCommit(widgetIndex, numberInput.value)}
|
||||
incrementCallbackIncrease={() => widgetValueCommitAndUpdate(widgetIndex, "Increment", false)}
|
||||
incrementCallbackDecrease={() => widgetValueCommitAndUpdate(widgetIndex, "Decrement", false)}
|
||||
/>
|
||||
{/if}
|
||||
{@const referencePointInput = narrowWidgetProps(component.props, "ReferencePointInput")}
|
||||
{#if referencePointInput}
|
||||
<ReferencePointInput {...exclude(referencePointInput)} on:value={({ detail }) => widgetValueCommitAndUpdate(index, detail)} />
|
||||
<ReferencePointInput {...exclude(referencePointInput)} on:value={({ detail }) => widgetValueCommitAndUpdate(widgetIndex, detail, true)} />
|
||||
{/if}
|
||||
{@const popoverButton = narrowWidgetProps(component.props, "PopoverButton")}
|
||||
{#if popoverButton}
|
||||
|
|
@ -166,7 +169,7 @@
|
|||
{/if}
|
||||
{@const radioInput = narrowWidgetProps(component.props, "RadioInput")}
|
||||
{#if radioInput}
|
||||
<RadioInput {...exclude(radioInput)} on:selectedIndex={({ detail }) => widgetValueCommitAndUpdate(index, detail)} />
|
||||
<RadioInput {...exclude(radioInput)} on:selectedIndex={({ detail }) => widgetValueCommitAndUpdate(widgetIndex, detail, true)} />
|
||||
{/if}
|
||||
{@const separator = narrowWidgetProps(component.props, "Separator")}
|
||||
{#if separator}
|
||||
|
|
@ -178,19 +181,23 @@
|
|||
{/if}
|
||||
{@const textAreaInput = narrowWidgetProps(component.props, "TextAreaInput")}
|
||||
{#if textAreaInput}
|
||||
<TextAreaInput {...exclude(textAreaInput)} on:commitText={({ detail }) => widgetValueCommitAndUpdate(index, detail)} />
|
||||
<TextAreaInput {...exclude(textAreaInput)} on:commitText={({ detail }) => widgetValueCommitAndUpdate(widgetIndex, detail, false)} />
|
||||
{/if}
|
||||
{@const textButton = narrowWidgetProps(component.props, "TextButton")}
|
||||
{#if textButton}
|
||||
<TextButton {...exclude(textButton)} action={() => widgetValueCommitAndUpdate(index, [])} on:selectedEntryValuePath={({ detail }) => widgetValueCommitAndUpdate(index, detail)} />
|
||||
<TextButton
|
||||
{...exclude(textButton)}
|
||||
action={() => widgetValueCommitAndUpdate(widgetIndex, [], true)}
|
||||
on:selectedEntryValuePath={({ detail }) => widgetValueCommitAndUpdate(widgetIndex, detail, false)}
|
||||
/>
|
||||
{/if}
|
||||
{@const breadcrumbTrailButtons = narrowWidgetProps(component.props, "BreadcrumbTrailButtons")}
|
||||
{#if breadcrumbTrailButtons}
|
||||
<BreadcrumbTrailButtons {...exclude(breadcrumbTrailButtons)} action={(breadcrumbIndex) => widgetValueCommitAndUpdate(index, breadcrumbIndex)} />
|
||||
<BreadcrumbTrailButtons {...exclude(breadcrumbTrailButtons)} action={(breadcrumbIndex) => widgetValueCommitAndUpdate(widgetIndex, breadcrumbIndex, true)} />
|
||||
{/if}
|
||||
{@const textInput = narrowWidgetProps(component.props, "TextInput")}
|
||||
{#if textInput}
|
||||
<TextInput {...exclude(textInput)} on:commitText={({ detail }) => widgetValueCommitAndUpdate(index, detail)} />
|
||||
<TextInput {...exclude(textInput)} on:commitText={({ detail }) => widgetValueCommitAndUpdate(widgetIndex, detail, true)} />
|
||||
{/if}
|
||||
{@const textLabel = narrowWidgetProps(component.props, "TextLabel")}
|
||||
{#if textLabel}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@
|
|||
export let tooltipDescription: string | undefined = undefined;
|
||||
export let tooltipShortcut: ActionShortcut | undefined = undefined;
|
||||
export let menuListChildren: MenuListEntry[][] | undefined = undefined;
|
||||
export let menuListChildrenHash: bigint | undefined = undefined;
|
||||
|
||||
// Callbacks
|
||||
// TODO: Replace this with an event binding (and on other components that do this)
|
||||
|
|
@ -90,6 +91,7 @@
|
|||
on:selectedEntryValuePath={({ detail }) => dispatch("selectedEntryValuePath", detail)}
|
||||
open={self?.open || false}
|
||||
entries={menuListChildren || []}
|
||||
entriesHash={menuListChildrenHash || 0n}
|
||||
direction="Bottom"
|
||||
minWidth={240}
|
||||
drawIcon={true}
|
||||
|
|
|
|||
|
|
@ -12,15 +12,16 @@
|
|||
|
||||
const dispatch = createEventDispatcher<{ selectedIndex: number; hoverInEntry: number; hoverOutEntry: number }>();
|
||||
|
||||
let menuList: MenuList | undefined;
|
||||
let self: LayoutRow | undefined;
|
||||
|
||||
export let entries: MenuListEntry[][];
|
||||
export let entriesHash: bigint | undefined = undefined;
|
||||
export let selectedIndex: number | undefined = undefined; // When not provided, a dash is displayed
|
||||
export let drawIcon = false;
|
||||
export let interactive = true;
|
||||
export let disabled = false;
|
||||
export let narrow = false;
|
||||
export let virtualScrolling = false;
|
||||
export let tooltipLabel: string | undefined = undefined;
|
||||
export let tooltipDescription: string | undefined = undefined;
|
||||
export let tooltipShortcut: ActionShortcut | undefined = undefined;
|
||||
|
|
@ -53,19 +54,32 @@
|
|||
activeEntry = makeActiveEntry();
|
||||
}
|
||||
|
||||
// Called when the `activeEntry` two-way binding on this component's MenuList component is changed, or by the `selectedIndex()` watcher above (but we want to skip that case)
|
||||
// Called when the `activeEntry` two-way binding on this component's MenuList component is changed, or by the `watchSelectedIndex()` watcher above (but we want to skip that case)
|
||||
function watchActiveEntry(activeEntry: MenuListEntry) {
|
||||
if (activeEntrySkipWatcher) {
|
||||
activeEntrySkipWatcher = false;
|
||||
} else if (activeEntry !== DASH_ENTRY) {
|
||||
// We need to set to the initial value first to track a right history step, as if we hover in initial selection.
|
||||
if (initialSelectedIndex !== undefined) dispatch("hoverInEntry", initialSelectedIndex);
|
||||
dispatch("selectedIndex", entries.flat().indexOf(activeEntry));
|
||||
const index = entries.flat().findIndex((entry) => entry.value === activeEntry.value);
|
||||
if (index !== -1) {
|
||||
dispatch("selectedIndex", index);
|
||||
} else {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Selected index not found in entries:", activeEntry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function dispatchHoverInEntry(hoveredEntry: MenuListEntry) {
|
||||
dispatch("hoverInEntry", entries.flat().indexOf(hoveredEntry));
|
||||
const index = entries.flat().findIndex((entry) => entry.value === hoveredEntry.value);
|
||||
|
||||
if (index !== -1) {
|
||||
dispatch("hoverInEntry", index);
|
||||
} else {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Hovered entry not found in entries:", hoveredEntry);
|
||||
}
|
||||
}
|
||||
|
||||
function dispatchHoverOutEntry() {
|
||||
|
|
@ -123,11 +137,12 @@
|
|||
{open}
|
||||
{activeEntry}
|
||||
{entries}
|
||||
entriesHash={entriesHash || 0n}
|
||||
{drawIcon}
|
||||
{interactive}
|
||||
{virtualScrolling}
|
||||
direction="Bottom"
|
||||
scrollableY={true}
|
||||
bind:this={menuList}
|
||||
/>
|
||||
</LayoutRow>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,186 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { createEventDispatcher, getContext, onMount, tick } from "svelte";
|
||||
|
||||
import type { MenuListEntry, ActionShortcut } from "@graphite/messages";
|
||||
import type { FontsState } from "@graphite/state-providers/fonts";
|
||||
|
||||
import MenuList from "@graphite/components/floating-menus/MenuList.svelte";
|
||||
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
|
||||
import IconLabel from "@graphite/components/widgets/labels/IconLabel.svelte";
|
||||
import TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte";
|
||||
|
||||
const fonts = getContext<FontsState>("fonts");
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
fontFamily: string;
|
||||
fontStyle: string;
|
||||
changeFont: { fontFamily: string; fontStyle: string; fontFileUrl: string | undefined };
|
||||
}>();
|
||||
|
||||
let menuList: MenuList | undefined;
|
||||
|
||||
export let fontFamily: string;
|
||||
export let fontStyle: string;
|
||||
export let isStyle = false;
|
||||
export let disabled = false;
|
||||
export let tooltipLabel: string | undefined = undefined;
|
||||
export let tooltipDescription: string | undefined = undefined;
|
||||
export let tooltipShortcut: ActionShortcut | undefined = undefined;
|
||||
|
||||
let open = false;
|
||||
let entries: MenuListEntry[] = [];
|
||||
let activeEntry: MenuListEntry | undefined = undefined;
|
||||
let minWidth = isStyle ? 0 : 300;
|
||||
|
||||
$: watchFont(fontFamily, fontStyle);
|
||||
|
||||
async function watchFont(..._: string[]) {
|
||||
// We set this function's result to a local variable to avoid reading from `entries` which causes Svelte to trigger an update that results in an infinite loop
|
||||
const newEntries = await getEntries();
|
||||
entries = newEntries;
|
||||
activeEntry = getActiveEntry(newEntries);
|
||||
}
|
||||
|
||||
async function setOpen() {
|
||||
open = true;
|
||||
|
||||
// Scroll to the active entry (the scroller div does not yet exist so we must wait for the component to render)
|
||||
await tick();
|
||||
|
||||
if (activeEntry) {
|
||||
const index = entries.indexOf(activeEntry);
|
||||
menuList?.scrollViewTo(Math.max(0, index * 20 - 190));
|
||||
}
|
||||
}
|
||||
|
||||
function toggleOpen() {
|
||||
if (!disabled) {
|
||||
open = !open;
|
||||
|
||||
if (open) setOpen();
|
||||
}
|
||||
}
|
||||
|
||||
async function selectFont(newName: string) {
|
||||
let family;
|
||||
let style;
|
||||
|
||||
if (isStyle) {
|
||||
dispatch("fontStyle", newName);
|
||||
|
||||
family = fontFamily;
|
||||
style = newName;
|
||||
} else {
|
||||
dispatch("fontFamily", newName);
|
||||
|
||||
family = newName;
|
||||
style = "Regular (400)";
|
||||
}
|
||||
|
||||
const fontFileUrl = await fonts.getFontFileUrl(family, style);
|
||||
dispatch("changeFont", { fontFamily: family, fontStyle: style, fontFileUrl });
|
||||
}
|
||||
|
||||
async function getEntries(): Promise<MenuListEntry[]> {
|
||||
const x = isStyle ? fonts.getFontStyles(fontFamily) : fonts.fontNames();
|
||||
return (await x).map((entry: { name: string; url: URL | undefined }) => ({
|
||||
value: entry.name,
|
||||
label: entry.name,
|
||||
font: entry.url,
|
||||
action: () => selectFont(entry.name),
|
||||
}));
|
||||
}
|
||||
|
||||
function getActiveEntry(entries: MenuListEntry[]): MenuListEntry {
|
||||
const selectedChoice = isStyle ? fontStyle : fontFamily;
|
||||
|
||||
return entries.find((entry) => entry.value === selectedChoice) as MenuListEntry;
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
entries = await getEntries();
|
||||
|
||||
activeEntry = getActiveEntry(entries);
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- TODO: Combine this widget into the DropdownInput widget -->
|
||||
<LayoutRow class="font-input">
|
||||
<LayoutRow
|
||||
class="dropdown-box"
|
||||
classes={{ disabled }}
|
||||
styles={{ ...(minWidth > 0 ? { "min-width": `${minWidth}px` } : {}) }}
|
||||
{tooltipLabel}
|
||||
{tooltipDescription}
|
||||
{tooltipShortcut}
|
||||
tabindex={disabled ? -1 : 0}
|
||||
on:click={toggleOpen}
|
||||
data-floating-menu-spawner
|
||||
>
|
||||
<TextLabel class="dropdown-label">{activeEntry?.value || ""}</TextLabel>
|
||||
<IconLabel class="dropdown-arrow" icon="DropdownArrow" />
|
||||
</LayoutRow>
|
||||
<MenuList
|
||||
on:naturalWidth={({ detail }) => isStyle && (minWidth = detail)}
|
||||
{activeEntry}
|
||||
on:activeEntry={({ detail }) => (activeEntry = detail)}
|
||||
{open}
|
||||
on:open={({ detail }) => (open = detail)}
|
||||
entries={[entries]}
|
||||
minWidth={isStyle ? 0 : minWidth}
|
||||
virtualScrollingEntryHeight={isStyle ? 0 : 20}
|
||||
scrollableY={true}
|
||||
bind:this={menuList}
|
||||
/>
|
||||
</LayoutRow>
|
||||
|
||||
<style lang="scss" global>
|
||||
.font-input {
|
||||
position: relative;
|
||||
|
||||
.dropdown-box {
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
background: var(--color-1-nearblack);
|
||||
height: 24px;
|
||||
border-radius: 2px;
|
||||
|
||||
.dropdown-label {
|
||||
margin: 0;
|
||||
margin-left: 8px;
|
||||
flex: 1 1 100%;
|
||||
}
|
||||
|
||||
.dropdown-arrow {
|
||||
margin: 6px 2px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&.open {
|
||||
background: var(--color-6-lowergray);
|
||||
|
||||
.text-label {
|
||||
color: var(--color-f-white);
|
||||
}
|
||||
}
|
||||
|
||||
&.open {
|
||||
border-radius: 2px 2px 0 0;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
background: var(--color-2-mildblack);
|
||||
|
||||
.text-label {
|
||||
color: var(--color-8-uppergray);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.menu-list .floating-menu-container .floating-menu-content {
|
||||
max-height: 400px;
|
||||
padding: 4px 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
44
frontend/src/io-managers/fonts.ts
Normal file
44
frontend/src/io-managers/fonts.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import { type Editor } from "@graphite/editor";
|
||||
import { TriggerFontCatalogLoad, TriggerFontDataLoad } from "@graphite/messages";
|
||||
|
||||
type ApiResponse = { family: string; variants: string[]; files: Record<string, string> }[];
|
||||
|
||||
const FONT_LIST_API = "https://api.graphite.art/font-list";
|
||||
|
||||
export function createFontsManager(editor: Editor) {
|
||||
// Subscribe to process backend events
|
||||
editor.subscriptions.subscribeJsMessage(TriggerFontCatalogLoad, async () => {
|
||||
const response = await fetch(FONT_LIST_API);
|
||||
const fontListResponse = (await response.json()) as { items: ApiResponse };
|
||||
const fontListData = fontListResponse.items;
|
||||
|
||||
const catalog = fontListData.map((font) => {
|
||||
const styles = font.variants.map((variant) => {
|
||||
const weight = variant === "regular" || variant === "italic" ? 400 : parseInt(variant, 10);
|
||||
const italic = variant.endsWith("italic");
|
||||
const url = font.files[variant].replace("http://", "https://");
|
||||
|
||||
return { weight, italic, url };
|
||||
});
|
||||
return { name: font.family, styles };
|
||||
});
|
||||
|
||||
editor.handle.onFontCatalogLoad(catalog);
|
||||
});
|
||||
|
||||
editor.subscriptions.subscribeJsMessage(TriggerFontDataLoad, async (triggerFontDataLoad) => {
|
||||
const { fontFamily, fontStyle } = triggerFontDataLoad.font;
|
||||
|
||||
try {
|
||||
if (!triggerFontDataLoad.url) throw new Error("No URL provided for font data load");
|
||||
const response = await fetch(triggerFontDataLoad.url);
|
||||
const buffer = await response.arrayBuffer();
|
||||
const data = new Uint8Array(buffer);
|
||||
|
||||
editor.handle.onFontLoad(fontFamily, fontStyle, data);
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Failed to load font:", error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -793,7 +793,7 @@ export class DisplayEditableTextbox extends JsMessage {
|
|||
@Type(() => Color)
|
||||
readonly color!: Color;
|
||||
|
||||
readonly url!: string;
|
||||
readonly fontData!: ArrayBuffer;
|
||||
|
||||
readonly transform!: number[];
|
||||
|
||||
|
|
@ -804,6 +804,10 @@ export class DisplayEditableTextbox extends JsMessage {
|
|||
readonly align!: TextAlign;
|
||||
}
|
||||
|
||||
export class DisplayEditableTextboxUpdateFontData extends JsMessage {
|
||||
readonly fontData!: ArrayBuffer;
|
||||
}
|
||||
|
||||
export class DisplayEditableTextboxTransform extends JsMessage {
|
||||
readonly transform!: number[];
|
||||
}
|
||||
|
|
@ -865,9 +869,13 @@ export class Font {
|
|||
fontStyle!: string;
|
||||
}
|
||||
|
||||
export class TriggerFontLoad extends JsMessage {
|
||||
export class TriggerFontCatalogLoad extends JsMessage {}
|
||||
|
||||
export class TriggerFontDataLoad extends JsMessage {
|
||||
@Type(() => Font)
|
||||
font!: Font;
|
||||
|
||||
url!: string;
|
||||
}
|
||||
|
||||
export class TriggerVisitLink extends JsMessage {
|
||||
|
|
@ -998,13 +1006,14 @@ export function contrastingOutlineFactor(value: FillChoice, proximityColor: stri
|
|||
export type MenuListEntry = {
|
||||
value: string;
|
||||
label: string;
|
||||
font?: URL;
|
||||
font?: string;
|
||||
icon?: IconName;
|
||||
disabled?: boolean;
|
||||
tooltipLabel?: string;
|
||||
tooltipDescription?: string;
|
||||
tooltipShortcut?: ActionShortcut;
|
||||
children?: MenuListEntry[][];
|
||||
childrenHash?: bigint;
|
||||
};
|
||||
|
||||
export class CurveManipulatorGroup {
|
||||
|
|
@ -1036,6 +1045,8 @@ export class CurveInput extends WidgetProps {
|
|||
export class DropdownInput extends WidgetProps {
|
||||
entries!: MenuListEntry[][];
|
||||
|
||||
entriesHash!: bigint;
|
||||
|
||||
selectedIndex!: number | undefined;
|
||||
|
||||
drawIcon!: boolean;
|
||||
|
|
@ -1046,6 +1057,8 @@ export class DropdownInput extends WidgetProps {
|
|||
|
||||
narrow!: boolean;
|
||||
|
||||
virtualScrolling!: boolean;
|
||||
|
||||
@Transform(({ value }: { value: string }) => value || undefined)
|
||||
tooltipLabel!: string | undefined;
|
||||
|
||||
|
|
@ -1062,25 +1075,6 @@ export class DropdownInput extends WidgetProps {
|
|||
maxWidth!: number;
|
||||
}
|
||||
|
||||
export class FontInput extends WidgetProps {
|
||||
fontFamily!: string;
|
||||
|
||||
fontStyle!: string;
|
||||
|
||||
isStyle!: boolean;
|
||||
|
||||
disabled!: boolean;
|
||||
|
||||
@Transform(({ value }: { value: string }) => value || undefined)
|
||||
tooltipLabel!: string | undefined;
|
||||
|
||||
@Transform(({ value }: { value: string }) => value || undefined)
|
||||
tooltipDescription!: string | undefined;
|
||||
|
||||
@Transform(({ value }: { value: ActionShortcut }) => value || undefined)
|
||||
tooltipShortcut!: ActionShortcut | undefined;
|
||||
}
|
||||
|
||||
export class IconButton extends WidgetProps {
|
||||
icon!: IconName;
|
||||
|
||||
|
|
@ -1349,6 +1343,8 @@ export class TextButton extends WidgetProps {
|
|||
tooltipShortcut!: ActionShortcut | undefined;
|
||||
|
||||
menuListChildren!: MenuListEntry[][];
|
||||
|
||||
menuListChildrenHash!: bigint;
|
||||
}
|
||||
|
||||
export class BreadcrumbTrailButtons extends WidgetProps {
|
||||
|
|
@ -1449,7 +1445,6 @@ const widgetSubTypes = [
|
|||
{ value: ColorInput, name: "ColorInput" },
|
||||
{ value: CurveInput, name: "CurveInput" },
|
||||
{ value: DropdownInput, name: "DropdownInput" },
|
||||
{ value: FontInput, name: "FontInput" },
|
||||
{ value: IconButton, name: "IconButton" },
|
||||
{ value: ImageButton, name: "ImageButton" },
|
||||
{ value: ImageLabel, name: "ImageLabel" },
|
||||
|
|
@ -1695,6 +1690,7 @@ export const messageMakers: Record<string, MessageMaker> = {
|
|||
DisplayDialogDismiss,
|
||||
DisplayDialogPanic,
|
||||
DisplayEditableTextbox,
|
||||
DisplayEditableTextboxUpdateFontData,
|
||||
DisplayEditableTextboxTransform,
|
||||
DisplayRemoveEditableTextbox,
|
||||
SendUIMetadata,
|
||||
|
|
@ -1704,7 +1700,8 @@ export const messageMakers: Record<string, MessageMaker> = {
|
|||
TriggerDisplayThirdPartyLicensesDialog,
|
||||
TriggerExportImage,
|
||||
TriggerFetchAndOpenDocument,
|
||||
TriggerFontLoad,
|
||||
TriggerFontCatalogLoad,
|
||||
TriggerFontDataLoad,
|
||||
TriggerImport,
|
||||
TriggerLoadFirstAutoSaveDocument,
|
||||
TriggerLoadPreferences,
|
||||
|
|
|
|||
|
|
@ -1,115 +0,0 @@
|
|||
import { writable } from "svelte/store";
|
||||
|
||||
import { type Editor } from "@graphite/editor";
|
||||
import { TriggerFontLoad } from "@graphite/messages";
|
||||
|
||||
export function createFontsState(editor: Editor) {
|
||||
// TODO: Do some code cleanup to remove the need for this empty store
|
||||
const { subscribe } = writable({});
|
||||
|
||||
function createURL(font: string, weight: string): URL {
|
||||
const url = new URL("https://fonts.googleapis.com/css2");
|
||||
url.searchParams.set("display", "swap");
|
||||
url.searchParams.set("family", `${font}:wght@${weight}`);
|
||||
url.searchParams.set("text", font);
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
async function fontNames(): Promise<{ name: string; url: URL | undefined }[]> {
|
||||
const pickPreviewWeight = (variants: string[]) => {
|
||||
const weights = variants.map((variant) => Number(variant.match(/.* \((\d+)\)/)?.[1] || "NaN"));
|
||||
const weightGoal = 400;
|
||||
const sorted = weights.map((weight) => [weight, Math.abs(weightGoal - weight - 1)]);
|
||||
sorted.sort(([_, a], [__, b]) => a - b);
|
||||
return sorted[0][0].toString();
|
||||
};
|
||||
return (await loadFontList()).map((font) => ({ name: font.family, url: createURL(font.family, pickPreviewWeight(font.variants)) }));
|
||||
}
|
||||
|
||||
async function getFontStyles(fontFamily: string): Promise<{ name: string; url: URL | undefined }[]> {
|
||||
const font = (await loadFontList()).find((value) => value.family === fontFamily);
|
||||
return font?.variants.map((variant) => ({ name: variant, url: undefined })) || [];
|
||||
}
|
||||
|
||||
async function getFontFileUrl(fontFamily: string, fontStyle: string): Promise<string | undefined> {
|
||||
const font = (await loadFontList()).find((value) => value.family === fontFamily);
|
||||
const fontFileUrl = font?.files.get(fontStyle);
|
||||
return fontFileUrl?.replace("http://", "https://");
|
||||
}
|
||||
|
||||
function formatFontStyleName(fontStyle: string): string {
|
||||
const isItalic = fontStyle.endsWith("italic");
|
||||
const weight = fontStyle === "regular" || fontStyle === "italic" ? 400 : parseInt(fontStyle, 10);
|
||||
let weightName = "";
|
||||
|
||||
let bestWeight = Infinity;
|
||||
weightNameMapping.forEach((nameChecking, weightChecking) => {
|
||||
if (Math.abs(weightChecking - weight) < bestWeight) {
|
||||
bestWeight = Math.abs(weightChecking - weight);
|
||||
weightName = nameChecking;
|
||||
}
|
||||
});
|
||||
|
||||
return `${weightName}${isItalic ? " Italic" : ""} (${weight})`;
|
||||
}
|
||||
|
||||
let fontList: Promise<{ family: string; variants: string[]; files: Map<string, string> }[]> | undefined;
|
||||
|
||||
async function loadFontList(): Promise<{ family: string; variants: string[]; files: Map<string, string> }[]> {
|
||||
if (fontList) return fontList;
|
||||
|
||||
fontList = new Promise<{ family: string; variants: string[]; files: Map<string, string> }[]>((resolve) => {
|
||||
fetch(fontListAPI)
|
||||
.then((response) => response.json())
|
||||
.then((fontListResponse) => {
|
||||
const fontListData = fontListResponse.items as { family: string; variants: string[]; files: Record<string, string> }[];
|
||||
const result = fontListData.map((font) => {
|
||||
const { family } = font;
|
||||
const variants = font.variants.map(formatFontStyleName);
|
||||
const files = new Map(font.variants.map((x) => [formatFontStyleName(x), font.files[x]]));
|
||||
return { family, variants, files };
|
||||
});
|
||||
|
||||
resolve(result);
|
||||
});
|
||||
});
|
||||
|
||||
return fontList;
|
||||
}
|
||||
|
||||
// Subscribe to process backend events
|
||||
editor.subscriptions.subscribeJsMessage(TriggerFontLoad, async (triggerFontLoad) => {
|
||||
const url = await getFontFileUrl(triggerFontLoad.font.fontFamily, triggerFontLoad.font.fontStyle);
|
||||
if (url) {
|
||||
const response = await (await fetch(url)).arrayBuffer();
|
||||
editor.handle.onFontLoad(triggerFontLoad.font.fontFamily, triggerFontLoad.font.fontStyle, url, new Uint8Array(response));
|
||||
} else {
|
||||
editor.handle.errorDialog("Failed to load font", `The font ${triggerFontLoad.font.fontFamily} with style ${triggerFontLoad.font.fontStyle} does not exist`);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
fontNames,
|
||||
getFontStyles,
|
||||
getFontFileUrl,
|
||||
};
|
||||
}
|
||||
export type FontsState = ReturnType<typeof createFontsState>;
|
||||
|
||||
const fontListAPI = "https://api.graphite.art/font-list";
|
||||
|
||||
// From https://developer.mozilla.org/en-US/docs/Web/CSS/font-weight#common_weight_name_mapping
|
||||
const weightNameMapping = new Map([
|
||||
[100, "Thin"],
|
||||
[200, "Extra Light"],
|
||||
[300, "Light"],
|
||||
[400, "Regular"],
|
||||
[500, "Medium"],
|
||||
[600, "Semi Bold"],
|
||||
[700, "Bold"],
|
||||
[800, "Extra Bold"],
|
||||
[900, "Black"],
|
||||
[950, "Extra Black"],
|
||||
]);
|
||||
|
|
@ -12,7 +12,7 @@ use editor::messages::input_mapper::utility_types::input_keyboard::ModifierKeys;
|
|||
use editor::messages::input_mapper::utility_types::input_mouse::{EditorMouseState, ScrollDelta};
|
||||
use editor::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||
use editor::messages::portfolio::document::utility_types::network_interface::ImportOrExport;
|
||||
use editor::messages::portfolio::utility_types::Platform;
|
||||
use editor::messages::portfolio::utility_types::{FontCatalog, FontCatalogFamily, Platform};
|
||||
use editor::messages::prelude::*;
|
||||
use editor::messages::tool::tool_messages::tool_prelude::WidgetId;
|
||||
use graph_craft::document::NodeId;
|
||||
|
|
@ -116,7 +116,6 @@ impl EditorHandle {
|
|||
#[cfg(not(feature = "native"))]
|
||||
fn dispatch<T: Into<Message>>(&self, message: T) {
|
||||
// Process no further messages after a crash to avoid spamming the console
|
||||
|
||||
use crate::MESSAGE_BUFFER;
|
||||
if EDITOR_HAS_CRASHED.load(Ordering::SeqCst) {
|
||||
return;
|
||||
|
|
@ -330,21 +329,43 @@ impl EditorHandle {
|
|||
|
||||
/// Update the value of a given UI widget, but don't commit it to the history (unless `commit_layout()` is called, which handles that)
|
||||
#[wasm_bindgen(js_name = widgetValueUpdate)]
|
||||
pub fn widget_value_update(&self, layout_target: JsValue, widget_id: u64, value: JsValue) -> Result<(), JsValue> {
|
||||
pub fn widget_value_update(&self, layout_target: JsValue, widget_id: u64, value: JsValue, resend_widget: bool) -> Result<(), JsValue> {
|
||||
self.widget_value_update_helper(layout_target, widget_id, value, resend_widget)
|
||||
}
|
||||
|
||||
/// Commit the value of a given UI widget to the history
|
||||
#[wasm_bindgen(js_name = widgetValueCommit)]
|
||||
pub fn widget_value_commit(&self, layout_target: JsValue, widget_id: u64, value: JsValue) -> Result<(), JsValue> {
|
||||
self.widget_value_commit_helper(layout_target, widget_id, value)
|
||||
}
|
||||
|
||||
/// Update the value of a given UI widget, and commit it to the history
|
||||
#[wasm_bindgen(js_name = widgetValueCommitAndUpdate)]
|
||||
pub fn widget_value_commit_and_update(&self, layout_target: JsValue, widget_id: u64, value: JsValue, resend_widget: bool) -> Result<(), JsValue> {
|
||||
self.widget_value_commit_helper(layout_target.clone(), widget_id, value.clone())?;
|
||||
self.widget_value_update_helper(layout_target, widget_id, value, resend_widget)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn widget_value_update_helper(&self, layout_target: JsValue, widget_id: u64, value: JsValue, resend_widget: bool) -> Result<(), JsValue> {
|
||||
let widget_id = WidgetId(widget_id);
|
||||
match (from_value(layout_target), from_value(value)) {
|
||||
(Ok(layout_target), Ok(value)) => {
|
||||
let message = LayoutMessage::WidgetValueUpdate { layout_target, widget_id, value };
|
||||
self.dispatch(message);
|
||||
|
||||
if resend_widget {
|
||||
let resend_message = LayoutMessage::ResendActiveWidget { layout_target, widget_id };
|
||||
self.dispatch(resend_message);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
(target, val) => Err(Error::new(&format!("Could not update UI\nDetails:\nTarget: {target:?}\nValue: {val:?}")).into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Commit the value of a given UI widget to the history
|
||||
#[wasm_bindgen(js_name = widgetValueCommit)]
|
||||
pub fn widget_value_commit(&self, layout_target: JsValue, widget_id: u64, value: JsValue) -> Result<(), JsValue> {
|
||||
pub fn widget_value_commit_helper(&self, layout_target: JsValue, widget_id: u64, value: JsValue) -> Result<(), JsValue> {
|
||||
let widget_id = WidgetId(widget_id);
|
||||
match (from_value(layout_target), from_value(value)) {
|
||||
(Ok(layout_target), Ok(value)) => {
|
||||
|
|
@ -356,14 +377,6 @@ impl EditorHandle {
|
|||
}
|
||||
}
|
||||
|
||||
/// Update the value of a given UI widget, and commit it to the history
|
||||
#[wasm_bindgen(js_name = widgetValueCommitAndUpdate)]
|
||||
pub fn widget_value_commit_and_update(&self, layout_target: JsValue, widget_id: u64, value: JsValue) -> Result<(), JsValue> {
|
||||
self.widget_value_commit(layout_target.clone(), widget_id, value.clone())?;
|
||||
self.widget_value_update(layout_target, widget_id, value)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = loadPreferences)]
|
||||
pub fn load_preferences(&self, preferences: Option<String>) {
|
||||
let preferences = if let Some(preferences) = preferences {
|
||||
|
|
@ -562,15 +575,21 @@ impl EditorHandle {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// The font catalog has been loaded
|
||||
#[wasm_bindgen(js_name = onFontCatalogLoad)]
|
||||
pub fn on_font_catalog_load(&self, catalog: JsValue) -> Result<(), JsValue> {
|
||||
// Deserializing from TS type: `{ name: string; styles: { weight: number, italic: boolean, url: string }[] }[]`
|
||||
let families = serde_wasm_bindgen::from_value::<Vec<FontCatalogFamily>>(catalog)?;
|
||||
let message = PortfolioMessage::FontCatalogLoaded { catalog: FontCatalog(families) };
|
||||
self.dispatch(message);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A font has been downloaded
|
||||
#[wasm_bindgen(js_name = onFontLoad)]
|
||||
pub fn on_font_load(&self, font_family: String, font_style: String, preview_url: String, data: Vec<u8>) -> Result<(), JsValue> {
|
||||
let message = PortfolioMessage::FontLoaded {
|
||||
font_family,
|
||||
font_style,
|
||||
preview_url,
|
||||
data,
|
||||
};
|
||||
pub fn on_font_load(&self, font_family: String, font_style: String, data: Vec<u8>) -> Result<(), JsValue> {
|
||||
let message = PortfolioMessage::FontLoaded { font_family, font_style, data };
|
||||
self.dispatch(message);
|
||||
|
||||
Ok(())
|
||||
|
|
|
|||
|
|
@ -5,5 +5,5 @@ pub const LAYER_OUTLINE_STROKE_COLOR: Color = Color::BLACK;
|
|||
pub const LAYER_OUTLINE_STROKE_WEIGHT: f64 = 0.5;
|
||||
|
||||
// Fonts
|
||||
pub const DEFAULT_FONT_FAMILY: &str = "Cabin";
|
||||
pub const DEFAULT_FONT_FAMILY: &str = "Lato";
|
||||
pub const DEFAULT_FONT_STYLE: &str = "Regular (400)";
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ fn text<'i: 'n>(
|
|||
_: impl Ctx,
|
||||
editor: &'i WasmEditorApi,
|
||||
text: String,
|
||||
font_name: Font,
|
||||
font: Font,
|
||||
#[unit(" px")]
|
||||
#[default(24.)]
|
||||
font_size: f64,
|
||||
|
|
@ -39,5 +39,5 @@ fn text<'i: 'n>(
|
|||
align,
|
||||
};
|
||||
|
||||
to_path(&text, &font_name, &editor.font_cache, typesetting, per_glyph_instances)
|
||||
to_path(&text, &font, &editor.font_cache, typesetting, per_glyph_instances)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,16 +7,55 @@ use std::sync::Arc;
|
|||
use core_types::specta;
|
||||
|
||||
/// A font type (storing font family and font style and an optional preview URL)
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Hash, PartialEq, Eq, DynAny, core_types::specta::Type)]
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Eq, DynAny, core_types::specta::Type)]
|
||||
pub struct Font {
|
||||
#[serde(rename = "fontFamily")]
|
||||
pub font_family: String,
|
||||
#[serde(rename = "fontStyle", deserialize_with = "migrate_font_style")]
|
||||
pub font_style: String,
|
||||
#[serde(skip)]
|
||||
pub font_style_to_restore: Option<String>,
|
||||
}
|
||||
|
||||
impl std::hash::Hash for Font {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.font_family.hash(state);
|
||||
self.font_style.hash(state);
|
||||
// Don't consider `font_style_to_restore` in the HashMaps
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Font {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
// Don't consider `font_style_to_restore` in the HashMaps
|
||||
self.font_family == other.font_family && self.font_style == other.font_style
|
||||
}
|
||||
}
|
||||
|
||||
impl Font {
|
||||
pub fn new(font_family: String, font_style: String) -> Self {
|
||||
Self { font_family, font_style }
|
||||
Self {
|
||||
font_family,
|
||||
font_style,
|
||||
font_style_to_restore: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn named_weight(weight: u32) -> &'static str {
|
||||
// From https://developer.mozilla.org/en-US/docs/Web/CSS/font-weight#common_weight_name_mapping
|
||||
match weight {
|
||||
100 => "Thin",
|
||||
200 => "Extra Light",
|
||||
300 => "Light",
|
||||
400 => "Regular",
|
||||
500 => "Medium",
|
||||
600 => "Semi Bold",
|
||||
700 => "Bold",
|
||||
800 => "Extra Bold",
|
||||
900 => "Black",
|
||||
950 => "Extra Black",
|
||||
_ => "Regular",
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Default for Font {
|
||||
|
|
@ -24,21 +63,33 @@ impl Default for Font {
|
|||
Self::new(core_types::consts::DEFAULT_FONT_FAMILY.into(), core_types::consts::DEFAULT_FONT_STYLE.into())
|
||||
}
|
||||
}
|
||||
|
||||
/// A cache of all loaded font data and preview urls along with the default font (send from `init_app` in `editor_api.rs`)
|
||||
#[derive(Clone, serde::Serialize, serde::Deserialize, Default, PartialEq, DynAny)]
|
||||
#[derive(Clone, serde::Serialize, serde::Deserialize, Default, DynAny)]
|
||||
pub struct FontCache {
|
||||
/// Actual font file data used for rendering a font
|
||||
font_file_data: HashMap<Font, Vec<u8>>,
|
||||
/// Web font preview URLs used for showing fonts when live editing
|
||||
preview_urls: HashMap<Font, String>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for FontCache {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("FontCache")
|
||||
.field("font_file_data", &self.font_file_data.keys().collect::<Vec<_>>())
|
||||
.field("preview_urls", &self.preview_urls)
|
||||
.finish()
|
||||
f.debug_struct("FontCache").field("font_file_data", &self.font_file_data.keys().collect::<Vec<_>>()).finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::hash::Hash for FontCache {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.font_file_data.len().hash(state);
|
||||
self.font_file_data.keys().for_each(|font| font.hash(state));
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for FontCache {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
if self.font_file_data.len() != other.font_file_data.len() {
|
||||
return false;
|
||||
}
|
||||
self.font_file_data.keys().all(|font| other.font_file_data.contains_key(font))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -70,26 +121,8 @@ impl FontCache {
|
|||
}
|
||||
|
||||
/// Insert a new font into the cache
|
||||
pub fn insert(&mut self, font: Font, perview_url: String, data: Vec<u8>) {
|
||||
pub fn insert(&mut self, font: Font, data: Vec<u8>) {
|
||||
self.font_file_data.insert(font.clone(), data);
|
||||
self.preview_urls.insert(font, perview_url);
|
||||
}
|
||||
|
||||
/// Gets the preview URL for showing in text field when live editing
|
||||
pub fn get_preview_url(&self, font: &Font) -> Option<&String> {
|
||||
self.preview_urls.get(font)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::hash::Hash for FontCache {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.preview_urls.len().hash(state);
|
||||
self.preview_urls.iter().for_each(|(font, url)| {
|
||||
font.hash(state);
|
||||
url.hash(state)
|
||||
});
|
||||
self.font_file_data.len().hash(state);
|
||||
self.font_file_data.keys().for_each(|font| font.hash(state));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue