mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-23 06:48:11 +08:00
Migrate fonts to be stored as resources (#4165)
* Good start
* Impl
* allow responses.add(async { Message::NoOp });
* Fix
* Cache font blob in text node
* Refactor font handling to use Font struct direcly
* Fix fmt
* Embedd Font Resources by default
* Review
* Review
* Cache font info based on ResourceHash
This commit is contained in:
Generated
+1
@@ -5523,6 +5523,7 @@ dependencies = [
|
|||||||
"fancy-regex",
|
"fancy-regex",
|
||||||
"glam",
|
"glam",
|
||||||
"graphene-hash",
|
"graphene-hash",
|
||||||
|
"graphene-resource",
|
||||||
"log",
|
"log",
|
||||||
"node-macro",
|
"node-macro",
|
||||||
"parley",
|
"parley",
|
||||||
|
|||||||
@@ -65,7 +65,6 @@ const SIDE_EFFECT_FREE_MESSAGES: &[MessageDiscriminant] = &[
|
|||||||
))),
|
))),
|
||||||
MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::SubmitActiveGraphRender),
|
MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::SubmitActiveGraphRender),
|
||||||
MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::SubmitEyedropperPreviewRender),
|
MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::SubmitEyedropperPreviewRender),
|
||||||
MessageDiscriminant::Frontend(FrontendMessageDiscriminant::TriggerFontDataLoad),
|
|
||||||
MessageDiscriminant::Frontend(FrontendMessageDiscriminant::UpdateUIScale),
|
MessageDiscriminant::Frontend(FrontendMessageDiscriminant::UpdateUIScale),
|
||||||
];
|
];
|
||||||
/// Since we don't need to update the frontend multiple times per frame,
|
/// Since we don't need to update the frontend multiple times per frame,
|
||||||
@@ -80,6 +79,8 @@ const FRONTEND_UPDATE_MESSAGES: &[MessageDiscriminant] = &[
|
|||||||
MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::Document(DocumentMessageDiscriminant::RenderScrollbars)),
|
MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::Document(DocumentMessageDiscriminant::RenderScrollbars)),
|
||||||
MessageDiscriminant::Frontend(FrontendMessageDiscriminant::UpdateDocumentLayerStructure),
|
MessageDiscriminant::Frontend(FrontendMessageDiscriminant::UpdateDocumentLayerStructure),
|
||||||
];
|
];
|
||||||
|
// FrontendMessages that should be sent immediately
|
||||||
|
const IMMEDIATE_FRONTEND_MESSAGES: &[FrontendMessageDiscriminant] = &[FrontendMessageDiscriminant::TriggerResolveResource, FrontendMessageDiscriminant::TriggerFontCatalogLoad];
|
||||||
const DEBUG_MESSAGE_BLOCK_LIST: &[MessageDiscriminant] = &[
|
const DEBUG_MESSAGE_BLOCK_LIST: &[MessageDiscriminant] = &[
|
||||||
MessageDiscriminant::Broadcast(BroadcastMessageDiscriminant::TriggerEvent(EventMessageDiscriminant::AnimationFrame)),
|
MessageDiscriminant::Broadcast(BroadcastMessageDiscriminant::TriggerEvent(EventMessageDiscriminant::AnimationFrame)),
|
||||||
MessageDiscriminant::Animation(AnimationMessageDiscriminant::IncrementFrameCounter),
|
MessageDiscriminant::Animation(AnimationMessageDiscriminant::IncrementFrameCounter),
|
||||||
@@ -205,16 +206,13 @@ impl Dispatcher {
|
|||||||
self.message_handlers.dialog_message_handler.process_message(message, &mut queue, context);
|
self.message_handlers.dialog_message_handler.process_message(message, &mut queue, context);
|
||||||
}
|
}
|
||||||
Message::Frontend(message) => {
|
Message::Frontend(message) => {
|
||||||
// Handle these messages immediately by returning early
|
let decreminant = message.to_discriminant();
|
||||||
if let FrontendMessage::TriggerFontDataLoad { .. } | FrontendMessage::TriggerFontCatalogLoad = message {
|
self.responses.push(message);
|
||||||
self.responses.push(message);
|
|
||||||
self.cleanup_queues(false);
|
|
||||||
|
|
||||||
// Return early to avoid running the code after the match block
|
// Handle these message immediately by returning early
|
||||||
|
if IMMEDIATE_FRONTEND_MESSAGES.contains(&decreminant) {
|
||||||
|
self.cleanup_queues(false);
|
||||||
return;
|
return;
|
||||||
} else {
|
|
||||||
// `FrontendMessage`s are saved and will be sent to the frontend after the message queue is done being processed
|
|
||||||
self.responses.push(message);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Message::InputPreprocessor(message) => {
|
Message::InputPreprocessor(message) => {
|
||||||
@@ -325,7 +323,7 @@ impl Dispatcher {
|
|||||||
document_id,
|
document_id,
|
||||||
document,
|
document,
|
||||||
input: &self.message_handlers.input_preprocessor_message_handler,
|
input: &self.message_handlers.input_preprocessor_message_handler,
|
||||||
cached_data: &self.message_handlers.portfolio_message_handler.cached_data,
|
fonts: &self.message_handlers.portfolio_message_handler.fonts,
|
||||||
node_graph: &self.message_handlers.portfolio_message_handler.executor,
|
node_graph: &self.message_handlers.portfolio_message_handler.executor,
|
||||||
preferences: &self.message_handlers.preferences_message_handler,
|
preferences: &self.message_handlers.preferences_message_handler,
|
||||||
viewport: &self.message_handlers.viewport_message_handler,
|
viewport: &self.message_handlers.viewport_message_handler,
|
||||||
|
|||||||
@@ -12,10 +12,10 @@ use crate::messages::portfolio::document::utility_types::wires::{WirePath, WireP
|
|||||||
use crate::messages::portfolio::utility_types::WorkspacePanelLayout;
|
use crate::messages::portfolio::utility_types::WorkspacePanelLayout;
|
||||||
use crate::messages::prelude::*;
|
use crate::messages::prelude::*;
|
||||||
use crate::messages::tool::tool_messages::eyedropper_tool::PrimarySecondary;
|
use crate::messages::tool::tool_messages::eyedropper_tool::PrimarySecondary;
|
||||||
|
use graph_craft::application_io::resource::ResourceId;
|
||||||
use graph_craft::document::NodeId;
|
use graph_craft::document::NodeId;
|
||||||
use graphene_std::color::SRGBA8;
|
use graphene_std::color::SRGBA8;
|
||||||
use graphene_std::raster::Image;
|
use graphene_std::raster::Image;
|
||||||
use graphene_std::text::Font;
|
|
||||||
use graphene_std::vector::style::FillChoiceUI;
|
use graphene_std::vector::style::FillChoiceUI;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
@@ -112,8 +112,11 @@ pub enum FrontendMessage {
|
|||||||
filename: String,
|
filename: String,
|
||||||
},
|
},
|
||||||
TriggerFontCatalogLoad,
|
TriggerFontCatalogLoad,
|
||||||
TriggerFontDataLoad {
|
TriggerResolveResource {
|
||||||
font: Font,
|
#[serde(rename = "documentId")]
|
||||||
|
document_id: DocumentId,
|
||||||
|
#[serde(rename = "resourceId")]
|
||||||
|
resource_id: ResourceId,
|
||||||
url: String,
|
url: String,
|
||||||
},
|
},
|
||||||
TriggerPersistenceReadState,
|
TriggerPersistenceReadState,
|
||||||
|
|||||||
@@ -43,6 +43,21 @@ impl IntoFuture for MessageFuture {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<MessageFuture> for Message {
|
||||||
|
fn from(future: MessageFuture) -> Self {
|
||||||
|
FutureMessage::Await { future }.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> From<T> for Message
|
||||||
|
where
|
||||||
|
T: Future<Output = Message> + Send + 'static,
|
||||||
|
{
|
||||||
|
fn from(future: T) -> Self {
|
||||||
|
MessageFuture::new(future).into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Platform-specific async-task executor.
|
/// Platform-specific async-task executor.
|
||||||
/// Runs `future`, sends the resolved message on `results`, then calls `wake`.
|
/// Runs `future`, sends the resolved message on `results`, then calls `wake`.
|
||||||
pub trait MessageSpawner: Send + Sync {
|
pub trait MessageSpawner: Send + Sync {
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ use crate::messages::portfolio::document::properties_panel::properties_panel_mes
|
|||||||
use crate::messages::portfolio::document::utility_types::document_metadata::{DocumentMetadata, LayerNodeIdentifier};
|
use crate::messages::portfolio::document::utility_types::document_metadata::{DocumentMetadata, LayerNodeIdentifier};
|
||||||
use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate, AlignAxis, FlipAxis, PTZ};
|
use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate, AlignAxis, FlipAxis, PTZ};
|
||||||
use crate::messages::portfolio::document::utility_types::network_interface::{FlowType, InputConnector, NodeTemplate, OutputConnector};
|
use crate::messages::portfolio::document::utility_types::network_interface::{FlowType, InputConnector, NodeTemplate, OutputConnector};
|
||||||
use crate::messages::portfolio::utility_types::{CachedData, PanelType};
|
use crate::messages::portfolio::utility_types::PanelType;
|
||||||
use crate::messages::prelude::*;
|
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::common_functionality::graph_modification_utils::{self, get_blend_mode, get_fill, get_opacity};
|
||||||
use crate::messages::tool::tool_messages::select_tool::SelectToolPointerKeys;
|
use crate::messages::tool::tool_messages::select_tool::SelectToolPointerKeys;
|
||||||
@@ -52,7 +52,6 @@ use std::time::Duration;
|
|||||||
pub struct DocumentMessageContext<'a> {
|
pub struct DocumentMessageContext<'a> {
|
||||||
pub document_id: DocumentId,
|
pub document_id: DocumentId,
|
||||||
pub ipp: &'a InputPreprocessorMessageHandler,
|
pub ipp: &'a InputPreprocessorMessageHandler,
|
||||||
pub cached_data: &'a CachedData,
|
|
||||||
pub executor: &'a mut NodeGraphExecutor,
|
pub executor: &'a mut NodeGraphExecutor,
|
||||||
pub current_tool: &'a ToolType,
|
pub current_tool: &'a ToolType,
|
||||||
pub preferences: &'a PreferencesMessageHandler,
|
pub preferences: &'a PreferencesMessageHandler,
|
||||||
@@ -61,6 +60,7 @@ pub struct DocumentMessageContext<'a> {
|
|||||||
pub properties_panel_open: bool,
|
pub properties_panel_open: bool,
|
||||||
pub viewport: &'a ViewportMessageHandler,
|
pub viewport: &'a ViewportMessageHandler,
|
||||||
pub resource_storage: &'a ResourceStorageMessageHandler,
|
pub resource_storage: &'a ResourceStorageMessageHandler,
|
||||||
|
pub fonts: &'a FontsMessageHandler,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, ExtractField)]
|
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, ExtractField)]
|
||||||
@@ -198,7 +198,6 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
|
|||||||
let DocumentMessageContext {
|
let DocumentMessageContext {
|
||||||
document_id,
|
document_id,
|
||||||
ipp,
|
ipp,
|
||||||
cached_data,
|
|
||||||
executor,
|
executor,
|
||||||
viewport,
|
viewport,
|
||||||
current_tool,
|
current_tool,
|
||||||
@@ -207,6 +206,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
|
|||||||
layers_panel_open,
|
layers_panel_open,
|
||||||
properties_panel_open,
|
properties_panel_open,
|
||||||
resource_storage,
|
resource_storage,
|
||||||
|
fonts,
|
||||||
} = context;
|
} = context;
|
||||||
|
|
||||||
match message {
|
match message {
|
||||||
@@ -233,11 +233,12 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
|
|||||||
}
|
}
|
||||||
DocumentMessage::PropertiesPanel(message) => {
|
DocumentMessage::PropertiesPanel(message) => {
|
||||||
let context = PropertiesPanelMessageContext {
|
let context = PropertiesPanelMessageContext {
|
||||||
|
executor,
|
||||||
network_interface: &mut self.network_interface,
|
network_interface: &mut self.network_interface,
|
||||||
|
resources: &self.resources,
|
||||||
selection_network_path: &self.selection_network_path,
|
selection_network_path: &self.selection_network_path,
|
||||||
document_name: self.name.as_str(),
|
document_name: self.name.as_str(),
|
||||||
executor,
|
fonts,
|
||||||
cached_data,
|
|
||||||
properties_panel_open,
|
properties_panel_open,
|
||||||
};
|
};
|
||||||
self.properties_panel_message_handler.process_message(message, responses, context);
|
self.properties_panel_message_handler.process_message(message, responses, context);
|
||||||
@@ -282,7 +283,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
|
|||||||
graph_operation_message_handler.process_message(message, responses, context);
|
graph_operation_message_handler.process_message(message, responses, context);
|
||||||
}
|
}
|
||||||
DocumentMessage::Resource(message) => {
|
DocumentMessage::Resource(message) => {
|
||||||
let context = ResourceMessageContext {};
|
let context = ResourceMessageContext { document_id, fonts };
|
||||||
self.resources.process_message(message, responses, context);
|
self.resources.process_message(message, responses, context);
|
||||||
}
|
}
|
||||||
DocumentMessage::AlignSelectedLayers { axis, aggregate } => {
|
DocumentMessage::AlignSelectedLayers { axis, aggregate } => {
|
||||||
@@ -933,21 +934,19 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
|
|||||||
let mut document = self.clone();
|
let mut document = self.clone();
|
||||||
let resources_load_handle = resource_storage.resources();
|
let resources_load_handle = resource_storage.resources();
|
||||||
|
|
||||||
responses.add(FutureMessage::Await {
|
responses.add(async move {
|
||||||
future: MessageFuture::new(async move {
|
document.resources.collect_garbage(document.used_resources(false).as_ref());
|
||||||
document.resources.garbage_collect(document.used_resources(false).as_ref());
|
document.resources.embed_resources(resources_load_handle).await;
|
||||||
document.resources.embed_resources(resources_load_handle).await;
|
|
||||||
|
|
||||||
let content = document.serialize_document().into_bytes().into();
|
let content = document.serialize_document().into_bytes().into();
|
||||||
|
|
||||||
Message::Frontend(FrontendMessage::TriggerSaveDocument {
|
Message::Frontend(FrontendMessage::TriggerSaveDocument {
|
||||||
document_id,
|
document_id,
|
||||||
name,
|
name,
|
||||||
path,
|
path,
|
||||||
folder,
|
folder,
|
||||||
content,
|
content,
|
||||||
})
|
})
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
DocumentMessage::SavedDocument { path } => {
|
DocumentMessage::SavedDocument { path } => {
|
||||||
@@ -2658,23 +2657,6 @@ impl DocumentMessageHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Loads all of the fonts in the document.
|
|
||||||
pub fn load_layer_resources(&self, responses: &mut VecDeque<Message>) {
|
|
||||||
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_to_load.insert(font.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for font in fonts_to_load {
|
|
||||||
responses.add(PortfolioMessage::LoadFontData { font });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update_document_widgets(&self, responses: &mut VecDeque<Message>, animation_is_playing: bool, time: Duration) {
|
pub fn update_document_widgets(&self, responses: &mut VecDeque<Message>, animation_is_playing: bool, time: Duration) {
|
||||||
let mut snapping_state = self.snapping_state.clone();
|
let mut snapping_state = self.snapping_state.clone();
|
||||||
let mut snapping_state2 = self.snapping_state.clone();
|
let mut snapping_state2 = self.snapping_state.clone();
|
||||||
@@ -3470,7 +3452,7 @@ impl DocumentMessageHandler {
|
|||||||
|
|
||||||
pub fn garbage_collect_resources(&mut self) {
|
pub fn garbage_collect_resources(&mut self) {
|
||||||
let used_resources = self.used_resources(true);
|
let used_resources = self.used_resources(true);
|
||||||
self.resources.garbage_collect(&used_resources);
|
self.resources.collect_garbage(&used_resources);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn used_resources(&self, include_history: bool) -> Box<[ResourceId]> {
|
pub fn used_resources(&self, include_history: bool) -> Box<[ResourceId]> {
|
||||||
|
|||||||
@@ -249,12 +249,13 @@ impl<'a> ModifyInputsContext<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn insert_text(&mut self, text: String, font: Font, typesetting: TypesettingConfig, layer: LayerNodeIdentifier) {
|
pub fn insert_text(&mut self, text: String, font: Font, typesetting: TypesettingConfig, layer: LayerNodeIdentifier) {
|
||||||
|
let font_resource_id = ResourceId::new();
|
||||||
let text = resolve_proto_node_type(graphene_std::text::text::IDENTIFIER)
|
let text = resolve_proto_node_type(graphene_std::text::text::IDENTIFIER)
|
||||||
.expect("Text node does not exist")
|
.expect("Text node does not exist")
|
||||||
.node_template_input_override([
|
.node_template_input_override([
|
||||||
Some(NodeInput::scope("editor-api")),
|
Some(NodeInput::value(TaggedValue::None, false)),
|
||||||
Some(NodeInput::value(TaggedValue::String(text), false)),
|
Some(NodeInput::value(TaggedValue::String(text), false)),
|
||||||
Some(NodeInput::value(TaggedValue::Font(font), false)),
|
Some(NodeInput::value(TaggedValue::Resource(font_resource_id), false)),
|
||||||
Some(NodeInput::value(TaggedValue::F64(typesetting.font_size), false)),
|
Some(NodeInput::value(TaggedValue::F64(typesetting.font_size), false)),
|
||||||
Some(NodeInput::value(TaggedValue::F64(typesetting.line_height_ratio), false)),
|
Some(NodeInput::value(TaggedValue::F64(typesetting.line_height_ratio), false)),
|
||||||
Some(NodeInput::value(TaggedValue::F64(typesetting.character_spacing), false)),
|
Some(NodeInput::value(TaggedValue::F64(typesetting.character_spacing), false)),
|
||||||
@@ -264,6 +265,7 @@ impl<'a> ModifyInputsContext<'a> {
|
|||||||
Some(NodeInput::value(TaggedValue::F64(typesetting.max_height.unwrap_or(100.)), false)),
|
Some(NodeInput::value(TaggedValue::F64(typesetting.max_height.unwrap_or(100.)), false)),
|
||||||
Some(NodeInput::value(TaggedValue::F64(typesetting.tilt), false)),
|
Some(NodeInput::value(TaggedValue::F64(typesetting.tilt), false)),
|
||||||
Some(NodeInput::value(TaggedValue::TextAlign(typesetting.align), false)),
|
Some(NodeInput::value(TaggedValue::TextAlign(typesetting.align), false)),
|
||||||
|
Some(NodeInput::value(TaggedValue::Bool(false), false)),
|
||||||
]);
|
]);
|
||||||
let transform = resolve_proto_node_type(graphene_std::transform_nodes::transform::IDENTIFIER)
|
let transform = resolve_proto_node_type(graphene_std::transform_nodes::transform::IDENTIFIER)
|
||||||
.expect("Transform node does not exist")
|
.expect("Transform node does not exist")
|
||||||
@@ -276,6 +278,8 @@ impl<'a> ModifyInputsContext<'a> {
|
|||||||
self.network_interface.insert_node(text_id, text, &[]);
|
self.network_interface.insert_node(text_id, text, &[]);
|
||||||
self.network_interface.move_node_to_chain_start(&text_id, layer, &[], self.import);
|
self.network_interface.move_node_to_chain_start(&text_id, layer, &[], self.import);
|
||||||
|
|
||||||
|
self.responses.add(DocumentMessage::Resource(ResourceMessage::AddFont { resource_id: font_resource_id, font }));
|
||||||
|
|
||||||
let transform_id = NodeId::new();
|
let transform_id = NodeId::new();
|
||||||
self.network_interface.insert_node(transform_id, transform, &[]);
|
self.network_interface.insert_node(transform_id, transform, &[]);
|
||||||
self.network_interface.move_node_to_chain_start(&transform_id, layer, &[], self.import);
|
self.network_interface.move_node_to_chain_start(&transform_id, layer, &[], self.import);
|
||||||
|
|||||||
@@ -8,8 +8,7 @@ use crate::messages::portfolio::document::utility_types::network_interface::{
|
|||||||
DocumentNodeMetadata, DocumentNodePersistentMetadata, InputMetadata, NodeNetworkInterface, NodeNetworkMetadata, NodeNetworkPersistentMetadata, NodeTemplate, NodeTypePersistentMetadata,
|
DocumentNodeMetadata, DocumentNodePersistentMetadata, InputMetadata, NodeNetworkInterface, NodeNetworkMetadata, NodeNetworkPersistentMetadata, NodeTemplate, NodeTypePersistentMetadata,
|
||||||
Vec2InputSettings, WidgetOverride,
|
Vec2InputSettings, WidgetOverride,
|
||||||
};
|
};
|
||||||
use crate::messages::portfolio::utility_types::CachedData;
|
use crate::messages::prelude::{FontsMessageHandler, Message, ResourceMessageHandler};
|
||||||
use crate::messages::prelude::Message;
|
|
||||||
use crate::node_graph_executor::NodeGraphExecutor;
|
use crate::node_graph_executor::NodeGraphExecutor;
|
||||||
use glam::DVec2;
|
use glam::DVec2;
|
||||||
use graph_craft::ProtoNodeIdentifier;
|
use graph_craft::ProtoNodeIdentifier;
|
||||||
@@ -28,10 +27,11 @@ use serde_json::Value;
|
|||||||
use std::collections::{HashMap, VecDeque};
|
use std::collections::{HashMap, VecDeque};
|
||||||
|
|
||||||
pub struct NodePropertiesContext<'a> {
|
pub struct NodePropertiesContext<'a> {
|
||||||
pub cached_data: &'a CachedData,
|
|
||||||
pub responses: &'a mut VecDeque<Message>,
|
pub responses: &'a mut VecDeque<Message>,
|
||||||
pub executor: &'a mut NodeGraphExecutor,
|
pub executor: &'a mut NodeGraphExecutor,
|
||||||
pub network_interface: &'a mut NodeNetworkInterface,
|
pub network_interface: &'a mut NodeNetworkInterface,
|
||||||
|
pub resources: &'a ResourceMessageHandler,
|
||||||
|
pub fonts: &'a FontsMessageHandler,
|
||||||
pub selection_network_path: &'a [NodeId],
|
pub selection_network_path: &'a [NodeId],
|
||||||
pub document_name: &'a str,
|
pub document_name: &'a str,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,12 +6,13 @@ use crate::messages::layout::utility_types::widget_prelude::*;
|
|||||||
use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type;
|
use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type;
|
||||||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||||
use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeNetworkInterface};
|
use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeNetworkInterface};
|
||||||
use crate::messages::portfolio::utility_types::{CachedData, FontCatalogStyle};
|
use crate::messages::portfolio::fonts::utility_types::FontCatalogStyle;
|
||||||
use crate::messages::prelude::*;
|
use crate::messages::prelude::*;
|
||||||
use crate::messages::tool::common_functionality::graph_modification_utils;
|
use crate::messages::tool::common_functionality::graph_modification_utils;
|
||||||
use choice::enum_choice;
|
use choice::enum_choice;
|
||||||
use dyn_any::DynAny;
|
use dyn_any::DynAny;
|
||||||
use glam::{DAffine2, DVec2};
|
use glam::{DAffine2, DVec2};
|
||||||
|
use graph_craft::application_io::resource::ResourceId;
|
||||||
use graph_craft::document::value::TaggedValue;
|
use graph_craft::document::value::TaggedValue;
|
||||||
use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeId, NodeInput};
|
use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeId, NodeInput};
|
||||||
use graph_craft::{Type, concrete};
|
use graph_craft::{Type, concrete};
|
||||||
@@ -822,11 +823,27 @@ pub fn array_of_number_widget(parameter_widgets_info: ParameterWidgetsInfo, text
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn font_inputs(parameter_widgets_info: ParameterWidgetsInfo) -> (Vec<WidgetInstance>, Option<Vec<WidgetInstance>>) {
|
pub fn font_inputs(parameter_widgets_info: ParameterWidgetsInfo) -> (Vec<WidgetInstance>, Option<Vec<WidgetInstance>>) {
|
||||||
|
pub fn assign_font_message(node_id: NodeId, font: Font) -> Message {
|
||||||
|
let resource_id = ResourceId::new();
|
||||||
|
Message::Batched {
|
||||||
|
messages: Box::new([
|
||||||
|
DocumentMessage::Resource(ResourceMessage::AddFont { resource_id, font }).into(),
|
||||||
|
NodeGraphMessage::SetInputValue {
|
||||||
|
node_id,
|
||||||
|
input_index: graphene_std::text::text::FontInput::INDEX,
|
||||||
|
value: TaggedValue::Resource(resource_id),
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let ParameterWidgetsInfo {
|
let ParameterWidgetsInfo {
|
||||||
cached_data,
|
|
||||||
document_node,
|
document_node,
|
||||||
node_id,
|
node_id,
|
||||||
index,
|
index,
|
||||||
|
resources,
|
||||||
|
fonts,
|
||||||
..
|
..
|
||||||
} = parameter_widgets_info;
|
} = parameter_widgets_info;
|
||||||
|
|
||||||
@@ -839,66 +856,32 @@ pub fn font_inputs(parameter_widgets_info: ParameterWidgetsInfo) -> (Vec<WidgetI
|
|||||||
return (vec![], None);
|
return (vec![], None);
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(TaggedValue::Font(font)) = &input.as_non_exposed_value() {
|
if let Some(TaggedValue::Resource(resource_id)) = input.as_non_exposed_value() {
|
||||||
|
let font = fonts.id_font(resources, *resource_id).unwrap_or_default();
|
||||||
first_widgets.extend_from_slice(&[
|
first_widgets.extend_from_slice(&[
|
||||||
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||||
DropdownInput::new(vec![
|
DropdownInput::new(vec![
|
||||||
cached_data
|
fonts
|
||||||
.font_catalog
|
.font_catalog
|
||||||
.0
|
|
||||||
.iter()
|
.iter()
|
||||||
.map(|family| {
|
.map(|family| {
|
||||||
|
let FontCatalogStyle { weight, italic, .. } = FontCatalogStyle::from_named_style(&font.font_style, "");
|
||||||
|
let new_font = Font::new(family.name.clone(), family.closest_style(weight, italic).to_named_style());
|
||||||
|
let commit_font = new_font.clone();
|
||||||
MenuListEntry::new(family.name.clone())
|
MenuListEntry::new(family.name.clone())
|
||||||
.label(family.name.clone())
|
.label(family.name.clone())
|
||||||
.font(family.closest_style(400, false).preview_url(&family.name))
|
.font(family.closest_style(400, false).preview_url(&family.name))
|
||||||
.on_update({
|
.on_update(move |_| assign_font_message(node_id, new_font.clone()))
|
||||||
// Construct the new font using the new family and the initial or previous style, although this style might not exist in the catalog
|
.on_commit(move |_| {
|
||||||
let mut new_font = Font::new(family.name.clone(), font.font_style_to_restore.clone().unwrap_or_else(|| font.font_style.clone()));
|
DeferMessage::AfterGraphRun {
|
||||||
new_font.font_style_to_restore = font.font_style_to_restore.clone();
|
messages: vec![assign_font_message(node_id, commit_font.clone()), commit_value(&())],
|
||||||
|
|
||||||
// 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 |_| {
|
|
||||||
// Intentionally drop `font_style_to_restore` on commit so the committed style becomes the new basis
|
|
||||||
// for subsequent family switches. Preserving the original style intent is hover-only behavior.
|
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
|
.into()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>(),
|
.collect::<Vec<_>>(),
|
||||||
])
|
])
|
||||||
.selected_index(cached_data.font_catalog.0.iter().position(|family| family.name == font.font_family).map(|i| i as u32))
|
.selected_index(fonts.font_catalog.iter().position(|family| family.name == font.font_family).map(|i| i as u32))
|
||||||
.virtual_scrolling(true)
|
.virtual_scrolling(true)
|
||||||
.widget_instance(),
|
.widget_instance(),
|
||||||
]);
|
]);
|
||||||
@@ -908,32 +891,18 @@ pub fn font_inputs(parameter_widgets_info: ParameterWidgetsInfo) -> (Vec<WidgetI
|
|||||||
second_row.extend_from_slice(&[
|
second_row.extend_from_slice(&[
|
||||||
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||||
DropdownInput::new({
|
DropdownInput::new({
|
||||||
cached_data
|
fonts
|
||||||
.font_catalog
|
.font_catalog
|
||||||
.0
|
|
||||||
.iter()
|
.iter()
|
||||||
.find(|family| family.name == font.font_family)
|
.find(|family| family.name == font.font_family)
|
||||||
.map(|family| {
|
.map(|family| {
|
||||||
let build_entry = |style: &FontCatalogStyle| {
|
let build_entry = |style: &FontCatalogStyle| {
|
||||||
let font_style = style.to_named_style();
|
let font_style = style.to_named_style();
|
||||||
|
let font_family = font.font_family.clone();
|
||||||
|
let new_font = Font::new(font_family, font_style.clone());
|
||||||
MenuListEntry::new(font_style.clone())
|
MenuListEntry::new(font_style.clone())
|
||||||
.label(font_style.clone())
|
.label(font_style)
|
||||||
.on_update({
|
.on_update(move |_| assign_font_message(node_id, new_font.clone()))
|
||||||
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)
|
.on_commit(commit_value)
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -946,9 +915,8 @@ pub fn font_inputs(parameter_widgets_info: ParameterWidgetsInfo) -> (Vec<WidgetI
|
|||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
})
|
})
|
||||||
.selected_index(
|
.selected_index(
|
||||||
cached_data
|
fonts
|
||||||
.font_catalog
|
.font_catalog
|
||||||
.0
|
|
||||||
.iter()
|
.iter()
|
||||||
.find(|family| family.name == font.font_family)
|
.find(|family| family.name == font.font_family)
|
||||||
.and_then(|family| {
|
.and_then(|family| {
|
||||||
@@ -2813,8 +2781,8 @@ pub fn math_properties(node_id: NodeId, context: &mut NodePropertiesContext) ->
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct ParameterWidgetsInfo<'a> {
|
pub struct ParameterWidgetsInfo<'a> {
|
||||||
cached_data: &'a CachedData,
|
|
||||||
network_interface: &'a NodeNetworkInterface,
|
network_interface: &'a NodeNetworkInterface,
|
||||||
|
resources: &'a ResourceMessageHandler,
|
||||||
selection_network_path: &'a [NodeId],
|
selection_network_path: &'a [NodeId],
|
||||||
document_node: Option<&'a DocumentNode>,
|
document_node: Option<&'a DocumentNode>,
|
||||||
node_id: NodeId,
|
node_id: NodeId,
|
||||||
@@ -2824,6 +2792,7 @@ pub struct ParameterWidgetsInfo<'a> {
|
|||||||
input_type: FrontendGraphDataType,
|
input_type: FrontendGraphDataType,
|
||||||
blank_assist: bool,
|
blank_assist: bool,
|
||||||
exposable: bool,
|
exposable: bool,
|
||||||
|
fonts: &'a FontsMessageHandler,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> ParameterWidgetsInfo<'a> {
|
impl<'a> ParameterWidgetsInfo<'a> {
|
||||||
@@ -2836,9 +2805,10 @@ impl<'a> ParameterWidgetsInfo<'a> {
|
|||||||
let document_node = context.network_interface.document_node(&node_id, context.selection_network_path);
|
let document_node = context.network_interface.document_node(&node_id, context.selection_network_path);
|
||||||
|
|
||||||
ParameterWidgetsInfo {
|
ParameterWidgetsInfo {
|
||||||
cached_data: context.cached_data,
|
|
||||||
network_interface: context.network_interface,
|
network_interface: context.network_interface,
|
||||||
|
resources: context.resources,
|
||||||
selection_network_path: context.selection_network_path,
|
selection_network_path: context.selection_network_path,
|
||||||
|
fonts: context.fonts,
|
||||||
document_node,
|
document_node,
|
||||||
node_id,
|
node_id,
|
||||||
index,
|
index,
|
||||||
|
|||||||
@@ -2,11 +2,12 @@ use super::utility_types::{DrawHandles, OverlayContext};
|
|||||||
use crate::consts::HIDE_HANDLE_DISTANCE;
|
use crate::consts::HIDE_HANDLE_DISTANCE;
|
||||||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||||
use crate::messages::portfolio::document::utility_types::network_interface::NodeNetworkInterface;
|
use crate::messages::portfolio::document::utility_types::network_interface::NodeNetworkInterface;
|
||||||
|
use crate::messages::portfolio::fonts::FALLBACK_FONT_RESOURCE;
|
||||||
use crate::messages::tool::common_functionality::shape_editor::{SelectedLayerState, ShapeState};
|
use crate::messages::tool::common_functionality::shape_editor::{SelectedLayerState, ShapeState};
|
||||||
use crate::messages::tool::tool_messages::tool_prelude::DocumentMessageHandler;
|
use crate::messages::tool::tool_messages::tool_prelude::DocumentMessageHandler;
|
||||||
use glam::{DAffine2, DVec2};
|
use glam::{DAffine2, DVec2};
|
||||||
use graphene_std::subpath::{Bezier, BezierHandles};
|
use graphene_std::subpath::{Bezier, BezierHandles};
|
||||||
use graphene_std::text::{Font, FontCache, TextAlign, TextContext, TypesettingConfig};
|
use graphene_std::text::{TextAlign, TextContext, TypesettingConfig};
|
||||||
use graphene_std::vector::misc::ManipulatorPointId;
|
use graphene_std::vector::misc::ManipulatorPointId;
|
||||||
use graphene_std::vector::{PointId, SegmentId, Vector};
|
use graphene_std::vector::{PointId, SegmentId, Vector};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
@@ -221,16 +222,6 @@ pub fn path_endpoint_overlays(document: &DocumentMessageHandler, shape_editor: &
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Global lazy initialized font cache and text context
|
|
||||||
pub static GLOBAL_FONT_CACHE: LazyLock<FontCache> = LazyLock::new(|| {
|
|
||||||
let mut font_cache = FontCache::default();
|
|
||||||
// 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, FONT_DATA.to_vec());
|
|
||||||
font_cache
|
|
||||||
});
|
|
||||||
|
|
||||||
pub static GLOBAL_TEXT_CONTEXT: LazyLock<Mutex<TextContext>> = LazyLock::new(|| Mutex::new(TextContext::default()));
|
pub static GLOBAL_TEXT_CONTEXT: LazyLock<Mutex<TextContext>> = LazyLock::new(|| Mutex::new(TextContext::default()));
|
||||||
|
|
||||||
pub fn text_width(text: &str, font_size: f64) -> f64 {
|
pub fn text_width(text: &str, font_size: f64) -> f64 {
|
||||||
@@ -244,12 +235,8 @@ pub fn text_width(text: &str, font_size: f64) -> f64 {
|
|||||||
align: TextAlign::AlignLeft,
|
align: TextAlign::AlignLeft,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Load Source Sans Pro font data
|
|
||||||
// TODO: Grab this from the node_modules folder (either with `include_bytes!` or ideally at runtime) instead of checking the font file into the repo.
|
|
||||||
// TODO: And maybe use the WOFF2 version (if it's supported) for its smaller, compressed file size.
|
|
||||||
let font = Font::new("Source Sans Pro".to_string(), "Regular".to_string());
|
|
||||||
let mut text_context = GLOBAL_TEXT_CONTEXT.lock().expect("Failed to lock global text context");
|
let mut text_context = GLOBAL_TEXT_CONTEXT.lock().expect("Failed to lock global text context");
|
||||||
let bounds = text_context.bounding_box(text, &font, &GLOBAL_FONT_CACHE, typesetting, false);
|
let bounds = text_context.bounding_box(text, &FALLBACK_FONT_RESOURCE, typesetting, false);
|
||||||
bounds.x
|
bounds.x
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,9 @@ use crate::consts::{
|
|||||||
COLOR_OVERLAY_YELLOW_DULL, COMPASS_ROSE_ARROW_SIZE, COMPASS_ROSE_HOVER_RING_DIAMETER, COMPASS_ROSE_MAIN_RING_DIAMETER, COMPASS_ROSE_RING_INNER_DIAMETER, DOWEL_PIN_RADIUS,
|
COLOR_OVERLAY_YELLOW_DULL, COMPASS_ROSE_ARROW_SIZE, COMPASS_ROSE_HOVER_RING_DIAMETER, COMPASS_ROSE_MAIN_RING_DIAMETER, COMPASS_ROSE_RING_INNER_DIAMETER, DOWEL_PIN_RADIUS,
|
||||||
GRADIENT_MIDPOINT_DIAMOND_RADIUS, MANIPULATOR_GROUP_MARKER_SIZE, PIVOT_CROSSHAIR_LENGTH, PIVOT_CROSSHAIR_THICKNESS, PIVOT_DIAMETER, RESIZE_HANDLE_SIZE, SKEW_TRIANGLE_OFFSET, SKEW_TRIANGLE_SIZE,
|
GRADIENT_MIDPOINT_DIAMOND_RADIUS, MANIPULATOR_GROUP_MARKER_SIZE, PIVOT_CROSSHAIR_LENGTH, PIVOT_CROSSHAIR_THICKNESS, PIVOT_DIAMETER, RESIZE_HANDLE_SIZE, SKEW_TRIANGLE_OFFSET, SKEW_TRIANGLE_SIZE,
|
||||||
};
|
};
|
||||||
use crate::messages::portfolio::document::overlays::utility_functions::{GLOBAL_FONT_CACHE, GLOBAL_TEXT_CONTEXT, hex_to_rgba_u8};
|
use crate::messages::portfolio::document::overlays::utility_functions::{GLOBAL_TEXT_CONTEXT, hex_to_rgba_u8};
|
||||||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||||
|
use crate::messages::portfolio::fonts::FALLBACK_FONT_RESOURCE;
|
||||||
use crate::messages::prelude::Message;
|
use crate::messages::prelude::Message;
|
||||||
use crate::messages::prelude::ViewportMessageHandler;
|
use crate::messages::prelude::ViewportMessageHandler;
|
||||||
use core::borrow::Borrow;
|
use core::borrow::Borrow;
|
||||||
@@ -14,7 +15,7 @@ use graphene_std::ATTR_TRANSFORM;
|
|||||||
use graphene_std::list::List;
|
use graphene_std::list::List;
|
||||||
use graphene_std::math::quad::Quad;
|
use graphene_std::math::quad::Quad;
|
||||||
use graphene_std::subpath::{self, Subpath};
|
use graphene_std::subpath::{self, Subpath};
|
||||||
use graphene_std::text::{Font, TextAlign, TypesettingConfig};
|
use graphene_std::text::{TextAlign, TypesettingConfig};
|
||||||
use graphene_std::vector::click_target::ClickTargetType;
|
use graphene_std::vector::click_target::ClickTargetType;
|
||||||
use graphene_std::vector::misc::point_to_dvec2;
|
use graphene_std::vector::misc::point_to_dvec2;
|
||||||
use graphene_std::vector::{PointId, SegmentId, Vector};
|
use graphene_std::vector::{PointId, SegmentId, Vector};
|
||||||
@@ -1115,21 +1116,16 @@ impl OverlayContextInternal {
|
|||||||
align: TextAlign::AlignLeft,
|
align: TextAlign::AlignLeft,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Load Source Sans Pro font data
|
|
||||||
// TODO: Grab this from the node_modules folder (either with `include_bytes!` or ideally at runtime) instead of checking the font file into the repo.
|
|
||||||
// TODO: And maybe use the WOFF2 version (if it's supported) for its smaller, compressed file size.
|
|
||||||
let font = Font::new("Source Sans Pro".to_string(), "Regular".to_string());
|
|
||||||
|
|
||||||
// Get text dimensions directly from layout
|
// Get text dimensions directly from layout
|
||||||
let mut text_context = GLOBAL_TEXT_CONTEXT.lock().expect("Failed to lock global text context");
|
let mut text_context = GLOBAL_TEXT_CONTEXT.lock().expect("Failed to lock global text context");
|
||||||
let text_size = text_context.bounding_box(text, &font, &GLOBAL_FONT_CACHE, typesetting, false);
|
let text_size = text_context.bounding_box(text, &FALLBACK_FONT_RESOURCE, typesetting, false);
|
||||||
let text_width = text_size.x;
|
let text_width = text_size.x;
|
||||||
let text_height = text_size.y;
|
let text_height = text_size.y;
|
||||||
// Create a rect from the size (assuming text starts at origin)
|
// Create a rect from the size (assuming text starts at origin)
|
||||||
let text_bounds = kurbo::Rect::new(0., 0., text_width, text_height);
|
let text_bounds = kurbo::Rect::new(0., 0., text_width, text_height);
|
||||||
|
|
||||||
// Convert text to vector paths for rendering
|
// Convert text to vector paths for rendering
|
||||||
let text_list = text_context.to_path(text, &font, &GLOBAL_FONT_CACHE, typesetting, false);
|
let text_list = text_context.to_path(text, &FALLBACK_FONT_RESOURCE, typesetting, false);
|
||||||
|
|
||||||
// Calculate position based on pivot
|
// Calculate position based on pivot
|
||||||
let mut position = DVec2::ZERO;
|
let mut position = DVec2::ZERO;
|
||||||
|
|||||||
+9
-7
@@ -3,17 +3,17 @@ use graphene_std::uuid::NodeId;
|
|||||||
use crate::messages::layout::utility_types::widget_prelude::*;
|
use crate::messages::layout::utility_types::widget_prelude::*;
|
||||||
use crate::messages::portfolio::document::node_graph::document_node_definitions::NodePropertiesContext;
|
use crate::messages::portfolio::document::node_graph::document_node_definitions::NodePropertiesContext;
|
||||||
use crate::messages::portfolio::document::utility_types::network_interface::NodeNetworkInterface;
|
use crate::messages::portfolio::document::utility_types::network_interface::NodeNetworkInterface;
|
||||||
use crate::messages::portfolio::utility_types::CachedData;
|
|
||||||
use crate::messages::prelude::*;
|
use crate::messages::prelude::*;
|
||||||
use crate::node_graph_executor::NodeGraphExecutor;
|
use crate::node_graph_executor::NodeGraphExecutor;
|
||||||
|
|
||||||
#[derive(ExtractField)]
|
#[derive(ExtractField)]
|
||||||
pub struct PropertiesPanelMessageContext<'a> {
|
pub struct PropertiesPanelMessageContext<'a> {
|
||||||
|
pub executor: &'a mut NodeGraphExecutor,
|
||||||
pub network_interface: &'a mut NodeNetworkInterface,
|
pub network_interface: &'a mut NodeNetworkInterface,
|
||||||
|
pub resources: &'a ResourceMessageHandler,
|
||||||
pub selection_network_path: &'a [NodeId],
|
pub selection_network_path: &'a [NodeId],
|
||||||
pub document_name: &'a str,
|
pub document_name: &'a str,
|
||||||
pub executor: &'a mut NodeGraphExecutor,
|
pub fonts: &'a FontsMessageHandler,
|
||||||
pub cached_data: &'a CachedData,
|
|
||||||
pub properties_panel_open: bool,
|
pub properties_panel_open: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,11 +24,12 @@ pub struct PropertiesPanelMessageHandler {}
|
|||||||
impl MessageHandler<PropertiesPanelMessage, PropertiesPanelMessageContext<'_>> for PropertiesPanelMessageHandler {
|
impl MessageHandler<PropertiesPanelMessage, PropertiesPanelMessageContext<'_>> for PropertiesPanelMessageHandler {
|
||||||
fn process_message(&mut self, message: PropertiesPanelMessage, responses: &mut VecDeque<Message>, context: PropertiesPanelMessageContext) {
|
fn process_message(&mut self, message: PropertiesPanelMessage, responses: &mut VecDeque<Message>, context: PropertiesPanelMessageContext) {
|
||||||
let PropertiesPanelMessageContext {
|
let PropertiesPanelMessageContext {
|
||||||
|
executor,
|
||||||
network_interface,
|
network_interface,
|
||||||
|
resources,
|
||||||
selection_network_path,
|
selection_network_path,
|
||||||
document_name,
|
document_name,
|
||||||
executor,
|
fonts,
|
||||||
cached_data,
|
|
||||||
properties_panel_open,
|
properties_panel_open,
|
||||||
} = context;
|
} = context;
|
||||||
|
|
||||||
@@ -46,12 +47,13 @@ impl MessageHandler<PropertiesPanelMessage, PropertiesPanelMessageContext<'_>> f
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut node_properties_context = NodePropertiesContext {
|
let mut node_properties_context = NodePropertiesContext {
|
||||||
cached_data,
|
|
||||||
responses,
|
responses,
|
||||||
|
executor,
|
||||||
network_interface,
|
network_interface,
|
||||||
|
resources,
|
||||||
selection_network_path,
|
selection_network_path,
|
||||||
document_name,
|
document_name,
|
||||||
executor,
|
fonts,
|
||||||
};
|
};
|
||||||
let layout = Layout(NodeGraphMessageHandler::collate_properties(&mut node_properties_context));
|
let layout = Layout(NodeGraphMessageHandler::collate_properties(&mut node_properties_context));
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
use crate::messages::prelude::*;
|
use crate::messages::prelude::*;
|
||||||
use graph_craft::application_io::resource::ResourceId;
|
use graph_craft::application_io::resource::ResourceId;
|
||||||
|
use graphene_std::text::Font;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
#[impl_message(Message, DocumentMessage, Resource)]
|
#[impl_message(Message, DocumentMessage, Resource)]
|
||||||
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
|
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||||
pub enum ResourceMessage {
|
pub enum ResourceMessage {
|
||||||
StoreEmbedded { resource_id: ResourceId, data: Arc<[u8]> },
|
StoreEmbedded { resource_id: ResourceId, data: Arc<[u8]> },
|
||||||
|
AddFont { resource_id: ResourceId, font: Font },
|
||||||
|
Resolve,
|
||||||
|
ResolveStep { resource_id: ResourceId },
|
||||||
|
Resolved { resource_id: ResourceId, data: Arc<[u8]> },
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,25 +3,149 @@ use crate::messages::prelude::*;
|
|||||||
use base64::Engine;
|
use base64::Engine;
|
||||||
use base64::engine::general_purpose::STANDARD as BASE64;
|
use base64::engine::general_purpose::STANDARD as BASE64;
|
||||||
use graph_craft::application_io::resource::{DataSource, LoadResource, Resource, ResourceHash, ResourceId, ResourceRegistry};
|
use graph_craft::application_io::resource::{DataSource, LoadResource, Resource, ResourceHash, ResourceId, ResourceRegistry};
|
||||||
|
use graphene_std::text::Font;
|
||||||
|
|
||||||
#[derive(ExtractField)]
|
#[derive(ExtractField)]
|
||||||
pub struct ResourceMessageContext {}
|
pub struct ResourceMessageContext<'a> {
|
||||||
|
pub document_id: DocumentId,
|
||||||
|
pub fonts: &'a FontsMessageHandler,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Default, serde::Serialize, ExtractField)]
|
#[derive(Debug, Clone, PartialEq, Default, serde::Serialize, ExtractField)]
|
||||||
pub struct ResourceMessageHandler {
|
pub struct ResourceMessageHandler {
|
||||||
pub registry: ResourceRegistry,
|
pub registry: ResourceRegistry,
|
||||||
pub embedded: EmbeddedResources,
|
pub embedded: EmbeddedResources,
|
||||||
|
#[serde(skip)]
|
||||||
|
pending_resolves: HashMap<ResourceId, Option<ResolveProgress>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
struct ResolveProgress {
|
||||||
|
index: usize,
|
||||||
|
source: DataSource,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[message_handler_data]
|
#[message_handler_data]
|
||||||
impl MessageHandler<ResourceMessage, ResourceMessageContext> for ResourceMessageHandler {
|
impl MessageHandler<ResourceMessage, ResourceMessageContext<'_>> for ResourceMessageHandler {
|
||||||
fn process_message(&mut self, message: ResourceMessage, responses: &mut VecDeque<Message>, _context: ResourceMessageContext) {
|
fn process_message(&mut self, message: ResourceMessage, responses: &mut VecDeque<Message>, context: ResourceMessageContext) {
|
||||||
|
let ResourceMessageContext { document_id, fonts } = context;
|
||||||
|
|
||||||
match message {
|
match message {
|
||||||
ResourceMessage::StoreEmbedded { resource_id, data } => {
|
ResourceMessage::StoreEmbedded { resource_id, data } => {
|
||||||
let hash = ResourceHash::from(data.as_ref());
|
let hash = ResourceHash::from(data.as_ref());
|
||||||
self.registry.push_source_back(&resource_id, DataSource::Embedded);
|
self.registry.push_source_back(&resource_id, DataSource::Embedded);
|
||||||
self.registry.resolve(&resource_id, hash);
|
self.registry.resolve(&resource_id, hash);
|
||||||
responses.add(ResourceStorageMessage::Store { data });
|
responses.add(ResourceStorageMessage::Store { data });
|
||||||
|
responses.add(ResourceMessage::Resolve);
|
||||||
|
}
|
||||||
|
ResourceMessage::AddFont { resource_id, font } => {
|
||||||
|
let style = fonts.font_catalog.find_font_style_in_catalog(&font);
|
||||||
|
let style_name = style.map(|style| style.to_named_style()).unwrap_or_else(|| font.font_style.clone());
|
||||||
|
self.registry.push_source_back(&resource_id, DataSource::Embedded);
|
||||||
|
self.registry.push_source_back(
|
||||||
|
&resource_id,
|
||||||
|
DataSource::Font {
|
||||||
|
family: font.font_family,
|
||||||
|
style: Some(style_name),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
responses.add(ResourceMessage::Resolve);
|
||||||
|
}
|
||||||
|
ResourceMessage::Resolve => {
|
||||||
|
let unresolved_ids: Vec<ResourceId> = self.registry.unresolved().map(|info| info.id).collect();
|
||||||
|
for id in unresolved_ids {
|
||||||
|
if self.pending_resolves.contains_key(&id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
self.pending_resolves.insert(id, None);
|
||||||
|
responses.add(ResourceMessage::ResolveStep { resource_id: id });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ResourceMessage::ResolveStep { resource_id } => {
|
||||||
|
let Some(progress) = self.pending_resolves.get_mut(&resource_id) else { return };
|
||||||
|
|
||||||
|
let Some(info) = self.registry.info(&resource_id) else {
|
||||||
|
log::error!("ResolveStep for {resource_id}: no registry entry");
|
||||||
|
self.pending_resolves.remove(&resource_id);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let index = if let Some(progress) = progress { progress.index + 1 } else { 0 };
|
||||||
|
let Some(source) = info.sources.get(index).cloned() else {
|
||||||
|
log::error!("ResolveStep for {resource_id}: no more sources to try");
|
||||||
|
self.pending_resolves.remove(&resource_id);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
*progress = Some(ResolveProgress { index, source: source.clone() });
|
||||||
|
|
||||||
|
match source {
|
||||||
|
DataSource::Embedded => {
|
||||||
|
// Embedded resources are loaded on document load.
|
||||||
|
// If we get to this point, it means the resource was not embedded and we should try the next source.
|
||||||
|
responses.add(ResourceMessage::ResolveStep { resource_id });
|
||||||
|
}
|
||||||
|
DataSource::Url(url) => {
|
||||||
|
responses.add(FrontendMessage::TriggerResolveResource {
|
||||||
|
document_id,
|
||||||
|
resource_id,
|
||||||
|
url: url.to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
DataSource::Font { family, style } => {
|
||||||
|
let font = match style {
|
||||||
|
Some(style) => Font::new(family, style),
|
||||||
|
None => Font::new_with_default_style(family),
|
||||||
|
};
|
||||||
|
if let Some(hash) = fonts.cached_hash(&font) {
|
||||||
|
self.registry.resolve(&resource_id, hash);
|
||||||
|
self.pending_resolves.remove(&resource_id);
|
||||||
|
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if let Some(url) = fonts.cached_url(&font) {
|
||||||
|
responses.add(FrontendMessage::TriggerResolveResource { document_id, resource_id, url });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
responses.add(FrontendMessage::TriggerFontCatalogLoad);
|
||||||
|
self.pending_resolves.remove(&resource_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ResourceMessage::Resolved { resource_id, data } => {
|
||||||
|
let hash = ResourceHash::from(data.as_ref());
|
||||||
|
let Some(progress) = self.pending_resolves.remove(&resource_id).and_then(|p| p) else {
|
||||||
|
log::error!("Resolved message for {resource_id} with no pending resolve");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(info) = self.registry.info(&resource_id) else {
|
||||||
|
// ResourceId was removed from registry after resolve started.
|
||||||
|
// This can happen if the document was modified while resolves were in-flight.
|
||||||
|
// Likely safe to ignore for now.
|
||||||
|
// TODO: Consider adding cleaner cancelation for in-flight resolves.
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(source) = info.sources.get(progress.index).cloned() else {
|
||||||
|
log::error!("Resolved message for {resource_id} with no current source");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if progress.source != source {
|
||||||
|
log::error!("Resolved message for {resource_id} with mismatched source");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.registry.resolve(&resource_id, hash);
|
||||||
|
responses.add(ResourceStorageMessage::Store { data });
|
||||||
|
|
||||||
|
if let DataSource::Font { family, style } = source {
|
||||||
|
let font = match style {
|
||||||
|
Some(style) => Font::new(family, style),
|
||||||
|
None => Font::new_with_default_style(family),
|
||||||
|
};
|
||||||
|
responses.add(FontsMessage::ResourceResolved { font, hash });
|
||||||
|
}
|
||||||
|
|
||||||
|
responses.add(ResourceMessage::Resolve);
|
||||||
|
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -54,7 +178,7 @@ impl ResourceMessageHandler {
|
|||||||
self.embedded = EmbeddedResources::from_iter(futures::future::join_all(embedded).await.into_iter().flatten());
|
self.embedded = EmbeddedResources::from_iter(futures::future::join_all(embedded).await.into_iter().flatten());
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn garbage_collect(&mut self, used: &[ResourceId]) {
|
pub fn collect_garbage(&mut self, used: &[ResourceId]) {
|
||||||
let used = HashSet::<ResourceId>::from_iter(used.iter().cloned());
|
let used = HashSet::<ResourceId>::from_iter(used.iter().cloned());
|
||||||
let unused = self.registry.ids().filter(|id| !used.contains(id)).collect::<Vec<_>>();
|
let unused = self.registry.ids().filter(|id| !used.contains(id)).collect::<Vec<_>>();
|
||||||
unused.into_iter().for_each(|id| {
|
unused.into_iter().for_each(|id| {
|
||||||
|
|||||||
@@ -1685,6 +1685,28 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId],
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Convert text nodes from the old `editor-api` scope + `Font` input to a single font `Resource` input.
|
||||||
|
// The chosen typeface is recorded as a `DataSource::Font` in the document's resource registry and loaded on open.
|
||||||
|
if reference == DefinitionIdentifier::ProtoNode(graphene_std::text::text::IDENTIFIER) && inputs_count == 13 && matches!(node.inputs.first(), Some(NodeInput::Scope(_))) {
|
||||||
|
document
|
||||||
|
.network_interface
|
||||||
|
.set_input(&InputConnector::node(*node_id, 0), NodeInput::value(TaggedValue::None, false), network_path);
|
||||||
|
|
||||||
|
if let Some(TaggedValue::Font(font)) = node.inputs.get(2).and_then(|input| input.as_value()) {
|
||||||
|
let resource_id = ResourceId::new();
|
||||||
|
document.resources.registry.push_source_back(
|
||||||
|
&resource_id,
|
||||||
|
DataSource::Font {
|
||||||
|
family: font.font_family.clone(),
|
||||||
|
style: Some(font.font_style.clone()),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
document
|
||||||
|
.network_interface
|
||||||
|
.set_input(&InputConnector::node(*node_id, 2), NodeInput::value(TaggedValue::Resource(resource_id), false), network_path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if reference == DefinitionIdentifier::ProtoNode(graphene_std::raster_nodes::std_nodes::noise_pattern::IDENTIFIER) && inputs_count == 15 {
|
if reference == DefinitionIdentifier::ProtoNode(graphene_std::raster_nodes::std_nodes::noise_pattern::IDENTIFIER) && inputs_count == 15 {
|
||||||
let mut node_template = resolve_document_node_type(&reference)?.default_node_template();
|
let mut node_template = resolve_document_node_type(&reference)?.default_node_template();
|
||||||
document.network_interface.replace_implementation(node_id, network_path, &mut node_template);
|
document.network_interface.replace_implementation(node_id, network_path, &mut node_template);
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
use graph_craft::application_io::resource::Resource;
|
||||||
|
use std::sync::LazyLock;
|
||||||
|
|
||||||
|
const FALLBACK_FONT_BYTES: &[u8] = include_bytes!("source-sans-pro-regular.ttf");
|
||||||
|
pub static FALLBACK_FONT_RESOURCE: LazyLock<Resource> = LazyLock::new(|| Resource::new(FALLBACK_FONT_BYTES));
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
use crate::messages::portfolio::fonts::utility_types::FontCatalog;
|
||||||
|
use crate::messages::prelude::*;
|
||||||
|
use graph_craft::application_io::resource::{Resource, ResourceHash};
|
||||||
|
use graphene_std::text::Font;
|
||||||
|
|
||||||
|
#[impl_message(Message, PortfolioMessage, Fonts)]
|
||||||
|
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub enum FontsMessage {
|
||||||
|
CatalogLoaded {
|
||||||
|
catalog: FontCatalog,
|
||||||
|
},
|
||||||
|
ResourceResolved {
|
||||||
|
font: Font,
|
||||||
|
hash: ResourceHash,
|
||||||
|
},
|
||||||
|
Load {
|
||||||
|
font: Font,
|
||||||
|
response: Box<Message>,
|
||||||
|
},
|
||||||
|
Cached {
|
||||||
|
hash: ResourceHash,
|
||||||
|
#[serde(skip, default = "Resource::empty")]
|
||||||
|
resource: Resource,
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
use crate::messages::portfolio::fonts::FALLBACK_FONT_RESOURCE;
|
||||||
|
use crate::messages::portfolio::fonts::utility_types::FontCatalog;
|
||||||
|
use crate::messages::prelude::*;
|
||||||
|
use graph_craft::application_io::resource::{DataSource, Resource, ResourceHash, ResourceId};
|
||||||
|
use graphene_std::text::Font;
|
||||||
|
|
||||||
|
#[derive(ExtractField)]
|
||||||
|
pub struct FontsMessageContext<'a> {
|
||||||
|
pub resource_storage: &'a ResourceStorageMessageHandler,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, ExtractField)]
|
||||||
|
pub struct FontsMessageHandler {
|
||||||
|
pub font_catalog: FontCatalog,
|
||||||
|
font_hashes: HashMap<Font, ResourceHash>,
|
||||||
|
font_data: HashMap<ResourceHash, Resource>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[message_handler_data]
|
||||||
|
impl MessageHandler<FontsMessage, FontsMessageContext<'_>> for FontsMessageHandler {
|
||||||
|
fn process_message(&mut self, message: FontsMessage, responses: &mut VecDeque<Message>, context: FontsMessageContext) {
|
||||||
|
let FontsMessageContext { resource_storage } = context;
|
||||||
|
|
||||||
|
match message {
|
||||||
|
FontsMessage::CatalogLoaded { catalog } => {
|
||||||
|
self.font_catalog = catalog;
|
||||||
|
responses.add(PortfolioMessage::ResolveResources);
|
||||||
|
}
|
||||||
|
FontsMessage::ResourceResolved { font, hash } => {
|
||||||
|
let font = self.normalize(font);
|
||||||
|
self.font_hashes.insert(font, hash);
|
||||||
|
}
|
||||||
|
FontsMessage::Load { font, response } => {
|
||||||
|
let font = self.normalize(font);
|
||||||
|
let Some(hash) = self.font_hashes.get(&font).copied() else {
|
||||||
|
log::warn!("FontsMessage::Load for {font:?} with no known hash; ignoring");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if self.font_data.contains_key(&hash) {
|
||||||
|
responses.add(*response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let loader = resource_storage.resources();
|
||||||
|
responses.add(async move {
|
||||||
|
let resource = loader.load(hash).await;
|
||||||
|
match resource {
|
||||||
|
Some(resource) => Message::Batched {
|
||||||
|
messages: Box::new([FontsMessage::Cached { hash, resource }.into(), *response]),
|
||||||
|
},
|
||||||
|
None => {
|
||||||
|
log::warn!("Storage missing data for font hash {hash}");
|
||||||
|
*response
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
FontsMessage::Cached { hash, resource } => {
|
||||||
|
self.font_data.insert(hash, resource);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
advertise_actions!(FontsMessageDiscriminant;);
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FontsMessageHandler {
|
||||||
|
pub fn cached_hash(&self, font: &Font) -> Option<ResourceHash> {
|
||||||
|
let font = self.normalize(font.clone());
|
||||||
|
self.font_hashes.get(&font).copied()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cached_url(&self, font: &Font) -> Option<String> {
|
||||||
|
let font = self.normalize(font.clone());
|
||||||
|
self.font_catalog.download_url(&font)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_resource_or_queue_load(&self, font: &Font, responses: &mut VecDeque<Message>) -> Resource {
|
||||||
|
let font = self.normalize(font.clone());
|
||||||
|
if let Some(hash) = self.font_hashes.get(&font) {
|
||||||
|
if let Some(resource) = self.font_data.get(hash) {
|
||||||
|
return resource.clone();
|
||||||
|
}
|
||||||
|
responses.add(FontsMessage::Load {
|
||||||
|
font: font.clone(),
|
||||||
|
response: Message::NoOp.into(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
FALLBACK_FONT_RESOURCE.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn id_font(&self, resources: &ResourceMessageHandler, resource_id: ResourceId) -> Option<Font> {
|
||||||
|
let info = resources.registry.info(&resource_id)?;
|
||||||
|
info.sources.iter().find_map(|source| match source {
|
||||||
|
DataSource::Font { family, style } => Some(match style {
|
||||||
|
Some(style) => Font::new(family.clone(), style.clone()),
|
||||||
|
None => Font::new_with_default_style(family.clone()),
|
||||||
|
}),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn used_resources(&self) -> impl Iterator<Item = ResourceHash> + '_ {
|
||||||
|
self.font_hashes.values().copied().chain(self.font_data.keys().copied())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn normalize(&self, font: Font) -> Font {
|
||||||
|
match self.font_catalog.find_font_style_in_catalog(&font) {
|
||||||
|
Some(style) => Font::new(font.font_family, style.to_named_style()),
|
||||||
|
None => font,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
mod fallback;
|
||||||
|
mod fonts_message;
|
||||||
|
mod fonts_message_handler;
|
||||||
|
|
||||||
|
pub mod utility_types;
|
||||||
|
|
||||||
|
#[doc(inline)]
|
||||||
|
pub use fallback::FALLBACK_FONT_RESOURCE;
|
||||||
|
#[doc(inline)]
|
||||||
|
pub use fonts_message::{FontsMessage, FontsMessageDiscriminant};
|
||||||
|
#[doc(inline)]
|
||||||
|
pub use fonts_message_handler::{FontsMessageContext, FontsMessageHandler};
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
use graphene_std::text::Font;
|
||||||
|
|
||||||
|
// TODO: Should this be a BTreeMap instead?
|
||||||
|
#[derive(Clone, Debug, Default, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub struct FontCatalog(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
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn download_url(&self, font: &Font) -> Option<String> {
|
||||||
|
let catalog_family = self.0.iter().find(|catalog_family| catalog_family.name == font.font_family)?;
|
||||||
|
let FontCatalogStyle { weight, italic, .. } = FontCatalogStyle::from_named_style(&font.font_style, "");
|
||||||
|
Some(catalog_family.closest_style(weight, italic).url.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn iter(&self) -> impl Iterator<Item = &FontCatalogFamily> {
|
||||||
|
self.0.iter()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.0.is_empty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Vec<FontCatalogFamily>> for FontCatalog {
|
||||||
|
fn from(value: Vec<FontCatalogFamily>) -> Self {
|
||||||
|
Self(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(feature = "wasm", derive(tsify::Tsify), tsify(from_wasm_abi))]
|
||||||
|
#[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 {
|
||||||
|
static FALLBACK_STYLE: FontCatalogStyle = FontCatalogStyle {
|
||||||
|
weight: 400,
|
||||||
|
italic: false,
|
||||||
|
url: String::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
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.first().unwrap_or(&FALLBACK_STYLE))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
||||||
|
#[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}")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,9 +3,12 @@ mod portfolio_message_handler;
|
|||||||
|
|
||||||
pub mod document;
|
pub mod document;
|
||||||
pub mod document_migration;
|
pub mod document_migration;
|
||||||
|
pub mod fonts;
|
||||||
pub mod persistent_state;
|
pub mod persistent_state;
|
||||||
pub mod utility_types;
|
pub mod utility_types;
|
||||||
|
|
||||||
|
#[doc(inline)]
|
||||||
|
pub use fonts::{FontsMessage, FontsMessageContext, FontsMessageHandler};
|
||||||
#[doc(inline)]
|
#[doc(inline)]
|
||||||
pub use persistent_state::{PersistentStateMessage, PersistentStateMessageContext, PersistentStateMessageHandler};
|
pub use persistent_state::{PersistentStateMessage, PersistentStateMessageContext, PersistentStateMessageHandler};
|
||||||
#[doc(inline)]
|
#[doc(inline)]
|
||||||
|
|||||||
@@ -52,3 +52,9 @@ impl MessageHandler<PersistentStateMessage, PersistentStateMessageContext> for P
|
|||||||
|
|
||||||
advertise_actions!(PersistentStateMessageDiscriminant;);
|
advertise_actions!(PersistentStateMessageDiscriminant;);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl PersistentStateMessageHandler {
|
||||||
|
pub fn loaded(&self) -> bool {
|
||||||
|
self.loaded
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,11 +3,9 @@ use super::persistent_state::PersistentStateMessage;
|
|||||||
use super::utility_types::{DockingSplitDirection, PanelGroupId, PanelType};
|
use super::utility_types::{DockingSplitDirection, PanelGroupId, PanelType};
|
||||||
use crate::messages::frontend::utility_types::{ExportBounds, FileType, PersistedState};
|
use crate::messages::frontend::utility_types::{ExportBounds, FileType, PersistedState};
|
||||||
use crate::messages::portfolio::document::utility_types::clipboards::Clipboard;
|
use crate::messages::portfolio::document::utility_types::clipboards::Clipboard;
|
||||||
use crate::messages::portfolio::utility_types::FontCatalog;
|
|
||||||
use crate::messages::prelude::*;
|
use crate::messages::prelude::*;
|
||||||
use graphene_std::Color;
|
use graphene_std::Color;
|
||||||
use graphene_std::raster::Image;
|
use graphene_std::raster::Image;
|
||||||
use graphene_std::text::Font;
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
#[impl_message(Message, Portfolio)]
|
#[impl_message(Message, Portfolio)]
|
||||||
@@ -17,6 +15,8 @@ pub enum PortfolioMessage {
|
|||||||
#[child]
|
#[child]
|
||||||
Document(DocumentMessage),
|
Document(DocumentMessage),
|
||||||
#[child]
|
#[child]
|
||||||
|
Fonts(FontsMessage),
|
||||||
|
#[child]
|
||||||
PersistentState(PersistentStateMessage),
|
PersistentState(PersistentStateMessage),
|
||||||
|
|
||||||
// Messages
|
// Messages
|
||||||
@@ -51,20 +51,10 @@ pub enum PortfolioMessage {
|
|||||||
DestroyAllDocuments,
|
DestroyAllDocuments,
|
||||||
EditorPreferences,
|
EditorPreferences,
|
||||||
GarbageCollectResources,
|
GarbageCollectResources,
|
||||||
FontCatalogLoaded {
|
ResolveDocumentResources {
|
||||||
catalog: FontCatalog,
|
|
||||||
},
|
|
||||||
LoadFontData {
|
|
||||||
font: Font,
|
|
||||||
},
|
|
||||||
FontLoaded {
|
|
||||||
font_family: String,
|
|
||||||
font_style: String,
|
|
||||||
data: Vec<u8>,
|
|
||||||
},
|
|
||||||
LoadDocumentResources {
|
|
||||||
document_id: DocumentId,
|
document_id: DocumentId,
|
||||||
},
|
},
|
||||||
|
ResolveResources,
|
||||||
LoadPersistedState {
|
LoadPersistedState {
|
||||||
state: PersistedState,
|
state: PersistedState,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use super::document::utility_types::document_metadata::LayerNodeIdentifier;
|
use super::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||||
use super::document::utility_types::network_interface;
|
use super::document::utility_types::network_interface;
|
||||||
use super::persistent_state::{PersistentStateMessage, PersistentStateMessageContext, PersistentStateMessageHandler};
|
use super::persistent_state::{PersistentStateMessage, PersistentStateMessageContext, PersistentStateMessageHandler};
|
||||||
use super::utility_types::{CachedData, PanelLayoutSubdivision, PanelType, WorkspacePanelLayout};
|
use super::utility_types::{PanelLayoutSubdivision, PanelType, WorkspacePanelLayout};
|
||||||
use crate::application::{Editor, generate_uuid};
|
use crate::application::{Editor, generate_uuid};
|
||||||
use crate::consts::{DEFAULT_DOCUMENT_NAME, DEFAULT_STROKE_WIDTH, FILE_EXTENSION};
|
use crate::consts::{DEFAULT_DOCUMENT_NAME, DEFAULT_STROKE_WIDTH, FILE_EXTENSION};
|
||||||
use crate::messages::animation::TimingInformation;
|
use crate::messages::animation::TimingInformation;
|
||||||
@@ -32,7 +32,6 @@ use graphene_std::Color;
|
|||||||
use graphene_std::raster_types::Image;
|
use graphene_std::raster_types::Image;
|
||||||
use graphene_std::renderer::Quad;
|
use graphene_std::renderer::Quad;
|
||||||
use graphene_std::subpath::BezierHandles;
|
use graphene_std::subpath::BezierHandles;
|
||||||
use graphene_std::text::Font;
|
|
||||||
use graphene_std::vector::misc::HandleId;
|
use graphene_std::vector::misc::HandleId;
|
||||||
use graphene_std::vector::{PointId, SegmentId, Vector, VectorModificationType};
|
use graphene_std::vector::{PointId, SegmentId, Vector, VectorModificationType};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
@@ -68,7 +67,7 @@ pub struct PortfolioMessageHandler {
|
|||||||
document_ids: VecDeque<DocumentId>,
|
document_ids: VecDeque<DocumentId>,
|
||||||
pub(crate) active_document_id: Option<DocumentId>,
|
pub(crate) active_document_id: Option<DocumentId>,
|
||||||
persistent_state: PersistentStateMessageHandler,
|
persistent_state: PersistentStateMessageHandler,
|
||||||
pub cached_data: CachedData,
|
pub fonts: FontsMessageHandler,
|
||||||
copy_buffer: [Vec<CopyBufferEntry>; INTERNAL_CLIPBOARD_COUNT as usize],
|
copy_buffer: [Vec<CopyBufferEntry>; INTERNAL_CLIPBOARD_COUNT as usize],
|
||||||
pub executor: NodeGraphExecutor,
|
pub executor: NodeGraphExecutor,
|
||||||
pub selection_mode: SelectionMode,
|
pub selection_mode: SelectionMode,
|
||||||
@@ -99,7 +98,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
|||||||
let document_inputs = DocumentMessageContext {
|
let document_inputs = DocumentMessageContext {
|
||||||
document_id,
|
document_id,
|
||||||
ipp,
|
ipp,
|
||||||
cached_data: &self.cached_data,
|
fonts: &self.fonts,
|
||||||
executor: &mut self.executor,
|
executor: &mut self.executor,
|
||||||
current_tool,
|
current_tool,
|
||||||
preferences,
|
preferences,
|
||||||
@@ -118,6 +117,10 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
|||||||
};
|
};
|
||||||
self.persistent_state.process_message(message, responses, context);
|
self.persistent_state.process_message(message, responses, context);
|
||||||
}
|
}
|
||||||
|
PortfolioMessage::Fonts(message) => {
|
||||||
|
let context = FontsMessageContext { resource_storage };
|
||||||
|
self.fonts.process_message(message, responses, context);
|
||||||
|
}
|
||||||
|
|
||||||
// Messages
|
// Messages
|
||||||
PortfolioMessage::Init => {
|
PortfolioMessage::Init => {
|
||||||
@@ -169,7 +172,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
|||||||
let document_inputs = DocumentMessageContext {
|
let document_inputs = DocumentMessageContext {
|
||||||
document_id,
|
document_id,
|
||||||
ipp,
|
ipp,
|
||||||
cached_data: &self.cached_data,
|
fonts: &self.fonts,
|
||||||
executor: &mut self.executor,
|
executor: &mut self.executor,
|
||||||
current_tool,
|
current_tool,
|
||||||
preferences,
|
preferences,
|
||||||
@@ -208,6 +211,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
|||||||
document_id,
|
document_id,
|
||||||
document: document.serialize_document(),
|
document: document.serialize_document(),
|
||||||
});
|
});
|
||||||
|
responses.add(PersistentStateMessage::WriteState);
|
||||||
}
|
}
|
||||||
PortfolioMessage::CloseActiveDocumentWithConfirmation => {
|
PortfolioMessage::CloseActiveDocumentWithConfirmation => {
|
||||||
if let Some(document_id) = self.active_document_id {
|
if let Some(document_id) = self.active_document_id {
|
||||||
@@ -386,78 +390,16 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
|||||||
responses.add(MenuBarMessage::SendLayout);
|
responses.add(MenuBarMessage::SendLayout);
|
||||||
responses.add(PersistentStateMessage::WriteState);
|
responses.add(PersistentStateMessage::WriteState);
|
||||||
}
|
}
|
||||||
PortfolioMessage::FontCatalogLoaded { catalog } => {
|
|
||||||
self.cached_data.font_catalog = catalog;
|
|
||||||
|
|
||||||
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.cached_data.font_catalog.find_font_style_in_catalog(&font) {
|
|
||||||
let font = Font::new(font.font_family, style.to_named_style());
|
|
||||||
|
|
||||||
if !self.cached_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.cached_data.font_cache.insert(font, data);
|
|
||||||
self.executor.update_font_cache(self.cached_data.font_cache.clone());
|
|
||||||
|
|
||||||
for document_id in self.document_ids.iter() {
|
|
||||||
let node_to_inspect = self.node_to_inspect();
|
|
||||||
|
|
||||||
let Some(document) = self.documents.get_mut(document_id) else {
|
|
||||||
if self.unloaded_documents.contains_key(document_id) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
log::error!("Tried to render non-existent document");
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
let document_to_viewport = document
|
|
||||||
.navigation_handler
|
|
||||||
.calculate_offset_transform(viewport.center_in_viewport_space().into(), &document.document_ptz);
|
|
||||||
let pointer_position = document_to_viewport.inverse().transform_point2(ipp.mouse.position);
|
|
||||||
|
|
||||||
let scale = viewport.scale();
|
|
||||||
// Use exact physical dimensions from browser (via ResizeObserver's devicePixelContentBoxSize)
|
|
||||||
let physical_resolution = viewport.size().to_physical().into_dvec2().round().as_uvec2();
|
|
||||||
|
|
||||||
// TODO: Remove this when we do the SVG rendering with a separate library on desktop, thus avoiding a need for the hole punch.
|
|
||||||
// TODO: See #3796. There is a second instance of this todo comment and code block (be sure to remove both).
|
|
||||||
#[cfg(not(target_family = "wasm"))]
|
|
||||||
responses.add_front(FrontendMessage::UpdateViewportHolePunch {
|
|
||||||
active: document.render_mode != graphene_std::vector::style::RenderMode::SvgPreview,
|
|
||||||
});
|
|
||||||
|
|
||||||
if let Ok(message) = self
|
|
||||||
.executor
|
|
||||||
.submit_node_graph_evaluation(document, *document_id, physical_resolution, scale, timing_information, node_to_inspect, true, pointer_position)
|
|
||||||
{
|
|
||||||
responses.add_front(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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::EditorPreferences => self.executor.update_editor_preferences(preferences.editor_preferences()),
|
||||||
PortfolioMessage::GarbageCollectResources => {
|
PortfolioMessage::GarbageCollectResources => {
|
||||||
|
if !self.persistent_state.loaded() {
|
||||||
|
// We don't know what can be safely garbage collected
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let mut used_resources = HashSet::new();
|
let mut used_resources = HashSet::new();
|
||||||
for (id, info) in self.unloaded_documents.iter() {
|
for (id, info) in self.unloaded_documents.iter() {
|
||||||
|
log::info!("Checking resources for unloaded document {:?}: {:?}", info.name, info.resources);
|
||||||
if let Some(resources) = &info.resources {
|
if let Some(resources) = &info.resources {
|
||||||
used_resources.extend(resources.iter());
|
used_resources.extend(resources.iter());
|
||||||
} else {
|
} else {
|
||||||
@@ -469,21 +411,26 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
|||||||
document.garbage_collect_resources();
|
document.garbage_collect_resources();
|
||||||
used_resources.extend(document.resources.registry.resolved().filter_map(|info| info.hash.cloned()));
|
used_resources.extend(document.resources.registry.resolved().filter_map(|info| info.hash.cloned()));
|
||||||
}
|
}
|
||||||
|
used_resources.extend(self.fonts.used_resources());
|
||||||
responses.add(ResourceStorageMessage::GarbageCollect {
|
responses.add(ResourceStorageMessage::GarbageCollect {
|
||||||
used: Vec::from_iter(used_resources).into_boxed_slice(),
|
used: Vec::from_iter(used_resources).into_boxed_slice(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
PortfolioMessage::LoadDocumentResources { document_id } => {
|
PortfolioMessage::ResolveResources => {
|
||||||
let catalog = &self.cached_data.font_catalog;
|
for document_id in self.document_ids.iter().copied().collect::<Vec<_>>() {
|
||||||
|
responses.add(PortfolioMessage::ResolveDocumentResources { document_id });
|
||||||
if catalog.0.is_empty() {
|
}
|
||||||
|
}
|
||||||
|
PortfolioMessage::ResolveDocumentResources { document_id } => {
|
||||||
|
if self.fonts.font_catalog.is_empty() {
|
||||||
responses.add_front(FrontendMessage::TriggerFontCatalogLoad);
|
responses.add_front(FrontendMessage::TriggerFontCatalogLoad);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(document) = self.documents.get_mut(&document_id) {
|
responses.add(PortfolioMessage::DocumentPassMessage {
|
||||||
document.load_layer_resources(responses);
|
document_id,
|
||||||
}
|
message: DocumentMessage::Resource(ResourceMessage::Resolve),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
PortfolioMessage::LoadPersistedState { state } => {
|
PortfolioMessage::LoadPersistedState { state } => {
|
||||||
if let Some(layout) = state.workspace_layout {
|
if let Some(layout) = state.workspace_layout {
|
||||||
@@ -884,6 +831,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
|||||||
let mut document = match document {
|
let mut document = match document {
|
||||||
Ok(document) => document,
|
Ok(document) => document,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
log::error!("{e}");
|
||||||
// TODO: Eventually remove this document upgrade code
|
// TODO: Eventually remove this document upgrade code
|
||||||
// TODO: (Only the `if` branch, the `else` branch's manual-open dialog stays)
|
// TODO: (Only the `if` branch, the `else` branch's manual-open dialog stays)
|
||||||
if document_is_auto_saved {
|
if document_is_auto_saved {
|
||||||
@@ -908,7 +856,6 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
|||||||
responses.add(PortfolioMessage::UpdateOpenDocumentsList);
|
responses.add(PortfolioMessage::UpdateOpenDocumentsList);
|
||||||
self.tick_autosave_load_progress(responses, true);
|
self.tick_autosave_load_progress(responses, true);
|
||||||
} else {
|
} else {
|
||||||
log::error!("{e}");
|
|
||||||
let name = document_name
|
let name = document_name
|
||||||
.filter(|n| !n.trim().is_empty())
|
.filter(|n| !n.trim().is_empty())
|
||||||
.or_else(|| document_path.as_ref().and_then(|p| p.file_stem()).map(|s| s.to_string_lossy().into_owned()))
|
.or_else(|| document_path.as_ref().and_then(|p| p.file_stem()).map(|s| s.to_string_lossy().into_owned()))
|
||||||
@@ -1134,7 +1081,6 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
|||||||
added_nodes = true;
|
added_nodes = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
document.load_layer_resources(responses);
|
|
||||||
let new_ids: HashMap<_, _> = entry.nodes.iter().map(|(id, _)| (*id, NodeId::new())).collect();
|
let new_ids: HashMap<_, _> = entry.nodes.iter().map(|(id, _)| (*id, NodeId::new())).collect();
|
||||||
let layer = LayerNodeIdentifier::new_unchecked(new_ids[&NodeId(0)]);
|
let layer = LayerNodeIdentifier::new_unchecked(new_ids[&NodeId(0)]);
|
||||||
all_new_ids.extend(new_ids.values().cloned());
|
all_new_ids.extend(new_ids.values().cloned());
|
||||||
@@ -1619,7 +1565,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
|||||||
};
|
};
|
||||||
if !document.is_loaded {
|
if !document.is_loaded {
|
||||||
document.is_loaded = true;
|
document.is_loaded = true;
|
||||||
responses.add(PortfolioMessage::LoadDocumentResources { document_id });
|
responses.add(PortfolioMessage::ResolveDocumentResources { document_id });
|
||||||
responses.add(PortfolioMessage::UpdateDocumentWidgets);
|
responses.add(PortfolioMessage::UpdateDocumentWidgets);
|
||||||
responses.add(PropertiesPanelMessage::Clear);
|
responses.add(PropertiesPanelMessage::Clear);
|
||||||
}
|
}
|
||||||
@@ -1665,6 +1611,13 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Skip rendering while any resource is still unresolved — the preprocessor would otherwise fail with
|
||||||
|
// `ResourceNotFound`. `ResourceMessage::Resolved` queues `RunDocumentGraph` once each id resolves,
|
||||||
|
// so the render fires automatically once the registry is complete.
|
||||||
|
if document.resources.registry.unresolved().next().is_some() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let document_to_viewport = document
|
let document_to_viewport = document
|
||||||
.navigation_handler
|
.navigation_handler
|
||||||
.calculate_offset_transform(viewport.center_in_viewport_space().into(), &document.document_ptz);
|
.calculate_offset_transform(viewport.center_in_viewport_space().into(), &document.document_ptz);
|
||||||
|
|||||||
@@ -1,95 +1,11 @@
|
|||||||
use graphene_std::Color;
|
use graphene_std::Color;
|
||||||
use graphene_std::raster::Image;
|
use graphene_std::raster::Image;
|
||||||
use graphene_std::text::{Font, FontCache};
|
|
||||||
|
|
||||||
/// Proportional share (0-1) for the document panel's side when splitting adjacent to non-document panels.
|
/// Proportional share (0-1) for the document panel's side when splitting adjacent to non-document panels.
|
||||||
const DOCUMENT_PANEL_SHARE: f64 = 0.8;
|
const DOCUMENT_PANEL_SHARE: f64 = 0.8;
|
||||||
/// Proportional share for each side when neither (or both) contain the document panel.
|
/// Proportional share for each side when neither (or both) contain the document panel.
|
||||||
const EQUAL_PANEL_SHARE: f64 = 0.5;
|
const EQUAL_PANEL_SHARE: f64 = 0.5;
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
|
||||||
pub struct CachedData {
|
|
||||||
pub font_cache: FontCache,
|
|
||||||
pub font_catalog: FontCatalog,
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify), tsify(from_wasm_abi))]
|
|
||||||
#[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])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
|
||||||
#[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}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify), tsify(from_wasm_abi))]
|
#[cfg_attr(feature = "wasm", derive(tsify::Tsify), tsify(from_wasm_abi))]
|
||||||
#[derive(PartialEq, Eq, Hash, Clone, Copy, Debug, serde::Serialize, serde::Deserialize)]
|
#[derive(PartialEq, Eq, Hash, Clone, Copy, Debug, serde::Serialize, serde::Deserialize)]
|
||||||
pub enum PanelType {
|
pub enum PanelType {
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ pub use crate::messages::portfolio::document::overlays::{OverlaysMessage, Overla
|
|||||||
pub use crate::messages::portfolio::document::properties_panel::{PropertiesPanelMessage, PropertiesPanelMessageDiscriminant, PropertiesPanelMessageHandler};
|
pub use crate::messages::portfolio::document::properties_panel::{PropertiesPanelMessage, PropertiesPanelMessageDiscriminant, PropertiesPanelMessageHandler};
|
||||||
pub use crate::messages::portfolio::document::resource::{ResourceMessage, ResourceMessageContext, ResourceMessageDiscriminant, ResourceMessageHandler};
|
pub use crate::messages::portfolio::document::resource::{ResourceMessage, ResourceMessageContext, ResourceMessageDiscriminant, ResourceMessageHandler};
|
||||||
pub use crate::messages::portfolio::document::{DocumentMessage, DocumentMessageContext, DocumentMessageDiscriminant, DocumentMessageHandler};
|
pub use crate::messages::portfolio::document::{DocumentMessage, DocumentMessageContext, DocumentMessageDiscriminant, DocumentMessageHandler};
|
||||||
|
pub use crate::messages::portfolio::fonts::{FontsMessage, FontsMessageContext, FontsMessageDiscriminant, FontsMessageHandler};
|
||||||
pub use crate::messages::portfolio::persistent_state::{PersistentStateMessage, PersistentStateMessageContext, PersistentStateMessageDiscriminant, PersistentStateMessageHandler};
|
pub use crate::messages::portfolio::persistent_state::{PersistentStateMessage, PersistentStateMessageContext, PersistentStateMessageDiscriminant, PersistentStateMessageHandler};
|
||||||
pub use crate::messages::portfolio::{PortfolioMessage, PortfolioMessageContext, PortfolioMessageDiscriminant, PortfolioMessageHandler};
|
pub use crate::messages::portfolio::{PortfolioMessage, PortfolioMessageContext, PortfolioMessageDiscriminant, PortfolioMessageHandler};
|
||||||
pub use crate::messages::preferences::{PreferencesMessage, PreferencesMessageDiscriminant, PreferencesMessageHandler};
|
pub use crate::messages::preferences::{PreferencesMessage, PreferencesMessageDiscriminant, PreferencesMessageHandler};
|
||||||
|
|||||||
@@ -431,15 +431,21 @@ pub fn get_grid_id(layer: LayerNodeIdentifier, network_interface: &NodeNetworkIn
|
|||||||
NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name(&DefinitionIdentifier::ProtoNode(graphene_std::vector::generator_nodes::grid::IDENTIFIER))
|
NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name(&DefinitionIdentifier::ProtoNode(graphene_std::vector::generator_nodes::grid::IDENTIFIER))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Gets properties from the Text node
|
/// Gets properties from the Text node. Resolves the font selection by reading the resource id and lookup via the fonts message handler.
|
||||||
pub fn get_text(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<(&String, &Font, TypesettingConfig, bool)> {
|
pub fn get_text<'a>(
|
||||||
|
layer: LayerNodeIdentifier,
|
||||||
|
network_interface: &'a NodeNetworkInterface,
|
||||||
|
fonts: &FontsMessageHandler,
|
||||||
|
resources: &ResourceMessageHandler,
|
||||||
|
) -> Option<(&'a String, Font, TypesettingConfig, bool)> {
|
||||||
let inputs = NodeGraphLayer::new(layer, network_interface).find_node_inputs(&DefinitionIdentifier::ProtoNode(graphene_std::text::text::IDENTIFIER))?;
|
let inputs = NodeGraphLayer::new(layer, network_interface).find_node_inputs(&DefinitionIdentifier::ProtoNode(graphene_std::text::text::IDENTIFIER))?;
|
||||||
|
|
||||||
let Some(TaggedValue::String(text)) = &inputs[graphene_std::text::text::TextInput::INDEX].as_value() else {
|
let Some(TaggedValue::String(text)) = &inputs[graphene_std::text::text::TextInput::INDEX].as_value() else {
|
||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
let Some(TaggedValue::Font(font)) = &inputs[graphene_std::text::text::FontInput::INDEX].as_value() else {
|
let font = match &inputs[graphene_std::text::text::FontInput::INDEX].as_value() {
|
||||||
return None;
|
Some(TaggedValue::Resource(resource_id)) => fonts.id_font(resources, *resource_id).unwrap_or_default(),
|
||||||
|
_ => Font::default(),
|
||||||
};
|
};
|
||||||
let Some(&TaggedValue::F64(font_size)) = inputs[graphene_std::text::text::SizeInput::INDEX].as_value() else {
|
let Some(&TaggedValue::F64(font_size)) = inputs[graphene_std::text::text::SizeInput::INDEX].as_value() else {
|
||||||
return None;
|
return None;
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ use graph_craft::document::value::TaggedValue;
|
|||||||
use graphene_std::list::List;
|
use graphene_std::list::List;
|
||||||
use graphene_std::renderer::Quad;
|
use graphene_std::renderer::Quad;
|
||||||
use graphene_std::subpath::{Bezier, BezierHandles};
|
use graphene_std::subpath::{Bezier, BezierHandles};
|
||||||
use graphene_std::text::FontCache;
|
|
||||||
use graphene_std::vector::algorithms::bezpath_algorithms::pathseg_compute_lookup_table;
|
use graphene_std::vector::algorithms::bezpath_algorithms::pathseg_compute_lookup_table;
|
||||||
use graphene_std::vector::misc::{HandleId, ManipulatorPointId, dvec2_to_point};
|
use graphene_std::vector::misc::{HandleId, ManipulatorPointId, dvec2_to_point};
|
||||||
use graphene_std::vector::{HandleExt, PointId, SegmentId, Vector, VectorModification, VectorModificationType};
|
use graphene_std::vector::{HandleExt, PointId, SegmentId, Vector, VectorModification, VectorModificationType};
|
||||||
@@ -63,17 +62,18 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Calculates the bounding box of the layer's text, based on the settings for max width and height specified in the typesetting config.
|
/// Calculates the bounding box of the layer's text, based on the settings for max width and height specified in the typesetting config.
|
||||||
pub fn text_bounding_box(layer: LayerNodeIdentifier, document: &DocumentMessageHandler, font_cache: &FontCache) -> Quad {
|
pub fn text_bounding_box(layer: LayerNodeIdentifier, document: &DocumentMessageHandler, fonts: &FontsMessageHandler, responses: &mut VecDeque<Message>) -> Quad {
|
||||||
// Use the `editor:text_frame` attribute if available (handles multi-item glyphs and the 'Index Elements' node)
|
// Use the `editor:text_frame` attribute if available (handles multi-item glyphs and the 'Index Elements' node)
|
||||||
if let Some(&frame) = document.metadata().text_frames.get(&layer) {
|
if let Some(&frame) = document.metadata().text_frames.get(&layer) {
|
||||||
return frame * Quad::from_box([DVec2::ZERO, DVec2::ONE]);
|
return frame * Quad::from_box([DVec2::ZERO, DVec2::ONE]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback: recompute from text content (e.g. layer hasn't rendered yet)
|
// Fallback: recompute from text content (e.g. layer hasn't rendered yet)
|
||||||
let Some((text, font, typesetting, _)) = get_text(layer, &document.network_interface) else {
|
let Some((text, font, typesetting, _)) = get_text(layer, &document.network_interface, fonts, &document.resources) else {
|
||||||
return Quad::from_box([DVec2::ZERO, DVec2::ZERO]);
|
return Quad::from_box([DVec2::ZERO, DVec2::ZERO]);
|
||||||
};
|
};
|
||||||
let far = graphene_std::text::bounding_box(text, font, font_cache, typesetting, false);
|
let font = fonts.get_resource_or_queue_load(&font, responses);
|
||||||
|
let far = graphene_std::text::bounding_box(text, &font, typesetting, false);
|
||||||
Quad::from_box([DVec2::ZERO, far])
|
Quad::from_box([DVec2::ZERO, far])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ use super::utility_types::{ToolActionMessageContext, ToolFsmState, tool_message_
|
|||||||
use crate::application::generate_uuid;
|
use crate::application::generate_uuid;
|
||||||
use crate::messages::layout::utility_types::widget_prelude::*;
|
use crate::messages::layout::utility_types::widget_prelude::*;
|
||||||
use crate::messages::portfolio::document::overlays::utility_types::OverlayProvider;
|
use crate::messages::portfolio::document::overlays::utility_types::OverlayProvider;
|
||||||
use crate::messages::portfolio::utility_types::CachedData;
|
|
||||||
use crate::messages::prelude::*;
|
use crate::messages::prelude::*;
|
||||||
use crate::messages::tool::transform_layer::transform_layer_message_handler::TransformLayerMessageContext;
|
use crate::messages::tool::transform_layer::transform_layer_message_handler::TransformLayerMessageContext;
|
||||||
use crate::messages::tool::utility_types::{HintData, ToolType};
|
use crate::messages::tool::utility_types::{HintData, ToolType};
|
||||||
@@ -19,7 +18,7 @@ pub struct ToolMessageContext<'a> {
|
|||||||
pub document_id: DocumentId,
|
pub document_id: DocumentId,
|
||||||
pub document: &'a mut DocumentMessageHandler,
|
pub document: &'a mut DocumentMessageHandler,
|
||||||
pub input: &'a InputPreprocessorMessageHandler,
|
pub input: &'a InputPreprocessorMessageHandler,
|
||||||
pub cached_data: &'a CachedData,
|
pub fonts: &'a FontsMessageHandler,
|
||||||
pub node_graph: &'a NodeGraphExecutor,
|
pub node_graph: &'a NodeGraphExecutor,
|
||||||
pub preferences: &'a PreferencesMessageHandler,
|
pub preferences: &'a PreferencesMessageHandler,
|
||||||
pub viewport: &'a ViewportMessageHandler,
|
pub viewport: &'a ViewportMessageHandler,
|
||||||
@@ -40,7 +39,7 @@ impl MessageHandler<ToolMessage, ToolMessageContext<'_>> for ToolMessageHandler
|
|||||||
document_id,
|
document_id,
|
||||||
document,
|
document,
|
||||||
input,
|
input,
|
||||||
cached_data,
|
fonts,
|
||||||
node_graph,
|
node_graph,
|
||||||
preferences,
|
preferences,
|
||||||
viewport,
|
viewport,
|
||||||
@@ -127,7 +126,7 @@ impl MessageHandler<ToolMessage, ToolMessageContext<'_>> for ToolMessageHandler
|
|||||||
node_graph,
|
node_graph,
|
||||||
preferences,
|
preferences,
|
||||||
viewport,
|
viewport,
|
||||||
cached_data,
|
fonts,
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(tool_abort_message) = tool.event_to_message_map().tool_abort {
|
if let Some(tool_abort_message) = tool.event_to_message_map().tool_abort {
|
||||||
@@ -218,7 +217,7 @@ impl MessageHandler<ToolMessage, ToolMessageContext<'_>> for ToolMessageHandler
|
|||||||
tool_data.tools.get(active_tool).unwrap().activate(responses);
|
tool_data.tools.get(active_tool).unwrap().activate(responses);
|
||||||
|
|
||||||
// Register initial properties
|
// Register initial properties
|
||||||
tool_data.tools.get(active_tool).unwrap().refresh_options(responses, cached_data);
|
tool_data.tools.get(active_tool).unwrap().refresh_options(responses);
|
||||||
|
|
||||||
// Notify the frontend about the initial active tool
|
// Notify the frontend about the initial active tool
|
||||||
tool_data.send_layout(responses, LayoutTarget::ToolShelf, preferences.brush_tool);
|
tool_data.send_layout(responses, LayoutTarget::ToolShelf, preferences.brush_tool);
|
||||||
@@ -235,7 +234,7 @@ impl MessageHandler<ToolMessage, ToolMessageContext<'_>> for ToolMessageHandler
|
|||||||
node_graph,
|
node_graph,
|
||||||
preferences,
|
preferences,
|
||||||
viewport,
|
viewport,
|
||||||
cached_data,
|
fonts,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Set initial hints and cursor
|
// Set initial hints and cursor
|
||||||
@@ -259,7 +258,7 @@ impl MessageHandler<ToolMessage, ToolMessageContext<'_>> for ToolMessageHandler
|
|||||||
ToolMessage::RefreshToolOptions => {
|
ToolMessage::RefreshToolOptions => {
|
||||||
let tool_data = &mut self.tool_state.tool_data;
|
let tool_data = &mut self.tool_state.tool_data;
|
||||||
|
|
||||||
tool_data.tools.get(&tool_data.active_tool_type).unwrap().refresh_options(responses, cached_data);
|
tool_data.tools.get(&tool_data.active_tool_type).unwrap().refresh_options(responses);
|
||||||
}
|
}
|
||||||
ToolMessage::RefreshToolShelf => {
|
ToolMessage::RefreshToolShelf => {
|
||||||
let tool_data = &mut self.tool_state.tool_data;
|
let tool_data = &mut self.tool_state.tool_data;
|
||||||
@@ -347,7 +346,7 @@ impl MessageHandler<ToolMessage, ToolMessageContext<'_>> for ToolMessageHandler
|
|||||||
node_graph,
|
node_graph,
|
||||||
preferences,
|
preferences,
|
||||||
viewport,
|
viewport,
|
||||||
cached_data,
|
fonts,
|
||||||
};
|
};
|
||||||
if matches!(tool_message, ToolMessage::UpdateHints) {
|
if matches!(tool_message, ToolMessage::UpdateHints) {
|
||||||
if graph_view_overlay_open {
|
if graph_view_overlay_open {
|
||||||
|
|||||||
@@ -720,13 +720,7 @@ impl Fsm for SelectToolFsmState {
|
|||||||
type ToolOptions = ();
|
type ToolOptions = ();
|
||||||
|
|
||||||
fn transition(self, event: ToolMessage, tool_data: &mut Self::ToolData, tool_action_data: &mut ToolActionMessageContext, _tool_options: &(), responses: &mut VecDeque<Message>) -> Self {
|
fn transition(self, event: ToolMessage, tool_data: &mut Self::ToolData, tool_action_data: &mut ToolActionMessageContext, _tool_options: &(), responses: &mut VecDeque<Message>) -> Self {
|
||||||
let ToolActionMessageContext {
|
let ToolActionMessageContext { document, input, viewport, fonts, .. } = tool_action_data;
|
||||||
document,
|
|
||||||
input,
|
|
||||||
viewport,
|
|
||||||
cached_data,
|
|
||||||
..
|
|
||||||
} = tool_action_data;
|
|
||||||
|
|
||||||
let ToolMessage::Select(event) = event else { return self };
|
let ToolMessage::Select(event) = event else { return self };
|
||||||
match (self, event) {
|
match (self, event) {
|
||||||
@@ -751,7 +745,7 @@ impl Fsm for SelectToolFsmState {
|
|||||||
|
|
||||||
if document.metadata().is_text_layer(layer) {
|
if document.metadata().is_text_layer(layer) {
|
||||||
let layer_to_viewport = document.metadata().transform_to_viewport(layer);
|
let layer_to_viewport = document.metadata().transform_to_viewport(layer);
|
||||||
let transformed_quad = layer_to_viewport * text_bounding_box(layer, document, &cached_data.font_cache);
|
let transformed_quad = layer_to_viewport * text_bounding_box(layer, document, fonts, responses);
|
||||||
overlay_context.dashed_quad(transformed_quad, None, None, Some(7.), Some(5.), None);
|
overlay_context.dashed_quad(transformed_quad, None, None, Some(7.), Some(5.), None);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +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::overlays::utility_types::OverlayContext;
|
||||||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||||
use crate::messages::portfolio::document::utility_types::network_interface::InputConnector;
|
use crate::messages::portfolio::document::utility_types::network_interface::InputConnector;
|
||||||
use crate::messages::portfolio::utility_types::{CachedData, FontCatalog, FontCatalogStyle};
|
use crate::messages::portfolio::fonts::utility_types::{FontCatalog, FontCatalogStyle};
|
||||||
use crate::messages::tool::common_functionality::auto_panning::AutoPanning;
|
use crate::messages::tool::common_functionality::auto_panning::AutoPanning;
|
||||||
use crate::messages::tool::common_functionality::color_selector::{
|
use crate::messages::tool::common_functionality::color_selector::{
|
||||||
ToolColorOptions, apply_fill_only_color_pick, apply_fill_only_enabled, refresh_slot_working_color, selection_changed_since_last_sync, solid, sync_fill_only,
|
ToolColorOptions, apply_fill_only_color_pick, apply_fill_only_enabled, refresh_slot_working_color, selection_changed_since_last_sync, solid, sync_fill_only,
|
||||||
@@ -17,12 +17,13 @@ use crate::messages::tool::common_functionality::snapping::{self, SnapCandidateP
|
|||||||
use crate::messages::tool::common_functionality::transformation_cage::*;
|
use crate::messages::tool::common_functionality::transformation_cage::*;
|
||||||
use crate::messages::tool::common_functionality::utility_functions::text_bounding_box;
|
use crate::messages::tool::common_functionality::utility_functions::text_bounding_box;
|
||||||
use crate::messages::tool::utility_types::ToolRefreshOptions;
|
use crate::messages::tool::utility_types::ToolRefreshOptions;
|
||||||
|
use graph_craft::application_io::resource::ResourceId;
|
||||||
use graph_craft::document::value::TaggedValue;
|
use graph_craft::document::value::TaggedValue;
|
||||||
use graph_craft::document::{NodeId, NodeInput};
|
use graph_craft::document::{NodeId, NodeInput};
|
||||||
use graphene_std::choice_type::ChoiceTypeStatic;
|
use graphene_std::choice_type::ChoiceTypeStatic;
|
||||||
use graphene_std::color::SRGBA8;
|
use graphene_std::color::SRGBA8;
|
||||||
use graphene_std::renderer::Quad;
|
use graphene_std::renderer::Quad;
|
||||||
use graphene_std::text::{Font, FontCache, TextAlign, TypesettingConfig, lines_clipping};
|
use graphene_std::text::{Font, TextAlign, TypesettingConfig, lines_clipping};
|
||||||
use graphene_std::vector::style::{Fill, FillChoice, FillChoiceUI};
|
use graphene_std::vector::style::{Fill, FillChoice, FillChoiceUI};
|
||||||
use graphene_std::{Color, NodeInputDecleration};
|
use graphene_std::{Color, NodeInputDecleration};
|
||||||
|
|
||||||
@@ -105,31 +106,30 @@ impl ToolMetadata for TextTool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn create_text_widgets(tool: &TextTool, font_catalog: &FontCatalog, document: &DocumentMessageHandler) -> Vec<WidgetInstance> {
|
fn create_text_widgets(tool: &TextTool, font_catalog: &FontCatalog, document: &DocumentMessageHandler) -> Vec<WidgetInstance> {
|
||||||
// If a single text layer is selected, the control bar's font/style menus drive that layer's text node directly, going through the
|
|
||||||
// same code path as the Properties panel (LoadFontData + SetInputValue, with closest_style and font_style_to_restore bookkeeping).
|
|
||||||
// Otherwise the menus only update the control bar option for the next created text.
|
|
||||||
let text_node_id = can_edit_selected(document).and_then(|layer| graph_modification_utils::get_text_id(layer, &document.network_interface));
|
let text_node_id = can_edit_selected(document).and_then(|layer| graph_modification_utils::get_text_id(layer, &document.network_interface));
|
||||||
|
|
||||||
let font_input_index = graphene_std::text::text::FontInput::INDEX;
|
let apply_font = move |font: Font| -> Message {
|
||||||
let apply_font = move |new_font: Font| -> Message {
|
|
||||||
match text_node_id {
|
match text_node_id {
|
||||||
Some(node_id) => NodeGraphMessage::SetInputValue {
|
Some(node_id) => {
|
||||||
node_id,
|
let resource_id = ResourceId::new();
|
||||||
input_index: font_input_index,
|
Message::Batched {
|
||||||
value: TaggedValue::Font(new_font),
|
messages: Box::new([
|
||||||
|
DocumentMessage::Resource(ResourceMessage::AddFont { resource_id, font }).into(),
|
||||||
|
NodeGraphMessage::SetInputValue {
|
||||||
|
node_id,
|
||||||
|
input_index: graphene_std::text::text::FontInput::INDEX,
|
||||||
|
value: TaggedValue::Resource(resource_id),
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
]),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.into(),
|
|
||||||
None => TextToolMessage::UpdateOptions {
|
None => TextToolMessage::UpdateOptions {
|
||||||
options: TextOptionsUpdate::Font { font: new_font },
|
options: TextOptionsUpdate::Font { font },
|
||||||
}
|
}
|
||||||
.into(),
|
.into(),
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let preview_font = move |new_font: Font| -> Message {
|
|
||||||
Message::Batched {
|
|
||||||
messages: Box::new([PortfolioMessage::LoadFontData { font: new_font.clone() }.into(), apply_font(new_font)]),
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let commit_font = move |new_font: Font| -> Message {
|
let commit_font = move |new_font: Font| -> Message {
|
||||||
match text_node_id {
|
match text_node_id {
|
||||||
Some(_) => DeferMessage::AfterGraphRun {
|
Some(_) => DeferMessage::AfterGraphRun {
|
||||||
@@ -142,35 +142,27 @@ fn create_text_widgets(tool: &TextTool, font_catalog: &FontCatalog, document: &D
|
|||||||
|
|
||||||
let font = DropdownInput::new(vec![
|
let font = DropdownInput::new(vec![
|
||||||
font_catalog
|
font_catalog
|
||||||
.0
|
|
||||||
.iter()
|
.iter()
|
||||||
.map(|family| {
|
.map(|family| {
|
||||||
let current_font = &tool.options.font;
|
let current_font = &tool.options.font;
|
||||||
let mut new_font = Font::new(family.name.clone(), current_font.font_style_to_restore.clone().unwrap_or_else(|| current_font.font_style.clone()));
|
|
||||||
new_font.font_style_to_restore = current_font.font_style_to_restore.clone().or_else(|| Some(new_font.font_style.clone()));
|
|
||||||
let FontCatalogStyle { weight, italic, .. } = FontCatalogStyle::from_named_style(&new_font.font_style, "");
|
|
||||||
new_font.font_style = family.closest_style(weight, italic).to_named_style();
|
|
||||||
|
|
||||||
// Intentionally drop `font_style_to_restore` on commit so the committed style becomes the new basis for
|
|
||||||
// subsequent family switches. Preserving the original style intent is hover-only behavior (handled by `new_font`).
|
|
||||||
let FontCatalogStyle { weight, italic, .. } = FontCatalogStyle::from_named_style(¤t_font.font_style, "");
|
let FontCatalogStyle { weight, italic, .. } = FontCatalogStyle::from_named_style(¤t_font.font_style, "");
|
||||||
let commit_only_font = Font::new(family.name.clone(), family.closest_style(weight, italic).to_named_style());
|
let new_font = Font::new(family.name.clone(), family.closest_style(weight, italic).to_named_style());
|
||||||
|
let commit_only_font = new_font.clone();
|
||||||
|
|
||||||
MenuListEntry::new(family.name.clone())
|
MenuListEntry::new(family.name.clone())
|
||||||
.label(family.name.clone())
|
.label(family.name.clone())
|
||||||
.font(family.closest_style(400, false).preview_url(&family.name))
|
.font(family.closest_style(400, false).preview_url(&family.name))
|
||||||
.on_update(move |_| preview_font(new_font.clone()))
|
.on_update(move |_| apply_font(new_font.clone()))
|
||||||
.on_commit(move |_| commit_font(commit_only_font.clone()))
|
.on_commit(move |_| commit_font(commit_only_font.clone()))
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>(),
|
.collect::<Vec<_>>(),
|
||||||
])
|
])
|
||||||
.selected_index(font_catalog.0.iter().position(|family| family.name == tool.options.font.font_family).map(|i| i as u32))
|
.selected_index(font_catalog.iter().position(|family| family.name == tool.options.font.font_family).map(|i| i as u32))
|
||||||
.virtual_scrolling(true)
|
.virtual_scrolling(true)
|
||||||
.widget_instance();
|
.widget_instance();
|
||||||
|
|
||||||
let style = DropdownInput::new({
|
let style = DropdownInput::new({
|
||||||
font_catalog
|
font_catalog
|
||||||
.0
|
|
||||||
.iter()
|
.iter()
|
||||||
.find(|family| family.name == tool.options.font.font_family)
|
.find(|family| family.name == tool.options.font.font_family)
|
||||||
.map(|family| {
|
.map(|family| {
|
||||||
@@ -182,7 +174,7 @@ fn create_text_widgets(tool: &TextTool, font_catalog: &FontCatalog, document: &D
|
|||||||
|
|
||||||
MenuListEntry::new(font_style.clone())
|
MenuListEntry::new(font_style.clone())
|
||||||
.label(font_style)
|
.label(font_style)
|
||||||
.on_update(move |_| preview_font(new_font.clone()))
|
.on_update(move |_| apply_font(new_font.clone()))
|
||||||
.on_commit(move |_| commit_font(new_font_for_commit.clone()))
|
.on_commit(move |_| commit_font(new_font_for_commit.clone()))
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -196,7 +188,6 @@ fn create_text_widgets(tool: &TextTool, font_catalog: &FontCatalog, document: &D
|
|||||||
})
|
})
|
||||||
.selected_index(
|
.selected_index(
|
||||||
font_catalog
|
font_catalog
|
||||||
.0
|
|
||||||
.iter()
|
.iter()
|
||||||
.find(|family| family.name == tool.options.font.font_family)
|
.find(|family| family.name == tool.options.font.font_family)
|
||||||
.and_then(|family| {
|
.and_then(|family| {
|
||||||
@@ -253,7 +244,7 @@ fn create_text_widgets(tool: &TextTool, font_catalog: &FontCatalog, document: &D
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ToolRefreshOptions for TextTool {
|
impl ToolRefreshOptions for TextTool {
|
||||||
fn refresh_options(&self, responses: &mut VecDeque<Message>, _cached_data: &CachedData) {
|
fn refresh_options(&self, responses: &mut VecDeque<Message>) {
|
||||||
// Defer to the SelectionChanged handler which has document context, required for the font/style
|
// Defer to the SelectionChanged handler which has document context, required for the font/style
|
||||||
// dropdowns to bind to the selected text layer's node graph inputs
|
// dropdowns to bind to the selected text layer's node graph inputs
|
||||||
responses.add(TextToolMessage::SelectionChanged);
|
responses.add(TextToolMessage::SelectionChanged);
|
||||||
@@ -303,7 +294,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Text
|
|||||||
ToolMessage::Text(TextToolMessage::UpdateOptions { options }) => options,
|
ToolMessage::Text(TextToolMessage::UpdateOptions { options }) => options,
|
||||||
ToolMessage::Text(TextToolMessage::SelectionChanged) => {
|
ToolMessage::Text(TextToolMessage::SelectionChanged) => {
|
||||||
if let Some(layer) = can_edit_selected(context.document)
|
if let Some(layer) = can_edit_selected(context.document)
|
||||||
&& let Some((_, font, typesetting, _)) = graph_modification_utils::get_text(layer, &context.document.network_interface)
|
&& let Some((_, font, typesetting, _)) = graph_modification_utils::get_text(layer, &context.document.network_interface, context.fonts, &context.document.resources)
|
||||||
{
|
{
|
||||||
self.options.align = typesetting.align;
|
self.options.align = typesetting.align;
|
||||||
self.options.font_size = typesetting.font_size;
|
self.options.font_size = typesetting.font_size;
|
||||||
@@ -311,7 +302,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Text
|
|||||||
if let Some(editing_text) = self.tool_data.editing_text.as_mut() {
|
if let Some(editing_text) = self.tool_data.editing_text.as_mut() {
|
||||||
editing_text.typesetting.align = typesetting.align;
|
editing_text.typesetting.align = typesetting.align;
|
||||||
editing_text.typesetting.font_size = typesetting.font_size;
|
editing_text.typesetting.font_size = typesetting.font_size;
|
||||||
editing_text.font = font.clone();
|
editing_text.font = font;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -326,7 +317,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Text
|
|||||||
// Text tool has no fill checkbox; keep enabled so new text never starts with `None`
|
// Text tool has no fill checkbox; keep enabled so new text never starts with `None`
|
||||||
self.options.fill.enabled = Some(true);
|
self.options.fill.enabled = Some(true);
|
||||||
|
|
||||||
self.send_layout(responses, LayoutTarget::ToolOptions, &context.cached_data.font_catalog, context.document);
|
self.send_layout(responses, LayoutTarget::ToolOptions, &context.fonts.font_catalog, context.document);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
@@ -385,7 +376,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Text
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.send_layout(responses, LayoutTarget::ToolOptions, &context.cached_data.font_catalog, context.document);
|
self.send_layout(responses, LayoutTarget::ToolOptions, &context.fonts.font_catalog, context.document);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn actions(&self) -> ActionList {
|
fn actions(&self) -> ActionList {
|
||||||
@@ -476,9 +467,9 @@ struct TextToolData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl TextToolData {
|
impl TextToolData {
|
||||||
fn delete_empty_layer(&mut self, font_cache: &FontCache, responses: &mut VecDeque<Message>) -> TextToolFsmState {
|
fn delete_empty_layer(&mut self, fonts: &FontsMessageHandler, responses: &mut VecDeque<Message>) -> TextToolFsmState {
|
||||||
// Remove the editable textbox UI first
|
// Remove the editable textbox UI first
|
||||||
self.set_editing(false, font_cache, responses);
|
self.set_editing(false, fonts, responses);
|
||||||
|
|
||||||
// Delete the empty text layer and update the graph
|
// Delete the empty text layer and update the graph
|
||||||
responses.add(NodeGraphMessage::DeleteNodes {
|
responses.add(NodeGraphMessage::DeleteNodes {
|
||||||
@@ -491,15 +482,16 @@ impl TextToolData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Set the editing state of the currently modifying layer
|
/// Set the editing state of the currently modifying layer
|
||||||
fn set_editing(&self, editable: bool, font_cache: &FontCache, responses: &mut VecDeque<Message>) {
|
fn set_editing(&self, editable: bool, fonts: &FontsMessageHandler, responses: &mut VecDeque<Message>) {
|
||||||
if let Some(editing_text) = self.editing_text.as_ref().filter(|_| editable) {
|
if let Some(editing_text) = self.editing_text.as_ref().filter(|_| editable) {
|
||||||
let (align, align_last) = editing_text.typesetting.align.css();
|
let (align, align_last) = editing_text.typesetting.align.css();
|
||||||
|
let font_data = fonts.get_resource_or_queue_load(&editing_text.font, responses).as_ref().to_vec().into();
|
||||||
responses.add(FrontendMessage::DisplayEditableTextbox {
|
responses.add(FrontendMessage::DisplayEditableTextbox {
|
||||||
text: editing_text.text.clone(),
|
text: editing_text.text.clone(),
|
||||||
line_height_ratio: editing_text.typesetting.line_height_ratio,
|
line_height_ratio: editing_text.typesetting.line_height_ratio,
|
||||||
font_size: editing_text.typesetting.font_size,
|
font_size: editing_text.typesetting.font_size,
|
||||||
color: editing_text.color.map_or("#000000".to_string(), |color| SRGBA8::from(color).to_css_hex()),
|
color: editing_text.color.map_or("#000000".to_string(), |color| SRGBA8::from(color).to_css_hex()),
|
||||||
font_data: font_cache.get(&editing_text.font).map(|(data, _)| data.clone()).unwrap_or_default().into(),
|
font_data,
|
||||||
transform: editing_text.transform.to_cols_array(),
|
transform: editing_text.transform.to_cols_array(),
|
||||||
max_width: editing_text.typesetting.max_width,
|
max_width: editing_text.typesetting.max_width,
|
||||||
max_height: editing_text.typesetting.max_height,
|
max_height: editing_text.typesetting.max_height,
|
||||||
@@ -517,13 +509,13 @@ impl TextToolData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_layer_text_node(&mut self, document: &DocumentMessageHandler) -> Option<()> {
|
fn load_layer_text_node(&mut self, document: &DocumentMessageHandler, fonts: &FontsMessageHandler) -> Option<()> {
|
||||||
let transform = document.metadata().transform_to_viewport(self.layer);
|
let transform = document.metadata().transform_to_viewport(self.layer);
|
||||||
let color = graph_modification_utils::get_fill_color(self.layer, &document.network_interface).unwrap_or(Color::BLACK);
|
let color = graph_modification_utils::get_fill_color(self.layer, &document.network_interface).unwrap_or(Color::BLACK);
|
||||||
let (text, font, typesetting, _) = graph_modification_utils::get_text(self.layer, &document.network_interface)?;
|
let (text, font, typesetting, _) = graph_modification_utils::get_text(self.layer, &document.network_interface, fonts, &document.resources)?;
|
||||||
self.editing_text = Some(EditingText {
|
self.editing_text = Some(EditingText {
|
||||||
text: text.clone(),
|
text: text.clone(),
|
||||||
font: font.clone(),
|
font,
|
||||||
typesetting,
|
typesetting,
|
||||||
color: Some(color),
|
color: Some(color),
|
||||||
transform,
|
transform,
|
||||||
@@ -532,20 +524,20 @@ impl TextToolData {
|
|||||||
Some(())
|
Some(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn start_editing_layer(&mut self, layer: LayerNodeIdentifier, tool_state: TextToolFsmState, document: &DocumentMessageHandler, font_cache: &FontCache, responses: &mut VecDeque<Message>) {
|
fn start_editing_layer(&mut self, layer: LayerNodeIdentifier, tool_state: TextToolFsmState, document: &DocumentMessageHandler, fonts: &FontsMessageHandler, responses: &mut VecDeque<Message>) {
|
||||||
if layer == LayerNodeIdentifier::ROOT_PARENT {
|
if layer == LayerNodeIdentifier::ROOT_PARENT {
|
||||||
log::error!("Cannot edit ROOT_PARENT in TextTooLData")
|
log::error!("Cannot edit ROOT_PARENT in TextTooLData")
|
||||||
}
|
}
|
||||||
|
|
||||||
if tool_state == TextToolFsmState::Editing {
|
if tool_state == TextToolFsmState::Editing {
|
||||||
self.set_editing(false, font_cache, responses);
|
self.set_editing(false, fonts, responses);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.layer = layer;
|
self.layer = layer;
|
||||||
if self.load_layer_text_node(document).is_some() {
|
if self.load_layer_text_node(document, fonts).is_some() {
|
||||||
responses.add(DocumentMessage::AddTransaction);
|
responses.add(DocumentMessage::AddTransaction);
|
||||||
|
|
||||||
self.set_editing(true, font_cache, responses);
|
self.set_editing(true, fonts, responses);
|
||||||
|
|
||||||
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![self.layer.to_node()] });
|
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![self.layer.to_node()] });
|
||||||
// Make the rendered text invisible while editing
|
// Make the rendered text invisible while editing
|
||||||
@@ -557,14 +549,16 @@ impl TextToolData {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn new_text(&mut self, document: &DocumentMessageHandler, editing_text: EditingText, font_cache: &FontCache, responses: &mut VecDeque<Message>) {
|
fn new_text(&mut self, document: &DocumentMessageHandler, editing_text: EditingText, fonts: &FontsMessageHandler, responses: &mut VecDeque<Message>) {
|
||||||
// Create new text
|
|
||||||
self.new_text = String::new();
|
self.new_text = String::new();
|
||||||
responses.add(DocumentMessage::AddTransaction);
|
responses.add(DocumentMessage::AddTransaction);
|
||||||
|
|
||||||
self.layer = LayerNodeIdentifier::new_unchecked(NodeId::new());
|
self.layer = LayerNodeIdentifier::new_unchecked(NodeId::new());
|
||||||
|
|
||||||
responses.add(PortfolioMessage::LoadFontData { font: editing_text.font.clone() });
|
responses.add(FontsMessage::Load {
|
||||||
|
font: editing_text.font.clone(),
|
||||||
|
response: Box::new(NodeGraphMessage::RunDocumentGraph.into()),
|
||||||
|
});
|
||||||
responses.add(GraphOperationMessage::NewTextLayer {
|
responses.add(GraphOperationMessage::NewTextLayer {
|
||||||
id: self.layer.to_node(),
|
id: self.layer.to_node(),
|
||||||
text: String::new(),
|
text: String::new(),
|
||||||
@@ -580,7 +574,7 @@ impl TextToolData {
|
|||||||
let transform = editing_text.transform;
|
let transform = editing_text.transform;
|
||||||
self.editing_text = Some(editing_text);
|
self.editing_text = Some(editing_text);
|
||||||
|
|
||||||
self.set_editing(true, font_cache, responses);
|
self.set_editing(true, fonts, responses);
|
||||||
|
|
||||||
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![self.layer.to_node()] });
|
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![self.layer.to_node()] });
|
||||||
|
|
||||||
@@ -602,20 +596,23 @@ impl TextToolData {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn check_click(document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, font_cache: &FontCache) -> Option<LayerNodeIdentifier> {
|
fn check_click(document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, fonts: &FontsMessageHandler, responses: &mut VecDeque<Message>) -> Option<LayerNodeIdentifier> {
|
||||||
document.metadata().all_layers().filter(|&layer| document.metadata().is_text_layer(layer)).find(|&layer| {
|
let mouse = DVec2::new(input.mouse.position.x, input.mouse.position.y);
|
||||||
let transformed_quad = document.metadata().transform_to_viewport(layer) * text_bounding_box(layer, document, font_cache);
|
let layers: Vec<LayerNodeIdentifier> = document.metadata().all_layers().filter(|&layer| document.metadata().is_text_layer(layer)).collect();
|
||||||
let mouse = DVec2::new(input.mouse.position.x, input.mouse.position.y);
|
for layer in layers {
|
||||||
|
let transformed_quad = document.metadata().transform_to_viewport(layer) * text_bounding_box(layer, document, fonts, responses);
|
||||||
transformed_quad.contains(mouse)
|
if transformed_quad.contains(mouse) {
|
||||||
})
|
return Some(layer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_snap_candidates(&mut self, document: &DocumentMessageHandler, font_cache: &FontCache) {
|
fn get_snap_candidates(&mut self, document: &DocumentMessageHandler, fonts: &FontsMessageHandler, responses: &mut VecDeque<Message>) {
|
||||||
self.snap_candidates.clear();
|
self.snap_candidates.clear();
|
||||||
|
|
||||||
if let Some(ResizingLayer { id, .. }) = self.layer_dragging {
|
if let Some(ResizingLayer { id, .. }) = self.layer_dragging {
|
||||||
let quad = document.metadata().transform_to_document(id) * text_bounding_box(id, document, font_cache);
|
let quad = document.metadata().transform_to_document(id) * text_bounding_box(id, document, fonts, responses);
|
||||||
snapping::get_bbox_points(quad, &mut self.snap_candidates, snapping::BBoxSnapValues::BOUNDING_BOX, document);
|
snapping::get_bbox_points(quad, &mut self.snap_candidates, snapping::BBoxSnapValues::BOUNDING_BOX, document);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -651,14 +648,7 @@ impl Fsm for TextToolFsmState {
|
|||||||
tool_options: &Self::ToolOptions,
|
tool_options: &Self::ToolOptions,
|
||||||
responses: &mut VecDeque<Message>,
|
responses: &mut VecDeque<Message>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let ToolActionMessageContext {
|
let ToolActionMessageContext { document, input, fonts, viewport, .. } = transition_data;
|
||||||
document,
|
|
||||||
input,
|
|
||||||
cached_data,
|
|
||||||
viewport,
|
|
||||||
..
|
|
||||||
} = transition_data;
|
|
||||||
let font_cache = &cached_data.font_cache;
|
|
||||||
let fill_color = COLOR_OVERLAY_BLUE_05;
|
let fill_color = COLOR_OVERLAY_BLUE_05;
|
||||||
|
|
||||||
let ToolMessage::Text(event) = event else { return self };
|
let ToolMessage::Text(event) = event else { return self };
|
||||||
@@ -667,7 +657,8 @@ impl Fsm for TextToolFsmState {
|
|||||||
let transform = document.metadata().transform_to_viewport(tool_data.layer).to_cols_array();
|
let transform = document.metadata().transform_to_viewport(tool_data.layer).to_cols_array();
|
||||||
responses.add(FrontendMessage::DisplayEditableTextboxTransform { transform });
|
responses.add(FrontendMessage::DisplayEditableTextboxTransform { transform });
|
||||||
if let Some(editing_text) = tool_data.editing_text.as_mut() {
|
if let Some(editing_text) = tool_data.editing_text.as_mut() {
|
||||||
let far = graphene_std::text::bounding_box(&tool_data.new_text, &editing_text.font, font_cache, editing_text.typesetting, false);
|
let font_resource = fonts.get_resource_or_queue_load(&editing_text.font, responses);
|
||||||
|
let far = graphene_std::text::bounding_box(&tool_data.new_text, &font_resource, editing_text.typesetting, false);
|
||||||
if far.x != 0. && far.y != 0. {
|
if far.x != 0. && far.y != 0. {
|
||||||
let quad = Quad::from_box([DVec2::ZERO, far]);
|
let quad = Quad::from_box([DVec2::ZERO, far]);
|
||||||
let transformed_quad = document.metadata().transform_to_viewport(tool_data.layer) * quad;
|
let transformed_quad = document.metadata().transform_to_viewport(tool_data.layer) * quad;
|
||||||
@@ -694,7 +685,7 @@ impl Fsm for TextToolFsmState {
|
|||||||
let selected = document.network_interface.selected_nodes();
|
let selected = document.network_interface.selected_nodes();
|
||||||
let mut all_layers = selected.selected_visible_and_unlocked_layers(&document.network_interface);
|
let mut all_layers = selected.selected_visible_and_unlocked_layers(&document.network_interface);
|
||||||
let layer = all_layers.find(|&layer| document.metadata().is_text_layer(layer));
|
let layer = all_layers.find(|&layer| document.metadata().is_text_layer(layer));
|
||||||
let bounds = layer.map(|layer| text_bounding_box(layer, document, font_cache));
|
let bounds = layer.map(|layer| text_bounding_box(layer, document, fonts, responses));
|
||||||
let layer_transform = layer.map(|layer| document.metadata().transform_to_viewport(layer)).unwrap_or(DAffine2::IDENTITY);
|
let layer_transform = layer.map(|layer| document.metadata().transform_to_viewport(layer)).unwrap_or(DAffine2::IDENTITY);
|
||||||
|
|
||||||
if layer.is_none() || bounds.is_none() || layer_transform.matrix2.determinant() == 0. {
|
if layer.is_none() || bounds.is_none() || layer_transform.matrix2.determinant() == 0. {
|
||||||
@@ -710,10 +701,11 @@ impl Fsm for TextToolFsmState {
|
|||||||
bounding_box_manager.render_quad(&mut overlay_context);
|
bounding_box_manager.render_quad(&mut overlay_context);
|
||||||
// Draw red overlay if text is clipped
|
// Draw red overlay if text is clipped
|
||||||
let transformed_quad = layer_transform * bounds;
|
let transformed_quad = layer_transform * bounds;
|
||||||
if let Some((text, font, typesetting, _)) = graph_modification_utils::get_text(layer.unwrap(), &document.network_interface)
|
if let Some((text, font, typesetting, _)) = graph_modification_utils::get_text(layer.unwrap(), &document.network_interface, fonts, &document.resources) {
|
||||||
&& lines_clipping(text.as_str(), font, font_cache, typesetting)
|
let font_resource = fonts.get_resource_or_queue_load(&font, responses);
|
||||||
{
|
if lines_clipping(text.as_str(), &font_resource, typesetting) {
|
||||||
overlay_context.line(transformed_quad.0[2], transformed_quad.0[3], Some(COLOR_OVERLAY_RED), Some(3.));
|
overlay_context.line(transformed_quad.0[2], transformed_quad.0[3], Some(COLOR_OVERLAY_RED), Some(3.));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bounding_box_manager.render_overlays(&mut overlay_context, false);
|
bounding_box_manager.render_overlays(&mut overlay_context, false);
|
||||||
@@ -728,7 +720,7 @@ impl Fsm for TextToolFsmState {
|
|||||||
}
|
}
|
||||||
(state, TextToolMessage::EditSelected) => {
|
(state, TextToolMessage::EditSelected) => {
|
||||||
if let Some(layer) = can_edit_selected(document) {
|
if let Some(layer) = can_edit_selected(document) {
|
||||||
tool_data.start_editing_layer(layer, state, document, font_cache, responses);
|
tool_data.start_editing_layer(layer, state, document, fonts, responses);
|
||||||
return TextToolFsmState::Editing;
|
return TextToolFsmState::Editing;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -769,10 +761,10 @@ impl Fsm for TextToolFsmState {
|
|||||||
bounds.original_bound_transform = bounds.transform;
|
bounds.original_bound_transform = bounds.transform;
|
||||||
bounds.center_of_transformation = bounds.transform.transform_point2((bounds.bounds[0] + bounds.bounds[1]) / 2.);
|
bounds.center_of_transformation = bounds.transform.transform_point2((bounds.bounds[0] + bounds.bounds[1]) / 2.);
|
||||||
}
|
}
|
||||||
tool_data.get_snap_candidates(document, font_cache);
|
tool_data.get_snap_candidates(document, fonts, responses);
|
||||||
|
|
||||||
return TextToolFsmState::ResizingBounds;
|
return TextToolFsmState::ResizingBounds;
|
||||||
} else if let Some(clicked_layer) = TextToolData::check_click(document, input, font_cache) {
|
} else if let Some(clicked_layer) = TextToolData::check_click(document, input, fonts, responses) {
|
||||||
responses.add(DocumentMessage::StartTransaction);
|
responses.add(DocumentMessage::StartTransaction);
|
||||||
|
|
||||||
if selected != Some(clicked_layer) {
|
if selected != Some(clicked_layer) {
|
||||||
@@ -784,7 +776,7 @@ impl Fsm for TextToolFsmState {
|
|||||||
id: clicked_layer,
|
id: clicked_layer,
|
||||||
original_transform,
|
original_transform,
|
||||||
});
|
});
|
||||||
tool_data.get_snap_candidates(document, font_cache);
|
tool_data.get_snap_candidates(document, fonts, responses);
|
||||||
return TextToolFsmState::Dragging;
|
return TextToolFsmState::Dragging;
|
||||||
}
|
}
|
||||||
TextToolFsmState::Placing
|
TextToolFsmState::Placing
|
||||||
@@ -967,8 +959,8 @@ impl Fsm for TextToolFsmState {
|
|||||||
let has_dragged = (start - end).length_squared() > DRAG_THRESHOLD * DRAG_THRESHOLD;
|
let has_dragged = (start - end).length_squared() > DRAG_THRESHOLD * DRAG_THRESHOLD;
|
||||||
|
|
||||||
// Check if the user has clicked (no dragging) on some existing text
|
// Check if the user has clicked (no dragging) on some existing text
|
||||||
if !has_dragged && let Some(clicked_text_layer_path) = TextToolData::check_click(document, input, font_cache) {
|
if !has_dragged && let Some(clicked_text_layer_path) = TextToolData::check_click(document, input, fonts, responses) {
|
||||||
tool_data.start_editing_layer(clicked_text_layer_path, self, document, font_cache, responses);
|
tool_data.start_editing_layer(clicked_text_layer_path, self, document, fonts, responses);
|
||||||
return TextToolFsmState::Editing;
|
return TextToolFsmState::Editing;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -989,7 +981,7 @@ impl Fsm for TextToolFsmState {
|
|||||||
font: Font::new(tool_options.font.font_family.clone(), tool_options.font.font_style.clone()),
|
font: Font::new(tool_options.font.font_family.clone(), tool_options.font.font_style.clone()),
|
||||||
color: tool_options.fill.active_color(),
|
color: tool_options.fill.active_color(),
|
||||||
};
|
};
|
||||||
tool_data.new_text(document, editing_text, font_cache, responses);
|
tool_data.new_text(document, editing_text, fonts, responses);
|
||||||
TextToolFsmState::Editing
|
TextToolFsmState::Editing
|
||||||
}
|
}
|
||||||
(TextToolFsmState::Dragging, TextToolMessage::DragStop) => {
|
(TextToolFsmState::Dragging, TextToolMessage::DragStop) => {
|
||||||
@@ -1004,7 +996,7 @@ impl Fsm for TextToolFsmState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if drag_too_small && let Some(layer_info) = &tool_data.layer_dragging {
|
if drag_too_small && let Some(layer_info) = &tool_data.layer_dragging {
|
||||||
tool_data.start_editing_layer(layer_info.id, self, document, font_cache, responses);
|
tool_data.start_editing_layer(layer_info.id, self, document, fonts, responses);
|
||||||
return TextToolFsmState::Editing;
|
return TextToolFsmState::Editing;
|
||||||
}
|
}
|
||||||
tool_data.layer_dragging.take();
|
tool_data.layer_dragging.take();
|
||||||
@@ -1013,8 +1005,9 @@ impl Fsm for TextToolFsmState {
|
|||||||
}
|
}
|
||||||
(TextToolFsmState::Editing, TextToolMessage::RefreshEditingFontData) => {
|
(TextToolFsmState::Editing, TextToolMessage::RefreshEditingFontData) => {
|
||||||
let font = Font::new(tool_options.font.font_family.clone(), tool_options.font.font_style.clone());
|
let font = Font::new(tool_options.font.font_family.clone(), tool_options.font.font_style.clone());
|
||||||
|
let font_resource = fonts.get_resource_or_queue_load(&font, responses);
|
||||||
responses.add(FrontendMessage::DisplayEditableTextboxUpdateFontData {
|
responses.add(FrontendMessage::DisplayEditableTextboxUpdateFontData {
|
||||||
font_data: font_cache.get(&font).map(|(data, _)| data.clone()).unwrap_or_default().into(),
|
font_data: font_resource.as_ref().to_vec().into(),
|
||||||
});
|
});
|
||||||
|
|
||||||
TextToolFsmState::Editing
|
TextToolFsmState::Editing
|
||||||
@@ -1023,7 +1016,7 @@ impl Fsm for TextToolFsmState {
|
|||||||
tool_data.new_text = new_text;
|
tool_data.new_text = new_text;
|
||||||
|
|
||||||
if !is_left_or_right_click {
|
if !is_left_or_right_click {
|
||||||
tool_data.set_editing(false, font_cache, responses);
|
tool_data.set_editing(false, fonts, responses);
|
||||||
|
|
||||||
responses.add(NodeGraphMessage::SetInput {
|
responses.add(NodeGraphMessage::SetInput {
|
||||||
input_connector: InputConnector::node(graph_modification_utils::get_text_id(tool_data.layer, &document.network_interface).unwrap(), 1),
|
input_connector: InputConnector::node(graph_modification_utils::get_text_id(tool_data.layer, &document.network_interface).unwrap(), 1),
|
||||||
@@ -1034,7 +1027,7 @@ impl Fsm for TextToolFsmState {
|
|||||||
TextToolFsmState::Ready
|
TextToolFsmState::Ready
|
||||||
} else {
|
} else {
|
||||||
if tool_data.new_text.is_empty() {
|
if tool_data.new_text.is_empty() {
|
||||||
return tool_data.delete_empty_layer(font_cache, responses);
|
return tool_data.delete_empty_layer(fonts, responses);
|
||||||
}
|
}
|
||||||
|
|
||||||
responses.add(FrontendMessage::TriggerTextCommit);
|
responses.add(FrontendMessage::TriggerTextCommit);
|
||||||
@@ -1055,7 +1048,7 @@ impl Fsm for TextToolFsmState {
|
|||||||
}
|
}
|
||||||
(TextToolFsmState::Editing, TextToolMessage::Abort) => {
|
(TextToolFsmState::Editing, TextToolMessage::Abort) => {
|
||||||
if tool_data.new_text.is_empty() {
|
if tool_data.new_text.is_empty() {
|
||||||
return tool_data.delete_empty_layer(font_cache, responses);
|
return tool_data.delete_empty_layer(fonts, responses);
|
||||||
}
|
}
|
||||||
|
|
||||||
responses.add(FrontendMessage::TriggerTextCommit);
|
responses.add(FrontendMessage::TriggerTextCommit);
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ use crate::messages::input_mapper::utility_types::macros::action_shortcut;
|
|||||||
use crate::messages::input_mapper::utility_types::misc::ActionShortcut;
|
use crate::messages::input_mapper::utility_types::misc::ActionShortcut;
|
||||||
use crate::messages::layout::utility_types::widget_prelude::*;
|
use crate::messages::layout::utility_types::widget_prelude::*;
|
||||||
use crate::messages::portfolio::document::overlays::utility_types::OverlayProvider;
|
use crate::messages::portfolio::document::overlays::utility_types::OverlayProvider;
|
||||||
use crate::messages::portfolio::utility_types::CachedData;
|
|
||||||
use crate::messages::preferences::PreferencesMessageHandler;
|
use crate::messages::preferences::PreferencesMessageHandler;
|
||||||
use crate::messages::prelude::*;
|
use crate::messages::prelude::*;
|
||||||
use crate::messages::tool::common_functionality::shapes::shape_utility::ShapeType;
|
use crate::messages::tool::common_functionality::shapes::shape_utility::ShapeType;
|
||||||
@@ -25,7 +24,7 @@ pub struct ToolActionMessageContext<'a> {
|
|||||||
pub document_id: DocumentId,
|
pub document_id: DocumentId,
|
||||||
pub global_tool_data: &'a DocumentToolData,
|
pub global_tool_data: &'a DocumentToolData,
|
||||||
pub input: &'a InputPreprocessorMessageHandler,
|
pub input: &'a InputPreprocessorMessageHandler,
|
||||||
pub cached_data: &'a CachedData,
|
pub fonts: &'a FontsMessageHandler,
|
||||||
pub shape_editor: &'a mut ShapeState,
|
pub shape_editor: &'a mut ShapeState,
|
||||||
pub node_graph: &'a NodeGraphExecutor,
|
pub node_graph: &'a NodeGraphExecutor,
|
||||||
pub preferences: &'a PreferencesMessageHandler,
|
pub preferences: &'a PreferencesMessageHandler,
|
||||||
@@ -38,11 +37,11 @@ impl<T> ToolCommon for T where T: for<'a, 'b> MessageHandler<ToolMessage, &'b mu
|
|||||||
type Tool = dyn ToolCommon + Send + Sync;
|
type Tool = dyn ToolCommon + Send + Sync;
|
||||||
|
|
||||||
pub trait ToolRefreshOptions {
|
pub trait ToolRefreshOptions {
|
||||||
fn refresh_options(&self, responses: &mut VecDeque<Message>, _cached_data: &CachedData);
|
fn refresh_options(&self, responses: &mut VecDeque<Message>);
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: LayoutHolder> ToolRefreshOptions for T {
|
impl<T: LayoutHolder> ToolRefreshOptions for T {
|
||||||
fn refresh_options(&self, responses: &mut VecDeque<Message>, _cached_data: &CachedData) {
|
fn refresh_options(&self, responses: &mut VecDeque<Message>) {
|
||||||
self.send_layout(responses, LayoutTarget::ToolOptions);
|
self.send_layout(responses, LayoutTarget::ToolOptions);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ use graphene_std::application_io::{NodeGraphUpdateMessage, RenderConfig, TimingI
|
|||||||
use graphene_std::color::SRGBA8;
|
use graphene_std::color::SRGBA8;
|
||||||
use graphene_std::raster::{CPU, Raster};
|
use graphene_std::raster::{CPU, Raster};
|
||||||
use graphene_std::renderer::RenderMetadata;
|
use graphene_std::renderer::RenderMetadata;
|
||||||
use graphene_std::text::FontCache;
|
|
||||||
use graphene_std::transform::Footprint;
|
use graphene_std::transform::Footprint;
|
||||||
use graphene_std::vector::Vector;
|
use graphene_std::vector::Vector;
|
||||||
use interpreted_executor::dynamic_executor::ResolvedDocumentNodeTypesDelta;
|
use interpreted_executor::dynamic_executor::ResolvedDocumentNodeTypesDelta;
|
||||||
@@ -95,10 +94,6 @@ impl NodeGraphExecutor {
|
|||||||
execution_id
|
execution_id
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_font_cache(&self, font_cache: FontCache) {
|
|
||||||
self.runtime_io.send(GraphRuntimeRequest::FontCacheUpdate(font_cache)).expect("Failed to send font cache update");
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update_editor_preferences(&self, editor_preferences: EditorPreferences) {
|
pub fn update_editor_preferences(&self, editor_preferences: EditorPreferences) {
|
||||||
self.runtime_io
|
self.runtime_io
|
||||||
.send(GraphRuntimeRequest::EditorPreferencesUpdate(editor_preferences))
|
.send(GraphRuntimeRequest::EditorPreferencesUpdate(editor_preferences))
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ use graphene_std::ops::Convert;
|
|||||||
use graphene_std::platform_application_io::canvas_utils::{Canvas, CanvasSurface, CanvasSurfaceHandle};
|
use graphene_std::platform_application_io::canvas_utils::{Canvas, CanvasSurface, CanvasSurfaceHandle};
|
||||||
use graphene_std::raster_types::Raster;
|
use graphene_std::raster_types::Raster;
|
||||||
use graphene_std::renderer::{Render, RenderParams, RenderSvgSegmentList, SvgRender, SvgSegment};
|
use graphene_std::renderer::{Render, RenderParams, RenderSvgSegmentList, SvgRender, SvgSegment};
|
||||||
use graphene_std::text::FontCache;
|
|
||||||
use graphene_std::transform::RenderQuality;
|
use graphene_std::transform::RenderQuality;
|
||||||
use graphene_std::vector::Vector;
|
use graphene_std::vector::Vector;
|
||||||
use graphene_std::vector::style::RenderMode;
|
use graphene_std::vector::style::RenderMode;
|
||||||
@@ -71,7 +70,6 @@ pub struct NodeRuntime {
|
|||||||
pub enum GraphRuntimeRequest {
|
pub enum GraphRuntimeRequest {
|
||||||
GraphUpdate(GraphUpdate),
|
GraphUpdate(GraphUpdate),
|
||||||
ExecutionRequest(ExecutionRequest),
|
ExecutionRequest(ExecutionRequest),
|
||||||
FontCacheUpdate(FontCache),
|
|
||||||
EditorPreferencesUpdate(EditorPreferences),
|
EditorPreferencesUpdate(EditorPreferences),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,7 +132,6 @@ impl NodeRuntime {
|
|||||||
update_thumbnails: true,
|
update_thumbnails: true,
|
||||||
|
|
||||||
editor_api: PlatformEditorApi {
|
editor_api: PlatformEditorApi {
|
||||||
font_cache: FontCache::default(),
|
|
||||||
editor_preferences: Box::new(EditorPreferences::default()),
|
editor_preferences: Box::new(EditorPreferences::default()),
|
||||||
node_graph_message_sender: Box::new(InternalNodeGraphUpdateSender(sender)),
|
node_graph_message_sender: Box::new(InternalNodeGraphUpdateSender(sender)),
|
||||||
|
|
||||||
@@ -162,7 +159,6 @@ impl NodeRuntime {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn run(&mut self) -> Option<ImageTexture> {
|
pub async fn run(&mut self) -> Option<ImageTexture> {
|
||||||
let mut font = None;
|
|
||||||
let mut preferences = None;
|
let mut preferences = None;
|
||||||
let mut graph = None;
|
let mut graph = None;
|
||||||
let mut eyedropper = None;
|
let mut eyedropper = None;
|
||||||
@@ -186,7 +182,6 @@ impl NodeRuntime {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
GraphRuntimeRequest::FontCacheUpdate(_) => font = Some(request),
|
|
||||||
GraphRuntimeRequest::EditorPreferencesUpdate(_) => preferences = Some(request),
|
GraphRuntimeRequest::EditorPreferencesUpdate(_) => preferences = Some(request),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -199,27 +194,13 @@ impl NodeRuntime {
|
|||||||
eyedropper.render_config.pointer = execution.render_config.pointer;
|
eyedropper.render_config.pointer = execution.render_config.pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
let requests = [font, preferences, graph, eyedropper, execution].into_iter().flatten();
|
let requests = [preferences, graph, eyedropper, execution].into_iter().flatten();
|
||||||
|
|
||||||
for request in requests {
|
for request in requests {
|
||||||
match request {
|
match request {
|
||||||
GraphRuntimeRequest::FontCacheUpdate(font_cache) => {
|
|
||||||
self.editor_api = PlatformEditorApi {
|
|
||||||
font_cache,
|
|
||||||
application_io: self.editor_api.application_io.clone(),
|
|
||||||
node_graph_message_sender: Box::new(self.sender.clone()),
|
|
||||||
editor_preferences: Box::new(self.editor_preferences.clone()),
|
|
||||||
}
|
|
||||||
.into();
|
|
||||||
if let Some(graph) = self.old_graph.clone() {
|
|
||||||
// We ignore this result as compilation errors should have been reported in an earlier iteration
|
|
||||||
let _ = self.update_network(graph).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
GraphRuntimeRequest::EditorPreferencesUpdate(preferences) => {
|
GraphRuntimeRequest::EditorPreferencesUpdate(preferences) => {
|
||||||
self.editor_preferences = preferences.clone();
|
self.editor_preferences = preferences.clone();
|
||||||
self.editor_api = PlatformEditorApi {
|
self.editor_api = PlatformEditorApi {
|
||||||
font_cache: self.editor_api.font_cache.clone(),
|
|
||||||
application_io: self.editor_api.application_io.clone(),
|
application_io: self.editor_api.application_io.clone(),
|
||||||
node_graph_message_sender: Box::new(self.sender.clone()),
|
node_graph_message_sender: Box::new(self.sender.clone()),
|
||||||
editor_preferences: Box::new(preferences),
|
editor_preferences: Box::new(preferences),
|
||||||
@@ -587,7 +568,6 @@ pub(crate) fn replace_application_io(application_io: PlatformApplicationIo) {
|
|||||||
impl NodeRuntime {
|
impl NodeRuntime {
|
||||||
pub(crate) fn replace_application_io(&mut self, application_io: PlatformApplicationIo) {
|
pub(crate) fn replace_application_io(&mut self, application_io: PlatformApplicationIo) {
|
||||||
self.editor_api = PlatformEditorApi {
|
self.editor_api = PlatformEditorApi {
|
||||||
font_cache: self.editor_api.font_cache.clone(),
|
|
||||||
application_io: Some(application_io.into()),
|
application_io: Some(application_io.into()),
|
||||||
node_graph_message_sender: Box::new(self.sender.clone()),
|
node_graph_message_sender: Box::new(self.sender.clone()),
|
||||||
editor_preferences: Box::new(self.editor_preferences.clone()),
|
editor_preferences: Box::new(self.editor_preferences.clone()),
|
||||||
|
|||||||
@@ -41,21 +41,21 @@ export function createFontsManager(subscriptions: SubscriptionsRouter, editor: E
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
subscriptions.subscribeFrontendMessage("TriggerFontDataLoad", async (data) => {
|
// Generic URL resolver
|
||||||
const { fontFamily, fontStyle } = data.font;
|
// TODO(keavon): This is currently only used for fonts, but it could be used for other resources and thus should be moved to a more sesible location
|
||||||
|
subscriptions.subscribeFrontendMessage("TriggerResolveResource", async (data) => {
|
||||||
try {
|
try {
|
||||||
if (!data.url) throw new Error("No URL provided for font data load");
|
if (!data.url) throw new Error("No URL provided for resource resolution");
|
||||||
const response = await fetch(data.url, abortController ? { signal: abortController.signal } : undefined);
|
const response = await fetch(data.url, abortController ? { signal: abortController.signal } : undefined);
|
||||||
if (!response.ok) throw new Error(`Font data request failed with status ${response.status}`);
|
if (!response.ok) throw new Error(`Resource request failed with status ${response.status}`);
|
||||||
const buffer = await response.arrayBuffer();
|
const buffer = await response.arrayBuffer();
|
||||||
const bytes = new Uint8Array(buffer);
|
const bytes = new Uint8Array(buffer);
|
||||||
|
|
||||||
editor.onFontLoad(fontFamily, fontStyle, bytes);
|
editor.onResourceResolved(data.documentId, data.resourceId, bytes);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof DOMException && error.name === "AbortError") return;
|
if (error instanceof DOMException && error.name === "AbortError") return;
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.error("Failed to load font:", error);
|
console.error("Failed to resolve resource:", error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -66,7 +66,7 @@ export function destroyFontsManager() {
|
|||||||
|
|
||||||
abortController?.abort();
|
abortController?.abort();
|
||||||
subscriptions.unsubscribeFrontendMessage("TriggerFontCatalogLoad");
|
subscriptions.unsubscribeFrontendMessage("TriggerFontCatalogLoad");
|
||||||
subscriptions.unsubscribeFrontendMessage("TriggerFontDataLoad");
|
subscriptions.unsubscribeFrontendMessage("TriggerResolveResource");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Self-accepting HMR: tear down the old instance and re-create with the new module's code
|
// Self-accepting HMR: tear down the old instance and re-create with the new module's code
|
||||||
|
|||||||
@@ -20,7 +20,8 @@ use editor::messages::input_mapper::utility_types::input_mouse::{EditorMouseStat
|
|||||||
use editor::messages::layout::utility_types::layout_widget::LayoutTarget;
|
use editor::messages::layout::utility_types::layout_widget::LayoutTarget;
|
||||||
use editor::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
use editor::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||||
use editor::messages::portfolio::document::utility_types::network_interface::ImportOrExport;
|
use editor::messages::portfolio::document::utility_types::network_interface::ImportOrExport;
|
||||||
use editor::messages::portfolio::utility_types::{DockingSplitDirection, FontCatalog, FontCatalogFamily, PanelGroupId, PanelType};
|
use editor::messages::portfolio::fonts::utility_types::{FontCatalog, FontCatalogFamily};
|
||||||
|
use editor::messages::portfolio::utility_types::{DockingSplitDirection, PanelGroupId, PanelType};
|
||||||
use editor::messages::prelude::*;
|
use editor::messages::prelude::*;
|
||||||
use editor::messages::tool::tool_messages::tool_prelude::WidgetId;
|
use editor::messages::tool::tool_messages::tool_prelude::WidgetId;
|
||||||
use graph_craft::document::NodeId;
|
use graph_craft::document::NodeId;
|
||||||
@@ -650,14 +651,19 @@ impl EditorWrapper {
|
|||||||
/// The font catalog has been loaded
|
/// The font catalog has been loaded
|
||||||
#[wasm_bindgen(js_name = onFontCatalogLoad)]
|
#[wasm_bindgen(js_name = onFontCatalogLoad)]
|
||||||
pub fn on_font_catalog_load(&self, catalog: Vec<FontCatalogFamily>) {
|
pub fn on_font_catalog_load(&self, catalog: Vec<FontCatalogFamily>) {
|
||||||
self.dispatch(PortfolioMessage::FontCatalogLoaded { catalog: FontCatalog(catalog) });
|
self.dispatch(FontsMessage::CatalogLoaded { catalog: FontCatalog::from(catalog) });
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A font has been downloaded
|
/// A requested resource has been resolved by the frontend.
|
||||||
#[wasm_bindgen(js_name = onFontLoad)]
|
#[wasm_bindgen(js_name = onResourceResolved)]
|
||||||
pub fn on_font_load(&self, font_family: String, font_style: String, data: Vec<u8>) -> Result<(), JsValue> {
|
pub fn on_resource_resolved(&self, document_id: u64, resource_id: u64, data: Vec<u8>) -> Result<(), JsValue> {
|
||||||
let message = PortfolioMessage::FontLoaded { font_family, font_style, data };
|
self.dispatch(PortfolioMessage::DocumentPassMessage {
|
||||||
self.dispatch(message);
|
document_id: DocumentId(document_id),
|
||||||
|
message: DocumentMessage::Resource(ResourceMessage::Resolved {
|
||||||
|
resource_id: resource_id.into(),
|
||||||
|
data: std::sync::Arc::from(data),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ use graph_craft::graphene_compiler::Compiler;
|
|||||||
use graph_craft::proto::ProtoNetwork;
|
use graph_craft::proto::ProtoNetwork;
|
||||||
use graph_craft::util::load_network;
|
use graph_craft::util::load_network;
|
||||||
use graphene_std::application_io::{ApplicationIo, NodeGraphUpdateMessage, NodeGraphUpdateSender};
|
use graphene_std::application_io::{ApplicationIo, NodeGraphUpdateMessage, NodeGraphUpdateSender};
|
||||||
use graphene_std::text::FontCache;
|
|
||||||
use interpreted_executor::dynamic_executor::DynamicExecutor;
|
use interpreted_executor::dynamic_executor::DynamicExecutor;
|
||||||
use interpreted_executor::util::wrap_network_in_scope;
|
use interpreted_executor::util::wrap_network_in_scope;
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
@@ -134,7 +133,6 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
max_render_region_size: EditorPreferences::default().max_render_region_size,
|
max_render_region_size: EditorPreferences::default().max_render_region_size,
|
||||||
};
|
};
|
||||||
let editor_api = Arc::new(PlatformEditorApi {
|
let editor_api = Arc::new(PlatformEditorApi {
|
||||||
font_cache: FontCache::default(),
|
|
||||||
application_io: Some(application_io_for_api),
|
application_io: Some(application_io_for_api),
|
||||||
node_graph_message_sender: Box::new(UpdateLogger {}),
|
node_graph_message_sender: Box::new(UpdateLogger {}),
|
||||||
editor_preferences: Box::new(preferences),
|
editor_preferences: Box::new(preferences),
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ use std::hash::{Hash, Hasher};
|
|||||||
use std::ptr::addr_of;
|
use std::ptr::addr_of;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use text_nodes::FontCache;
|
|
||||||
use vector_types::vector::style::RenderMode;
|
use vector_types::vector::style::RenderMode;
|
||||||
|
|
||||||
pub use graphene_resource as resource;
|
pub use graphene_resource as resource;
|
||||||
@@ -126,8 +125,6 @@ impl GetEditorPreferences for DummyPreferences {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct EditorApi<Io> {
|
pub struct EditorApi<Io> {
|
||||||
/// Font data (for rendering text) made available to the graph through the `PlatformEditorApi`.
|
|
||||||
pub font_cache: FontCache,
|
|
||||||
/// Gives access to APIs like resources.
|
/// Gives access to APIs like resources.
|
||||||
pub application_io: Option<Arc<Io>>,
|
pub application_io: Option<Arc<Io>>,
|
||||||
pub node_graph_message_sender: Box<dyn NodeGraphUpdateSender + Send + Sync>,
|
pub node_graph_message_sender: Box<dyn NodeGraphUpdateSender + Send + Sync>,
|
||||||
@@ -140,7 +137,6 @@ impl<Io> Eq for EditorApi<Io> {}
|
|||||||
impl<Io: Default> Default for EditorApi<Io> {
|
impl<Io: Default> Default for EditorApi<Io> {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
font_cache: FontCache::default(),
|
|
||||||
application_io: None,
|
application_io: None,
|
||||||
node_graph_message_sender: Box::new(Logger),
|
node_graph_message_sender: Box::new(Logger),
|
||||||
editor_preferences: Box::new(DummyPreferences),
|
editor_preferences: Box::new(DummyPreferences),
|
||||||
@@ -150,7 +146,6 @@ impl<Io: Default> Default for EditorApi<Io> {
|
|||||||
|
|
||||||
impl<Io> Hash for EditorApi<Io> {
|
impl<Io> Hash for EditorApi<Io> {
|
||||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||||
self.font_cache.hash(state);
|
|
||||||
self.application_io.as_ref().map_or(0, |io| io as *const _ as usize).hash(state);
|
self.application_io.as_ref().map_or(0, |io| io as *const _ as usize).hash(state);
|
||||||
(self.node_graph_message_sender.as_ref() as *const dyn NodeGraphUpdateSender).hash(state);
|
(self.node_graph_message_sender.as_ref() as *const dyn NodeGraphUpdateSender).hash(state);
|
||||||
(self.editor_preferences.as_ref() as *const dyn GetEditorPreferences).hash(state);
|
(self.editor_preferences.as_ref() as *const dyn GetEditorPreferences).hash(state);
|
||||||
@@ -165,8 +160,7 @@ impl<Io> core_types::graphene_hash::CacheHash for EditorApi<Io> {
|
|||||||
|
|
||||||
impl<Io> PartialEq for EditorApi<Io> {
|
impl<Io> PartialEq for EditorApi<Io> {
|
||||||
fn eq(&self, other: &Self) -> bool {
|
fn eq(&self, other: &Self) -> bool {
|
||||||
self.font_cache == other.font_cache
|
self.application_io.as_ref().map_or(0, |io| addr_of!(io) as usize) == other.application_io.as_ref().map_or(0, |io| addr_of!(io) as usize)
|
||||||
&& self.application_io.as_ref().map_or(0, |io| addr_of!(io) as usize) == other.application_io.as_ref().map_or(0, |io| addr_of!(io) as usize)
|
|
||||||
&& std::ptr::eq(self.node_graph_message_sender.as_ref() as *const _, other.node_graph_message_sender.as_ref() as *const _)
|
&& std::ptr::eq(self.node_graph_message_sender.as_ref() as *const _, other.node_graph_message_sender.as_ref() as *const _)
|
||||||
&& std::ptr::eq(self.editor_preferences.as_ref() as *const _, other.editor_preferences.as_ref() as *const _)
|
&& std::ptr::eq(self.editor_preferences.as_ref() as *const _, other.editor_preferences.as_ref() as *const _)
|
||||||
}
|
}
|
||||||
@@ -174,7 +168,7 @@ impl<Io> PartialEq for EditorApi<Io> {
|
|||||||
|
|
||||||
impl<T> Debug for EditorApi<T> {
|
impl<T> Debug for EditorApi<T> {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
f.debug_struct("EditorApi").field("font_cache", &self.font_cache).finish()
|
f.debug_struct("EditorApi").finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,10 @@ impl Resource {
|
|||||||
pub fn hash(&self) -> ResourceHash {
|
pub fn hash(&self) -> ResourceHash {
|
||||||
self.hash
|
self.hash
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn empty() -> Self {
|
||||||
|
Self::new([])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<&Resource> for Arc<dyn AsRef<[u8]> + Send + Sync> {
|
impl From<&Resource> for Arc<dyn AsRef<[u8]> + Send + Sync> {
|
||||||
@@ -67,7 +71,7 @@ impl CacheHash for Resource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Blake3 content hash of a resource, represented as 32 bytes
|
/// Blake3 content hash of a resource, represented as 32 bytes
|
||||||
#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq, PartialOrd, Ord, DynAny)]
|
#[derive(Clone, Copy, Default, Hash, PartialEq, Eq, PartialOrd, Ord, DynAny)]
|
||||||
pub struct ResourceHash([u8; 32]);
|
pub struct ResourceHash([u8; 32]);
|
||||||
|
|
||||||
impl From<&[u8]> for ResourceHash {
|
impl From<&[u8]> for ResourceHash {
|
||||||
@@ -135,6 +139,12 @@ impl std::str::FromStr for ResourceHash {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Debug for ResourceHash {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_str(&String::from(self))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum ResourceHashParseError {
|
pub enum ResourceHashParseError {
|
||||||
InvalidLength { found: usize },
|
InvalidLength { found: usize },
|
||||||
@@ -238,6 +248,18 @@ impl ResourceId {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<u64> for ResourceId {
|
||||||
|
fn from(value: u64) -> Self {
|
||||||
|
Self(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ResourceId> for u64 {
|
||||||
|
fn from(id: ResourceId) -> Self {
|
||||||
|
id.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for ResourceId {
|
impl std::fmt::Display for ResourceId {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
write!(f, "{}", self.0)
|
write!(f, "{}", self.0)
|
||||||
|
|||||||
@@ -1,23 +1,21 @@
|
|||||||
use core_types::Ctx;
|
use core_types::Ctx;
|
||||||
use core_types::list::List;
|
use core_types::list::List;
|
||||||
use graph_craft::application_io::PlatformEditorApi;
|
use graph_craft::application_io::resource::Resource;
|
||||||
use graphic_types::Vector;
|
use graphic_types::Vector;
|
||||||
pub use text_nodes::*;
|
pub use text_nodes::*;
|
||||||
|
|
||||||
/// Draws a text string as vector geometry with a choice of font and styling.
|
/// Draws a text string as vector geometry with a choice of font and styling.
|
||||||
#[node_macro::node(category("Text"))]
|
#[node_macro::node(category("Text"))]
|
||||||
fn text<'i: 'n>(
|
fn text(
|
||||||
_: impl Ctx,
|
_: impl Ctx,
|
||||||
/// The Graphite editor's source for global font resources.
|
_primary: (),
|
||||||
#[scope("editor-api")]
|
|
||||||
editor_resources: &'i PlatformEditorApi,
|
|
||||||
/// The text content to be drawn.
|
/// The text content to be drawn.
|
||||||
#[widget(ParsedWidgetOverride::Custom = "text_area")]
|
#[widget(ParsedWidgetOverride::Custom = "text_area")]
|
||||||
#[default("Lorem ipsum")]
|
#[default("Lorem ipsum")]
|
||||||
text: String,
|
text: String,
|
||||||
/// The typeface used to draw the text.
|
/// The loaded font file used to draw the text. The editor resolves the chosen typeface to these bytes via the resource system.
|
||||||
#[widget(ParsedWidgetOverride::Custom = "text_font")]
|
#[widget(ParsedWidgetOverride::Custom = "text_font")]
|
||||||
font: Font,
|
font: Resource,
|
||||||
/// The font size used to draw the text.
|
/// The font size used to draw the text.
|
||||||
#[unit(" px")]
|
#[unit(" px")]
|
||||||
#[default(24.)]
|
#[default(24.)]
|
||||||
@@ -73,5 +71,5 @@ fn text<'i: 'n>(
|
|||||||
align,
|
align,
|
||||||
};
|
};
|
||||||
|
|
||||||
to_path(&text, &font, &editor_resources.font_cache, typesetting, separate_glyphs)
|
to_path(&text, &font, typesetting, separate_glyphs)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ wasm = ["core-types/wasm", "tsify", "wasm-bindgen"]
|
|||||||
# Local dependencies
|
# Local dependencies
|
||||||
core-types = { workspace = true }
|
core-types = { workspace = true }
|
||||||
graphene-hash = { workspace = true }
|
graphene-hash = { workspace = true }
|
||||||
|
graphene-resource = { workspace = true }
|
||||||
raster-types = { workspace = true }
|
raster-types = { workspace = true }
|
||||||
vector-types = { workspace = true }
|
vector-types = { workspace = true }
|
||||||
node-macro = { workspace = true }
|
node-macro = { workspace = true }
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
use core_types::graphene_hash::CacheHash;
|
||||||
|
use dyn_any::DynAny;
|
||||||
|
|
||||||
|
/// A font type (storing font family and font style)
|
||||||
|
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
||||||
|
#[derive(Debug, Clone, Eq, DynAny)]
|
||||||
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||||
|
pub struct Font {
|
||||||
|
#[cfg_attr(feature = "serde", serde(rename = "fontFamily"))]
|
||||||
|
pub font_family: String,
|
||||||
|
#[cfg_attr(feature = "serde", serde(rename = "fontStyle", deserialize_with = "migrate_font_style"))]
|
||||||
|
pub font_style: 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CacheHash for Font {
|
||||||
|
fn cache_hash<H: core::hash::Hasher>(&self, state: &mut H) {
|
||||||
|
self.font_family.cache_hash(state);
|
||||||
|
self.font_style.cache_hash(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for Font {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
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 }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_with_default_style(font_family: String) -> Self {
|
||||||
|
Self::new(font_family, core_types::consts::DEFAULT_FONT_STYLE.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new(core_types::consts::DEFAULT_FONT_FAMILY.into(), core_types::consts::DEFAULT_FONT_STYLE.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Eventually remove this migration document upgrade code
|
||||||
|
fn migrate_font_style<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<String, D::Error> {
|
||||||
|
use serde::Deserialize;
|
||||||
|
String::deserialize(deserializer).map(|name| if name == "Normal (400)" { "Regular (400)".to_string() } else { name })
|
||||||
|
}
|
||||||
@@ -1,142 +0,0 @@
|
|||||||
use core_types::graphene_hash::CacheHash;
|
|
||||||
use dyn_any::DynAny;
|
|
||||||
use parley::fontique::Blob;
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
/// A font type (storing font family and font style and an optional preview URL)
|
|
||||||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
|
||||||
#[derive(Debug, Clone, Eq, DynAny)]
|
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
|
||||||
pub struct Font {
|
|
||||||
#[cfg_attr(feature = "serde", serde(rename = "fontFamily"))]
|
|
||||||
pub font_family: String,
|
|
||||||
#[cfg_attr(feature = "serde", serde(rename = "fontStyle", deserialize_with = "migrate_font_style"))]
|
|
||||||
pub font_style: String,
|
|
||||||
#[cfg_attr(feature = "serde", 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 CacheHash for Font {
|
|
||||||
fn cache_hash<H: core::hash::Hasher>(&self, state: &mut H) {
|
|
||||||
self.font_family.cache_hash(state);
|
|
||||||
self.font_style.cache_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,
|
|
||||||
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 {
|
|
||||||
fn default() -> Self {
|
|
||||||
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, Default, DynAny)]
|
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
|
||||||
pub struct FontCache {
|
|
||||||
/// Actual font file data used for rendering a font
|
|
||||||
font_file_data: HashMap<Font, Vec<u8>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
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<_>>()).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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FontCache {
|
|
||||||
/// Returns the font family name if the font is cached, otherwise returns the fallback font family name if that is cached
|
|
||||||
pub fn resolve_font<'a>(&'a self, font: &'a Font) -> Option<&'a Font> {
|
|
||||||
if self.font_file_data.contains_key(font) {
|
|
||||||
Some(font)
|
|
||||||
} else {
|
|
||||||
self.font_file_data
|
|
||||||
.keys()
|
|
||||||
.find(|font| font.font_family == core_types::consts::DEFAULT_FONT_FAMILY && font.font_style == core_types::consts::DEFAULT_FONT_STYLE)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Try to get the bytes for a font
|
|
||||||
pub fn get<'a>(&'a self, font: &'a Font) -> Option<(&'a Vec<u8>, &'a Font)> {
|
|
||||||
self.resolve_font(font).and_then(|font| self.font_file_data.get(font).map(|data| (data, font)))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get font data as a Blob for use with parley/skrifa
|
|
||||||
pub fn get_blob<'a>(&'a self, font: &'a Font) -> Option<(Blob<u8>, &'a Font)> {
|
|
||||||
self.get(font).map(|(data, font)| (Blob::new(Arc::new(data.clone())), font))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if the font is already loaded
|
|
||||||
pub fn loaded_font(&self, font: &Font) -> bool {
|
|
||||||
self.font_file_data.contains_key(font)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Insert a new font into the cache
|
|
||||||
pub fn insert(&mut self, font: Font, data: Vec<u8>) {
|
|
||||||
self.font_file_data.insert(font.clone(), data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Eventually remove this migration document upgrade code
|
|
||||||
fn migrate_font_style<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<String, D::Error> {
|
|
||||||
use serde::Deserialize;
|
|
||||||
String::deserialize(deserializer).map(|name| if name == "Normal (400)" { "Regular (400)".to_string() } else { name })
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
mod font_cache;
|
mod font;
|
||||||
pub mod json;
|
pub mod json;
|
||||||
mod path_builder;
|
mod path_builder;
|
||||||
pub mod regex;
|
pub mod regex;
|
||||||
@@ -16,7 +16,7 @@ use unicode_segmentation::UnicodeSegmentation;
|
|||||||
|
|
||||||
// Re-export for convenience
|
// Re-export for convenience
|
||||||
pub use core_types as gcore;
|
pub use core_types as gcore;
|
||||||
pub use font_cache::*;
|
pub use font::*;
|
||||||
pub use text_context::TextContext;
|
pub use text_context::TextContext;
|
||||||
pub use to_path::*;
|
pub use to_path::*;
|
||||||
pub use vector_types;
|
pub use vector_types;
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
use super::{Font, FontCache, TypesettingConfig};
|
use super::TypesettingConfig;
|
||||||
use core::cell::RefCell;
|
use core::cell::RefCell;
|
||||||
use core_types::list::List;
|
use core_types::list::List;
|
||||||
use glam::DVec2;
|
use glam::DVec2;
|
||||||
|
use graphene_resource::{Resource, ResourceHash};
|
||||||
use parley::fontique::{Blob, FamilyId, FontInfo};
|
use parley::fontique::{Blob, FamilyId, FontInfo};
|
||||||
use parley::{AlignmentOptions, FontContext, Layout, LayoutContext, LineHeight, PositionedLayoutItem, StyleProperty};
|
use parley::{AlignmentOptions, FontContext, Layout, LayoutContext, LineHeight, PositionedLayoutItem, StyleProperty};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
@@ -19,8 +20,7 @@ thread_local! {
|
|||||||
pub struct TextContext {
|
pub struct TextContext {
|
||||||
font_context: FontContext,
|
font_context: FontContext,
|
||||||
layout_context: LayoutContext<()>,
|
layout_context: LayoutContext<()>,
|
||||||
/// Cached font metadata for performance optimization
|
font_info_cache: HashMap<ResourceHash, (FamilyId, FontInfo)>,
|
||||||
font_info_cache: HashMap<Font, (FamilyId, FontInfo)>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TextContext {
|
impl TextContext {
|
||||||
@@ -32,40 +32,30 @@ impl TextContext {
|
|||||||
THREAD_TEXT.with_borrow_mut(f)
|
THREAD_TEXT.with_borrow_mut(f)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resolve a font and return its data as a Blob if available
|
/// Get or cache font information for the given font resource.
|
||||||
fn resolve_font_data<'a>(&self, font: &'a Font, font_cache: &'a FontCache) -> Option<(Blob<u8>, &'a Font)> {
|
fn get_font_info(&mut self, font: &Resource) -> Option<(String, FontInfo)> {
|
||||||
font_cache.get_blob(font)
|
let hash = font.hash();
|
||||||
}
|
if let Some((family_id, font_info)) = self.font_info_cache.get(&hash)
|
||||||
|
|
||||||
/// Get or cache font information for a given font
|
|
||||||
fn get_font_info(&mut self, font: &Font, font_data: &Blob<u8>) -> Option<(String, FontInfo)> {
|
|
||||||
// Check if we already have the font info cached
|
|
||||||
if let Some((family_id, font_info)) = self.font_info_cache.get(font)
|
|
||||||
&& let Some(family_name) = self.font_context.collection.family_name(*family_id)
|
&& let Some(family_name) = self.font_context.collection.family_name(*family_id)
|
||||||
{
|
{
|
||||||
return Some((family_name.to_string(), font_info.clone()));
|
return Some((family_name.to_string(), font_info.clone()));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register the font and cache the info
|
let families = self.font_context.collection.register_fonts(Blob::new(font.into()), None);
|
||||||
let families = self.font_context.collection.register_fonts(font_data.clone(), None);
|
|
||||||
|
|
||||||
families.first().and_then(|(family_id, fonts_info)| {
|
families.first().and_then(|(family_id, fonts_info)| {
|
||||||
fonts_info.first().and_then(|font_info| {
|
fonts_info.first().and_then(|font_info| {
|
||||||
self.font_context.collection.family_name(*family_id).map(|family_name| {
|
self.font_context.collection.family_name(*family_id).map(|family_name| {
|
||||||
// Cache the font info for future use
|
self.font_info_cache.insert(hash, (*family_id, font_info.clone()));
|
||||||
self.font_info_cache.insert(font.clone(), (*family_id, font_info.clone()));
|
|
||||||
(family_name.to_string(), font_info.clone())
|
(family_name.to_string(), font_info.clone())
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a text layout using the specified font and typesetting configuration
|
/// Create a text layout from the given font resource and typesetting configuration.
|
||||||
fn layout_text(&mut self, text: &str, font: &Font, font_cache: &FontCache, typesetting: TypesettingConfig) -> Option<Layout<()>> {
|
fn layout_text(&mut self, text: &str, font: &Resource, typesetting: TypesettingConfig) -> Option<Layout<()>> {
|
||||||
// Note that the actual_font may not be the desired font if that font is not yet loaded.
|
let (font_family, font_info) = self.get_font_info(font)?;
|
||||||
// It is important not to cache the default font under the name of another font.
|
|
||||||
let (font_data, actual_font) = self.resolve_font_data(font, font_cache)?;
|
|
||||||
let (font_family, font_info) = self.get_font_info(actual_font, &font_data)?;
|
|
||||||
|
|
||||||
const DISPLAY_SCALE: f32 = 1.;
|
const DISPLAY_SCALE: f32 = 1.;
|
||||||
let mut builder = self.layout_context.ranged_builder(&mut self.font_context, text, DISPLAY_SCALE, false);
|
let mut builder = self.layout_context.ranged_builder(&mut self.font_context, text, DISPLAY_SCALE, false);
|
||||||
@@ -89,8 +79,8 @@ impl TextContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Convert text to vector paths using the specified font and typesetting configuration
|
/// Convert text to vector paths using the specified font and typesetting configuration
|
||||||
pub fn to_path(&mut self, text: &str, font: &Font, font_cache: &FontCache, typesetting: TypesettingConfig, per_glyph_items: bool) -> List<Vector> {
|
pub fn to_path(&mut self, text: &str, font: &Resource, typesetting: TypesettingConfig, per_glyph_items: bool) -> List<Vector> {
|
||||||
let Some(layout) = self.layout_text(text, font, font_cache, typesetting) else {
|
let Some(layout) = self.layout_text(text, font, typesetting) else {
|
||||||
return List::new_from_element(Vector::default());
|
return List::new_from_element(Vector::default());
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -162,8 +152,8 @@ impl TextContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Calculate the bounding box of text using the specified font and typesetting configuration
|
/// Calculate the bounding box of text using the specified font and typesetting configuration
|
||||||
pub fn bounding_box(&mut self, text: &str, font: &Font, font_cache: &FontCache, typesetting: TypesettingConfig, for_clipping_test: bool) -> DVec2 {
|
pub fn bounding_box(&mut self, text: &str, font: &Resource, typesetting: TypesettingConfig, for_clipping_test: bool) -> DVec2 {
|
||||||
let Some(layout) = self.layout_text(text, font, font_cache, typesetting) else {
|
let Some(layout) = self.layout_text(text, font, typesetting) else {
|
||||||
return DVec2::ZERO;
|
return DVec2::ZERO;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -181,9 +171,9 @@ impl TextContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Check if text lines are being clipped due to height constraints
|
/// Check if text lines are being clipped due to height constraints
|
||||||
pub fn lines_clipping(&mut self, text: &str, font: &Font, font_cache: &FontCache, typesetting: TypesettingConfig) -> bool {
|
pub fn lines_clipping(&mut self, text: &str, font: &Resource, typesetting: TypesettingConfig) -> bool {
|
||||||
let Some(max_height) = typesetting.max_height else { return false };
|
let Some(max_height) = typesetting.max_height else { return false };
|
||||||
let bounds = self.bounding_box(text, font, font_cache, typesetting, true);
|
let bounds = self.bounding_box(text, font, typesetting, true);
|
||||||
max_height < bounds.y
|
max_height < bounds.y
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,18 @@
|
|||||||
|
use super::TypesettingConfig;
|
||||||
use super::text_context::TextContext;
|
use super::text_context::TextContext;
|
||||||
use super::{Font, FontCache, TypesettingConfig};
|
|
||||||
use core_types::list::List;
|
use core_types::list::List;
|
||||||
use glam::DVec2;
|
use glam::DVec2;
|
||||||
use parley::fontique::Blob;
|
use graphene_resource::Resource;
|
||||||
use std::sync::Arc;
|
|
||||||
use vector_types::Vector;
|
use vector_types::Vector;
|
||||||
|
|
||||||
pub fn to_path(text: &str, font: &Font, font_cache: &FontCache, typesetting: TypesettingConfig, per_glyph_items: bool) -> List<Vector> {
|
pub fn to_path(text: &str, font: &Resource, typesetting: TypesettingConfig, per_glyph_items: bool) -> List<Vector> {
|
||||||
TextContext::with_thread_local(|ctx| ctx.to_path(text, font, font_cache, typesetting, per_glyph_items))
|
TextContext::with_thread_local(|ctx| ctx.to_path(text, font, typesetting, per_glyph_items))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn bounding_box(text: &str, font: &Font, font_cache: &FontCache, typesetting: TypesettingConfig, for_clipping_test: bool) -> DVec2 {
|
pub fn bounding_box(text: &str, font: &Resource, typesetting: TypesettingConfig, for_clipping_test: bool) -> DVec2 {
|
||||||
TextContext::with_thread_local(|ctx| ctx.bounding_box(text, font, font_cache, typesetting, for_clipping_test))
|
TextContext::with_thread_local(|ctx| ctx.bounding_box(text, font, typesetting, for_clipping_test))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn load_font(data: &[u8]) -> Blob<u8> {
|
pub fn lines_clipping(text: &str, font: &Resource, typesetting: TypesettingConfig) -> bool {
|
||||||
Blob::new(Arc::new(data.to_vec()))
|
TextContext::with_thread_local(|ctx| ctx.lines_clipping(text, font, typesetting))
|
||||||
}
|
|
||||||
|
|
||||||
pub fn lines_clipping(text: &str, font: &Font, font_cache: &FontCache, typesetting: TypesettingConfig) -> bool {
|
|
||||||
TextContext::with_thread_local(|ctx| ctx.lines_clipping(text, font, font_cache, typesetting))
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user