mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 14:18:04 +08:00
Introduce network request handler (#4194)
* Impl NetworkMessageHandler * Repalce Frontend fetch like messages with NetworkMessage * Reimpl resource loading with NetworkMessage * Fix wasm * dedublicate resource requests * Embedd font resources created by migration * Fixup * Fix tests * Fix font catalog not loading * Cleanup * Fix layouts not updating when font catalog is loaded * Review * Review * Cleanup
This commit is contained in:
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -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",
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")]
|
||||
|
||||
@@ -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<Box<dyn Future<Output = Message> + Send + 'static>>;
|
||||
#[cfg(target_family = "wasm")]
|
||||
type InnerMessageFuture = Pin<Box<dyn Future<Output = Message> + 'static>>;
|
||||
|
||||
/// Invoked by the spawner after a result is sent, to wake the platform event loop.
|
||||
pub type Wake = Arc<dyn Fn() + Send + Sync>;
|
||||
@@ -23,7 +27,7 @@ pub struct MessageFuture {
|
||||
}
|
||||
|
||||
impl MessageFuture {
|
||||
pub fn new(future: impl Future<Output = Message> + Send + 'static) -> Self {
|
||||
pub fn new(future: impl Future<Output = Message> + WasmNotSend + 'static) -> Self {
|
||||
Self {
|
||||
inner: Arc::new(Mutex::new(Some(Box::pin(future)))),
|
||||
}
|
||||
@@ -51,7 +55,7 @@ impl From<MessageFuture> for Message {
|
||||
|
||||
impl<T> From<T> for Message
|
||||
where
|
||||
T: Future<Output = Message> + Send + 'static,
|
||||
T: Future<Output = Message> + WasmNotSend + 'static,
|
||||
{
|
||||
fn from(future: T) -> Self {
|
||||
MessageFuture::new(future).into()
|
||||
|
||||
@@ -34,6 +34,8 @@ pub enum Message {
|
||||
#[child]
|
||||
MenuBar(MenuBarMessage),
|
||||
#[child]
|
||||
Network(NetworkMessage),
|
||||
#[child]
|
||||
Portfolio(PortfolioMessage),
|
||||
#[child]
|
||||
Preferences(PreferencesMessage),
|
||||
|
||||
@@ -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;
|
||||
|
||||
10
editor/src/messages/network/mod.rs
Normal file
10
editor/src/messages/network/mod.rs
Normal file
@@ -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;
|
||||
49
editor/src/messages/network/network_message.rs
Normal file
49
editor/src/messages/network/network_message.rs
Normal file
@@ -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<RequestFn>,
|
||||
},
|
||||
}
|
||||
impl NetworkMessage {
|
||||
pub fn request<F, Fut>(f: F) -> Self
|
||||
where
|
||||
F: FnOnce(Client) -> Fut + WasmNotSend + 'static,
|
||||
Fut: Future<Output = Message> + WasmNotSend + 'static,
|
||||
{
|
||||
NetworkMessage::Request {
|
||||
request: Some(Box::new(move |c| Box::pin(f(c)))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
type RequestFuture = Pin<Box<dyn Future<Output = Message> + Send>>;
|
||||
#[cfg(target_family = "wasm")]
|
||||
type RequestFuture = Pin<Box<dyn Future<Output = Message>>>;
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
type RequestFn = Box<dyn FnOnce(Client) -> RequestFuture + Send>;
|
||||
#[cfg(target_family = "wasm")]
|
||||
type RequestFn = Box<dyn FnOnce(Client) -> 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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
27
editor/src/messages/network/network_message_handler.rs
Normal file
27
editor/src/messages/network/network_message_handler.rs
Normal file
@@ -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<NetworkMessage, NetworkMessageContext> for NetworkMessageHandler {
|
||||
fn process_message(&mut self, message: NetworkMessage, responses: &mut VecDeque<Message>, _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;
|
||||
);
|
||||
}
|
||||
30
editor/src/messages/network/utility_types.rs
Normal file
30
editor/src/messages/network/utility_types.rs
Normal file
@@ -0,0 +1,30 @@
|
||||
use reqwest::IntoUrl;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Client {
|
||||
inner: Option<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().ok(),
|
||||
#[cfg(target_family = "wasm")]
|
||||
inner: reqwest::Client::builder().build().ok(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Client {
|
||||
pub async fn fetch<U: IntoUrl>(&self, url: U) -> Option<Box<[u8]>> {
|
||||
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())
|
||||
}
|
||||
}
|
||||
@@ -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 },
|
||||
}
|
||||
|
||||
@@ -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<ResourceId, Option<ResolveProgress>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
struct ResolveProgress {
|
||||
index: usize,
|
||||
source: DataSource,
|
||||
pending_resolves: HashSet<ResourceId>,
|
||||
}
|
||||
|
||||
#[message_handler_data]
|
||||
@@ -36,7 +33,6 @@ impl MessageHandler<ResourceMessage, ResourceMessageContext<'_>> 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<ResourceMessage, ResourceMessageContext<'_>> for ResourceMes
|
||||
style: Some(style_name),
|
||||
},
|
||||
);
|
||||
responses.add(ResourceMessage::Resolve);
|
||||
responses.add(ResourceMessage::Resolve { resource_id });
|
||||
}
|
||||
ResourceMessage::Resolve => {
|
||||
ResourceMessage::ResolveAll => {
|
||||
let unresolved_ids: Vec<ResourceId> = self.registry.unresolved().map(|info| info.id).collect();
|
||||
for id in unresolved_ids {
|
||||
if self.pending_resolves.contains_key(&id) {
|
||||
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 });
|
||||
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)
|
||||
}
|
||||
source => (source.clone(), None),
|
||||
})
|
||||
.collect::<Vec<(DataSource, Option<ResourceHash>)>>();
|
||||
|
||||
async fn resolve_to_message(document_id: DocumentId, resource_id: ResourceId, source: DataSource, url: Url, client: &Client) -> Option<Message> {
|
||||
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
|
||||
}
|
||||
}
|
||||
DataSource::Url(url) => {
|
||||
responses.add(FrontendMessage::TriggerResolveResource {
|
||||
}
|
||||
|
||||
responses.add(NetworkMessage::request(async move |client| {
|
||||
let mut loaded_catalog = None;
|
||||
let mut response: Option<Message> = 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,
|
||||
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),
|
||||
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()]),
|
||||
};
|
||||
if let Some(hash) = fonts.cached_hash(&font) {
|
||||
self.registry.resolve(&resource_id, hash);
|
||||
self.pending_resolves.remove(&resource_id);
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
return;
|
||||
}
|
||||
if let Some(url) = fonts.cached_url(&font) {
|
||||
responses.add(FrontendMessage::TriggerResolveResource { document_id, resource_id, url });
|
||||
return;
|
||||
}
|
||||
responses.add(FrontendMessage::TriggerFontCatalogLoad);
|
||||
self.pending_resolves.remove(&resource_id);
|
||||
}
|
||||
}
|
||||
|
||||
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<ResourceMessage, ResourceMessageContext<'_>> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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<FontCatalog>,
|
||||
font_hashes: HashMap<Font, ResourceHash>,
|
||||
font_data: HashMap<ResourceHash, Resource>,
|
||||
}
|
||||
@@ -22,9 +23,20 @@ impl MessageHandler<FontsMessage, FontsMessageContext<'_>> 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<String> {
|
||||
let font = self.normalize(font.clone());
|
||||
self.font_catalog.download_url(&font)
|
||||
}
|
||||
|
||||
pub fn get_resource_or_queue_load(&self, font: &Font, responses: &mut VecDeque<Message>) -> Resource {
|
||||
let font = self.normalize(font.clone());
|
||||
if let Some(hash) = self.font_hashes.get(&font) {
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<FontListApiFamily>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct FontListApiFamily {
|
||||
family: String,
|
||||
variants: Vec<String>,
|
||||
files: HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl FontCatalog {
|
||||
pub async fn load_from_api(client: &Client) -> Option<Self> {
|
||||
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::<String>().parse::<u32>().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<Item = &FontCatalogFamily> {
|
||||
self.0.iter()
|
||||
}
|
||||
@@ -41,7 +101,7 @@ impl From<Vec<FontCatalogFamily>> 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.
|
||||
|
||||
@@ -399,7 +399,6 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> 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<PortfolioMessage, PortfolioMessageContext<'_>> 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 } => {
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -290,6 +290,10 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> 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<Message>) -> Option<LayerNodeIdentifier> {
|
||||
let mouse = DVec2::new(input.mouse.position.x, input.mouse.position.y);
|
||||
let layers: Vec<LayerNodeIdentifier> = 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<Message>) {
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -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<string, string> }[];
|
||||
|
||||
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);
|
||||
});
|
||||
@@ -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<FontCatalogFamily>) {
|
||||
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<u8>) -> 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) {
|
||||
|
||||
Reference in New Issue
Block a user