Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions desktop/wrapper/src/intercept_frontend_message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@ use super::messages::{DesktopFrontendMessage, FileFilter, OpenFileDialogContext,
pub(super) fn intercept_frontend_message(dispatcher: &mut DesktopWrapperMessageDispatcher, message: FrontendMessage) -> Option<FrontendMessage> {
match message {
FrontendMessage::Await { future } => {
let message = futures::executor::block_on(async move { future.await });
return intercept_frontend_message(dispatcher, message);
dispatcher.queue_editor_message(futures::executor::block_on(async move { future.await }));
}
FrontendMessage::RenderOverlays { context } => {
dispatcher.respond(DesktopFrontendMessage::UpdateOverlays(context.take_scene()));
Expand Down
8 changes: 6 additions & 2 deletions editor/src/messages/frontend/frontend_message.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use super::IconName;
use super::utility_types::{MouseCursorIcon, PersistedState};
use crate::messages::app_window::app_window_message_handler::AppWindowPlatform;
use crate::messages::frontend::utility_types::{DocumentInfo, EyedropperPreviewImage, FrontendMessageFuture};
use crate::messages::frontend::utility_types::{DocumentInfo, EyedropperPreviewImage, MessageFuture};
use crate::messages::input_mapper::utility_types::misc::ActionShortcut;
use crate::messages::layout::utility_types::widget_prelude::*;
use crate::messages::portfolio::document::node_graph::utility_types::{
Expand Down Expand Up @@ -31,7 +31,7 @@ pub enum FrontendMessage {
#[serde(skip, default)]
#[derivative(Debug = "ignore", PartialEq = "ignore")]
#[cfg_attr(feature = "wasm", tsify(type = "unknown"))]
future: FrontendMessageFuture,
future: MessageFuture,
},

// Display prefix: make the frontend show something, like a dialog
Expand Down Expand Up @@ -123,6 +123,10 @@ pub enum FrontendMessage {
font: Font,
url: String,
},
TriggerFontRegister {
name: String,
data: serde_bytes::ByteBuf,
},
TriggerPersistenceReadState,
TriggerPersistenceReadDocument {
#[serde(rename = "documentId")]
Expand Down
2 changes: 1 addition & 1 deletion editor/src/messages/frontend/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ pub mod utility_types;

#[doc(inline)]
pub use frontend_message::{FrontendMessage, FrontendMessageDiscriminant};
pub use utility_types::FrontendMessageFuture;
pub use utility_types::MessageFuture;

// TODO: Make this an enum with the actual icon names, somehow derived from or tied to the frontend icon set.
// TODO: Then remove `#[widget_builder(string)]` from all icon fields.
Expand Down
16 changes: 8 additions & 8 deletions editor/src/messages/frontend/utility_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,23 +87,23 @@ pub struct EyedropperPreviewImage {
}

#[derive(Clone, Default)]
pub struct FrontendMessageFuture {
inner: Arc<Mutex<Option<InnerFrontendMessageFuture>>>,
pub struct MessageFuture {
inner: Arc<Mutex<Option<InnerMessageFuture>>>,
}

impl FrontendMessageFuture {
pub fn new(future: impl Future<Output = FrontendMessage> + Send + 'static) -> Self {
impl MessageFuture {
pub fn new(future: impl Future<Output = Message> + Send + 'static) -> Self {
Self {
inner: Arc::new(Mutex::new(Some(Box::pin(future)))),
}
}
}

type InnerFrontendMessageFuture = Pin<Box<dyn Future<Output = FrontendMessage> + Send + 'static>>;
type InnerMessageFuture = Pin<Box<dyn Future<Output = Message> + Send + 'static>>;

impl IntoFuture for FrontendMessageFuture {
type Output = FrontendMessage;
type IntoFuture = InnerFrontendMessageFuture;
impl IntoFuture for MessageFuture {
type Output = Message;
type IntoFuture = InnerMessageFuture;

fn into_future(self) -> Self::IntoFuture {
self.inner
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ impl std::hash::Hash for MenuListEntry {
self.label.hash(state);
self.icon.hash(state);
self.disabled.hash(state);
self.font.hash(state);
}
}

Expand Down
15 changes: 3 additions & 12 deletions editor/src/messages/portfolio/document/document_message_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -931,7 +931,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
let name = format!("{}.{}", self.name.clone(), FILE_EXTENSION);

responses.add(FrontendMessage::Await {
future: FrontendMessageFuture::new(async move {
future: MessageFuture::new(async move {
let loads = resource_hashes
.into_iter()
.map(|hash| {
Expand All @@ -950,6 +950,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
folder,
content: content.into_bytes().into(),
}
.into()
}),
});
}
Expand Down Expand Up @@ -2663,17 +2664,7 @@ 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 {
for font in super::utility_types::text_resource_resolution::fonts_missing_text_resources(self.document_network()) {
responses.add(PortfolioMessage::LoadFontData { font });
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -251,9 +251,10 @@ impl<'a> ModifyInputsContext<'a> {
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")),
None,
Some(NodeInput::value(TaggedValue::String(text), false)),
Some(NodeInput::value(TaggedValue::Font(font), false)),
Some(NodeInput::value(TaggedValue::None, 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)),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -850,7 +850,12 @@ pub fn font_inputs(parameter_widgets_info: ParameterWidgetsInfo) -> (Vec<WidgetI
.map(|family| {
MenuListEntry::new(family.name.clone())
.label(family.name.clone())
.font(family.closest_style(400, false).preview_url(&family.name))
.font({
let style = family.closest_style(400, false);
let font = Font::new(family.name.clone(), style.to_named_style());
let hash = cached_data.font_cache.get_hash(&font).map(|h| h.to_string()).unwrap_or("unknown".into());
format!("font-resource-{}", hash)
})
.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()));
Expand All @@ -867,11 +872,19 @@ pub fn font_inputs(parameter_widgets_info: ParameterWidgetsInfo) -> (Vec<WidgetI

move |_| {
let new_font = new_font.clone();
let load_font = new_font.clone();
let set_font_value = 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)(&()),
update_value(move |_| TaggedValue::Font(set_font_value.clone()), node_id, index)(&()),
NodeGraphMessage::SetInputValue {
node_id,
input_index: graphene_std::text::text::FontResourceInput::<()>::INDEX,
value: TaggedValue::None,
}
.into(),
PortfolioMessage::LoadFontData { font: load_font }.into(),
]),
}
}
Expand Down Expand Up @@ -925,11 +938,18 @@ pub fn font_inputs(parameter_widgets_info: ParameterWidgetsInfo) -> (Vec<WidgetI
move |_| {
// Keep the existing family
let new_font = Font::new(font_family.clone(), font_style.clone());
let load_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)(&()),
NodeGraphMessage::SetInputValue {
node_id,
input_index: graphene_std::text::text::FontResourceInput::<()>::INDEX,
value: TaggedValue::None,
}
.into(),
PortfolioMessage::LoadFontData { font: load_font }.into(),
]),
}
}
Expand Down
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use crate::messages::tool::common_functionality::shape_editor::{SelectedLayerSta
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::{Blob, FALLBACK_FONT_BYTES, Font, TextAlign, TextContext, TypesettingConfig, load_font};
use graphene_std::vector::misc::ManipulatorPointId;
use graphene_std::vector::{PointId, SegmentId, Vector};
use std::collections::HashMap;
Expand Down Expand Up @@ -221,15 +221,9 @@ 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
});
// Global lazy initialized font data and text context used for overlay text measurement.
// Reuses the embedded fallback font bytes from the text node so we don't ship a second copy of the file.
pub static OVERLAY_FONT_BLOB: LazyLock<Blob<u8>> = LazyLock::new(|| load_font(FALLBACK_FONT_BYTES));

pub static GLOBAL_TEXT_CONTEXT: LazyLock<Mutex<TextContext>> = LazyLock::new(|| Mutex::new(TextContext::default()));

Expand All @@ -249,7 +243,7 @@ pub fn text_width(text: &str, font_size: f64) -> f64 {
// 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, &font, &OVERLAY_FONT_BLOB, typesetting, false);
bounds.x
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ 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, OVERLAY_FONT_BLOB, hex_to_rgba_u8};
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::prelude::Message;
use crate::messages::prelude::ViewportMessageHandler;
Expand Down Expand Up @@ -1122,14 +1122,14 @@ impl OverlayContextInternal {

// 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, &font, &OVERLAY_FONT_BLOB, typesetting, false);
let text_width = text_size.x;
let text_height = text_size.y;
// Create a rect from the size (assuming text starts at origin)
let text_bounds = kurbo::Rect::new(0., 0., text_width, text_height);

// Convert text to vector paths for rendering
let text_list = text_context.to_path(text, &font, &GLOBAL_FONT_CACHE, typesetting, false);
let text_list = text_context.to_path(text, &font, &OVERLAY_FONT_BLOB, typesetting, false);

// Calculate position based on pivot
let mut position = DVec2::ZERO;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ pub mod error;
pub mod misc;
pub mod network_interface;
pub mod nodes;
pub mod text_resource_resolution;
pub mod transformation;
pub mod wires;
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeNetworkInterface};
use crate::messages::portfolio::utility_types::FontCache;
use graph_craft::application_io::ResourceHash;
use graph_craft::document::value::TaggedValue;
use graph_craft::document::{DocumentNodeImplementation, NodeId, NodeInput, NodeNetwork};
use graphene_std::NodeInputDecleration;
use graphene_std::text::Font;

const TEXT_NODE_FONT_RESOURCE_INPUT_INDEX: usize = 3;

pub fn patch_text_nodes_with_loaded_font(network_interface: &mut NodeNetworkInterface, target_font: &Font, hash: ResourceHash) -> bool {
let updates = collect_text_node_updates(network_interface.document_network(), &[], &mut |font, current| {
if font != target_font {
return false;
}
matches!(current, TaggedValue::None)
});

let patched_any = !updates.is_empty();
for (network_path, node_id, _font) in updates {
network_interface.set_input(
&InputConnector::node(node_id, TEXT_NODE_FONT_RESOURCE_INPUT_INDEX),
NodeInput::value(TaggedValue::Resource(hash), false),
&network_path,
);
}
patched_any
}

pub fn fonts_missing_text_resources(network: &NodeNetwork) -> Vec<Font> {
let updates = collect_text_node_updates(network, &[], &mut |_font, current| matches!(current, TaggedValue::None));

let mut fonts = Vec::new();
for (_network_path, _node_id, font) in updates {
if !fonts.contains(&font) {
fonts.push(font);
}
}
fonts
}

pub fn refresh_text_node_font_resources(network_interface: &mut NodeNetworkInterface, font_cache: &FontCache) -> Vec<Font> {
let updates = collect_text_node_updates(network_interface.document_network(), &[], &mut |_font, _current| true);

let mut needs_loading = Vec::new();
for (network_path, node_id, font) in updates {
let current_input = network_interface
.input_from_connector(&InputConnector::node(node_id, TEXT_NODE_FONT_RESOURCE_INPUT_INDEX), &network_path)
.cloned();

let current_value = current_input.as_ref().and_then(|input| input.as_value());

if font_cache.contains(&font) {
if matches!(current_value, Some(TaggedValue::None)) && !needs_loading.contains(&font) {
needs_loading.push(font);
}
} else {
if !matches!(current_value, Some(TaggedValue::None)) {
network_interface.set_input(
&InputConnector::node(node_id, TEXT_NODE_FONT_RESOURCE_INPUT_INDEX),
NodeInput::value(TaggedValue::None, false),
&network_path,
);
}
if !needs_loading.contains(&font) {
needs_loading.push(font);
}
}
}

needs_loading
}

fn collect_text_node_updates(network: &NodeNetwork, parent_path: &[NodeId], predicate: &mut dyn FnMut(&Font, &TaggedValue) -> bool) -> Vec<(Vec<NodeId>, NodeId, Font)> {
let mut out = Vec::new();
for (node_id, node) in network.nodes.iter() {
if let DocumentNodeImplementation::Network(nested) = &node.implementation {
let mut path = parent_path.to_vec();
path.push(*node_id);
out.extend(collect_text_node_updates(nested, &path, predicate));
continue;
}

let is_text_node = matches!(&node.implementation, DocumentNodeImplementation::ProtoNode(identifier) if *identifier == graphene_std::text::text::IDENTIFIER);
if !is_text_node {
continue;
}

let Some(NodeInput::Value { tagged_value, .. }) = node.inputs.get(graphene_std::text::text::FontInput::INDEX) else {
continue;
};
let TaggedValue::Font(font) = (**tagged_value).clone() else { continue };

let Some(NodeInput::Value { tagged_value: resource_value, .. }) = node.inputs.get(TEXT_NODE_FONT_RESOURCE_INPUT_INDEX) else {
continue;
};

if predicate(&font, resource_value) {
out.push((parent_path.to_vec(), *node_id, font));
}
}
out
}
16 changes: 16 additions & 0 deletions editor/src/messages/portfolio/document_migration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1501,6 +1501,22 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId],
for i in 10..=12 {
document.network_interface.set_input(&InputConnector::node(*node_id, i), old_inputs[i - 2].clone(), network_path);
}

inputs_count = 13;
}

// Convert text nodes to use resource for loading font data.
if reference == DefinitionIdentifier::ProtoNode(graphene_std::text::text::IDENTIFIER) && inputs_count == 13 && matches!(node.inputs.first(), Some(NodeInput::Scope(_))) {
let mut template: NodeTemplate = resolve_document_node_type(&reference)?.default_node_template();
document.network_interface.replace_implementation(node_id, network_path, &mut template);
let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut template)?;

document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[1].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 2), old_inputs[2].clone(), network_path);
#[allow(clippy::needless_range_loop)]
for i in 3..=12 {
document.network_interface.set_input(&InputConnector::node(*node_id, i + 1), old_inputs[i].clone(), network_path);
}
Comment on lines +1517 to +1519
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The migration loop bounds are incorrect. The new text node definition has 12 inputs (indices 0-11). The current loop 3..=12 attempts to set inputs up to index 13, which exceeds the available slots and will cause a panic. While removing old data migrations for features that are being retired is acceptable, the loop must still stay within the bounds of the new node's inputs.

Suggested change
for i in 3..=12 {
document.network_interface.set_input(&InputConnector::node(*node_id, i + 1), old_inputs[i].clone(), network_path);
}
for i in 3..=10 {
document.network_interface.set_input(&InputConnector::node(*node_id, i + 1), old_inputs[i].clone(), network_path);
}
References
  1. It is acceptable to remove old data migrations for features that are being retired, even if it breaks backward compatibility.

}

// Upgrade Sine, Cosine, and Tangent nodes to include a boolean input for whether the output should be in radians, which was previously the only option but is now not the default
Expand Down
Loading
Loading