From 2c91efe16c56108240a12508d562a35f5c666173 Mon Sep 17 00:00:00 2001 From: Timon Date: Tue, 2 Jun 2026 15:02:33 +0000 Subject: [PATCH 01/14] Impl NetworkMessageHandler --- Cargo.lock | 1 + editor/Cargo.toml | 1 + editor/src/dispatcher.rs | 4 ++ editor/src/messages/message.rs | 2 + editor/src/messages/mod.rs | 1 + editor/src/messages/network/mod.rs | 10 +++++ .../src/messages/network/network_message.rs | 38 +++++++++++++++++++ .../network/network_message_handler.rs | 27 +++++++++++++ editor/src/messages/network/utility_types.rs | 24 ++++++++++++ editor/src/messages/prelude.rs | 1 + 10 files changed, 109 insertions(+) create mode 100644 editor/src/messages/network/mod.rs create mode 100644 editor/src/messages/network/network_message.rs create mode 100644 editor/src/messages/network/network_message_handler.rs create mode 100644 editor/src/messages/network/utility_types.rs diff --git a/Cargo.lock b/Cargo.lock index ae0a34d6fe3..9940d2f378e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2191,6 +2191,7 @@ dependencies = [ "num_enum", "once_cell", "preprocessor", + "reqwest", "serde", "serde_bytes", "serde_json", diff --git a/editor/Cargo.toml b/editor/Cargo.toml index 8d05eaca4b7..4d9f631657e 100644 --- a/editor/Cargo.toml +++ b/editor/Cargo.toml @@ -48,6 +48,7 @@ spin = { workspace = true } image = { workspace = true } color = { workspace = true } zip = { workspace = true } +reqwest = { workspace = true } # Optional local dependencies wgpu-executor = { workspace = true, optional = true } diff --git a/editor/src/dispatcher.rs b/editor/src/dispatcher.rs index 580c0b88e01..6a57a80bfbb 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, @@ -238,6 +239,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/message.rs b/editor/src/messages/message.rs index 94f29695cd1..e3e3e6ec3a9 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 b8e734f8eb1..9c2b4c6895a 100644 --- a/editor/src/messages/mod.rs +++ b/editor/src/messages/mod.rs @@ -16,6 +16,7 @@ pub mod layout; pub mod menu_bar; pub mod message; pub mod portfolio; +pub mod network; pub mod preferences; pub mod prelude; pub mod resource_storage; diff --git a/editor/src/messages/network/mod.rs b/editor/src/messages/network/mod.rs new file mode 100644 index 00000000000..95de6eb8db8 --- /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 00000000000..c91c5ff4d4b --- /dev/null +++ b/editor/src/messages/network/network_message.rs @@ -0,0 +1,38 @@ +use std::pin::Pin; + +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 + Send + 'static, + Fut: Future + Send + 'static, + { + NetworkMessage::Request { + request: Some(Box::new(move |c| Box::pin(f(c)))), + } + } +} + +type RequestFuture = Pin + Send>>; +type RequestFn = Box RequestFuture + Send>; + +// Custom clone implementation to avoid cloning the request function +impl Clone for NetworkMessage { + fn clone(&self) -> Self { + match self { + NetworkMessage::Request { .. } => 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 00000000000..89e31d6df4d --- /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 00000000000..f8ec32e9a10 --- /dev/null +++ b/editor/src/messages/network/utility_types.rs @@ -0,0 +1,24 @@ +use reqwest::IntoUrl; + +#[derive(Debug, Default, Clone)] +pub struct Client { + inner: reqwest::Client, +} + +impl Client { + pub async fn fetch(&self, url: U) -> Option> { + match self.inner.get(url).send().await { + Ok(response) => match response.bytes().await { + Ok(bytes) => Some(bytes.to_vec().into_boxed_slice()), + Err(err) => { + log::error!("failed to read response body: {err}"); + None + } + }, + Err(err) => { + log::error!("failed to fetch: {err}"); + None + } + } + } +} diff --git a/editor/src/messages/prelude.rs b/editor/src/messages/prelude.rs index 2f9971da836..cded44b1c29 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}; From d3234d2278d613d7abfbdb9e8e2428070a78bd4c Mon Sep 17 00:00:00 2001 From: Timon Date: Tue, 2 Jun 2026 16:09:30 +0000 Subject: [PATCH 02/14] Repalce Frontend fetch like messages with NetworkMessage --- editor/src/dispatcher.rs | 9 --- .../src/messages/frontend/frontend_message.rs | 9 --- .../resource/resource_message_handler.rs | 26 +++++-- .../messages/portfolio/fonts/fonts_message.rs | 1 + .../portfolio/fonts/fonts_message_handler.rs | 9 ++- .../messages/portfolio/fonts/utility_types.rs | 50 ++++++++++++- .../portfolio/portfolio_message_handler.rs | 2 +- frontend/src/components/Editor.svelte | 3 - frontend/src/managers/fonts.ts | 75 ------------------- frontend/wrapper/src/editor_wrapper.rs | 21 ------ 10 files changed, 78 insertions(+), 127 deletions(-) delete mode 100644 frontend/src/managers/fonts.ts diff --git a/editor/src/dispatcher.rs b/editor/src/dispatcher.rs index 6a57a80bfbb..b7d6e123095 100644 --- a/editor/src/dispatcher.rs +++ b/editor/src/dispatcher.rs @@ -80,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), @@ -207,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( diff --git a/editor/src/messages/frontend/frontend_message.rs b/editor/src/messages/frontend/frontend_message.rs index a99203596d7..5d9e73e7653 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/portfolio/document/resource/resource_message_handler.rs b/editor/src/messages/portfolio/document/resource/resource_message_handler.rs index f7a767c3640..6a4340c6a78 100644 --- a/editor/src/messages/portfolio/document/resource/resource_message_handler.rs +++ b/editor/src/messages/portfolio/document/resource/resource_message_handler.rs @@ -4,6 +4,7 @@ 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; #[derive(ExtractField)] pub struct ResourceMessageContext<'a> { @@ -85,11 +86,7 @@ impl MessageHandler> for ResourceMes responses.add(ResourceMessage::ResolveStep { resource_id }); } DataSource::Url(url) => { - responses.add(FrontendMessage::TriggerResolveResource { - document_id, - resource_id, - url: url.to_string(), - }); + responses.add(fetch_resource(document_id, resource_id, url.to_string())); } DataSource::Font { family, style } => { let font = match style { @@ -103,10 +100,10 @@ impl MessageHandler> for ResourceMes return; } if let Some(url) = fonts.cached_url(&font) { - responses.add(FrontendMessage::TriggerResolveResource { document_id, resource_id, url }); + responses.add(fetch_resource(document_id, resource_id, url)); return; } - responses.add(FrontendMessage::TriggerFontCatalogLoad); + responses.add(FontsMessage::LoadCatalog); self.pending_resolves.remove(&resource_id); } } @@ -187,6 +184,21 @@ impl ResourceMessageHandler { } } +fn fetch_resource(document_id: DocumentId, resource_id: ResourceId, url: String) -> Message { + NetworkMessage::request(move |client| async move { + let Some(bytes) = client.fetch(&url).await else { return Message::NoOp }; + PortfolioMessage::DocumentPassMessage { + document_id, + message: DocumentMessage::Resource(ResourceMessage::Resolved { + resource_id, + data: Arc::from(bytes), + }), + } + .into() + }) + .into() +} + // TODO: Eventually remove this document upgrade code impl<'de> serde::Deserialize<'de> for ResourceMessageHandler { fn deserialize>(deserializer: D) -> Result { diff --git a/editor/src/messages/portfolio/fonts/fonts_message.rs b/editor/src/messages/portfolio/fonts/fonts_message.rs index e192f609fe3..757e926e30f 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 e83efa1f17c..f1d356e517f 100644 --- a/editor/src/messages/portfolio/fonts/fonts_message_handler.rs +++ b/editor/src/messages/portfolio/fonts/fonts_message_handler.rs @@ -1,5 +1,5 @@ use crate::messages::portfolio::fonts::FALLBACK_FONT_RESOURCE; -use crate::messages::portfolio::fonts::utility_types::FontCatalog; +use crate::messages::portfolio::fonts::utility_types::{FONT_LIST_API, FontCatalog}; use crate::messages::prelude::*; use graph_craft::application_io::resource::{DataSource, Resource, ResourceHash, ResourceId}; use graphene_std::text::Font; @@ -22,6 +22,13 @@ impl MessageHandler> for FontsMessageHandl let FontsMessageContext { resource_storage } = context; match message { + FontsMessage::LoadCatalog => { + responses.add(NetworkMessage::request(|client| async move { + let Some(bytes) = client.fetch(FONT_LIST_API).await else { return Message::NoOp }; + let Some(catalog) = FontCatalog::from_api_response(&bytes) else { return Message::NoOp }; + FontsMessage::CatalogLoaded { catalog }.into() + })); + } FontsMessage::CatalogLoaded { catalog } => { self.font_catalog = catalog; responses.add(PortfolioMessage::ResolveResources); diff --git a/editor/src/messages/portfolio/fonts/utility_types.rs b/editor/src/messages/portfolio/fonts/utility_types.rs index 88f28ccca27..e6e95e0b77f 100644 --- a/editor/src/messages/portfolio/fonts/utility_types.rs +++ b/editor/src/messages/portfolio/fonts/utility_types.rs @@ -1,4 +1,52 @@ use graphene_std::text::Font; +use std::collections::HashMap; + +pub 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 { + /// Parse the response bytes from [`FONT_LIST_API`] into a [`FontCatalog`]. + pub fn from_api_response(bytes: &[u8]) -> Option { + 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 = if variant == "regular" || variant == "italic" { 400 } else { variant.parse::().ok()? }; + 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)] @@ -41,7 +89,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 1c8fe2fb82d..8f83701de7c 100644 --- a/editor/src/messages/portfolio/portfolio_message_handler.rs +++ b/editor/src/messages/portfolio/portfolio_message_handler.rs @@ -423,7 +423,7 @@ impl MessageHandler> for Portfolio } PortfolioMessage::ResolveDocumentResources { document_id } => { if self.fonts.font_catalog.is_empty() { - responses.add_front(FrontendMessage::TriggerFontCatalogLoad); + responses.add_front(FontsMessage::LoadCatalog); return; } diff --git a/frontend/src/components/Editor.svelte b/frontend/src/components/Editor.svelte index 61c3cbc587e..0122277a834 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 41e94cf1e6d..00000000000 --- 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 c7cd60fa839..ad83b8f5632 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) { From 3a2b0d38900d6bff9a4535fb346f046a1b5d7608 Mon Sep 17 00:00:00 2001 From: Timon Date: Wed, 3 Jun 2026 01:14:58 +0000 Subject: [PATCH 03/14] Reimpl resource loading with NetworkMessage --- Cargo.lock | 1 + editor/Cargo.toml | 1 + .../document/resource/resource_message.rs | 8 +- .../resource/resource_message_handler.rs | 187 +++++++++--------- .../portfolio/fonts/fonts_message_handler.rs | 23 +-- .../messages/portfolio/fonts/utility_types.rs | 20 +- .../portfolio/portfolio_message_handler.rs | 2 +- 7 files changed, 130 insertions(+), 112 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9940d2f378e..2729c0baaba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2199,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 4d9f631657e..dc9612ed745 100644 --- a/editor/Cargo.toml +++ b/editor/Cargo.toml @@ -49,6 +49,7 @@ 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/messages/portfolio/document/resource/resource_message.rs b/editor/src/messages/portfolio/document/resource/resource_message.rs index 4df3dfd5f75..6f566d945fb 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,7 @@ 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 }, } 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 6a4340c6a78..38623abca9c 100644 --- a/editor/src/messages/portfolio/document/resource/resource_message_handler.rs +++ b/editor/src/messages/portfolio/document/resource/resource_message_handler.rs @@ -1,10 +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,14 +18,6 @@ pub struct ResourceMessageContext<'a> { 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] @@ -37,7 +31,7 @@ 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); + responses.add(ResourceMessage::ResolveAll); } ResourceMessage::AddFont { resource_id, font } => { let style = fonts.font_catalog.find_font_style_in_catalog(&font); @@ -50,88 +44,119 @@ impl MessageHandler> for ResourceMes style: Some(style_name), }, ); - responses.add(ResourceMessage::Resolve); + responses.add(ResourceMessage::ResolveAll); } - 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) { - 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 } => { 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(fetch_resource(document_id, resource_id, 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; + 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(fetch_resource(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(FontsMessage::LoadCatalog); - self.pending_resolves.remove(&resource_id); } } + + responses.add(NetworkMessage::request(move |client| async move { + for (source, hash) in sources { + if let Some(hash) = hash { + return ResourceMessage::Resolved { resource_id, source, hash }.into(); + } + + match &source { + DataSource::Embedded => { + continue; + } + DataSource::Url(url) => { + if let Some(message) = resolve_to_message(document_id, resource_id, source.clone(), url.clone(), &client).await { + return message; + }; + } + 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 new_font_catalog = if font_catalog.is_empty() { FontCatalog::load_from_api(&client).await } else { None }; + + let url = new_font_catalog + .and_then(|catalog: FontCatalog| catalog.download_url(&font)) + .or_else(|| font_catalog.download_url(&font)); + if let Some(url) = url { + let url = match Url::parse(&url) { + Ok(url) => url, + Err(err) => { + log::warn!("Invalid URL {url} for font resource {resource_id}: {err}"); + continue; + } + }; + if let Some(message) = resolve_to_message(document_id, resource_id, source.clone(), url, &client).await { + return message; + }; + } else { + log::warn!("No download URL found for font resource {resource_id}"); + } + } + } + } + log::error!("Resolve for {resource_id}: all sources exhausted"); + Message::NoOp + })); } - 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 } => { + if self.registry.info(&resource_id).is_none() { + // Resource was removed from registry after the fetch started; safe to drop the result. return; } self.registry.resolve(&resource_id, hash); - responses.add(ResourceStorageMessage::Store { data }); if let DataSource::Font { family, style } = source { let font = match style { @@ -141,7 +166,6 @@ impl MessageHandler> for ResourceMes responses.add(FontsMessage::ResourceResolved { font, hash }); } - responses.add(ResourceMessage::Resolve); responses.add(NodeGraphMessage::RunDocumentGraph); } } @@ -184,21 +208,6 @@ impl ResourceMessageHandler { } } -fn fetch_resource(document_id: DocumentId, resource_id: ResourceId, url: String) -> Message { - NetworkMessage::request(move |client| async move { - let Some(bytes) = client.fetch(&url).await else { return Message::NoOp }; - PortfolioMessage::DocumentPassMessage { - document_id, - message: DocumentMessage::Resource(ResourceMessage::Resolved { - resource_id, - data: Arc::from(bytes), - }), - } - .into() - }) - .into() -} - // TODO: Eventually remove this document upgrade code impl<'de> serde::Deserialize<'de> for ResourceMessageHandler { fn deserialize>(deserializer: D) -> Result { diff --git a/editor/src/messages/portfolio/fonts/fonts_message_handler.rs b/editor/src/messages/portfolio/fonts/fonts_message_handler.rs index f1d356e517f..b9ae4bff49a 100644 --- a/editor/src/messages/portfolio/fonts/fonts_message_handler.rs +++ b/editor/src/messages/portfolio/fonts/fonts_message_handler.rs @@ -1,8 +1,9 @@ use crate::messages::portfolio::fonts::FALLBACK_FONT_RESOURCE; -use crate::messages::portfolio::fonts::utility_types::{FONT_LIST_API, FontCatalog}; +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, } @@ -24,13 +25,15 @@ impl MessageHandler> for FontsMessageHandl match message { FontsMessage::LoadCatalog => { responses.add(NetworkMessage::request(|client| async move { - let Some(bytes) = client.fetch(FONT_LIST_API).await else { return Message::NoOp }; - let Some(catalog) = FontCatalog::from_api_response(&bytes) else { return Message::NoOp }; + 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); } FontsMessage::ResourceResolved { font, hash } => { @@ -76,11 +79,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) { @@ -111,9 +109,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 e6e95e0b77f..c8a99022508 100644 --- a/editor/src/messages/portfolio/fonts/utility_types.rs +++ b/editor/src/messages/portfolio/fonts/utility_types.rs @@ -1,7 +1,8 @@ +use crate::messages::network::Client; use graphene_std::text::Font; use std::collections::HashMap; -pub const FONT_LIST_API: &str = "https://api.graphite.art/font-list"; +const FONT_LIST_API: &str = "https://api.graphite.art/font-list"; #[derive(serde::Deserialize)] struct FontListApiResponse { @@ -16,9 +17,13 @@ struct FontListApiFamily { } impl FontCatalog { - /// Parse the response bytes from [`FONT_LIST_API`] into a [`FontCatalog`]. - pub fn from_api_response(bytes: &[u8]) -> Option { - let response: FontListApiResponse = match serde_json::from_slice(bytes) { + 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}"); @@ -74,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() } diff --git a/editor/src/messages/portfolio/portfolio_message_handler.rs b/editor/src/messages/portfolio/portfolio_message_handler.rs index 8f83701de7c..166e696c0ee 100644 --- a/editor/src/messages/portfolio/portfolio_message_handler.rs +++ b/editor/src/messages/portfolio/portfolio_message_handler.rs @@ -429,7 +429,7 @@ impl MessageHandler> for Portfolio responses.add(PortfolioMessage::DocumentPassMessage { document_id, - message: DocumentMessage::Resource(ResourceMessage::Resolve), + message: DocumentMessage::Resource(ResourceMessage::ResolveAll), }); } PortfolioMessage::LoadPersistedState { state } => { From a4a97d2a48986accb486d40d31d0c4870999a4da Mon Sep 17 00:00:00 2001 From: Timon Date: Wed, 3 Jun 2026 01:15:04 +0000 Subject: [PATCH 04/14] Fix wasm --- .../src/messages/future/future_message_handler.rs | 8 ++++++-- editor/src/messages/network/network_message.rs | 13 +++++++++++-- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/editor/src/messages/future/future_message_handler.rs b/editor/src/messages/future/future_message_handler.rs index a7ebdc71d93..353efda4817 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/network/network_message.rs b/editor/src/messages/network/network_message.rs index c91c5ff4d4b..98b0cc23abc 100644 --- a/editor/src/messages/network/network_message.rs +++ b/editor/src/messages/network/network_message.rs @@ -1,5 +1,7 @@ use std::pin::Pin; +use dyn_any::WasmNotSend; + use crate::messages::network::utility_types::Client; use crate::messages::prelude::*; @@ -16,8 +18,8 @@ pub enum NetworkMessage { impl NetworkMessage { pub fn request(f: F) -> Self where - F: FnOnce(Client) -> Fut + Send + 'static, - Fut: Future + Send + 'static, + F: FnOnce(Client) -> Fut + WasmNotSend + 'static, + Fut: Future + WasmNotSend + 'static, { NetworkMessage::Request { request: Some(Box::new(move |c| Box::pin(f(c)))), @@ -25,8 +27,15 @@ impl NetworkMessage { } } +#[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>; // Custom clone implementation to avoid cloning the request function impl Clone for NetworkMessage { From 25b0ce6233f459b60035e758f7772fdc5750628c Mon Sep 17 00:00:00 2001 From: Timon Date: Wed, 3 Jun 2026 01:50:09 +0000 Subject: [PATCH 05/14] dedublicate resource requests --- .../document/resource/resource_message.rs | 1 + .../resource/resource_message_handler.rs | 21 ++++++++++++++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/editor/src/messages/portfolio/document/resource/resource_message.rs b/editor/src/messages/portfolio/document/resource/resource_message.rs index 6f566d945fb..af858ad017f 100644 --- a/editor/src/messages/portfolio/document/resource/resource_message.rs +++ b/editor/src/messages/portfolio/document/resource/resource_message.rs @@ -11,4 +11,5 @@ pub enum ResourceMessage { 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 38623abca9c..25323996a34 100644 --- a/editor/src/messages/portfolio/document/resource/resource_message_handler.rs +++ b/editor/src/messages/portfolio/document/resource/resource_message_handler.rs @@ -18,6 +18,7 @@ pub struct ResourceMessageContext<'a> { pub struct ResourceMessageHandler { pub registry: ResourceRegistry, pub embedded: EmbeddedResources, + pending_resolves: HashSet, } #[message_handler_data] @@ -49,10 +50,17 @@ impl MessageHandler> for ResourceMes ResourceMessage::ResolveAll => { let unresolved_ids: Vec = self.registry.unresolved().map(|info| info.id).collect(); for id in unresolved_ids { + if self.pending_resolves.contains(&id) { + continue; + } responses.add(ResourceMessage::Resolve { resource_id: id }); } } 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!("Resolve for {resource_id}: no registry entry"); return; @@ -62,6 +70,8 @@ impl MessageHandler> for ResourceMes return; } + self.pending_resolves.insert(resource_id); + let font_catalog = fonts.font_catalog.clone(); let sources = info @@ -129,6 +139,7 @@ impl MessageHandler> for ResourceMes let url = new_font_catalog .and_then(|catalog: FontCatalog| catalog.download_url(&font)) .or_else(|| font_catalog.download_url(&font)); + if let Some(url) = url { let url = match Url::parse(&url) { Ok(url) => url, @@ -147,10 +158,15 @@ impl MessageHandler> for ResourceMes } } log::error!("Resolve for {resource_id}: all sources exhausted"); - Message::NoOp + PortfolioMessage::DocumentPassMessage { + document_id, + message: ResourceMessage::ResolveFailed { resource_id }.into(), + } + .into() })); } 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; safe to drop the result. return; @@ -168,6 +184,9 @@ impl MessageHandler> for ResourceMes responses.add(NodeGraphMessage::RunDocumentGraph); } + ResourceMessage::ResolveFailed { resource_id } => { + self.pending_resolves.remove(&resource_id); + } } } From 0cbf7428d741481444073f848e4018b9e57fec03 Mon Sep 17 00:00:00 2001 From: Timon Date: Wed, 3 Jun 2026 13:01:48 +0000 Subject: [PATCH 06/14] Embedd font resources created by migration --- editor/src/messages/portfolio/document_migration.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/editor/src/messages/portfolio/document_migration.rs b/editor/src/messages/portfolio/document_migration.rs index 6d2d5f69221..8f7915b7a45 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 { From 168ee59d5ca7091f09bd3c01f560db9c14bd086c Mon Sep 17 00:00:00 2001 From: Timon Date: Wed, 3 Jun 2026 13:01:54 +0000 Subject: [PATCH 07/14] Fixup --- editor/src/messages/mod.rs | 2 +- editor/src/messages/network/utility_types.rs | 12 +++-- .../resource/resource_message_handler.rs | 50 ++++++++++++------- .../messages/portfolio/fonts/utility_types.rs | 2 +- 4 files changed, 42 insertions(+), 24 deletions(-) diff --git a/editor/src/messages/mod.rs b/editor/src/messages/mod.rs index 9c2b4c6895a..f141622838e 100644 --- a/editor/src/messages/mod.rs +++ b/editor/src/messages/mod.rs @@ -15,8 +15,8 @@ pub mod input_preprocessor; pub mod layout; pub mod menu_bar; pub mod message; -pub mod portfolio; pub mod network; +pub mod portfolio; pub mod preferences; pub mod prelude; pub mod resource_storage; diff --git a/editor/src/messages/network/utility_types.rs b/editor/src/messages/network/utility_types.rs index f8ec32e9a10..976e4263fbe 100644 --- a/editor/src/messages/network/utility_types.rs +++ b/editor/src/messages/network/utility_types.rs @@ -8,10 +8,16 @@ pub struct Client { impl Client { pub async fn fetch(&self, url: U) -> Option> { match self.inner.get(url).send().await { - Ok(response) => match response.bytes().await { - Ok(bytes) => Some(bytes.to_vec().into_boxed_slice()), + Ok(response) => match response.error_for_status() { + Ok(response) => match response.bytes().await { + Ok(bytes) => Some(bytes.to_vec().into_boxed_slice()), + Err(err) => { + log::error!("failed to read response body: {err}"); + None + } + }, Err(err) => { - log::error!("failed to read response body: {err}"); + log::error!("failed to fetch: {err}"); None } }, 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 25323996a34..1feeb466dbe 100644 --- a/editor/src/messages/portfolio/document/resource/resource_message_handler.rs +++ b/editor/src/messages/portfolio/document/resource/resource_message_handler.rs @@ -18,6 +18,7 @@ pub struct ResourceMessageContext<'a> { pub struct ResourceMessageHandler { pub registry: ResourceRegistry, pub embedded: EmbeddedResources, + #[serde(skip)] pending_resolves: HashSet, } @@ -32,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::ResolveAll); } ResourceMessage::AddFont { resource_id, font } => { let style = fonts.font_catalog.find_font_style_in_catalog(&font); @@ -45,7 +45,7 @@ impl MessageHandler> for ResourceMes style: Some(style_name), }, ); - responses.add(ResourceMessage::ResolveAll); + responses.add(ResourceMessage::Resolve { resource_id }); } ResourceMessage::ResolveAll => { let unresolved_ids: Vec = self.registry.unresolved().map(|info| info.id).collect(); @@ -114,9 +114,12 @@ impl MessageHandler> for ResourceMes } responses.add(NetworkMessage::request(move |client| async move { + let mut loaded_catalog = None; + let mut response: Option = None; for (source, hash) in sources { if let Some(hash) = hash { - return ResourceMessage::Resolved { resource_id, source, hash }.into(); + response = Some(ResourceMessage::Resolved { resource_id, source, hash }.into()); + break; } match &source { @@ -124,9 +127,8 @@ impl MessageHandler> for ResourceMes continue; } DataSource::Url(url) => { - if let Some(message) = resolve_to_message(document_id, resource_id, source.clone(), url.clone(), &client).await { - return message; - }; + response = resolve_to_message(document_id, resource_id, source.clone(), url.clone(), &client).await; + break; } DataSource::Font { family, style } => { let font = match style { @@ -134,11 +136,11 @@ impl MessageHandler> for ResourceMes None => Font::new_with_default_style(family.clone()), }; - let new_font_catalog = if font_catalog.is_empty() { FontCatalog::load_from_api(&client).await } else { None }; + if font_catalog.is_empty() && loaded_catalog.as_ref().is_none() { + loaded_catalog = FontCatalog::load_from_api(&client).await; + } - let url = new_font_catalog - .and_then(|catalog: FontCatalog| catalog.download_url(&font)) - .or_else(|| font_catalog.download_url(&font)); + 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 url = match Url::parse(&url) { @@ -148,22 +150,32 @@ impl MessageHandler> for ResourceMes continue; } }; - if let Some(message) = resolve_to_message(document_id, resource_id, source.clone(), url, &client).await { - return message; - }; + response = resolve_to_message(document_id, resource_id, source.clone(), url, &client).await; + break; } else { log::warn!("No download URL found for font resource {resource_id}"); } } } } - log::error!("Resolve for {resource_id}: all sources exhausted"); - PortfolioMessage::DocumentPassMessage { - document_id, - message: ResourceMessage::ResolveFailed { resource_id }.into(), + let responese = if let Some(response) = response { + response + } 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() { + return Message::Batched { + messages: Box::new([responese, FontsMessage::CatalogLoaded { catalog }.into()]), + }; } - .into() - })); + responese + })) } ResourceMessage::Resolved { resource_id, source, hash } => { self.pending_resolves.remove(&resource_id); diff --git a/editor/src/messages/portfolio/fonts/utility_types.rs b/editor/src/messages/portfolio/fonts/utility_types.rs index c8a99022508..e8e5604625c 100644 --- a/editor/src/messages/portfolio/fonts/utility_types.rs +++ b/editor/src/messages/portfolio/fonts/utility_types.rs @@ -39,7 +39,7 @@ impl FontCatalog { .variants .iter() .filter_map(|variant| { - let weight = if variant == "regular" || variant == "italic" { 400 } else { variant.parse::().ok()? }; + 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 }) From 015343d8fa946f6bc95d33f8797d20254bab1df2 Mon Sep 17 00:00:00 2001 From: Timon Date: Wed, 3 Jun 2026 13:30:53 +0000 Subject: [PATCH 08/14] Fix tests --- editor/src/messages/portfolio/portfolio_message_handler.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/editor/src/messages/portfolio/portfolio_message_handler.rs b/editor/src/messages/portfolio/portfolio_message_handler.rs index 166e696c0ee..146b7fc7e33 100644 --- a/editor/src/messages/portfolio/portfolio_message_handler.rs +++ b/editor/src/messages/portfolio/portfolio_message_handler.rs @@ -422,11 +422,6 @@ impl MessageHandler> for Portfolio } } PortfolioMessage::ResolveDocumentResources { document_id } => { - if self.fonts.font_catalog.is_empty() { - responses.add_front(FontsMessage::LoadCatalog); - return; - } - responses.add(PortfolioMessage::DocumentPassMessage { document_id, message: DocumentMessage::Resource(ResourceMessage::ResolveAll), From 17aefe15b59ad5a4fbb869463078dace820762e8 Mon Sep 17 00:00:00 2001 From: Timon Date: Wed, 3 Jun 2026 15:11:25 +0000 Subject: [PATCH 09/14] Fix font catalog not loading --- .../src/messages/tool/tool_messages/text_tool.rs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/editor/src/messages/tool/tool_messages/text_tool.rs b/editor/src/messages/tool/tool_messages/text_tool.rs index 3e92d283ae3..dd842cdef6d 100644 --- a/editor/src/messages/tool/tool_messages/text_tool.rs +++ b/editor/src/messages/tool/tool_messages/text_tool.rs @@ -290,6 +290,11 @@ 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); + return; + } + let options = match message { ToolMessage::Text(TextToolMessage::UpdateOptions { options }) => options, ToolMessage::Text(TextToolMessage::SelectionChanged) => { @@ -598,14 +603,11 @@ 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 { + let text_layers: Vec<_> = document.metadata().all_layers().filter(|&layer| document.metadata().is_text_layer(layer)).collect(); + text_layers.into_iter().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) { From db2b737632259211d0f5a385146a107f0d24e5cf Mon Sep 17 00:00:00 2001 From: Timon Date: Wed, 3 Jun 2026 15:11:40 +0000 Subject: [PATCH 10/14] Cleanup --- .../src/messages/network/network_message.rs | 1 - editor/src/messages/network/utility_types.rs | 23 ++++--------------- .../resource/resource_message_handler.rs | 12 ++++------ .../portfolio/portfolio_message_handler.rs | 1 - 4 files changed, 9 insertions(+), 28 deletions(-) diff --git a/editor/src/messages/network/network_message.rs b/editor/src/messages/network/network_message.rs index 98b0cc23abc..8eb46b7cdb9 100644 --- a/editor/src/messages/network/network_message.rs +++ b/editor/src/messages/network/network_message.rs @@ -37,7 +37,6 @@ type RequestFn = Box RequestFuture + Send>; #[cfg(target_family = "wasm")] type RequestFn = Box RequestFuture>; -// Custom clone implementation to avoid cloning the request function impl Clone for NetworkMessage { fn clone(&self) -> Self { match self { diff --git a/editor/src/messages/network/utility_types.rs b/editor/src/messages/network/utility_types.rs index 976e4263fbe..2d738cb98e0 100644 --- a/editor/src/messages/network/utility_types.rs +++ b/editor/src/messages/network/utility_types.rs @@ -7,24 +7,9 @@ pub struct Client { impl Client { pub async fn fetch(&self, url: U) -> Option> { - match self.inner.get(url).send().await { - Ok(response) => match response.error_for_status() { - Ok(response) => match response.bytes().await { - Ok(bytes) => Some(bytes.to_vec().into_boxed_slice()), - Err(err) => { - log::error!("failed to read response body: {err}"); - None - } - }, - Err(err) => { - log::error!("failed to fetch: {err}"); - None - } - }, - Err(err) => { - log::error!("failed to fetch: {err}"); - None - } - } + let response = self.inner.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_handler.rs b/editor/src/messages/portfolio/document/resource/resource_message_handler.rs index 1feeb466dbe..5bcbce4adff 100644 --- a/editor/src/messages/portfolio/document/resource/resource_message_handler.rs +++ b/editor/src/messages/portfolio/document/resource/resource_message_handler.rs @@ -123,9 +123,7 @@ impl MessageHandler> for ResourceMes } match &source { - DataSource::Embedded => { - continue; - } + DataSource::Embedded => continue, DataSource::Url(url) => { response = resolve_to_message(document_id, resource_id, source.clone(), url.clone(), &client).await; break; @@ -158,7 +156,7 @@ impl MessageHandler> for ResourceMes } } } - let responese = if let Some(response) = response { + let response = if let Some(response) = response { response } else { log::error!("Resolve for {resource_id}: all sources exhausted"); @@ -171,16 +169,16 @@ impl MessageHandler> for ResourceMes if let Some(catalog) = loaded_catalog.take() { return Message::Batched { - messages: Box::new([responese, FontsMessage::CatalogLoaded { catalog }.into()]), + messages: Box::new([response, FontsMessage::CatalogLoaded { catalog }.into()]), }; } - responese + response })) } 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; safe to drop the result. + // Resource was removed from registry after the fetch started. return; } diff --git a/editor/src/messages/portfolio/portfolio_message_handler.rs b/editor/src/messages/portfolio/portfolio_message_handler.rs index 146b7fc7e33..cad6354eca6 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 { From 6f4e6c973c88f1e899bf7eac8e7b2da37a1bf42d Mon Sep 17 00:00:00 2001 From: Timon Date: Wed, 3 Jun 2026 17:55:23 +0000 Subject: [PATCH 11/14] Fix layouts not updating when font catalog is loaded --- editor/src/messages/portfolio/fonts/fonts_message_handler.rs | 2 ++ editor/src/messages/tool/tool_messages/text_tool.rs | 1 - 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/editor/src/messages/portfolio/fonts/fonts_message_handler.rs b/editor/src/messages/portfolio/fonts/fonts_message_handler.rs index b9ae4bff49a..3b4f49cd26a 100644 --- a/editor/src/messages/portfolio/fonts/fonts_message_handler.rs +++ b/editor/src/messages/portfolio/fonts/fonts_message_handler.rs @@ -35,6 +35,8 @@ impl MessageHandler> for FontsMessageHandl FontsMessage::CatalogLoaded { 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); diff --git a/editor/src/messages/tool/tool_messages/text_tool.rs b/editor/src/messages/tool/tool_messages/text_tool.rs index dd842cdef6d..108f4012309 100644 --- a/editor/src/messages/tool/tool_messages/text_tool.rs +++ b/editor/src/messages/tool/tool_messages/text_tool.rs @@ -292,7 +292,6 @@ impl<'a> MessageHandler> for Text if context.fonts.font_catalog.is_empty() { responses.add_front(FontsMessage::LoadCatalog); - return; } let options = match message { From 0c65b265fdefc52e029e6d3cbbe74b4460acf863 Mon Sep 17 00:00:00 2001 From: Timon Date: Wed, 3 Jun 2026 20:35:30 +0000 Subject: [PATCH 12/14] Review --- .../src/messages/network/network_message.rs | 5 +++- editor/src/messages/network/utility_types.rs | 13 +++++++++- .../resource/resource_message_handler.rs | 24 +++++++++---------- .../messages/tool/tool_messages/text_tool.rs | 3 +-- 4 files changed, 28 insertions(+), 17 deletions(-) diff --git a/editor/src/messages/network/network_message.rs b/editor/src/messages/network/network_message.rs index 8eb46b7cdb9..6a6a8532340 100644 --- a/editor/src/messages/network/network_message.rs +++ b/editor/src/messages/network/network_message.rs @@ -40,7 +40,10 @@ type RequestFn = Box RequestFuture>; impl Clone for NetworkMessage { fn clone(&self) -> Self { match self { - NetworkMessage::Request { .. } => NetworkMessage::Request { request: None }, + NetworkMessage::Request { .. } => { + log::error!("Cloning a NetworkMessage::Request is not supported"); + NetworkMessage::Request { request: None } + } } } } diff --git a/editor/src/messages/network/utility_types.rs b/editor/src/messages/network/utility_types.rs index 2d738cb98e0..d605d3b1893 100644 --- a/editor/src/messages/network/utility_types.rs +++ b/editor/src/messages/network/utility_types.rs @@ -1,10 +1,21 @@ use reqwest::IntoUrl; -#[derive(Debug, Default, Clone)] +#[derive(Debug, Clone)] pub struct Client { inner: reqwest::Client, } +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().unwrap_or_default(), + #[cfg(target_family = "wasm")] + inner: reqwest::Client::new(), + } + } +} + impl Client { pub async fn fetch(&self, url: U) -> Option> { let response = self.inner.get(url).send().await; 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 5bcbce4adff..24f506e197a 100644 --- a/editor/src/messages/portfolio/document/resource/resource_message_handler.rs +++ b/editor/src/messages/portfolio/document/resource/resource_message_handler.rs @@ -126,7 +126,6 @@ impl MessageHandler> for ResourceMes DataSource::Embedded => continue, DataSource::Url(url) => { response = resolve_to_message(document_id, resource_id, source.clone(), url.clone(), &client).await; - break; } DataSource::Font { family, style } => { let font = match style { @@ -141,37 +140,36 @@ impl MessageHandler> for ResourceMes 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 url = match Url::parse(&url) { - Ok(url) => url, - Err(err) => { - log::warn!("Invalid URL {url} for font resource {resource_id}: {err}"); - continue; - } + 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; - break; } else { log::warn!("No download URL found for font resource {resource_id}"); } } } + if response.is_some() { + break; + } } - let response = if let Some(response) = response { - response - } else { + + 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() { - return Message::Batched { + response = Message::Batched { messages: Box::new([response, FontsMessage::CatalogLoaded { catalog }.into()]), }; } + response })) } diff --git a/editor/src/messages/tool/tool_messages/text_tool.rs b/editor/src/messages/tool/tool_messages/text_tool.rs index 108f4012309..9e4c4700c27 100644 --- a/editor/src/messages/tool/tool_messages/text_tool.rs +++ b/editor/src/messages/tool/tool_messages/text_tool.rs @@ -602,8 +602,7 @@ 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 text_layers: Vec<_> = document.metadata().all_layers().filter(|&layer| document.metadata().is_text_layer(layer)).collect(); - text_layers.into_iter().find(|&layer| { + 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); transformed_quad.contains(mouse) }) From aa7273c2e358047a4ecb234c2683fa729aceb096 Mon Sep 17 00:00:00 2001 From: Timon Date: Wed, 3 Jun 2026 20:44:57 +0000 Subject: [PATCH 13/14] Review --- editor/src/messages/network/utility_types.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/editor/src/messages/network/utility_types.rs b/editor/src/messages/network/utility_types.rs index d605d3b1893..78fda01692d 100644 --- a/editor/src/messages/network/utility_types.rs +++ b/editor/src/messages/network/utility_types.rs @@ -2,23 +2,27 @@ use reqwest::IntoUrl; #[derive(Debug, Clone)] pub struct Client { - inner: reqwest::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().unwrap_or_default(), + inner: reqwest::Client::builder().timeout(std::time::Duration::from_secs(100)).build().ok(), #[cfg(target_family = "wasm")] - inner: reqwest::Client::new(), + inner: reqwest::Client::builder().build().ok(), } } } impl Client { pub async fn fetch(&self, url: U) -> Option> { - let response = self.inner.get(url).send().await; + 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()) From af644d5476fbe67264f788210547256c0b41857c Mon Sep 17 00:00:00 2001 From: Timon Date: Thu, 4 Jun 2026 13:23:01 +0000 Subject: [PATCH 14/14] Cleanup --- .../portfolio/document/resource/resource_message_handler.rs | 2 +- editor/src/messages/portfolio/fonts/fonts_message_handler.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 24f506e197a..7a450601496 100644 --- a/editor/src/messages/portfolio/document/resource/resource_message_handler.rs +++ b/editor/src/messages/portfolio/document/resource/resource_message_handler.rs @@ -113,7 +113,7 @@ impl MessageHandler> for ResourceMes } } - responses.add(NetworkMessage::request(move |client| async move { + responses.add(NetworkMessage::request(async move |client| { let mut loaded_catalog = None; let mut response: Option = None; for (source, hash) in sources { diff --git a/editor/src/messages/portfolio/fonts/fonts_message_handler.rs b/editor/src/messages/portfolio/fonts/fonts_message_handler.rs index 3b4f49cd26a..fe16ab4775f 100644 --- a/editor/src/messages/portfolio/fonts/fonts_message_handler.rs +++ b/editor/src/messages/portfolio/fonts/fonts_message_handler.rs @@ -24,7 +24,7 @@ impl MessageHandler> for FontsMessageHandl match message { FontsMessage::LoadCatalog => { - responses.add(NetworkMessage::request(|client| async move { + 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;