Migrate fonts to be stored as resources (#4165)

* Good start

* Impl

* allow responses.add(async { Message::NoOp });

* Fix

* Cache font blob in text node

* Refactor font handling to use Font struct direcly

* Fix fmt

* Embedd Font Resources by default

* Review

* Review

* Cache font info based on ResourceHash
This commit is contained in:
Timon
2026-06-03 09:21:51 +00:00
committed by Keavon Chambers
parent 7b9c480a8d
commit cd8ea1f554
46 changed files with 838 additions and 703 deletions

View File

@@ -65,7 +65,6 @@ const SIDE_EFFECT_FREE_MESSAGES: &[MessageDiscriminant] = &[
))),
MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::SubmitActiveGraphRender),
MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::SubmitEyedropperPreviewRender),
MessageDiscriminant::Frontend(FrontendMessageDiscriminant::TriggerFontDataLoad),
MessageDiscriminant::Frontend(FrontendMessageDiscriminant::UpdateUIScale),
];
/// Since we don't need to update the frontend multiple times per frame,
@@ -80,6 +79,8 @@ const FRONTEND_UPDATE_MESSAGES: &[MessageDiscriminant] = &[
MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::Document(DocumentMessageDiscriminant::RenderScrollbars)),
MessageDiscriminant::Frontend(FrontendMessageDiscriminant::UpdateDocumentLayerStructure),
];
// FrontendMessages that should be sent immediately
const IMMEDIATE_FRONTEND_MESSAGES: &[FrontendMessageDiscriminant] = &[FrontendMessageDiscriminant::TriggerResolveResource, FrontendMessageDiscriminant::TriggerFontCatalogLoad];
const DEBUG_MESSAGE_BLOCK_LIST: &[MessageDiscriminant] = &[
MessageDiscriminant::Broadcast(BroadcastMessageDiscriminant::TriggerEvent(EventMessageDiscriminant::AnimationFrame)),
MessageDiscriminant::Animation(AnimationMessageDiscriminant::IncrementFrameCounter),
@@ -205,16 +206,13 @@ impl Dispatcher {
self.message_handlers.dialog_message_handler.process_message(message, &mut queue, context);
}
Message::Frontend(message) => {
// Handle these messages immediately by returning early
if let FrontendMessage::TriggerFontDataLoad { .. } | FrontendMessage::TriggerFontCatalogLoad = message {
self.responses.push(message);
self.cleanup_queues(false);
let decreminant = message.to_discriminant();
self.responses.push(message);
// Return early to avoid running the code after the match block
// Handle these message immediately by returning early
if IMMEDIATE_FRONTEND_MESSAGES.contains(&decreminant) {
self.cleanup_queues(false);
return;
} else {
// `FrontendMessage`s are saved and will be sent to the frontend after the message queue is done being processed
self.responses.push(message);
}
}
Message::InputPreprocessor(message) => {
@@ -325,7 +323,7 @@ impl Dispatcher {
document_id,
document,
input: &self.message_handlers.input_preprocessor_message_handler,
cached_data: &self.message_handlers.portfolio_message_handler.cached_data,
fonts: &self.message_handlers.portfolio_message_handler.fonts,
node_graph: &self.message_handlers.portfolio_message_handler.executor,
preferences: &self.message_handlers.preferences_message_handler,
viewport: &self.message_handlers.viewport_message_handler,

View File

@@ -12,10 +12,10 @@ use crate::messages::portfolio::document::utility_types::wires::{WirePath, WireP
use crate::messages::portfolio::utility_types::WorkspacePanelLayout;
use crate::messages::prelude::*;
use crate::messages::tool::tool_messages::eyedropper_tool::PrimarySecondary;
use graph_craft::application_io::resource::ResourceId;
use graph_craft::document::NodeId;
use graphene_std::color::SRGBA8;
use graphene_std::raster::Image;
use graphene_std::text::Font;
use graphene_std::vector::style::FillChoiceUI;
use std::path::PathBuf;
@@ -112,8 +112,11 @@ pub enum FrontendMessage {
filename: String,
},
TriggerFontCatalogLoad,
TriggerFontDataLoad {
font: Font,
TriggerResolveResource {
#[serde(rename = "documentId")]
document_id: DocumentId,
#[serde(rename = "resourceId")]
resource_id: ResourceId,
url: String,
},
TriggerPersistenceReadState,

View File

@@ -43,6 +43,21 @@ impl IntoFuture for MessageFuture {
}
}
impl From<MessageFuture> for Message {
fn from(future: MessageFuture) -> Self {
FutureMessage::Await { future }.into()
}
}
impl<T> From<T> for Message
where
T: Future<Output = Message> + Send + 'static,
{
fn from(future: T) -> Self {
MessageFuture::new(future).into()
}
}
/// Platform-specific async-task executor.
/// Runs `future`, sends the resolved message on `results`, then calls `wake`.
pub trait MessageSpawner: Send + Sync {

View File

@@ -21,7 +21,7 @@ use crate::messages::portfolio::document::properties_panel::properties_panel_mes
use crate::messages::portfolio::document::utility_types::document_metadata::{DocumentMetadata, LayerNodeIdentifier};
use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate, AlignAxis, FlipAxis, PTZ};
use crate::messages::portfolio::document::utility_types::network_interface::{FlowType, InputConnector, NodeTemplate, OutputConnector};
use crate::messages::portfolio::utility_types::{CachedData, PanelType};
use crate::messages::portfolio::utility_types::PanelType;
use crate::messages::prelude::*;
use crate::messages::tool::common_functionality::graph_modification_utils::{self, get_blend_mode, get_fill, get_opacity};
use crate::messages::tool::tool_messages::select_tool::SelectToolPointerKeys;
@@ -52,7 +52,6 @@ use std::time::Duration;
pub struct DocumentMessageContext<'a> {
pub document_id: DocumentId,
pub ipp: &'a InputPreprocessorMessageHandler,
pub cached_data: &'a CachedData,
pub executor: &'a mut NodeGraphExecutor,
pub current_tool: &'a ToolType,
pub preferences: &'a PreferencesMessageHandler,
@@ -61,6 +60,7 @@ pub struct DocumentMessageContext<'a> {
pub properties_panel_open: bool,
pub viewport: &'a ViewportMessageHandler,
pub resource_storage: &'a ResourceStorageMessageHandler,
pub fonts: &'a FontsMessageHandler,
}
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, ExtractField)]
@@ -198,7 +198,6 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
let DocumentMessageContext {
document_id,
ipp,
cached_data,
executor,
viewport,
current_tool,
@@ -207,6 +206,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
layers_panel_open,
properties_panel_open,
resource_storage,
fonts,
} = context;
match message {
@@ -233,11 +233,12 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
}
DocumentMessage::PropertiesPanel(message) => {
let context = PropertiesPanelMessageContext {
executor,
network_interface: &mut self.network_interface,
resources: &self.resources,
selection_network_path: &self.selection_network_path,
document_name: self.name.as_str(),
executor,
cached_data,
fonts,
properties_panel_open,
};
self.properties_panel_message_handler.process_message(message, responses, context);
@@ -282,7 +283,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
graph_operation_message_handler.process_message(message, responses, context);
}
DocumentMessage::Resource(message) => {
let context = ResourceMessageContext {};
let context = ResourceMessageContext { document_id, fonts };
self.resources.process_message(message, responses, context);
}
DocumentMessage::AlignSelectedLayers { axis, aggregate } => {
@@ -933,21 +934,19 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
let mut document = self.clone();
let resources_load_handle = resource_storage.resources();
responses.add(FutureMessage::Await {
future: MessageFuture::new(async move {
document.resources.garbage_collect(document.used_resources(false).as_ref());
document.resources.embed_resources(resources_load_handle).await;
responses.add(async move {
document.resources.collect_garbage(document.used_resources(false).as_ref());
document.resources.embed_resources(resources_load_handle).await;
let content = document.serialize_document().into_bytes().into();
let content = document.serialize_document().into_bytes().into();
Message::Frontend(FrontendMessage::TriggerSaveDocument {
document_id,
name,
path,
folder,
content,
})
}),
Message::Frontend(FrontendMessage::TriggerSaveDocument {
document_id,
name,
path,
folder,
content,
})
});
}
DocumentMessage::SavedDocument { path } => {
@@ -2658,23 +2657,6 @@ impl DocumentMessageHandler {
}
}
/// Loads all of the fonts in the document.
pub fn load_layer_resources(&self, responses: &mut VecDeque<Message>) {
let mut fonts_to_load = HashSet::new();
for (_, node, _) in self.document_network().recursive_nodes() {
for input in &node.inputs {
if let Some(TaggedValue::Font(font)) = input.as_value() {
fonts_to_load.insert(font.clone());
}
}
}
for font in fonts_to_load {
responses.add(PortfolioMessage::LoadFontData { font });
}
}
pub fn update_document_widgets(&self, responses: &mut VecDeque<Message>, animation_is_playing: bool, time: Duration) {
let mut snapping_state = self.snapping_state.clone();
let mut snapping_state2 = self.snapping_state.clone();
@@ -3470,7 +3452,7 @@ impl DocumentMessageHandler {
pub fn garbage_collect_resources(&mut self) {
let used_resources = self.used_resources(true);
self.resources.garbage_collect(&used_resources);
self.resources.collect_garbage(&used_resources);
}
pub fn used_resources(&self, include_history: bool) -> Box<[ResourceId]> {

View File

@@ -249,12 +249,13 @@ impl<'a> ModifyInputsContext<'a> {
}
pub fn insert_text(&mut self, text: String, font: Font, typesetting: TypesettingConfig, layer: LayerNodeIdentifier) {
let font_resource_id = ResourceId::new();
let text = resolve_proto_node_type(graphene_std::text::text::IDENTIFIER)
.expect("Text node does not exist")
.node_template_input_override([
Some(NodeInput::scope("editor-api")),
Some(NodeInput::value(TaggedValue::None, false)),
Some(NodeInput::value(TaggedValue::String(text), false)),
Some(NodeInput::value(TaggedValue::Font(font), false)),
Some(NodeInput::value(TaggedValue::Resource(font_resource_id), false)),
Some(NodeInput::value(TaggedValue::F64(typesetting.font_size), false)),
Some(NodeInput::value(TaggedValue::F64(typesetting.line_height_ratio), false)),
Some(NodeInput::value(TaggedValue::F64(typesetting.character_spacing), false)),
@@ -264,6 +265,7 @@ impl<'a> ModifyInputsContext<'a> {
Some(NodeInput::value(TaggedValue::F64(typesetting.max_height.unwrap_or(100.)), false)),
Some(NodeInput::value(TaggedValue::F64(typesetting.tilt), false)),
Some(NodeInput::value(TaggedValue::TextAlign(typesetting.align), false)),
Some(NodeInput::value(TaggedValue::Bool(false), false)),
]);
let transform = resolve_proto_node_type(graphene_std::transform_nodes::transform::IDENTIFIER)
.expect("Transform node does not exist")
@@ -276,6 +278,8 @@ impl<'a> ModifyInputsContext<'a> {
self.network_interface.insert_node(text_id, text, &[]);
self.network_interface.move_node_to_chain_start(&text_id, layer, &[], self.import);
self.responses.add(DocumentMessage::Resource(ResourceMessage::AddFont { resource_id: font_resource_id, font }));
let transform_id = NodeId::new();
self.network_interface.insert_node(transform_id, transform, &[]);
self.network_interface.move_node_to_chain_start(&transform_id, layer, &[], self.import);

View File

@@ -8,8 +8,7 @@ use crate::messages::portfolio::document::utility_types::network_interface::{
DocumentNodeMetadata, DocumentNodePersistentMetadata, InputMetadata, NodeNetworkInterface, NodeNetworkMetadata, NodeNetworkPersistentMetadata, NodeTemplate, NodeTypePersistentMetadata,
Vec2InputSettings, WidgetOverride,
};
use crate::messages::portfolio::utility_types::CachedData;
use crate::messages::prelude::Message;
use crate::messages::prelude::{FontsMessageHandler, Message, ResourceMessageHandler};
use crate::node_graph_executor::NodeGraphExecutor;
use glam::DVec2;
use graph_craft::ProtoNodeIdentifier;
@@ -28,10 +27,11 @@ use serde_json::Value;
use std::collections::{HashMap, VecDeque};
pub struct NodePropertiesContext<'a> {
pub cached_data: &'a CachedData,
pub responses: &'a mut VecDeque<Message>,
pub executor: &'a mut NodeGraphExecutor,
pub network_interface: &'a mut NodeNetworkInterface,
pub resources: &'a ResourceMessageHandler,
pub fonts: &'a FontsMessageHandler,
pub selection_network_path: &'a [NodeId],
pub document_name: &'a str,
}

View File

@@ -6,12 +6,13 @@ use crate::messages::layout::utility_types::widget_prelude::*;
use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeNetworkInterface};
use crate::messages::portfolio::utility_types::{CachedData, FontCatalogStyle};
use crate::messages::portfolio::fonts::utility_types::FontCatalogStyle;
use crate::messages::prelude::*;
use crate::messages::tool::common_functionality::graph_modification_utils;
use choice::enum_choice;
use dyn_any::DynAny;
use glam::{DAffine2, DVec2};
use graph_craft::application_io::resource::ResourceId;
use graph_craft::document::value::TaggedValue;
use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeId, NodeInput};
use graph_craft::{Type, concrete};
@@ -822,11 +823,27 @@ pub fn array_of_number_widget(parameter_widgets_info: ParameterWidgetsInfo, text
}
pub fn font_inputs(parameter_widgets_info: ParameterWidgetsInfo) -> (Vec<WidgetInstance>, Option<Vec<WidgetInstance>>) {
pub fn assign_font_message(node_id: NodeId, font: Font) -> Message {
let resource_id = ResourceId::new();
Message::Batched {
messages: Box::new([
DocumentMessage::Resource(ResourceMessage::AddFont { resource_id, font }).into(),
NodeGraphMessage::SetInputValue {
node_id,
input_index: graphene_std::text::text::FontInput::INDEX,
value: TaggedValue::Resource(resource_id),
}
.into(),
]),
}
}
let ParameterWidgetsInfo {
cached_data,
document_node,
node_id,
index,
resources,
fonts,
..
} = parameter_widgets_info;
@@ -839,66 +856,32 @@ pub fn font_inputs(parameter_widgets_info: ParameterWidgetsInfo) -> (Vec<WidgetI
return (vec![], None);
};
if let Some(TaggedValue::Font(font)) = &input.as_non_exposed_value() {
if let Some(TaggedValue::Resource(resource_id)) = input.as_non_exposed_value() {
let font = fonts.id_font(resources, *resource_id).unwrap_or_default();
first_widgets.extend_from_slice(&[
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
DropdownInput::new(vec![
cached_data
fonts
.font_catalog
.0
.iter()
.map(|family| {
let FontCatalogStyle { weight, italic, .. } = FontCatalogStyle::from_named_style(&font.font_style, "");
let new_font = Font::new(family.name.clone(), family.closest_style(weight, italic).to_named_style());
let commit_font = new_font.clone();
MenuListEntry::new(family.name.clone())
.label(family.name.clone())
.font(family.closest_style(400, false).preview_url(&family.name))
.on_update({
// Construct the new font using the new family and the initial or previous style, although this style might not exist in the catalog
let mut new_font = Font::new(family.name.clone(), font.font_style_to_restore.clone().unwrap_or_else(|| font.font_style.clone()));
new_font.font_style_to_restore = font.font_style_to_restore.clone();
// If not already, store the initial style so it can be restored if the user switches to another family
if new_font.font_style_to_restore.is_none() {
new_font.font_style_to_restore = Some(new_font.font_style.clone());
}
// Use the closest style available in the family for the new font to ensure the style exists
let FontCatalogStyle { weight, italic, .. } = FontCatalogStyle::from_named_style(&new_font.font_style, "");
new_font.font_style = family.closest_style(weight, italic).to_named_style();
move |_| {
let new_font = new_font.clone();
Message::Batched {
messages: Box::new([
PortfolioMessage::LoadFontData { font: new_font.clone() }.into(),
update_value(move |_| TaggedValue::Font(new_font.clone()), node_id, index)(&()),
]),
}
}
})
.on_commit({
// Use the new value from the user selection
let font_family = family.name.clone();
// Use the previous style selection and extract its weight and italic properties, then find the closest style in the new family
let FontCatalogStyle { weight, italic, .. } = FontCatalogStyle::from_named_style(&font.font_style, "");
let font_style = family.closest_style(weight, italic).to_named_style();
move |_| {
// Intentionally drop `font_style_to_restore` on commit so the committed style becomes the new basis
// for subsequent family switches. Preserving the original style intent is hover-only behavior.
let new_font = Font::new(font_family.clone(), font_style.clone());
DeferMessage::AfterGraphRun {
messages: vec![update_value(move |_| TaggedValue::Font(new_font.clone()), node_id, index)(&()), commit_value(&())],
}
.into()
.on_update(move |_| assign_font_message(node_id, new_font.clone()))
.on_commit(move |_| {
DeferMessage::AfterGraphRun {
messages: vec![assign_font_message(node_id, commit_font.clone()), commit_value(&())],
}
.into()
})
})
.collect::<Vec<_>>(),
])
.selected_index(cached_data.font_catalog.0.iter().position(|family| family.name == font.font_family).map(|i| i as u32))
.selected_index(fonts.font_catalog.iter().position(|family| family.name == font.font_family).map(|i| i as u32))
.virtual_scrolling(true)
.widget_instance(),
]);
@@ -908,32 +891,18 @@ pub fn font_inputs(parameter_widgets_info: ParameterWidgetsInfo) -> (Vec<WidgetI
second_row.extend_from_slice(&[
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
DropdownInput::new({
cached_data
fonts
.font_catalog
.0
.iter()
.find(|family| family.name == font.font_family)
.map(|family| {
let build_entry = |style: &FontCatalogStyle| {
let font_style = style.to_named_style();
let font_family = font.font_family.clone();
let new_font = Font::new(font_family, font_style.clone());
MenuListEntry::new(font_style.clone())
.label(font_style.clone())
.on_update({
let font_family = font.font_family.clone();
let font_style = font_style.clone();
move |_| {
// Keep the existing family
let new_font = Font::new(font_family.clone(), font_style.clone());
Message::Batched {
messages: Box::new([
PortfolioMessage::LoadFontData { font: new_font.clone() }.into(),
update_value(move |_| TaggedValue::Font(new_font.clone()), node_id, index)(&()),
]),
}
}
})
.label(font_style)
.on_update(move |_| assign_font_message(node_id, new_font.clone()))
.on_commit(commit_value)
};
@@ -946,9 +915,8 @@ pub fn font_inputs(parameter_widgets_info: ParameterWidgetsInfo) -> (Vec<WidgetI
.unwrap_or_default()
})
.selected_index(
cached_data
fonts
.font_catalog
.0
.iter()
.find(|family| family.name == font.font_family)
.and_then(|family| {
@@ -2813,8 +2781,8 @@ pub fn math_properties(node_id: NodeId, context: &mut NodePropertiesContext) ->
}
pub struct ParameterWidgetsInfo<'a> {
cached_data: &'a CachedData,
network_interface: &'a NodeNetworkInterface,
resources: &'a ResourceMessageHandler,
selection_network_path: &'a [NodeId],
document_node: Option<&'a DocumentNode>,
node_id: NodeId,
@@ -2824,6 +2792,7 @@ pub struct ParameterWidgetsInfo<'a> {
input_type: FrontendGraphDataType,
blank_assist: bool,
exposable: bool,
fonts: &'a FontsMessageHandler,
}
impl<'a> ParameterWidgetsInfo<'a> {
@@ -2836,9 +2805,10 @@ impl<'a> ParameterWidgetsInfo<'a> {
let document_node = context.network_interface.document_node(&node_id, context.selection_network_path);
ParameterWidgetsInfo {
cached_data: context.cached_data,
network_interface: context.network_interface,
resources: context.resources,
selection_network_path: context.selection_network_path,
fonts: context.fonts,
document_node,
node_id,
index,

View File

@@ -2,11 +2,12 @@ use super::utility_types::{DrawHandles, OverlayContext};
use crate::consts::HIDE_HANDLE_DISTANCE;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::network_interface::NodeNetworkInterface;
use crate::messages::portfolio::fonts::FALLBACK_FONT_RESOURCE;
use crate::messages::tool::common_functionality::shape_editor::{SelectedLayerState, ShapeState};
use crate::messages::tool::tool_messages::tool_prelude::DocumentMessageHandler;
use glam::{DAffine2, DVec2};
use graphene_std::subpath::{Bezier, BezierHandles};
use graphene_std::text::{Font, FontCache, TextAlign, TextContext, TypesettingConfig};
use graphene_std::text::{TextAlign, TextContext, TypesettingConfig};
use graphene_std::vector::misc::ManipulatorPointId;
use graphene_std::vector::{PointId, SegmentId, Vector};
use std::collections::HashMap;
@@ -221,16 +222,6 @@ pub fn path_endpoint_overlays(document: &DocumentMessageHandler, shape_editor: &
}
}
// Global lazy initialized font cache and text context
pub static GLOBAL_FONT_CACHE: LazyLock<FontCache> = LazyLock::new(|| {
let mut font_cache = FontCache::default();
// Initialize with the hardcoded font used by overlay text
const FONT_DATA: &[u8] = include_bytes!("source-sans-pro-regular.ttf");
let font = Font::new("Source Sans Pro".to_string(), "Regular".to_string());
font_cache.insert(font, FONT_DATA.to_vec());
font_cache
});
pub static GLOBAL_TEXT_CONTEXT: LazyLock<Mutex<TextContext>> = LazyLock::new(|| Mutex::new(TextContext::default()));
pub fn text_width(text: &str, font_size: f64) -> f64 {
@@ -244,12 +235,8 @@ pub fn text_width(text: &str, font_size: f64) -> f64 {
align: TextAlign::AlignLeft,
};
// Load Source Sans Pro font data
// TODO: Grab this from the node_modules folder (either with `include_bytes!` or ideally at runtime) instead of checking the font file into the repo.
// TODO: And maybe use the WOFF2 version (if it's supported) for its smaller, compressed file size.
let font = Font::new("Source Sans Pro".to_string(), "Regular".to_string());
let mut text_context = GLOBAL_TEXT_CONTEXT.lock().expect("Failed to lock global text context");
let bounds = text_context.bounding_box(text, &font, &GLOBAL_FONT_CACHE, typesetting, false);
let bounds = text_context.bounding_box(text, &FALLBACK_FONT_RESOURCE, typesetting, false);
bounds.x
}

View File

@@ -3,8 +3,9 @@ use crate::consts::{
COLOR_OVERLAY_YELLOW_DULL, COMPASS_ROSE_ARROW_SIZE, COMPASS_ROSE_HOVER_RING_DIAMETER, COMPASS_ROSE_MAIN_RING_DIAMETER, COMPASS_ROSE_RING_INNER_DIAMETER, DOWEL_PIN_RADIUS,
GRADIENT_MIDPOINT_DIAMOND_RADIUS, MANIPULATOR_GROUP_MARKER_SIZE, PIVOT_CROSSHAIR_LENGTH, PIVOT_CROSSHAIR_THICKNESS, PIVOT_DIAMETER, RESIZE_HANDLE_SIZE, SKEW_TRIANGLE_OFFSET, SKEW_TRIANGLE_SIZE,
};
use crate::messages::portfolio::document::overlays::utility_functions::{GLOBAL_FONT_CACHE, GLOBAL_TEXT_CONTEXT, hex_to_rgba_u8};
use crate::messages::portfolio::document::overlays::utility_functions::{GLOBAL_TEXT_CONTEXT, hex_to_rgba_u8};
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::fonts::FALLBACK_FONT_RESOURCE;
use crate::messages::prelude::Message;
use crate::messages::prelude::ViewportMessageHandler;
use core::borrow::Borrow;
@@ -14,7 +15,7 @@ use graphene_std::ATTR_TRANSFORM;
use graphene_std::list::List;
use graphene_std::math::quad::Quad;
use graphene_std::subpath::{self, Subpath};
use graphene_std::text::{Font, TextAlign, TypesettingConfig};
use graphene_std::text::{TextAlign, TypesettingConfig};
use graphene_std::vector::click_target::ClickTargetType;
use graphene_std::vector::misc::point_to_dvec2;
use graphene_std::vector::{PointId, SegmentId, Vector};
@@ -1115,21 +1116,16 @@ impl OverlayContextInternal {
align: TextAlign::AlignLeft,
};
// Load Source Sans Pro font data
// TODO: Grab this from the node_modules folder (either with `include_bytes!` or ideally at runtime) instead of checking the font file into the repo.
// TODO: And maybe use the WOFF2 version (if it's supported) for its smaller, compressed file size.
let font = Font::new("Source Sans Pro".to_string(), "Regular".to_string());
// Get text dimensions directly from layout
let mut text_context = GLOBAL_TEXT_CONTEXT.lock().expect("Failed to lock global text context");
let text_size = text_context.bounding_box(text, &font, &GLOBAL_FONT_CACHE, typesetting, false);
let text_size = text_context.bounding_box(text, &FALLBACK_FONT_RESOURCE, typesetting, false);
let text_width = text_size.x;
let text_height = text_size.y;
// Create a rect from the size (assuming text starts at origin)
let text_bounds = kurbo::Rect::new(0., 0., text_width, text_height);
// Convert text to vector paths for rendering
let text_list = text_context.to_path(text, &font, &GLOBAL_FONT_CACHE, typesetting, false);
let text_list = text_context.to_path(text, &FALLBACK_FONT_RESOURCE, typesetting, false);
// Calculate position based on pivot
let mut position = DVec2::ZERO;

View File

@@ -3,17 +3,17 @@ use graphene_std::uuid::NodeId;
use crate::messages::layout::utility_types::widget_prelude::*;
use crate::messages::portfolio::document::node_graph::document_node_definitions::NodePropertiesContext;
use crate::messages::portfolio::document::utility_types::network_interface::NodeNetworkInterface;
use crate::messages::portfolio::utility_types::CachedData;
use crate::messages::prelude::*;
use crate::node_graph_executor::NodeGraphExecutor;
#[derive(ExtractField)]
pub struct PropertiesPanelMessageContext<'a> {
pub executor: &'a mut NodeGraphExecutor,
pub network_interface: &'a mut NodeNetworkInterface,
pub resources: &'a ResourceMessageHandler,
pub selection_network_path: &'a [NodeId],
pub document_name: &'a str,
pub executor: &'a mut NodeGraphExecutor,
pub cached_data: &'a CachedData,
pub fonts: &'a FontsMessageHandler,
pub properties_panel_open: bool,
}
@@ -24,11 +24,12 @@ pub struct PropertiesPanelMessageHandler {}
impl MessageHandler<PropertiesPanelMessage, PropertiesPanelMessageContext<'_>> for PropertiesPanelMessageHandler {
fn process_message(&mut self, message: PropertiesPanelMessage, responses: &mut VecDeque<Message>, context: PropertiesPanelMessageContext) {
let PropertiesPanelMessageContext {
executor,
network_interface,
resources,
selection_network_path,
document_name,
executor,
cached_data,
fonts,
properties_panel_open,
} = context;
@@ -46,12 +47,13 @@ impl MessageHandler<PropertiesPanelMessage, PropertiesPanelMessageContext<'_>> f
}
let mut node_properties_context = NodePropertiesContext {
cached_data,
responses,
executor,
network_interface,
resources,
selection_network_path,
document_name,
executor,
fonts,
};
let layout = Layout(NodeGraphMessageHandler::collate_properties(&mut node_properties_context));

View File

@@ -1,9 +1,14 @@
use crate::messages::prelude::*;
use graph_craft::application_io::resource::ResourceId;
use graphene_std::text::Font;
use std::sync::Arc;
#[impl_message(Message, DocumentMessage, Resource)]
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
pub enum ResourceMessage {
StoreEmbedded { resource_id: ResourceId, data: Arc<[u8]> },
AddFont { resource_id: ResourceId, font: Font },
Resolve,
ResolveStep { resource_id: ResourceId },
Resolved { resource_id: ResourceId, data: Arc<[u8]> },
}

View File

@@ -3,25 +3,149 @@ use crate::messages::prelude::*;
use base64::Engine;
use base64::engine::general_purpose::STANDARD as BASE64;
use graph_craft::application_io::resource::{DataSource, LoadResource, Resource, ResourceHash, ResourceId, ResourceRegistry};
use graphene_std::text::Font;
#[derive(ExtractField)]
pub struct ResourceMessageContext {}
pub struct ResourceMessageContext<'a> {
pub document_id: DocumentId,
pub fonts: &'a FontsMessageHandler,
}
#[derive(Debug, Clone, PartialEq, Default, serde::Serialize, ExtractField)]
pub struct ResourceMessageHandler {
pub registry: ResourceRegistry,
pub embedded: EmbeddedResources,
#[serde(skip)]
pending_resolves: HashMap<ResourceId, Option<ResolveProgress>>,
}
#[derive(Debug, Clone, PartialEq)]
struct ResolveProgress {
index: usize,
source: DataSource,
}
#[message_handler_data]
impl MessageHandler<ResourceMessage, ResourceMessageContext> for ResourceMessageHandler {
fn process_message(&mut self, message: ResourceMessage, responses: &mut VecDeque<Message>, _context: ResourceMessageContext) {
impl MessageHandler<ResourceMessage, ResourceMessageContext<'_>> for ResourceMessageHandler {
fn process_message(&mut self, message: ResourceMessage, responses: &mut VecDeque<Message>, context: ResourceMessageContext) {
let ResourceMessageContext { document_id, fonts } = context;
match message {
ResourceMessage::StoreEmbedded { resource_id, data } => {
let hash = ResourceHash::from(data.as_ref());
self.registry.push_source_back(&resource_id, DataSource::Embedded);
self.registry.resolve(&resource_id, hash);
responses.add(ResourceStorageMessage::Store { data });
responses.add(ResourceMessage::Resolve);
}
ResourceMessage::AddFont { resource_id, font } => {
let style = fonts.font_catalog.find_font_style_in_catalog(&font);
let style_name = style.map(|style| style.to_named_style()).unwrap_or_else(|| font.font_style.clone());
self.registry.push_source_back(&resource_id, DataSource::Embedded);
self.registry.push_source_back(
&resource_id,
DataSource::Font {
family: font.font_family,
style: Some(style_name),
},
);
responses.add(ResourceMessage::Resolve);
}
ResourceMessage::Resolve => {
let unresolved_ids: Vec<ResourceId> = self.registry.unresolved().map(|info| info.id).collect();
for id in unresolved_ids {
if self.pending_resolves.contains_key(&id) {
continue;
}
self.pending_resolves.insert(id, None);
responses.add(ResourceMessage::ResolveStep { resource_id: id });
}
}
ResourceMessage::ResolveStep { resource_id } => {
let Some(progress) = self.pending_resolves.get_mut(&resource_id) else { return };
let Some(info) = self.registry.info(&resource_id) else {
log::error!("ResolveStep for {resource_id}: no registry entry");
self.pending_resolves.remove(&resource_id);
return;
};
let index = if let Some(progress) = progress { progress.index + 1 } else { 0 };
let Some(source) = info.sources.get(index).cloned() else {
log::error!("ResolveStep for {resource_id}: no more sources to try");
self.pending_resolves.remove(&resource_id);
return;
};
*progress = Some(ResolveProgress { index, source: source.clone() });
match source {
DataSource::Embedded => {
// Embedded resources are loaded on document load.
// If we get to this point, it means the resource was not embedded and we should try the next source.
responses.add(ResourceMessage::ResolveStep { resource_id });
}
DataSource::Url(url) => {
responses.add(FrontendMessage::TriggerResolveResource {
document_id,
resource_id,
url: url.to_string(),
});
}
DataSource::Font { family, style } => {
let font = match style {
Some(style) => Font::new(family, style),
None => Font::new_with_default_style(family),
};
if let Some(hash) = fonts.cached_hash(&font) {
self.registry.resolve(&resource_id, hash);
self.pending_resolves.remove(&resource_id);
responses.add(NodeGraphMessage::RunDocumentGraph);
return;
}
if let Some(url) = fonts.cached_url(&font) {
responses.add(FrontendMessage::TriggerResolveResource { document_id, resource_id, url });
return;
}
responses.add(FrontendMessage::TriggerFontCatalogLoad);
self.pending_resolves.remove(&resource_id);
}
}
}
ResourceMessage::Resolved { resource_id, data } => {
let hash = ResourceHash::from(data.as_ref());
let Some(progress) = self.pending_resolves.remove(&resource_id).and_then(|p| p) else {
log::error!("Resolved message for {resource_id} with no pending resolve");
return;
};
let Some(info) = self.registry.info(&resource_id) else {
// ResourceId was removed from registry after resolve started.
// This can happen if the document was modified while resolves were in-flight.
// Likely safe to ignore for now.
// TODO: Consider adding cleaner cancelation for in-flight resolves.
return;
};
let Some(source) = info.sources.get(progress.index).cloned() else {
log::error!("Resolved message for {resource_id} with no current source");
return;
};
if progress.source != source {
log::error!("Resolved message for {resource_id} with mismatched source");
return;
}
self.registry.resolve(&resource_id, hash);
responses.add(ResourceStorageMessage::Store { data });
if let DataSource::Font { family, style } = source {
let font = match style {
Some(style) => Font::new(family, style),
None => Font::new_with_default_style(family),
};
responses.add(FontsMessage::ResourceResolved { font, hash });
}
responses.add(ResourceMessage::Resolve);
responses.add(NodeGraphMessage::RunDocumentGraph);
}
}
}
@@ -54,7 +178,7 @@ impl ResourceMessageHandler {
self.embedded = EmbeddedResources::from_iter(futures::future::join_all(embedded).await.into_iter().flatten());
}
pub fn garbage_collect(&mut self, used: &[ResourceId]) {
pub fn collect_garbage(&mut self, used: &[ResourceId]) {
let used = HashSet::<ResourceId>::from_iter(used.iter().cloned());
let unused = self.registry.ids().filter(|id| !used.contains(id)).collect::<Vec<_>>();
unused.into_iter().for_each(|id| {

View File

@@ -1685,6 +1685,28 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId],
}
}
// Convert text nodes from the old `editor-api` scope + `Font` input to a single font `Resource` input.
// The chosen typeface is recorded as a `DataSource::Font` in the document's resource registry and loaded on open.
if reference == DefinitionIdentifier::ProtoNode(graphene_std::text::text::IDENTIFIER) && inputs_count == 13 && matches!(node.inputs.first(), Some(NodeInput::Scope(_))) {
document
.network_interface
.set_input(&InputConnector::node(*node_id, 0), NodeInput::value(TaggedValue::None, false), network_path);
if let Some(TaggedValue::Font(font)) = node.inputs.get(2).and_then(|input| input.as_value()) {
let resource_id = ResourceId::new();
document.resources.registry.push_source_back(
&resource_id,
DataSource::Font {
family: font.font_family.clone(),
style: Some(font.font_style.clone()),
},
);
document
.network_interface
.set_input(&InputConnector::node(*node_id, 2), NodeInput::value(TaggedValue::Resource(resource_id), false), network_path);
}
}
if reference == DefinitionIdentifier::ProtoNode(graphene_std::raster_nodes::std_nodes::noise_pattern::IDENTIFIER) && inputs_count == 15 {
let mut node_template = resolve_document_node_type(&reference)?.default_node_template();
document.network_interface.replace_implementation(node_id, network_path, &mut node_template);

View File

@@ -0,0 +1,5 @@
use graph_craft::application_io::resource::Resource;
use std::sync::LazyLock;
const FALLBACK_FONT_BYTES: &[u8] = include_bytes!("source-sans-pro-regular.ttf");
pub static FALLBACK_FONT_RESOURCE: LazyLock<Resource> = LazyLock::new(|| Resource::new(FALLBACK_FONT_BYTES));

View File

@@ -0,0 +1,25 @@
use crate::messages::portfolio::fonts::utility_types::FontCatalog;
use crate::messages::prelude::*;
use graph_craft::application_io::resource::{Resource, ResourceHash};
use graphene_std::text::Font;
#[impl_message(Message, PortfolioMessage, Fonts)]
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
pub enum FontsMessage {
CatalogLoaded {
catalog: FontCatalog,
},
ResourceResolved {
font: Font,
hash: ResourceHash,
},
Load {
font: Font,
response: Box<Message>,
},
Cached {
hash: ResourceHash,
#[serde(skip, default = "Resource::empty")]
resource: Resource,
},
}

View File

@@ -0,0 +1,112 @@
use crate::messages::portfolio::fonts::FALLBACK_FONT_RESOURCE;
use crate::messages::portfolio::fonts::utility_types::FontCatalog;
use crate::messages::prelude::*;
use graph_craft::application_io::resource::{DataSource, Resource, ResourceHash, ResourceId};
use graphene_std::text::Font;
#[derive(ExtractField)]
pub struct FontsMessageContext<'a> {
pub resource_storage: &'a ResourceStorageMessageHandler,
}
#[derive(Debug, Default, ExtractField)]
pub struct FontsMessageHandler {
pub font_catalog: FontCatalog,
font_hashes: HashMap<Font, ResourceHash>,
font_data: HashMap<ResourceHash, Resource>,
}
#[message_handler_data]
impl MessageHandler<FontsMessage, FontsMessageContext<'_>> for FontsMessageHandler {
fn process_message(&mut self, message: FontsMessage, responses: &mut VecDeque<Message>, context: FontsMessageContext) {
let FontsMessageContext { resource_storage } = context;
match message {
FontsMessage::CatalogLoaded { catalog } => {
self.font_catalog = catalog;
responses.add(PortfolioMessage::ResolveResources);
}
FontsMessage::ResourceResolved { font, hash } => {
let font = self.normalize(font);
self.font_hashes.insert(font, hash);
}
FontsMessage::Load { font, response } => {
let font = self.normalize(font);
let Some(hash) = self.font_hashes.get(&font).copied() else {
log::warn!("FontsMessage::Load for {font:?} with no known hash; ignoring");
return;
};
if self.font_data.contains_key(&hash) {
responses.add(*response);
return;
}
let loader = resource_storage.resources();
responses.add(async move {
let resource = loader.load(hash).await;
match resource {
Some(resource) => Message::Batched {
messages: Box::new([FontsMessage::Cached { hash, resource }.into(), *response]),
},
None => {
log::warn!("Storage missing data for font hash {hash}");
*response
}
}
});
}
FontsMessage::Cached { hash, resource } => {
self.font_data.insert(hash, resource);
}
}
}
advertise_actions!(FontsMessageDiscriminant;);
}
impl FontsMessageHandler {
pub fn cached_hash(&self, font: &Font) -> Option<ResourceHash> {
let font = self.normalize(font.clone());
self.font_hashes.get(&font).copied()
}
pub fn cached_url(&self, font: &Font) -> Option<String> {
let font = self.normalize(font.clone());
self.font_catalog.download_url(&font)
}
pub fn get_resource_or_queue_load(&self, font: &Font, responses: &mut VecDeque<Message>) -> Resource {
let font = self.normalize(font.clone());
if let Some(hash) = self.font_hashes.get(&font) {
if let Some(resource) = self.font_data.get(hash) {
return resource.clone();
}
responses.add(FontsMessage::Load {
font: font.clone(),
response: Message::NoOp.into(),
});
}
FALLBACK_FONT_RESOURCE.clone()
}
pub fn id_font(&self, resources: &ResourceMessageHandler, resource_id: ResourceId) -> Option<Font> {
let info = resources.registry.info(&resource_id)?;
info.sources.iter().find_map(|source| match source {
DataSource::Font { family, style } => Some(match style {
Some(style) => Font::new(family.clone(), style.clone()),
None => Font::new_with_default_style(family.clone()),
}),
_ => None,
})
}
pub fn used_resources(&self) -> impl Iterator<Item = ResourceHash> + '_ {
self.font_hashes.values().copied().chain(self.font_data.keys().copied())
}
fn normalize(&self, font: Font) -> Font {
match self.font_catalog.find_font_style_in_catalog(&font) {
Some(style) => Font::new(font.font_family, style.to_named_style()),
None => font,
}
}
}

View File

@@ -0,0 +1,12 @@
mod fallback;
mod fonts_message;
mod fonts_message_handler;
pub mod utility_types;
#[doc(inline)]
pub use fallback::FALLBACK_FONT_RESOURCE;
#[doc(inline)]
pub use fonts_message::{FontsMessage, FontsMessageDiscriminant};
#[doc(inline)]
pub use fonts_message_handler::{FontsMessageContext, FontsMessageHandler};

View File

@@ -0,0 +1,104 @@
use graphene_std::text::Font;
// TODO: Should this be a BTreeMap instead?
#[derive(Clone, Debug, Default, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct FontCatalog(Vec<FontCatalogFamily>);
impl FontCatalog {
pub fn find_font_style_in_catalog(&self, font: &Font) -> Option<FontCatalogStyle> {
let family = self.0.iter().find(|family| family.name == font.font_family);
let found_style = family.map(|family| {
let FontCatalogStyle { weight, italic, .. } = FontCatalogStyle::from_named_style(&font.font_style, "");
family.closest_style(weight, italic).clone()
});
if found_style.is_none() {
log::warn!("Font not found in catalog: {:?}", font);
}
found_style
}
pub fn download_url(&self, font: &Font) -> Option<String> {
let catalog_family = self.0.iter().find(|catalog_family| catalog_family.name == font.font_family)?;
let FontCatalogStyle { weight, italic, .. } = FontCatalogStyle::from_named_style(&font.font_style, "");
Some(catalog_family.closest_style(weight, italic).url.clone())
}
pub fn iter(&self) -> impl Iterator<Item = &FontCatalogFamily> {
self.0.iter()
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
}
impl From<Vec<FontCatalogFamily>> for FontCatalog {
fn from(value: Vec<FontCatalogFamily>) -> Self {
Self(value)
}
}
#[cfg_attr(feature = "wasm", derive(tsify::Tsify), tsify(from_wasm_abi))]
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct FontCatalogFamily {
/// The font family name.
pub name: String,
/// The font styles (variants) available for the font family.
pub styles: Vec<FontCatalogStyle>,
}
impl FontCatalogFamily {
/// Finds the closest style to the given weight and italic setting.
/// Aims to find the nearest weight while maintaining the italic setting if possible, but italic may change if no other option is available.
pub fn closest_style(&self, weight: u32, italic: bool) -> &FontCatalogStyle {
static FALLBACK_STYLE: FontCatalogStyle = FontCatalogStyle {
weight: 400,
italic: false,
url: String::new(),
};
self.styles
.iter()
.map(|style| ((style.weight as i32 - weight as i32).unsigned_abs() + 10000 * (style.italic != italic) as u32, style))
.min_by_key(|(distance, _)| *distance)
.map(|(_, style)| style)
.unwrap_or(self.styles.first().unwrap_or(&FALLBACK_STYLE))
}
}
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct FontCatalogStyle {
pub weight: u32,
pub italic: bool,
pub url: String,
}
impl FontCatalogStyle {
pub fn to_named_style(&self) -> String {
let weight = self.weight;
let italic = self.italic;
let named_weight = Font::named_weight(weight);
let maybe_italic = if italic { " Italic" } else { "" };
format!("{named_weight}{maybe_italic} ({weight})")
}
pub fn from_named_style(named_style: &str, url: impl Into<String>) -> FontCatalogStyle {
let weight = named_style.split_terminator(['(', ')']).next_back().and_then(|x| x.parse::<u32>().ok()).unwrap_or(400);
let italic = named_style.contains("Italic (");
FontCatalogStyle { weight, italic, url: url.into() }
}
/// Get the URL for the stylesheet for loading a font preview for this style of the given family name, subsetted to only the letters in the family name.
pub fn preview_url(&self, family: impl Into<String>) -> String {
let name = family.into().replace(' ', "+");
let italic = if self.italic { "ital," } else { "" };
let weight = self.weight;
format!("https://fonts.googleapis.com/css2?display=swap&family={name}:{italic}wght@{weight}&text={name}")
}
}

View File

@@ -3,9 +3,12 @@ mod portfolio_message_handler;
pub mod document;
pub mod document_migration;
pub mod fonts;
pub mod persistent_state;
pub mod utility_types;
#[doc(inline)]
pub use fonts::{FontsMessage, FontsMessageContext, FontsMessageHandler};
#[doc(inline)]
pub use persistent_state::{PersistentStateMessage, PersistentStateMessageContext, PersistentStateMessageHandler};
#[doc(inline)]

View File

@@ -52,3 +52,9 @@ impl MessageHandler<PersistentStateMessage, PersistentStateMessageContext> for P
advertise_actions!(PersistentStateMessageDiscriminant;);
}
impl PersistentStateMessageHandler {
pub fn loaded(&self) -> bool {
self.loaded
}
}

View File

@@ -3,11 +3,9 @@ use super::persistent_state::PersistentStateMessage;
use super::utility_types::{DockingSplitDirection, PanelGroupId, PanelType};
use crate::messages::frontend::utility_types::{ExportBounds, FileType, PersistedState};
use crate::messages::portfolio::document::utility_types::clipboards::Clipboard;
use crate::messages::portfolio::utility_types::FontCatalog;
use crate::messages::prelude::*;
use graphene_std::Color;
use graphene_std::raster::Image;
use graphene_std::text::Font;
use std::path::PathBuf;
#[impl_message(Message, Portfolio)]
@@ -17,6 +15,8 @@ pub enum PortfolioMessage {
#[child]
Document(DocumentMessage),
#[child]
Fonts(FontsMessage),
#[child]
PersistentState(PersistentStateMessage),
// Messages
@@ -51,20 +51,10 @@ pub enum PortfolioMessage {
DestroyAllDocuments,
EditorPreferences,
GarbageCollectResources,
FontCatalogLoaded {
catalog: FontCatalog,
},
LoadFontData {
font: Font,
},
FontLoaded {
font_family: String,
font_style: String,
data: Vec<u8>,
},
LoadDocumentResources {
ResolveDocumentResources {
document_id: DocumentId,
},
ResolveResources,
LoadPersistedState {
state: PersistedState,
},

View File

@@ -1,7 +1,7 @@
use super::document::utility_types::document_metadata::LayerNodeIdentifier;
use super::document::utility_types::network_interface;
use super::persistent_state::{PersistentStateMessage, PersistentStateMessageContext, PersistentStateMessageHandler};
use super::utility_types::{CachedData, PanelLayoutSubdivision, PanelType, WorkspacePanelLayout};
use super::utility_types::{PanelLayoutSubdivision, PanelType, WorkspacePanelLayout};
use crate::application::{Editor, generate_uuid};
use crate::consts::{DEFAULT_DOCUMENT_NAME, DEFAULT_STROKE_WIDTH, FILE_EXTENSION};
use crate::messages::animation::TimingInformation;
@@ -32,7 +32,6 @@ use graphene_std::Color;
use graphene_std::raster_types::Image;
use graphene_std::renderer::Quad;
use graphene_std::subpath::BezierHandles;
use graphene_std::text::Font;
use graphene_std::vector::misc::HandleId;
use graphene_std::vector::{PointId, SegmentId, Vector, VectorModificationType};
use std::path::PathBuf;
@@ -68,7 +67,7 @@ pub struct PortfolioMessageHandler {
document_ids: VecDeque<DocumentId>,
pub(crate) active_document_id: Option<DocumentId>,
persistent_state: PersistentStateMessageHandler,
pub cached_data: CachedData,
pub fonts: FontsMessageHandler,
copy_buffer: [Vec<CopyBufferEntry>; INTERNAL_CLIPBOARD_COUNT as usize],
pub executor: NodeGraphExecutor,
pub selection_mode: SelectionMode,
@@ -99,7 +98,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
let document_inputs = DocumentMessageContext {
document_id,
ipp,
cached_data: &self.cached_data,
fonts: &self.fonts,
executor: &mut self.executor,
current_tool,
preferences,
@@ -118,6 +117,10 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
};
self.persistent_state.process_message(message, responses, context);
}
PortfolioMessage::Fonts(message) => {
let context = FontsMessageContext { resource_storage };
self.fonts.process_message(message, responses, context);
}
// Messages
PortfolioMessage::Init => {
@@ -169,7 +172,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
let document_inputs = DocumentMessageContext {
document_id,
ipp,
cached_data: &self.cached_data,
fonts: &self.fonts,
executor: &mut self.executor,
current_tool,
preferences,
@@ -208,6 +211,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
document_id,
document: document.serialize_document(),
});
responses.add(PersistentStateMessage::WriteState);
}
PortfolioMessage::CloseActiveDocumentWithConfirmation => {
if let Some(document_id) = self.active_document_id {
@@ -386,78 +390,16 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
responses.add(MenuBarMessage::SendLayout);
responses.add(PersistentStateMessage::WriteState);
}
PortfolioMessage::FontCatalogLoaded { catalog } => {
self.cached_data.font_catalog = catalog;
if let Some(document_id) = self.active_document_id {
responses.add(PortfolioMessage::LoadDocumentResources { document_id });
}
// Load the default font
let font = Font::new(graphene_std::consts::DEFAULT_FONT_FAMILY.into(), graphene_std::consts::DEFAULT_FONT_STYLE.into());
responses.add(PortfolioMessage::LoadFontData { font });
}
PortfolioMessage::LoadFontData { font } => {
if let Some(style) = self.cached_data.font_catalog.find_font_style_in_catalog(&font) {
let font = Font::new(font.font_family, style.to_named_style());
if !self.cached_data.font_cache.loaded_font(&font) {
responses.add(FrontendMessage::TriggerFontDataLoad { font, url: style.url });
}
}
}
PortfolioMessage::FontLoaded { font_family, font_style, data } => {
let font = Font::new(font_family, font_style);
self.cached_data.font_cache.insert(font, data);
self.executor.update_font_cache(self.cached_data.font_cache.clone());
for document_id in self.document_ids.iter() {
let node_to_inspect = self.node_to_inspect();
let Some(document) = self.documents.get_mut(document_id) else {
if self.unloaded_documents.contains_key(document_id) {
continue;
}
log::error!("Tried to render non-existent document");
continue;
};
let document_to_viewport = document
.navigation_handler
.calculate_offset_transform(viewport.center_in_viewport_space().into(), &document.document_ptz);
let pointer_position = document_to_viewport.inverse().transform_point2(ipp.mouse.position);
let scale = viewport.scale();
// Use exact physical dimensions from browser (via ResizeObserver's devicePixelContentBoxSize)
let physical_resolution = viewport.size().to_physical().into_dvec2().round().as_uvec2();
// TODO: Remove this when we do the SVG rendering with a separate library on desktop, thus avoiding a need for the hole punch.
// TODO: See #3796. There is a second instance of this todo comment and code block (be sure to remove both).
#[cfg(not(target_family = "wasm"))]
responses.add_front(FrontendMessage::UpdateViewportHolePunch {
active: document.render_mode != graphene_std::vector::style::RenderMode::SvgPreview,
});
if let Ok(message) = self
.executor
.submit_node_graph_evaluation(document, *document_id, physical_resolution, scale, timing_information, node_to_inspect, true, pointer_position)
{
responses.add_front(message);
}
}
if self.active_document_mut().is_some() {
responses.add(NodeGraphMessage::RunDocumentGraph);
}
if current_tool == &ToolType::Text {
responses.add(TextToolMessage::RefreshEditingFontData);
}
}
PortfolioMessage::EditorPreferences => self.executor.update_editor_preferences(preferences.editor_preferences()),
PortfolioMessage::GarbageCollectResources => {
if !self.persistent_state.loaded() {
// We don't know what can be safely garbage collected
return;
}
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 {
@@ -469,21 +411,26 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
document.garbage_collect_resources();
used_resources.extend(document.resources.registry.resolved().filter_map(|info| info.hash.cloned()));
}
used_resources.extend(self.fonts.used_resources());
responses.add(ResourceStorageMessage::GarbageCollect {
used: Vec::from_iter(used_resources).into_boxed_slice(),
});
}
PortfolioMessage::LoadDocumentResources { document_id } => {
let catalog = &self.cached_data.font_catalog;
if catalog.0.is_empty() {
PortfolioMessage::ResolveResources => {
for document_id in self.document_ids.iter().copied().collect::<Vec<_>>() {
responses.add(PortfolioMessage::ResolveDocumentResources { document_id });
}
}
PortfolioMessage::ResolveDocumentResources { document_id } => {
if self.fonts.font_catalog.is_empty() {
responses.add_front(FrontendMessage::TriggerFontCatalogLoad);
return;
}
if let Some(document) = self.documents.get_mut(&document_id) {
document.load_layer_resources(responses);
}
responses.add(PortfolioMessage::DocumentPassMessage {
document_id,
message: DocumentMessage::Resource(ResourceMessage::Resolve),
});
}
PortfolioMessage::LoadPersistedState { state } => {
if let Some(layout) = state.workspace_layout {
@@ -884,6 +831,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
let mut document = match document {
Ok(document) => document,
Err(e) => {
log::error!("{e}");
// TODO: Eventually remove this document upgrade code
// TODO: (Only the `if` branch, the `else` branch's manual-open dialog stays)
if document_is_auto_saved {
@@ -908,7 +856,6 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
responses.add(PortfolioMessage::UpdateOpenDocumentsList);
self.tick_autosave_load_progress(responses, true);
} else {
log::error!("{e}");
let name = document_name
.filter(|n| !n.trim().is_empty())
.or_else(|| document_path.as_ref().and_then(|p| p.file_stem()).map(|s| s.to_string_lossy().into_owned()))
@@ -1134,7 +1081,6 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
added_nodes = true;
}
document.load_layer_resources(responses);
let new_ids: HashMap<_, _> = entry.nodes.iter().map(|(id, _)| (*id, NodeId::new())).collect();
let layer = LayerNodeIdentifier::new_unchecked(new_ids[&NodeId(0)]);
all_new_ids.extend(new_ids.values().cloned());
@@ -1619,7 +1565,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
};
if !document.is_loaded {
document.is_loaded = true;
responses.add(PortfolioMessage::LoadDocumentResources { document_id });
responses.add(PortfolioMessage::ResolveDocumentResources { document_id });
responses.add(PortfolioMessage::UpdateDocumentWidgets);
responses.add(PropertiesPanelMessage::Clear);
}
@@ -1665,6 +1611,13 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
return;
};
// Skip rendering while any resource is still unresolved — the preprocessor would otherwise fail with
// `ResourceNotFound`. `ResourceMessage::Resolved` queues `RunDocumentGraph` once each id resolves,
// so the render fires automatically once the registry is complete.
if document.resources.registry.unresolved().next().is_some() {
return;
}
let document_to_viewport = document
.navigation_handler
.calculate_offset_transform(viewport.center_in_viewport_space().into(), &document.document_ptz);

View File

@@ -1,95 +1,11 @@
use graphene_std::Color;
use graphene_std::raster::Image;
use graphene_std::text::{Font, FontCache};
/// Proportional share (0-1) for the document panel's side when splitting adjacent to non-document panels.
const DOCUMENT_PANEL_SHARE: f64 = 0.8;
/// Proportional share for each side when neither (or both) contain the document panel.
const EQUAL_PANEL_SHARE: f64 = 0.5;
#[derive(Debug, Default)]
pub struct CachedData {
pub font_cache: FontCache,
pub font_catalog: FontCatalog,
}
// TODO: Should this be a BTreeMap instead?
#[derive(Clone, Debug, Default, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct FontCatalog(pub Vec<FontCatalogFamily>);
impl FontCatalog {
pub fn find_font_style_in_catalog(&self, font: &Font) -> Option<FontCatalogStyle> {
let family = self.0.iter().find(|family| family.name == font.font_family);
let found_style = family.map(|family| {
let FontCatalogStyle { weight, italic, .. } = FontCatalogStyle::from_named_style(&font.font_style, "");
family.closest_style(weight, italic).clone()
});
if found_style.is_none() {
log::warn!("Font not found in catalog: {:?}", font);
}
found_style
}
}
#[cfg_attr(feature = "wasm", derive(tsify::Tsify), tsify(from_wasm_abi))]
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct FontCatalogFamily {
/// The font family name.
pub name: String,
/// The font styles (variants) available for the font family.
pub styles: Vec<FontCatalogStyle>,
}
impl FontCatalogFamily {
/// Finds the closest style to the given weight and italic setting.
/// Aims to find the nearest weight while maintaining the italic setting if possible, but italic may change if no other option is available.
pub fn closest_style(&self, weight: u32, italic: bool) -> &FontCatalogStyle {
self.styles
.iter()
.map(|style| ((style.weight as i32 - weight as i32).unsigned_abs() + 10000 * (style.italic != italic) as u32, style))
.min_by_key(|(distance, _)| *distance)
.map(|(_, style)| style)
.unwrap_or(&self.styles[0])
}
}
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct FontCatalogStyle {
pub weight: u32,
pub italic: bool,
pub url: String,
}
impl FontCatalogStyle {
pub fn to_named_style(&self) -> String {
let weight = self.weight;
let italic = self.italic;
let named_weight = Font::named_weight(weight);
let maybe_italic = if italic { " Italic" } else { "" };
format!("{named_weight}{maybe_italic} ({weight})")
}
pub fn from_named_style(named_style: &str, url: impl Into<String>) -> FontCatalogStyle {
let weight = named_style.split_terminator(['(', ')']).next_back().and_then(|x| x.parse::<u32>().ok()).unwrap_or(400);
let italic = named_style.contains("Italic (");
FontCatalogStyle { weight, italic, url: url.into() }
}
/// Get the URL for the stylesheet for loading a font preview for this style of the given family name, subsetted to only the letters in the family name.
pub fn preview_url(&self, family: impl Into<String>) -> String {
let name = family.into().replace(' ', "+");
let italic = if self.italic { "ital," } else { "" };
let weight = self.weight;
format!("https://fonts.googleapis.com/css2?display=swap&family={name}:{italic}wght@{weight}&text={name}")
}
}
#[cfg_attr(feature = "wasm", derive(tsify::Tsify), tsify(from_wasm_abi))]
#[derive(PartialEq, Eq, Hash, Clone, Copy, Debug, serde::Serialize, serde::Deserialize)]
pub enum PanelType {

View File

@@ -30,6 +30,7 @@ pub use crate::messages::portfolio::document::overlays::{OverlaysMessage, Overla
pub use crate::messages::portfolio::document::properties_panel::{PropertiesPanelMessage, PropertiesPanelMessageDiscriminant, PropertiesPanelMessageHandler};
pub use crate::messages::portfolio::document::resource::{ResourceMessage, ResourceMessageContext, ResourceMessageDiscriminant, ResourceMessageHandler};
pub use crate::messages::portfolio::document::{DocumentMessage, DocumentMessageContext, DocumentMessageDiscriminant, DocumentMessageHandler};
pub use crate::messages::portfolio::fonts::{FontsMessage, FontsMessageContext, FontsMessageDiscriminant, FontsMessageHandler};
pub use crate::messages::portfolio::persistent_state::{PersistentStateMessage, PersistentStateMessageContext, PersistentStateMessageDiscriminant, PersistentStateMessageHandler};
pub use crate::messages::portfolio::{PortfolioMessage, PortfolioMessageContext, PortfolioMessageDiscriminant, PortfolioMessageHandler};
pub use crate::messages::preferences::{PreferencesMessage, PreferencesMessageDiscriminant, PreferencesMessageHandler};

View File

@@ -431,15 +431,21 @@ pub fn get_grid_id(layer: LayerNodeIdentifier, network_interface: &NodeNetworkIn
NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name(&DefinitionIdentifier::ProtoNode(graphene_std::vector::generator_nodes::grid::IDENTIFIER))
}
/// Gets properties from the Text node
pub fn get_text(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<(&String, &Font, TypesettingConfig, bool)> {
/// Gets properties from the Text node. Resolves the font selection by reading the resource id and lookup via the fonts message handler.
pub fn get_text<'a>(
layer: LayerNodeIdentifier,
network_interface: &'a NodeNetworkInterface,
fonts: &FontsMessageHandler,
resources: &ResourceMessageHandler,
) -> Option<(&'a String, Font, TypesettingConfig, bool)> {
let inputs = NodeGraphLayer::new(layer, network_interface).find_node_inputs(&DefinitionIdentifier::ProtoNode(graphene_std::text::text::IDENTIFIER))?;
let Some(TaggedValue::String(text)) = &inputs[graphene_std::text::text::TextInput::INDEX].as_value() else {
return None;
};
let Some(TaggedValue::Font(font)) = &inputs[graphene_std::text::text::FontInput::INDEX].as_value() else {
return None;
let font = match &inputs[graphene_std::text::text::FontInput::INDEX].as_value() {
Some(TaggedValue::Resource(resource_id)) => fonts.id_font(resources, *resource_id).unwrap_or_default(),
_ => Font::default(),
};
let Some(&TaggedValue::F64(font_size)) = inputs[graphene_std::text::text::SizeInput::INDEX].as_value() else {
return None;

View File

@@ -16,7 +16,6 @@ use graph_craft::document::value::TaggedValue;
use graphene_std::list::List;
use graphene_std::renderer::Quad;
use graphene_std::subpath::{Bezier, BezierHandles};
use graphene_std::text::FontCache;
use graphene_std::vector::algorithms::bezpath_algorithms::pathseg_compute_lookup_table;
use graphene_std::vector::misc::{HandleId, ManipulatorPointId, dvec2_to_point};
use graphene_std::vector::{HandleExt, PointId, SegmentId, Vector, VectorModification, VectorModificationType};
@@ -63,17 +62,18 @@ where
}
/// Calculates the bounding box of the layer's text, based on the settings for max width and height specified in the typesetting config.
pub fn text_bounding_box(layer: LayerNodeIdentifier, document: &DocumentMessageHandler, font_cache: &FontCache) -> Quad {
pub fn text_bounding_box(layer: LayerNodeIdentifier, document: &DocumentMessageHandler, fonts: &FontsMessageHandler, responses: &mut VecDeque<Message>) -> Quad {
// Use the `editor:text_frame` attribute if available (handles multi-item glyphs and the 'Index Elements' node)
if let Some(&frame) = document.metadata().text_frames.get(&layer) {
return frame * Quad::from_box([DVec2::ZERO, DVec2::ONE]);
}
// Fallback: recompute from text content (e.g. layer hasn't rendered yet)
let Some((text, font, typesetting, _)) = get_text(layer, &document.network_interface) else {
let Some((text, font, typesetting, _)) = get_text(layer, &document.network_interface, fonts, &document.resources) else {
return Quad::from_box([DVec2::ZERO, DVec2::ZERO]);
};
let far = graphene_std::text::bounding_box(text, font, font_cache, typesetting, false);
let font = fonts.get_resource_or_queue_load(&font, responses);
let far = graphene_std::text::bounding_box(text, &font, typesetting, false);
Quad::from_box([DVec2::ZERO, far])
}

View File

@@ -4,7 +4,6 @@ use super::utility_types::{ToolActionMessageContext, ToolFsmState, tool_message_
use crate::application::generate_uuid;
use crate::messages::layout::utility_types::widget_prelude::*;
use crate::messages::portfolio::document::overlays::utility_types::OverlayProvider;
use crate::messages::portfolio::utility_types::CachedData;
use crate::messages::prelude::*;
use crate::messages::tool::transform_layer::transform_layer_message_handler::TransformLayerMessageContext;
use crate::messages::tool::utility_types::{HintData, ToolType};
@@ -19,7 +18,7 @@ pub struct ToolMessageContext<'a> {
pub document_id: DocumentId,
pub document: &'a mut DocumentMessageHandler,
pub input: &'a InputPreprocessorMessageHandler,
pub cached_data: &'a CachedData,
pub fonts: &'a FontsMessageHandler,
pub node_graph: &'a NodeGraphExecutor,
pub preferences: &'a PreferencesMessageHandler,
pub viewport: &'a ViewportMessageHandler,
@@ -40,7 +39,7 @@ impl MessageHandler<ToolMessage, ToolMessageContext<'_>> for ToolMessageHandler
document_id,
document,
input,
cached_data,
fonts,
node_graph,
preferences,
viewport,
@@ -127,7 +126,7 @@ impl MessageHandler<ToolMessage, ToolMessageContext<'_>> for ToolMessageHandler
node_graph,
preferences,
viewport,
cached_data,
fonts,
};
if let Some(tool_abort_message) = tool.event_to_message_map().tool_abort {
@@ -218,7 +217,7 @@ impl MessageHandler<ToolMessage, ToolMessageContext<'_>> for ToolMessageHandler
tool_data.tools.get(active_tool).unwrap().activate(responses);
// Register initial properties
tool_data.tools.get(active_tool).unwrap().refresh_options(responses, cached_data);
tool_data.tools.get(active_tool).unwrap().refresh_options(responses);
// Notify the frontend about the initial active tool
tool_data.send_layout(responses, LayoutTarget::ToolShelf, preferences.brush_tool);
@@ -235,7 +234,7 @@ impl MessageHandler<ToolMessage, ToolMessageContext<'_>> for ToolMessageHandler
node_graph,
preferences,
viewport,
cached_data,
fonts,
};
// Set initial hints and cursor
@@ -259,7 +258,7 @@ impl MessageHandler<ToolMessage, ToolMessageContext<'_>> for ToolMessageHandler
ToolMessage::RefreshToolOptions => {
let tool_data = &mut self.tool_state.tool_data;
tool_data.tools.get(&tool_data.active_tool_type).unwrap().refresh_options(responses, cached_data);
tool_data.tools.get(&tool_data.active_tool_type).unwrap().refresh_options(responses);
}
ToolMessage::RefreshToolShelf => {
let tool_data = &mut self.tool_state.tool_data;
@@ -347,7 +346,7 @@ impl MessageHandler<ToolMessage, ToolMessageContext<'_>> for ToolMessageHandler
node_graph,
preferences,
viewport,
cached_data,
fonts,
};
if matches!(tool_message, ToolMessage::UpdateHints) {
if graph_view_overlay_open {

View File

@@ -720,13 +720,7 @@ impl Fsm for SelectToolFsmState {
type ToolOptions = ();
fn transition(self, event: ToolMessage, tool_data: &mut Self::ToolData, tool_action_data: &mut ToolActionMessageContext, _tool_options: &(), responses: &mut VecDeque<Message>) -> Self {
let ToolActionMessageContext {
document,
input,
viewport,
cached_data,
..
} = tool_action_data;
let ToolActionMessageContext { document, input, viewport, fonts, .. } = tool_action_data;
let ToolMessage::Select(event) = event else { return self };
match (self, event) {
@@ -751,7 +745,7 @@ impl Fsm for SelectToolFsmState {
if document.metadata().is_text_layer(layer) {
let layer_to_viewport = document.metadata().transform_to_viewport(layer);
let transformed_quad = layer_to_viewport * text_bounding_box(layer, document, &cached_data.font_cache);
let transformed_quad = layer_to_viewport * text_bounding_box(layer, document, fonts, responses);
overlay_context.dashed_quad(transformed_quad, None, None, Some(7.), Some(5.), None);
}
}

View File

@@ -6,7 +6,7 @@ use crate::messages::portfolio::document::graph_operation::utility_types::Transf
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::network_interface::InputConnector;
use crate::messages::portfolio::utility_types::{CachedData, FontCatalog, FontCatalogStyle};
use crate::messages::portfolio::fonts::utility_types::{FontCatalog, FontCatalogStyle};
use crate::messages::tool::common_functionality::auto_panning::AutoPanning;
use crate::messages::tool::common_functionality::color_selector::{
ToolColorOptions, apply_fill_only_color_pick, apply_fill_only_enabled, refresh_slot_working_color, selection_changed_since_last_sync, solid, sync_fill_only,
@@ -17,12 +17,13 @@ use crate::messages::tool::common_functionality::snapping::{self, SnapCandidateP
use crate::messages::tool::common_functionality::transformation_cage::*;
use crate::messages::tool::common_functionality::utility_functions::text_bounding_box;
use crate::messages::tool::utility_types::ToolRefreshOptions;
use graph_craft::application_io::resource::ResourceId;
use graph_craft::document::value::TaggedValue;
use graph_craft::document::{NodeId, NodeInput};
use graphene_std::choice_type::ChoiceTypeStatic;
use graphene_std::color::SRGBA8;
use graphene_std::renderer::Quad;
use graphene_std::text::{Font, FontCache, TextAlign, TypesettingConfig, lines_clipping};
use graphene_std::text::{Font, TextAlign, TypesettingConfig, lines_clipping};
use graphene_std::vector::style::{Fill, FillChoice, FillChoiceUI};
use graphene_std::{Color, NodeInputDecleration};
@@ -105,31 +106,30 @@ impl ToolMetadata for TextTool {
}
fn create_text_widgets(tool: &TextTool, font_catalog: &FontCatalog, document: &DocumentMessageHandler) -> Vec<WidgetInstance> {
// If a single text layer is selected, the control bar's font/style menus drive that layer's text node directly, going through the
// same code path as the Properties panel (LoadFontData + SetInputValue, with closest_style and font_style_to_restore bookkeeping).
// Otherwise the menus only update the control bar option for the next created text.
let text_node_id = can_edit_selected(document).and_then(|layer| graph_modification_utils::get_text_id(layer, &document.network_interface));
let font_input_index = graphene_std::text::text::FontInput::INDEX;
let apply_font = move |new_font: Font| -> Message {
let apply_font = move |font: Font| -> Message {
match text_node_id {
Some(node_id) => NodeGraphMessage::SetInputValue {
node_id,
input_index: font_input_index,
value: TaggedValue::Font(new_font),
Some(node_id) => {
let resource_id = ResourceId::new();
Message::Batched {
messages: Box::new([
DocumentMessage::Resource(ResourceMessage::AddFont { resource_id, font }).into(),
NodeGraphMessage::SetInputValue {
node_id,
input_index: graphene_std::text::text::FontInput::INDEX,
value: TaggedValue::Resource(resource_id),
}
.into(),
]),
}
}
.into(),
None => TextToolMessage::UpdateOptions {
options: TextOptionsUpdate::Font { font: new_font },
options: TextOptionsUpdate::Font { font },
}
.into(),
}
};
let preview_font = move |new_font: Font| -> Message {
Message::Batched {
messages: Box::new([PortfolioMessage::LoadFontData { font: new_font.clone() }.into(), apply_font(new_font)]),
}
};
let commit_font = move |new_font: Font| -> Message {
match text_node_id {
Some(_) => DeferMessage::AfterGraphRun {
@@ -142,35 +142,27 @@ fn create_text_widgets(tool: &TextTool, font_catalog: &FontCatalog, document: &D
let font = DropdownInput::new(vec![
font_catalog
.0
.iter()
.map(|family| {
let current_font = &tool.options.font;
let mut new_font = Font::new(family.name.clone(), current_font.font_style_to_restore.clone().unwrap_or_else(|| current_font.font_style.clone()));
new_font.font_style_to_restore = current_font.font_style_to_restore.clone().or_else(|| Some(new_font.font_style.clone()));
let FontCatalogStyle { weight, italic, .. } = FontCatalogStyle::from_named_style(&new_font.font_style, "");
new_font.font_style = family.closest_style(weight, italic).to_named_style();
// Intentionally drop `font_style_to_restore` on commit so the committed style becomes the new basis for
// subsequent family switches. Preserving the original style intent is hover-only behavior (handled by `new_font`).
let FontCatalogStyle { weight, italic, .. } = FontCatalogStyle::from_named_style(&current_font.font_style, "");
let commit_only_font = Font::new(family.name.clone(), family.closest_style(weight, italic).to_named_style());
let new_font = Font::new(family.name.clone(), family.closest_style(weight, italic).to_named_style());
let commit_only_font = new_font.clone();
MenuListEntry::new(family.name.clone())
.label(family.name.clone())
.font(family.closest_style(400, false).preview_url(&family.name))
.on_update(move |_| preview_font(new_font.clone()))
.on_update(move |_| apply_font(new_font.clone()))
.on_commit(move |_| commit_font(commit_only_font.clone()))
})
.collect::<Vec<_>>(),
])
.selected_index(font_catalog.0.iter().position(|family| family.name == tool.options.font.font_family).map(|i| i as u32))
.selected_index(font_catalog.iter().position(|family| family.name == tool.options.font.font_family).map(|i| i as u32))
.virtual_scrolling(true)
.widget_instance();
let style = DropdownInput::new({
font_catalog
.0
.iter()
.find(|family| family.name == tool.options.font.font_family)
.map(|family| {
@@ -182,7 +174,7 @@ fn create_text_widgets(tool: &TextTool, font_catalog: &FontCatalog, document: &D
MenuListEntry::new(font_style.clone())
.label(font_style)
.on_update(move |_| preview_font(new_font.clone()))
.on_update(move |_| apply_font(new_font.clone()))
.on_commit(move |_| commit_font(new_font_for_commit.clone()))
};
@@ -196,7 +188,6 @@ fn create_text_widgets(tool: &TextTool, font_catalog: &FontCatalog, document: &D
})
.selected_index(
font_catalog
.0
.iter()
.find(|family| family.name == tool.options.font.font_family)
.and_then(|family| {
@@ -253,7 +244,7 @@ fn create_text_widgets(tool: &TextTool, font_catalog: &FontCatalog, document: &D
}
impl ToolRefreshOptions for TextTool {
fn refresh_options(&self, responses: &mut VecDeque<Message>, _cached_data: &CachedData) {
fn refresh_options(&self, responses: &mut VecDeque<Message>) {
// Defer to the SelectionChanged handler which has document context, required for the font/style
// dropdowns to bind to the selected text layer's node graph inputs
responses.add(TextToolMessage::SelectionChanged);
@@ -303,7 +294,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Text
ToolMessage::Text(TextToolMessage::UpdateOptions { options }) => options,
ToolMessage::Text(TextToolMessage::SelectionChanged) => {
if let Some(layer) = can_edit_selected(context.document)
&& let Some((_, font, typesetting, _)) = graph_modification_utils::get_text(layer, &context.document.network_interface)
&& let Some((_, font, typesetting, _)) = graph_modification_utils::get_text(layer, &context.document.network_interface, context.fonts, &context.document.resources)
{
self.options.align = typesetting.align;
self.options.font_size = typesetting.font_size;
@@ -311,7 +302,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Text
if let Some(editing_text) = self.tool_data.editing_text.as_mut() {
editing_text.typesetting.align = typesetting.align;
editing_text.typesetting.font_size = typesetting.font_size;
editing_text.font = font.clone();
editing_text.font = font;
}
}
@@ -326,7 +317,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Text
// Text tool has no fill checkbox; keep enabled so new text never starts with `None`
self.options.fill.enabled = Some(true);
self.send_layout(responses, LayoutTarget::ToolOptions, &context.cached_data.font_catalog, context.document);
self.send_layout(responses, LayoutTarget::ToolOptions, &context.fonts.font_catalog, context.document);
return;
}
_ => {
@@ -385,7 +376,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Text
}
}
self.send_layout(responses, LayoutTarget::ToolOptions, &context.cached_data.font_catalog, context.document);
self.send_layout(responses, LayoutTarget::ToolOptions, &context.fonts.font_catalog, context.document);
}
fn actions(&self) -> ActionList {
@@ -476,9 +467,9 @@ struct TextToolData {
}
impl TextToolData {
fn delete_empty_layer(&mut self, font_cache: &FontCache, responses: &mut VecDeque<Message>) -> TextToolFsmState {
fn delete_empty_layer(&mut self, fonts: &FontsMessageHandler, responses: &mut VecDeque<Message>) -> TextToolFsmState {
// Remove the editable textbox UI first
self.set_editing(false, font_cache, responses);
self.set_editing(false, fonts, responses);
// Delete the empty text layer and update the graph
responses.add(NodeGraphMessage::DeleteNodes {
@@ -491,15 +482,16 @@ impl TextToolData {
}
/// Set the editing state of the currently modifying layer
fn set_editing(&self, editable: bool, font_cache: &FontCache, responses: &mut VecDeque<Message>) {
fn set_editing(&self, editable: bool, fonts: &FontsMessageHandler, responses: &mut VecDeque<Message>) {
if let Some(editing_text) = self.editing_text.as_ref().filter(|_| editable) {
let (align, align_last) = editing_text.typesetting.align.css();
let font_data = fonts.get_resource_or_queue_load(&editing_text.font, responses).as_ref().to_vec().into();
responses.add(FrontendMessage::DisplayEditableTextbox {
text: editing_text.text.clone(),
line_height_ratio: editing_text.typesetting.line_height_ratio,
font_size: editing_text.typesetting.font_size,
color: editing_text.color.map_or("#000000".to_string(), |color| SRGBA8::from(color).to_css_hex()),
font_data: font_cache.get(&editing_text.font).map(|(data, _)| data.clone()).unwrap_or_default().into(),
font_data,
transform: editing_text.transform.to_cols_array(),
max_width: editing_text.typesetting.max_width,
max_height: editing_text.typesetting.max_height,
@@ -517,13 +509,13 @@ impl TextToolData {
}
}
fn load_layer_text_node(&mut self, document: &DocumentMessageHandler) -> Option<()> {
fn load_layer_text_node(&mut self, document: &DocumentMessageHandler, fonts: &FontsMessageHandler) -> Option<()> {
let transform = document.metadata().transform_to_viewport(self.layer);
let color = graph_modification_utils::get_fill_color(self.layer, &document.network_interface).unwrap_or(Color::BLACK);
let (text, font, typesetting, _) = graph_modification_utils::get_text(self.layer, &document.network_interface)?;
let (text, font, typesetting, _) = graph_modification_utils::get_text(self.layer, &document.network_interface, fonts, &document.resources)?;
self.editing_text = Some(EditingText {
text: text.clone(),
font: font.clone(),
font,
typesetting,
color: Some(color),
transform,
@@ -532,20 +524,20 @@ impl TextToolData {
Some(())
}
fn start_editing_layer(&mut self, layer: LayerNodeIdentifier, tool_state: TextToolFsmState, document: &DocumentMessageHandler, font_cache: &FontCache, responses: &mut VecDeque<Message>) {
fn start_editing_layer(&mut self, layer: LayerNodeIdentifier, tool_state: TextToolFsmState, document: &DocumentMessageHandler, fonts: &FontsMessageHandler, responses: &mut VecDeque<Message>) {
if layer == LayerNodeIdentifier::ROOT_PARENT {
log::error!("Cannot edit ROOT_PARENT in TextTooLData")
}
if tool_state == TextToolFsmState::Editing {
self.set_editing(false, font_cache, responses);
self.set_editing(false, fonts, responses);
}
self.layer = layer;
if self.load_layer_text_node(document).is_some() {
if self.load_layer_text_node(document, fonts).is_some() {
responses.add(DocumentMessage::AddTransaction);
self.set_editing(true, font_cache, responses);
self.set_editing(true, fonts, responses);
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![self.layer.to_node()] });
// Make the rendered text invisible while editing
@@ -557,14 +549,16 @@ impl TextToolData {
};
}
fn new_text(&mut self, document: &DocumentMessageHandler, editing_text: EditingText, font_cache: &FontCache, responses: &mut VecDeque<Message>) {
// Create new text
fn new_text(&mut self, document: &DocumentMessageHandler, editing_text: EditingText, fonts: &FontsMessageHandler, responses: &mut VecDeque<Message>) {
self.new_text = String::new();
responses.add(DocumentMessage::AddTransaction);
self.layer = LayerNodeIdentifier::new_unchecked(NodeId::new());
responses.add(PortfolioMessage::LoadFontData { font: editing_text.font.clone() });
responses.add(FontsMessage::Load {
font: editing_text.font.clone(),
response: Box::new(NodeGraphMessage::RunDocumentGraph.into()),
});
responses.add(GraphOperationMessage::NewTextLayer {
id: self.layer.to_node(),
text: String::new(),
@@ -580,7 +574,7 @@ impl TextToolData {
let transform = editing_text.transform;
self.editing_text = Some(editing_text);
self.set_editing(true, font_cache, responses);
self.set_editing(true, fonts, responses);
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![self.layer.to_node()] });
@@ -602,20 +596,23 @@ impl TextToolData {
});
}
fn check_click(document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, font_cache: &FontCache) -> Option<LayerNodeIdentifier> {
document.metadata().all_layers().filter(|&layer| document.metadata().is_text_layer(layer)).find(|&layer| {
let transformed_quad = document.metadata().transform_to_viewport(layer) * text_bounding_box(layer, document, font_cache);
let mouse = DVec2::new(input.mouse.position.x, input.mouse.position.y);
transformed_quad.contains(mouse)
})
fn check_click(document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, fonts: &FontsMessageHandler, responses: &mut VecDeque<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 {
let transformed_quad = document.metadata().transform_to_viewport(layer) * text_bounding_box(layer, document, fonts, responses);
if transformed_quad.contains(mouse) {
return Some(layer);
}
}
None
}
fn get_snap_candidates(&mut self, document: &DocumentMessageHandler, font_cache: &FontCache) {
fn get_snap_candidates(&mut self, document: &DocumentMessageHandler, fonts: &FontsMessageHandler, responses: &mut VecDeque<Message>) {
self.snap_candidates.clear();
if let Some(ResizingLayer { id, .. }) = self.layer_dragging {
let quad = document.metadata().transform_to_document(id) * text_bounding_box(id, document, font_cache);
let quad = document.metadata().transform_to_document(id) * text_bounding_box(id, document, fonts, responses);
snapping::get_bbox_points(quad, &mut self.snap_candidates, snapping::BBoxSnapValues::BOUNDING_BOX, document);
}
}
@@ -651,14 +648,7 @@ impl Fsm for TextToolFsmState {
tool_options: &Self::ToolOptions,
responses: &mut VecDeque<Message>,
) -> Self {
let ToolActionMessageContext {
document,
input,
cached_data,
viewport,
..
} = transition_data;
let font_cache = &cached_data.font_cache;
let ToolActionMessageContext { document, input, fonts, viewport, .. } = transition_data;
let fill_color = COLOR_OVERLAY_BLUE_05;
let ToolMessage::Text(event) = event else { return self };
@@ -667,7 +657,8 @@ impl Fsm for TextToolFsmState {
let transform = document.metadata().transform_to_viewport(tool_data.layer).to_cols_array();
responses.add(FrontendMessage::DisplayEditableTextboxTransform { transform });
if let Some(editing_text) = tool_data.editing_text.as_mut() {
let far = graphene_std::text::bounding_box(&tool_data.new_text, &editing_text.font, font_cache, editing_text.typesetting, false);
let font_resource = fonts.get_resource_or_queue_load(&editing_text.font, responses);
let far = graphene_std::text::bounding_box(&tool_data.new_text, &font_resource, editing_text.typesetting, false);
if far.x != 0. && far.y != 0. {
let quad = Quad::from_box([DVec2::ZERO, far]);
let transformed_quad = document.metadata().transform_to_viewport(tool_data.layer) * quad;
@@ -694,7 +685,7 @@ impl Fsm for TextToolFsmState {
let selected = document.network_interface.selected_nodes();
let mut all_layers = selected.selected_visible_and_unlocked_layers(&document.network_interface);
let layer = all_layers.find(|&layer| document.metadata().is_text_layer(layer));
let bounds = layer.map(|layer| text_bounding_box(layer, document, font_cache));
let bounds = layer.map(|layer| text_bounding_box(layer, document, fonts, responses));
let layer_transform = layer.map(|layer| document.metadata().transform_to_viewport(layer)).unwrap_or(DAffine2::IDENTITY);
if layer.is_none() || bounds.is_none() || layer_transform.matrix2.determinant() == 0. {
@@ -710,10 +701,11 @@ impl Fsm for TextToolFsmState {
bounding_box_manager.render_quad(&mut overlay_context);
// Draw red overlay if text is clipped
let transformed_quad = layer_transform * bounds;
if let Some((text, font, typesetting, _)) = graph_modification_utils::get_text(layer.unwrap(), &document.network_interface)
&& lines_clipping(text.as_str(), font, font_cache, typesetting)
{
overlay_context.line(transformed_quad.0[2], transformed_quad.0[3], Some(COLOR_OVERLAY_RED), Some(3.));
if let Some((text, font, typesetting, _)) = graph_modification_utils::get_text(layer.unwrap(), &document.network_interface, fonts, &document.resources) {
let font_resource = fonts.get_resource_or_queue_load(&font, responses);
if lines_clipping(text.as_str(), &font_resource, typesetting) {
overlay_context.line(transformed_quad.0[2], transformed_quad.0[3], Some(COLOR_OVERLAY_RED), Some(3.));
}
}
bounding_box_manager.render_overlays(&mut overlay_context, false);
@@ -728,7 +720,7 @@ impl Fsm for TextToolFsmState {
}
(state, TextToolMessage::EditSelected) => {
if let Some(layer) = can_edit_selected(document) {
tool_data.start_editing_layer(layer, state, document, font_cache, responses);
tool_data.start_editing_layer(layer, state, document, fonts, responses);
return TextToolFsmState::Editing;
}
@@ -769,10 +761,10 @@ impl Fsm for TextToolFsmState {
bounds.original_bound_transform = bounds.transform;
bounds.center_of_transformation = bounds.transform.transform_point2((bounds.bounds[0] + bounds.bounds[1]) / 2.);
}
tool_data.get_snap_candidates(document, font_cache);
tool_data.get_snap_candidates(document, fonts, responses);
return TextToolFsmState::ResizingBounds;
} else if let Some(clicked_layer) = TextToolData::check_click(document, input, font_cache) {
} else if let Some(clicked_layer) = TextToolData::check_click(document, input, fonts, responses) {
responses.add(DocumentMessage::StartTransaction);
if selected != Some(clicked_layer) {
@@ -784,7 +776,7 @@ impl Fsm for TextToolFsmState {
id: clicked_layer,
original_transform,
});
tool_data.get_snap_candidates(document, font_cache);
tool_data.get_snap_candidates(document, fonts, responses);
return TextToolFsmState::Dragging;
}
TextToolFsmState::Placing
@@ -967,8 +959,8 @@ impl Fsm for TextToolFsmState {
let has_dragged = (start - end).length_squared() > DRAG_THRESHOLD * DRAG_THRESHOLD;
// Check if the user has clicked (no dragging) on some existing text
if !has_dragged && let Some(clicked_text_layer_path) = TextToolData::check_click(document, input, font_cache) {
tool_data.start_editing_layer(clicked_text_layer_path, self, document, font_cache, responses);
if !has_dragged && let Some(clicked_text_layer_path) = TextToolData::check_click(document, input, fonts, responses) {
tool_data.start_editing_layer(clicked_text_layer_path, self, document, fonts, responses);
return TextToolFsmState::Editing;
}
@@ -989,7 +981,7 @@ impl Fsm for TextToolFsmState {
font: Font::new(tool_options.font.font_family.clone(), tool_options.font.font_style.clone()),
color: tool_options.fill.active_color(),
};
tool_data.new_text(document, editing_text, font_cache, responses);
tool_data.new_text(document, editing_text, fonts, responses);
TextToolFsmState::Editing
}
(TextToolFsmState::Dragging, TextToolMessage::DragStop) => {
@@ -1004,7 +996,7 @@ impl Fsm for TextToolFsmState {
}
if drag_too_small && let Some(layer_info) = &tool_data.layer_dragging {
tool_data.start_editing_layer(layer_info.id, self, document, font_cache, responses);
tool_data.start_editing_layer(layer_info.id, self, document, fonts, responses);
return TextToolFsmState::Editing;
}
tool_data.layer_dragging.take();
@@ -1013,8 +1005,9 @@ impl Fsm for TextToolFsmState {
}
(TextToolFsmState::Editing, TextToolMessage::RefreshEditingFontData) => {
let font = Font::new(tool_options.font.font_family.clone(), tool_options.font.font_style.clone());
let font_resource = fonts.get_resource_or_queue_load(&font, responses);
responses.add(FrontendMessage::DisplayEditableTextboxUpdateFontData {
font_data: font_cache.get(&font).map(|(data, _)| data.clone()).unwrap_or_default().into(),
font_data: font_resource.as_ref().to_vec().into(),
});
TextToolFsmState::Editing
@@ -1023,7 +1016,7 @@ impl Fsm for TextToolFsmState {
tool_data.new_text = new_text;
if !is_left_or_right_click {
tool_data.set_editing(false, font_cache, responses);
tool_data.set_editing(false, fonts, responses);
responses.add(NodeGraphMessage::SetInput {
input_connector: InputConnector::node(graph_modification_utils::get_text_id(tool_data.layer, &document.network_interface).unwrap(), 1),
@@ -1034,7 +1027,7 @@ impl Fsm for TextToolFsmState {
TextToolFsmState::Ready
} else {
if tool_data.new_text.is_empty() {
return tool_data.delete_empty_layer(font_cache, responses);
return tool_data.delete_empty_layer(fonts, responses);
}
responses.add(FrontendMessage::TriggerTextCommit);
@@ -1055,7 +1048,7 @@ impl Fsm for TextToolFsmState {
}
(TextToolFsmState::Editing, TextToolMessage::Abort) => {
if tool_data.new_text.is_empty() {
return tool_data.delete_empty_layer(font_cache, responses);
return tool_data.delete_empty_layer(fonts, responses);
}
responses.add(FrontendMessage::TriggerTextCommit);

View File

@@ -9,7 +9,6 @@ use crate::messages::input_mapper::utility_types::macros::action_shortcut;
use crate::messages::input_mapper::utility_types::misc::ActionShortcut;
use crate::messages::layout::utility_types::widget_prelude::*;
use crate::messages::portfolio::document::overlays::utility_types::OverlayProvider;
use crate::messages::portfolio::utility_types::CachedData;
use crate::messages::preferences::PreferencesMessageHandler;
use crate::messages::prelude::*;
use crate::messages::tool::common_functionality::shapes::shape_utility::ShapeType;
@@ -25,7 +24,7 @@ pub struct ToolActionMessageContext<'a> {
pub document_id: DocumentId,
pub global_tool_data: &'a DocumentToolData,
pub input: &'a InputPreprocessorMessageHandler,
pub cached_data: &'a CachedData,
pub fonts: &'a FontsMessageHandler,
pub shape_editor: &'a mut ShapeState,
pub node_graph: &'a NodeGraphExecutor,
pub preferences: &'a PreferencesMessageHandler,
@@ -38,11 +37,11 @@ impl<T> ToolCommon for T where T: for<'a, 'b> MessageHandler<ToolMessage, &'b mu
type Tool = dyn ToolCommon + Send + Sync;
pub trait ToolRefreshOptions {
fn refresh_options(&self, responses: &mut VecDeque<Message>, _cached_data: &CachedData);
fn refresh_options(&self, responses: &mut VecDeque<Message>);
}
impl<T: LayoutHolder> ToolRefreshOptions for T {
fn refresh_options(&self, responses: &mut VecDeque<Message>, _cached_data: &CachedData) {
fn refresh_options(&self, responses: &mut VecDeque<Message>) {
self.send_layout(responses, LayoutTarget::ToolOptions);
}
}

View File

@@ -9,7 +9,6 @@ use graphene_std::application_io::{NodeGraphUpdateMessage, RenderConfig, TimingI
use graphene_std::color::SRGBA8;
use graphene_std::raster::{CPU, Raster};
use graphene_std::renderer::RenderMetadata;
use graphene_std::text::FontCache;
use graphene_std::transform::Footprint;
use graphene_std::vector::Vector;
use interpreted_executor::dynamic_executor::ResolvedDocumentNodeTypesDelta;
@@ -95,10 +94,6 @@ impl NodeGraphExecutor {
execution_id
}
pub fn update_font_cache(&self, font_cache: FontCache) {
self.runtime_io.send(GraphRuntimeRequest::FontCacheUpdate(font_cache)).expect("Failed to send font cache update");
}
pub fn update_editor_preferences(&self, editor_preferences: EditorPreferences) {
self.runtime_io
.send(GraphRuntimeRequest::EditorPreferencesUpdate(editor_preferences))

View File

@@ -17,7 +17,6 @@ use graphene_std::ops::Convert;
use graphene_std::platform_application_io::canvas_utils::{Canvas, CanvasSurface, CanvasSurfaceHandle};
use graphene_std::raster_types::Raster;
use graphene_std::renderer::{Render, RenderParams, RenderSvgSegmentList, SvgRender, SvgSegment};
use graphene_std::text::FontCache;
use graphene_std::transform::RenderQuality;
use graphene_std::vector::Vector;
use graphene_std::vector::style::RenderMode;
@@ -71,7 +70,6 @@ pub struct NodeRuntime {
pub enum GraphRuntimeRequest {
GraphUpdate(GraphUpdate),
ExecutionRequest(ExecutionRequest),
FontCacheUpdate(FontCache),
EditorPreferencesUpdate(EditorPreferences),
}
@@ -134,7 +132,6 @@ impl NodeRuntime {
update_thumbnails: true,
editor_api: PlatformEditorApi {
font_cache: FontCache::default(),
editor_preferences: Box::new(EditorPreferences::default()),
node_graph_message_sender: Box::new(InternalNodeGraphUpdateSender(sender)),
@@ -162,7 +159,6 @@ impl NodeRuntime {
}
pub async fn run(&mut self) -> Option<ImageTexture> {
let mut font = None;
let mut preferences = None;
let mut graph = None;
let mut eyedropper = None;
@@ -186,7 +182,6 @@ impl NodeRuntime {
break;
}
}
GraphRuntimeRequest::FontCacheUpdate(_) => font = Some(request),
GraphRuntimeRequest::EditorPreferencesUpdate(_) => preferences = Some(request),
}
}
@@ -199,27 +194,13 @@ impl NodeRuntime {
eyedropper.render_config.pointer = execution.render_config.pointer;
}
let requests = [font, preferences, graph, eyedropper, execution].into_iter().flatten();
let requests = [preferences, graph, eyedropper, execution].into_iter().flatten();
for request in requests {
match request {
GraphRuntimeRequest::FontCacheUpdate(font_cache) => {
self.editor_api = PlatformEditorApi {
font_cache,
application_io: self.editor_api.application_io.clone(),
node_graph_message_sender: Box::new(self.sender.clone()),
editor_preferences: Box::new(self.editor_preferences.clone()),
}
.into();
if let Some(graph) = self.old_graph.clone() {
// We ignore this result as compilation errors should have been reported in an earlier iteration
let _ = self.update_network(graph).await;
}
}
GraphRuntimeRequest::EditorPreferencesUpdate(preferences) => {
self.editor_preferences = preferences.clone();
self.editor_api = PlatformEditorApi {
font_cache: self.editor_api.font_cache.clone(),
application_io: self.editor_api.application_io.clone(),
node_graph_message_sender: Box::new(self.sender.clone()),
editor_preferences: Box::new(preferences),
@@ -587,7 +568,6 @@ pub(crate) fn replace_application_io(application_io: PlatformApplicationIo) {
impl NodeRuntime {
pub(crate) fn replace_application_io(&mut self, application_io: PlatformApplicationIo) {
self.editor_api = PlatformEditorApi {
font_cache: self.editor_api.font_cache.clone(),
application_io: Some(application_io.into()),
node_graph_message_sender: Box::new(self.sender.clone()),
editor_preferences: Box::new(self.editor_preferences.clone()),