diff --git a/editor/src/dispatcher.rs b/editor/src/dispatcher.rs index 229fc2e974..580c0b88e0 100644 --- a/editor/src/dispatcher.rs +++ b/editor/src/dispatcher.rs @@ -65,7 +65,6 @@ const SIDE_EFFECT_FREE_MESSAGES: &[MessageDiscriminant] = &[ ))), MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::SubmitActiveGraphRender), MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::SubmitEyedropperPreviewRender), - MessageDiscriminant::Frontend(FrontendMessageDiscriminant::TriggerFontDataLoad), MessageDiscriminant::Frontend(FrontendMessageDiscriminant::UpdateUIScale), ]; /// 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::Frontend(FrontendMessageDiscriminant::UpdateDocumentLayerStructure), ]; +// FrontendMessages that should be sent immediately +const IMMEDIATE_FRONTEND_MESSAGES: &[FrontendMessageDiscriminant] = &[FrontendMessageDiscriminant::TriggerResolveResource, FrontendMessageDiscriminant::TriggerFontCatalogLoad]; const DEBUG_MESSAGE_BLOCK_LIST: &[MessageDiscriminant] = &[ MessageDiscriminant::Broadcast(BroadcastMessageDiscriminant::TriggerEvent(EventMessageDiscriminant::AnimationFrame)), MessageDiscriminant::Animation(AnimationMessageDiscriminant::IncrementFrameCounter), @@ -205,16 +206,13 @@ impl Dispatcher { self.message_handlers.dialog_message_handler.process_message(message, &mut queue, context); } Message::Frontend(message) => { - // Handle these messages immediately by returning early - if let FrontendMessage::TriggerFontDataLoad { .. } | FrontendMessage::TriggerFontCatalogLoad = message { - self.responses.push(message); - self.cleanup_queues(false); + let decreminant = message.to_discriminant(); + self.responses.push(message); - // 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; - } 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) => { @@ -325,7 +323,7 @@ impl Dispatcher { document_id, document, 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, preferences: &self.message_handlers.preferences_message_handler, viewport: &self.message_handlers.viewport_message_handler, diff --git a/editor/src/messages/frontend/frontend_message.rs b/editor/src/messages/frontend/frontend_message.rs index c7b4489fbb..a99203596d 100644 --- a/editor/src/messages/frontend/frontend_message.rs +++ b/editor/src/messages/frontend/frontend_message.rs @@ -12,10 +12,10 @@ use crate::messages::portfolio::document::utility_types::wires::{WirePath, WireP use crate::messages::portfolio::utility_types::WorkspacePanelLayout; use crate::messages::prelude::*; use crate::messages::tool::tool_messages::eyedropper_tool::PrimarySecondary; +use graph_craft::application_io::resource::ResourceId; use graph_craft::document::NodeId; use graphene_std::color::SRGBA8; use graphene_std::raster::Image; -use graphene_std::text::Font; use graphene_std::vector::style::FillChoiceUI; use std::path::PathBuf; @@ -112,8 +112,11 @@ pub enum FrontendMessage { filename: String, }, TriggerFontCatalogLoad, - TriggerFontDataLoad { - font: Font, + TriggerResolveResource { + #[serde(rename = "documentId")] + document_id: DocumentId, + #[serde(rename = "resourceId")] + resource_id: ResourceId, url: String, }, TriggerPersistenceReadState, diff --git a/editor/src/messages/future/future_message_handler.rs b/editor/src/messages/future/future_message_handler.rs index 9d97fc5894..a7ebdc71d9 100644 --- a/editor/src/messages/future/future_message_handler.rs +++ b/editor/src/messages/future/future_message_handler.rs @@ -43,6 +43,21 @@ impl IntoFuture for MessageFuture { } } +impl From for Message { + fn from(future: MessageFuture) -> Self { + FutureMessage::Await { future }.into() + } +} + +impl From for Message +where + T: Future + Send + 'static, +{ + fn from(future: T) -> Self { + MessageFuture::new(future).into() + } +} + /// Platform-specific async-task executor. /// Runs `future`, sends the resolved message on `results`, then calls `wake`. pub trait MessageSpawner: Send + Sync { diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index 17332fab8f..fed421a143 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -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::misc::{AlignAggregate, AlignAxis, FlipAxis, PTZ}; 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::tool::common_functionality::graph_modification_utils::{self, get_blend_mode, get_fill, get_opacity}; use crate::messages::tool::tool_messages::select_tool::SelectToolPointerKeys; @@ -52,7 +52,6 @@ use std::time::Duration; pub struct DocumentMessageContext<'a> { pub document_id: DocumentId, pub ipp: &'a InputPreprocessorMessageHandler, - pub cached_data: &'a CachedData, pub executor: &'a mut NodeGraphExecutor, pub current_tool: &'a ToolType, pub preferences: &'a PreferencesMessageHandler, @@ -61,6 +60,7 @@ pub struct DocumentMessageContext<'a> { pub properties_panel_open: bool, pub viewport: &'a ViewportMessageHandler, pub resource_storage: &'a ResourceStorageMessageHandler, + pub fonts: &'a FontsMessageHandler, } #[derive(Clone, Debug, serde::Serialize, serde::Deserialize, ExtractField)] @@ -198,7 +198,6 @@ impl MessageHandler> for DocumentMes let DocumentMessageContext { document_id, ipp, - cached_data, executor, viewport, current_tool, @@ -207,6 +206,7 @@ impl MessageHandler> for DocumentMes layers_panel_open, properties_panel_open, resource_storage, + fonts, } = context; match message { @@ -233,11 +233,12 @@ impl MessageHandler> for DocumentMes } DocumentMessage::PropertiesPanel(message) => { let context = PropertiesPanelMessageContext { + executor, network_interface: &mut self.network_interface, + resources: &self.resources, selection_network_path: &self.selection_network_path, document_name: self.name.as_str(), - executor, - cached_data, + fonts, properties_panel_open, }; self.properties_panel_message_handler.process_message(message, responses, context); @@ -282,7 +283,7 @@ impl MessageHandler> for DocumentMes graph_operation_message_handler.process_message(message, responses, context); } DocumentMessage::Resource(message) => { - let context = ResourceMessageContext {}; + let context = ResourceMessageContext { document_id, fonts }; self.resources.process_message(message, responses, context); } DocumentMessage::AlignSelectedLayers { axis, aggregate } => { @@ -933,21 +934,19 @@ impl MessageHandler> for DocumentMes let mut document = self.clone(); let resources_load_handle = resource_storage.resources(); - responses.add(FutureMessage::Await { - future: MessageFuture::new(async move { - document.resources.garbage_collect(document.used_resources(false).as_ref()); - document.resources.embed_resources(resources_load_handle).await; + responses.add(async move { + document.resources.collect_garbage(document.used_resources(false).as_ref()); + 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 { - document_id, - name, - path, - folder, - content, - }) - }), + Message::Frontend(FrontendMessage::TriggerSaveDocument { + document_id, + name, + path, + folder, + content, + }) }); } 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) { - 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, animation_is_playing: bool, time: Duration) { let mut snapping_state = 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) { 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]> { diff --git a/editor/src/messages/portfolio/document/graph_operation/utility_types.rs b/editor/src/messages/portfolio/document/graph_operation/utility_types.rs index 2938942cb6..202bb7ba9e 100644 --- a/editor/src/messages/portfolio/document/graph_operation/utility_types.rs +++ b/editor/src/messages/portfolio/document/graph_operation/utility_types.rs @@ -249,12 +249,13 @@ impl<'a> ModifyInputsContext<'a> { } 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) .expect("Text node does not exist") .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::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.line_height_ratio), 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.tilt), 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) .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.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(); self.network_interface.insert_node(transform_id, transform, &[]); self.network_interface.move_node_to_chain_start(&transform_id, layer, &[], self.import); diff --git a/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs b/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs index f62db25f8c..f3912ecdb5 100644 --- a/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs +++ b/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs @@ -8,8 +8,7 @@ use crate::messages::portfolio::document::utility_types::network_interface::{ DocumentNodeMetadata, DocumentNodePersistentMetadata, InputMetadata, NodeNetworkInterface, NodeNetworkMetadata, NodeNetworkPersistentMetadata, NodeTemplate, NodeTypePersistentMetadata, Vec2InputSettings, WidgetOverride, }; -use crate::messages::portfolio::utility_types::CachedData; -use crate::messages::prelude::Message; +use crate::messages::prelude::{FontsMessageHandler, Message, ResourceMessageHandler}; use crate::node_graph_executor::NodeGraphExecutor; use glam::DVec2; use graph_craft::ProtoNodeIdentifier; @@ -28,10 +27,11 @@ use serde_json::Value; use std::collections::{HashMap, VecDeque}; pub struct NodePropertiesContext<'a> { - pub cached_data: &'a CachedData, pub responses: &'a mut VecDeque, pub executor: &'a mut NodeGraphExecutor, pub network_interface: &'a mut NodeNetworkInterface, + pub resources: &'a ResourceMessageHandler, + pub fonts: &'a FontsMessageHandler, pub selection_network_path: &'a [NodeId], pub document_name: &'a str, } diff --git a/editor/src/messages/portfolio/document/node_graph/node_properties.rs b/editor/src/messages/portfolio/document/node_graph/node_properties.rs index a58576c408..01cc4fc8bb 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_properties.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_properties.rs @@ -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::utility_types::document_metadata::LayerNodeIdentifier; 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::tool::common_functionality::graph_modification_utils; use choice::enum_choice; use dyn_any::DynAny; use glam::{DAffine2, DVec2}; +use graph_craft::application_io::resource::ResourceId; use graph_craft::document::value::TaggedValue; use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeId, NodeInput}; 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, Option>) { + 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 { - cached_data, document_node, node_id, index, + resources, + fonts, .. } = parameter_widgets_info; @@ -839,66 +856,32 @@ pub fn font_inputs(parameter_widgets_info: ParameterWidgetsInfo) -> (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) .widget_instance(), ]); @@ -908,32 +891,18 @@ pub fn font_inputs(parameter_widgets_info: ParameterWidgetsInfo) -> (Vec (Vec } pub struct ParameterWidgetsInfo<'a> { - cached_data: &'a CachedData, network_interface: &'a NodeNetworkInterface, + resources: &'a ResourceMessageHandler, selection_network_path: &'a [NodeId], document_node: Option<&'a DocumentNode>, node_id: NodeId, @@ -2824,6 +2792,7 @@ pub struct ParameterWidgetsInfo<'a> { input_type: FrontendGraphDataType, blank_assist: bool, exposable: bool, + fonts: &'a FontsMessageHandler, } 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); ParameterWidgetsInfo { - cached_data: context.cached_data, network_interface: context.network_interface, + resources: context.resources, selection_network_path: context.selection_network_path, + fonts: context.fonts, document_node, node_id, index, diff --git a/editor/src/messages/portfolio/document/overlays/utility_functions.rs b/editor/src/messages/portfolio/document/overlays/utility_functions.rs index cdc6968ee5..72b6a33c99 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_functions.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_functions.rs @@ -2,11 +2,12 @@ use super::utility_types::{DrawHandles, OverlayContext}; use crate::consts::HIDE_HANDLE_DISTANCE; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; use crate::messages::portfolio::document::utility_types::network_interface::NodeNetworkInterface; +use crate::messages::portfolio::fonts::FALLBACK_FONT_BLOB; use crate::messages::tool::common_functionality::shape_editor::{SelectedLayerState, ShapeState}; use crate::messages::tool::tool_messages::tool_prelude::DocumentMessageHandler; use glam::{DAffine2, DVec2}; 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::{PointId, SegmentId, Vector}; 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 = 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> = LazyLock::new(|| Mutex::new(TextContext::default())); 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, }; - // 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 bounds = text_context.bounding_box(text, &font, &GLOBAL_FONT_CACHE, typesetting, false); + let bounds = text_context.bounding_box(text, &FALLBACK_FONT_BLOB, typesetting, false); bounds.x } diff --git a/editor/src/messages/portfolio/document/overlays/utility_types_native.rs b/editor/src/messages/portfolio/document/overlays/utility_types_native.rs index acda35d859..4380024309 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types_native.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types_native.rs @@ -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, 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::fonts::FALLBACK_FONT_BLOB; use crate::messages::prelude::Message; use crate::messages::prelude::ViewportMessageHandler; use core::borrow::Borrow; @@ -14,7 +15,7 @@ use graphene_std::ATTR_TRANSFORM; use graphene_std::list::List; use graphene_std::math::quad::Quad; 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::misc::point_to_dvec2; use graphene_std::vector::{PointId, SegmentId, Vector}; @@ -1115,21 +1116,16 @@ impl OverlayContextInternal { 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 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_BLOB, typesetting, false); let text_width = text_size.x; let text_height = text_size.y; // Create a rect from the size (assuming text starts at origin) let text_bounds = kurbo::Rect::new(0., 0., text_width, text_height); // 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_BLOB, typesetting, false); // Calculate position based on pivot let mut position = DVec2::ZERO; diff --git a/editor/src/messages/portfolio/document/properties_panel/properties_panel_message_handler.rs b/editor/src/messages/portfolio/document/properties_panel/properties_panel_message_handler.rs index 8e8052d5a3..e62de328c5 100644 --- a/editor/src/messages/portfolio/document/properties_panel/properties_panel_message_handler.rs +++ b/editor/src/messages/portfolio/document/properties_panel/properties_panel_message_handler.rs @@ -3,17 +3,17 @@ use graphene_std::uuid::NodeId; use crate::messages::layout::utility_types::widget_prelude::*; 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::utility_types::CachedData; use crate::messages::prelude::*; use crate::node_graph_executor::NodeGraphExecutor; #[derive(ExtractField)] pub struct PropertiesPanelMessageContext<'a> { + pub executor: &'a mut NodeGraphExecutor, pub network_interface: &'a mut NodeNetworkInterface, + pub resources: &'a ResourceMessageHandler, pub selection_network_path: &'a [NodeId], pub document_name: &'a str, - pub executor: &'a mut NodeGraphExecutor, - pub cached_data: &'a CachedData, + pub fonts: &'a FontsMessageHandler, pub properties_panel_open: bool, } @@ -24,11 +24,12 @@ pub struct PropertiesPanelMessageHandler {} impl MessageHandler> for PropertiesPanelMessageHandler { fn process_message(&mut self, message: PropertiesPanelMessage, responses: &mut VecDeque, context: PropertiesPanelMessageContext) { let PropertiesPanelMessageContext { + executor, network_interface, + resources, selection_network_path, document_name, - executor, - cached_data, + fonts, properties_panel_open, } = context; @@ -46,12 +47,13 @@ impl MessageHandler> f } let mut node_properties_context = NodePropertiesContext { - cached_data, responses, + executor, network_interface, + resources, selection_network_path, document_name, - executor, + fonts, }; let layout = Layout(NodeGraphMessageHandler::collate_properties(&mut node_properties_context)); diff --git a/editor/src/messages/portfolio/document/resource/resource_message.rs b/editor/src/messages/portfolio/document/resource/resource_message.rs index e586e5cd74..4df3dfd5f7 100644 --- a/editor/src/messages/portfolio/document/resource/resource_message.rs +++ b/editor/src/messages/portfolio/document/resource/resource_message.rs @@ -1,9 +1,14 @@ use crate::messages::prelude::*; use graph_craft::application_io::resource::ResourceId; +use graphene_std::text::Font; use std::sync::Arc; #[impl_message(Message, DocumentMessage, Resource)] #[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)] pub enum ResourceMessage { 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]> }, } diff --git a/editor/src/messages/portfolio/document/resource/resource_message_handler.rs b/editor/src/messages/portfolio/document/resource/resource_message_handler.rs index bc233bda5d..d7cad368c5 100644 --- a/editor/src/messages/portfolio/document/resource/resource_message_handler.rs +++ b/editor/src/messages/portfolio/document/resource/resource_message_handler.rs @@ -3,25 +3,142 @@ use crate::messages::prelude::*; use base64::Engine; use base64::engine::general_purpose::STANDARD as BASE64; use graph_craft::application_io::resource::{DataSource, LoadResource, Resource, ResourceHash, ResourceId, ResourceRegistry}; +use graphene_std::text::Font; #[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)] pub struct ResourceMessageHandler { pub registry: ResourceRegistry, pub embedded: EmbeddedResources, + #[serde(skip)] + pending_resolves: HashMap>, +} + +#[derive(Debug, Clone, PartialEq)] +struct ResolveProgress { + index: usize, + source: DataSource, } #[message_handler_data] -impl MessageHandler for ResourceMessageHandler { - fn process_message(&mut self, message: ResourceMessage, responses: &mut VecDeque, _context: ResourceMessageContext) { +impl MessageHandler> for ResourceMessageHandler { + fn process_message(&mut self, message: ResourceMessage, responses: &mut VecDeque, context: ResourceMessageContext) { + let ResourceMessageContext { document_id, fonts } = context; + match message { ResourceMessage::StoreEmbedded { resource_id, data } => { let hash = ResourceHash::from(data.as_ref()); self.registry.push_source_back(&resource_id, DataSource::Embedded); self.registry.resolve(&resource_id, hash); 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::Font { + family: font.font_family, + style: Some(style_name), + }, + ); + responses.add(ResourceMessage::Resolve); + } + ResourceMessage::Resolve => { + let unresolved_ids: Vec = 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 => { + log::error!("Resource {resource_id} is embedded but failed to resolve before reaching ResolveStep"); + } + 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); + } + } + } + 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 { + log::error!("Resolved message for {resource_id} with no registry entry"); + 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 +171,7 @@ impl ResourceMessageHandler { 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::::from_iter(used.iter().cloned()); let unused = self.registry.ids().filter(|id| !used.contains(id)).collect::>(); unused.into_iter().for_each(|id| { diff --git a/editor/src/messages/portfolio/document_migration.rs b/editor/src/messages/portfolio/document_migration.rs index e1985069ce..6d2d5f6922 100644 --- a/editor/src/messages/portfolio/document_migration.rs +++ b/editor/src/messages/portfolio/document_migration.rs @@ -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 { let mut node_template = resolve_document_node_type(&reference)?.default_node_template(); document.network_interface.replace_implementation(node_id, network_path, &mut node_template); diff --git a/editor/src/messages/portfolio/fonts/fallback.rs b/editor/src/messages/portfolio/fonts/fallback.rs new file mode 100644 index 0000000000..4e8bdacd71 --- /dev/null +++ b/editor/src/messages/portfolio/fonts/fallback.rs @@ -0,0 +1,5 @@ +use graphene_std::text::Blob; +use std::sync::{Arc, LazyLock}; + +const FALLBACK_FONT_BYTES: &[u8] = include_bytes!("source-sans-pro-regular.ttf"); +pub static FALLBACK_FONT_BLOB: LazyLock> = LazyLock::new(|| Blob::new(Arc::new(FALLBACK_FONT_BYTES.to_vec()))); diff --git a/editor/src/messages/portfolio/fonts/fonts_message.rs b/editor/src/messages/portfolio/fonts/fonts_message.rs new file mode 100644 index 0000000000..e192f609fe --- /dev/null +++ b/editor/src/messages/portfolio/fonts/fonts_message.rs @@ -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, + }, + Cached { + hash: ResourceHash, + #[serde(skip, default = "Resource::empty")] + resource: Resource, + }, +} diff --git a/editor/src/messages/portfolio/fonts/fonts_message_handler.rs b/editor/src/messages/portfolio/fonts/fonts_message_handler.rs new file mode 100644 index 0000000000..9041fff3d6 --- /dev/null +++ b/editor/src/messages/portfolio/fonts/fonts_message_handler.rs @@ -0,0 +1,108 @@ +use crate::messages::portfolio::fonts::FALLBACK_FONT_BLOB; +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::{Blob, 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_data: HashMap, +} + +#[message_handler_data] +impl MessageHandler> for FontsMessageHandler { + fn process_message(&mut self, message: FontsMessage, responses: &mut VecDeque, context: FontsMessageContext) { + let FontsMessageContext { resource_storage } = context; + + match message { + FontsMessage::CatalogLoaded { catalog } => { + self.font_catalog = catalog; + responses.add(PortfolioMessage::ResolveResources); + } + FontsMessage::ResourceResolved { font, hash } => { + 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 { + self.font_hashes.get(font).copied() + } + + pub fn cached_url(&self, font: &Font) -> Option { + self.font_catalog.download_url(font) + } + + pub fn get_blob_or_queue_load(&self, font: &Font, responses: &mut VecDeque) -> Blob { + if let Some(hash) = self.font_hashes.get(font) { + if let Some(resource) = self.font_data.get(hash) { + return Blob::new(resource.into()); + } + responses.add(FontsMessage::Load { + font: font.clone(), + response: Message::NoOp.into(), + }); + } + FALLBACK_FONT_BLOB.clone() + } + + pub fn id_font(&self, resources: &ResourceMessageHandler, resource_id: ResourceId) -> Option { + 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 + '_ { + 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, + } + } +} diff --git a/editor/src/messages/portfolio/fonts/mod.rs b/editor/src/messages/portfolio/fonts/mod.rs new file mode 100644 index 0000000000..a872292096 --- /dev/null +++ b/editor/src/messages/portfolio/fonts/mod.rs @@ -0,0 +1,12 @@ +mod fallback; +mod fonts_message; +mod fonts_message_handler; + +pub mod utility_types; + +#[doc(inline)] +pub use fallback::FALLBACK_FONT_BLOB; +#[doc(inline)] +pub use fonts_message::{FontsMessage, FontsMessageDiscriminant}; +#[doc(inline)] +pub use fonts_message_handler::{FontsMessageContext, FontsMessageHandler}; diff --git a/editor/src/messages/portfolio/document/overlays/source-sans-pro-regular.ttf b/editor/src/messages/portfolio/fonts/source-sans-pro-regular.ttf similarity index 100% rename from editor/src/messages/portfolio/document/overlays/source-sans-pro-regular.ttf rename to editor/src/messages/portfolio/fonts/source-sans-pro-regular.ttf diff --git a/editor/src/messages/portfolio/fonts/utility_types.rs b/editor/src/messages/portfolio/fonts/utility_types.rs new file mode 100644 index 0000000000..bc1a12265f --- /dev/null +++ b/editor/src/messages/portfolio/fonts/utility_types.rs @@ -0,0 +1,98 @@ +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); + +impl FontCatalog { + pub fn find_font_style_in_catalog(&self, font: &Font) -> Option { + 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 { + 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 { + self.0.iter() + } + + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } +} + +impl From> for FontCatalog { + fn from(value: Vec) -> 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, +} + +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) -> FontCatalogStyle { + let weight = named_style.split_terminator(['(', ')']).next_back().and_then(|x| x.parse::().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 { + 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}") + } +} diff --git a/editor/src/messages/portfolio/mod.rs b/editor/src/messages/portfolio/mod.rs index f0f8c442c5..5ea092088b 100644 --- a/editor/src/messages/portfolio/mod.rs +++ b/editor/src/messages/portfolio/mod.rs @@ -3,9 +3,12 @@ mod portfolio_message_handler; pub mod document; pub mod document_migration; +pub mod fonts; pub mod persistent_state; pub mod utility_types; +#[doc(inline)] +pub use fonts::{FontsMessage, FontsMessageContext, FontsMessageHandler}; #[doc(inline)] pub use persistent_state::{PersistentStateMessage, PersistentStateMessageContext, PersistentStateMessageHandler}; #[doc(inline)] diff --git a/editor/src/messages/portfolio/portfolio_message.rs b/editor/src/messages/portfolio/portfolio_message.rs index 2b42a972b0..596afae458 100644 --- a/editor/src/messages/portfolio/portfolio_message.rs +++ b/editor/src/messages/portfolio/portfolio_message.rs @@ -3,11 +3,9 @@ use super::persistent_state::PersistentStateMessage; use super::utility_types::{DockingSplitDirection, PanelGroupId, PanelType}; use crate::messages::frontend::utility_types::{ExportBounds, FileType, PersistedState}; use crate::messages::portfolio::document::utility_types::clipboards::Clipboard; -use crate::messages::portfolio::utility_types::FontCatalog; use crate::messages::prelude::*; use graphene_std::Color; use graphene_std::raster::Image; -use graphene_std::text::Font; use std::path::PathBuf; #[impl_message(Message, Portfolio)] @@ -17,6 +15,8 @@ pub enum PortfolioMessage { #[child] Document(DocumentMessage), #[child] + Fonts(FontsMessage), + #[child] PersistentState(PersistentStateMessage), // Messages @@ -51,20 +51,10 @@ pub enum PortfolioMessage { DestroyAllDocuments, EditorPreferences, GarbageCollectResources, - FontCatalogLoaded { - catalog: FontCatalog, - }, - LoadFontData { - font: Font, - }, - FontLoaded { - font_family: String, - font_style: String, - data: Vec, - }, - LoadDocumentResources { + ResolveDocumentResources { document_id: DocumentId, }, + ResolveResources, LoadPersistedState { state: PersistedState, }, diff --git a/editor/src/messages/portfolio/portfolio_message_handler.rs b/editor/src/messages/portfolio/portfolio_message_handler.rs index 5cc240823f..e3f7bba174 100644 --- a/editor/src/messages/portfolio/portfolio_message_handler.rs +++ b/editor/src/messages/portfolio/portfolio_message_handler.rs @@ -1,7 +1,7 @@ use super::document::utility_types::document_metadata::LayerNodeIdentifier; use super::document::utility_types::network_interface; 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::consts::{DEFAULT_DOCUMENT_NAME, DEFAULT_STROKE_WIDTH, FILE_EXTENSION}; use crate::messages::animation::TimingInformation; @@ -32,7 +32,6 @@ use graphene_std::Color; use graphene_std::raster_types::Image; use graphene_std::renderer::Quad; use graphene_std::subpath::BezierHandles; -use graphene_std::text::Font; use graphene_std::vector::misc::HandleId; use graphene_std::vector::{PointId, SegmentId, Vector, VectorModificationType}; use std::path::PathBuf; @@ -68,7 +67,7 @@ pub struct PortfolioMessageHandler { document_ids: VecDeque, pub(crate) active_document_id: Option, persistent_state: PersistentStateMessageHandler, - pub cached_data: CachedData, + pub fonts: FontsMessageHandler, copy_buffer: [Vec; INTERNAL_CLIPBOARD_COUNT as usize], pub executor: NodeGraphExecutor, pub selection_mode: SelectionMode, @@ -99,7 +98,7 @@ impl MessageHandler> for Portfolio let document_inputs = DocumentMessageContext { document_id, ipp, - cached_data: &self.cached_data, + fonts: &self.fonts, executor: &mut self.executor, current_tool, preferences, @@ -118,6 +117,10 @@ impl MessageHandler> for Portfolio }; self.persistent_state.process_message(message, responses, context); } + PortfolioMessage::Fonts(message) => { + let context = FontsMessageContext { resource_storage }; + self.fonts.process_message(message, responses, context); + } // Messages PortfolioMessage::Init => { @@ -169,7 +172,7 @@ impl MessageHandler> for Portfolio let document_inputs = DocumentMessageContext { document_id, ipp, - cached_data: &self.cached_data, + fonts: &self.fonts, executor: &mut self.executor, current_tool, preferences, @@ -386,74 +389,6 @@ impl MessageHandler> for Portfolio responses.add(MenuBarMessage::SendLayout); 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::GarbageCollectResources => { let mut used_resources = HashSet::new(); @@ -469,21 +404,29 @@ impl MessageHandler> for Portfolio document.garbage_collect_resources(); used_resources.extend(document.resources.registry.resolved().filter_map(|info| info.hash.cloned())); } + // Fonts loaded earlier in the session may not be referenced by any current document but still need + // to survive — the picker can re-assign them at any moment, and re-fetching would cause a render + // blank-out while the bytes come back from the network. + used_resources.extend(self.fonts.used_resources()); responses.add(ResourceStorageMessage::GarbageCollect { used: Vec::from_iter(used_resources).into_boxed_slice(), }); } - PortfolioMessage::LoadDocumentResources { document_id } => { - let catalog = &self.cached_data.font_catalog; - - if catalog.0.is_empty() { + PortfolioMessage::ResolveResources => { + for document_id in self.document_ids.iter().copied().collect::>() { + responses.add(PortfolioMessage::ResolveDocumentResources { document_id }); + } + } + PortfolioMessage::ResolveDocumentResources { document_id } => { + if self.fonts.font_catalog.is_empty() { responses.add_front(FrontendMessage::TriggerFontCatalogLoad); return; } - if let Some(document) = self.documents.get_mut(&document_id) { - document.load_layer_resources(responses); - } + responses.add(PortfolioMessage::DocumentPassMessage { + document_id, + message: DocumentMessage::Resource(ResourceMessage::Resolve), + }); } PortfolioMessage::LoadPersistedState { state } => { if let Some(layout) = state.workspace_layout { @@ -884,6 +827,7 @@ impl MessageHandler> for Portfolio let mut document = match document { Ok(document) => document, Err(e) => { + log::error!("{e}"); // TODO: Eventually remove this document upgrade code // TODO: (Only the `if` branch, the `else` branch's manual-open dialog stays) if document_is_auto_saved { @@ -908,7 +852,6 @@ impl MessageHandler> for Portfolio responses.add(PortfolioMessage::UpdateOpenDocumentsList); self.tick_autosave_load_progress(responses, true); } else { - log::error!("{e}"); let name = document_name .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())) @@ -1134,7 +1077,6 @@ impl MessageHandler> for Portfolio added_nodes = true; } - document.load_layer_resources(responses); let new_ids: HashMap<_, _> = entry.nodes.iter().map(|(id, _)| (*id, NodeId::new())).collect(); let layer = LayerNodeIdentifier::new_unchecked(new_ids[&NodeId(0)]); all_new_ids.extend(new_ids.values().cloned()); @@ -1619,7 +1561,7 @@ impl MessageHandler> for Portfolio }; if !document.is_loaded { document.is_loaded = true; - responses.add(PortfolioMessage::LoadDocumentResources { document_id }); + responses.add(PortfolioMessage::ResolveDocumentResources { document_id }); responses.add(PortfolioMessage::UpdateDocumentWidgets); responses.add(PropertiesPanelMessage::Clear); } @@ -1665,6 +1607,13 @@ impl MessageHandler> for Portfolio 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 .navigation_handler .calculate_offset_transform(viewport.center_in_viewport_space().into(), &document.document_ptz); diff --git a/editor/src/messages/portfolio/utility_types.rs b/editor/src/messages/portfolio/utility_types.rs index ebb39c5c96..3e3f464ec0 100644 --- a/editor/src/messages/portfolio/utility_types.rs +++ b/editor/src/messages/portfolio/utility_types.rs @@ -1,95 +1,11 @@ use graphene_std::Color; 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. const DOCUMENT_PANEL_SHARE: f64 = 0.8; /// Proportional share for each side when neither (or both) contain the document panel. 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); - -impl FontCatalog { - pub fn find_font_style_in_catalog(&self, font: &Font) -> Option { - 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, -} - -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) -> FontCatalogStyle { - let weight = named_style.split_terminator(['(', ')']).next_back().and_then(|x| x.parse::().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 { - 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))] #[derive(PartialEq, Eq, Hash, Clone, Copy, Debug, serde::Serialize, serde::Deserialize)] pub enum PanelType { diff --git a/editor/src/messages/prelude.rs b/editor/src/messages/prelude.rs index 3ef31b2174..2f9971da83 100644 --- a/editor/src/messages/prelude.rs +++ b/editor/src/messages/prelude.rs @@ -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::resource::{ResourceMessage, ResourceMessageContext, ResourceMessageDiscriminant, ResourceMessageHandler}; 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::{PortfolioMessage, PortfolioMessageContext, PortfolioMessageDiscriminant, PortfolioMessageHandler}; pub use crate::messages::preferences::{PreferencesMessage, PreferencesMessageDiscriminant, PreferencesMessageHandler}; diff --git a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs index e658f7846a..56ddf80297 100644 --- a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs +++ b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs @@ -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)) } -/// Gets properties from the Text node -pub fn get_text(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<(&String, &Font, TypesettingConfig, bool)> { +/// 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<'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 Some(TaggedValue::String(text)) = &inputs[graphene_std::text::text::TextInput::INDEX].as_value() else { return None; }; - let Some(TaggedValue::Font(font)) = &inputs[graphene_std::text::text::FontInput::INDEX].as_value() else { - return None; + let font = match &inputs[graphene_std::text::text::FontInput::INDEX].as_value() { + 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 { return None; diff --git a/editor/src/messages/tool/common_functionality/utility_functions.rs b/editor/src/messages/tool/common_functionality/utility_functions.rs index 567672498c..17f77ff939 100644 --- a/editor/src/messages/tool/common_functionality/utility_functions.rs +++ b/editor/src/messages/tool/common_functionality/utility_functions.rs @@ -16,7 +16,6 @@ use graph_craft::document::value::TaggedValue; use graphene_std::list::List; use graphene_std::renderer::Quad; 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::misc::{HandleId, ManipulatorPointId, dvec2_to_point}; 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. -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) -> Quad { // 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) { return frame * Quad::from_box([DVec2::ZERO, DVec2::ONE]); } // 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]); }; - let far = graphene_std::text::bounding_box(text, font, font_cache, typesetting, false); + let blob = fonts.get_blob_or_queue_load(&font, responses); + let far = graphene_std::text::bounding_box(text, &blob, typesetting, false); Quad::from_box([DVec2::ZERO, far]) } diff --git a/editor/src/messages/tool/tool_message_handler.rs b/editor/src/messages/tool/tool_message_handler.rs index 7f8577d500..c3cc9d8d84 100644 --- a/editor/src/messages/tool/tool_message_handler.rs +++ b/editor/src/messages/tool/tool_message_handler.rs @@ -4,7 +4,6 @@ use super::utility_types::{ToolActionMessageContext, ToolFsmState, tool_message_ use crate::application::generate_uuid; use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::portfolio::document::overlays::utility_types::OverlayProvider; -use crate::messages::portfolio::utility_types::CachedData; use crate::messages::prelude::*; use crate::messages::tool::transform_layer::transform_layer_message_handler::TransformLayerMessageContext; use crate::messages::tool::utility_types::{HintData, ToolType}; @@ -19,7 +18,7 @@ pub struct ToolMessageContext<'a> { pub document_id: DocumentId, pub document: &'a mut DocumentMessageHandler, pub input: &'a InputPreprocessorMessageHandler, - pub cached_data: &'a CachedData, + pub fonts: &'a FontsMessageHandler, pub node_graph: &'a NodeGraphExecutor, pub preferences: &'a PreferencesMessageHandler, pub viewport: &'a ViewportMessageHandler, @@ -40,7 +39,7 @@ impl MessageHandler> for ToolMessageHandler document_id, document, input, - cached_data, + fonts, node_graph, preferences, viewport, @@ -127,7 +126,7 @@ impl MessageHandler> for ToolMessageHandler node_graph, preferences, viewport, - cached_data, + fonts, }; if let Some(tool_abort_message) = tool.event_to_message_map().tool_abort { @@ -218,7 +217,7 @@ impl MessageHandler> for ToolMessageHandler tool_data.tools.get(active_tool).unwrap().activate(responses); // 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 tool_data.send_layout(responses, LayoutTarget::ToolShelf, preferences.brush_tool); @@ -235,7 +234,7 @@ impl MessageHandler> for ToolMessageHandler node_graph, preferences, viewport, - cached_data, + fonts, }; // Set initial hints and cursor @@ -259,7 +258,7 @@ impl MessageHandler> for ToolMessageHandler ToolMessage::RefreshToolOptions => { 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 => { let tool_data = &mut self.tool_state.tool_data; @@ -347,7 +346,7 @@ impl MessageHandler> for ToolMessageHandler node_graph, preferences, viewport, - cached_data, + fonts, }; if matches!(tool_message, ToolMessage::UpdateHints) { if graph_view_overlay_open { diff --git a/editor/src/messages/tool/tool_messages/select_tool.rs b/editor/src/messages/tool/tool_messages/select_tool.rs index c46643a5b4..0bb01f9000 100644 --- a/editor/src/messages/tool/tool_messages/select_tool.rs +++ b/editor/src/messages/tool/tool_messages/select_tool.rs @@ -720,13 +720,7 @@ impl Fsm for SelectToolFsmState { type ToolOptions = (); fn transition(self, event: ToolMessage, tool_data: &mut Self::ToolData, tool_action_data: &mut ToolActionMessageContext, _tool_options: &(), responses: &mut VecDeque) -> Self { - let ToolActionMessageContext { - document, - input, - viewport, - cached_data, - .. - } = tool_action_data; + let ToolActionMessageContext { document, input, viewport, fonts, .. } = tool_action_data; let ToolMessage::Select(event) = event else { return self }; match (self, event) { @@ -751,7 +745,7 @@ impl Fsm for SelectToolFsmState { if document.metadata().is_text_layer(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); } } diff --git a/editor/src/messages/tool/tool_messages/text_tool.rs b/editor/src/messages/tool/tool_messages/text_tool.rs index 7c94b8d82c..5e77dc5058 100644 --- a/editor/src/messages/tool/tool_messages/text_tool.rs +++ b/editor/src/messages/tool/tool_messages/text_tool.rs @@ -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::utility_types::document_metadata::LayerNodeIdentifier; 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::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, @@ -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::utility_functions::text_bounding_box; use crate::messages::tool::utility_types::ToolRefreshOptions; +use graph_craft::application_io::resource::ResourceId; use graph_craft::document::value::TaggedValue; use graph_craft::document::{NodeId, NodeInput}; use graphene_std::choice_type::ChoiceTypeStatic; use graphene_std::color::SRGBA8; 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::{Color, NodeInputDecleration}; @@ -105,31 +106,30 @@ impl ToolMetadata for TextTool { } fn create_text_widgets(tool: &TextTool, font_catalog: &FontCatalog, document: &DocumentMessageHandler) -> Vec { - // 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 font_input_index = graphene_std::text::text::FontInput::INDEX; - let apply_font = move |new_font: Font| -> Message { + let apply_font = move |font: Font| -> Message { match text_node_id { - Some(node_id) => NodeGraphMessage::SetInputValue { - node_id, - input_index: font_input_index, - value: TaggedValue::Font(new_font), + Some(node_id) => { + 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(), + ]), + } } - .into(), None => TextToolMessage::UpdateOptions { - options: TextOptionsUpdate::Font { font: new_font }, + options: TextOptionsUpdate::Font { font }, } .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 { match text_node_id { Some(_) => DeferMessage::AfterGraphRun { @@ -142,35 +142,27 @@ fn create_text_widgets(tool: &TextTool, font_catalog: &FontCatalog, document: &D let font = DropdownInput::new(vec![ font_catalog - .0 .iter() .map(|family| { 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 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()) .label(family.name.clone()) .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())) }) .collect::>(), ]) - .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) .widget_instance(); let style = DropdownInput::new({ font_catalog - .0 .iter() .find(|family| family.name == tool.options.font.font_family) .map(|family| { @@ -182,7 +174,7 @@ fn create_text_widgets(tool: &TextTool, font_catalog: &FontCatalog, document: &D MenuListEntry::new(font_style.clone()) .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())) }; @@ -196,7 +188,6 @@ fn create_text_widgets(tool: &TextTool, font_catalog: &FontCatalog, document: &D }) .selected_index( font_catalog - .0 .iter() .find(|family| family.name == tool.options.font.font_family) .and_then(|family| { @@ -253,7 +244,7 @@ fn create_text_widgets(tool: &TextTool, font_catalog: &FontCatalog, document: &D } impl ToolRefreshOptions for TextTool { - fn refresh_options(&self, responses: &mut VecDeque, _cached_data: &CachedData) { + fn refresh_options(&self, responses: &mut VecDeque) { // 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 responses.add(TextToolMessage::SelectionChanged); @@ -303,7 +294,7 @@ impl<'a> MessageHandler> for Text ToolMessage::Text(TextToolMessage::UpdateOptions { options }) => options, ToolMessage::Text(TextToolMessage::SelectionChanged) => { 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.font_size = typesetting.font_size; @@ -311,7 +302,7 @@ impl<'a> MessageHandler> for Text if let Some(editing_text) = self.tool_data.editing_text.as_mut() { editing_text.typesetting.align = typesetting.align; editing_text.typesetting.font_size = typesetting.font_size; - editing_text.font = font.clone(); + editing_text.font = font; } } @@ -326,7 +317,7 @@ impl<'a> MessageHandler> for Text // Text tool has no fill checkbox; keep enabled so new text never starts with `None` 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; } _ => { @@ -385,7 +376,7 @@ impl<'a> MessageHandler> 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 { @@ -476,9 +467,9 @@ struct TextToolData { } impl TextToolData { - fn delete_empty_layer(&mut self, font_cache: &FontCache, responses: &mut VecDeque) -> TextToolFsmState { + fn delete_empty_layer(&mut self, fonts: &FontsMessageHandler, responses: &mut VecDeque) -> TextToolFsmState { // 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 responses.add(NodeGraphMessage::DeleteNodes { @@ -491,15 +482,16 @@ impl TextToolData { } /// Set the editing state of the currently modifying layer - fn set_editing(&self, editable: bool, font_cache: &FontCache, responses: &mut VecDeque) { + fn set_editing(&self, editable: bool, fonts: &FontsMessageHandler, responses: &mut VecDeque) { if let Some(editing_text) = self.editing_text.as_ref().filter(|_| editable) { let (align, align_last) = editing_text.typesetting.align.css(); + let font_data = fonts.get_blob_or_queue_load(&editing_text.font, responses).as_ref().to_vec().into(); responses.add(FrontendMessage::DisplayEditableTextbox { text: editing_text.text.clone(), line_height_ratio: editing_text.typesetting.line_height_ratio, font_size: editing_text.typesetting.font_size, 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(), max_width: editing_text.typesetting.max_width, 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 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 { text: text.clone(), - font: font.clone(), + font, typesetting, color: Some(color), transform, @@ -532,20 +524,20 @@ impl TextToolData { Some(()) } - fn start_editing_layer(&mut self, layer: LayerNodeIdentifier, tool_state: TextToolFsmState, document: &DocumentMessageHandler, font_cache: &FontCache, responses: &mut VecDeque) { + fn start_editing_layer(&mut self, layer: LayerNodeIdentifier, tool_state: TextToolFsmState, document: &DocumentMessageHandler, fonts: &FontsMessageHandler, responses: &mut VecDeque) { if layer == LayerNodeIdentifier::ROOT_PARENT { log::error!("Cannot edit ROOT_PARENT in TextTooLData") } if tool_state == TextToolFsmState::Editing { - self.set_editing(false, font_cache, responses); + self.set_editing(false, fonts, responses); } 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); - self.set_editing(true, font_cache, responses); + self.set_editing(true, fonts, responses); responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![self.layer.to_node()] }); // 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) { - // Create new text + fn new_text(&mut self, document: &DocumentMessageHandler, editing_text: EditingText, fonts: &FontsMessageHandler, responses: &mut VecDeque) { self.new_text = String::new(); responses.add(DocumentMessage::AddTransaction); 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 { id: self.layer.to_node(), text: String::new(), @@ -580,7 +574,7 @@ impl TextToolData { let transform = editing_text.transform; 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()] }); @@ -602,20 +596,23 @@ impl TextToolData { }); } - fn check_click(document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, font_cache: &FontCache) -> Option { - document.metadata().all_layers().filter(|&layer| document.metadata().is_text_layer(layer)).find(|&layer| { - let transformed_quad = document.metadata().transform_to_viewport(layer) * text_bounding_box(layer, document, font_cache); - let mouse = DVec2::new(input.mouse.position.x, input.mouse.position.y); - - transformed_quad.contains(mouse) - }) + fn check_click(document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, fonts: &FontsMessageHandler, responses: &mut VecDeque) -> Option { + let mouse = DVec2::new(input.mouse.position.x, input.mouse.position.y); + let layers: Vec = document.metadata().all_layers().filter(|&layer| document.metadata().is_text_layer(layer)).collect(); + for layer in layers { + let transformed_quad = document.metadata().transform_to_viewport(layer) * text_bounding_box(layer, document, fonts, responses); + 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) { self.snap_candidates.clear(); 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); } } @@ -651,14 +648,7 @@ impl Fsm for TextToolFsmState { tool_options: &Self::ToolOptions, responses: &mut VecDeque, ) -> Self { - let ToolActionMessageContext { - document, - input, - cached_data, - viewport, - .. - } = transition_data; - let font_cache = &cached_data.font_cache; + let ToolActionMessageContext { document, input, fonts, viewport, .. } = transition_data; let fill_color = COLOR_OVERLAY_BLUE_05; 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(); responses.add(FrontendMessage::DisplayEditableTextboxTransform { transform }); 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 blob = fonts.get_blob_or_queue_load(&editing_text.font, responses); + let far = graphene_std::text::bounding_box(&tool_data.new_text, &blob, editing_text.typesetting, false); if far.x != 0. && far.y != 0. { let quad = Quad::from_box([DVec2::ZERO, far]); 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 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 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); 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); // Draw red overlay if text is clipped let transformed_quad = layer_transform * bounds; - if let Some((text, font, typesetting, _)) = graph_modification_utils::get_text(layer.unwrap(), &document.network_interface) - && lines_clipping(text.as_str(), font, font_cache, typesetting) - { - overlay_context.line(transformed_quad.0[2], transformed_quad.0[3], Some(COLOR_OVERLAY_RED), Some(3.)); + if let Some((text, font, typesetting, _)) = graph_modification_utils::get_text(layer.unwrap(), &document.network_interface, fonts, &document.resources) { + let blob = fonts.get_blob_or_queue_load(&font, responses); + if lines_clipping(text.as_str(), &blob, typesetting) { + 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); @@ -728,7 +720,7 @@ impl Fsm for TextToolFsmState { } (state, TextToolMessage::EditSelected) => { 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; } @@ -769,10 +761,10 @@ impl Fsm for TextToolFsmState { bounds.original_bound_transform = bounds.transform; 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; - } 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); if selected != Some(clicked_layer) { @@ -784,7 +776,7 @@ impl Fsm for TextToolFsmState { id: clicked_layer, original_transform, }); - tool_data.get_snap_candidates(document, font_cache); + tool_data.get_snap_candidates(document, fonts, responses); return TextToolFsmState::Dragging; } TextToolFsmState::Placing @@ -967,8 +959,8 @@ impl Fsm for TextToolFsmState { let has_dragged = (start - end).length_squared() > DRAG_THRESHOLD * DRAG_THRESHOLD; // 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) { - tool_data.start_editing_layer(clicked_text_layer_path, self, document, font_cache, responses); + 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, fonts, responses); 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()), 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::Dragging, TextToolMessage::DragStop) => { @@ -1004,7 +996,7 @@ impl Fsm for TextToolFsmState { } 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; } tool_data.layer_dragging.take(); @@ -1013,8 +1005,9 @@ impl Fsm for TextToolFsmState { } (TextToolFsmState::Editing, TextToolMessage::RefreshEditingFontData) => { let font = Font::new(tool_options.font.font_family.clone(), tool_options.font.font_style.clone()); + let blob = fonts.get_blob_or_queue_load(&font, responses); responses.add(FrontendMessage::DisplayEditableTextboxUpdateFontData { - font_data: font_cache.get(&font).map(|(data, _)| data.clone()).unwrap_or_default().into(), + font_data: blob.as_ref().to_vec().into(), }); TextToolFsmState::Editing @@ -1023,7 +1016,7 @@ impl Fsm for TextToolFsmState { tool_data.new_text = new_text; 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 { 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 } else { 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); @@ -1055,7 +1048,7 @@ impl Fsm for TextToolFsmState { } (TextToolFsmState::Editing, TextToolMessage::Abort) => { 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); diff --git a/editor/src/messages/tool/utility_types.rs b/editor/src/messages/tool/utility_types.rs index b29884fa25..6fbb206d8b 100644 --- a/editor/src/messages/tool/utility_types.rs +++ b/editor/src/messages/tool/utility_types.rs @@ -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::layout::utility_types::widget_prelude::*; use crate::messages::portfolio::document::overlays::utility_types::OverlayProvider; -use crate::messages::portfolio::utility_types::CachedData; use crate::messages::preferences::PreferencesMessageHandler; use crate::messages::prelude::*; use crate::messages::tool::common_functionality::shapes::shape_utility::ShapeType; @@ -25,7 +24,7 @@ pub struct ToolActionMessageContext<'a> { pub document_id: DocumentId, pub global_tool_data: &'a DocumentToolData, pub input: &'a InputPreprocessorMessageHandler, - pub cached_data: &'a CachedData, + pub fonts: &'a FontsMessageHandler, pub shape_editor: &'a mut ShapeState, pub node_graph: &'a NodeGraphExecutor, pub preferences: &'a PreferencesMessageHandler, @@ -38,11 +37,11 @@ impl ToolCommon for T where T: for<'a, 'b> MessageHandler, _cached_data: &CachedData); + fn refresh_options(&self, responses: &mut VecDeque); } impl ToolRefreshOptions for T { - fn refresh_options(&self, responses: &mut VecDeque, _cached_data: &CachedData) { + fn refresh_options(&self, responses: &mut VecDeque) { self.send_layout(responses, LayoutTarget::ToolOptions); } } diff --git a/editor/src/node_graph_executor.rs b/editor/src/node_graph_executor.rs index fe088ea5e7..ae089b9041 100644 --- a/editor/src/node_graph_executor.rs +++ b/editor/src/node_graph_executor.rs @@ -9,7 +9,6 @@ use graphene_std::application_io::{NodeGraphUpdateMessage, RenderConfig, TimingI use graphene_std::color::SRGBA8; use graphene_std::raster::{CPU, Raster}; use graphene_std::renderer::RenderMetadata; -use graphene_std::text::FontCache; use graphene_std::transform::Footprint; use graphene_std::vector::Vector; use interpreted_executor::dynamic_executor::ResolvedDocumentNodeTypesDelta; @@ -95,10 +94,6 @@ impl NodeGraphExecutor { 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) { self.runtime_io .send(GraphRuntimeRequest::EditorPreferencesUpdate(editor_preferences)) diff --git a/editor/src/node_graph_executor/runtime.rs b/editor/src/node_graph_executor/runtime.rs index 9d6f7ec428..df671f1809 100644 --- a/editor/src/node_graph_executor/runtime.rs +++ b/editor/src/node_graph_executor/runtime.rs @@ -17,7 +17,6 @@ use graphene_std::ops::Convert; use graphene_std::platform_application_io::canvas_utils::{Canvas, CanvasSurface, CanvasSurfaceHandle}; use graphene_std::raster_types::Raster; use graphene_std::renderer::{Render, RenderParams, RenderSvgSegmentList, SvgRender, SvgSegment}; -use graphene_std::text::FontCache; use graphene_std::transform::RenderQuality; use graphene_std::vector::Vector; use graphene_std::vector::style::RenderMode; @@ -71,7 +70,6 @@ pub struct NodeRuntime { pub enum GraphRuntimeRequest { GraphUpdate(GraphUpdate), ExecutionRequest(ExecutionRequest), - FontCacheUpdate(FontCache), EditorPreferencesUpdate(EditorPreferences), } @@ -134,7 +132,6 @@ impl NodeRuntime { update_thumbnails: true, editor_api: PlatformEditorApi { - font_cache: FontCache::default(), editor_preferences: Box::new(EditorPreferences::default()), node_graph_message_sender: Box::new(InternalNodeGraphUpdateSender(sender)), @@ -162,7 +159,6 @@ impl NodeRuntime { } pub async fn run(&mut self) -> Option { - let mut font = None; let mut preferences = None; let mut graph = None; let mut eyedropper = None; @@ -186,7 +182,6 @@ impl NodeRuntime { break; } } - GraphRuntimeRequest::FontCacheUpdate(_) => font = Some(request), GraphRuntimeRequest::EditorPreferencesUpdate(_) => preferences = Some(request), } } @@ -199,27 +194,13 @@ impl NodeRuntime { 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 { 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) => { self.editor_preferences = preferences.clone(); self.editor_api = PlatformEditorApi { - font_cache: self.editor_api.font_cache.clone(), application_io: self.editor_api.application_io.clone(), node_graph_message_sender: Box::new(self.sender.clone()), editor_preferences: Box::new(preferences), @@ -587,7 +568,6 @@ pub(crate) fn replace_application_io(application_io: PlatformApplicationIo) { impl NodeRuntime { pub(crate) fn replace_application_io(&mut self, application_io: PlatformApplicationIo) { self.editor_api = PlatformEditorApi { - font_cache: self.editor_api.font_cache.clone(), application_io: Some(application_io.into()), node_graph_message_sender: Box::new(self.sender.clone()), editor_preferences: Box::new(self.editor_preferences.clone()), diff --git a/frontend/src/managers/fonts.ts b/frontend/src/managers/fonts.ts index 043f7ae18e..41e94cf1e6 100644 --- a/frontend/src/managers/fonts.ts +++ b/frontend/src/managers/fonts.ts @@ -41,21 +41,21 @@ export function createFontsManager(subscriptions: SubscriptionsRouter, editor: E } }); - subscriptions.subscribeFrontendMessage("TriggerFontDataLoad", async (data) => { - const { fontFamily, fontStyle } = data.font; - + // Generic URL resolver + // 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 { - 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); - 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 bytes = new Uint8Array(buffer); - editor.onFontLoad(fontFamily, fontStyle, bytes); + editor.onResourceResolved(data.documentId, data.resourceId, bytes); } catch (error) { if (error instanceof DOMException && error.name === "AbortError") return; // 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(); 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 diff --git a/frontend/wrapper/src/editor_wrapper.rs b/frontend/wrapper/src/editor_wrapper.rs index 3de6709876..c7cd60fa83 100644 --- a/frontend/wrapper/src/editor_wrapper.rs +++ b/frontend/wrapper/src/editor_wrapper.rs @@ -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::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; 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::tool::tool_messages::tool_prelude::WidgetId; use graph_craft::document::NodeId; @@ -650,14 +651,19 @@ impl EditorWrapper { /// The font catalog has been loaded #[wasm_bindgen(js_name = onFontCatalogLoad)] pub fn on_font_catalog_load(&self, catalog: Vec) { - self.dispatch(PortfolioMessage::FontCatalogLoaded { catalog: FontCatalog(catalog) }); + self.dispatch(FontsMessage::CatalogLoaded { catalog: FontCatalog::from(catalog) }); } - /// A font has been downloaded - #[wasm_bindgen(js_name = onFontLoad)] - pub fn on_font_load(&self, font_family: String, font_style: String, data: Vec) -> Result<(), JsValue> { - let message = PortfolioMessage::FontLoaded { font_family, font_style, data }; - self.dispatch(message); + /// A requested resource has been resolved by the frontend. + #[wasm_bindgen(js_name = onResourceResolved)] + pub fn on_resource_resolved(&self, document_id: u64, resource_id: u64, data: Vec) -> Result<(), JsValue> { + self.dispatch(PortfolioMessage::DocumentPassMessage { + document_id: DocumentId(document_id), + message: DocumentMessage::Resource(ResourceMessage::Resolved { + resource_id: resource_id.into(), + data: std::sync::Arc::from(data), + }), + }); Ok(()) } diff --git a/node-graph/graphene-cli/src/main.rs b/node-graph/graphene-cli/src/main.rs index c6572eb6a9..a5e0151fec 100644 --- a/node-graph/graphene-cli/src/main.rs +++ b/node-graph/graphene-cli/src/main.rs @@ -11,7 +11,6 @@ use graph_craft::graphene_compiler::Compiler; use graph_craft::proto::ProtoNetwork; use graph_craft::util::load_network; use graphene_std::application_io::{ApplicationIo, NodeGraphUpdateMessage, NodeGraphUpdateSender}; -use graphene_std::text::FontCache; use interpreted_executor::dynamic_executor::DynamicExecutor; use interpreted_executor::util::wrap_network_in_scope; use std::error::Error; @@ -134,7 +133,6 @@ async fn main() -> Result<(), Box> { max_render_region_size: EditorPreferences::default().max_render_region_size, }; let editor_api = Arc::new(PlatformEditorApi { - font_cache: FontCache::default(), application_io: Some(application_io_for_api), node_graph_message_sender: Box::new(UpdateLogger {}), editor_preferences: Box::new(preferences), diff --git a/node-graph/libraries/application-io/src/lib.rs b/node-graph/libraries/application-io/src/lib.rs index 88d13211c4..3ca7dcbd49 100644 --- a/node-graph/libraries/application-io/src/lib.rs +++ b/node-graph/libraries/application-io/src/lib.rs @@ -6,7 +6,6 @@ use std::hash::{Hash, Hasher}; use std::ptr::addr_of; use std::sync::Arc; use std::time::Duration; -use text_nodes::FontCache; use vector_types::vector::style::RenderMode; pub use graphene_resource as resource; @@ -126,8 +125,6 @@ impl GetEditorPreferences for DummyPreferences { } pub struct EditorApi { - /// Font data (for rendering text) made available to the graph through the `PlatformEditorApi`. - pub font_cache: FontCache, /// Gives access to APIs like resources. pub application_io: Option>, pub node_graph_message_sender: Box, @@ -140,7 +137,6 @@ impl Eq for EditorApi {} impl Default for EditorApi { fn default() -> Self { Self { - font_cache: FontCache::default(), application_io: None, node_graph_message_sender: Box::new(Logger), editor_preferences: Box::new(DummyPreferences), @@ -150,7 +146,6 @@ impl Default for EditorApi { impl Hash for EditorApi { fn hash(&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.node_graph_message_sender.as_ref() as *const dyn NodeGraphUpdateSender).hash(state); (self.editor_preferences.as_ref() as *const dyn GetEditorPreferences).hash(state); @@ -165,8 +160,7 @@ impl core_types::graphene_hash::CacheHash for EditorApi { impl PartialEq for EditorApi { 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.editor_preferences.as_ref() as *const _, other.editor_preferences.as_ref() as *const _) } @@ -174,7 +168,7 @@ impl PartialEq for EditorApi { impl Debug for EditorApi { 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() } } diff --git a/node-graph/libraries/resources/src/lib.rs b/node-graph/libraries/resources/src/lib.rs index 319d4f2580..4a93a287b8 100644 --- a/node-graph/libraries/resources/src/lib.rs +++ b/node-graph/libraries/resources/src/lib.rs @@ -26,6 +26,10 @@ impl Resource { pub fn hash(&self) -> ResourceHash { self.hash } + + pub fn empty() -> Self { + Self::new([]) + } } impl From<&Resource> for Arc + Send + Sync> { @@ -238,6 +242,18 @@ impl ResourceId { } } +impl From for ResourceId { + fn from(value: u64) -> Self { + Self(value) + } +} + +impl From for u64 { + fn from(id: ResourceId) -> Self { + id.0 + } +} + impl std::fmt::Display for ResourceId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.0) diff --git a/node-graph/nodes/gstd/src/text.rs b/node-graph/nodes/gstd/src/text.rs index 89c557e7ca..a4db66f00f 100644 --- a/node-graph/nodes/gstd/src/text.rs +++ b/node-graph/nodes/gstd/src/text.rs @@ -1,23 +1,21 @@ use core_types::Ctx; use core_types::list::List; -use graph_craft::application_io::PlatformEditorApi; +use graph_craft::application_io::resource::{Resource, ResourceHash}; use graphic_types::Vector; pub use text_nodes::*; /// Draws a text string as vector geometry with a choice of font and styling. #[node_macro::node(category("Text"))] -fn text<'i: 'n>( +fn text( _: impl Ctx, - /// The Graphite editor's source for global font resources. - #[scope("editor-api")] - editor_resources: &'i PlatformEditorApi, + _primary: (), /// The text content to be drawn. #[widget(ParsedWidgetOverride::Custom = "text_area")] #[default("Lorem ipsum")] 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")] - font: Font, + font: Resource, /// The font size used to draw the text. #[unit(" px")] #[default(24.)] @@ -62,6 +60,8 @@ fn text<'i: 'n>( align: TextAlign, /// Whether to split every letterform into its own vector item. Otherwise, a single vector compound path is produced. separate_glyphs: bool, + + #[data] cache: std::sync::Arc)>>>, ) -> List { let typesetting = TypesettingConfig { font_size: size, @@ -73,5 +73,17 @@ fn text<'i: 'n>( align, }; - to_path(&text, &font, &editor_resources.font_cache, typesetting, separate_glyphs) + let font_blob = { + let mut cache = cache.lock().unwrap(); + match cache.as_ref() { + Some((cached_hash, cached_blob)) if *cached_hash == font.hash() => cached_blob.clone(), + _ => { + let new_blob = Blob::new((&font).into()); + *cache = Some((font.hash(), new_blob.clone())); + new_blob + } + } + }; + + to_path(&text, &font_blob, typesetting, separate_glyphs) } diff --git a/node-graph/nodes/text/src/font.rs b/node-graph/nodes/text/src/font.rs new file mode 100644 index 0000000000..74024b8096 --- /dev/null +++ b/node-graph/nodes/text/src/font.rs @@ -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(&self, state: &mut H) { + self.font_family.hash(state); + self.font_style.hash(state); + } +} + +impl CacheHash for Font { + fn cache_hash(&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 { + use serde::Deserialize; + String::deserialize(deserializer).map(|name| if name == "Normal (400)" { "Regular (400)".to_string() } else { name }) +} diff --git a/node-graph/nodes/text/src/font_cache.rs b/node-graph/nodes/text/src/font_cache.rs deleted file mode 100644 index 258452ebc4..0000000000 --- a/node-graph/nodes/text/src/font_cache.rs +++ /dev/null @@ -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, -} - -impl std::hash::Hash for Font { - fn hash(&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(&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>, -} - -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::>()).finish() - } -} - -impl std::hash::Hash for FontCache { - fn hash(&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, &'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, &'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) { - 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 { - use serde::Deserialize; - String::deserialize(deserializer).map(|name| if name == "Normal (400)" { "Regular (400)".to_string() } else { name }) -} diff --git a/node-graph/nodes/text/src/lib.rs b/node-graph/nodes/text/src/lib.rs index 99f2968b1d..ed91407d06 100644 --- a/node-graph/nodes/text/src/lib.rs +++ b/node-graph/nodes/text/src/lib.rs @@ -1,4 +1,4 @@ -mod font_cache; +mod font; pub mod json; mod path_builder; pub mod regex; @@ -16,7 +16,8 @@ use unicode_segmentation::UnicodeSegmentation; // Re-export for convenience pub use core_types as gcore; -pub use font_cache::*; +pub use font::*; +pub use parley::fontique::Blob; pub use text_context::TextContext; pub use to_path::*; pub use vector_types; diff --git a/node-graph/nodes/text/src/text_context.rs b/node-graph/nodes/text/src/text_context.rs index ec9bf95a17..e6dcb1597e 100644 --- a/node-graph/nodes/text/src/text_context.rs +++ b/node-graph/nodes/text/src/text_context.rs @@ -1,4 +1,4 @@ -use super::{Font, FontCache, TypesettingConfig}; +use super::TypesettingConfig; use core::cell::RefCell; use core_types::list::List; use glam::DVec2; @@ -19,8 +19,7 @@ thread_local! { pub struct TextContext { font_context: FontContext, layout_context: LayoutContext<()>, - /// Cached font metadata for performance optimization - font_info_cache: HashMap, + font_info_cache: HashMap, } impl TextContext { @@ -32,15 +31,11 @@ impl TextContext { THREAD_TEXT.with_borrow_mut(f) } - /// Resolve a font and return its data as a Blob if available - fn resolve_font_data<'a>(&self, font: &'a Font, font_cache: &'a FontCache) -> Option<(Blob, &'a Font)> { - font_cache.get_blob(font) - } - - /// Get or cache font information for a given font - fn get_font_info(&mut self, font: &Font, font_data: &Blob) -> Option<(String, FontInfo)> { + /// Get or cache font information for the given font blob. + fn get_font_info(&mut self, font_data: &Blob) -> Option<(String, FontInfo)> { + let key = font_data.id(); // Check if we already have the font info cached - if let Some((family_id, font_info)) = self.font_info_cache.get(font) + if let Some((family_id, font_info)) = self.font_info_cache.get(&key) && let Some(family_name) = self.font_context.collection.family_name(*family_id) { return Some((family_name.to_string(), font_info.clone())); @@ -53,19 +48,16 @@ impl TextContext { fonts_info.first().and_then(|font_info| { self.font_context.collection.family_name(*family_id).map(|family_name| { // Cache the font info for future use - self.font_info_cache.insert(font.clone(), (*family_id, font_info.clone())); + self.font_info_cache.insert(key, (*family_id, font_info.clone())); (family_name.to_string(), font_info.clone()) }) }) }) } - /// Create a text layout using the specified font and typesetting configuration - fn layout_text(&mut self, text: &str, font: &Font, font_cache: &FontCache, typesetting: TypesettingConfig) -> Option> { - // Note that the actual_font may not be the desired font if that font is not yet loaded. - // 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)?; + /// Create a text layout from the given font blob and typesetting configuration. + fn layout_text(&mut self, text: &str, font_data: &Blob, typesetting: TypesettingConfig) -> Option> { + let (font_family, font_info) = self.get_font_info(font_data)?; const DISPLAY_SCALE: f32 = 1.; let mut builder = self.layout_context.ranged_builder(&mut self.font_context, text, DISPLAY_SCALE, false); @@ -89,8 +81,8 @@ impl TextContext { } /// 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 { - let Some(layout) = self.layout_text(text, font, font_cache, typesetting) else { + pub fn to_path(&mut self, text: &str, font_data: &Blob, typesetting: TypesettingConfig, per_glyph_items: bool) -> List { + let Some(layout) = self.layout_text(text, font_data, typesetting) else { return List::new_from_element(Vector::default()); }; @@ -162,8 +154,8 @@ impl TextContext { } /// 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 { - let Some(layout) = self.layout_text(text, font, font_cache, typesetting) else { + pub fn bounding_box(&mut self, text: &str, font_data: &Blob, typesetting: TypesettingConfig, for_clipping_test: bool) -> DVec2 { + let Some(layout) = self.layout_text(text, font_data, typesetting) else { return DVec2::ZERO; }; @@ -181,9 +173,9 @@ impl TextContext { } /// 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_data: &Blob, typesetting: TypesettingConfig) -> bool { 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_data, typesetting, true); max_height < bounds.y } } diff --git a/node-graph/nodes/text/src/to_path.rs b/node-graph/nodes/text/src/to_path.rs index d8ba556c17..1f063bd2ed 100644 --- a/node-graph/nodes/text/src/to_path.rs +++ b/node-graph/nodes/text/src/to_path.rs @@ -1,23 +1,23 @@ +use super::TypesettingConfig; use super::text_context::TextContext; -use super::{Font, FontCache, TypesettingConfig}; use core_types::list::List; use glam::DVec2; use parley::fontique::Blob; use std::sync::Arc; use vector_types::Vector; -pub fn to_path(text: &str, font: &Font, font_cache: &FontCache, typesetting: TypesettingConfig, per_glyph_items: bool) -> List { - TextContext::with_thread_local(|ctx| ctx.to_path(text, font, font_cache, typesetting, per_glyph_items)) +pub fn to_path(text: &str, font_data: &Blob, typesetting: TypesettingConfig, per_glyph_items: bool) -> List { + TextContext::with_thread_local(|ctx| ctx.to_path(text, font_data, typesetting, per_glyph_items)) } -pub fn bounding_box(text: &str, font: &Font, font_cache: &FontCache, typesetting: TypesettingConfig, for_clipping_test: bool) -> DVec2 { - TextContext::with_thread_local(|ctx| ctx.bounding_box(text, font, font_cache, typesetting, for_clipping_test)) +pub fn bounding_box(text: &str, font_data: &Blob, typesetting: TypesettingConfig, for_clipping_test: bool) -> DVec2 { + TextContext::with_thread_local(|ctx| ctx.bounding_box(text, font_data, typesetting, for_clipping_test)) } pub fn load_font(data: &[u8]) -> Blob { Blob::new(Arc::new(data.to_vec())) } -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)) +pub fn lines_clipping(text: &str, font_data: &Blob, typesetting: TypesettingConfig) -> bool { + TextContext::with_thread_local(|ctx| ctx.lines_clipping(text, font_data, typesetting)) }