diff --git a/Cargo.lock b/Cargo.lock index ae0a34d6fe..2729c0baab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2191,6 +2191,7 @@ dependencies = [ "num_enum", "once_cell", "preprocessor", + "reqwest", "serde", "serde_bytes", "serde_json", @@ -2198,6 +2199,7 @@ dependencies = [ "thiserror 2.0.18", "tokio", "tsify", + "url", "usvg", "vello", "wasm-bindgen", diff --git a/editor/Cargo.toml b/editor/Cargo.toml index 8d05eaca4b..dc9612ed74 100644 --- a/editor/Cargo.toml +++ b/editor/Cargo.toml @@ -48,6 +48,8 @@ spin = { workspace = true } image = { workspace = true } color = { workspace = true } zip = { workspace = true } +reqwest = { workspace = true } +url = { workspace = true } # Optional local dependencies wgpu-executor = { workspace = true, optional = true } diff --git a/editor/src/dispatcher.rs b/editor/src/dispatcher.rs index 580c0b88e0..b7d6e12309 100644 --- a/editor/src/dispatcher.rs +++ b/editor/src/dispatcher.rs @@ -32,6 +32,7 @@ pub struct DispatcherMessageHandlers { key_mapping_message_handler: KeyMappingMessageHandler, layout_message_handler: LayoutMessageHandler, menu_bar_message_handler: MenuBarMessageHandler, + network_message_handler: NetworkMessageHandler, pub(crate) portfolio_message_handler: PortfolioMessageHandler, preferences_message_handler: PreferencesMessageHandler, pub(crate) resource_storage_message_handler: ResourceStorageMessageHandler, @@ -79,8 +80,6 @@ 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), @@ -206,14 +205,7 @@ impl Dispatcher { self.message_handlers.dialog_message_handler.process_message(message, &mut queue, context); } Message::Frontend(message) => { - let decreminant = message.to_discriminant(); self.responses.push(message); - - // Handle these message immediately by returning early - if IMMEDIATE_FRONTEND_MESSAGES.contains(&decreminant) { - self.cleanup_queues(false); - return; - } } Message::InputPreprocessor(message) => { self.message_handlers.input_preprocessor_message_handler.process_message( @@ -238,6 +230,9 @@ impl Dispatcher { self.message_handlers.layout_message_handler.process_message(message, &mut queue, context); } + Message::Network(message) => { + self.message_handlers.network_message_handler.process_message(message, &mut queue, NetworkMessageContext {}); + } Message::ResourceStorage(message) => { self.message_handlers .resource_storage_message_handler diff --git a/editor/src/messages/frontend/frontend_message.rs b/editor/src/messages/frontend/frontend_message.rs index a99203596d..5d9e73e765 100644 --- a/editor/src/messages/frontend/frontend_message.rs +++ b/editor/src/messages/frontend/frontend_message.rs @@ -12,7 +12,6 @@ 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; @@ -111,14 +110,6 @@ pub enum FrontendMessage { name: String, filename: String, }, - TriggerFontCatalogLoad, - TriggerResolveResource { - #[serde(rename = "documentId")] - document_id: DocumentId, - #[serde(rename = "resourceId")] - resource_id: ResourceId, - url: String, - }, TriggerPersistenceReadState, TriggerPersistenceReadDocument { #[serde(rename = "documentId")] diff --git a/editor/src/messages/future/future_message_handler.rs b/editor/src/messages/future/future_message_handler.rs index a7ebdc71d9..353efda481 100644 --- a/editor/src/messages/future/future_message_handler.rs +++ b/editor/src/messages/future/future_message_handler.rs @@ -2,11 +2,15 @@ use std::future::{Future, IntoFuture}; use std::pin::Pin; use std::sync::{Arc, Mutex}; +use dyn_any::WasmNotSend; use futures::channel::mpsc::{UnboundedReceiver, UnboundedSender, unbounded}; use crate::messages::prelude::*; +#[cfg(not(target_family = "wasm"))] type InnerMessageFuture = Pin + Send + 'static>>; +#[cfg(target_family = "wasm")] +type InnerMessageFuture = Pin + 'static>>; /// Invoked by the spawner after a result is sent, to wake the platform event loop. pub type Wake = Arc; @@ -23,7 +27,7 @@ pub struct MessageFuture { } impl MessageFuture { - pub fn new(future: impl Future + Send + 'static) -> Self { + pub fn new(future: impl Future + WasmNotSend + 'static) -> Self { Self { inner: Arc::new(Mutex::new(Some(Box::pin(future)))), } @@ -51,7 +55,7 @@ impl From for Message { impl From for Message where - T: Future + Send + 'static, + T: Future + WasmNotSend + 'static, { fn from(future: T) -> Self { MessageFuture::new(future).into() diff --git a/editor/src/messages/message.rs b/editor/src/messages/message.rs index 94f29695cd..e3e3e6ec3a 100644 --- a/editor/src/messages/message.rs +++ b/editor/src/messages/message.rs @@ -34,6 +34,8 @@ pub enum Message { #[child] MenuBar(MenuBarMessage), #[child] + Network(NetworkMessage), + #[child] Portfolio(PortfolioMessage), #[child] Preferences(PreferencesMessage), diff --git a/editor/src/messages/mod.rs b/editor/src/messages/mod.rs index b8e734f8eb..f141622838 100644 --- a/editor/src/messages/mod.rs +++ b/editor/src/messages/mod.rs @@ -15,6 +15,7 @@ pub mod input_preprocessor; pub mod layout; pub mod menu_bar; pub mod message; +pub mod network; pub mod portfolio; pub mod preferences; pub mod prelude; diff --git a/editor/src/messages/network/mod.rs b/editor/src/messages/network/mod.rs new file mode 100644 index 0000000000..95de6eb8db --- /dev/null +++ b/editor/src/messages/network/mod.rs @@ -0,0 +1,10 @@ +mod network_message; +mod network_message_handler; +pub mod utility_types; + +#[doc(inline)] +pub use network_message::{NetworkMessage, NetworkMessageDiscriminant}; +#[doc(inline)] +pub use network_message_handler::{NetworkMessageContext, NetworkMessageHandler}; +#[doc(inline)] +pub use utility_types::Client; diff --git a/editor/src/messages/network/network_message.rs b/editor/src/messages/network/network_message.rs new file mode 100644 index 0000000000..6a6a853234 --- /dev/null +++ b/editor/src/messages/network/network_message.rs @@ -0,0 +1,49 @@ +use std::pin::Pin; + +use dyn_any::WasmNotSend; + +use crate::messages::network::utility_types::Client; +use crate::messages::prelude::*; + +#[impl_message(Message, Network)] +#[derive(derivative::Derivative, serde::Serialize, serde::Deserialize)] +#[derivative(Debug, PartialEq)] +pub enum NetworkMessage { + Request { + #[serde(skip, default)] + #[derivative(Debug = "ignore", PartialEq = "ignore")] + request: Option, + }, +} +impl NetworkMessage { + pub fn request(f: F) -> Self + where + F: FnOnce(Client) -> Fut + WasmNotSend + 'static, + Fut: Future + WasmNotSend + 'static, + { + NetworkMessage::Request { + request: Some(Box::new(move |c| Box::pin(f(c)))), + } + } +} + +#[cfg(not(target_family = "wasm"))] +type RequestFuture = Pin + Send>>; +#[cfg(target_family = "wasm")] +type RequestFuture = Pin>>; + +#[cfg(not(target_family = "wasm"))] +type RequestFn = Box RequestFuture + Send>; +#[cfg(target_family = "wasm")] +type RequestFn = Box RequestFuture>; + +impl Clone for NetworkMessage { + fn clone(&self) -> Self { + match self { + NetworkMessage::Request { .. } => { + log::error!("Cloning a NetworkMessage::Request is not supported"); + NetworkMessage::Request { request: None } + } + } + } +} diff --git a/editor/src/messages/network/network_message_handler.rs b/editor/src/messages/network/network_message_handler.rs new file mode 100644 index 0000000000..89e31d6df4 --- /dev/null +++ b/editor/src/messages/network/network_message_handler.rs @@ -0,0 +1,27 @@ +use crate::messages::network::utility_types::Client; +use crate::messages::prelude::*; + +#[derive(ExtractField)] +pub struct NetworkMessageContext {} + +#[derive(Debug, Default, ExtractField)] +pub struct NetworkMessageHandler { + client: Client, +} +#[message_handler_data] +impl MessageHandler for NetworkMessageHandler { + fn process_message(&mut self, message: NetworkMessage, responses: &mut VecDeque, _context: NetworkMessageContext) { + match message { + NetworkMessage::Request { request } => { + if let Some(request) = request { + responses.add(request(self.client.clone())); + } else { + log::error!("received a empty NetworkMessage::Request"); + } + } + } + } + + advertise_actions!(NetworkMessageDiscriminant; + ); +} diff --git a/editor/src/messages/network/utility_types.rs b/editor/src/messages/network/utility_types.rs new file mode 100644 index 0000000000..78fda01692 --- /dev/null +++ b/editor/src/messages/network/utility_types.rs @@ -0,0 +1,30 @@ +use reqwest::IntoUrl; + +#[derive(Debug, Clone)] +pub struct Client { + inner: Option, +} + +impl Default for Client { + fn default() -> Self { + Self { + #[cfg(not(target_family = "wasm"))] + inner: reqwest::Client::builder().timeout(std::time::Duration::from_secs(100)).build().ok(), + #[cfg(target_family = "wasm")] + inner: reqwest::Client::builder().build().ok(), + } + } +} + +impl Client { + pub async fn fetch(&self, url: U) -> Option> { + let Some(client) = &self.inner else { + log::error!("HTTP client failed to initialize, cannot fetch"); + return None; + }; + let response = client.get(url).send().await; + let response = response.and_then(|r| r.error_for_status()).map_err(|err| log::error!("failed to fetch: {err}")).ok()?; + let bytes = response.bytes().await.map_err(|err| log::error!("failed to read response body: {err}")).ok()?; + Some(bytes.to_vec().into_boxed_slice()) + } +} diff --git a/editor/src/messages/portfolio/document/resource/resource_message.rs b/editor/src/messages/portfolio/document/resource/resource_message.rs index 4df3dfd5f7..af858ad017 100644 --- a/editor/src/messages/portfolio/document/resource/resource_message.rs +++ b/editor/src/messages/portfolio/document/resource/resource_message.rs @@ -1,5 +1,5 @@ use crate::messages::prelude::*; -use graph_craft::application_io::resource::ResourceId; +use graph_craft::application_io::resource::{DataSource, ResourceHash, ResourceId}; use graphene_std::text::Font; use std::sync::Arc; @@ -8,7 +8,8 @@ use std::sync::Arc; 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]> }, + ResolveAll, + Resolve { resource_id: ResourceId }, + Resolved { resource_id: ResourceId, source: DataSource, hash: ResourceHash }, + ResolveFailed { resource_id: ResourceId }, } 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 f7a767c364..7a45060149 100644 --- a/editor/src/messages/portfolio/document/resource/resource_message_handler.rs +++ b/editor/src/messages/portfolio/document/resource/resource_message_handler.rs @@ -1,9 +1,12 @@ -use crate::messages::portfolio::document::resource::utility_types::EmbeddedResources; +use crate::messages::network::Client; +use crate::messages::portfolio::{document::resource::utility_types::EmbeddedResources, fonts::utility_types::FontCatalog}; 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; +use std::sync::Arc; +use url::Url; #[derive(ExtractField)] pub struct ResourceMessageContext<'a> { @@ -16,13 +19,7 @@ pub struct ResourceMessageHandler { pub registry: ResourceRegistry, pub embedded: EmbeddedResources, #[serde(skip)] - pending_resolves: HashMap>, -} - -#[derive(Debug, Clone, PartialEq)] -struct ResolveProgress { - index: usize, - source: DataSource, + pending_resolves: HashSet, } #[message_handler_data] @@ -36,7 +33,6 @@ impl MessageHandler> for ResourceMes 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); @@ -49,92 +45,142 @@ impl MessageHandler> for ResourceMes style: Some(style_name), }, ); - responses.add(ResourceMessage::Resolve); + responses.add(ResourceMessage::Resolve { resource_id }); } - ResourceMessage::Resolve => { + ResourceMessage::ResolveAll => { let unresolved_ids: Vec = self.registry.unresolved().map(|info| info.id).collect(); for id in unresolved_ids { - if self.pending_resolves.contains_key(&id) { + if self.pending_resolves.contains(&id) { continue; } - self.pending_resolves.insert(id, None); - responses.add(ResourceMessage::ResolveStep { resource_id: id }); + responses.add(ResourceMessage::Resolve { resource_id: id }); } } - ResourceMessage::ResolveStep { resource_id } => { - let Some(progress) = self.pending_resolves.get_mut(&resource_id) else { return }; - + ResourceMessage::Resolve { resource_id } => { + if self.pending_resolves.contains(&resource_id) { + log::warn!("Already pending resolve for {resource_id}; skipping"); + 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); + log::error!("Resolve for {resource_id}: no registry entry"); 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); + if info.hash.is_some() { + log::warn!("Resource {resource_id} already resolved"); return; - }; - *progress = Some(ResolveProgress { index, source: source.clone() }); + } - match source { - DataSource::Embedded => { - // Embedded resources are loaded on document load. - // If we get to this point, it means the resource was not embedded and we should try the next source. - responses.add(ResourceMessage::ResolveStep { resource_id }); - } - DataSource::Url(url) => { - responses.add(FrontendMessage::TriggerResolveResource { - document_id, - resource_id, - url: url.to_string(), - }); - } - DataSource::Font { family, style } => { - let font = match style { - Some(style) => Font::new(family, style), - None => Font::new_with_default_style(family), - }; - if let Some(hash) = fonts.cached_hash(&font) { - self.registry.resolve(&resource_id, hash); - self.pending_resolves.remove(&resource_id); - responses.add(NodeGraphMessage::RunDocumentGraph); - return; + self.pending_resolves.insert(resource_id); + + let font_catalog = fonts.font_catalog.clone(); + + let sources = info + .sources + .iter() + .map(|source| match source { + DataSource::Font { family, style } => { + let font = match style { + Some(style) => Font::new(family.clone(), style.clone()), + None => Font::new_with_default_style(family.clone()), + }; + let hash = fonts.cached_hash(&font); + (source.clone(), hash) } - if let Some(url) = fonts.cached_url(&font) { - responses.add(FrontendMessage::TriggerResolveResource { document_id, resource_id, url }); - return; + source => (source.clone(), None), + }) + .collect::)>>(); + + async fn resolve_to_message(document_id: DocumentId, resource_id: ResourceId, source: DataSource, url: Url, client: &Client) -> Option { + let result = client.fetch(url.clone()).await; + match result { + Some(data) => { + let hash = ResourceHash::from(data.as_ref()); + Some(Message::Batched { + messages: Box::new([ + PortfolioMessage::DocumentPassMessage { + document_id, + message: ResourceMessage::Resolved { resource_id, source, hash }.into(), + } + .into(), + ResourceStorageMessage::Store { data: Arc::from(data) }.into(), + ]), + }) + } + None => { + log::warn!("Failed to fetch resource {resource_id} from {url}"); + None } - responses.add(FrontendMessage::TriggerFontCatalogLoad); - self.pending_resolves.remove(&resource_id); } } + + responses.add(NetworkMessage::request(async move |client| { + let mut loaded_catalog = None; + let mut response: Option = None; + for (source, hash) in sources { + if let Some(hash) = hash { + response = Some(ResourceMessage::Resolved { resource_id, source, hash }.into()); + break; + } + + match &source { + DataSource::Embedded => continue, + DataSource::Url(url) => { + response = resolve_to_message(document_id, resource_id, source.clone(), url.clone(), &client).await; + } + DataSource::Font { family, style } => { + let font = match style { + Some(style) => Font::new(family.clone(), style.clone()), + None => Font::new_with_default_style(family.clone()), + }; + + if font_catalog.is_empty() && loaded_catalog.as_ref().is_none() { + loaded_catalog = FontCatalog::load_from_api(&client).await; + } + + let url = loaded_catalog.as_ref().and_then(|catalog| catalog.download_url(&font)).or_else(|| font_catalog.download_url(&font)); + + if let Some(url) = url { + let Ok(url) = Url::parse(&url) else { + log::warn!("Invalid URL {url} for font resource {resource_id}"); + continue; + }; + response = resolve_to_message(document_id, resource_id, source.clone(), url, &client).await; + } else { + log::warn!("No download URL found for font resource {resource_id}"); + } + } + } + if response.is_some() { + break; + } + } + + let mut response = response.unwrap_or_else(|| { + log::error!("Resolve for {resource_id}: all sources exhausted"); + PortfolioMessage::DocumentPassMessage { + document_id, + message: ResourceMessage::ResolveFailed { resource_id }.into(), + } + .into() + }); + + if let Some(catalog) = loaded_catalog.take() { + response = Message::Batched { + messages: Box::new([response, FontsMessage::CatalogLoaded { catalog }.into()]), + }; + } + + response + })) } - ResourceMessage::Resolved { resource_id, data } => { - let hash = ResourceHash::from(data.as_ref()); - let Some(progress) = self.pending_resolves.remove(&resource_id).and_then(|p| p) else { - log::error!("Resolved message for {resource_id} with no pending resolve"); - return; - }; - let Some(info) = self.registry.info(&resource_id) else { - // ResourceId was removed from registry after resolve started. - // This can happen if the document was modified while resolves were in-flight. - // Likely safe to ignore for now. - // TODO: Consider adding cleaner cancelation for in-flight resolves. - return; - }; - let Some(source) = info.sources.get(progress.index).cloned() else { - log::error!("Resolved message for {resource_id} with no current source"); - return; - }; - if progress.source != source { - log::error!("Resolved message for {resource_id} with mismatched source"); + ResourceMessage::Resolved { resource_id, source, hash } => { + self.pending_resolves.remove(&resource_id); + if self.registry.info(&resource_id).is_none() { + // Resource was removed from registry after the fetch started. return; } self.registry.resolve(&resource_id, hash); - responses.add(ResourceStorageMessage::Store { data }); if let DataSource::Font { family, style } = source { let font = match style { @@ -144,9 +190,11 @@ impl MessageHandler> for ResourceMes responses.add(FontsMessage::ResourceResolved { font, hash }); } - responses.add(ResourceMessage::Resolve); responses.add(NodeGraphMessage::RunDocumentGraph); } + ResourceMessage::ResolveFailed { resource_id } => { + self.pending_resolves.remove(&resource_id); + } } } diff --git a/editor/src/messages/portfolio/document_migration.rs b/editor/src/messages/portfolio/document_migration.rs index 6d2d5f6922..8f7915b7a4 100644 --- a/editor/src/messages/portfolio/document_migration.rs +++ b/editor/src/messages/portfolio/document_migration.rs @@ -1694,6 +1694,7 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId], 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::Embedded); document.resources.registry.push_source_back( &resource_id, DataSource::Font { diff --git a/editor/src/messages/portfolio/fonts/fonts_message.rs b/editor/src/messages/portfolio/fonts/fonts_message.rs index e192f609fe..757e926e30 100644 --- a/editor/src/messages/portfolio/fonts/fonts_message.rs +++ b/editor/src/messages/portfolio/fonts/fonts_message.rs @@ -6,6 +6,7 @@ use graphene_std::text::Font; #[impl_message(Message, PortfolioMessage, Fonts)] #[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)] pub enum FontsMessage { + LoadCatalog, CatalogLoaded { catalog: FontCatalog, }, diff --git a/editor/src/messages/portfolio/fonts/fonts_message_handler.rs b/editor/src/messages/portfolio/fonts/fonts_message_handler.rs index e83efa1f17..fe16ab4775 100644 --- a/editor/src/messages/portfolio/fonts/fonts_message_handler.rs +++ b/editor/src/messages/portfolio/fonts/fonts_message_handler.rs @@ -3,6 +3,7 @@ use crate::messages::portfolio::fonts::utility_types::FontCatalog; use crate::messages::prelude::*; use graph_craft::application_io::resource::{DataSource, Resource, ResourceHash, ResourceId}; use graphene_std::text::Font; +use std::sync::Arc; #[derive(ExtractField)] pub struct FontsMessageContext<'a> { @@ -11,7 +12,7 @@ pub struct FontsMessageContext<'a> { #[derive(Debug, Default, ExtractField)] pub struct FontsMessageHandler { - pub font_catalog: FontCatalog, + pub font_catalog: Arc, font_hashes: HashMap, font_data: HashMap, } @@ -22,9 +23,20 @@ impl MessageHandler> for FontsMessageHandl let FontsMessageContext { resource_storage } = context; match message { + FontsMessage::LoadCatalog => { + responses.add(NetworkMessage::request(async move |client| { + let Some(catalog) = FontCatalog::load_from_api(&client).await else { + log::error!("failed to load font catalog"); + return Message::NoOp; + }; + FontsMessage::CatalogLoaded { catalog }.into() + })); + } FontsMessage::CatalogLoaded { catalog } => { - self.font_catalog = catalog; + self.font_catalog = Arc::new(catalog); responses.add(PortfolioMessage::ResolveResources); + responses.add(ToolMessage::RefreshToolOptions); + responses.add(PropertiesPanelMessage::Refresh); } FontsMessage::ResourceResolved { font, hash } => { let font = self.normalize(font); @@ -69,11 +81,6 @@ impl FontsMessageHandler { self.font_hashes.get(&font).copied() } - pub fn cached_url(&self, font: &Font) -> Option { - let font = self.normalize(font.clone()); - self.font_catalog.download_url(&font) - } - pub fn get_resource_or_queue_load(&self, font: &Font, responses: &mut VecDeque) -> Resource { let font = self.normalize(font.clone()); if let Some(hash) = self.font_hashes.get(&font) { @@ -104,9 +111,6 @@ impl FontsMessageHandler { } 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, - } + self.font_catalog.normalize(font) } } diff --git a/editor/src/messages/portfolio/fonts/utility_types.rs b/editor/src/messages/portfolio/fonts/utility_types.rs index 88f28ccca2..e8e5604625 100644 --- a/editor/src/messages/portfolio/fonts/utility_types.rs +++ b/editor/src/messages/portfolio/fonts/utility_types.rs @@ -1,4 +1,57 @@ +use crate::messages::network::Client; use graphene_std::text::Font; +use std::collections::HashMap; + +const FONT_LIST_API: &str = "https://api.graphite.art/font-list"; + +#[derive(serde::Deserialize)] +struct FontListApiResponse { + items: Vec, +} + +#[derive(serde::Deserialize)] +struct FontListApiFamily { + family: String, + variants: Vec, + files: HashMap, +} + +impl FontCatalog { + pub async fn load_from_api(client: &Client) -> Option { + let Some(bytes) = client.fetch(FONT_LIST_API).await else { + log::error!("failed to fetch font catalog from API"); + return None; + }; + + let response: FontListApiResponse = match serde_json::from_slice(&bytes) { + Ok(response) => response, + Err(err) => { + log::error!("failed to parse font catalog response: {err}"); + return None; + } + }; + + let families = response + .items + .into_iter() + .map(|family| { + let styles = family + .variants + .iter() + .filter_map(|variant| { + let weight = variant.chars().take_while(|c| c.is_ascii_digit()).collect::().parse::().unwrap_or(400); + let italic = variant.ends_with("italic"); + let url = family.files.get(variant)?.replacen("http://", "https://", 1); + Some(FontCatalogStyle { weight, italic, url }) + }) + .collect(); + FontCatalogFamily { name: family.family, styles } + }) + .collect(); + + Some(Self(families)) + } +} // TODO: Should this be a BTreeMap instead? #[derive(Clone, Debug, Default, Eq, PartialEq, serde::Serialize, serde::Deserialize)] @@ -26,6 +79,13 @@ impl FontCatalog { Some(catalog_family.closest_style(weight, italic).url.clone()) } + pub fn normalize(&self, font: Font) -> Font { + match self.find_font_style_in_catalog(&font) { + Some(style) => Font::new(font.font_family, style.to_named_style()), + None => font, + } + } + pub fn iter(&self) -> impl Iterator { self.0.iter() } @@ -41,7 +101,7 @@ impl From> for FontCatalog { } } -#[cfg_attr(feature = "wasm", derive(tsify::Tsify), tsify(from_wasm_abi))] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] pub struct FontCatalogFamily { /// The font family name. diff --git a/editor/src/messages/portfolio/portfolio_message_handler.rs b/editor/src/messages/portfolio/portfolio_message_handler.rs index 1c8fe2fb82..cad6354eca 100644 --- a/editor/src/messages/portfolio/portfolio_message_handler.rs +++ b/editor/src/messages/portfolio/portfolio_message_handler.rs @@ -399,7 +399,6 @@ impl MessageHandler> for Portfolio let mut used_resources = HashSet::new(); for (id, info) in self.unloaded_documents.iter() { - log::info!("Checking resources for unloaded document {:?}: {:?}", info.name, info.resources); if let Some(resources) = &info.resources { used_resources.extend(resources.iter()); } else { @@ -422,14 +421,9 @@ impl MessageHandler> for Portfolio } } PortfolioMessage::ResolveDocumentResources { document_id } => { - if self.fonts.font_catalog.is_empty() { - responses.add_front(FrontendMessage::TriggerFontCatalogLoad); - return; - } - responses.add(PortfolioMessage::DocumentPassMessage { document_id, - message: DocumentMessage::Resource(ResourceMessage::Resolve), + message: DocumentMessage::Resource(ResourceMessage::ResolveAll), }); } PortfolioMessage::LoadPersistedState { state } => { diff --git a/editor/src/messages/prelude.rs b/editor/src/messages/prelude.rs index 2f9971da83..cded44b1c2 100644 --- a/editor/src/messages/prelude.rs +++ b/editor/src/messages/prelude.rs @@ -22,6 +22,7 @@ pub use crate::messages::input_mapper::{InputMapperMessage, InputMapperMessageCo pub use crate::messages::input_preprocessor::{InputPreprocessorMessage, InputPreprocessorMessageContext, InputPreprocessorMessageDiscriminant, InputPreprocessorMessageHandler}; pub use crate::messages::layout::{LayoutMessage, LayoutMessageDiscriminant, LayoutMessageHandler}; pub use crate::messages::menu_bar::{MenuBarMessage, MenuBarMessageDiscriminant, MenuBarMessageHandler}; +pub use crate::messages::network::{NetworkMessage, NetworkMessageContext, NetworkMessageDiscriminant, NetworkMessageHandler}; pub use crate::messages::portfolio::document::data_panel::{DataPanelMessage, DataPanelMessageDiscriminant}; pub use crate::messages::portfolio::document::graph_operation::{GraphOperationMessage, GraphOperationMessageContext, GraphOperationMessageDiscriminant, GraphOperationMessageHandler}; pub use crate::messages::portfolio::document::navigation::{NavigationMessage, NavigationMessageContext, NavigationMessageDiscriminant, NavigationMessageHandler}; diff --git a/editor/src/messages/tool/tool_messages/text_tool.rs b/editor/src/messages/tool/tool_messages/text_tool.rs index 3e92d283ae..9e4c4700c2 100644 --- a/editor/src/messages/tool/tool_messages/text_tool.rs +++ b/editor/src/messages/tool/tool_messages/text_tool.rs @@ -290,6 +290,10 @@ impl<'a> MessageHandler> for Text self.options.fill.fill_choice = Some(solid(context.global_tool_data.primary_color)); } + if context.fonts.font_catalog.is_empty() { + responses.add_front(FontsMessage::LoadCatalog); + } + let options = match message { ToolMessage::Text(TextToolMessage::UpdateOptions { options }) => options, ToolMessage::Text(TextToolMessage::SelectionChanged) => { @@ -598,14 +602,10 @@ impl TextToolData { 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 { + 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, fonts, responses); - if transformed_quad.contains(mouse) { - return Some(layer); - } - } - None + transformed_quad.contains(mouse) + }) } fn get_snap_candidates(&mut self, document: &DocumentMessageHandler, fonts: &FontsMessageHandler, responses: &mut VecDeque) { diff --git a/frontend/src/components/Editor.svelte b/frontend/src/components/Editor.svelte index 61c3cbc587..0122277a83 100644 --- a/frontend/src/components/Editor.svelte +++ b/frontend/src/components/Editor.svelte @@ -2,7 +2,6 @@ import { onMount, onDestroy, setContext } from "svelte"; import MainWindow from "/src/components/window/MainWindow.svelte"; import { createClipboardManager, destroyClipboardManager } from "/src/managers/clipboard"; - import { createFontsManager, destroyFontsManager } from "/src/managers/fonts"; import { createHyperlinkManager, destroyHyperlinkManager } from "/src/managers/hyperlink"; import { createInputManager, destroyInputManager } from "/src/managers/input"; import { createLocalizationManager, destroyLocalizationManager } from "/src/managers/localization"; @@ -43,7 +42,6 @@ createLocalizationManager(subscriptions, editor); createPanicManager(subscriptions); createPersistenceManager(subscriptions, editor, stores.portfolio); - createFontsManager(subscriptions, editor); createInputManager(subscriptions, editor, stores.dialog, stores.portfolio, stores.document); // Initialize certain setup tasks required by the editor backend to be ready for the user now that the frontend is ready. @@ -71,7 +69,6 @@ destroyLocalizationManager(); destroyPanicManager(); destroyPersistenceManager(); - destroyFontsManager(); destroyInputManager(); }); diff --git a/frontend/src/managers/fonts.ts b/frontend/src/managers/fonts.ts deleted file mode 100644 index 41e94cf1e6..0000000000 --- a/frontend/src/managers/fonts.ts +++ /dev/null @@ -1,75 +0,0 @@ -import type { SubscriptionsRouter } from "/src/subscriptions-router"; -import type { EditorWrapper } from "/wrapper/pkg/graphite_wasm_wrapper"; - -type ApiResponse = { family: string; variants: string[]; files: Record }[]; - -const FONT_LIST_API = "https://api.graphite.art/font-list"; - -let subscriptionsRouter: SubscriptionsRouter | undefined = undefined; -let editorWrapper: EditorWrapper | undefined = undefined; -let abortController: AbortController | undefined = undefined; - -export function createFontsManager(subscriptions: SubscriptionsRouter, editor: EditorWrapper) { - destroyFontsManager(); - - subscriptionsRouter = subscriptions; - editorWrapper = editor; - abortController = new AbortController(); - - subscriptions.subscribeFrontendMessage("TriggerFontCatalogLoad", async () => { - try { - const response = await fetch(FONT_LIST_API, abortController ? { signal: abortController.signal } : undefined); - if (!response.ok) throw new Error(`Font catalog request failed with status ${response.status}`); - const fontListResponse: { items: ApiResponse } = await response.json(); - const fontListData = fontListResponse.items; - - const catalog = fontListData.map((font) => { - const styles = font.variants.map((variant) => { - const weight = variant === "regular" || variant === "italic" ? 400 : parseInt(variant, 10); - const italic = variant.endsWith("italic"); - const url = font.files[variant].replace("http://", "https://"); - - return { weight, italic, url }; - }); - return { name: font.family, styles }; - }); - - editor.onFontCatalogLoad(catalog); - } catch (error) { - if (error instanceof DOMException && error.name === "AbortError") return; - throw error; - } - }); - - // 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 resource resolution"); - const response = await fetch(data.url, abortController ? { signal: abortController.signal } : undefined); - if (!response.ok) throw new Error(`Resource request failed with status ${response.status}`); - const buffer = await response.arrayBuffer(); - const bytes = new Uint8Array(buffer); - - 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 resolve resource:", error); - } - }); -} - -export function destroyFontsManager() { - const subscriptions = subscriptionsRouter; - if (!subscriptions) return; - - abortController?.abort(); - subscriptions.unsubscribeFrontendMessage("TriggerFontCatalogLoad"); - subscriptions.unsubscribeFrontendMessage("TriggerResolveResource"); -} - -// Self-accepting HMR: tear down the old instance and re-create with the new module's code -import.meta.hot?.accept((newModule) => { - if (subscriptionsRouter && editorWrapper) newModule?.createFontsManager(subscriptionsRouter, editorWrapper); -}); diff --git a/frontend/wrapper/src/editor_wrapper.rs b/frontend/wrapper/src/editor_wrapper.rs index c7cd60fa83..ad83b8f563 100644 --- a/frontend/wrapper/src/editor_wrapper.rs +++ b/frontend/wrapper/src/editor_wrapper.rs @@ -20,7 +20,6 @@ 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::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; @@ -648,26 +647,6 @@ impl EditorWrapper { Ok(()) } - /// The font catalog has been loaded - #[wasm_bindgen(js_name = onFontCatalogLoad)] - pub fn on_font_catalog_load(&self, catalog: Vec) { - self.dispatch(FontsMessage::CatalogLoaded { catalog: FontCatalog::from(catalog) }); - } - - /// 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(()) - } - /// Dialog got dismissed #[wasm_bindgen(js_name = onDialogDismiss)] pub fn on_dialog_dismiss(&self) {